ajout fichiers manquant

This commit is contained in:
Ambulance Clerc
2022-03-04 18:47:24 +01:00
parent 2e92cb8cf4
commit aa5b7f9254
170 changed files with 554027 additions and 1 deletions

View File

@@ -0,0 +1 @@
"""Empty __init__.py file to signal Python this directory is a package."""

View File

@@ -0,0 +1,46 @@
"""Collection of utilities for command-line interfaces and console scripts."""
import os
import re
numberAddedRE = re.compile(r"#\d+$")
def makeOutputFileName(input, outputDir=None, extension=None, overWrite=False):
"""Generates a suitable file name for writing output.
Often tools will want to take a file, do some kind of transformation to it,
and write it out again. This function determines an appropriate name for the
output file, through one or more of the following steps:
- changing the output directory
- replacing the file extension
- suffixing the filename with a number (``#1``, ``#2``, etc.) to avoid
overwriting an existing file.
Args:
input: Name of input file.
outputDir: Optionally, a new directory to write the file into.
extension: Optionally, a replacement for the current file extension.
overWrite: Overwriting an existing file is permitted if true; if false
and the proposed filename exists, a new name will be generated by
adding an appropriate number suffix.
Returns:
str: Suitable output filename
"""
dirName, fileName = os.path.split(input)
fileName, ext = os.path.splitext(fileName)
if outputDir:
dirName = outputDir
fileName = numberAddedRE.split(fileName)[0]
if extension is None:
extension = os.path.splitext(input)[1]
output = os.path.join(dirName, fileName + extension)
n = 1
if not overWrite:
while os.path.exists(output):
output = os.path.join(
dirName, fileName + "#" + repr(n) + extension)
n += 1
return output

View File

@@ -0,0 +1,25 @@
""" Exports a no-op 'cython' namespace similar to
https://github.com/cython/cython/blob/master/Cython/Shadow.py
This allows to optionally compile @cython decorated functions
(when cython is available at built time), or run the same code
as pure-python, without runtime dependency on cython module.
We only define the symbols that we use. E.g. see fontTools.cu2qu
"""
from types import SimpleNamespace
def _empty_decorator(x):
return x
compiled = False
for name in ("double", "complex", "int"):
globals()[name] = None
for name in ("cfunc", "inline"):
globals()[name] = _empty_decorator
locals = lambda **_: _empty_decorator
returns = lambda _: _empty_decorator

View File

@@ -0,0 +1,66 @@
"""Misc dict tools."""
__all__ = ['hashdict']
# https://stackoverflow.com/questions/1151658/python-hashable-dicts
class hashdict(dict):
"""
hashable dict implementation, suitable for use as a key into
other dicts.
>>> h1 = hashdict({"apples": 1, "bananas":2})
>>> h2 = hashdict({"bananas": 3, "mangoes": 5})
>>> h1+h2
hashdict(apples=1, bananas=3, mangoes=5)
>>> d1 = {}
>>> d1[h1] = "salad"
>>> d1[h1]
'salad'
>>> d1[h2]
Traceback (most recent call last):
...
KeyError: hashdict(bananas=3, mangoes=5)
based on answers from
http://stackoverflow.com/questions/1151658/python-hashable-dicts
"""
def __key(self):
return tuple(sorted(self.items()))
def __repr__(self):
return "{0}({1})".format(self.__class__.__name__,
", ".join("{0}={1}".format(
str(i[0]),repr(i[1])) for i in self.__key()))
def __hash__(self):
return hash(self.__key())
def __setitem__(self, key, value):
raise TypeError("{0} does not support item assignment"
.format(self.__class__.__name__))
def __delitem__(self, key):
raise TypeError("{0} does not support item assignment"
.format(self.__class__.__name__))
def clear(self):
raise TypeError("{0} does not support item assignment"
.format(self.__class__.__name__))
def pop(self, *args, **kwargs):
raise TypeError("{0} does not support item assignment"
.format(self.__class__.__name__))
def popitem(self, *args, **kwargs):
raise TypeError("{0} does not support item assignment"
.format(self.__class__.__name__))
def setdefault(self, *args, **kwargs):
raise TypeError("{0} does not support item assignment"
.format(self.__class__.__name__))
def update(self, *args, **kwargs):
raise TypeError("{0} does not support item assignment"
.format(self.__class__.__name__))
# update is not ok because it mutates the object
# __add__ is ok because it creates a new object
# while the new object is under construction, it's ok to mutate it
def __add__(self, right):
result = hashdict(self)
dict.update(result, right)
return result

View File

