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.writeGlyph("A_", glyph) 163 dst.writeGlyph("i_j", glyph) 164 165 assert dst.contents == { 166 'a': 'a.glif', 167 'A': 'A_.glif', 168 'a_': 'a_000000000000001.glif', 169 'A_': 'A__.glif', 170 'i_j': 'i_j.glif', 171 } 172 173 # make sure filenames are unique even on case-insensitive filesystems 174 assert len({fileName.lower() for fileName in dst.contents.values()}) == 5 175 176 177class _Glyph: 178 pass 179 180 181class ReadWriteFuncTest: 182 183 def test_roundtrip(self): 184 glyph = _Glyph() 185 glyph.name = "a" 186 glyph.unicodes = [0x0061] 187 188 s1 = writeGlyphToString(glyph.name, glyph) 189 190 glyph2 = _Glyph() 191 readGlyphFromString(s1, glyph2) 192 assert glyph.__dict__ == glyph2.__dict__ 193 194 s2 = writeGlyphToString(glyph2.name, glyph2) 195 assert s1 == s2 196 197 def test_xml_declaration(self): 198 s = writeGlyphToString("a", _Glyph()) 199 assert s.startswith(XML_DECLARATION % "UTF-8") 200 201 def test_parse_xml_remove_comments(self): 202 s = b"""<?xml version='1.0' encoding='UTF-8'?> 203 <!-- a comment --> 204 <glyph name="A" format="2"> 205 <advance width="1290"/> 206 <unicode hex="0041"/> 207 <!-- another comment --> 208 </glyph> 209 """ 210 211 g = _Glyph() 212 readGlyphFromString(s, g) 213 214 assert g.name == "A" 215 assert g.width == 1290 216 assert g.unicodes == [0x0041] 217 218 def test_read_unsupported_format_version(self, caplog): 219 s = """<?xml version='1.0' encoding='utf-8'?> 220 <glyph name="A" format="0" formatMinor="0"> 221 <advance width="500"/> 222 <unicode hex="0041"/> 223 </glyph> 224 """ 225 226 with pytest.raises(UnsupportedGLIFFormat): 227 readGlyphFromString(s, _Glyph()) # validate=True by default 228 229 with pytest.raises(UnsupportedGLIFFormat): 230 readGlyphFromString(s, _Glyph(), validate=True) 231 232 caplog.clear() 233 with caplog.at_level(logging.WARNING, logger="fontTools.ufoLib.glifLib"): 234 readGlyphFromString(s, _Glyph(), validate=False) 235 236 assert len(caplog.records) == 1 237 assert "Unsupported GLIF format" in caplog.text 238 assert "Assuming the latest supported version" in caplog.text 239 240 def test_read_allow_format_versions(self): 241 s = """<?xml version='1.0' encoding='utf-8'?> 242 <glyph name="A" format="2"> 243 <advance width="500"/> 244 <unicode hex="0041"/> 245 </glyph> 246 """ 247 248 # these two calls are are equivalent 249 readGlyphFromString(s, _Glyph(), formatVersions=[1, 2]) 250 readGlyphFromString(s, _Glyph(), formatVersions=[(1, 0), (2, 0)]) 251 252 # if at least one supported formatVersion, unsupported ones are ignored 253 readGlyphFromString(s, _Glyph(), formatVersions=[(2, 0), (123, 456)]) 254 255 with pytest.raises( 256 ValueError, 257 match="None of the requested GLIF formatVersions are supported" 258 ): 259 readGlyphFromString(s, _Glyph(), formatVersions=[0, 2001]) 260 261 with pytest.raises(GlifLibError, match="Forbidden GLIF format version"): 262 readGlyphFromString(s, _Glyph(), formatVersions=[1]) 263 264 def test_read_ensure_x_y(self): 265 """Ensure that a proper GlifLibError is raised when point coordinates are 266 missing, regardless of validation setting.""" 267 268 s = """<?xml version='1.0' encoding='utf-8'?> 269 <glyph name="A" format="2"> 270 <outline> 271 <contour> 272 <point x="545" y="0" type="line"/> 273 <point x="638" type="line"/> 274 </contour> 275 </outline> 276 </glyph> 277 """ 278 pen = RecordingPointPen() 279 280 with pytest.raises(GlifLibError, match="Required y attribute"): 281 readGlyphFromString(s, _Glyph(), pen) 282 283 with pytest.raises(GlifLibError, match="Required y attribute"): 284 readGlyphFromString(s, _Glyph(), pen, validate=False) 285 286def test_GlyphSet_unsupported_ufoFormatVersion(tmp_path, caplog): 287 with pytest.raises(UnsupportedUFOFormat): 288 GlyphSet(tmp_path, ufoFormatVersion=0) 289 with pytest.raises(UnsupportedUFOFormat): 290 GlyphSet(tmp_path, ufoFormatVersion=(0, 1)) 291 292 293def test_GlyphSet_writeGlyph_formatVersion(tmp_path): 294 src = GlyphSet(GLYPHSETDIR) 295 dst = GlyphSet(tmp_path, ufoFormatVersion=(2, 0)) 296 glyph = src["A"] 297 298 # no explicit formatVersion passed: use the more recent GLIF formatVersion 299 # that is supported by given ufoFormatVersion (GLIF 1 for UFO 2) 300 dst.writeGlyph("A", glyph) 301 glif = dst.getGLIF("A") 302 assert b'format="1"' in glif 303 assert b'formatMinor' not in glif # omitted when 0 304 305 # explicit, unknown formatVersion 306 with pytest.raises(UnsupportedGLIFFormat): 307 dst.writeGlyph("A", glyph, formatVersion=(0, 0)) 308 309 # explicit, known formatVersion but unsupported by given ufoFormatVersion 310 with pytest.raises( 311 UnsupportedGLIFFormat, 312 match="Unsupported GLIF format version .*for UFO format version", 313 ): 314 dst.writeGlyph("A", glyph, formatVersion=(2, 0)) 315