From da875fcb62c801b8d19b3d4d984ad963574fb356 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Thu, 2 Mar 2023 21:01:10 +0100 Subject: Adding upstream version 1.6.0. Signed-off-by: Daniel Baumann --- lib/silfont/ftml.py | 433 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 433 insertions(+) create mode 100644 lib/silfont/ftml.py (limited to 'lib/silfont/ftml.py') diff --git a/lib/silfont/ftml.py b/lib/silfont/ftml.py new file mode 100644 index 0000000..9f63cab --- /dev/null +++ b/lib/silfont/ftml.py @@ -0,0 +1,433 @@ +#!/usr/bin/env python +'Classes and functions for use handling FTML objects in pysilfont scripts' +__url__ = 'http://github.com/silnrsi/pysilfont' +__copyright__ = 'Copyright (c) 2016 SIL International (http://www.sil.org)' +__license__ = 'Released under the MIT License (http://opensource.org/licenses/MIT)' +__author__ = 'David Raymond' + +from xml.etree import ElementTree as ET +from fontTools import ttLib +import re +from xml.sax.saxutils import quoteattr +import silfont.core +import silfont.etutil as ETU + +# Regular expression for parsing font name +fontspec = re.compile(r"""^ # beginning of string + (?P[A-Za-z ]+?) # Font Family Name + \s*(?PBold)? # Bold + \s*(?PItalic)? # Italic + \s*(?PRegular)? # Regular + $""", re.VERBOSE) # end of string + +class Fxml(ETU.ETelement) : + def __init__(self, file = None, xmlstring = None, testgrouplabel = None, logger = None, params = None) : + self.logger = logger if logger is not None else silfont.core.loggerobj() + self.params = params if params is not None else silfont.core.parameters() + self.parseerrors=None + if not exactlyoneof(file, xmlstring, testgrouplabel) : self.logger.log("Must supply exactly one of file, xmlstring and testgrouplabel","X") + + if testgrouplabel : # Create minimal valid ftml + xmlstring = '' + + if file and not hasattr(file, 'read') : self.logger.log("'file' is not a file object", "X") # ET.parse would also work on file name, but other code assumes file object + + try : + if file : + self.element = ET.parse(file).getroot() + else : + self.element = ET.fromstring(xmlstring) + except Exception as e : + self.logger.log("Error parsing FTML input: " + str(e), "S") + + super(Fxml,self).__init__(self.element) + + self.version = getattrib(self.element,"version") + if self.version != "1.0" : self.logger.log("ftml items must have a version of 1.0", "S") + + self.process_subelements(( + ("head", "head" , Fhead, True, False), + ("testgroup", "testgroups", Ftestgroup, True, True )), + offspec = False) + + self.stylesheet = {} + if file : # If reading from file, look to see if a stylesheet is present in xml processing instructions + file.seek(0) # Have to re-read file since ElementTree does not support processing instructions + for line in file : + if line[0:2] == " + parts = line.split(" ") + if parts[0] == "['"])(\w{4})(?P=quote)\s+(\d+|on|off)$''', f) + if m: + dict[m.group(2)] = m.group(3) + else: + self.logger.log(f'Invalid feature syntax "{f}"', 'E') + return dict + + def dict_to_string(self, dict) : + str="" + for name in sorted(dict) : + if dict[name] is not None : str += "'" + name + "' " + dict[name] + ", " + str = str[0:-2] # remove final ", " + return str + + def create_element(self) : + element = ET.Element("style", name = self.name) + if self.feats : element.set("feats", self.dict_to_string(self.feats)) + if self.lang : element.set("lang", self.lang) + return element + + +class _Fwidth(ETU.ETelement) : # Only used temporarily whilst parsing xml + def __init__(self, parent, element) : + super(_Fwidth,self).__init__(element) + self.parent = parent + self.logger = parent.logger + + self.process_attributes(( + ("comment", "comment", False), + ("label", "label", False), + ("string", "string", False), + ("stylename", "stylename", False), + ("table", "table", False)), + others = False) + self.widthsdict = { + "comment": self.comment, + "label": self.label, + "string": self.string, + "stylename": self.stylename, + "table": self.table} + +class Ftestgroup(ETU.ETelement) : + def __init__(self, parent, element = None, label = None) : + self.parent = parent + self.logger = parent.logger + if not exactlyoneof(element, label) : self.logger.log("Must supply exactly one of element and label","X") + + if label : element = ET.Element("testgroup", label = label) + + super(Ftestgroup,self).__init__(element) + + self.subgroup = True if type(parent) is Ftestgroup else False + self.process_attributes(( + ("background", "background", False), + ("label", "label", True)), + others = False) + self.process_subelements(( + ("comment", "comment", None, False, False), + ("test", "tests", Ftest, False, True), + ("testgroup", "testgroups", Ftestgroup, False, True)), + offspec = False) + if self.subgroup and self.testgroups != [] : parent.parseerrors.append("Only one level of testgroup nesting permitted") + + # Merge any sub-testgroups into tests + if self.testgroups != [] : + tests = [] + tg = list(self.testgroups) # Want to preserve original list + for elem in self.element : + if elem.tag == "test": + tests.append(self.tests.pop(0)) + elif elem.tag == "testgroup" : + tests.append(tg.pop(0)) + self.tests = tests + + def create_element(self) : + element = ET.Element("testgroup") + if self.background : element.set("background", self.background) + element.set("label", self.label) + if self.comment : x = ET.SubElement(element, 'comment') ; x.text = self.comment + for test in self.tests : element.append(test.create_element()) + return element + +class Ftest(ETU.ETelement) : + def __init__(self, parent, element = None, label = None, string = None) : + self.parent = parent + self.logger = parent.logger + if not exactlyoneof(element, (label, string)) : self.logger.log("Must supply exactly one of element and label/string","X") + + if label : + element = ET.Element("test", label = label) + x = ET.SubElement(element,"string") ; x.text = string + + super(Ftest,self).__init__(element) + + self.process_attributes(( + ("background", "background", False), + ("label", "label", True), + ("rtl", "rtl", False), + ("stylename", "stylename", False)), + others = False) + + self.process_subelements(( + ("comment", "comment", None, False, False), + ("string", "string", _Fstring, True, False)), + offspec = False) + + self.string = self.string.string # self.string initially a temporary _Fstring element + + def str(self, noems = False) : # Return formatted version of string + string = self.string + if noems : + string = string.replace("","") + string = string.replace("","") + return string ## Other formatting options to be added as needed cf ftml2odt + + def create_element(self) : + element = ET.Element("test") + if self.background : element.set("background", self.background) + element.set("label", self.label) + if self.rtl : element.set("rtl", self.rtl) + if self.stylename : element.set("stylename", self.stylename) + if self.comment : x = ET.SubElement(element, "comment") ; x.text = self.comment + x = ET.SubElement(element, "string") ; x.text = self.string + + return element + +class _Fstring(ETU.ETelement) : # Only used temporarily whilst parsing xml + def __init__(self, parent, element = None) : + self.parent = parent + self.logger = parent.logger + super(_Fstring,self).__init__(element) + self.process_subelements((("em", "em", ETU.ETelement,False, True),), offspec = False) + # Need to build text of string to include subelements + self.string = element.text if element.text else "" + for em in self.em : + self.string += "{}{}".format(em.element.text, em.element.tail) + +def getattrib(element,attrib) : + return element.attrib[attrib] if attrib in element.attrib else None + +def exactlyoneof( *args ) : # Check one and only one of args is not None + + last = args[-1] # Check if last argument is a tuple - in which case + if type(last) is tuple : # either all or none of list must be None + for test in last[1:] : + if (test is None) != (last[0] == None) : return False + args = list(args) # Convert to list so last val can be changed + args[-1] = last[0] # Now valid to test on any item in tuple + + one = False + for test in args : + if test is not None : + if one : return False # already have found one not None + one = True + if one : return True + return False + +def parsefontsrc(text, allowplain = False) : # Check fontsrc text is valid and return normalised text, url and local values + ''' - if multiple (fallback) fonts are specified, just process the first one + - just handles simple url() or local() formats + - if allowplain is set, allows text without url() or local() and decides which based on "." in text ''' + text = text.split(",")[0] # If multiple (fallback) fonts are specified, just process the first one + #if allowplain and not re.match(r"^(url|local)[(][^)]+[)]",text) : # Allow for text without url() or local() form + if allowplain and not "(" in text : # Allow for text without url() or local() form + plain = True + if "." in text : + type = "url" + else : + type = "local" + else : + type = text.split("(")[0] + if type == "url" : + text = text.split("(")[1][:-1].strip() + elif type == "local" : + text = text.split("(")[1][:-1].strip() + else : raise ValueError("Invalid fontsrc string") + if type == "url" : + return ("url("+text+")", text, None) + else : + return ("local("+text+")", None , text) + + return (text,url,local) + +def getfontinfo(filename) : # peek inside the font for the name, weight, style + f = ttLib.TTFont(filename) + # take name from name table, NameID 1, platform ID 3, Encoding ID 1 (possible fallback platformID 1, EncodingID =0) + n = f['name'] # name table from font + fontname = n.getName(1,3,1).toUnicode() # nameID 1 = Font Family name + # take bold and italic info from OS/2 table, fsSelection bits 0 and 5 + o = f['OS/2'] # OS/2 table + italic = (o.fsSelection & 1) > 0 + bold = (o.fsSelection & 32) > 0 + return (fontname, bold, italic) + -- cgit v1.2.3