@@ -0,0 +1,242 @@
"""
This module implements the algorithm for converting between a "user name" -
something that a user can choose arbitrarily inside a font editor - and a file
name suitable for use in a wide range of operating systems and filesystems.
The `UFO 3 specification <http://unifiedfontobject.org/versions/ufo3/conventions/>`_
provides an example of an algorithm for such conversion, which avoids illegal
characters, reserved file names, ambiguity between upper- and lower-case
characters, and clashes with existing files.
This code was originally copied from
`ufoLib <https://github.com/unified-font-object/ufoLib/blob/8747da7/Lib/ufoLib/filenames.py>`_
by Tal Leming and is copyright (c) 2005-2016, The RoboFab Developers:
- Erik van Blokland
- Tal Leming
- Just van Rossum
"""
illegalCharacters = r"\" * + / : < > ? [ \ ] | \0".split(" ")
illegalCharacters += [chr(i) for i in range(1, 32)]
illegalCharacters += [chr(0x7F)]
reservedFileNames = "CON PRN AUX CLOCK$ NUL A:-Z: COM1".lower().split(" ")
reservedFileNames += "LPT1 LPT2 LPT3 COM2 COM3 COM4".lower().split(" ")
maxFileNameLength = 255
class NameTranslationError(Exception):
pass
def userNameToFileName(userName, existing=[], prefix="", suffix=""):
"""Converts from a user name to a file name.
Takes care to avoid illegal characters, reserved file names, ambiguity between
upper- and lower-case characters, and clashes with existing files.
Args:
userName (str): The input file name.
existing: A case-insensitive list of all existing file names.
prefix: Prefix to be prepended to the file name.
suffix: Suffix to be appended to the file name.
Returns:
A suitable filename.
Raises:
NameTranslationError: If no suitable name could be generated.
Examples::
>>> userNameToFileName("a") == "a"
True
>>> userNameToFileName("A") == "A_"
True
>>> userNameToFileName("AE") == "A_E_"
True
>>> userNameToFileName("Ae") == "A_e"
True
>>> userNameToFileName("ae") == "ae"
True
>>> userNameToFileName("aE") == "aE_"
True
>>> userNameToFileName("a.alt") == "a.alt"
True
>>> userNameToFileName("A.alt") == "A_.alt"
True
>>> userNameToFileName("A.Alt") == "A_.A_lt"
True
>>> userNameToFileName("A.aLt") == "A_.aL_t"
True
>>> userNameToFileName(u"A.alT") == "A_.alT_"
True
>>> userNameToFileName("T_H") == "T__H_"
True
>>> userNameToFileName("T_h") == "T__h"
True
>>> userNameToFileName("t_h") == "t_h"
True
>>> userNameToFileName("F_F_I") == "F__F__I_"
True
>>> userNameToFileName("f_f_i") == "f_f_i"
True
>>> userNameToFileName("Aacute_V.swash") == "A_acute_V_.swash"
True
>>> userNameToFileName(".notdef") == "_notdef"
True
>>> userNameToFileName("con") == "_con"
True
>>> userNameToFileName("CON") == "C_O_N_"
True
>>> userNameToFileName("con.alt") == "_con.alt"
True
>>> userNameToFileName("alt.con") == "alt._con"
True
"""
# the incoming name must be a str
if not isinstance(userName, str):
raise ValueError("The value for userName must be a string.")
# establish the prefix and suffix lengths
prefixLength = len(prefix)
suffixLength = len(suffix)
# replace an initial period with an _
# if no prefix is to be added
if not prefix and userName[0] == ".":
userName = "_" + userName[1:]
# filter the user name
filteredUserName = []
for character in userName:
# replace illegal characters with _
if character in illegalCharacters:
character = "_"
# add _ to all non-lower characters
elif character != character.lower():
character += "_"
filteredUserName.append(character)
userName = "".join(filteredUserName)
# clip to 255
sliceLength = maxFileNameLength - prefixLength - suffixLength
userName = userName[:sliceLength]
# test for illegal files names
parts = []
for part in userName.split("."):
if part.lower() in reservedFileNames:
part = "_" + part
parts.append(part)
userName = ".".join(parts)
# test for clash
fullName = prefix + userName + suffix
if fullName.lower() in existing:
fullName = handleClash1(userName, existing, prefix, suffix)
# finished
return fullName
def handleClash1(userName, existing=[], prefix="", suffix=""):
"""
existing should be a case-insensitive list
of all existing file names.
>>> prefix = ("0" * 5) + "."
>>> suffix = "." + ("0" * 10)
>>> existing = ["a" * 5]
>>> e = list(existing)
>>> handleClash1(userName="A" * 5, existing=e,
... prefix=prefix, suffix=suffix) == (
... '00000.AAAAA000000000000001.0000000000')
True
>>> e = list(existing)
>>> e.append(prefix + "aaaaa" + "1".zfill(15) + suffix)
>>> handleClash1(userName="A" * 5, existing=e,
... prefix=prefix, suffix=suffix) == (
... '00000.AAAAA000000000000002.0000000000')
True
>>> e = list(existing)
>>> e.append(prefix + "AAAAA" + "2".zfill(15) + suffix)
>>> handleClash1(userName="A" * 5, existing=e,
... prefix=prefix, suffix=suffix) == (
... '00000.AAAAA000000000000001.0000000000')
True
"""
# if the prefix length + user name length + suffix length + 15 is at
# or past the maximum length, silce 15 characters off of the user name
prefixLength = len(prefix)
suffixLength = len(suffix)
if prefixLength + len(userName) + suffixLength + 15 > maxFileNameLength:
l = (prefixLength + len(userName) + suffixLength + 15)
sliceLength = maxFileNameLength - l
userName = userName[:sliceLength]
finalName = None
# try to add numbers to create a unique name
counter = 1
while finalName is None:
name = userName + str(counter).zfill(15)
fullName = prefix + name + suffix
if fullName.lower() not in existing:
finalName = fullName
break
else:
counter += 1
if counter >= 999999999999999:
break
# if there is a clash, go to the next fallback
if finalName is None:
finalName = handleClash2(existing, prefix, suffix)
# finished
return finalName
def handleClash2(existing=[], prefix="", suffix=""):
"""
existing should be a case-insensitive list
of all existing file names.
>>> prefix = ("0" * 5) + "."
>>> suffix = "." + ("0" * 10)
>>> existing = [prefix + str(i) + suffix for i in range(100)]
>>> e = list(existing)
>>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == (
... '00000.100.0000000000')
True
>>> e = list(existing)
>>> e.remove(prefix + "1" + suffix)
>>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == (
... '00000.1.0000000000')
True
>>> e = list(existing)
>>> e.remove(prefix + "2" + suffix)
>>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == (
... '00000.2.0000000000')
True
"""
# calculate the longest possible string
maxLength = maxFileNameLength - len(prefix) - len(suffix)
maxValue = int("9" * maxLength)
# try to find a number
finalName = None
counter = 1
while finalName is None:
fullName = prefix + str(counter) + suffix
if fullName.lower() not in existing:
finalName = fullName
break
else:
counter += 1
if counter >= maxValue:
break
# raise an error if nothing has been found
if finalName is None:
raise NameTranslationError("No unique name could be found.")
# finished
return finalName
if __name__ == "__main__":
import doctest
import sys
sys.exit(doctest.testmod().failed)

View File

@@ -0,0 +1,25 @@
__all__ = ["popCount"]
try:
bit_count = int.bit_count
except AttributeError:
def bit_count(v):
return bin(v).count("1")
"""Return number of 1 bits (population count) of the absolute value of an integer.
See https://docs.python.org/3.10/library/stdtypes.html#int.bit_count
"""
popCount = bit_count
def bit_indices(v):
"""Return list of indices where bits are set, 0 being the index of the least significant bit.
>>> bit_indices(0b101)
[0, 2]
"""
return [i for i, b in enumerate(bin(v)[::-1]) if b == "1"]

View File

