1import gzip 2import io 3import struct 4 5from fontTools.misc import etree 6from fontTools.misc.testTools import getXML, parseXML 7from fontTools.ttLib import TTFont 8from fontTools.ttLib.tables.S_V_G_ import table_S_V_G_ 9 10import pytest 11 12 13def dump(table, ttFont=None): 14 print("\n".join(getXML(table.toXML, ttFont))) 15 16 17def compress(data: bytes) -> bytes: 18 buf = io.BytesIO() 19 with gzip.GzipFile(None, "w", fileobj=buf, mtime=0) as gz: 20 gz.write(data) 21 return buf.getvalue() 22 23 24def strip_xml_whitespace(xml_string): 25 def strip_or_none(text): 26 text = text.strip() if text else None 27 return text if text else None 28 29 tree = etree.fromstring(xml_string) 30 for e in tree.iter("*"): 31 e.text = strip_or_none(e.text) 32 e.tail = strip_or_none(e.tail) 33 return etree.tostring(tree, encoding="utf-8") 34 35 36SVG_DOCS = [ 37 strip_xml_whitespace(svg) 38 for svg in ( 39 b"""\ 40 <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"> 41 <defs> 42 <rect x="100" y="-200" width="300" height="400" id="p1"/> 43 </defs> 44 <g id="glyph1"> 45 <use xlink:href="#p1" fill="#red"/> 46 </g> 47 <g id="glyph2"> 48 <use xlink:href="#p1" fill="#blue"/> 49 </g> 50 <g id="glyph4"> 51 <use xlink:href="#p1" fill="#green"/> 52 </g> 53 </svg>""", 54 b"""\ 55 <svg xmlns="http://www.w3.org/2000/svg" version="1.1"> 56 <g id="glyph3"> 57 <path d="M0,0 L100,0 L50,100 Z" fill="#red"/> 58 <path d="M10,10 L110,10 L60,110 Z" fill="#blue"/> 59 <path d="M20,20 L120,20 L70,120 Z" fill="#green"/> 60 </g> 61 </svg>""", 62 ) 63] 64 65 66OTSVG_DATA = b"".join( 67 [ 68 # SVG table header 69 b"\x00\x00" # version (0) 70 b"\x00\x00\x00\x0a" # offset to SVGDocumentList (10) 71 b"\x00\x00\x00\x00" # reserved (0) 72 # SVGDocumentList 73 b"\x00\x03" # number of SVGDocumentRecords (3) 74 # SVGDocumentRecord[0] 75 b"\x00\x01" # startGlyphID (1) 76 b"\x00\x02" # endGlyphID (2) 77 b"\x00\x00\x00\x26" # svgDocOffset (2 + 12*3 == 38 == 0x26) 78 + struct.pack(">L", len(SVG_DOCS[0])) # svgDocLength 79 # SVGDocumentRecord[1] (compressed) 80 + b"\x00\x03" # startGlyphID (3) 81 b"\x00\x03" # endGlyphID (3) 82 + struct.pack(">L", 0x26 + len(SVG_DOCS[0])) # svgDocOffset 83 + struct.pack(">L", len(compress(SVG_DOCS[1]))) # svgDocLength 84 # SVGDocumentRecord[2] 85 + b"\x00\x04" # startGlyphID (4) 86 b"\x00\x04" # endGlyphID (4) 87 b"\x00\x00\x00\x26" # svgDocOffset (38); records 0 and 2 point to same SVG doc 88 + struct.pack(">L", len(SVG_DOCS[0])) # svgDocLength 89 ] 90 + [SVG_DOCS[0], compress(SVG_DOCS[1])] 91) 92 93OTSVG_TTX = [ 94 '<svgDoc endGlyphID="2" startGlyphID="1">', 95 f" <![CDATA[{SVG_DOCS[0].decode()}]]>", 96 "</svgDoc>", 97 '<svgDoc compressed="1" endGlyphID="3" startGlyphID="3">', 98 f" <![CDATA[{SVG_DOCS[1].decode()}]]>", 99 "</svgDoc>", 100 '<svgDoc endGlyphID="4" startGlyphID="4">', 101 f" <![CDATA[{SVG_DOCS[0].decode()}]]>", 102 "</svgDoc>", 103] 104 105 106@pytest.fixture 107def font(): 108 font = TTFont() 109 font.setGlyphOrder([".notdef"] + ["glyph%05d" % i for i in range(1, 30)]) 110 return font 111 112 113def test_decompile_and_compile(font): 114 table = table_S_V_G_() 115 table.decompile(OTSVG_DATA, font) 116 assert table.compile(font) == OTSVG_DATA 117 118 119def test_decompile_and_dump_ttx(font): 120 table = table_S_V_G_() 121 table.decompile(OTSVG_DATA, font) 122 123 dump(table, font) 124 assert getXML(table.toXML, font) == OTSVG_TTX 125 126 127def test_load_from_ttx_and_compile(font): 128 table = table_S_V_G_() 129 for name, attrs, content in parseXML(OTSVG_TTX): 130 table.fromXML(name, attrs, content, font) 131 assert table.compile(font) == OTSVG_DATA 132 133 134def test_round_trip_ttx(font): 135 table = table_S_V_G_() 136 for name, attrs, content in parseXML(OTSVG_TTX): 137 table.fromXML(name, attrs, content, font) 138 compiled = table.compile(font) 139 140 table = table_S_V_G_() 141 table.decompile(compiled, font) 142 assert getXML(table.toXML, font) == OTSVG_TTX 143 144 145def test_unpack_svg_doc_as_3_tuple(): 146 # test that the legacy docList as list of 3-tuples interface still works 147 # even after the new SVGDocument class with extra `compressed` attribute 148 # was added 149 table = table_S_V_G_() 150 table.decompile(OTSVG_DATA, font) 151 152 for doc, compressed in zip(table.docList, (False, True, False)): 153 assert len(doc) == 3 154 data, startGID, endGID = doc 155 assert doc.data == data 156 assert doc.startGlyphID == startGID 157 assert doc.endGlyphID == endGID 158 assert doc.compressed == compressed 159