blob: 8f48168d882bc49d4474597a867bc37c8d124a50 [file] [log] [blame]
import logging
import os
import tempfile
import shutil
import unittest
from pathlib import Path
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])
def testReadGlyphInvalidXml(self):
"""Test that calling readGlyph() to read a .glif with invalid XML raises
a library error, instead of an exception from the XML dependency that is
used internally. In addition, check that the raised exception describes
the glyph by name and gives the location of the broken .glif file."""
# Create a glyph set with three empty glyphs.
glyph_set = GlyphSet(self.dstDir)
glyph_set.writeGlyph("a", _Glyph())
glyph_set.writeGlyph("b", _Glyph())
glyph_set.writeGlyph("c", _Glyph())
# Corrupt the XML of /c.
invalid_xml = b"<abc></def>"
Path(self.dstDir, glyph_set.contents["c"]).write_bytes(invalid_xml)
# Confirm that reading /a and /b is fine...
glyph_set.readGlyph("a", _Glyph())
glyph_set.readGlyph("b", _Glyph())
# ...but that reading /c raises a descriptive library error.
expected_message = (
r"GLIF contains invalid XML\.\n"
r"The issue is in glyph 'c', located in '.*c\.glif.*\."
)
with pytest.raises(GlifLibError, match=expected_message):
glyph_set.readGlyph("c", _Glyph())
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.deleteGlyph("a_")
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_invalid_xml(self):
"""Test that calling readGlyphFromString() with invalid XML raises a
library error, instead of an exception from the XML dependency that is
used internally."""
invalid_xml = b"<abc></def>"
empty_glyph = _Glyph()
with pytest.raises(GlifLibError, match="GLIF contains invalid XML"):
readGlyphFromString(invalid_xml, empty_glyph)
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))