@@ -0,0 +1,216 @@
"""sstruct.py -- SuperStruct
Higher level layer on top of the struct module, enabling to
bind names to struct elements. The interface is similar to
struct, except the objects passed and returned are not tuples
(or argument lists), but dictionaries or instances.
Just like struct, we use fmt strings to describe a data
structure, except we use one line per element. Lines are
separated by newlines or semi-colons. Each line contains
either one of the special struct characters ('@', '=', '<',
'>' or '!') or a 'name:formatchar' combo (eg. 'myFloat:f').
Repetitions, like the struct module offers them are not useful
in this context, except for fixed length strings (eg. 'myInt:5h'
is not allowed but 'myString:5s' is). The 'x' fmt character
(pad byte) is treated as 'special', since it is by definition
anonymous. Extra whitespace is allowed everywhere.
The sstruct module offers one feature that the "normal" struct
module doesn't: support for fixed point numbers. These are spelled
as "n.mF", where n is the number of bits before the point, and m
the number of bits after the point. Fixed point numbers get
converted to floats.
pack(fmt, object):
'object' is either a dictionary or an instance (or actually
anything that has a __dict__ attribute). If it is a dictionary,
its keys are used for names. If it is an instance, it's
attributes are used to grab struct elements from. Returns
a string containing the data.
unpack(fmt, data, object=None)
If 'object' is omitted (or None), a new dictionary will be
returned. If 'object' is a dictionary, it will be used to add
struct elements to. If it is an instance (or in fact anything
that has a __dict__ attribute), an attribute will be added for
each struct element. In the latter two cases, 'object' itself
is returned.
unpack2(fmt, data, object=None)
Convenience function. Same as unpack, except data may be longer
than needed. The returned value is a tuple: (object, leftoverdata).
calcsize(fmt)
like struct.calcsize(), but uses our own fmt strings:
it returns the size of the data in bytes.
"""
from fontTools.misc.fixedTools import fixedToFloat as fi2fl, floatToFixed as fl2fi
from fontTools.misc.textTools import tobytes, tostr
import struct
import re
__version__ = "1.2"
__copyright__ = "Copyright 1998, Just van Rossum <just@letterror.com>"
class Error(Exception):
pass
def pack(fmt, obj):
formatstring, names, fixes = getformat(fmt, keep_pad_byte=True)
elements = []
if not isinstance(obj, dict):
obj = obj.__dict__
for name in names:
value = obj[name]
if name in fixes:
# fixed point conversion
value = fl2fi(value, fixes[name])
elif isinstance(value, str):
value = tobytes(value)
elements.append(value)
data = struct.pack(*(formatstring,) + tuple(elements))
return data
def unpack(fmt, data, obj=None):
if obj is None:
obj = {}
data = tobytes(data)
formatstring, names, fixes = getformat(fmt)
if isinstance(obj, dict):
d = obj
else:
d = obj.__dict__
elements = struct.unpack(formatstring, data)
for i in range(len(names)):
name = names[i]
value = elements[i]
if name in fixes:
# fixed point conversion
value = fi2fl(value, fixes[name])
elif isinstance(value, bytes):
try:
value = tostr(value)
except UnicodeDecodeError:
pass
d[name] = value
return obj
def unpack2(fmt, data, obj=None):
length = calcsize(fmt)
return unpack(fmt, data[:length], obj), data[length:]
def calcsize(fmt):
formatstring, names, fixes = getformat(fmt)
return struct.calcsize(formatstring)
# matches "name:formatchar" (whitespace is allowed)
_elementRE = re.compile(
r"\s*" # whitespace
r"([A-Za-z_][A-Za-z_0-9]*)" # name (python identifier)
r"\s*:\s*" # whitespace : whitespace
r"([xcbB?hHiIlLqQfd]|" # formatchar...
r"[0-9]+[ps]|" # ...formatchar...
r"([0-9]+)\.([0-9]+)(F))" # ...formatchar
r"\s*" # whitespace
r"(#.*)?$" # [comment] + end of string
)
# matches the special struct fmt chars and 'x' (pad byte)
_extraRE = re.compile(r"\s*([x@=<>!])\s*(#.*)?$")
# matches an "empty" string, possibly containing whitespace and/or a comment
_emptyRE = re.compile(r"\s*(#.*)?$")
_fixedpointmappings = {
8: "b",
16: "h",
32: "l"}
_formatcache = {}
def getformat(fmt, keep_pad_byte=False):
fmt = tostr(fmt, encoding="ascii")
try:
formatstring, names, fixes = _formatcache[fmt]
except KeyError:
lines = re.split("[\n;]", fmt)
formatstring = ""
names = []
fixes = {}
for line in lines:
if _emptyRE.match(line):
continue
m = _extraRE.match(line)
if m:
formatchar = m.group(1)
if formatchar != 'x' and formatstring:
raise Error("a special fmt char must be first")
else:
m = _elementRE.match(line)
if not m:
raise Error("syntax error in fmt: '%s'" % line)
name = m.group(1)
formatchar = m.group(2)
if keep_pad_byte or formatchar != "x":
names.append(name)
if m.group(3):
# fixed point
before = int(m.group(3))
after = int(m.group(4))
bits = before + after
if bits not in [8, 16, 32]:
raise Error("fixed point must be 8, 16 or 32 bits long")
formatchar = _fixedpointmappings[bits]
assert m.group(5) == "F"
fixes[name] = after
formatstring = formatstring + formatchar
_formatcache[fmt] = formatstring, names, fixes
return formatstring, names, fixes
def _test():
fmt = """
# comments are allowed
> # big endian (see documentation for struct)
# empty lines are allowed:
ashort: h
along: l
abyte: b # a byte
achar: c
astr: 5s
afloat: f; adouble: d # multiple "statements" are allowed
afixed: 16.16F
abool: ?
apad: x
"""
print('size:', calcsize(fmt))
class foo(object):
pass
i = foo()
i.ashort = 0x7fff
i.along = 0x7fffffff
i.abyte = 0x7f
i.achar = "a"
i.astr = "12345"
i.afloat = 0.5
i.adouble = 0.5
i.afixed = 1.5
i.abool = True
data = pack(fmt, i)
print('data:', repr(data))
print(unpack(fmt, data))
i2 = foo()
unpack(fmt, data, i2)
print(vars(i2))
if __name__ == "__main__":
_test()

View File

