def __init__(self, elementName, attributes, locator, config, parent): self.elementName = elementName self.config = config if locator is not None: self.locator = locator elif parent is not None: # handy when we add nodes in preprocessing self.locator = parent.locator else: self.locator = NodeLocator(None) self.text = u'' self.children = [] self.attributes = attributes self.parent = parent self.metriclist = None self.nominalMetric = None if parent is not None: self.nodeIndex = len(parent.children) self.defaults = parent.defaults parent.children.append(self) else: self.defaults = globalDefaults.copy() self.defaults.update(config.defaults) self.nodeIndex = 0
def startElementNS(self, elementName, qName, attributes): if self.skip > 0: self.skip += 1; return locator = NodeLocator(self.locator) (namespace, localName) = elementName if namespace and namespace != MathNS: if self.config.verbose: locator.message("Skipped element '%s' from an unknown namespace '%s'" % (localName, namespace), "INFO") self.skip = 1; return properties = {} for (attName, value) in attributes.items(): (attNamespace, attLocalName) = attName if attNamespace and attNamespace != MathNS: if self.config.verbose: locator.message("Ignored attribute '%s' from an unknown namespace '%s'" % (attLocalName, attNamespace), "INFO") continue properties[attLocalName] = value self.currentNode = MathNode (localName, properties, locator, self.config, self.currentNode)
def startElementNS(self, elementName, qName, attributes): if self.skip > 0: self.skip += 1 return locator = NodeLocator(self.locator) (namespace, localName) = elementName if namespace and namespace != MathNS: if self.config.verbose: locator.message( "Skipped element '%s' from an unknown namespace '%s'" % (localName, namespace), "INFO") self.skip = 1 return properties = {} for (attName, value) in attributes.items(): (attNamespace, attLocalName) = attName if attNamespace and attNamespace != MathNS: if self.config.verbose: locator.message( "Ignored attribute '%s' from an unknown namespace '%s'" % (attLocalName, attNamespace), "INFO") continue properties[attLocalName] = value self.currentNode = MathNode(localName, properties, locator, self.config, self.currentNode)
class MathNode: """MathML node descriptor. This class defines properties and methods that permit to building blocks to combine with each other, creating a complex mathematical expression. It uses dynamic binding to find methods to process specific MathML elements: these methods are contained in three other modules - contextmakers, measurers, and generators. """ def __init__(self, elementName, attributes, locator, config, parent): self.elementName = elementName self.config = config if locator is not None: self.locator = locator elif parent is not None: # handy when we add nodes in preprocessing self.locator = parent.locator else: self.locator = NodeLocator(None) self.text = u'' self.children = [] self.attributes = attributes self.parent = parent self.metriclist = None self.nominalMetric = None if parent is not None: self.nodeIndex = len(parent.children) self.defaults = parent.defaults parent.children.append(self) else: self.defaults = globalDefaults.copy() self.defaults.update(config.defaults) self.nodeIndex = 0 def makeContext (self): contextmakers.__dict__.get(u"context_"+self.elementName, contextmakers.default_context)(self) def makeChildContext (self, child): contextmakers.__dict__.get(u"child_context_"+self.elementName, contextmakers.default_child_context)(self, child) def measure(self): # Create the context for the node self.makeContext() # Measure all children for ch in self.children: ch.measure() # Perform node-specific measurement self.measureNode() def measureNode(self): measureMethod = measurers.__dict__.get(u"measure_"+self.elementName, measurers.default_measure) if self.config.verbose and measureMethod is measurers.default_measure: self.warning("MathML element '%s' is unsupported" % self.elementName) measureMethod(self) def draw (self, output): generators.__dict__.get(u"draw_"+self.elementName, generators.default_draw)(self, output) def makeImage(self, output): if self.elementName != 'math': self.warning("Root element in MathML document must be 'math'") self.measure() generators.drawImage(self, output) def warning(self, msg): self.locator.message(msg, "WARNING") def error(self, msg): self.locator.message(msg, "ERROR") def info(self, msg): if self.config.verbose: self.locator.message(msg, "INFO") def debug(self, event, msg): if event.strip() in self.config.debug: self.locator.message(msg, "DEBUG") def parseInt (self, x): try: return int(x, 10) except TypeError: self.error("Cannot parse string '%s' as an integer" % str(x)) return 0 def parseFloat (self, x): try: value = float(x) except ValueError: self.error("Cannot parse string '%s' as a float" % str(x)) return 0.0 text = str(value).lower() if text.find("nan") >= 0 or text.find("inf") >= 0: self.error("Cannot parse string '%s' as a float" % str(x)) return 0.0 return value def parseLength(self, lenattr, unitlessScale = 0.75): lenattr = lenattr.strip() if lenattr.endswith("pt"): return self.parseFloat(lenattr[:-2]) elif lenattr.endswith("mm"): return self.parseFloat(lenattr[:-2]) * 72.0 / 25.4 elif lenattr.endswith("cm"): return self.parseFloat(lenattr[:-2]) * 72.0 / 2.54 elif lenattr.endswith("in"): return self.parseFloat(lenattr[:-2]) * 72.0 elif lenattr.endswith("pc"): return self.parseFloat(lenattr[:-2]) * 12.0 elif lenattr.endswith("px"): # pixels are calculated for 96 dpi return self.parseFloat(lenattr[:-2]) * 72.0 / 96.0 elif lenattr.endswith("em"): return self.parseFloat(lenattr[:-2]) * self.fontSize elif lenattr.endswith("ex"): return self.parseFloat(lenattr[:-2]) * self.fontSize * self.metric().xheight else: # unitless lengths are treated as if expressed in pixels return self.parseFloat(lenattr) * unitlessScale def parseSpace(self, spaceattr, unitlessScale = 0.75): sign = 1.0 spaceattr = spaceattr.strip() if spaceattr.endswith(u"mathspace"): if spaceattr.startswith(u"negative"): sign = -1.0 spaceattr = spaceattr[8:] realspaceattr = self.defaults.get(spaceattr); if realspaceattr is None: self.error("Bad space token: '%s'" % spaceattr) realspaceattr = "0em" return self.parseLength(realspaceattr, unitlessScale) else: return self.parseLength(spaceattr, unitlessScale) def parsePercent(self, lenattr, percentBase): value = self.parseFloat(lenattr[:-1]) if value is not None: return percentBase * value / 100 else: return 0 def parseLengthOrPercent(self, lenattr, percentBase, unitlessScale = 0.75): if lenattr.endswith(u"%"): return self.parsePercent(lenattr, percentBase) else: return self.parseLength(lenattr, unitlessScale) def parseSpaceOrPercent(self, lenattr, percentBase, unitlessScale = 0.75): if lenattr.endswith(u"%"): return self.parsePercent(lenattr, percentBase) else: return self.parseSpace(lenattr, unitlessScale) def getProperty(self, key, defvalue = None): return self.attributes.get(key, self.defaults.get(key, defvalue)) def getListProperty(self, attr, value = None): if value is None: value = self.getProperty(attr) splitvalue = value.split() if len(splitvalue) > 0: return splitvalue self.error("Bad value for '%s' attribute: empty list" % attr) return self.defaults[attr].split() def getUCSText(self): codes = [] hisurr = None for ch in self.text: chcode = ord(ch) # Processing surrogate pairs if isLowSurrogate(ch): if hisurr is None: self.error("Invalid Unicode sequence - low surrogate character (U+%X) not preceded by a high surrogate" % ord(ch)) else: chcode = decodeSurrogatePair(hisurr, ch) hisurr = None if hisurr is not None: self.error("Invalid Unicode sequence - high surrogate character (U+%X) not followed by a low surrogate" % ord(hisurr)) hisurr = None if isHighSurrogate(ch): hisurr = ch; continue codes.append(chcode) if hisurr is not None: self.error("Invalid Unicode sequence - high surrogate character (U+%X) not followed by a low surrogate" % ord(hisurr)) return codes def fontpool(self): if self.metriclist is None: def fillMetricList(familylist): metriclist = [] for family in familylist: metric = self.config.findfont(self.fontweight, self.fontstyle, family) if metric is not None: metriclist.append(FontMetricRecord(family, metric)) if len(metriclist) == 0: self.warning("Cannot find any font metric for family "+(", ".join(familylist))) return None else: return metriclist self.metriclist = fillMetricList(self.fontfamilies) if self.metriclist is None: self.fontfamilies = self.config.fallbackFamilies self.metriclist = fillMetricList(self.fontfamilies) if self.metriclist is None: self.error("Fatal error: cannot find any font metric for the node; fallback font families misconfiguration") raise sax.SAXException("Fatal error: cannot find any font metric for the node") return self.metriclist def metric(self): if self.nominalMetric is None: self.nominalMetric = self.fontpool()[0].metric for fd in self.metriclist: if fd.used: self.nominalMetric = fd.metric; break return self.nominalMetric def axis(self): return self.metric().axisposition * self.fontSize def nominalLineWidth(self): return self.metric().rulewidth * self.fontSize def nominalThinStrokeWidth(self): return 0.04 * self.originalFontSize def nominalMediumStrokeWidth(self): return 0.06 * self.originalFontSize def nominalThickStrokeWidth(self): return 0.08 * self.originalFontSize def nominalLineGap(self): return self.metric().vgap * self.fontSize def nominalAscender(self): return self.metric().ascender * self.fontSize def nominalDescender(self): return (- self.metric().descender * self.fontSize) def hasGlyph(self, ch): for fdesc in self.fontpool(): if fdesc.metric.chardata.get(ch) is not None: return True return False def findChar(self, ch): for fd in self.fontpool(): cm = fd.metric.chardata.get(ch) if cm: return (cm, fd) else: if 0 < ch and ch < 0xFFFF and unichr(ch) in specialChars.keys(): return self.findChar(ord(specialChars[unichr(ch)])) self.warning("Glyph U+%X not found" % ch) return None def measureText(self): """Measures text contents of a node""" if len(self.text) == 0: self.isSpace = True; return cm0 = None; cm1 = None; ucstext = self.getUCSText() for chcode in ucstext: chardesc = self.findChar(chcode) if chardesc is None: self.width += self.metric().missingGlyph.width else: (cm, fd) = chardesc fd.used = True if chcode == ucstext[0]: cm0 = cm if chcode == ucstext[-1]: cm1 = cm self.width += cm.width if self.height + self.depth == 0: self.height = cm.bbox[3] self.depth = - cm.bbox[1] elif cm.bbox[3] != cm.bbox[1]: # exclude space self.height = max (self.height, cm.bbox[3]) self.depth = max (self.depth, - cm.bbox[1]) # Normalize to the font size self.width *= self.fontSize self.depth *= self.fontSize self.height *= self.fontSize # Add ascender/descender values self.ascender = self.nominalAscender() self.descender = self.nominalDescender() # Shape correction if cm0 is not None: self.leftBearing = max(0, - cm0.bbox[0]) * self.fontSize if cm1 is not None: self.rightBearing = max(0, cm1.bbox[2] - cm.width) * self.fontSize self.width += self.leftBearing + self.rightBearing # Reset nominal metric self.nominalMetric = None
class MathNode: """MathML node descriptor. This class defines properties and methods that permit to building blocks to combine with each other, creating a complex mathematical expression. It uses dynamic binding to find methods to process specific MathML elements: these methods are contained in three other modules - contextmakers, measurers, and generators. """ def __init__(self, elementName, attributes, locator, config, parent): self.elementName = elementName self.config = config if locator is not None: self.locator = locator elif parent is not None: # handy when we add nodes in preprocessing self.locator = parent.locator else: self.locator = NodeLocator(None) self.text = u'' self.children = [] self.attributes = attributes self.parent = parent self.metriclist = None self.nominalMetric = None if parent is not None: self.nodeIndex = len(parent.children) self.defaults = parent.defaults parent.children.append(self) else: self.defaults = globalDefaults.copy() self.defaults.update(config.defaults) self.nodeIndex = 0 def makeContext(self): contextmakers.__dict__.get(u"context_" + self.elementName, contextmakers.default_context)(self) def makeChildContext(self, child): contextmakers.__dict__.get(u"child_context_" + self.elementName, contextmakers.default_child_context)(self, child) def measure(self): # Create the context for the node self.makeContext() # Measure all children for ch in self.children: ch.measure() # Perform node-specific measurement self.measureNode() def measureNode(self): measureMethod = measurers.__dict__.get(u"measure_" + self.elementName, measurers.default_measure) if self.config.verbose and measureMethod is measurers.default_measure: self.warning("MathML element '%s' is unsupported" % self.elementName) measureMethod(self) def draw(self, output): generators.__dict__.get(u"draw_" + self.elementName, generators.default_draw)(self, output) def makeImage(self, output): if self.elementName != 'math': self.warning("Root element in MathML document must be 'math'") self.measure() generators.drawImage(self, output) def warning(self, msg): self.locator.message(msg, "WARNING") def error(self, msg): self.locator.message(msg, "ERROR") def info(self, msg): if self.config.verbose: self.locator.message(msg, "INFO") def debug(self, event, msg): if event.strip() in self.config.debug: self.locator.message(msg, "DEBUG") def parseInt(self, x): try: return int(x, 10) except TypeError: self.error("Cannot parse string '%s' as an integer" % str(x)) return 0 def parseFloat(self, x): try: value = float(x) except ValueError: self.error("Cannot parse string '%s' as a float" % str(x)) return 0.0 text = str(value).lower() if text.find("nan") >= 0 or text.find("inf") >= 0: self.error("Cannot parse string '%s' as a float" % str(x)) return 0.0 return value def parseLength(self, lenattr, unitlessScale=0.75): lenattr = lenattr.strip() if lenattr.endswith("pt"): return self.parseFloat(lenattr[:-2]) elif lenattr.endswith("mm"): return self.parseFloat(lenattr[:-2]) * 72.0 / 25.4 elif lenattr.endswith("cm"): return self.parseFloat(lenattr[:-2]) * 72.0 / 2.54 elif lenattr.endswith("in"): return self.parseFloat(lenattr[:-2]) * 72.0 elif lenattr.endswith("pc"): return self.parseFloat(lenattr[:-2]) * 12.0 elif lenattr.endswith("px"): # pixels are calculated for 96 dpi return self.parseFloat(lenattr[:-2]) * 72.0 / 96.0 elif lenattr.endswith("em"): return self.parseFloat(lenattr[:-2]) * self.fontSize elif lenattr.endswith("ex"): return self.parseFloat( lenattr[:-2]) * self.fontSize * self.metric().xheight else: # unitless lengths are treated as if expressed in pixels return self.parseFloat(lenattr) * unitlessScale def parseSpace(self, spaceattr, unitlessScale=0.75): sign = 1.0 spaceattr = spaceattr.strip() if spaceattr.endswith(u"mathspace"): if spaceattr.startswith(u"negative"): sign = -1.0 spaceattr = spaceattr[8:] realspaceattr = self.defaults.get(spaceattr) if realspaceattr is None: self.error("Bad space token: '%s'" % spaceattr) realspaceattr = "0em" return self.parseLength(realspaceattr, unitlessScale) else: return self.parseLength(spaceattr, unitlessScale) def parsePercent(self, lenattr, percentBase): value = self.parseFloat(lenattr[:-1]) if value is not None: return percentBase * value / 100 else: return 0 def parseLengthOrPercent(self, lenattr, percentBase, unitlessScale=0.75): if lenattr.endswith(u"%"): return self.parsePercent(lenattr, percentBase) else: return self.parseLength(lenattr, unitlessScale) def parseSpaceOrPercent(self, lenattr, percentBase, unitlessScale=0.75): if lenattr.endswith(u"%"): return self.parsePercent(lenattr, percentBase) else: return self.parseSpace(lenattr, unitlessScale) def getProperty(self, key, defvalue=None): return self.attributes.get(key, self.defaults.get(key, defvalue)) def getListProperty(self, attr, value=None): if value is None: value = self.getProperty(attr) splitvalue = value.split() if len(splitvalue) > 0: return splitvalue self.error("Bad value for '%s' attribute: empty list" % attr) return self.defaults[attr].split() def getUCSText(self): codes = [] hisurr = None for ch in self.text: chcode = ord(ch) # Processing surrogate pairs if isLowSurrogate(ch): if hisurr is None: self.error( "Invalid Unicode sequence - low surrogate character (U+%X) not preceded by a high surrogate" % ord(ch)) else: chcode = decodeSurrogatePair(hisurr, ch) hisurr = None if hisurr is not None: self.error( "Invalid Unicode sequence - high surrogate character (U+%X) not followed by a low surrogate" % ord(hisurr)) hisurr = None if isHighSurrogate(ch): hisurr = ch continue codes.append(chcode) if hisurr is not None: self.error( "Invalid Unicode sequence - high surrogate character (U+%X) not followed by a low surrogate" % ord(hisurr)) return codes def fontpool(self): if self.metriclist is None: def fillMetricList(familylist): metriclist = [] for family in familylist: metric = self.config.findfont(self.fontweight, self.fontstyle, family) if metric is not None: metriclist.append(FontMetricRecord(family, metric)) if len(metriclist) == 0: self.warning("Cannot find any font metric for family " + (", ".join(familylist))) return None else: return metriclist self.metriclist = fillMetricList(self.fontfamilies) if self.metriclist is None: self.fontfamilies = self.config.fallbackFamilies self.metriclist = fillMetricList(self.fontfamilies) if self.metriclist is None: self.error( "Fatal error: cannot find any font metric for the node; fallback font families misconfiguration" ) raise sax.SAXException( "Fatal error: cannot find any font metric for the node") return self.metriclist def metric(self): if self.nominalMetric is None: self.nominalMetric = self.fontpool()[0].metric for fd in self.metriclist: if fd.used: self.nominalMetric = fd.metric break return self.nominalMetric def axis(self): return self.metric().axisposition * self.fontSize def nominalLineWidth(self): return self.metric().rulewidth * self.fontSize def nominalThinStrokeWidth(self): return 0.04 * self.originalFontSize def nominalMediumStrokeWidth(self): return 0.06 * self.originalFontSize def nominalThickStrokeWidth(self): return 0.08 * self.originalFontSize def nominalLineGap(self): return self.metric().vgap * self.fontSize def nominalAscender(self): return self.metric().ascender * self.fontSize def nominalDescender(self): return (-self.metric().descender * self.fontSize) def hasGlyph(self, ch): for fdesc in self.fontpool(): if fdesc.metric.chardata.get(ch) is not None: return True return False def findChar(self, ch): for fd in self.fontpool(): cm = fd.metric.chardata.get(ch) if cm: return (cm, fd) else: if 0 < ch and ch < 0xFFFF and unichr(ch) in specialChars.keys(): return self.findChar(ord(specialChars[unichr(ch)])) self.warning("Glyph U+%X not found" % ch) return None def measureText(self): """Measures text contents of a node""" if len(self.text) == 0: self.isSpace = True return cm0 = None cm1 = None ucstext = self.getUCSText() for chcode in ucstext: chardesc = self.findChar(chcode) if chardesc is None: self.width += self.metric().missingGlyph.width else: (cm, fd) = chardesc fd.used = True if chcode == ucstext[0]: cm0 = cm if chcode == ucstext[-1]: cm1 = cm self.width += cm.width if self.height + self.depth == 0: self.height = cm.bbox[3] self.depth = -cm.bbox[1] elif cm.bbox[3] != cm.bbox[1]: # exclude space self.height = max(self.height, cm.bbox[3]) self.depth = max(self.depth, -cm.bbox[1]) # Normalize to the font size self.width *= self.fontSize self.depth *= self.fontSize self.height *= self.fontSize # Add ascender/descender values self.ascender = self.nominalAscender() self.descender = self.nominalDescender() # Shape correction if cm0 is not None: self.leftBearing = max(0, -cm0.bbox[0]) * self.fontSize if cm1 is not None: self.rightBearing = max(0, cm1.bbox[2] - cm.width) * self.fontSize self.width += self.leftBearing + self.rightBearing # Reset nominal metric self.nominalMetric = None