You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
315 lines
9.8 KiB
315 lines
9.8 KiB
import logging
|
|
import os
|
|
import tempfile
|
|
import shutil
|
|
import unittest
|
|
from io import open
|
|
from .testSupport import getDemoFontGlyphSetPath
|
|
from fontTools.ufoLib.glifLib import (
|
|
GlyphSet, glyphNameToFileName, readGlyphFromString, writeGlyphToString,
|
|
)
|
|
from fontTools.ufoLib.errors import GlifLibError, UnsupportedGLIFFormat, UnsupportedUFOFormat
|
|
from fontTools.misc.etree import XML_DECLARATION
|
|
from fontTools.pens.recordingPen import RecordingPointPen
|
|
import pytest
|
|
|
|
GLYPHSETDIR = getDemoFontGlyphSetPath()
|
|
|
|
|
|
class GlyphSetTests(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
self.dstDir = tempfile.mktemp()
|
|
os.mkdir(self.dstDir)
|
|
|
|
def tearDown(self):
|
|
shutil.rmtree(self.dstDir)
|
|
|
|
def testRoundTrip(self):
|
|
import difflib
|
|
srcDir = GLYPHSETDIR
|
|
dstDir = self.dstDir
|
|
src = GlyphSet(srcDir, ufoFormatVersion=2, validateRead=True, validateWrite=True)
|
|
dst = GlyphSet(dstDir, ufoFormatVersion=2, validateRead=True, validateWrite=True)
|
|
for glyphName in src.keys():
|
|
g = src[glyphName]
|
|
g.drawPoints(None) # load attrs
|
|
dst.writeGlyph(glyphName, g, g.drawPoints)
|
|
# compare raw file data:
|
|
for glyphName in sorted(src.keys()):
|
|
fileName = src.contents[glyphName]
|
|
with open(os.path.join(srcDir, fileName), "r") as f:
|
|
org = f.read()
|
|
with open(os.path.join(dstDir, fileName), "r") as f:
|
|
new = f.read()
|
|
added = []
|
|
removed = []
|
|
for line in difflib.unified_diff(
|
|
org.split("\n"), new.split("\n")):
|
|
if line.startswith("+ "):
|
|
added.append(line[1:])
|
|
elif line.startswith("- "):
|
|
removed.append(line[1:])
|
|
self.assertEqual(
|
|
added, removed,
|
|
"%s.glif file differs after round tripping" % glyphName)
|
|
|
|
def testContentsExist(self):
|
|
with self.assertRaises(GlifLibError):
|
|
GlyphSet(
|
|
self.dstDir,
|
|
ufoFormatVersion=2,
|
|
validateRead=True,
|
|
validateWrite=True,
|
|
expectContentsFile=True,
|
|
)
|
|
|
|
def testRebuildContents(self):
|
|
gset = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True)
|
|
contents = gset.contents
|
|
gset.rebuildContents()
|
|
self.assertEqual(contents, gset.contents)
|
|
|
|
def testReverseContents(self):
|
|
gset = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True)
|
|
d = {}
|
|
for k, v in gset.getReverseContents().items():
|
|
d[v] = k
|
|
org = {}
|
|
for k, v in gset.contents.items():
|
|
org[k] = v.lower()
|
|
self.assertEqual(d, org)
|
|
|
|
def testReverseContents2(self):
|
|
src = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True)
|
|
dst = GlyphSet(self.dstDir, validateRead=True, validateWrite=True)
|
|
dstMap = dst.getReverseContents()
|
|
self.assertEqual(dstMap, {})
|
|
for glyphName in src.keys():
|
|
g = src[glyphName]
|
|
g.drawPoints(None) # load attrs
|
|
dst.writeGlyph(glyphName, g, g.drawPoints)
|
|
self.assertNotEqual(dstMap, {})
|
|
srcMap = dict(src.getReverseContents()) # copy
|
|
self.assertEqual(dstMap, srcMap)
|
|
del srcMap["a.glif"]
|
|
dst.deleteGlyph("a")
|
|
self.assertEqual(dstMap, srcMap)
|
|
|
|
def testCustomFileNamingScheme(self):
|
|
def myGlyphNameToFileName(glyphName, glyphSet):
|
|
return "prefix" + glyphNameToFileName(glyphName, glyphSet)
|
|
src = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True)
|
|
dst = GlyphSet(self.dstDir, myGlyphNameToFileName, validateRead=True, validateWrite=True)
|
|
for glyphName in src.keys():
|
|
g = src[glyphName]
|
|
g.drawPoints(None) # load attrs
|
|
dst.writeGlyph(glyphName, g, g.drawPoints)
|
|
d = {}
|
|
for k, v in src.contents.items():
|
|
d[k] = "prefix" + v
|
|
self.assertEqual(d, dst.contents)
|
|
|
|
def testGetUnicodes(self):
|
|
src = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True)
|
|
unicodes = src.getUnicodes()
|
|
for glyphName in src.keys():
|
|
g = src[glyphName]
|
|
g.drawPoints(None) # load attrs
|
|
if not hasattr(g, "unicodes"):
|
|
self.assertEqual(unicodes[glyphName], [])
|
|
else:
|
|
self.assertEqual(g.unicodes, unicodes[glyphName])
|
|
|
|
|
|
class FileNameTest:
|
|
|
|
def test_default_file_name_scheme(self):
|
|
assert glyphNameToFileName("a", None) == "a.glif"
|
|
assert glyphNameToFileName("A", None) == "A_.glif"
|
|
assert glyphNameToFileName("Aring", None) == "A_ring.glif"
|
|
assert glyphNameToFileName("F_A_B", None) == "F__A__B_.glif"
|
|
assert glyphNameToFileName("A.alt", None) == "A_.alt.glif"
|
|
assert glyphNameToFileName("A.Alt", None) == "A_.A_lt.glif"
|
|
assert glyphNameToFileName(".notdef", None) == "_notdef.glif"
|
|
assert glyphNameToFileName("T_H", None) =="T__H_.glif"
|
|
assert glyphNameToFileName("T_h", None) =="T__h.glif"
|
|
assert glyphNameToFileName("t_h", None) =="t_h.glif"
|
|
assert glyphNameToFileName("F_F_I", None) == "F__F__I_.glif"
|
|
assert glyphNameToFileName("f_f_i", None) == "f_f_i.glif"
|
|
assert glyphNameToFileName("AE", None) == "A_E_.glif"
|
|
assert glyphNameToFileName("Ae", None) == "A_e.glif"
|
|
assert glyphNameToFileName("ae", None) == "ae.glif"
|
|
assert glyphNameToFileName("aE", None) == "aE_.glif"
|
|
assert glyphNameToFileName("a.alt", None) == "a.alt.glif"
|
|
assert glyphNameToFileName("A.aLt", None) == "A_.aL_t.glif"
|
|
assert glyphNameToFileName("A.alT", None) == "A_.alT_.glif"
|
|
assert glyphNameToFileName("Aacute_V.swash", None) == "A_acute_V_.swash.glif"
|
|
assert glyphNameToFileName(".notdef", None) == "_notdef.glif"
|
|
assert glyphNameToFileName("con", None) == "_con.glif"
|
|
assert glyphNameToFileName("CON", None) == "C_O_N_.glif"
|
|
assert glyphNameToFileName("con.alt", None) == "_con.alt.glif"
|
|
assert glyphNameToFileName("alt.con", None) == "alt._con.glif"
|
|
|
|
def test_conflicting_case_insensitive_file_names(self, tmp_path):
|
|
src = GlyphSet(GLYPHSETDIR)
|
|
dst = GlyphSet(tmp_path)
|
|
glyph = src["a"]
|
|
|
|
dst.writeGlyph("a", glyph)
|
|
dst.writeGlyph("A", glyph)
|
|
dst.writeGlyph("a_", glyph)
|
|
dst.writeGlyph("A_", glyph)
|
|
dst.writeGlyph("i_j", glyph)
|
|
|
|
assert dst.contents == {
|
|
'a': 'a.glif',
|
|
'A': 'A_.glif',
|
|
'a_': 'a_000000000000001.glif',
|
|
'A_': 'A__.glif',
|
|
'i_j': 'i_j.glif',
|
|
}
|
|
|
|
# make sure filenames are unique even on case-insensitive filesystems
|
|
assert len({fileName.lower() for fileName in dst.contents.values()}) == 5
|
|
|
|
|
|
class _Glyph:
|
|
pass
|
|
|
|
|
|
class ReadWriteFuncTest:
|
|
|
|
def test_roundtrip(self):
|
|
glyph = _Glyph()
|
|
glyph.name = "a"
|
|
glyph.unicodes = [0x0061]
|
|
|
|
s1 = writeGlyphToString(glyph.name, glyph)
|
|
|
|
glyph2 = _Glyph()
|
|
readGlyphFromString(s1, glyph2)
|
|
assert glyph.__dict__ == glyph2.__dict__
|
|
|
|
s2 = writeGlyphToString(glyph2.name, glyph2)
|
|
assert s1 == s2
|
|
|
|
def test_xml_declaration(self):
|
|
s = writeGlyphToString("a", _Glyph())
|
|
assert s.startswith(XML_DECLARATION % "UTF-8")
|
|
|
|
def test_parse_xml_remove_comments(self):
|
|
s = b"""<?xml version='1.0' encoding='UTF-8'?>
|
|
<!-- a comment -->
|
|
<glyph name="A" format="2">
|
|
<advance width="1290"/>
|
|
<unicode hex="0041"/>
|
|
<!-- another comment -->
|
|
</glyph>
|
|
"""
|
|
|
|
g = _Glyph()
|
|
readGlyphFromString(s, g)
|
|
|
|
assert g.name == "A"
|
|
assert g.width == 1290
|
|
assert g.unicodes == [0x0041]
|
|
|
|
def test_read_unsupported_format_version(self, caplog):
|
|
s = """<?xml version='1.0' encoding='utf-8'?>
|
|
<glyph name="A" format="0" formatMinor="0">
|
|
<advance width="500"/>
|
|
<unicode hex="0041"/>
|
|
</glyph>
|
|
"""
|
|
|
|
with pytest.raises(UnsupportedGLIFFormat):
|
|
readGlyphFromString(s, _Glyph()) # validate=True by default
|
|
|
|
with pytest.raises(UnsupportedGLIFFormat):
|
|
readGlyphFromString(s, _Glyph(), validate=True)
|
|
|
|
caplog.clear()
|
|
with caplog.at_level(logging.WARNING, logger="fontTools.ufoLib.glifLib"):
|
|
readGlyphFromString(s, _Glyph(), validate=False)
|
|
|
|
assert len(caplog.records) == 1
|
|
assert "Unsupported GLIF format" in caplog.text
|
|
assert "Assuming the latest supported version" in caplog.text
|
|
|
|
def test_read_allow_format_versions(self):
|
|
s = """<?xml version='1.0' encoding='utf-8'?>
|
|
<glyph name="A" format="2">
|
|
<advance width="500"/>
|
|
<unicode hex="0041"/>
|
|
</glyph>
|
|
"""
|
|
|
|
# these two calls are are equivalent
|
|
readGlyphFromString(s, _Glyph(), formatVersions=[1, 2])
|
|
readGlyphFromString(s, _Glyph(), formatVersions=[(1, 0), (2, 0)])
|
|
|
|
# if at least one supported formatVersion, unsupported ones are ignored
|
|
readGlyphFromString(s, _Glyph(), formatVersions=[(2, 0), (123, 456)])
|
|
|
|
with pytest.raises(
|
|
ValueError,
|
|
match="None of the requested GLIF formatVersions are supported"
|
|
):
|
|
readGlyphFromString(s, _Glyph(), formatVersions=[0, 2001])
|
|
|
|
with pytest.raises(GlifLibError, match="Forbidden GLIF format version"):
|
|
readGlyphFromString(s, _Glyph(), formatVersions=[1])
|
|
|
|
def test_read_ensure_x_y(self):
|
|
"""Ensure that a proper GlifLibError is raised when point coordinates are
|
|
missing, regardless of validation setting."""
|
|
|
|
s = """<?xml version='1.0' encoding='utf-8'?>
|
|
<glyph name="A" format="2">
|
|
<outline>
|
|
<contour>
|
|
<point x="545" y="0" type="line"/>
|
|
<point x="638" type="line"/>
|
|
</contour>
|
|
</outline>
|
|
</glyph>
|
|
"""
|
|
pen = RecordingPointPen()
|
|
|
|
with pytest.raises(GlifLibError, match="Required y attribute"):
|
|
readGlyphFromString(s, _Glyph(), pen)
|
|
|
|
with pytest.raises(GlifLibError, match="Required y attribute"):
|
|
readGlyphFromString(s, _Glyph(), pen, validate=False)
|
|
|
|
def test_GlyphSet_unsupported_ufoFormatVersion(tmp_path, caplog):
|
|
with pytest.raises(UnsupportedUFOFormat):
|
|
GlyphSet(tmp_path, ufoFormatVersion=0)
|
|
with pytest.raises(UnsupportedUFOFormat):
|
|
GlyphSet(tmp_path, ufoFormatVersion=(0, 1))
|
|
|
|
|
|
def test_GlyphSet_writeGlyph_formatVersion(tmp_path):
|
|
src = GlyphSet(GLYPHSETDIR)
|
|
dst = GlyphSet(tmp_path, ufoFormatVersion=(2, 0))
|
|
glyph = src["A"]
|
|
|
|
# no explicit formatVersion passed: use the more recent GLIF formatVersion
|
|
# that is supported by given ufoFormatVersion (GLIF 1 for UFO 2)
|
|
dst.writeGlyph("A", glyph)
|
|
glif = dst.getGLIF("A")
|
|
assert b'format="1"' in glif
|
|
assert b'formatMinor' not in glif # omitted when 0
|
|
|
|
# explicit, unknown formatVersion
|
|
with pytest.raises(UnsupportedGLIFFormat):
|
|
dst.writeGlyph("A", glyph, formatVersion=(0, 0))
|
|
|
|
# explicit, known formatVersion but unsupported by given ufoFormatVersion
|
|
with pytest.raises(
|
|
UnsupportedGLIFFormat,
|
|
match="Unsupported GLIF format version .*for UFO format version",
|
|
):
|
|
dst.writeGlyph("A", glyph, formatVersion=(2, 0))
|