@@ -0,0 +1,192 @@
from fontTools.pens.basePen import BasePen
from functools import partial
from itertools import count
import sympy as sp
import sys
n = 3 # Max Bezier degree; 3 for cubic, 2 for quadratic
t, x, y = sp.symbols('t x y', real=True)
c = sp.symbols('c', real=False) # Complex representation instead of x/y
X = tuple(sp.symbols('x:%d'%(n+1), real=True))
Y = tuple(sp.symbols('y:%d'%(n+1), real=True))
P = tuple(zip(*(sp.symbols('p:%d[%s]'%(n+1,w), real=True) for w in '01')))
C = tuple(sp.symbols('c:%d'%(n+1), real=False))
# Cubic Bernstein basis functions
BinomialCoefficient = [(1, 0)]
for i in range(1, n+1):
last = BinomialCoefficient[-1]
this = tuple(last[j-1]+last[j] for j in range(len(last)))+(0,)
BinomialCoefficient.append(this)
BinomialCoefficient = tuple(tuple(item[:-1]) for item in BinomialCoefficient)
del last, this
BernsteinPolynomial = tuple(
tuple(c * t**i * (1-t)**(n-i) for i,c in enumerate(coeffs))
for n,coeffs in enumerate(BinomialCoefficient))
BezierCurve = tuple(
tuple(sum(P[i][j]*bernstein for i,bernstein in enumerate(bernsteins))
for j in range(2))
for n,bernsteins in enumerate(BernsteinPolynomial))
BezierCurveC = tuple(
sum(C[i]*bernstein for i,bernstein in enumerate(bernsteins))
for n,bernsteins in enumerate(BernsteinPolynomial))
def green(f, curveXY):
f = -sp.integrate(sp.sympify(f), y)
f = f.subs({x:curveXY[0], y:curveXY[1]})
f = sp.integrate(f * sp.diff(curveXY[0], t), (t, 0, 1))
return f
class _BezierFuncsLazy(dict):
def __init__(self, symfunc):
self._symfunc = symfunc
self._bezfuncs = {}
def __missing__(self, i):
args = ['p%d'%d for d in range(i+1)]
f = green(self._symfunc, BezierCurve[i])
f = sp.gcd_terms(f.collect(sum(P,()))) # Optimize
return sp.lambdify(args, f)
class GreenPen(BasePen):
_BezierFuncs = {}
@classmethod
def _getGreenBezierFuncs(celf, func):
funcstr = str(func)
if not funcstr in celf._BezierFuncs:
celf._BezierFuncs[funcstr] = _BezierFuncsLazy(func)
return celf._BezierFuncs[funcstr]
def __init__(self, func, glyphset=None):
BasePen.__init__(self, glyphset)
self._funcs = self._getGreenBezierFuncs(func)
self.value = 0
def _moveTo(self, p0):
self.__startPoint = p0
def _closePath(self):
p0 = self._getCurrentPoint()
if p0 != self.__startPoint:
self._lineTo(self.__startPoint)
def _endPath(self):
p0 = self._getCurrentPoint()
if p0 != self.__startPoint:
# Green theorem is not defined on open contours.
raise NotImplementedError
def _lineTo(self, p1):
p0 = self._getCurrentPoint()
self.value += self._funcs[1](p0, p1)
def _qCurveToOne(self, p1, p2):
p0 = self._getCurrentPoint()
self.value += self._funcs[2](p0, p1, p2)
def _curveToOne(self, p1, p2, p3):
p0 = self._getCurrentPoint()
self.value += self._funcs[3](p0, p1, p2, p3)
# Sample pens.
# Do not use this in real code.
# Use fontTools.pens.momentsPen.MomentsPen instead.
AreaPen = partial(GreenPen, func=1)
MomentXPen = partial(GreenPen, func=x)
MomentYPen = partial(GreenPen, func=y)
MomentXXPen = partial(GreenPen, func=x*x)
MomentYYPen = partial(GreenPen, func=y*y)
MomentXYPen = partial(GreenPen, func=x*y)
def printGreenPen(penName, funcs, file=sys.stdout):
print(
'''from fontTools.pens.basePen import BasePen
class %s(BasePen):
def __init__(self, glyphset=None):
BasePen.__init__(self, glyphset)
'''%penName, file=file)
for name,f in funcs:
print(' self.%s = 0' % name, file=file)
print('''
def _moveTo(self, p0):
self.__startPoint = p0
def _closePath(self):
p0 = self._getCurrentPoint()
if p0 != self.__startPoint:
self._lineTo(self.__startPoint)
def _endPath(self):
p0 = self._getCurrentPoint()
if p0 != self.__startPoint:
# Green theorem is not defined on open contours.
raise NotImplementedError
''', end='', file=file)
for n in (1, 2, 3):
if n == 1:
print('''
def _lineTo(self, p1):
x0,y0 = self._getCurrentPoint()
x1,y1 = p1
''', file=file)
elif n == 2:
print('''
def _qCurveToOne(self, p1, p2):
x0,y0 = self._getCurrentPoint()
x1,y1 = p1
x2,y2 = p2
''', file=file)
elif n == 3:
print('''
def _curveToOne(self, p1, p2, p3):
x0,y0 = self._getCurrentPoint()
x1,y1 = p1
x2,y2 = p2
x3,y3 = p3
''', file=file)
subs = {P[i][j]: [X, Y][j][i] for i in range(n+1) for j in range(2)}
greens = [green(f, BezierCurve[n]) for name,f in funcs]
greens = [sp.gcd_terms(f.collect(sum(P,()))) for f in greens] # Optimize
greens = [f.subs(subs) for f in greens] # Convert to p to x/y
defs, exprs = sp.cse(greens,
optimizations='basic',
symbols=(sp.Symbol('r%d'%i) for i in count()))
for name,value in defs:
print(' %s = %s' % (name, value), file=file)
print(file=file)
for name,value in zip([f[0] for f in funcs], exprs):
print(' self.%s += %s' % (name, value), file=file)
print('''
if __name__ == '__main__':
from fontTools.misc.symfont import x, y, printGreenPen
printGreenPen('%s', ['''%penName, file=file)
for name,f in funcs:
print(" ('%s', %s)," % (name, str(f)), file=file)
print(' ])', file=file)
if __name__ == '__main__':
pen = AreaPen()
pen.moveTo((100,100))
pen.lineTo((100,200))
pen.lineTo((200,200))
pen.curveTo((200,250),(300,300),(250,350))
pen.lineTo((200,100))
pen.closePath()
print(pen.value)

View File

@@ -0,0 +1,198 @@
"""Helpers for writing unit tests."""
from collections.abc import Iterable
from io import BytesIO
import os
import shutil
import sys
import tempfile
from unittest import TestCase as _TestCase
from fontTools.misc.textTools import tobytes
from fontTools.misc.xmlWriter import XMLWriter
def parseXML(xmlSnippet):
"""Parses a snippet of XML.
Input can be either a single string (unicode or UTF-8 bytes), or a
a sequence of strings.
The result is in the same format that would be returned by
XMLReader, but the parser imposes no constraints on the root
element so it can be called on small snippets of TTX files.
"""
# To support snippets with multiple elements, we add a fake root.
reader = TestXMLReader_()
xml = b"<root>"
if isinstance(xmlSnippet, bytes):
xml += xmlSnippet
elif isinstance(xmlSnippet, str):
xml += tobytes(xmlSnippet, 'utf-8')
elif isinstance(xmlSnippet, Iterable):
xml += b"".join(tobytes(s, 'utf-8') for s in xmlSnippet)
else:
raise TypeError("expected string or sequence of strings; found %r"
% type(xmlSnippet).__name__)
xml += b"</root>"
reader.parser.Parse(xml, 0)
return reader.root[2]
def parseXmlInto(font, parseInto, xmlSnippet):
parsed_xml = [e for e in parseXML(xmlSnippet.strip()) if not isinstance(e, str)]
for name, attrs, content in parsed_xml:
parseInto.fromXML(name, attrs, content, font)
parseInto.populateDefaults()
return parseInto
class FakeFont:
def __init__(self, glyphs):
self.glyphOrder_ = glyphs
self.reverseGlyphOrderDict_ = {g: i for i, g in enumerate(glyphs)}
self.lazy = False
self.tables = {}
def __getitem__(self, tag):
return self.tables[tag]
def __setitem__(self, tag, table):
self.tables[tag] = table
def get(self, tag, default=None):
return self.tables.get(tag, default)
def getGlyphID(self, name):
return self.reverseGlyphOrderDict_[name]
def getGlyphIDMany(self, lst):
return [self.getGlyphID(gid) for gid in lst]
def getGlyphName(self, glyphID):
if glyphID < len(self.glyphOrder_):
return self.glyphOrder_[glyphID]
else:
return "glyph%.5d" % glyphID
def getGlyphNameMany(self, lst):
return [self.getGlyphName(gid) for gid in lst]
def getGlyphOrder(self):
return self.glyphOrder_
def getReverseGlyphMap(self):
return self.reverseGlyphOrderDict_
def getGlyphNames(self):
return sorted(self.getGlyphOrder())
class TestXMLReader_(object):
def __init__(self):
from xml.parsers.expat import ParserCreate
self.parser = ParserCreate()
self.parser.StartElementHandler = self.startElement_
self.parser.EndElementHandler = self.endElement_
self.parser.CharacterDataHandler = self.addCharacterData_
self.root = None
self.stack = []
def startElement_(self, name, attrs):
element = (name, attrs, [])
if self.stack:
self.stack[-1][2].append(element)
else:
self.root = element
self.stack.append(element)
def endElement_(self, name):
self.stack.pop()
def addCharacterData_(self, data):
self.stack[-1][2].append(data)
def makeXMLWriter(newlinestr='\n'):
# don't write OS-specific new lines
writer = XMLWriter(BytesIO(), newlinestr=newlinestr)
# erase XML declaration
writer.file.seek(0)
writer.file.truncate()
return writer
def getXML(func, ttFont=None):
"""Call the passed toXML function and return the written content as a
list of lines (unicode strings).
Result is stripped of XML declaration and OS-specific newline characters.
"""
writer = makeXMLWriter()
func(writer, ttFont)
xml = writer.file.getvalue().decode("utf-8")
# toXML methods must always end with a writer.newline()
assert xml.endswith("\n")
return xml.splitlines()
class MockFont(object):
"""A font-like object that automatically adds any looked up glyphname
to its glyphOrder."""
def __init__(self):
self._glyphOrder = ['.notdef']
class AllocatingDict(dict):
def __missing__(reverseDict, key):
self._glyphOrder.append(key)
gid = len(reverseDict)
reverseDict[key] = gid
return gid
self._reverseGlyphOrder = AllocatingDict({'.notdef': 0})
self.lazy = False
def getGlyphID(self, glyph):
gid = self._reverseGlyphOrder[glyph]
return gid
def getReverseGlyphMap(self):
return self._reverseGlyphOrder
def getGlyphName(self, gid):
return self._glyphOrder[gid]
def getGlyphOrder(self):
return self._glyphOrder
class TestCase(_TestCase):
def __init__(self, methodName):
_TestCase.__init__(self, methodName)
# Python 3 renamed assertRaisesRegexp to assertRaisesRegex,
# and fires deprecation warnings if a program uses the old name.
if not hasattr(self, "assertRaisesRegex"):
self.assertRaisesRegex = self.assertRaisesRegexp
class DataFilesHandler(TestCase):
def setUp(self):
self.tempdir = None
self.num_tempfiles = 0
def tearDown(self):
if self.tempdir:
shutil.rmtree(self.tempdir)
def getpath(self, testfile):
folder = os.path.dirname(sys.modules[self.__module__].__file__)
return os.path.join(folder, "data", testfile)
def temp_dir(self):
if not self.tempdir:
self.tempdir = tempfile.mkdtemp()
def temp_font(self, font_path, file_name):
self.temp_dir()
temppath = os.path.join(self.tempdir, file_name)
shutil.copy2(font_path, temppath)
return temppath

