summaryrefslogtreecommitdiffstats
path: root/lib/silfont/scripts/psfmakewoffmetadata.py
blob: c7d8128a09513d7172f6dcaf8bb2e41001215a6a (plain)
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 python
__doc__ = 'Make the WOFF metadata xml file based on input UFO (and optionally FONTLOG.txt)'
__url__ = 'http://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2017 SIL International (http://www.sil.org)'
__license__ = 'Released under the MIT License (http://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="http://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'&', '&amp;', txt)
    txt = re.sub(r'<', '&lt;', txt)
    txt = re.sub(r'>', '&gt;', txt)
    return txt

def attrprotect(txt):  # Switch special characters in text to use &...; format
    txt = re.sub(r'&', '&amp;', txt)
    txt = re.sub(r'<', '&lt;', txt)
    txt = re.sub(r'>', '&gt;', txt)
    txt = re.sub(r'"', '&quot;', txt)
    return txt

def cmd(): execute("UFO", doit, argspec)
if __name__ == "__main__": cmd()