1import logging 2import os 3import tempfile 4import shutil 5import unittest 6from io import open 7from .testSupport import getDemoFontGlyphSetPath 8from fontTools.ufoLib.glifLib import ( 9 GlyphSet, glyphNameToFileName, readGlyphFromString, writeGlyphToString, 10) 11from fontTools.ufoLib.errors import GlifLibError, UnsupportedGLIFFormat, UnsupportedUFOFormat 12from fontTools.misc.etree import XML_DECLARATION 13from fontTools.pens.recordingPen import RecordingPointPen 14import pytest 15 16GLYPHSETDIR = getDemoFontGlyphSetPath() 17 18 19class GlyphSetTests(unittest.TestCase): 20 21 def setUp(self): 22 self.dstDir = tempfile.mktemp() 23 os.mkdir(self.dstDir) 24 25 def tearDown(self): 26 shutil.rmtree(self.dstDir) 27 28 def testRoundTrip(self): 29 import difflib 30 srcDir = GLYPHSETDIR 31 dstDir = self.dstDir 32 src = GlyphSet(srcDir, ufoFormatVersion=2, validateRead=True, validateWrite=True) 33 dst = GlyphSet(dstDir, ufoFormatVersion=2, validateRead=True, validateWrite=True) 34 for glyphName in src.keys(): 35 g = src[glyphName] 36 g.drawPoints(None) # load attrs 37 dst.writeGlyph(glyphName, g, g.drawPoints) 38 # compare raw file data: 39 for glyphName in sorted(src.keys()): 40 fileName = src.contents[glyphName] 41 with open(os.path.join(srcDir, fileName), "r") as f: 42 org = f.read() 43 with open(os.path.join(dstDir, fileName), "r") as f: 44 new = f.read() 45 added = [] 46 removed = [] 47 for line in difflib.unified_diff( 48 org.split("\n"), new.split("\n")): 49 if line.startswith("+ "): 50 added.append(line[1:]) 51 elif line.startswith("- "): 52 removed.append(line[1:]) 53 self.assertEqual( 54 added, removed, 55 "%s.glif file differs after round tripping" % glyphName) 56 57 def testContentsExist(self): 58 with self.assertRaises(GlifLibError): 59 GlyphSet( 60 self.dstDir, 61 ufoFormatVersion=2, 62 validateRead=True, 63 validateWrite=True, 64 expectContentsFile=True, 65 ) 66 67 def testRebuildContents(self): 68 gset = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True) 69 contents = gset.contents 70 gset.rebuildContents() 71 self.assertEqual(contents, gset.contents) 72 73 def testReverseContents(self): 74 gset = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True) 75 d = {} 76 for k, v in gset.getReverseContents().items(): 77 d[v] = k 78 org = {} 79 for k, v in gset.contents.items(): 80 org[k] = v.lower() 81 self.assertEqual(d, org) 82 83 def testReverseContents2(self): 84 src = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True) 85 dst = GlyphSet(self.dstDir, validateRead=True, validateWrite=True) 86 dstMap = dst.getReverseContents() 87 self.assertEqual(dstMap, {}) 88 for glyphName in src.keys(): 89 g = src[glyphName] 90 g.drawPoints(None) # load attrs 91 dst.writeGlyph(glyphName, g, g.drawPoints) 92 self.assertNotEqual(dstMap, {}) 93 srcMap = dict(src.getReverseContents()) # copy 94 self.assertEqual(dstMap, srcMap) 95 del srcMap["a.glif"] 96 dst.deleteGlyph("a") 97 self.assertEqual(dstMap, srcMap) 98 99 def testCustomFileNamingScheme(self): 100 def myGlyphNameToFileName(glyphName, glyphSet): 101 return "prefix" + glyphNameToFileName(glyphName, glyphSet) 102 src = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True) 103 dst = GlyphSet(self.dstDir, myGlyphNameToFileName, validateRead=True, validateWrite=True) 104 for glyphName in src.keys(): 105 g = src[glyphName] 106 g.drawPoints(None) # load attrs 107 dst.writeGlyph(glyphName, g, g.drawPoints) 108 d = {} 109 for k, v in src.contents.items(): 110 d[k] = "prefix" + v 111 self.assertEqual(d, dst.contents) 112 113 def testGetUnicodes(self): 114 src = GlyphSet(GLYPHSETDIR, validateRead=True, validateWrite=True) 115 unicodes = src.getUnicodes() 116 for glyphName in src.keys(): 117 g = src[glyphName] 118 g.drawPoints(None) # load attrs 119 if not hasattr(g, "unicodes"): 120 self.assertEqual(unicodes[glyphName], []) 121 else: 122 self.assertEqual(g.unicodes, unicodes[glyphName]) 123 124 125class FileNameTest: 126 127 def test_default_file_name_scheme(self): 128 assert glyphNameToFileName("a", None) == "a.glif" 129 assert glyphNameToFileName("A", None) == "A_.glif" 130 assert glyphNameToFileName("Aring", None) == "A_ring.glif" 131 assert glyphNameToFileName("F_A_B", None) == "F__A__B_.glif" 132 assert glyphNameToFileName("A.alt", None) == "A_.alt.glif" 133 assert glyphNameToFileName("A.Alt", None) == "A_.A_lt.glif" 134 assert glyphNameToFileName(".notdef", None) == "_notdef.glif" 135 assert glyphNameToFileName("T_H", None) =="T__H_.glif" 136 assert glyphNameToFileName("T_h", None) =="T__h.glif" 137 assert glyphNameToFileName("t_h", None) =="t_h.glif" 138 assert glyphNameToFileName("F_F_I", None) == "F__F__I_.glif" 139 assert glyphNameToFileName("f_f_i", None) == "f_f_i.glif" 140 assert glyphNameToFileName("AE", None) == "A_E_.glif" 141 assert glyphNameToFileName("Ae", None) == "A_e.glif" 142 assert glyphNameToFileName("ae", None) == "ae.glif" 143 assert glyphNameToFileName("aE", None) == "aE_.glif" 144 assert glyphNameToFileName("a.alt", None) == "a.alt.glif" 145 assert glyphNameToFileName("A.aLt", None) == "A_.aL_t.glif" 146 assert glyphNameToFileName("A.alT", None) == "A_.alT_.glif" 147 assert glyphNameToFileName("Aacute_V.swash", None) == "A_acute_V_.swash.glif" 148 assert glyphNameToFileName(".notdef", None) == "_notdef.glif" 149 assert glyphNameToFileName("con", None) == "_con.glif" 150 assert glyphNameToFileName("CON", None) == "C_O_N_.glif" 151 assert glyphNameToFileName("con.alt", None) == "_con.alt.glif" 152 assert glyphNameToFileName("alt.con", None) == "alt._con.glif" 153 154 def test_conflicting_case_insensitive_file_names(self, tmp_path): 155 src = GlyphSet(GLYPHSETDIR) 156 dst = GlyphSet(tmp_path) 157 glyph = src["a"] 158 159 dst.writeGlyph("a", glyph) 160 dst.writeGlyph("A", glyph) 161 dst.writeGlyph("a_", glyph) 162 dst.deleteGlyph("a_") 163 dst.writeGlyph("a_", glyph) 164 dst.writeGlyph("A_", glyph) 165 dst.writeGlyph("i_j", glyph) 166 167 assert dst.contents == { 168 'a': 'a.glif', 169 'A': 'A_.glif', 170 'a_': 'a_000000000000001.glif', 171 'A_': 'A__.glif', 172 'i_j': 'i_j.glif', 173 } 174 175 # make sure filenames are unique even on case-insensitive filesystems 176 assert len({fileName.lower() for fileName in dst.contents.values()}) == 5 177 178 179class _Glyph: 180 pass 181 182 183class ReadWriteFuncTest: 184 185 def test_roundtrip(self): 186 glyph = _Glyph() 187 glyph.name = "a" 188 glyph.unicodes = [0x0061] 189 190 s1 = writeGlyphToString(glyph.name, glyph) 191 192 glyph2 = _Glyph() 193 readGlyphFromString(s1, glyph2) 194 assert glyph.__dict__ == glyph2.__dict__ 195 196 s2 = writeGlyphToString(glyph2.name, glyph2) 197 assert s1 == s2 198 199 def test_xml_declaration(self): 200 s = writeGlyphToString("a", _Glyph()) 201 assert s.startswith(XML_DECLARATION % "UTF-8") 202 203 def test_parse_xml_remove_comments(self): 204 s = b"""<?xml version='1.0' encoding='UTF-8'?> 205 <!-- a comment --> 206 <glyph name="A" format="2"> 207 <advance width="1290"/> 208 <unicode hex="0041"/> 209 <!-- another comment --> 210 </glyph> 211 """ 212 213 g = _Glyph() 214 readGlyphFromString(s, g) 215 216 assert g.name == "A" 217 assert g.width == 1290 218 assert g.unicodes == [0x0041] 219 220 def test_read_unsupported_format_version(self, caplog): 221 s = """<?xml version='1.0' encoding='utf-8'?> 222 <glyph name="A" format="0" formatMinor="0"> 223 <advance width="500"/> 224 <unicode hex="0041"/> 225 </glyph> 226 """ 227 228 with pytest.raises(UnsupportedGLIFFormat): 229 readGlyphFromString(s, _Glyph()) # validate=True by default 230 231 with pytest.raises(UnsupportedGLIFFormat): 232 readGlyphFromString(s, _Glyph(), validate=True) 233 234 caplog.clear() 235 with caplog.at_level(logging.WARNING, logger="fontTools.ufoLib.glifLib"): 236 readGlyphFromString(s, _Glyph(), validate=False) 237 238 assert len(caplog.records) == 1 239 assert "Unsupported GLIF format" in caplog.text 240 assert "Assuming the latest supported version" in caplog.text 241 242 def test_read_allow_format_versions(self): 243 s = """<?xml version='1.0' encoding='utf-8'?> 244 <glyph name="A" format="2"> 245 <advance width="500"/> 246 <unicode hex="0041"/> 247 </glyph> 248 """ 249 250 # these two calls are are equivalent 251 readGlyphFromString(s, _Glyph(), formatVersions=[1, 2]) 252 readGlyphFromString(s, _Glyph(), formatVersions=[(1, 0), (2, 0)]) 253 254 # if at least one supported formatVersion, unsupported ones are ignored 255 readGlyphFromString(s, _Glyph(), formatVersions=[(2, 0), (123, 456)]) 256 257 with pytest.raises( 258 ValueError, 259 match="None of the requested GLIF formatVersions are supported" 260 ): 261 readGlyphFromString(s, _Glyph(), formatVersions=[0, 2001]) 262 263 with pytest.raises(GlifLibError, match="Forbidden GLIF format version"): 264 readGlyphFromString(s, _Glyph(), formatVersions=[1]) 265 266 def test_read_ensure_x_y(self): 267 """Ensure that a proper GlifLibError is raised when point coordinates are 268 missing, regardless of validation setting.""" 269 270 s = """<?xml version='1.0' encoding='utf-8'?> 271 <glyph name="A" format="2"> 272 <outline> 273 <contour> 274 <point x="545" y="0" type="line"/> 275 <point x="638" type="line"/> 276 </contour> 277 </outline> 278 </glyph> 279 """ 280 pen = RecordingPointPen() 281 282 with pytest.raises(GlifLibError, match="Required y attribute"): 283 readGlyphFromString(s, _Glyph(), pen) 284 285 with pytest.raises(GlifLibError, match="Required y attribute"): 286 readGlyphFromString(s, _Glyph(), pen, validate=False) 287 288def test_GlyphSet_unsupported_ufoFormatVersion(tmp_path, caplog): 289 with pytest.raises(UnsupportedUFOFormat): 290 GlyphSet(tmp_path, ufoFormatVersion=0) 291 with pytest.raises(UnsupportedUFOFormat): 292 GlyphSet(tmp_path, ufoFormatVersion=(0, 1)) 293 294 295def test_GlyphSet_writeGlyph_formatVersion(tmp_path): 296 src = GlyphSet(GLYPHSETDIR) 297 dst = GlyphSet(tmp_path, ufoFormatVersion=(2, 0)) 298 glyph = src["A"] 299 300 # no explicit formatVersion passed: use the more recent GLIF formatVersion 301 # that is supported by given ufoFormatVersion (GLIF 1 for UFO 2) 302 dst.writeGlyph("A", glyph) 303 glif = dst.getGLIF("A") 304 assert b'format="1"' in glif 305 assert b'formatMinor' not in glif # omitted when 0 306 307 # explicit, unknown formatVersion 308 with pytest.raises(UnsupportedGLIFFormat): 309 dst.writeGlyph("A", glyph, formatVersion=(0, 0)) 310 311 # explicit, known formatVersion but unsupported by given ufoFormatVersion 312 with pytest.raises( 313 UnsupportedGLIFFormat, 314 match="Unsupported GLIF format version .*for UFO format version", 315 ): 316 dst.writeGlyph("A", glyph, formatVersion=(2, 0)) 317