View File

@@ -0,0 +1,154 @@
"""fontTools.misc.textTools.py -- miscellaneous routines."""
import ast
import string
# alias kept for backward compatibility
safeEval = ast.literal_eval
class Tag(str):
@staticmethod
def transcode(blob):
if isinstance(blob, bytes):
blob = blob.decode("latin-1")
return blob
def __new__(self, content):
return str.__new__(self, self.transcode(content))
def __ne__(self, other):
return not self.__eq__(other)
def __eq__(self, other):
return str.__eq__(self, self.transcode(other))
def __hash__(self):
return str.__hash__(self)
def tobytes(self):
return self.encode("latin-1")
def readHex(content):
"""Convert a list of hex strings to binary data."""
return deHexStr(strjoin(chunk for chunk in content if isinstance(chunk, str)))
def deHexStr(hexdata):
"""Convert a hex string to binary data."""
hexdata = strjoin(hexdata.split())
if len(hexdata) % 2:
hexdata = hexdata + "0"
data = []
for i in range(0, len(hexdata), 2):
data.append(bytechr(int(hexdata[i:i+2], 16)))
return bytesjoin(data)
def hexStr(data):
"""Convert binary data to a hex string."""
h = string.hexdigits
r = ''
for c in data:
i = byteord(c)
r = r + h[(i >> 4) & 0xF] + h[i & 0xF]
return r
def num2binary(l, bits=32):
items = []
binary = ""
for i in range(bits):
if l & 0x1:
binary = "1" + binary
else:
binary = "0" + binary
l = l >> 1
if not ((i+1) % 8):
items.append(binary)
binary = ""
if binary:
items.append(binary)
items.reverse()
assert l in (0, -1), "number doesn't fit in number of bits"
return ' '.join(items)
def binary2num(bin):
bin = strjoin(bin.split())
l = 0
for digit in bin:
l = l << 1
if digit != "0":
l = l | 0x1
return l
def caselessSort(alist):
"""Return a sorted copy of a list. If there are only strings
in the list, it will not consider case.
"""
try:
return sorted(alist, key=lambda a: (a.lower(), a))
except TypeError:
return sorted(alist)
def pad(data, size):
r""" Pad byte string 'data' with null bytes until its length is a
multiple of 'size'.
>>> len(pad(b'abcd', 4))
4
>>> len(pad(b'abcde', 2))
6
>>> len(pad(b'abcde', 4))
8
>>> pad(b'abcdef', 4) == b'abcdef\x00\x00'
True
"""
data = tobytes(data)
if size > 1:
remainder = len(data) % size
if remainder:
data += b"\0" * (size - remainder)
return data
def tostr(s, encoding="ascii", errors="strict"):
if not isinstance(s, str):
return s.decode(encoding, errors)
else:
return s
def tobytes(s, encoding="ascii", errors="strict"):
if isinstance(s, str):
return s.encode(encoding, errors)
else:
return bytes(s)
def bytechr(n):
return bytes([n])
def byteord(c):
return c if isinstance(c, int) else ord(c)
def strjoin(iterable, joiner=""):
return tostr(joiner).join(iterable)
def bytesjoin(iterable, joiner=b""):
return tobytes(joiner).join(tobytes(item) for item in iterable)
if __name__ == "__main__":
import doctest, sys
sys.exit(doctest.testmod().failed)

View File

