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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
|
#!/usr/bin/env python3
__doc__ = 'Make the WOFF metadata xml file based on input UFO (and optionally FONTLOG.txt)'
__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
import silfont.ufo as UFO
import re, os, datetime
from xml.etree import ElementTree as ET
argspec = [
('font', {'help': 'Source font file'}, {'type': 'infont'}),
('-n', '--primaryname', {'help': 'Primary Font Name', 'required': True}, {}),
('-i', '--orgid', {'help': 'orgId', 'required': True}, {}),
('-f', '--fontlog', {'help': 'FONTLOG.txt file', 'default': 'FONTLOG.txt'}, {'type': 'filename'}),
('-o', '--output', {'help': 'Override output file'}, {'type': 'filename', 'def': None}),
('--populateufowoff', {'help': 'Copy info from FONTLOG.txt to UFO', 'action': 'store_true', 'default': False},{}),
('-l', '--log', {'help': 'Log file'}, {'type': 'outfile', 'def': '_makewoff.log'})]
def doit(args):
font = args.font
pfn = args.primaryname
orgid = args.orgid
logger = args.logger
ofn = args.output
# Find & process info required in the UFO
fi = font.fontinfo
ufofields = {}
missing = None
for field in ("versionMajor", "versionMinor", "openTypeNameManufacturer", "openTypeNameManufacturerURL",
"openTypeNameLicense", "copyright", "trademark"):
if field in fi:
ufofields[field] = fi[field][1].text
elif field != 'trademark': # trademark is no longer required
missing = field if missing is None else missing + ", " + field
if missing is not None: logger.log("Field(s) missing from fontinfo.plist: " + missing, "S")
version = ufofields["versionMajor"] + "." + ufofields["versionMinor"].zfill(3)
# Find & process WOFF fields if present in the UFO
missing = None
ufofields["woffMetadataDescriptionurl"] = None
ufowoff = {"woffMetadataCredits": "credits", "woffMetadataDescription": "text"} # Field, dict name
for field in ufowoff:
fival = fi.getval(field) if field in fi else None
if fival is None:
missing = field if missing is None else missing + ", " + field
ufofields[field] = None
else:
ufofields[field] = fival[ufowoff[field]]
if field == "woffMetadataDescription" and "url" in fival:
ufofields["woffMetadataDescriptionurl"] = fival["url"]
# Process --populateufowoff setting, if present
if args.populateufowoff:
if missing != "woffMetadataCredits, woffMetadataDescription":
logger.log("Data exists in the UFO for woffMetadata - remove manually to reuse --populateufowoff", "S")
if args.populateufowoff or missing is not None:
if missing: logger.log("WOFF field(s) missing from fontinfo.plist will be generated from FONTLOG.txt: " + missing, "W")
# Open the fontlog file
try:
fontlog = open(args.fontlog)
except Exception as e:
logger.log(f"Unable to open {args.fontlog}: {str(e)}", "S")
# Parse the fontlog file
(section, match) = readuntil(fontlog, ("Basic Font Information",)) # Skip until start of "Basic Font Information" section
if match is None: logger.log("No 'Basic Font Information' section in fontlog", "S")
(fldescription, match) = readuntil(fontlog, ("Information for C", "Acknowledgements")) # Description ends when first of these sections is found
fldescription = [{"text": fldescription}]
if match == "Information for C": (section, match) = readuntil(fontlog, ("Acknowledgements",)) # If Info... section present then skip on to Acknowledgements
if match is None: logger.log("No 'Acknowledgements' section in fontlog", "S")
(acksection, match) = readuntil(fontlog, ("No match needed!!",))
flcredits = []
credit = {}
acktype = ""
flog2woff = {"N": "name", "E": "Not used", "W": "url", "D": "role"}
for line in acksection.splitlines():
if line == "":
if acktype != "": # Must be at the end of a credit section
if "name" in credit:
flcredits.append(credit)
else:
logger.log("Credit section found with no N: entry", "E")
credit = {}
acktype = ""
else:
match = re.match("^([NEWD]): (.*)", line)
if match is None:
if acktype == "N": credit["name"] = credit["name"] + line # Name entries can be multiple lines
else:
acktype = match.group(1)
if acktype in credit:
logger.log("Multiple " + acktype + " entries found in a credit section", "E")
else:
credit[flog2woff[acktype]] = match.group(2)
if flcredits == []: logger.log("No credits found in fontlog", "S")
if args.populateufowoff:
ufofields["woffMetadataDescription"] = fldescription # Force fontlog values to be used writing metadata.xml later
ufofields["woffMetadataCredits"] = flcredits
# Create xml strings and update fontinfo
xmlstring = "<dict>" + \
"<key>text</key><array><dict>" + \
"<key>text</key><string>" + textprotect(fldescription[0]["text"]) + "</string>" + \
"</dict></array>" + \
"<key>url</key><string>https://software.sil.org/</string>"\
"</dict>"
fi.setelem("woffMetadataDescription", ET.fromstring(xmlstring))
xmlstring = "<dict><key>credits</key><array>"
for credit in flcredits:
xmlstring += '<dict><key>name</key><string>' + textprotect(credit["name"]) + '</string>'
if "url" in credit: xmlstring += '<key>url</key><string>' + textprotect(credit["url"]) + '</string>'
if "role" in credit: xmlstring += '<key>role</key><string>' + textprotect(credit["role"]) + '</string>'
xmlstring += '</dict>'
xmlstring += '</array></dict>'
fi.setelem("woffMetadataCredits", ET.fromstring(xmlstring))
fi.setval("openTypeHeadCreated", "string", datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S"))
logger.log("Writing updated fontinfo.plist with values from FONTLOG.txt", "P")
exists = True if os.path.isfile(os.path.join(font.ufodir, "fontinfo.plist")) else False
UFO.writeXMLobject(fi, font.outparams, font.ufodir, "fontinfo.plist", exists, fobject=True)
description = ufofields["woffMetadataDescription"]
if description == None: description = fldescription
credits = ufofields["woffMetadataCredits"]
if credits == None : credits = flcredits
# Construct output file name
(folder, ufoname) = os.path.split(font.ufodir)
filename = os.path.join(folder, pfn + "-WOFF-metadata.xml") if ofn is None else ofn
try:
file = open(filename, "w")
except Exception as e:
logger.log("Unable to open " + filename + " for writing:\n" + str(e), "S")
logger.log("Writing to : " + filename, "P")
file.write('<?xml version="1.0" encoding="UTF-8"?>\n')
file.write('<metadata version="1.0">\n')
file.write(' <uniqueid id="' + orgid + '.' + pfn + '.' + version + '" />\n')
file.write(' <vendor name="' + attrprotect(ufofields["openTypeNameManufacturer"]) + '" url="'
+ attrprotect(ufofields["openTypeNameManufacturerURL"]) + '" />\n')
file.write(' <credits>\n')
for credit in credits:
file.write(' <credit\n')
file.write(' name="' + attrprotect(credit["name"]) + '"\n')
if "url" in credit: file.write(' url="' + attrprotect(credit["url"]) + '"\n')
if "role" in credit: file.write(' role="' + attrprotect(credit["role"]) + '"\n')
file.write(' />\n')
file.write(' </credits>\n')
if ufofields["woffMetadataDescriptionurl"]:
file.write(f' <description url="{ufofields["woffMetadataDescriptionurl"]}">\n')
else:
file.write(' <description>\n')
file.write(' <text lang="en">\n')
for entry in description:
for line in entry["text"].splitlines():
file.write(' ' + textprotect(line) + '\n')
file.write(' </text>\n')
file.write(' </description>\n')
file.write(' <license url="https://scripts.sil.org/OFL" id="org.sil.ofl.1.1">\n')
file.write(' <text lang="en">\n')
for line in ufofields["openTypeNameLicense"].splitlines(): file.write(' ' + textprotect(line) + '\n')
file.write(' </text>\n')
file.write(' </license>\n')
file.write(' <copyright>\n')
file.write(' <text lang="en">\n')
for line in ufofields["copyright"].splitlines(): file.write(' ' + textprotect(line) + '\n')
file.write(' </text>\n')
file.write(' </copyright>\n')
if 'trademark' in ufofields:
file.write(' <trademark>\n')
file.write(' <text lang="en">' + textprotect(ufofields["trademark"]) + '</text>\n')
file.write(' </trademark>\n')
file.write('</metadata>')
file.close()
def readuntil(file, texts): # Read through file until line is in texts. Return section up to there and the text matched
skip = True
match = None
for line in file:
line = line.strip()
if skip: # Skip underlines and blank lines at start of section
if line == "" or line[0:5] == "-----":
pass
else:
section = line
skip = False
else:
for text in texts:
if line[0:len(text)] == text: match = text
if match: break
section = section + "\n" + line
while section[-1] == "\n": section = section[:-1] # Strip blank lines at end
return (section, match)
def textprotect(txt): # Switch special characters in text to use &...; format
txt = re.sub(r'&', '&', txt)
txt = re.sub(r'<', '<', txt)
txt = re.sub(r'>', '>', txt)
return txt
def attrprotect(txt): # Switch special characters in text to use &...; format
txt = re.sub(r'&', '&', txt)
txt = re.sub(r'<', '<', txt)
txt = re.sub(r'>', '>', txt)
txt = re.sub(r'"', '"', txt)
return txt
def cmd(): execute("UFO", doit, argspec)
if __name__ == "__main__": cmd()
|