1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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()
|