@@ -0,0 +1,68 @@
"""fontTools.misc.timeTools.py -- tools for working with OpenType timestamps.
"""
import os
import time
from datetime import datetime, timezone
import calendar
epoch_diff = calendar.timegm((1904, 1, 1, 0, 0, 0, 0, 0, 0))
DAYNAMES = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
MONTHNAMES = [None, "Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
def asctime(t=None):
"""
Convert a tuple or struct_time representing a time as returned by gmtime()
or localtime() to a 24-character string of the following form:
>>> asctime(time.gmtime(0))
'Thu Jan 1 00:00:00 1970'
If t is not provided, the current time as returned by localtime() is used.
Locale information is not used by asctime().
This is meant to normalise the output of the built-in time.asctime() across
different platforms and Python versions.
In Python 3.x, the day of the month is right-justified, whereas on Windows
Python 2.7 it is padded with zeros.
See https://github.com/fonttools/fonttools/issues/455
"""
if t is None:
t = time.localtime()
s = "%s %s %2s %s" % (
DAYNAMES[t.tm_wday], MONTHNAMES[t.tm_mon], t.tm_mday,
time.strftime("%H:%M:%S %Y", t))
return s
def timestampToString(value):
return asctime(time.gmtime(max(0, value + epoch_diff)))
def timestampFromString(value):
wkday, mnth = value[:7].split()
t = datetime.strptime(value[7:], ' %d %H:%M:%S %Y')
t = t.replace(month=MONTHNAMES.index(mnth), tzinfo=timezone.utc)
wkday_idx = DAYNAMES.index(wkday)
assert t.weekday() == wkday_idx, '"' + value + '" has inconsistent weekday'
return int(t.timestamp()) - epoch_diff
def timestampNow():
# https://reproducible-builds.org/specs/source-date-epoch/
source_date_epoch = os.environ.get("SOURCE_DATE_EPOCH")
if source_date_epoch is not None:
return int(source_date_epoch) - epoch_diff
return int(time.time() - epoch_diff)
def timestampSinceEpoch(value):
return int(value - epoch_diff)
if __name__ == "__main__":
import sys
import doctest
sys.exit(doctest.testmod().failed)

View File

@@ -0,0 +1,398 @@
"""Affine 2D transformation matrix class.
The Transform class implements various transformation matrix operations,
both on the matrix itself, as well as on 2D coordinates.
Transform instances are effectively immutable: all methods that operate on the
transformation itself always return a new instance. This has as the
interesting side effect that Transform instances are hashable, ie. they can be
used as dictionary keys.
This module exports the following symbols:
Transform
this is the main class
Identity
Transform instance set to the identity transformation
Offset
Convenience function that returns a translating transformation
Scale
Convenience function that returns a scaling transformation
:Example:
>>> t = Transform(2, 0, 0, 3, 0, 0)
>>> t.transformPoint((100, 100))
(200, 300)
>>> t = Scale(2, 3)
>>> t.transformPoint((100, 100))
(200, 300)
>>> t.transformPoint((0, 0))
(0, 0)
>>> t = Offset(2, 3)
>>> t.transformPoint((100, 100))
(102, 103)
>>> t.transformPoint((0, 0))
(2, 3)
>>> t2 = t.scale(0.5)
>>> t2.transformPoint((100, 100))
(52.0, 53.0)
>>> import math
>>> t3 = t2.rotate(math.pi / 2)
>>> t3.transformPoint((0, 0))
(2.0, 3.0)
>>> t3.transformPoint((100, 100))
(-48.0, 53.0)
>>> t = Identity.scale(0.5).translate(100, 200).skew(0.1, 0.2)
>>> t.transformPoints([(0, 0), (1, 1), (100, 100)])
[(50.0, 100.0), (50.550167336042726, 100.60135501775433), (105.01673360427253, 160.13550177543362)]
>>>
"""
from typing import NamedTuple
__all__ = ["Transform", "Identity", "Offset", "Scale"]
_EPSILON = 1e-15
_ONE_EPSILON = 1 - _EPSILON
_MINUS_ONE_EPSILON = -1 + _EPSILON
def _normSinCos(v):
if abs(v) < _EPSILON:
v = 0
elif v > _ONE_EPSILON:
v = 1
elif v < _MINUS_ONE_EPSILON:
v = -1
return v
class Transform(NamedTuple):
"""2x2 transformation matrix plus offset, a.k.a. Affine transform.
Transform instances are immutable: all transforming methods, eg.
rotate(), return a new Transform instance.
:Example:
>>> t = Transform()
>>> t
<Transform [1 0 0 1 0 0]>
>>> t.scale(2)
<Transform [2 0 0 2 0 0]>
>>> t.scale(2.5, 5.5)
<Transform [2.5 0 0 5.5 0 0]>
>>>
>>> t.scale(2, 3).transformPoint((100, 100))
(200, 300)
Transform's constructor takes six arguments, all of which are
optional, and can be used as keyword arguments::
>>> Transform(12)
<Transform [12 0 0 1 0 0]>
>>> Transform(dx=12)
<Transform [1 0 0 1 12 0]>
>>> Transform(yx=12)
<Transform [1 0 12 1 0 0]>
Transform instances also behave like sequences of length 6::
>>> len(Identity)
6
>>> list(Identity)
[1, 0, 0, 1, 0, 0]
>>> tuple(Identity)
(1, 0, 0, 1, 0, 0)
Transform instances are comparable::
>>> t1 = Identity.scale(2, 3).translate(4, 6)
>>> t2 = Identity.translate(8, 18).scale(2, 3)
>>> t1 == t2
1
But beware of floating point rounding errors::
>>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6)
>>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3)
>>> t1
<Transform [0.2 0 0 0.3 0.08 0.18]>
>>> t2
<Transform [0.2 0 0 0.3 0.08 0.18]>
>>> t1 == t2
0
Transform instances are hashable, meaning you can use them as
keys in dictionaries::
>>> d = {Scale(12, 13): None}
>>> d
{<Transform [12 0 0 13 0 0]>: None}
But again, beware of floating point rounding errors::
>>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6)
>>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3)
>>> t1
<Transform [0.2 0 0 0.3 0.08 0.18]>
>>> t2
<Transform [0.2 0 0 0.3 0.08 0.18]>
>>> d = {t1: None}
>>> d
{<Transform [0.2 0 0 0.3 0.08 0.18]>: None}
>>> d[t2]
Traceback (most recent call last):
File "<stdin>", line 1, in ?
KeyError: <Transform [0.2 0 0 0.3 0.08 0.18]>
"""
xx: float = 1
xy: float = 0
yx: float = 0
yy: float = 1
dx: float = 0
dy: float = 0
def transformPoint(self, p):
"""Transform a point.
:Example:
>>> t = Transform()
>>> t = t.scale(2.5, 5.5)
>>> t.transformPoint((100, 100))
(250.0, 550.0)
"""
(x, y) = p
xx, xy, yx, yy, dx, dy = self
return (xx*x + yx*y + dx, xy*x + yy*y + dy)
def transformPoints(self, points):
"""Transform a list of points.
:Example:
>>> t = Scale(2, 3)
>>> t.transformPoints([(0, 0), (0, 100), (100, 100), (100, 0)])
[(0, 0), (0, 300), (200, 300), (200, 0)]
>>>
"""
xx, xy, yx, yy, dx, dy = self
return [(xx*x + yx*y + dx, xy*x + yy*y + dy) for x, y in points]
def transformVector(self, v):
"""Transform an (dx, dy) vector, treating translation as zero.
:Example:
>>> t = Transform(2, 0, 0, 2, 10, 20)
>>> t.transformVector((3, -4))
(6, -8)
>>>
"""
(dx, dy) = v
xx, xy, yx, yy = self[:4]
return (xx*dx + yx*dy, xy*dx + yy*dy)
def transformVectors(self, vectors):
"""Transform a list of (dx, dy) vector, treating translation as zero.
:Example:
>>> t = Transform(2, 0, 0, 2, 10, 20)
>>> t.transformVectors([(3, -4), (5, -6)])
[(6, -8), (10, -12)]
>>>
"""
xx, xy, yx, yy = self[:4]
return [(xx*dx + yx*dy, xy*dx + yy*dy) for dx, dy in vectors]
def translate(self, x=0, y=0):
"""Return a new transformation, translated (offset) by x, y.
:Example:
>>> t = Transform()
>>> t.translate(20, 30)
<Transform [1 0 0 1 20 30]>
>>>
"""
return self.transform((1, 0, 0, 1, x, y))
def scale(self, x=1, y=None):
"""Return a new transformation, scaled by x, y. The 'y' argument
may be None, which implies to use the x value for y as well.
:Example:
>>> t = Transform()
>>> t.scale(5)
<Transform [5 0 0 5 0 0]>
>>> t.scale(5, 6)
<Transform [5 0 0 6 0 0]>
>>>
"""
if y is None:
y = x
return self.transform((x, 0, 0, y, 0, 0))
def rotate(self, angle):
"""Return a new transformation, rotated by 'angle' (radians).
:Example:
>>> import math
>>> t = Transform()
>>> t.rotate(math.pi / 2)
<Transform [0 1 -1 0 0 0]>
>>>
"""
import math
c = _normSinCos(math.cos(angle))
s = _normSinCos(math.sin(angle))
return self.transform((c, s, -s, c, 0, 0))
def skew(self, x=0, y=0):
"""Return a new transformation, skewed by x and y.
:Example:
>>> import math
>>> t = Transform()
>>> t.skew(math.pi / 4)
<Transform [1 0 1 1 0 0]>
>>>
"""
import math
return self.transform((1, math.tan(y), math.tan(x), 1, 0, 0))
def transform(self, other):
"""Return a new transformation, transformed by another
transformation.
:Example:
>>> t = Transform(2, 0, 0, 3, 1, 6)
>>> t.transform((4, 3, 2, 1, 5, 6))
<Transform [8 9 4 3 11 24]>
>>>
"""
xx1, xy1, yx1, yy1, dx1, dy1 = other
xx2, xy2, yx2, yy2, dx2, dy2 = self
return self.__class__(
xx1*xx2 + xy1*yx2,
xx1*xy2 + xy1*yy2,
yx1*xx2 + yy1*yx2,
yx1*xy2 + yy1*yy2,
xx2*dx1 + yx2*dy1 + dx2,
xy2*dx1 + yy2*dy1 + dy2)
def reverseTransform(self, other):
"""Return a new transformation, which is the other transformation
transformed by self. self.reverseTransform(other) is equivalent to
other.transform(self).
:Example:
>>> t = Transform(2, 0, 0, 3, 1, 6)
>>> t.reverseTransform((4, 3, 2, 1, 5, 6))
<Transform [8 6 6 3 21 15]>
>>> Transform(4, 3, 2, 1, 5, 6).transform((2, 0, 0, 3, 1, 6))
<Transform [8 6 6 3 21 15]>
>>>
"""
xx1, xy1, yx1, yy1, dx1, dy1 = self
xx2, xy2, yx2, yy2, dx2, dy2 = other
return self.__class__(
xx1*xx2 + xy1*yx2,
xx1*xy2 + xy1*yy2,
yx1*xx2 + yy1*yx2,
yx1*xy2 + yy1*yy2,
xx2*dx1 + yx2*dy1 + dx2,
xy2*dx1 + yy2*dy1 + dy2)
def inverse(self):
"""Return the inverse transformation.
:Example:
>>> t = Identity.translate(2, 3).scale(4, 5)
>>> t.transformPoint((10, 20))
(42, 103)
>>> it = t.inverse()
>>> it.transformPoint((42, 103))
(10.0, 20.0)
>>>
"""
if self == Identity:
return self
xx, xy, yx, yy, dx, dy = self
det = xx*yy - yx*xy
xx, xy, yx, yy = yy/det, -xy/det, -yx/det, xx/det
dx, dy = -xx*dx - yx*dy, -xy*dx - yy*dy
return self.__class__(xx, xy, yx, yy, dx, dy)
def toPS(self):
"""Return a PostScript representation
:Example:
>>> t = Identity.scale(2, 3).translate(4, 5)
>>> t.toPS()
'[2 0 0 3 8 15]'
>>>
"""
return "[%s %s %s %s %s %s]" % self
def __bool__(self):
"""Returns True if transform is not identity, False otherwise.
:Example:
>>> bool(Identity)
False
>>> bool(Transform())
False
>>> bool(Scale(1.))
False
>>> bool(Scale(2))
True
>>> bool(Offset())
False
>>> bool(Offset(0))
False
>>> bool(Offset(2))
True
"""
return self != Identity
def __repr__(self):
return "<%s [%g %g %g %g %g %g]>" % ((self.__class__.__name__,) + self)
Identity = Transform()
def Offset(x=0, y=0):
"""Return the identity transformation offset by x, y.
:Example:
>>> Offset(2, 3)
<Transform [1 0 0 1 2 3]>
>>>
"""
return Transform(1, 0, 0, 1, x, y)
def Scale(x, y=None):
"""Return the identity transformation scaled by x, y. The 'y' argument
may be None, which implies to use the x value for y as well.
:Example:
>>> Scale(2, 3)
<Transform [2 0 0 3 0 0]>
>>>
"""
if y is None:
y = x
return Transform(x, 0, 0, y, 0, 0)
if __name__ == "__main__":
import sys
import doctest
sys.exit(doctest.testmod().failed)

View File

@@ -0,0 +1,171 @@
from fontTools import ttLib
from fontTools.misc.textTools import safeEval
from fontTools.ttLib.tables.DefaultTable import DefaultTable
import sys
import os
import logging
log = logging.getLogger(__name__)
class TTXParseError(Exception): pass
BUFSIZE = 0x4000
class XMLReader(object):
def __init__(self, fileOrPath, ttFont, progress=None, quiet=None, contentOnly=False):
if fileOrPath == '-':
fileOrPath = sys.stdin
if not hasattr(fileOrPath, "read"):
self.file = open(fileOrPath, "rb")
self._closeStream = True
else:
# assume readable file object
self.file = fileOrPath
self._closeStream = False
self.ttFont = ttFont
self.progress = progress
if quiet is not None:
from fontTools.misc.loggingTools import deprecateArgument
deprecateArgument("quiet", "configure logging instead")
self.quiet = quiet
self.root = None
self.contentStack = []
self.contentOnly = contentOnly
self.stackSize = 0
def read(self, rootless=False):
if rootless:
self.stackSize += 1
if self.progress:
self.file.seek(0, 2)
fileSize = self.file.tell()
self.progress.set(0, fileSize // 100 or 1)
self.file.seek(0)
self._parseFile(self.file)
if self._closeStream:
self.close()
if rootless:
self.stackSize -= 1
def close(self):
self.file.close()
def _parseFile(self, file):
from xml.parsers.expat import ParserCreate
parser = ParserCreate()
parser.StartElementHandler = self._startElementHandler
parser.EndElementHandler = self._endElementHandler
parser.CharacterDataHandler = self._characterDataHandler
pos = 0
while True:
chunk = file.read(BUFSIZE)
if not chunk:
parser.Parse(chunk, 1)
break
pos = pos + len(chunk)
if self.progress:
self.progress.set(pos // 100)
parser.Parse(chunk, 0)
def _startElementHandler(self, name, attrs):
if self.stackSize == 1 and self.contentOnly:
# We already know the table we're parsing, skip
# parsing the table tag and continue to
# stack '2' which begins parsing content
self.contentStack.append([])
self.stackSize = 2
return
stackSize = self.stackSize
self.stackSize = stackSize + 1
subFile = attrs.get("src")
if subFile is not None:
if hasattr(self.file, 'name'):
# if file has a name, get its parent directory
dirname = os.path.dirname(self.file.name)
else:
# else fall back to using the current working directory
dirname = os.getcwd()
subFile = os.path.join(dirname, subFile)
if not stackSize:
if name != "ttFont":
raise TTXParseError("illegal root tag: %s" % name)
if self.ttFont.reader is None and not self.ttFont.tables:
sfntVersion = attrs.get("sfntVersion")
if sfntVersion is not None:
if len(sfntVersion) != 4:
sfntVersion = safeEval('"' + sfntVersion + '"')
self.ttFont.sfntVersion = sfntVersion
self.contentStack.append([])
elif stackSize == 1:
if subFile is not None:
subReader = XMLReader(subFile, self.ttFont, self.progress)
subReader.read()
self.contentStack.append([])
return
tag = ttLib.xmlToTag(name)
msg = "Parsing '%s' table..." % tag
if self.progress:
self.progress.setLabel(msg)
log.info(msg)
if tag == "GlyphOrder":
tableClass = ttLib.GlyphOrder
elif "ERROR" in attrs or ('raw' in attrs and safeEval(attrs['raw'])):
tableClass = DefaultTable
else:
tableClass = ttLib.getTableClass(tag)
if tableClass is None:
tableClass = DefaultTable
if tag == 'loca' and tag in self.ttFont:
# Special-case the 'loca' table as we need the
# original if the 'glyf' table isn't recompiled.
self.currentTable = self.ttFont[tag]
else:
self.currentTable = tableClass(tag)
self.ttFont[tag] = self.currentTable
self.contentStack.append([])
elif stackSize == 2 and subFile is not None:
subReader = XMLReader(subFile, self.ttFont, self.progress, contentOnly=True)
subReader.read()
self.contentStack.append([])
self.root = subReader.root
elif stackSize == 2:
self.contentStack.append([])
self.root = (name, attrs, self.contentStack[-1])
else:
l = []
self.contentStack[-1].append((name, attrs, l))
self.contentStack.append(l)
def _characterDataHandler(self, data):
if self.stackSize > 1:
self.contentStack[-1].append(data)
def _endElementHandler(self, name):
self.stackSize = self.stackSize - 1
del self.contentStack[-1]
if not self.contentOnly:
if self.stackSize == 1:
self.root = None
elif self.stackSize == 2:
name, attrs, content = self.root
self.currentTable.fromXML(name, attrs, content, self.ttFont)
self.root = None
class ProgressPrinter(object):
def __init__(self, title, maxval=100):
print(title)
def set(self, val, maxval=None):
pass
def increment(self, val=1):
pass
def setLabel(self, text):
print(text)

View File

@@ -0,0 +1,194 @@
"""xmlWriter.py -- Simple XML authoring class"""
from fontTools.misc.textTools import byteord, strjoin, tobytes, tostr
import sys
import os
import string
INDENT = " "
class XMLWriter(object):
def __init__(self, fileOrPath, indentwhite=INDENT, idlefunc=None, encoding="utf_8",
newlinestr="\n"):
if encoding.lower().replace('-','').replace('_','') != 'utf8':
raise Exception('Only UTF-8 encoding is supported.')
if fileOrPath == '-':
fileOrPath = sys.stdout
if not hasattr(fileOrPath, "write"):
self.filename = fileOrPath
self.file = open(fileOrPath, "wb")
self._closeStream = True
else:
self.filename = None
# assume writable file object
self.file = fileOrPath
self._closeStream = False
# Figure out if writer expects bytes or unicodes
try:
# The bytes check should be first. See:
# https://github.com/fonttools/fonttools/pull/233
self.file.write(b'')
self.totype = tobytes
except TypeError:
# This better not fail.
self.file.write('')
self.totype = tostr
self.indentwhite = self.totype(indentwhite)
if newlinestr is None:
self.newlinestr = self.totype(os.linesep)
else:
self.newlinestr = self.totype(newlinestr)
self.indentlevel = 0
self.stack = []
self.needindent = 1
self.idlefunc = idlefunc
self.idlecounter = 0
self._writeraw('<?xml version="1.0" encoding="UTF-8"?>')
self.newline()
def __enter__(self):
return self
def __exit__(self, exception_type, exception_value, traceback):
self.close()
def close(self):
if self._closeStream:
self.file.close()
def write(self, string, indent=True):
"""Writes text."""
self._writeraw(escape(string), indent=indent)
def writecdata(self, string):
"""Writes text in a CDATA section."""
self._writeraw("<![CDATA[" + string + "]]>")
def write8bit(self, data, strip=False):
"""Writes a bytes() sequence into the XML, escaping
non-ASCII bytes. When this is read in xmlReader,
the original bytes can be recovered by encoding to
'latin-1'."""
self._writeraw(escape8bit(data), strip=strip)
def write_noindent(self, string):
"""Writes text without indentation."""
self._writeraw(escape(string), indent=False)
def _writeraw(self, data, indent=True, strip=False):
"""Writes bytes, possibly indented."""
if indent and self.needindent:
self.file.write(self.indentlevel * self.indentwhite)
self.needindent = 0
s = self.totype(data, encoding="utf_8")
if (strip):
s = s.strip()
self.file.write(s)
def newline(self):
self.file.write(self.newlinestr)
self.needindent = 1
idlecounter = self.idlecounter
if not idlecounter % 100 and self.idlefunc is not None:
self.idlefunc()
self.idlecounter = idlecounter + 1
def comment(self, data):
data = escape(data)
lines = data.split("\n")
self._writeraw("<!-- " + lines[0])
for line in lines[1:]:
self.newline()
self._writeraw(" " + line)
self._writeraw(" -->")
def simpletag(self, _TAG_, *args, **kwargs):
attrdata = self.stringifyattrs(*args, **kwargs)
data = "<%s%s/>" % (_TAG_, attrdata)
self._writeraw(data)
def begintag(self, _TAG_, *args, **kwargs):
attrdata = self.stringifyattrs(*args, **kwargs)
data = "<%s%s>" % (_TAG_, attrdata)
self._writeraw(data)
self.stack.append(_TAG_)
self.indent()
def endtag(self, _TAG_):
assert self.stack and self.stack[-1] == _TAG_, "nonmatching endtag"
del self.stack[-1]
self.dedent()
data = "</%s>" % _TAG_
self._writeraw(data)
def dumphex(self, data):
linelength = 16
hexlinelength = linelength * 2
chunksize = 8
for i in range(0, len(data), linelength):
hexline = hexStr(data[i:i+linelength])
line = ""
white = ""
for j in range(0, hexlinelength, chunksize):
line = line + white + hexline[j:j+chunksize]
white = " "
self._writeraw(line)
self.newline()
def indent(self):
self.indentlevel = self.indentlevel + 1
def dedent(self):
assert self.indentlevel > 0
self.indentlevel = self.indentlevel - 1
def stringifyattrs(self, *args, **kwargs):
if kwargs:
assert not args
attributes = sorted(kwargs.items())
elif args:
assert len(args) == 1
attributes = args[0]
else:
return ""
data = ""
for attr, value in attributes:
if not isinstance(value, (bytes, str)):
value = str(value)
data = data + ' %s="%s"' % (attr, escapeattr(value))
return data
def escape(data):
data = tostr(data, 'utf_8')
data = data.replace("&", "&amp;")
data = data.replace("<", "&lt;")
data = data.replace(">", "&gt;")
data = data.replace("\r", "&#13;")
return data
def escapeattr(data):
data = escape(data)
data = data.replace('"', "&quot;")
return data
def escape8bit(data):
"""Input is Unicode string."""
def escapechar(c):
n = ord(c)
if 32 <= n <= 127 and c not in "<&>":
return c
else:
return "&#" + repr(n) + ";"
return strjoin(map(escapechar, data.decode('latin-1')))
def hexStr(s):
h = string.hexdigits
r = ''
for c in s:
i = byteord(c)
r = r + h[(i >> 4) & 0xF] + h[i & 0xF]
return r