From 8b3c57bdcbbbd9cb1dc98546b567a138207b7638 Mon Sep 17 00:00:00 2001 From: Haibo Huang Date: Tue, 3 Jul 2018 17:43:11 -0700 Subject: [PATCH] Upgrade fonttools from 2.4 to 3.28.0 1. Add METADATA. 2. Run tools/external_updater/updater.sh update fonttools Test: m checkbuild Change-Id: Iab9e8c5da04f4c06347a924b4cea04f743f274c3 --- .appveyor.yml | 62 + .codecov.yml | 5 + .coveragerc | 34 + .travis.yml | 104 + .travis/after_success.sh | 11 + .travis/before_deploy.sh | 15 + .travis/before_install.sh | 6 + .travis/install.sh | 33 + .travis/run.sh | 10 + Doc/Makefile | 20 + Doc/changes.txt | 161 - Doc/documentation.html | 104 - Doc/install.txt | 114 - Doc/make.bat | 36 + Doc/{ => man/man1}/ttx.1 | 4 +- Doc/source/afmLib.rst | 7 + Doc/source/agl.rst | 7 + Doc/source/cffLib.rst | 7 + Doc/source/conf.py | 156 + Doc/source/designspaceLib/index.rst | 17 + Doc/source/designspaceLib/readme.rst | 1052 +++ Doc/source/designspaceLib/scripting.rst | 253 + Doc/source/encodings.rst | 14 + Doc/source/feaLib.rst | 43 + Doc/source/index.rst | 29 + Doc/source/inspect.rst | 7 + Doc/source/merge.rst | 7 + Doc/source/misc/arrayTools.rst | 7 + Doc/source/misc/bezierTools.rst | 7 + Doc/source/misc/classifyTools.rst | 7 + Doc/source/misc/eexec.rst | 7 + Doc/source/misc/encodingTools.rst | 7 + Doc/source/misc/fixedTools.rst | 7 + Doc/source/misc/index.rst | 23 + Doc/source/misc/loggingTools.rst | 7 + Doc/source/misc/psCharStrings.rst | 7 + Doc/source/misc/sstruct.rst | 7 + Doc/source/misc/testTools.rst | 7 + Doc/source/misc/textTools.rst | 7 + Doc/source/misc/timeTools.rst | 7 + Doc/source/misc/transform.rst | 7 + Doc/source/misc/xmlReader.rst | 7 + Doc/source/misc/xmlWriter.rst | 7 + Doc/source/pens/areaPen.rst | 7 + Doc/source/pens/basePen.rst | 7 + Doc/source/pens/boundsPen.rst | 7 + Doc/source/pens/filterPen.rst | 7 + Doc/source/pens/index.rst | 18 + Doc/source/pens/perimeterPen.rst | 7 + Doc/source/pens/pointInsidePen.rst | 7 + Doc/source/pens/recordingPen.rst | 7 + Doc/source/pens/statisticsPen.rst | 7 + Doc/source/pens/t2CharStringPen.rst | 7 + Doc/source/pens/teePen.rst | 7 + Doc/source/pens/transformPen.rst | 7 + Doc/source/subset.rst | 7 + Doc/source/t1Lib.rst | 7 + Doc/source/ttLib/index.rst | 15 + Doc/source/ttLib/macUtils.rst | 7 + Doc/source/ttLib/sfnt.rst | 7 + Doc/source/ttLib/tables.rst | 506 ++ Doc/source/ttLib/woff2.rst | 7 + Doc/source/ttx.rst | 7 + Doc/source/varLib/designspace.rst | 7 + Doc/source/varLib/index.rst | 17 + Doc/source/varLib/interpolatable.rst | 7 + Doc/source/varLib/interpolate_layout.rst | 7 + Doc/source/varLib/merger.rst | 7 + Doc/source/varLib/models.rst | 7 + Doc/source/varLib/mutator.rst | 7 + Doc/source/voltLib.rst | 35 + LICENSE | 21 + LICENSE.external | 148 + LICENSE.txt | 25 - Lib/fontTools/Android.bp | 7 +- Lib/fontTools/__init__.py | 11 +- Lib/fontTools/__main__.py | 33 + Lib/fontTools/afmLib.py | 136 +- Lib/fontTools/agl.py | 152 +- .../{cffLib.py => cffLib/__init__.py} | 1875 ++-- Lib/fontTools/cffLib/specializer.py | 551 ++ Lib/fontTools/cffLib/width.py | 163 + Lib/fontTools/designspaceLib/__init__.py | 1252 +++ Lib/fontTools/encodings/MacRoman.py | 70 +- Lib/fontTools/encodings/StandardEncoding.py | 3 + Lib/fontTools/encodings/__init__.py | 7 +- Lib/fontTools/encodings/codecs.py | 135 + Lib/fontTools/feaLib/__init__.py | 4 + Lib/fontTools/feaLib/__main__.py | 42 + Lib/fontTools/feaLib/ast.py | 1346 +++ Lib/fontTools/feaLib/builder.py | 1588 ++++ Lib/fontTools/feaLib/error.py | 20 + Lib/fontTools/feaLib/lexer.py | 261 + Lib/fontTools/feaLib/parser.py | 1694 ++++ Lib/fontTools/inspect.py | 24 +- Lib/fontTools/merge.py | 762 +- Lib/fontTools/misc/__init__.py | 7 +- Lib/fontTools/misc/arrayTools.py | 147 +- Lib/fontTools/misc/bezierTools.py | 355 +- Lib/fontTools/misc/classifyTools.py | 173 + Lib/fontTools/misc/cliTools.py | 26 + Lib/fontTools/misc/eexec.py | 34 +- Lib/fontTools/misc/encodingTools.py | 73 + Lib/fontTools/misc/filenames.py | 224 + Lib/fontTools/misc/fixedTools.py | 102 +- Lib/fontTools/misc/homeResFile.py | 96 - Lib/fontTools/misc/loggingTools.py | 593 ++ Lib/fontTools/misc/macCreatorType.py | 29 +- Lib/fontTools/misc/macRes.py | 233 + Lib/fontTools/misc/psCharStrings.py | 1081 ++- Lib/fontTools/misc/psLib.py | 159 +- Lib/fontTools/misc/psOperators.py | 123 +- Lib/fontTools/misc/py23.py | 457 +- Lib/fontTools/misc/sstruct.py | 57 +- Lib/fontTools/misc/symfont.py | 196 + Lib/fontTools/misc/testTools.py | 183 + Lib/fontTools/misc/textTools.py | 32 +- Lib/fontTools/misc/timeTools.py | 64 + Lib/fontTools/misc/transform.py | 44 +- Lib/fontTools/misc/xmlReader.py | 112 +- Lib/fontTools/misc/xmlWriter.py | 100 +- Lib/fontTools/mtiLib/__init__.py | 1196 +++ Lib/fontTools/mtiLib/__main__.py | 7 + Lib/fontTools/otlLib/__init__.py | 1 + Lib/fontTools/otlLib/builder.py | 640 ++ Lib/fontTools/pens/__init__.py | 7 +- Lib/fontTools/pens/areaPen.py | 59 + Lib/fontTools/pens/basePen.py | 73 +- Lib/fontTools/pens/boundsPen.py | 47 +- Lib/fontTools/pens/filterPen.py | 121 + Lib/fontTools/pens/momentsPen.py | 294 + Lib/fontTools/pens/perimeterPen.py | 60 + Lib/fontTools/pens/pointInsidePen.py | 59 +- Lib/fontTools/pens/qtPen.py | 31 + Lib/fontTools/pens/recordingPen.py | 101 + Lib/fontTools/pens/reportLabPen.py | 3 + Lib/fontTools/pens/reverseContourPen.py | 97 + Lib/fontTools/pens/statisticsPen.py | 102 + Lib/fontTools/pens/svgPathPen.py | 178 + Lib/fontTools/pens/t2CharStringPen.py | 91 + Lib/fontTools/pens/teePen.py | 48 + Lib/fontTools/pens/transformPen.py | 14 +- Lib/fontTools/pens/ttGlyphPen.py | 158 + Lib/fontTools/pens/wxPen.py | 31 + Lib/fontTools/subset.py | 2251 ----- Lib/fontTools/subset/__init__.py | 3313 +++++++ Lib/fontTools/subset/__main__.py | 7 + Lib/fontTools/svgLib/__init__.py | 6 + Lib/fontTools/svgLib/path/__init__.py | 58 + Lib/fontTools/svgLib/path/parser.py | 216 + Lib/fontTools/{t1Lib.py => t1Lib/__init__.py} | 90 +- Lib/fontTools/ttLib/__init__.py | 964 +-- Lib/fontTools/ttLib/macUtils.py | 191 +- Lib/fontTools/ttLib/sfnt.py | 289 +- Lib/fontTools/ttLib/standardGlyphOrder.py | 515 +- Lib/fontTools/ttLib/tables/B_A_S_E_.py | 2 + .../ttLib/tables/BitmapGlyphMetrics.py | 7 +- Lib/fontTools/ttLib/tables/C_B_L_C_.py | 2 + Lib/fontTools/ttLib/tables/C_F_F_.py | 31 +- Lib/fontTools/ttLib/tables/C_F_F__2.py | 16 + Lib/fontTools/ttLib/tables/C_O_L_R_.py | 6 +- Lib/fontTools/ttLib/tables/C_P_A_L_.py | 206 +- Lib/fontTools/ttLib/tables/D_S_I_G_.py | 12 +- Lib/fontTools/ttLib/tables/DefaultTable.py | 24 +- Lib/fontTools/ttLib/tables/E_B_D_T_.py | 35 +- Lib/fontTools/ttLib/tables/E_B_L_C_.py | 44 +- Lib/fontTools/ttLib/tables/F_F_T_M_.py | 52 +- Lib/fontTools/ttLib/tables/F__e_a_t.py | 114 + Lib/fontTools/ttLib/tables/G_D_E_F_.py | 2 + Lib/fontTools/ttLib/tables/G_M_A_P_.py | 23 +- Lib/fontTools/ttLib/tables/G_P_K_G_.py | 11 +- Lib/fontTools/ttLib/tables/G_P_O_S_.py | 2 + Lib/fontTools/ttLib/tables/G_S_U_B_.py | 2 + Lib/fontTools/ttLib/tables/G__l_a_t.py | 221 + Lib/fontTools/ttLib/tables/G__l_o_c.py | 71 + Lib/fontTools/ttLib/tables/H_V_A_R_.py | 7 + Lib/fontTools/ttLib/tables/J_S_T_F_.py | 2 + Lib/fontTools/ttLib/tables/L_T_S_H_.py | 9 +- Lib/fontTools/ttLib/tables/M_A_T_H_.py | 2 + Lib/fontTools/ttLib/tables/M_E_T_A_.py | 80 +- Lib/fontTools/ttLib/tables/M_V_A_R_.py | 7 + Lib/fontTools/ttLib/tables/O_S_2f_2.py | 366 +- Lib/fontTools/ttLib/tables/S_I_N_G_.py | 17 +- Lib/fontTools/ttLib/tables/S_T_A_T_.py | 7 + Lib/fontTools/ttLib/tables/S_V_G_.py | 141 +- Lib/fontTools/ttLib/tables/S__i_l_f.py | 877 ++ Lib/fontTools/ttLib/tables/S__i_l_l.py | 79 + Lib/fontTools/ttLib/tables/T_S_I_B_.py | 7 +- Lib/fontTools/ttLib/tables/T_S_I_D_.py | 7 +- Lib/fontTools/ttLib/tables/T_S_I_J_.py | 7 +- Lib/fontTools/ttLib/tables/T_S_I_P_.py | 7 +- Lib/fontTools/ttLib/tables/T_S_I_S_.py | 7 +- Lib/fontTools/ttLib/tables/T_S_I_V_.py | 18 +- Lib/fontTools/ttLib/tables/T_S_I__0.py | 28 +- Lib/fontTools/ttLib/tables/T_S_I__1.py | 116 +- Lib/fontTools/ttLib/tables/T_S_I__2.py | 12 +- Lib/fontTools/ttLib/tables/T_S_I__3.py | 13 +- Lib/fontTools/ttLib/tables/T_S_I__5.py | 16 +- Lib/fontTools/ttLib/tables/T_T_F_A_.py | 6 + Lib/fontTools/ttLib/tables/TupleVariation.py | 623 ++ Lib/fontTools/ttLib/tables/V_D_M_X_.py | 234 + Lib/fontTools/ttLib/tables/V_O_R_G_.py | 8 +- Lib/fontTools/ttLib/tables/V_V_A_R_.py | 7 + Lib/fontTools/ttLib/tables/__init__.py | 40 + Lib/fontTools/ttLib/tables/_a_n_k_r.py | 13 + Lib/fontTools/ttLib/tables/_a_v_a_r.py | 101 + Lib/fontTools/ttLib/tables/_b_s_l_n.py | 8 + Lib/fontTools/ttLib/tables/_c_i_d_g.py | 20 + Lib/fontTools/ttLib/tables/_c_m_a_p.py | 414 +- Lib/fontTools/ttLib/tables/_c_v_a_r.py | 84 + Lib/fontTools/ttLib/tables/_c_v_t.py | 17 +- Lib/fontTools/ttLib/tables/_f_e_a_t.py | 7 + Lib/fontTools/ttLib/tables/_f_p_g_m.py | 39 +- Lib/fontTools/ttLib/tables/_f_v_a_r.py | 219 + Lib/fontTools/ttLib/tables/_g_a_s_p.py | 9 +- Lib/fontTools/ttLib/tables/_g_c_i_d.py | 8 + Lib/fontTools/ttLib/tables/_g_l_y_f.py | 945 +- Lib/fontTools/ttLib/tables/_g_v_a_r.py | 229 + Lib/fontTools/ttLib/tables/_h_d_m_x.py | 37 +- Lib/fontTools/ttLib/tables/_h_e_a_d.py | 54 +- Lib/fontTools/ttLib/tables/_h_h_e_a.py | 82 +- Lib/fontTools/ttLib/tables/_h_m_t_x.py | 93 +- Lib/fontTools/ttLib/tables/_k_e_r_n.py | 224 +- Lib/fontTools/ttLib/tables/_l_c_a_r.py | 7 + Lib/fontTools/ttLib/tables/_l_o_c_a.py | 26 +- Lib/fontTools/ttLib/tables/_l_t_a_g.py | 65 + Lib/fontTools/ttLib/tables/_m_a_x_p.py | 30 +- Lib/fontTools/ttLib/tables/_m_e_t_a.py | 99 + Lib/fontTools/ttLib/tables/_m_o_r_t.py | 8 + Lib/fontTools/ttLib/tables/_m_o_r_x.py | 8 + Lib/fontTools/ttLib/tables/_n_a_m_e.py | 868 +- Lib/fontTools/ttLib/tables/_o_p_b_d.py | 8 + Lib/fontTools/ttLib/tables/_p_o_s_t.py | 53 +- Lib/fontTools/ttLib/tables/_p_r_e_p.py | 3 +- Lib/fontTools/ttLib/tables/_p_r_o_p.py | 8 + Lib/fontTools/ttLib/tables/_s_b_i_x.py | 166 +- Lib/fontTools/ttLib/tables/_t_r_a_k.py | 314 + Lib/fontTools/ttLib/tables/_v_h_e_a.py | 123 +- Lib/fontTools/ttLib/tables/_v_m_t_x.py | 4 +- Lib/fontTools/ttLib/tables/asciiTable.py | 11 +- Lib/fontTools/ttLib/tables/grUtils.py | 79 + Lib/fontTools/ttLib/tables/otBase.py | 655 +- Lib/fontTools/ttLib/tables/otConverters.py | 1422 ++- Lib/fontTools/ttLib/tables/otData.py | 578 +- Lib/fontTools/ttLib/tables/otTables.py | 867 +- Lib/fontTools/ttLib/tables/sbixBitmap.py | 104 - Lib/fontTools/ttLib/tables/sbixBitmapSet.py | 138 - Lib/fontTools/ttLib/tables/sbixGlyph.py | 119 + Lib/fontTools/ttLib/tables/sbixStrike.py | 150 + Lib/fontTools/ttLib/tables/ttProgram.py | 463 +- Lib/fontTools/ttLib/ttCollection.py | 107 + Lib/fontTools/ttLib/ttFont.py | 1001 +++ Lib/fontTools/ttLib/woff2.py | 1098 +++ Lib/fontTools/ttx.py | 260 +- Lib/fontTools/unicode.py | 7 +- Lib/fontTools/unicodedata/Blocks.py | 677 ++ Lib/fontTools/unicodedata/OTTags.py | 41 + Lib/fontTools/unicodedata/ScriptExtensions.py | 389 + Lib/fontTools/unicodedata/Scripts.py | 3201 +++++++ Lib/fontTools/unicodedata/__init__.py | 276 + Lib/fontTools/varLib/__init__.py | 869 ++ Lib/fontTools/varLib/__main__.py | 7 + Lib/fontTools/varLib/builder.py | 101 + Lib/fontTools/varLib/designspace.py | 113 + Lib/fontTools/varLib/featureVars.py | 392 + Lib/fontTools/varLib/interpolatable.py | 181 + Lib/fontTools/varLib/interpolate_layout.py | 91 + Lib/fontTools/varLib/iup.py | 305 + Lib/fontTools/varLib/merger.py | 830 ++ Lib/fontTools/varLib/models.py | 384 + Lib/fontTools/varLib/mutator.py | 212 + Lib/fontTools/varLib/mvar.py | 44 + Lib/fontTools/varLib/plot.py | 118 + Lib/fontTools/varLib/varStore.py | 520 ++ Lib/fontTools/voltLib/__init__.py | 5 + Lib/fontTools/voltLib/ast.py | 253 + Lib/fontTools/voltLib/error.py | 16 + Lib/fontTools/voltLib/lexer.py | 98 + Lib/fontTools/voltLib/parser.py | 651 ++ Lib/fonttools.egg-info/PKG-INFO | 1375 +++ Lib/fonttools.egg-info/SOURCES.txt | 1576 ++++ Lib/fonttools.egg-info/dependency_links.txt | 1 + Lib/fonttools.egg-info/entry_points.txt | 7 + Lib/fonttools.egg-info/top_level.txt | 1 + MANIFEST.in | 43 +- METADATA | 18 + Makefile | 22 + MetaTools/buildChangeLog.py | 10 - MetaTools/buildTableList.py | 36 +- MetaTools/buildUCD.py | 293 + NEWS.rst | 980 +++ PKG-INFO | 1375 +++ README.md | 34 - README.rst | 365 + README.version | 3 - Snippets/README.md | 11 + Snippets/checksum.py | 133 + Snippets/cmap-format.py | 40 + Snippets/dump_woff_metadata.py | 33 + Snippets/edit_raw_table_data.py | 31 + Snippets/fix-dflt-langsys.py | 85 + Snippets/interpolate.py | 144 + Snippets/layout-features.py | 51 + Snippets/merge_woff_metadata.py | 43 + Snippets/otf2ttf.py | 91 + Snippets/rename-fonts.py | 170 + Snippets/subset-fpgm.py | 60 + Snippets/svg2glif.py | 126 + Snippets/woff2_compress.py | 29 + Snippets/woff2_decompress.py | 39 + Tests/afmLib/afmLib_test.py | 55 + Tests/afmLib/data/TestAFM.afm | 37 + Tests/agl_test.py | 68 + Tests/cffLib/cffLib_test.py | 64 + Tests/cffLib/data/TestOTF.otf | Bin 0 -> 2308 bytes Tests/cffLib/specializer_test.py | 918 ++ Tests/designspaceLib/data/test.designspace | 107 + Tests/designspaceLib/designspace_test.py | 791 ++ Tests/encodings/codecs_test.py | 26 + Tests/feaLib/__init__.py | 0 Tests/feaLib/builder_test.py | 528 ++ Tests/feaLib/data/Attach.fea | 5 + Tests/feaLib/data/Attach.ttx | 24 + Tests/feaLib/data/GPOS_1.fea | 42 + Tests/feaLib/data/GPOS_1.ttx | 180 + Tests/feaLib/data/GPOS_1_zero.fea | 5 + Tests/feaLib/data/GPOS_1_zero.ttx | 53 + Tests/feaLib/data/GPOS_2.fea | 30 + Tests/feaLib/data/GPOS_2.ttx | 184 + Tests/feaLib/data/GPOS_2b.fea | 9 + Tests/feaLib/data/GPOS_2b.ttx | 127 + Tests/feaLib/data/GPOS_3.fea | 12 + Tests/feaLib/data/GPOS_3.ttx | 114 + Tests/feaLib/data/GPOS_4.fea | 12 + Tests/feaLib/data/GPOS_4.ttx | 147 + Tests/feaLib/data/GPOS_5.fea | 18 + Tests/feaLib/data/GPOS_5.ttx | 229 + Tests/feaLib/data/GPOS_6.fea | 10 + Tests/feaLib/data/GPOS_6.ttx | 162 + Tests/feaLib/data/GPOS_8.fea | 22 + Tests/feaLib/data/GPOS_8.ttx | 176 + Tests/feaLib/data/GSUB_2.fea | 14 + Tests/feaLib/data/GSUB_2.ttx | 63 + Tests/feaLib/data/GSUB_3.fea | 14 + Tests/feaLib/data/GSUB_3.ttx | 81 + Tests/feaLib/data/GSUB_6.fea | 28 + Tests/feaLib/data/GSUB_6.ttx | 267 + Tests/feaLib/data/GSUB_8.fea | 11 + Tests/feaLib/data/GSUB_8.ttx | 129 + Tests/feaLib/data/GlyphClassDef.fea | 5 + Tests/feaLib/data/GlyphClassDef.ttx | 22 + Tests/feaLib/data/LigatureCaretByIndex.fea | 10 + Tests/feaLib/data/LigatureCaretByIndex.ttx | 40 + Tests/feaLib/data/LigatureCaretByPos.fea | 10 + Tests/feaLib/data/LigatureCaretByPos.ttx | 40 + Tests/feaLib/data/PairPosSubtable.fea | 20 + Tests/feaLib/data/PairPosSubtable.ttx | 97 + .../ZeroValue_ChainSinglePos_horizontal.fea | 9 + .../ZeroValue_ChainSinglePos_horizontal.ttx | 89 + .../ZeroValue_ChainSinglePos_vertical.fea | 9 + .../ZeroValue_ChainSinglePos_vertical.ttx | 89 + .../data/ZeroValue_PairPos_horizontal.fea | 9 + .../data/ZeroValue_PairPos_horizontal.ttx | 75 + .../data/ZeroValue_PairPos_vertical.fea | 9 + .../data/ZeroValue_PairPos_vertical.ttx | 75 + .../data/ZeroValue_SinglePos_horizontal.fea | 8 + .../data/ZeroValue_SinglePos_horizontal.ttx | 47 + .../data/ZeroValue_SinglePos_vertical.fea | 8 + .../data/ZeroValue_SinglePos_vertical.ttx | 47 + Tests/feaLib/data/baseClass.fea | 10 + Tests/feaLib/data/baseClass.feax | 10 + Tests/feaLib/data/bug453.fea | 11 + Tests/feaLib/data/bug453.ttx | 110 + Tests/feaLib/data/bug457.fea | 5 + Tests/feaLib/data/bug457.ttx | 48 + Tests/feaLib/data/bug463.fea | 6 + Tests/feaLib/data/bug463.ttx | 103 + Tests/feaLib/data/bug501.fea | 8 + Tests/feaLib/data/bug501.ttx | 47 + Tests/feaLib/data/bug502.fea | 7 + Tests/feaLib/data/bug502.ttx | 56 + Tests/feaLib/data/bug504.fea | 5 + Tests/feaLib/data/bug504.ttx | 46 + Tests/feaLib/data/bug505.fea | 12 + Tests/feaLib/data/bug505.ttx | 43 + Tests/feaLib/data/bug506.fea | 4 + Tests/feaLib/data/bug506.ttx | 66 + Tests/feaLib/data/bug509.fea | 5 + Tests/feaLib/data/bug509.ttx | 74 + Tests/feaLib/data/bug512.fea | 6 + Tests/feaLib/data/bug512.ttx | 110 + Tests/feaLib/data/bug514.fea | 11 + Tests/feaLib/data/bug514.ttx | 154 + Tests/feaLib/data/bug568.fea | 11 + Tests/feaLib/data/bug568.ttx | 77 + Tests/feaLib/data/bug633.fea | 10 + Tests/feaLib/data/bug633.ttx | 68 + Tests/feaLib/data/enum.fea | 7 + Tests/feaLib/data/enum.ttx | 95 + Tests/feaLib/data/feature_aalt.fea | 29 + Tests/feaLib/data/feature_aalt.ttx | 184 + Tests/feaLib/data/ignore_pos.fea | 5 + Tests/feaLib/data/ignore_pos.ttx | 101 + Tests/feaLib/data/include/include1.fea | 3 + Tests/feaLib/data/include/include3.fea | 4 + Tests/feaLib/data/include/include4.fea | 4 + Tests/feaLib/data/include/include5.fea | 3 + Tests/feaLib/data/include/include6.fea | 3 + .../data/include/includemissingfile.fea | 1 + Tests/feaLib/data/include/includeself.fea | 1 + Tests/feaLib/data/include/subdir/include2.fea | 3 + Tests/feaLib/data/include0.fea | 1 + Tests/feaLib/data/language_required.fea | 22 + Tests/feaLib/data/language_required.ttx | 139 + Tests/feaLib/data/lookup.fea | 24 + Tests/feaLib/data/lookup.ttx | 78 + Tests/feaLib/data/lookupflag.fea | 97 + Tests/feaLib/data/lookupflag.ttx | 259 + Tests/feaLib/data/markClass.fea | 12 + Tests/feaLib/data/markClass.ttx | 15 + Tests/feaLib/data/mini.fea | 19 + Tests/feaLib/data/multiple_feature_blocks.fea | 18 + Tests/feaLib/data/multiple_feature_blocks.ttx | 82 + Tests/feaLib/data/name.fea | 23 + Tests/feaLib/data/name.ttx | 22 + Tests/feaLib/data/omitted_GlyphClassDef.fea | 6 + Tests/feaLib/data/omitted_GlyphClassDef.ttx | 14 + Tests/feaLib/data/size.fea | 4 + Tests/feaLib/data/size.ttx | 41 + Tests/feaLib/data/size2.fea | 3 + Tests/feaLib/data/size2.ttx | 41 + Tests/feaLib/data/spec10.fea | 12 + Tests/feaLib/data/spec10.ttx | 4 + Tests/feaLib/data/spec4h1.fea | 64 + Tests/feaLib/data/spec4h1.ttx | 169 + Tests/feaLib/data/spec4h2.fea | 40 + Tests/feaLib/data/spec4h2.ttx | 180 + Tests/feaLib/data/spec5d1.fea | 23 + Tests/feaLib/data/spec5d1.ttx | 81 + Tests/feaLib/data/spec5d2.fea | 22 + Tests/feaLib/data/spec5d2.ttx | 73 + Tests/feaLib/data/spec5f_ii_1.fea | 9 + Tests/feaLib/data/spec5f_ii_1.ttx | 97 + Tests/feaLib/data/spec5f_ii_2.fea | 9 + Tests/feaLib/data/spec5f_ii_2.ttx | 106 + Tests/feaLib/data/spec5f_ii_3.fea | 9 + Tests/feaLib/data/spec5f_ii_3.ttx | 155 + Tests/feaLib/data/spec5f_ii_4.fea | 23 + Tests/feaLib/data/spec5f_ii_4.ttx | 285 + Tests/feaLib/data/spec5fi1.fea | 20 + Tests/feaLib/data/spec5fi1.ttx | 122 + Tests/feaLib/data/spec5fi2.fea | 11 + Tests/feaLib/data/spec5fi2.ttx | 66 + Tests/feaLib/data/spec5fi3.fea | 9 + Tests/feaLib/data/spec5fi3.ttx | 139 + Tests/feaLib/data/spec5fi4.fea | 9 + Tests/feaLib/data/spec5fi4.ttx | 73 + Tests/feaLib/data/spec5h1.fea | 9 + Tests/feaLib/data/spec5h1.ttx | 54 + Tests/feaLib/data/spec6b_ii.fea | 12 + Tests/feaLib/data/spec6b_ii.ttx | 104 + Tests/feaLib/data/spec6d2.fea | 15 + Tests/feaLib/data/spec6d2.ttx | 152 + Tests/feaLib/data/spec6e.fea | 10 + Tests/feaLib/data/spec6e.ttx | 100 + Tests/feaLib/data/spec6f.fea | 6 + Tests/feaLib/data/spec6f.ttx | 76 + Tests/feaLib/data/spec6h_ii.fea | 21 + Tests/feaLib/data/spec6h_ii.ttx | 147 + Tests/feaLib/data/spec6h_iii_1.fea | 9 + Tests/feaLib/data/spec6h_iii_1.ttx | 75 + Tests/feaLib/data/spec6h_iii_3d.fea | 7 + Tests/feaLib/data/spec6h_iii_3d.ttx | 68 + Tests/feaLib/data/spec8a.fea | 21 + Tests/feaLib/data/spec8a.ttx | 200 + Tests/feaLib/data/spec8b.fea | 12 + Tests/feaLib/data/spec8b.ttx | 53 + Tests/feaLib/data/spec8c.fea | 13 + Tests/feaLib/data/spec8c.ttx | 62 + Tests/feaLib/data/spec8d.fea | 57 + Tests/feaLib/data/spec8d.ttx | 87 + Tests/feaLib/data/spec9a.fea | 4 + Tests/feaLib/data/spec9a.ttx | 114 + Tests/feaLib/data/spec9b.fea | 13 + Tests/feaLib/data/spec9b.ttx | 75 + Tests/feaLib/data/spec9c1.fea | 4 + Tests/feaLib/data/spec9c1.ttx | 25 + Tests/feaLib/data/spec9c2.fea | 3 + Tests/feaLib/data/spec9c2.ttx | 25 + Tests/feaLib/data/spec9c3.fea | 3 + Tests/feaLib/data/spec9c3.ttx | 25 + Tests/feaLib/data/spec9d.fea | 6 + Tests/feaLib/data/spec9d.ttx | 24 + Tests/feaLib/data/spec9e.fea | 4 + Tests/feaLib/data/spec9e.ttx | 13 + Tests/feaLib/data/spec9f.fea | 19 + Tests/feaLib/data/spec9f.ttx | 57 + Tests/feaLib/data/spec9g.fea | 5 + Tests/feaLib/data/spec9g.ttx | 24 + Tests/feaLib/error_test.py | 19 + Tests/feaLib/lexer_test.py | 237 + Tests/feaLib/parser_test.py | 1623 ++++ Tests/merge_test.py | 118 + Tests/misc/arrayTools_test.py | 85 + Tests/misc/bezierTools_test.py | 133 + Tests/misc/classifyTools_test.py | 30 + Tests/misc/eexec_test.py | 17 + Tests/misc/encodingTools_test.py | 32 + Tests/misc/filenames_test.py | 137 + Tests/misc/fixedTools_test.py | 43 + Tests/misc/loggingTools_test.py | 210 + Tests/misc/macRes_test.py | 97 + Tests/misc/psCharStrings_test.py | 32 + Tests/misc/py23_test.py | 485 ++ Tests/misc/testTools_test.py | 83 + Tests/misc/textTools_test.py | 11 + Tests/misc/timeTools_test.py | 24 + Tests/misc/transform_test.py | 101 + Tests/misc/xmlReader_test.py | 190 + Tests/misc/xmlWriter_test.py | 128 + .../mtiLib/data/featurename-backward.ttx.GSUB | 59 + Tests/mtiLib/data/featurename-backward.txt | 14 + .../mtiLib/data/featurename-forward.ttx.GSUB | 59 + Tests/mtiLib/data/featurename-forward.txt | 14 + .../mtiLib/data/lookupnames-backward.ttx.GSUB | 87 + Tests/mtiLib/data/lookupnames-backward.txt | 36 + .../mtiLib/data/lookupnames-forward.ttx.GSUB | 87 + Tests/mtiLib/data/lookupnames-forward.txt | 36 + Tests/mtiLib/data/mixed-toplevels.ttx.GSUB | 87 + Tests/mtiLib/data/mixed-toplevels.txt | 36 + Tests/mtiLib/data/mti/README | 12 + Tests/mtiLib/data/mti/chained-glyph.ttx.GPOS | 48 + Tests/mtiLib/data/mti/chained-glyph.ttx.GSUB | 48 + Tests/mtiLib/data/mti/chained-glyph.txt | 11 + Tests/mtiLib/data/mti/chainedclass.ttx.GSUB | 55 + Tests/mtiLib/data/mti/chainedclass.txt | 1 + .../mtiLib/data/mti/chainedcoverage.ttx.GSUB | 57 + Tests/mtiLib/data/mti/chainedcoverage.txt | 1 + Tests/mtiLib/data/mti/cmap.ttx | 40 + Tests/mtiLib/data/mti/cmap.ttx.cmap | 40 + Tests/mtiLib/data/mti/cmap.txt | 52 + Tests/mtiLib/data/mti/context-glyph.txt | 14 + Tests/mtiLib/data/mti/contextclass.txt | 1 + Tests/mtiLib/data/mti/contextcoverage.txt | 1 + Tests/mtiLib/data/mti/featuretable.txt | 1 + Tests/mtiLib/data/mti/gdefattach.ttx.GDEF | 21 + Tests/mtiLib/data/mti/gdefattach.txt | 7 + Tests/mtiLib/data/mti/gdefclasses.ttx.GDEF | 12 + Tests/mtiLib/data/mti/gdefclasses.txt | 1 + Tests/mtiLib/data/mti/gdefligcaret.ttx.GDEF | 26 + Tests/mtiLib/data/mti/gdefligcaret.txt | 7 + Tests/mtiLib/data/mti/gdefmarkattach.ttx.GDEF | 10 + Tests/mtiLib/data/mti/gdefmarkattach.txt | 1 + Tests/mtiLib/data/mti/gdefmarkfilter.ttx.GDEF | 23 + Tests/mtiLib/data/mti/gdefmarkfilter.txt | 1 + Tests/mtiLib/data/mti/gposcursive.ttx.GPOS | 43 + Tests/mtiLib/data/mti/gposcursive.txt | 1 + Tests/mtiLib/data/mti/gposkernset.ttx.GPOS | 114 + Tests/mtiLib/data/mti/gposkernset.txt | 37 + Tests/mtiLib/data/mti/gposmarktobase.ttx.GPOS | 213 + Tests/mtiLib/data/mti/gposmarktobase.txt | 1 + Tests/mtiLib/data/mti/gpospairclass.ttx.GPOS | 91 + Tests/mtiLib/data/mti/gpospairclass.txt | 27 + Tests/mtiLib/data/mti/gpospairglyph.ttx.GPOS | 103 + Tests/mtiLib/data/mti/gpospairglyph.txt | 17 + Tests/mtiLib/data/mti/gpossingle.ttx.GPOS | 51 + Tests/mtiLib/data/mti/gpossingle.txt | 1 + Tests/mtiLib/data/mti/gsubalternate.ttx.GSUB | 125 + Tests/mtiLib/data/mti/gsubalternate.txt | 25 + Tests/mtiLib/data/mti/gsubligature.ttx.GSUB | 42 + Tests/mtiLib/data/mti/gsubligature.txt | 1 + Tests/mtiLib/data/mti/gsubmultiple.ttx.GSUB | 16 + Tests/mtiLib/data/mti/gsubmultiple.txt | 1 + .../data/mti/gsubreversechanined.ttx.GSUB | 81 + Tests/mtiLib/data/mti/gsubreversechanined.txt | 1 + Tests/mtiLib/data/mti/gsubsingle.ttx.GSUB | 17 + Tests/mtiLib/data/mti/gsubsingle.txt | 1 + .../mtiLib/data/mti/mark-to-ligature.ttx.GPOS | 826 ++ Tests/mtiLib/data/mti/mark-to-ligature.txt | 93 + Tests/mtiLib/data/mti/scripttable.ttx.GPOS | 87 + Tests/mtiLib/data/mti/scripttable.ttx.GSUB | 87 + Tests/mtiLib/data/mti/scripttable.txt | 1 + Tests/mtiLib/mti_test.py | 236 + Tests/otlLib/builder_test.py | 1062 +++ Tests/pens/areaPen_test.py | 180 + Tests/pens/basePen_test.py | 179 + Tests/pens/boundsPen_test.py | 77 + Tests/pens/perimeterPen_test.py | 167 + Tests/pens/pointInsidePen_test.py | 225 + Tests/pens/recordingPen_test.py | 39 + Tests/pens/reverseContourPen_test.py | 319 + Tests/pens/t2CharStringPen_test.py | 184 + Tests/pens/ttGlyphPen_test.py | 257 + Tests/subset/data/Lobster.subset.ttx | 661 ++ Tests/subset/data/NotdefWidthCID-Regular.ttx | 267 + Tests/subset/data/TestANKR.ttx | 325 + Tests/subset/data/TestBSLN-0.ttx | 342 + Tests/subset/data/TestBSLN-1.ttx | 348 + Tests/subset/data/TestBSLN-2.ttx | 342 + Tests/subset/data/TestBSLN-3.ttx | 363 + Tests/subset/data/TestCID-Regular.ttx | 389 + Tests/subset/data/TestCLR-Regular.ttx | 763 ++ Tests/subset/data/TestGVAR.ttx | 655 ++ Tests/subset/data/TestLCAR-0.ttx | 320 + Tests/subset/data/TestLCAR-1.ttx | 320 + Tests/subset/data/TestMATH-Regular.ttx | 7590 +++++++++++++++++ Tests/subset/data/TestOPBD-0.ttx | 299 + Tests/subset/data/TestOPBD-1.ttx | 299 + Tests/subset/data/TestOTF-Regular.ttx | 262 + Tests/subset/data/TestPROP.ttx | 322 + Tests/subset/data/TestTTF-Regular.ttx | 705 ++ .../data/TestTTF-Regular_non_BMP_char.ttx | 722 ++ Tests/subset/data/expect_ankr.ttx | 19 + Tests/subset/data/expect_bsln_0.ttx | 43 + Tests/subset/data/expect_bsln_1.ttx | 46 + Tests/subset/data/expect_bsln_2.ttx | 44 + Tests/subset/data/expect_bsln_3.ttx | 47 + .../subset/data/expect_desubroutinize_CFF.ttx | 238 + Tests/subset/data/expect_keep_colr.ttx | 195 + Tests/subset/data/expect_keep_gvar.ttx | 166 + .../data/expect_keep_gvar_notdef_outline.ttx | 213 + Tests/subset/data/expect_keep_math.ttx | 634 ++ Tests/subset/data/expect_lcar_0.ttx | 16 + Tests/subset/data/expect_lcar_1.ttx | 16 + Tests/subset/data/expect_no_hinting_CFF.ttx | 212 + Tests/subset/data/expect_no_hinting_TTF.ttx | 88 + .../expect_no_hinting_desubroutinize_CFF.ttx | 202 + .../data/expect_no_notdef_outline_cid.ttx | 61 + .../data/expect_no_notdef_outline_otf.ttx | 49 + .../data/expect_no_notdef_outline_ttf.ttx | 17 + Tests/subset/data/expect_notdef_width_cid.ttx | 74 + Tests/subset/data/expect_opbd_0.ttx | 18 + Tests/subset/data/expect_opbd_1.ttx | 18 + Tests/subset/data/expect_prop_0.ttx | 11 + Tests/subset/data/expect_prop_1.ttx | 14 + Tests/subset/data/google_color.ttx | 507 ++ Tests/subset/subset_test.py | 482 ++ Tests/svgLib/path/__init__.py | 0 Tests/svgLib/path/parser_test.py | 297 + Tests/svgLib/path/path_test.py | 80 + Tests/t1Lib/data/TestT1-Regular.lwfn | Bin 0 -> 2641 bytes Tests/t1Lib/data/TestT1-Regular.pfa | 60 + Tests/t1Lib/data/TestT1-Regular.pfb | Bin 0 -> 2263 bytes Tests/t1Lib/t1Lib_test.py | 100 + Tests/ttLib/data/TestOTF-Regular.otx | 519 ++ Tests/ttLib/data/TestTTF-Regular.ttx | 553 ++ Tests/ttLib/data/TestTTFComplex-Regular.ttx | 114 + Tests/ttLib/data/test_woff2_metadata.xml | 103 + Tests/ttLib/sfnt_test.py | 8 + Tests/ttLib/tables/C_F_F__2_test.py | 60 + Tests/ttLib/tables/C_F_F_test.py | 51 + Tests/ttLib/tables/C_P_A_L_test.py | 227 + Tests/ttLib/tables/M_V_A_R_test.py | 139 + Tests/ttLib/tables/O_S_2f_2_test.py | 62 + Tests/ttLib/tables/S_T_A_T_test.py | 264 + Tests/ttLib/tables/T_S_I__0_test.py | 106 + Tests/ttLib/tables/T_S_I__1_test.py | 184 + Tests/ttLib/tables/TupleVariation_test.py | 687 ++ Tests/ttLib/tables/_a_n_k_r_test.py | 167 + Tests/ttLib/tables/_a_v_a_r_test.py | 85 + Tests/ttLib/tables/_b_s_l_n_test.py | 311 + Tests/ttLib/tables/_c_i_d_g_test.py | 70 + Tests/ttLib/tables/_c_m_a_p_test.py | 88 + Tests/ttLib/tables/_c_v_a_r_test.py | 111 + Tests/ttLib/tables/_f_p_g_m_test.py | 20 + Tests/ttLib/tables/_f_v_a_r_test.py | 264 + Tests/ttLib/tables/_g_c_i_d_test.py | 70 + Tests/ttLib/tables/_g_l_y_f_test.py | 160 + Tests/ttLib/tables/_g_v_a_r_test.py | 211 + Tests/ttLib/tables/_h_h_e_a_test.py | 198 + Tests/ttLib/tables/_h_m_t_x_test.py | 219 + Tests/ttLib/tables/_k_e_r_n_test.py | 370 + Tests/ttLib/tables/_l_c_a_r_test.py | 109 + Tests/ttLib/tables/_l_t_a_g_test.py | 63 + Tests/ttLib/tables/_m_e_t_a_test.py | 95 + Tests/ttLib/tables/_m_o_r_t_test.py | 115 + Tests/ttLib/tables/_m_o_r_x_test.py | 903 ++ Tests/ttLib/tables/_n_a_m_e_test.py | 303 + Tests/ttLib/tables/_o_p_b_d_test.py | 183 + Tests/ttLib/tables/_p_r_o_p_test.py | 84 + Tests/ttLib/tables/_t_r_a_k_test.py | 341 + Tests/ttLib/tables/_v_h_e_a_test.py | 275 + Tests/ttLib/tables/_v_m_t_x_test.py | 19 + Tests/ttLib/tables/data/C_F_F_.bin | Bin 0 -> 1167 bytes Tests/ttLib/tables/data/C_F_F_.ttx | 282 + Tests/ttLib/tables/data/C_F_F__2.bin | Bin 0 -> 2312 bytes Tests/ttLib/tables/data/C_F_F__2.ttx | 395 + .../ttLib/tables/data/_h_h_e_a_recalc_OTF.ttx | 72 + .../ttLib/tables/data/_h_h_e_a_recalc_TTF.ttx | 63 + .../tables/data/_h_h_e_a_recalc_empty.ttx | 62 + .../ttLib/tables/data/_v_h_e_a_recalc_OTF.ttx | 72 + .../ttLib/tables/data/_v_h_e_a_recalc_TTF.ttx | 63 + .../tables/data/_v_h_e_a_recalc_empty.ttx | 62 + Tests/ttLib/tables/data/aots/README | 10 + Tests/ttLib/tables/data/aots/base.otf | Bin 0 -> 4944 bytes Tests/ttLib/tables/data/aots/base.ttx.CFF | 735 ++ Tests/ttLib/tables/data/aots/base.ttx.OS_2 | 57 + Tests/ttLib/tables/data/aots/base.ttx.cmap | 210 + Tests/ttLib/tables/data/aots/base.ttx.head | 25 + Tests/ttLib/tables/data/aots/base.ttx.hhea | 24 + Tests/ttLib/tables/data/aots/base.ttx.hmtx | 107 + Tests/ttLib/tables/data/aots/base.ttx.maxp | 9 + Tests/ttLib/tables/data/aots/base.ttx.name | 25 + Tests/ttLib/tables/data/aots/base.ttx.post | 16 + .../tables/data/aots/classdef1_font1.otf | Bin 0 -> 6004 bytes .../tables/data/aots/classdef1_font1.ttx.GSUB | 483 ++ .../tables/data/aots/classdef1_font2.otf | Bin 0 -> 6020 bytes .../tables/data/aots/classdef1_font2.ttx.GSUB | 494 ++ .../tables/data/aots/classdef1_font3.otf | Bin 0 -> 6060 bytes .../tables/data/aots/classdef1_font3.ttx.GSUB | 510 ++ .../tables/data/aots/classdef1_font4.otf | Bin 0 -> 5984 bytes .../tables/data/aots/classdef1_font4.ttx.GSUB | 469 + .../tables/data/aots/classdef2_font1.otf | Bin 0 -> 6004 bytes .../tables/data/aots/classdef2_font1.ttx.GSUB | 483 ++ .../tables/data/aots/classdef2_font2.otf | Bin 0 -> 6016 bytes .../tables/data/aots/classdef2_font2.ttx.GSUB | 494 ++ .../tables/data/aots/classdef2_font3.otf | Bin 0 -> 6052 bytes .../tables/data/aots/classdef2_font3.ttx.GSUB | 510 ++ .../tables/data/aots/classdef2_font4.otf | Bin 0 -> 5984 bytes .../tables/data/aots/classdef2_font4.ttx.GSUB | 469 + Tests/ttLib/tables/data/aots/cmap0_font1.otf | Bin 0 -> 5196 bytes .../tables/data/aots/cmap0_font1.ttx.cmap | 13 + Tests/ttLib/tables/data/aots/cmap10_font1.otf | Bin 0 -> 4968 bytes .../tables/data/aots/cmap10_font1.ttx.cmap | 12 + Tests/ttLib/tables/data/aots/cmap10_font2.otf | Bin 0 -> 4960 bytes .../tables/data/aots/cmap10_font2.ttx.cmap | 12 + Tests/ttLib/tables/data/aots/cmap12_font1.otf | Bin 0 -> 4980 bytes .../tables/data/aots/cmap12_font1.ttx.cmap | 20 + Tests/ttLib/tables/data/aots/cmap14_font1.otf | Bin 0 -> 5028 bytes .../tables/data/aots/cmap14_font1.ttx.cmap | 29 + Tests/ttLib/tables/data/aots/cmap2_font1.otf | Bin 0 -> 6000 bytes .../tables/data/aots/cmap2_font1.ttx.cmap | 19 + Tests/ttLib/tables/data/aots/cmap4_font1.otf | Bin 0 -> 4964 bytes .../tables/data/aots/cmap4_font1.ttx.cmap | 24 + Tests/ttLib/tables/data/aots/cmap4_font2.otf | Bin 0 -> 4956 bytes .../tables/data/aots/cmap4_font2.ttx.cmap | 10 + Tests/ttLib/tables/data/aots/cmap4_font3.otf | Bin 0 -> 4956 bytes .../tables/data/aots/cmap4_font3.ttx.cmap | 10 + Tests/ttLib/tables/data/aots/cmap4_font4.otf | Bin 0 -> 4972 bytes .../tables/data/aots/cmap4_font4.ttx.cmap | 1011 +++ Tests/ttLib/tables/data/aots/cmap6_font1.otf | Bin 0 -> 4948 bytes .../tables/data/aots/cmap6_font1.ttx.cmap | 13 + Tests/ttLib/tables/data/aots/cmap6_font2.otf | Bin 0 -> 4944 bytes .../tables/data/aots/cmap6_font2.ttx.cmap | 10 + Tests/ttLib/tables/data/aots/cmap8_font1.otf | Bin 0 -> 13224 bytes .../tables/data/aots/cmap8_font1.ttx.cmap | 529 ++ .../data/aots/cmap_composition_font1.otf | Bin 0 -> 5096 bytes .../data/aots/cmap_composition_font1.ttx.cmap | 15 + .../aots/cmap_subtableselection_font1.otf | Bin 0 -> 6412 bytes .../cmap_subtableselection_font1.ttx.cmap | 23 + .../aots/cmap_subtableselection_font2.otf | Bin 0 -> 6140 bytes .../cmap_subtableselection_font2.ttx.cmap | 20 + .../aots/cmap_subtableselection_font3.otf | Bin 0 -> 5872 bytes .../cmap_subtableselection_font3.ttx.cmap | 17 + .../aots/cmap_subtableselection_font4.otf | Bin 0 -> 5600 bytes .../cmap_subtableselection_font4.ttx.cmap | 14 + .../aots/cmap_subtableselection_font5.otf | Bin 0 -> 5332 bytes .../cmap_subtableselection_font5.ttx.cmap | 11 + .../data/aots/gpos1_1_lookupflag_f1.otf | Bin 0 -> 5208 bytes .../data/aots/gpos1_1_lookupflag_f1.ttx.GDEF | 11 + .../data/aots/gpos1_1_lookupflag_f1.ttx.GPOS | 48 + .../tables/data/aots/gpos1_1_simple_f1.otf | Bin 0 -> 5136 bytes .../data/aots/gpos1_1_simple_f1.ttx.GPOS | 48 + .../tables/data/aots/gpos1_1_simple_f2.otf | Bin 0 -> 5136 bytes .../data/aots/gpos1_1_simple_f2.ttx.GPOS | 48 + .../tables/data/aots/gpos1_1_simple_f3.otf | Bin 0 -> 5136 bytes .../data/aots/gpos1_1_simple_f3.ttx.GPOS | 48 + .../tables/data/aots/gpos1_1_simple_f4.otf | Bin 0 -> 5136 bytes .../data/aots/gpos1_1_simple_f4.ttx.GPOS | 48 + .../ttLib/tables/data/aots/gpos1_2_font1.otf | Bin 0 -> 5108 bytes .../tables/data/aots/gpos1_2_font1.ttx.GPOS | 50 + .../ttLib/tables/data/aots/gpos1_2_font2.otf | Bin 0 -> 5148 bytes .../tables/data/aots/gpos1_2_font2.ttx.GDEF | 11 + .../tables/data/aots/gpos1_2_font2.ttx.GPOS | 50 + .../ttLib/tables/data/aots/gpos2_1_font6.otf | Bin 0 -> 5120 bytes .../tables/data/aots/gpos2_1_font6.ttx.GPOS | 61 + .../ttLib/tables/data/aots/gpos2_1_font7.otf | Bin 0 -> 5132 bytes .../tables/data/aots/gpos2_1_font7.ttx.GPOS | 70 + .../data/aots/gpos2_1_lookupflag_f1.otf | Bin 0 -> 5220 bytes .../data/aots/gpos2_1_lookupflag_f1.ttx.GDEF | 11 + .../data/aots/gpos2_1_lookupflag_f1.ttx.GPOS | 56 + .../data/aots/gpos2_1_lookupflag_f2.otf | Bin 0 -> 5220 bytes .../data/aots/gpos2_1_lookupflag_f2.ttx.GDEF | 11 + .../data/aots/gpos2_1_lookupflag_f2.ttx.GPOS | 56 + .../data/aots/gpos2_1_next_glyph_f1.otf | Bin 0 -> 5180 bytes .../data/aots/gpos2_1_next_glyph_f1.ttx.GPOS | 56 + .../data/aots/gpos2_1_next_glyph_f2.otf | Bin 0 -> 5176 bytes .../data/aots/gpos2_1_next_glyph_f2.ttx.GPOS | 55 + .../tables/data/aots/gpos2_1_simple_f1.otf | Bin 0 -> 5148 bytes .../data/aots/gpos2_1_simple_f1.ttx.GPOS | 56 + .../ttLib/tables/data/aots/gpos2_2_font1.otf | Bin 0 -> 5148 bytes .../tables/data/aots/gpos2_2_font1.ttx.GPOS | 75 + .../ttLib/tables/data/aots/gpos2_2_font2.otf | Bin 0 -> 5188 bytes .../tables/data/aots/gpos2_2_font2.ttx.GDEF | 11 + .../tables/data/aots/gpos2_2_font2.ttx.GPOS | 75 + .../ttLib/tables/data/aots/gpos2_2_font3.otf | Bin 0 -> 5188 bytes .../tables/data/aots/gpos2_2_font3.ttx.GDEF | 11 + .../tables/data/aots/gpos2_2_font3.ttx.GPOS | 75 + .../ttLib/tables/data/aots/gpos2_2_font4.otf | Bin 0 -> 5148 bytes .../tables/data/aots/gpos2_2_font4.ttx.GPOS | 75 + .../ttLib/tables/data/aots/gpos2_2_font5.otf | Bin 0 -> 5140 bytes .../tables/data/aots/gpos2_2_font5.ttx.GPOS | 71 + Tests/ttLib/tables/data/aots/gpos3_font1.otf | Bin 0 -> 5120 bytes .../tables/data/aots/gpos3_font1.ttx.GPOS | 67 + Tests/ttLib/tables/data/aots/gpos3_font2.otf | Bin 0 -> 5160 bytes .../tables/data/aots/gpos3_font2.ttx.GDEF | 11 + .../tables/data/aots/gpos3_font2.ttx.GPOS | 67 + Tests/ttLib/tables/data/aots/gpos3_font3.otf | Bin 0 -> 5164 bytes .../tables/data/aots/gpos3_font3.ttx.GDEF | 11 + .../tables/data/aots/gpos3_font3.ttx.GPOS | 70 + .../tables/data/aots/gpos4_lookupflag_f1.otf | Bin 0 -> 5256 bytes .../data/aots/gpos4_lookupflag_f1.ttx.GDEF | 18 + .../data/aots/gpos4_lookupflag_f1.ttx.GPOS | 68 + .../tables/data/aots/gpos4_lookupflag_f2.otf | Bin 0 -> 5240 bytes .../data/aots/gpos4_lookupflag_f2.ttx.GDEF | 14 + .../data/aots/gpos4_lookupflag_f2.ttx.GPOS | 68 + .../data/aots/gpos4_multiple_anchors_1.otf | Bin 0 -> 5352 bytes .../aots/gpos4_multiple_anchors_1.ttx.GDEF | 16 + .../aots/gpos4_multiple_anchors_1.ttx.GPOS | 107 + .../ttLib/tables/data/aots/gpos4_simple_1.otf | Bin 0 -> 5200 bytes .../tables/data/aots/gpos4_simple_1.ttx.GDEF | 14 + .../tables/data/aots/gpos4_simple_1.ttx.GPOS | 68 + Tests/ttLib/tables/data/aots/gpos5_font1.otf | Bin 0 -> 5284 bytes .../tables/data/aots/gpos5_font1.ttx.GDEF | 14 + .../tables/data/aots/gpos5_font1.ttx.GPOS | 77 + .../tables/data/aots/gpos5_font1.ttx.GSUB | 45 + Tests/ttLib/tables/data/aots/gpos6_font1.otf | Bin 0 -> 5176 bytes .../tables/data/aots/gpos6_font1.ttx.GDEF | 14 + .../tables/data/aots/gpos6_font1.ttx.GPOS | 68 + .../ttLib/tables/data/aots/gpos7_1_font1.otf | Bin 0 -> 5160 bytes .../tables/data/aots/gpos7_1_font1.ttx.GPOS | 84 + Tests/ttLib/tables/data/aots/gpos9_font1.otf | Bin 0 -> 5096 bytes .../tables/data/aots/gpos9_font1.ttx.GPOS | 51 + Tests/ttLib/tables/data/aots/gpos9_font2.otf | Bin 0 -> 5124 bytes .../tables/data/aots/gpos9_font2.ttx.GPOS | 62 + .../data/aots/gpos_chaining1_boundary_f1.otf | Bin 0 -> 5496 bytes .../aots/gpos_chaining1_boundary_f1.ttx.GDEF | 30 + .../aots/gpos_chaining1_boundary_f1.ttx.GPOS | 124 + .../data/aots/gpos_chaining1_boundary_f2.otf | Bin 0 -> 5500 bytes .../aots/gpos_chaining1_boundary_f2.ttx.GDEF | 30 + .../aots/gpos_chaining1_boundary_f2.ttx.GPOS | 128 + .../data/aots/gpos_chaining1_boundary_f3.otf | Bin 0 -> 5496 bytes .../aots/gpos_chaining1_boundary_f3.ttx.GDEF | 30 + .../aots/gpos_chaining1_boundary_f3.ttx.GPOS | 127 + .../data/aots/gpos_chaining1_boundary_f4.otf | Bin 0 -> 5496 bytes .../aots/gpos_chaining1_boundary_f4.ttx.GDEF | 30 + .../aots/gpos_chaining1_boundary_f4.ttx.GPOS | 127 + .../aots/gpos_chaining1_lookupflag_f1.otf | Bin 0 -> 5520 bytes .../gpos_chaining1_lookupflag_f1.ttx.GDEF | 30 + .../gpos_chaining1_lookupflag_f1.ttx.GPOS | 131 + .../gpos_chaining1_multiple_subrules_f1.otf | Bin 0 -> 5592 bytes ...os_chaining1_multiple_subrules_f1.ttx.GDEF | 30 + ...os_chaining1_multiple_subrules_f1.ttx.GPOS | 142 + .../gpos_chaining1_multiple_subrules_f2.otf | Bin 0 -> 5592 bytes ...os_chaining1_multiple_subrules_f2.ttx.GDEF | 30 + ...os_chaining1_multiple_subrules_f2.ttx.GPOS | 142 + .../aots/gpos_chaining1_next_glyph_f1.otf | Bin 0 -> 5540 bytes .../gpos_chaining1_next_glyph_f1.ttx.GDEF | 30 + .../gpos_chaining1_next_glyph_f1.ttx.GPOS | 146 + .../data/aots/gpos_chaining1_simple_f1.otf | Bin 0 -> 5488 bytes .../aots/gpos_chaining1_simple_f1.ttx.GDEF | 30 + .../aots/gpos_chaining1_simple_f1.ttx.GPOS | 132 + .../data/aots/gpos_chaining1_simple_f2.otf | Bin 0 -> 5488 bytes .../aots/gpos_chaining1_simple_f2.ttx.GDEF | 30 + .../aots/gpos_chaining1_simple_f2.ttx.GPOS | 131 + .../aots/gpos_chaining1_successive_f1.otf | Bin 0 -> 5524 bytes .../gpos_chaining1_successive_f1.ttx.GDEF | 30 + .../gpos_chaining1_successive_f1.ttx.GPOS | 134 + .../data/aots/gpos_chaining2_boundary_f1.otf | Bin 0 -> 5704 bytes .../aots/gpos_chaining2_boundary_f1.ttx.GDEF | 30 + .../aots/gpos_chaining2_boundary_f1.ttx.GPOS | 183 + .../data/aots/gpos_chaining2_boundary_f2.otf | Bin 0 -> 5708 bytes .../aots/gpos_chaining2_boundary_f2.ttx.GDEF | 30 + .../aots/gpos_chaining2_boundary_f2.ttx.GPOS | 187 + .../data/aots/gpos_chaining2_boundary_f3.otf | Bin 0 -> 5704 bytes .../aots/gpos_chaining2_boundary_f3.ttx.GDEF | 30 + .../aots/gpos_chaining2_boundary_f3.ttx.GPOS | 186 + .../data/aots/gpos_chaining2_boundary_f4.otf | Bin 0 -> 5704 bytes .../aots/gpos_chaining2_boundary_f4.ttx.GDEF | 30 + .../aots/gpos_chaining2_boundary_f4.ttx.GPOS | 186 + .../aots/gpos_chaining2_lookupflag_f1.otf | Bin 0 -> 5728 bytes .../gpos_chaining2_lookupflag_f1.ttx.GDEF | 30 + .../gpos_chaining2_lookupflag_f1.ttx.GPOS | 190 + .../gpos_chaining2_multiple_subrules_f1.otf | Bin 0 -> 5800 bytes ...os_chaining2_multiple_subrules_f1.ttx.GDEF | 30 + ...os_chaining2_multiple_subrules_f1.ttx.GPOS | 201 + .../gpos_chaining2_multiple_subrules_f2.otf | Bin 0 -> 5800 bytes ...os_chaining2_multiple_subrules_f2.ttx.GDEF | 30 + ...os_chaining2_multiple_subrules_f2.ttx.GPOS | 201 + .../aots/gpos_chaining2_next_glyph_f1.otf | Bin 0 -> 5744 bytes .../gpos_chaining2_next_glyph_f1.ttx.GDEF | 30 + .../gpos_chaining2_next_glyph_f1.ttx.GPOS | 203 + .../data/aots/gpos_chaining2_simple_f1.otf | Bin 0 -> 5696 bytes .../aots/gpos_chaining2_simple_f1.ttx.GDEF | 30 + .../aots/gpos_chaining2_simple_f1.ttx.GPOS | 191 + .../data/aots/gpos_chaining2_simple_f2.otf | Bin 0 -> 5696 bytes .../aots/gpos_chaining2_simple_f2.ttx.GDEF | 30 + .../aots/gpos_chaining2_simple_f2.ttx.GPOS | 190 + .../aots/gpos_chaining2_successive_f1.otf | Bin 0 -> 5732 bytes .../gpos_chaining2_successive_f1.ttx.GDEF | 30 + .../gpos_chaining2_successive_f1.ttx.GPOS | 193 + .../data/aots/gpos_chaining3_boundary_f1.otf | Bin 0 -> 5504 bytes .../aots/gpos_chaining3_boundary_f1.ttx.GDEF | 30 + .../aots/gpos_chaining3_boundary_f1.ttx.GPOS | 124 + .../data/aots/gpos_chaining3_boundary_f2.otf | Bin 0 -> 5508 bytes .../aots/gpos_chaining3_boundary_f2.ttx.GDEF | 30 + .../aots/gpos_chaining3_boundary_f2.ttx.GPOS | 128 + .../data/aots/gpos_chaining3_boundary_f3.otf | Bin 0 -> 5500 bytes .../aots/gpos_chaining3_boundary_f3.ttx.GDEF | 30 + .../aots/gpos_chaining3_boundary_f3.ttx.GPOS | 125 + .../data/aots/gpos_chaining3_boundary_f4.otf | Bin 0 -> 5500 bytes .../aots/gpos_chaining3_boundary_f4.ttx.GDEF | 30 + .../aots/gpos_chaining3_boundary_f4.ttx.GPOS | 125 + .../aots/gpos_chaining3_lookupflag_f1.otf | Bin 0 -> 5548 bytes .../gpos_chaining3_lookupflag_f1.ttx.GDEF | 30 + .../gpos_chaining3_lookupflag_f1.ttx.GPOS | 137 + .../aots/gpos_chaining3_next_glyph_f1.otf | Bin 0 -> 5524 bytes .../gpos_chaining3_next_glyph_f1.ttx.GDEF | 30 + .../gpos_chaining3_next_glyph_f1.ttx.GPOS | 128 + .../data/aots/gpos_chaining3_simple_f1.otf | Bin 0 -> 5496 bytes .../aots/gpos_chaining3_simple_f1.ttx.GDEF | 30 + .../aots/gpos_chaining3_simple_f1.ttx.GPOS | 132 + .../data/aots/gpos_chaining3_simple_f2.otf | Bin 0 -> 5516 bytes .../aots/gpos_chaining3_simple_f2.ttx.GDEF | 30 + .../aots/gpos_chaining3_simple_f2.ttx.GPOS | 137 + .../aots/gpos_chaining3_successive_f1.otf | Bin 0 -> 5544 bytes .../gpos_chaining3_successive_f1.ttx.GDEF | 30 + .../gpos_chaining3_successive_f1.ttx.GPOS | 138 + .../data/aots/gpos_context1_boundary_f1.otf | Bin 0 -> 5480 bytes .../aots/gpos_context1_boundary_f1.ttx.GDEF | 30 + .../aots/gpos_context1_boundary_f1.ttx.GPOS | 120 + .../data/aots/gpos_context1_boundary_f2.otf | Bin 0 -> 5480 bytes .../aots/gpos_context1_boundary_f2.ttx.GDEF | 30 + .../aots/gpos_context1_boundary_f2.ttx.GPOS | 123 + .../data/aots/gpos_context1_expansion_f1.otf | Bin 0 -> 5492 bytes .../aots/gpos_context1_expansion_f1.ttx.GDEF | 30 + .../aots/gpos_context1_expansion_f1.ttx.GPOS | 125 + .../data/aots/gpos_context1_lookupflag_f1.otf | Bin 0 -> 5508 bytes .../aots/gpos_context1_lookupflag_f1.ttx.GDEF | 30 + .../aots/gpos_context1_lookupflag_f1.ttx.GPOS | 133 + .../data/aots/gpos_context1_lookupflag_f2.otf | Bin 0 -> 5500 bytes .../aots/gpos_context1_lookupflag_f2.ttx.GDEF | 30 + .../aots/gpos_context1_lookupflag_f2.ttx.GPOS | 125 + .../gpos_context1_multiple_subrules_f1.otf | Bin 0 -> 5568 bytes ...pos_context1_multiple_subrules_f1.ttx.GDEF | 30 + ...pos_context1_multiple_subrules_f1.ttx.GPOS | 134 + .../gpos_context1_multiple_subrules_f2.otf | Bin 0 -> 5568 bytes ...pos_context1_multiple_subrules_f2.ttx.GDEF | 30 + ...pos_context1_multiple_subrules_f2.ttx.GPOS | 134 + .../data/aots/gpos_context1_next_glyph_f1.otf | Bin 0 -> 5500 bytes .../aots/gpos_context1_next_glyph_f1.ttx.GDEF | 30 + .../aots/gpos_context1_next_glyph_f1.ttx.GPOS | 124 + .../data/aots/gpos_context1_simple_f1.otf | Bin 0 -> 5476 bytes .../aots/gpos_context1_simple_f1.ttx.GDEF | 30 + .../aots/gpos_context1_simple_f1.ttx.GPOS | 133 + .../data/aots/gpos_context1_simple_f2.otf | Bin 0 -> 5468 bytes .../aots/gpos_context1_simple_f2.ttx.GDEF | 30 + .../aots/gpos_context1_simple_f2.ttx.GPOS | 125 + .../data/aots/gpos_context1_successive_f1.otf | Bin 0 -> 5508 bytes .../aots/gpos_context1_successive_f1.ttx.GDEF | 30 + .../aots/gpos_context1_successive_f1.ttx.GPOS | 130 + .../data/aots/gpos_context2_boundary_f1.otf | Bin 0 -> 5492 bytes .../aots/gpos_context2_boundary_f1.ttx.GDEF | 30 + .../aots/gpos_context2_boundary_f1.ttx.GPOS | 124 + .../data/aots/gpos_context2_boundary_f2.otf | Bin 0 -> 5496 bytes .../aots/gpos_context2_boundary_f2.ttx.GDEF | 30 + .../aots/gpos_context2_boundary_f2.ttx.GPOS | 127 + .../data/aots/gpos_context2_classes_f1.otf | Bin 0 -> 5540 bytes .../aots/gpos_context2_classes_f1.ttx.GDEF | 30 + .../aots/gpos_context2_classes_f1.ttx.GPOS | 140 + .../data/aots/gpos_context2_classes_f2.otf | Bin 0 -> 5564 bytes .../aots/gpos_context2_classes_f2.ttx.GDEF | 30 + .../aots/gpos_context2_classes_f2.ttx.GPOS | 155 + .../data/aots/gpos_context2_expansion_f1.otf | Bin 0 -> 5524 bytes .../aots/gpos_context2_expansion_f1.ttx.GDEF | 30 + .../aots/gpos_context2_expansion_f1.ttx.GPOS | 133 + .../data/aots/gpos_context2_lookupflag_f1.otf | Bin 0 -> 5540 bytes .../aots/gpos_context2_lookupflag_f1.ttx.GDEF | 30 + .../aots/gpos_context2_lookupflag_f1.ttx.GPOS | 141 + .../data/aots/gpos_context2_lookupflag_f2.otf | Bin 0 -> 5532 bytes .../aots/gpos_context2_lookupflag_f2.ttx.GDEF | 30 + .../aots/gpos_context2_lookupflag_f2.ttx.GPOS | 133 + .../gpos_context2_multiple_subrules_f1.otf | Bin 0 -> 5600 bytes ...pos_context2_multiple_subrules_f1.ttx.GDEF | 30 + ...pos_context2_multiple_subrules_f1.ttx.GPOS | 142 + .../gpos_context2_multiple_subrules_f2.otf | Bin 0 -> 5600 bytes ...pos_context2_multiple_subrules_f2.ttx.GDEF | 30 + ...pos_context2_multiple_subrules_f2.ttx.GPOS | 142 + .../data/aots/gpos_context2_next_glyph_f1.otf | Bin 0 -> 5512 bytes .../aots/gpos_context2_next_glyph_f1.ttx.GDEF | 30 + .../aots/gpos_context2_next_glyph_f1.ttx.GPOS | 128 + .../data/aots/gpos_context2_simple_f1.otf | Bin 0 -> 5508 bytes .../aots/gpos_context2_simple_f1.ttx.GDEF | 30 + .../aots/gpos_context2_simple_f1.ttx.GPOS | 141 + .../data/aots/gpos_context2_simple_f2.otf | Bin 0 -> 5484 bytes .../aots/gpos_context2_simple_f2.ttx.GDEF | 30 + .../aots/gpos_context2_simple_f2.ttx.GPOS | 129 + .../data/aots/gpos_context2_successive_f1.otf | Bin 0 -> 5544 bytes .../aots/gpos_context2_successive_f1.ttx.GDEF | 30 + .../aots/gpos_context2_successive_f1.ttx.GPOS | 140 + .../data/aots/gpos_context3_boundary_f1.otf | Bin 0 -> 5476 bytes .../aots/gpos_context3_boundary_f1.ttx.GDEF | 30 + .../aots/gpos_context3_boundary_f1.ttx.GPOS | 116 + .../data/aots/gpos_context3_boundary_f2.otf | Bin 0 -> 5472 bytes .../aots/gpos_context3_boundary_f2.ttx.GDEF | 30 + .../aots/gpos_context3_boundary_f2.ttx.GPOS | 117 + .../data/aots/gpos_context3_lookupflag_f1.otf | Bin 0 -> 5512 bytes .../aots/gpos_context3_lookupflag_f1.ttx.GDEF | 30 + .../aots/gpos_context3_lookupflag_f1.ttx.GPOS | 131 + .../data/aots/gpos_context3_lookupflag_f2.otf | Bin 0 -> 5504 bytes .../aots/gpos_context3_lookupflag_f2.ttx.GDEF | 30 + .../aots/gpos_context3_lookupflag_f2.ttx.GPOS | 123 + .../data/aots/gpos_context3_next_glyph_f1.otf | Bin 0 -> 5496 bytes .../aots/gpos_context3_next_glyph_f1.ttx.GDEF | 30 + .../aots/gpos_context3_next_glyph_f1.ttx.GPOS | 120 + .../data/aots/gpos_context3_simple_f1.otf | Bin 0 -> 5480 bytes .../aots/gpos_context3_simple_f1.ttx.GDEF | 30 + .../aots/gpos_context3_simple_f1.ttx.GPOS | 131 + .../data/aots/gpos_context3_successive_f1.otf | Bin 0 -> 5516 bytes .../aots/gpos_context3_successive_f1.ttx.GDEF | 30 + .../aots/gpos_context3_successive_f1.ttx.GPOS | 130 + .../data/aots/gsub1_1_lookupflag_f1.otf | Bin 0 -> 5208 bytes .../data/aots/gsub1_1_lookupflag_f1.ttx.GDEF | 11 + .../data/aots/gsub1_1_lookupflag_f1.ttx.GSUB | 44 + .../tables/data/aots/gsub1_1_modulo_f1.otf | Bin 0 -> 5216 bytes .../data/aots/gsub1_1_modulo_f1.ttx.GSUB | 66 + .../tables/data/aots/gsub1_1_simple_f1.otf | Bin 0 -> 5136 bytes .../data/aots/gsub1_1_simple_f1.ttx.GSUB | 44 + .../data/aots/gsub1_2_lookupflag_f1.otf | Bin 0 -> 5212 bytes .../data/aots/gsub1_2_lookupflag_f1.ttx.GDEF | 11 + .../data/aots/gsub1_2_lookupflag_f1.ttx.GSUB | 44 + .../tables/data/aots/gsub1_2_simple_f1.otf | Bin 0 -> 5140 bytes .../data/aots/gsub1_2_simple_f1.ttx.GSUB | 44 + .../data/aots/gsub2_1_lookupflag_f1.otf | Bin 0 -> 5224 bytes .../data/aots/gsub2_1_lookupflag_f1.ttx.GDEF | 11 + .../data/aots/gsub2_1_lookupflag_f1.ttx.GSUB | 44 + .../aots/gsub2_1_multiple_sequences_f1.otf | Bin 0 -> 5248 bytes .../gsub2_1_multiple_sequences_f1.ttx.GSUB | 44 + .../tables/data/aots/gsub2_1_simple_f1.otf | Bin 0 -> 5144 bytes .../data/aots/gsub2_1_simple_f1.ttx.GSUB | 43 + .../data/aots/gsub3_1_lookupflag_f1.otf | Bin 0 -> 5224 bytes .../data/aots/gsub3_1_lookupflag_f1.ttx.GDEF | 11 + .../data/aots/gsub3_1_lookupflag_f1.ttx.GSUB | 50 + .../tables/data/aots/gsub3_1_multiple_f1.otf | Bin 0 -> 5168 bytes .../data/aots/gsub3_1_multiple_f1.ttx.GSUB | 50 + .../tables/data/aots/gsub3_1_simple_f1.otf | Bin 0 -> 5144 bytes .../data/aots/gsub3_1_simple_f1.ttx.GSUB | 47 + .../data/aots/gsub4_1_lookupflag_f1.otf | Bin 0 -> 5220 bytes .../data/aots/gsub4_1_lookupflag_f1.ttx.GDEF | 11 + .../data/aots/gsub4_1_lookupflag_f1.ttx.GSUB | 45 + .../aots/gsub4_1_multiple_ligatures_f1.otf | Bin 0 -> 5252 bytes .../gsub4_1_multiple_ligatures_f1.ttx.GSUB | 46 + .../aots/gsub4_1_multiple_ligatures_f2.otf | Bin 0 -> 5252 bytes .../gsub4_1_multiple_ligatures_f2.ttx.GSUB | 46 + .../data/aots/gsub4_1_multiple_ligsets_f1.otf | Bin 0 -> 5240 bytes .../aots/gsub4_1_multiple_ligsets_f1.ttx.GSUB | 48 + .../tables/data/aots/gsub4_1_simple_f1.otf | Bin 0 -> 5148 bytes .../data/aots/gsub4_1_simple_f1.ttx.GSUB | 45 + Tests/ttLib/tables/data/aots/gsub7_font1.otf | Bin 0 -> 5096 bytes .../tables/data/aots/gsub7_font1.ttx.GSUB | 47 + Tests/ttLib/tables/data/aots/gsub7_font2.otf | Bin 0 -> 5116 bytes .../tables/data/aots/gsub7_font2.ttx.GSUB | 52 + .../data/aots/gsub_chaining1_boundary_f1.otf | Bin 0 -> 5516 bytes .../aots/gsub_chaining1_boundary_f1.ttx.GDEF | 30 + .../aots/gsub_chaining1_boundary_f1.ttx.GSUB | 103 + .../data/aots/gsub_chaining1_boundary_f2.otf | Bin 0 -> 5520 bytes .../aots/gsub_chaining1_boundary_f2.ttx.GDEF | 30 + .../aots/gsub_chaining1_boundary_f2.ttx.GSUB | 107 + .../data/aots/gsub_chaining1_boundary_f3.otf | Bin 0 -> 5520 bytes .../aots/gsub_chaining1_boundary_f3.ttx.GDEF | 30 + .../aots/gsub_chaining1_boundary_f3.ttx.GSUB | 106 + .../data/aots/gsub_chaining1_boundary_f4.otf | Bin 0 -> 5520 bytes .../aots/gsub_chaining1_boundary_f4.ttx.GDEF | 30 + .../aots/gsub_chaining1_boundary_f4.ttx.GSUB | 106 + .../aots/gsub_chaining1_lookupflag_f1.otf | Bin 0 -> 5544 bytes .../gsub_chaining1_lookupflag_f1.ttx.GDEF | 30 + .../gsub_chaining1_lookupflag_f1.ttx.GSUB | 110 + .../gsub_chaining1_multiple_subrules_f1.otf | Bin 0 -> 5616 bytes ...ub_chaining1_multiple_subrules_f1.ttx.GDEF | 30 + ...ub_chaining1_multiple_subrules_f1.ttx.GSUB | 121 + .../gsub_chaining1_multiple_subrules_f2.otf | Bin 0 -> 5616 bytes ...ub_chaining1_multiple_subrules_f2.ttx.GDEF | 30 + ...ub_chaining1_multiple_subrules_f2.ttx.GSUB | 121 + .../aots/gsub_chaining1_next_glyph_f1.otf | Bin 0 -> 5560 bytes .../gsub_chaining1_next_glyph_f1.ttx.GDEF | 30 + .../gsub_chaining1_next_glyph_f1.ttx.GSUB | 125 + .../data/aots/gsub_chaining1_simple_f1.otf | Bin 0 -> 5508 bytes .../aots/gsub_chaining1_simple_f1.ttx.GDEF | 30 + .../aots/gsub_chaining1_simple_f1.ttx.GSUB | 111 + .../data/aots/gsub_chaining1_simple_f2.otf | Bin 0 -> 5512 bytes .../aots/gsub_chaining1_simple_f2.ttx.GDEF | 30 + .../aots/gsub_chaining1_simple_f2.ttx.GSUB | 110 + .../aots/gsub_chaining1_successive_f1.otf | Bin 0 -> 5544 bytes .../gsub_chaining1_successive_f1.ttx.GDEF | 30 + .../gsub_chaining1_successive_f1.ttx.GSUB | 113 + .../data/aots/gsub_chaining2_boundary_f1.otf | Bin 0 -> 5724 bytes .../aots/gsub_chaining2_boundary_f1.ttx.GDEF | 30 + .../aots/gsub_chaining2_boundary_f1.ttx.GSUB | 162 + .../data/aots/gsub_chaining2_boundary_f2.otf | Bin 0 -> 5728 bytes .../aots/gsub_chaining2_boundary_f2.ttx.GDEF | 30 + .../aots/gsub_chaining2_boundary_f2.ttx.GSUB | 166 + .../data/aots/gsub_chaining2_boundary_f3.otf | Bin 0 -> 5728 bytes .../aots/gsub_chaining2_boundary_f3.ttx.GDEF | 30 + .../aots/gsub_chaining2_boundary_f3.ttx.GSUB | 165 + .../data/aots/gsub_chaining2_boundary_f4.otf | Bin 0 -> 5728 bytes .../aots/gsub_chaining2_boundary_f4.ttx.GDEF | 30 + .../aots/gsub_chaining2_boundary_f4.ttx.GSUB | 165 + .../aots/gsub_chaining2_lookupflag_f1.otf | Bin 0 -> 5752 bytes .../gsub_chaining2_lookupflag_f1.ttx.GDEF | 30 + .../gsub_chaining2_lookupflag_f1.ttx.GSUB | 169 + .../gsub_chaining2_multiple_subrules_f1.otf | Bin 0 -> 5824 bytes ...ub_chaining2_multiple_subrules_f1.ttx.GDEF | 30 + ...ub_chaining2_multiple_subrules_f1.ttx.GSUB | 180 + .../gsub_chaining2_multiple_subrules_f2.otf | Bin 0 -> 5824 bytes ...ub_chaining2_multiple_subrules_f2.ttx.GDEF | 30 + ...ub_chaining2_multiple_subrules_f2.ttx.GSUB | 180 + .../aots/gsub_chaining2_next_glyph_f1.otf | Bin 0 -> 5764 bytes .../gsub_chaining2_next_glyph_f1.ttx.GDEF | 30 + .../gsub_chaining2_next_glyph_f1.ttx.GSUB | 182 + .../data/aots/gsub_chaining2_simple_f1.otf | Bin 0 -> 5716 bytes .../aots/gsub_chaining2_simple_f1.ttx.GDEF | 30 + .../aots/gsub_chaining2_simple_f1.ttx.GSUB | 170 + .../data/aots/gsub_chaining2_simple_f2.otf | Bin 0 -> 5720 bytes .../aots/gsub_chaining2_simple_f2.ttx.GDEF | 30 + .../aots/gsub_chaining2_simple_f2.ttx.GSUB | 169 + .../aots/gsub_chaining2_successive_f1.otf | Bin 0 -> 5752 bytes .../gsub_chaining2_successive_f1.ttx.GDEF | 30 + .../gsub_chaining2_successive_f1.ttx.GSUB | 172 + .../data/aots/gsub_chaining3_boundary_f1.otf | Bin 0 -> 5528 bytes .../aots/gsub_chaining3_boundary_f1.ttx.GDEF | 30 + .../aots/gsub_chaining3_boundary_f1.ttx.GSUB | 103 + .../data/aots/gsub_chaining3_boundary_f2.otf | Bin 0 -> 5532 bytes .../aots/gsub_chaining3_boundary_f2.ttx.GDEF | 30 + .../aots/gsub_chaining3_boundary_f2.ttx.GSUB | 107 + .../data/aots/gsub_chaining3_boundary_f3.otf | Bin 0 -> 5524 bytes .../aots/gsub_chaining3_boundary_f3.ttx.GDEF | 30 + .../aots/gsub_chaining3_boundary_f3.ttx.GSUB | 104 + .../data/aots/gsub_chaining3_boundary_f4.otf | Bin 0 -> 5524 bytes .../aots/gsub_chaining3_boundary_f4.ttx.GDEF | 30 + .../aots/gsub_chaining3_boundary_f4.ttx.GSUB | 104 + .../aots/gsub_chaining3_lookupflag_f1.otf | Bin 0 -> 5572 bytes .../gsub_chaining3_lookupflag_f1.ttx.GDEF | 30 + .../gsub_chaining3_lookupflag_f1.ttx.GSUB | 116 + .../aots/gsub_chaining3_next_glyph_f1.otf | Bin 0 -> 5548 bytes .../gsub_chaining3_next_glyph_f1.ttx.GDEF | 30 + .../gsub_chaining3_next_glyph_f1.ttx.GSUB | 107 + .../data/aots/gsub_chaining3_simple_f1.otf | Bin 0 -> 5520 bytes .../aots/gsub_chaining3_simple_f1.ttx.GDEF | 30 + .../aots/gsub_chaining3_simple_f1.ttx.GSUB | 111 + .../data/aots/gsub_chaining3_simple_f2.otf | Bin 0 -> 5540 bytes .../aots/gsub_chaining3_simple_f2.ttx.GDEF | 30 + .../aots/gsub_chaining3_simple_f2.ttx.GSUB | 116 + .../aots/gsub_chaining3_successive_f1.otf | Bin 0 -> 5568 bytes .../gsub_chaining3_successive_f1.ttx.GDEF | 30 + .../gsub_chaining3_successive_f1.ttx.GSUB | 117 + .../data/aots/gsub_context1_boundary_f1.otf | Bin 0 -> 5500 bytes .../aots/gsub_context1_boundary_f1.ttx.GDEF | 30 + .../aots/gsub_context1_boundary_f1.ttx.GSUB | 99 + .../data/aots/gsub_context1_boundary_f2.otf | Bin 0 -> 5504 bytes .../aots/gsub_context1_boundary_f2.ttx.GDEF | 30 + .../aots/gsub_context1_boundary_f2.ttx.GSUB | 102 + .../data/aots/gsub_context1_expansion_f1.otf | Bin 0 -> 5516 bytes .../aots/gsub_context1_expansion_f1.ttx.GDEF | 30 + .../aots/gsub_context1_expansion_f1.ttx.GSUB | 104 + .../data/aots/gsub_context1_lookupflag_f1.otf | Bin 0 -> 5532 bytes .../aots/gsub_context1_lookupflag_f1.ttx.GDEF | 30 + .../aots/gsub_context1_lookupflag_f1.ttx.GSUB | 112 + .../data/aots/gsub_context1_lookupflag_f2.otf | Bin 0 -> 5524 bytes .../aots/gsub_context1_lookupflag_f2.ttx.GDEF | 30 + .../aots/gsub_context1_lookupflag_f2.ttx.GSUB | 104 + .../gsub_context1_multiple_subrules_f1.otf | Bin 0 -> 5592 bytes ...sub_context1_multiple_subrules_f1.ttx.GDEF | 30 + ...sub_context1_multiple_subrules_f1.ttx.GSUB | 113 + .../gsub_context1_multiple_subrules_f2.otf | Bin 0 -> 5592 bytes ...sub_context1_multiple_subrules_f2.ttx.GDEF | 30 + ...sub_context1_multiple_subrules_f2.ttx.GSUB | 113 + .../data/aots/gsub_context1_next_glyph_f1.otf | Bin 0 -> 5520 bytes .../aots/gsub_context1_next_glyph_f1.ttx.GDEF | 30 + .../aots/gsub_context1_next_glyph_f1.ttx.GSUB | 103 + .../data/aots/gsub_context1_simple_f1.otf | Bin 0 -> 5500 bytes .../aots/gsub_context1_simple_f1.ttx.GDEF | 30 + .../aots/gsub_context1_simple_f1.ttx.GSUB | 112 + .../data/aots/gsub_context1_simple_f2.otf | Bin 0 -> 5492 bytes .../aots/gsub_context1_simple_f2.ttx.GDEF | 30 + .../aots/gsub_context1_simple_f2.ttx.GSUB | 104 + .../data/aots/gsub_context1_successive_f1.otf | Bin 0 -> 5528 bytes .../aots/gsub_context1_successive_f1.ttx.GDEF | 30 + .../aots/gsub_context1_successive_f1.ttx.GSUB | 109 + .../data/aots/gsub_context2_boundary_f1.otf | Bin 0 -> 5516 bytes .../aots/gsub_context2_boundary_f1.ttx.GDEF | 30 + .../aots/gsub_context2_boundary_f1.ttx.GSUB | 103 + .../data/aots/gsub_context2_boundary_f2.otf | Bin 0 -> 5516 bytes .../aots/gsub_context2_boundary_f2.ttx.GDEF | 30 + .../aots/gsub_context2_boundary_f2.ttx.GSUB | 106 + .../data/aots/gsub_context2_classes_f1.otf | Bin 0 -> 5564 bytes .../aots/gsub_context2_classes_f1.ttx.GDEF | 30 + .../aots/gsub_context2_classes_f1.ttx.GSUB | 119 + .../data/aots/gsub_context2_classes_f2.otf | Bin 0 -> 5584 bytes .../aots/gsub_context2_classes_f2.ttx.GDEF | 30 + .../aots/gsub_context2_classes_f2.ttx.GSUB | 134 + .../data/aots/gsub_context2_expansion_f1.otf | Bin 0 -> 5544 bytes .../aots/gsub_context2_expansion_f1.ttx.GDEF | 30 + .../aots/gsub_context2_expansion_f1.ttx.GSUB | 112 + .../data/aots/gsub_context2_lookupflag_f1.otf | Bin 0 -> 5560 bytes .../aots/gsub_context2_lookupflag_f1.ttx.GDEF | 30 + .../aots/gsub_context2_lookupflag_f1.ttx.GSUB | 120 + .../data/aots/gsub_context2_lookupflag_f2.otf | Bin 0 -> 5552 bytes .../aots/gsub_context2_lookupflag_f2.ttx.GDEF | 30 + .../aots/gsub_context2_lookupflag_f2.ttx.GSUB | 112 + .../gsub_context2_multiple_subrules_f1.otf | Bin 0 -> 5620 bytes ...sub_context2_multiple_subrules_f1.ttx.GDEF | 30 + ...sub_context2_multiple_subrules_f1.ttx.GSUB | 121 + .../gsub_context2_multiple_subrules_f2.otf | Bin 0 -> 5620 bytes ...sub_context2_multiple_subrules_f2.ttx.GDEF | 30 + ...sub_context2_multiple_subrules_f2.ttx.GSUB | 121 + .../data/aots/gsub_context2_next_glyph_f1.otf | Bin 0 -> 5536 bytes .../aots/gsub_context2_next_glyph_f1.ttx.GDEF | 30 + .../aots/gsub_context2_next_glyph_f1.ttx.GSUB | 107 + .../data/aots/gsub_context2_simple_f1.otf | Bin 0 -> 5528 bytes .../aots/gsub_context2_simple_f1.ttx.GDEF | 30 + .../aots/gsub_context2_simple_f1.ttx.GSUB | 120 + .../data/aots/gsub_context2_simple_f2.otf | Bin 0 -> 5504 bytes .../aots/gsub_context2_simple_f2.ttx.GDEF | 30 + .../aots/gsub_context2_simple_f2.ttx.GSUB | 108 + .../data/aots/gsub_context2_successive_f1.otf | Bin 0 -> 5568 bytes .../aots/gsub_context2_successive_f1.ttx.GDEF | 30 + .../aots/gsub_context2_successive_f1.ttx.GSUB | 119 + .../data/aots/gsub_context3_boundary_f1.otf | Bin 0 -> 5500 bytes .../aots/gsub_context3_boundary_f1.ttx.GDEF | 30 + .../aots/gsub_context3_boundary_f1.ttx.GSUB | 95 + .../data/aots/gsub_context3_boundary_f2.otf | Bin 0 -> 5496 bytes .../aots/gsub_context3_boundary_f2.ttx.GDEF | 30 + .../aots/gsub_context3_boundary_f2.ttx.GSUB | 96 + .../data/aots/gsub_context3_lookupflag_f1.otf | Bin 0 -> 5536 bytes .../aots/gsub_context3_lookupflag_f1.ttx.GDEF | 30 + .../aots/gsub_context3_lookupflag_f1.ttx.GSUB | 110 + .../data/aots/gsub_context3_lookupflag_f2.otf | Bin 0 -> 5528 bytes .../aots/gsub_context3_lookupflag_f2.ttx.GDEF | 30 + .../aots/gsub_context3_lookupflag_f2.ttx.GSUB | 102 + .../data/aots/gsub_context3_next_glyph_f1.otf | Bin 0 -> 5520 bytes .../aots/gsub_context3_next_glyph_f1.ttx.GDEF | 30 + .../aots/gsub_context3_next_glyph_f1.ttx.GSUB | 99 + .../data/aots/gsub_context3_simple_f1.otf | Bin 0 -> 5504 bytes .../aots/gsub_context3_simple_f1.ttx.GDEF | 30 + .../aots/gsub_context3_simple_f1.ttx.GSUB | 110 + .../data/aots/gsub_context3_successive_f1.otf | Bin 0 -> 5540 bytes .../aots/gsub_context3_successive_f1.ttx.GDEF | 30 + .../aots/gsub_context3_successive_f1.ttx.GSUB | 109 + .../data/aots/lookupflag_ignore_attach_f1.otf | Bin 0 -> 5416 bytes .../aots/lookupflag_ignore_attach_f1.ttx.GDEF | 37 + .../aots/lookupflag_ignore_attach_f1.ttx.GSUB | 45 + .../data/aots/lookupflag_ignore_base_f1.otf | Bin 0 -> 5256 bytes .../aots/lookupflag_ignore_base_f1.ttx.GDEF | 12 + .../aots/lookupflag_ignore_base_f1.ttx.GSUB | 45 + .../aots/lookupflag_ignore_combination_f1.otf | Bin 0 -> 5408 bytes .../lookupflag_ignore_combination_f1.ttx.GDEF | 29 + .../lookupflag_ignore_combination_f1.ttx.GSUB | 45 + .../aots/lookupflag_ignore_ligatures_f1.otf | Bin 0 -> 5320 bytes .../lookupflag_ignore_ligatures_f1.ttx.GDEF | 16 + .../lookupflag_ignore_ligatures_f1.ttx.GSUB | 45 + .../data/aots/lookupflag_ignore_marks_f1.otf | Bin 0 -> 5288 bytes .../aots/lookupflag_ignore_marks_f1.ttx.GDEF | 16 + .../aots/lookupflag_ignore_marks_f1.ttx.GSUB | 45 + .../tables/data/graphite/graphite_tests.ttf | Bin 0 -> 2260 bytes .../data/graphite/graphite_tests.ttx.Feat | 18 + .../data/graphite/graphite_tests.ttx.Glat | 29 + .../graphite/graphite_tests.ttx.Glat.setup | 48 + .../data/graphite/graphite_tests.ttx.Silf | 101 + .../graphite/graphite_tests.ttx.Silf.setup | 48 + .../data/graphite/graphite_tests.ttx.Sill | 12 + Tests/ttLib/tables/data/ttProgram.ttx | 1627 ++++ Tests/ttLib/tables/otBase_test.py | 96 + Tests/ttLib/tables/otConverters_test.py | 433 + Tests/ttLib/tables/otTables_test.py | 490 ++ Tests/ttLib/tables/tables_test.py | 329 + Tests/ttLib/tables/ttProgram_test.py | 119 + Tests/ttLib/woff2_test.py | 840 ++ Tests/ttx/data/TestBOM.ttx | 3 + Tests/ttx/data/TestDFONT.dfont | Bin 0 -> 3505 bytes Tests/ttx/data/TestNoSFNT.ttx | 2 + Tests/ttx/data/TestNoXML.ttx | 2 + Tests/ttx/data/TestOTF.otf | Bin 0 -> 2308 bytes Tests/ttx/data/TestOTF.ttx | 519 ++ Tests/ttx/data/TestTTC.ttc | Bin 0 -> 2608 bytes Tests/ttx/data/TestTTF.ttf | Bin 0 -> 2336 bytes Tests/ttx/data/TestTTF.ttx | 553 ++ Tests/ttx/data/TestWOFF.woff | Bin 0 -> 1328 bytes Tests/ttx/data/TestWOFF2.woff2 | Bin 0 -> 808 bytes Tests/ttx/ttx_test.py | 1014 +++ Tests/unicodedata_test.py | 254 + Tests/varLib/__init__.py | 0 Tests/varLib/builder_test.py | 67 + Tests/varLib/data/Build.designspace | 300 + .../data/BuildAvarEmptyAxis.designspace | 59 + .../data/BuildAvarIdentityMaps.designspace | 80 + .../data/BuildAvarSingleAxis.designspace | 45 + Tests/varLib/data/Designspace.designspace | 39 + Tests/varLib/data/Designspace2.designspace | 8 + .../varLib/data/InterpolateLayout.designspace | 65 + .../data/InterpolateLayout2.designspace | 65 + .../data/InterpolateLayout3.designspace | 62 + .../TestFamily2-Master0.ttx | 855 ++ .../TestFamily2-Master1.ttx | 693 ++ .../TestFamily-Master0.ttx | 520 ++ .../TestFamily-Master1.ttx | 520 ++ .../TestFamily-Master2.ttx | 504 ++ .../TestFamily-Master3.ttx | 504 ++ .../TestFamily-Master4.ttx | 504 ++ .../TestFamily2-Master0.ttx | 1149 +++ .../TestFamily2-Master1.ttx | 986 +++ .../TestFamily3-Bold.ttx | 520 ++ .../TestFamily3-Condensed.ttx | 526 ++ .../TestFamily3-CondensedBold.ttx | 526 ++ .../TestFamily3-CondensedLight.ttx | 526 ++ .../TestFamily3-CondensedSemiBold.ttx | 526 ++ .../TestFamily3-Light.ttx | 526 ++ .../TestFamily3-Regular.ttx | 520 ++ .../TestFamily3-SemiBold.ttx | 526 ++ .../master_ttx_varfont_ttf/Mutator_IUP.ttx | 551 ++ .../TestFamily-Master0.ufo/features.fea | 1 + .../TestFamily-Master0.ufo/fontinfo.plist | 140 + .../TestFamily-Master0.ufo/glyphs/A_.glif | 39 + .../glyphs/_notdef.glif | 30 + .../TestFamily-Master0.ufo/glyphs/a.glif | 74 + .../glyphs/contents.plist | 18 + .../TestFamily-Master0.ufo/glyphs/dollar.glif | 75 + .../glyphs/dollar.nostroke.glif | 74 + .../TestFamily-Master0.ufo/glyphs/space.glif | 5 + .../TestFamily-Master0.ufo/lib.plist | 15 + .../TestFamily-Master0.ufo/metainfo.plist | 10 + .../TestFamily-Master1.ufo/features.fea | 1 + .../TestFamily-Master1.ufo/fontinfo.plist | 140 + .../TestFamily-Master1.ufo/glyphs/A_.glif | 39 + .../glyphs/_notdef.glif | 30 + .../TestFamily-Master1.ufo/glyphs/a.glif | 74 + .../glyphs/contents.plist | 18 + .../TestFamily-Master1.ufo/glyphs/dollar.glif | 75 + .../glyphs/dollar.nostroke.glif | 74 + .../TestFamily-Master1.ufo/glyphs/space.glif | 5 + .../TestFamily-Master1.ufo/lib.plist | 15 + .../TestFamily-Master1.ufo/metainfo.plist | 10 + .../TestFamily-Master2.ufo/features.fea | 1 + .../TestFamily-Master2.ufo/fontinfo.plist | 140 + .../TestFamily-Master2.ufo/glyphs/A_.glif | 39 + .../glyphs/_notdef.glif | 30 + .../TestFamily-Master2.ufo/glyphs/a.glif | 74 + .../glyphs/contents.plist | 18 + .../TestFamily-Master2.ufo/glyphs/dollar.glif | 75 + .../glyphs/dollar.nostroke.glif | 74 + .../TestFamily-Master2.ufo/glyphs/space.glif | 5 + .../TestFamily-Master2.ufo/lib.plist | 15 + .../TestFamily-Master2.ufo/metainfo.plist | 10 + .../TestFamily-Master3.ufo/features.fea | 1 + .../TestFamily-Master3.ufo/fontinfo.plist | 140 + .../TestFamily-Master3.ufo/glyphs/A_.glif | 39 + .../glyphs/_notdef.glif | 30 + .../TestFamily-Master3.ufo/glyphs/a.glif | 74 + .../glyphs/contents.plist | 18 + .../TestFamily-Master3.ufo/glyphs/dollar.glif | 75 + .../glyphs/dollar.nostroke.glif | 74 + .../TestFamily-Master3.ufo/glyphs/space.glif | 5 + .../TestFamily-Master3.ufo/lib.plist | 15 + .../TestFamily-Master3.ufo/metainfo.plist | 10 + .../TestFamily-Master4.ufo/features.fea | 1 + .../TestFamily-Master4.ufo/fontinfo.plist | 140 + .../TestFamily-Master4.ufo/glyphs/A_.glif | 39 + .../glyphs/_notdef.glif | 30 + .../TestFamily-Master4.ufo/glyphs/a.glif | 74 + .../glyphs/contents.plist | 18 + .../TestFamily-Master4.ufo/glyphs/dollar.glif | 75 + .../glyphs/dollar.nostroke.glif | 74 + .../TestFamily-Master4.ufo/glyphs/space.glif | 5 + .../TestFamily-Master4.ufo/lib.plist | 15 + .../TestFamily-Master4.ufo/metainfo.plist | 10 + .../TestFamily2-Master0.ufo/features.fea | 81 + .../TestFamily2-Master0.ufo/fontinfo.plist | 140 + .../TestFamily2-Master0.ufo/glyphs/A_.glif | 29 + .../TestFamily2-Master0.ufo/glyphs/A_.sc.glif | 28 + .../glyphs/_notdef.glif | 38 + .../TestFamily2-Master0.ufo/glyphs/a.alt.glif | 42 + .../TestFamily2-Master0.ufo/glyphs/a.glif | 47 + .../glyphs/ampersand.glif | 59 + .../glyphs/atilde.glif | 9 + .../glyphs/circledotted.glif | 175 + .../glyphs/contents.plist | 42 + .../TestFamily2-Master0.ufo/glyphs/d.glif | 43 + .../glyphs/dieresisbelowcmb.glif | 7 + .../glyphs/dieresiscmb.glif | 34 + .../TestFamily2-Master0.ufo/glyphs/f.glif | 32 + .../TestFamily2-Master0.ufo/glyphs/f_t.glif | 51 + .../TestFamily2-Master0.ufo/glyphs/n.glif | 30 + .../TestFamily2-Master0.ufo/glyphs/space.glif | 5 + .../TestFamily2-Master0.ufo/glyphs/t.glif | 33 + .../glyphs/tildebelowcmb.glif | 7 + .../glyphs/tildecmb.glif | 28 + .../TestFamily2-Master0.ufo/lib.plist | 40 + .../TestFamily2-Master0.ufo/metainfo.plist | 10 + .../TestFamily2-Master1.ufo/features.fea | 54 + .../TestFamily2-Master1.ufo/fontinfo.plist | 140 + .../TestFamily2-Master1.ufo/glyphs/A_.glif | 29 + .../TestFamily2-Master1.ufo/glyphs/A_.sc.glif | 28 + .../glyphs/_notdef.glif | 38 + .../TestFamily2-Master1.ufo/glyphs/a.alt.glif | 42 + .../TestFamily2-Master1.ufo/glyphs/a.glif | 47 + .../glyphs/ampersand.glif | 59 + .../glyphs/atilde.glif | 9 + .../glyphs/circledotted.glif | 175 + .../glyphs/contents.plist | 42 + .../TestFamily2-Master1.ufo/glyphs/d.glif | 43 + .../glyphs/dieresisbelowcmb.glif | 7 + .../glyphs/dieresiscmb.glif | 34 + .../TestFamily2-Master1.ufo/glyphs/f.glif | 32 + .../TestFamily2-Master1.ufo/glyphs/f_t.glif | 51 + .../TestFamily2-Master1.ufo/glyphs/n.glif | 30 + .../TestFamily2-Master1.ufo/glyphs/space.glif | 5 + .../TestFamily2-Master1.ufo/glyphs/t.glif | 33 + .../glyphs/tildebelowcmb.glif | 7 + .../glyphs/tildecmb.glif | 28 + .../TestFamily2-Master1.ufo/lib.plist | 40 + .../TestFamily2-Master1.ufo/metainfo.plist | 10 + .../TestFamily3-Bold.ufo/fontinfo.plist | 153 + .../TestFamily3-Bold.ufo/glyphs/F_.glif | 22 + .../TestFamily3-Bold.ufo/glyphs/T_.glif | 21 + .../glyphs/contents.plist | 20 + .../TestFamily3-Bold.ufo/glyphs/l.glif | 17 + .../glyphs/layerinfo.plist | 5 + .../TestFamily3-Bold.ufo/glyphs/n.glif | 33 + .../TestFamily3-Bold.ufo/glyphs/o.glif | 39 + .../TestFamily3-Bold.ufo/glyphs/s.glif | 50 + .../TestFamily3-Bold.ufo/glyphs/t.glif | 37 + .../TestFamily3-Bold.ufo/groups.plist | 50 + .../TestFamily3-Bold.ufo/kerning.plist | 17 + .../TestFamily3-Bold.ufo/layercontents.plist | 10 + .../master_ufo/TestFamily3-Bold.ufo/lib.plist | 16 + .../TestFamily3-Bold.ufo/metainfo.plist | 10 + .../TestFamily3-Condensed.ufo/fontinfo.plist | 153 + .../TestFamily3-Condensed.ufo/glyphs/F_.glif | 22 + .../TestFamily3-Condensed.ufo/glyphs/T_.glif | 21 + .../glyphs/contents.plist | 20 + .../TestFamily3-Condensed.ufo/glyphs/l.glif | 17 + .../glyphs/layerinfo.plist | 5 + .../TestFamily3-Condensed.ufo/glyphs/n.glif | 33 + .../TestFamily3-Condensed.ufo/glyphs/o.glif | 39 + .../TestFamily3-Condensed.ufo/glyphs/s.glif | 50 + .../TestFamily3-Condensed.ufo/glyphs/t.glif | 37 + .../TestFamily3-Condensed.ufo/groups.plist | 50 + .../TestFamily3-Condensed.ufo/kerning.plist | 17 + .../layercontents.plist | 10 + .../TestFamily3-Condensed.ufo/lib.plist | 16 + .../TestFamily3-Condensed.ufo/metainfo.plist | 10 + .../fontinfo.plist | 153 + .../glyphs/F_.glif | 22 + .../glyphs/T_.glif | 21 + .../glyphs/contents.plist | 20 + .../glyphs/l.glif | 17 + .../glyphs/layerinfo.plist | 5 + .../glyphs/n.glif | 33 + .../glyphs/o.glif | 39 + .../glyphs/s.glif | 50 + .../glyphs/t.glif | 37 + .../groups.plist | 50 + .../kerning.plist | 17 + .../layercontents.plist | 10 + .../TestFamily3-CondensedBold.ufo/lib.plist | 16 + .../metainfo.plist | 10 + .../fontinfo.plist | 149 + .../glyphs/F_.glif | 22 + .../glyphs/T_.glif | 21 + .../glyphs/contents.plist | 20 + .../glyphs/l.glif | 17 + .../glyphs/layerinfo.plist | 5 + .../glyphs/n.glif | 33 + .../glyphs/o.glif | 39 + .../glyphs/s.glif | 50 + .../glyphs/t.glif | 37 + .../groups.plist | 50 + .../kerning.plist | 17 + .../layercontents.plist | 10 + .../TestFamily3-CondensedLight.ufo/lib.plist | 16 + .../metainfo.plist | 10 + .../fontinfo.plist | 153 + .../glyphs/F_.glif | 22 + .../glyphs/T_.glif | 21 + .../glyphs/contents.plist | 20 + .../glyphs/l.glif | 17 + .../glyphs/layerinfo.plist | 5 + .../glyphs/n.glif | 33 + .../glyphs/o.glif | 39 + .../glyphs/s.glif | 50 + .../glyphs/t.glif | 37 + .../groups.plist | 50 + .../kerning.plist | 17 + .../layercontents.plist | 10 + .../lib.plist | 16 + .../metainfo.plist | 10 + .../TestFamily3-Light.ufo/fontinfo.plist | 149 + .../TestFamily3-Light.ufo/glyphs/F_.glif | 22 + .../TestFamily3-Light.ufo/glyphs/T_.glif | 21 + .../glyphs/contents.plist | 20 + .../TestFamily3-Light.ufo/glyphs/l.glif | 17 + .../glyphs/layerinfo.plist | 5 + .../TestFamily3-Light.ufo/glyphs/n.glif | 33 + .../TestFamily3-Light.ufo/glyphs/o.glif | 39 + .../TestFamily3-Light.ufo/glyphs/s.glif | 50 + .../TestFamily3-Light.ufo/glyphs/t.glif | 37 + .../TestFamily3-Light.ufo/groups.plist | 50 + .../TestFamily3-Light.ufo/kerning.plist | 17 + .../TestFamily3-Light.ufo/layercontents.plist | 10 + .../TestFamily3-Light.ufo/lib.plist | 16 + .../TestFamily3-Light.ufo/metainfo.plist | 10 + .../TestFamily3-Regular.ufo/fontinfo.plist | 153 + .../TestFamily3-Regular.ufo/glyphs/F_.glif | 22 + .../TestFamily3-Regular.ufo/glyphs/T_.glif | 21 + .../glyphs/contents.plist | 20 + .../TestFamily3-Regular.ufo/glyphs/l.glif | 17 + .../glyphs/layerinfo.plist | 5 + .../TestFamily3-Regular.ufo/glyphs/n.glif | 33 + .../TestFamily3-Regular.ufo/glyphs/o.glif | 39 + .../TestFamily3-Regular.ufo/glyphs/s.glif | 50 + .../TestFamily3-Regular.ufo/glyphs/t.glif | 37 + .../TestFamily3-Regular.ufo/groups.plist | 50 + .../TestFamily3-Regular.ufo/kerning.plist | 17 + .../layercontents.plist | 10 + .../TestFamily3-Regular.ufo/lib.plist | 16 + .../TestFamily3-Regular.ufo/metainfo.plist | 10 + .../TestFamily3-SemiBold.ufo/fontinfo.plist | 153 + .../TestFamily3-SemiBold.ufo/glyphs/F_.glif | 22 + .../TestFamily3-SemiBold.ufo/glyphs/T_.glif | 21 + .../glyphs/contents.plist | 20 + .../TestFamily3-SemiBold.ufo/glyphs/l.glif | 17 + .../glyphs/layerinfo.plist | 5 + .../TestFamily3-SemiBold.ufo/glyphs/n.glif | 33 + .../TestFamily3-SemiBold.ufo/glyphs/o.glif | 39 + .../TestFamily3-SemiBold.ufo/glyphs/s.glif | 50 + .../TestFamily3-SemiBold.ufo/glyphs/t.glif | 37 + .../TestFamily3-SemiBold.ufo/groups.plist | 50 + .../TestFamily3-SemiBold.ufo/kerning.plist | 17 + .../layercontents.plist | 10 + .../TestFamily3-SemiBold.ufo/lib.plist | 16 + .../TestFamily3-SemiBold.ufo/metainfo.plist | 10 + Tests/varLib/data/test_results/Build.ttx | 1601 ++++ Tests/varLib/data/test_results/Build3.ttx | 725 ++ .../data/test_results/BuildAvarEmptyAxis.ttx | 18 + .../test_results/BuildAvarIdentityMaps.ttx | 23 + .../data/test_results/BuildAvarSingleAxis.ttx | 18 + Tests/varLib/data/test_results/BuildMain.ttx | 2238 +++++ .../data/test_results/InterpolateLayout.ttx | 161 + .../data/test_results/InterpolateLayout2.ttx | 4 + .../InterpolateLayoutGPOS_1_diff.ttx | 47 + .../InterpolateLayoutGPOS_1_diff2.ttx | 50 + .../InterpolateLayoutGPOS_1_same.ttx | 47 + .../InterpolateLayoutGPOS_2_class_diff.ttx | 62 + .../InterpolateLayoutGPOS_2_class_diff2.ttx | 72 + .../InterpolateLayoutGPOS_2_class_same.ttx | 62 + .../InterpolateLayoutGPOS_2_spec_diff.ttx | 55 + .../InterpolateLayoutGPOS_2_spec_diff2.ttx | 63 + .../InterpolateLayoutGPOS_2_spec_same.ttx | 55 + .../InterpolateLayoutGPOS_3_diff.ttx | 56 + .../InterpolateLayoutGPOS_3_same.ttx | 56 + .../InterpolateLayoutGPOS_4_diff.ttx | 68 + .../InterpolateLayoutGPOS_4_same.ttx | 68 + .../InterpolateLayoutGPOS_5_diff.ttx | 77 + .../InterpolateLayoutGPOS_5_same.ttx | 77 + .../InterpolateLayoutGPOS_6_diff.ttx | 68 + .../InterpolateLayoutGPOS_6_same.ttx | 68 + .../InterpolateLayoutGPOS_8_diff.ttx | 116 + .../InterpolateLayoutGPOS_8_same.ttx | 116 + .../InterpolateLayoutGPOS_size_feat_same.ttx | 41 + .../test_results/InterpolateLayoutMain.ttx | 499 ++ Tests/varLib/data/test_results/Mutator.ttx | 499 ++ .../test_results/Mutator_IUP-instance.ttx | 282 + Tests/varLib/designspace_test.py | 69 + Tests/varLib/interpolatable_test.py | 102 + Tests/varLib/interpolate_layout_test.py | 890 ++ Tests/varLib/models_test.py | 99 + Tests/varLib/mutator_test.py | 144 + Tests/varLib/varLib_test.py | 236 + Tests/voltLib/lexer_test.py | 35 + Tests/voltLib/parser_test.py | 1034 +++ Tools/fontTools | 1 - Tools/pyftinspect | 6 - Tools/pyftmerge | 6 - Tools/pyftsubset | 6 - Tools/ttx | 6 - Windows/README.TXT | 53 - Windows/fonttools-win-setup.iss | 355 - Windows/fonttools-win-setup.txt | 12 - Windows/mcmillan.bat | 9 - Windows/ttx.ico | Bin 2238 -> 0 bytes dev-requirements.txt | 4 + fonttools | 12 + requirements.txt | 7 + run-tests.sh | 28 + setup.cfg | 57 + setup.py | 421 +- tox.ini | 68 + 1596 files changed, 170521 insertions(+), 8957 deletions(-) create mode 100644 .appveyor.yml create mode 100644 .codecov.yml create mode 100644 .coveragerc create mode 100644 .travis.yml create mode 100755 .travis/after_success.sh create mode 100755 .travis/before_deploy.sh create mode 100755 .travis/before_install.sh create mode 100755 .travis/install.sh create mode 100755 .travis/run.sh create mode 100644 Doc/Makefile delete mode 100644 Doc/changes.txt delete mode 100644 Doc/documentation.html delete mode 100644 Doc/install.txt create mode 100644 Doc/make.bat rename Doc/{ => man/man1}/ttx.1 (98%) create mode 100644 Doc/source/afmLib.rst create mode 100644 Doc/source/agl.rst create mode 100644 Doc/source/cffLib.rst create mode 100644 Doc/source/conf.py create mode 100644 Doc/source/designspaceLib/index.rst create mode 100644 Doc/source/designspaceLib/readme.rst create mode 100644 Doc/source/designspaceLib/scripting.rst create mode 100644 Doc/source/encodings.rst create mode 100644 Doc/source/feaLib.rst create mode 100644 Doc/source/index.rst create mode 100644 Doc/source/inspect.rst create mode 100644 Doc/source/merge.rst create mode 100644 Doc/source/misc/arrayTools.rst create mode 100644 Doc/source/misc/bezierTools.rst create mode 100644 Doc/source/misc/classifyTools.rst create mode 100644 Doc/source/misc/eexec.rst create mode 100644 Doc/source/misc/encodingTools.rst create mode 100644 Doc/source/misc/fixedTools.rst create mode 100644 Doc/source/misc/index.rst create mode 100644 Doc/source/misc/loggingTools.rst create mode 100644 Doc/source/misc/psCharStrings.rst create mode 100644 Doc/source/misc/sstruct.rst create mode 100644 Doc/source/misc/testTools.rst create mode 100644 Doc/source/misc/textTools.rst create mode 100644 Doc/source/misc/timeTools.rst create mode 100644 Doc/source/misc/transform.rst create mode 100644 Doc/source/misc/xmlReader.rst create mode 100644 Doc/source/misc/xmlWriter.rst create mode 100644 Doc/source/pens/areaPen.rst create mode 100644 Doc/source/pens/basePen.rst create mode 100644 Doc/source/pens/boundsPen.rst create mode 100644 Doc/source/pens/filterPen.rst create mode 100644 Doc/source/pens/index.rst create mode 100644 Doc/source/pens/perimeterPen.rst create mode 100644 Doc/source/pens/pointInsidePen.rst create mode 100644 Doc/source/pens/recordingPen.rst create mode 100644 Doc/source/pens/statisticsPen.rst create mode 100644 Doc/source/pens/t2CharStringPen.rst create mode 100644 Doc/source/pens/teePen.rst create mode 100644 Doc/source/pens/transformPen.rst create mode 100644 Doc/source/subset.rst create mode 100644 Doc/source/t1Lib.rst create mode 100644 Doc/source/ttLib/index.rst create mode 100644 Doc/source/ttLib/macUtils.rst create mode 100644 Doc/source/ttLib/sfnt.rst create mode 100644 Doc/source/ttLib/tables.rst create mode 100644 Doc/source/ttLib/woff2.rst create mode 100644 Doc/source/ttx.rst create mode 100644 Doc/source/varLib/designspace.rst create mode 100644 Doc/source/varLib/index.rst create mode 100644 Doc/source/varLib/interpolatable.rst create mode 100644 Doc/source/varLib/interpolate_layout.rst create mode 100644 Doc/source/varLib/merger.rst create mode 100644 Doc/source/varLib/models.rst create mode 100644 Doc/source/varLib/mutator.rst create mode 100644 Doc/source/voltLib.rst create mode 100644 LICENSE create mode 100644 LICENSE.external delete mode 100644 LICENSE.txt create mode 100644 Lib/fontTools/__main__.py rename Lib/fontTools/{cffLib.py => cffLib/__init__.py} (51%) create mode 100644 Lib/fontTools/cffLib/specializer.py create mode 100644 Lib/fontTools/cffLib/width.py create mode 100644 Lib/fontTools/designspaceLib/__init__.py create mode 100644 Lib/fontTools/encodings/codecs.py create mode 100644 Lib/fontTools/feaLib/__init__.py create mode 100644 Lib/fontTools/feaLib/__main__.py create mode 100644 Lib/fontTools/feaLib/ast.py create mode 100644 Lib/fontTools/feaLib/builder.py create mode 100644 Lib/fontTools/feaLib/error.py create mode 100644 Lib/fontTools/feaLib/lexer.py create mode 100644 Lib/fontTools/feaLib/parser.py create mode 100644 Lib/fontTools/misc/classifyTools.py create mode 100644 Lib/fontTools/misc/cliTools.py create mode 100644 Lib/fontTools/misc/encodingTools.py create mode 100644 Lib/fontTools/misc/filenames.py delete mode 100644 Lib/fontTools/misc/homeResFile.py create mode 100644 Lib/fontTools/misc/loggingTools.py create mode 100644 Lib/fontTools/misc/macRes.py create mode 100644 Lib/fontTools/misc/symfont.py create mode 100644 Lib/fontTools/misc/testTools.py create mode 100644 Lib/fontTools/misc/timeTools.py create mode 100644 Lib/fontTools/mtiLib/__init__.py create mode 100644 Lib/fontTools/mtiLib/__main__.py create mode 100644 Lib/fontTools/otlLib/__init__.py create mode 100644 Lib/fontTools/otlLib/builder.py create mode 100644 Lib/fontTools/pens/areaPen.py create mode 100644 Lib/fontTools/pens/filterPen.py create mode 100644 Lib/fontTools/pens/momentsPen.py create mode 100644 Lib/fontTools/pens/perimeterPen.py create mode 100644 Lib/fontTools/pens/qtPen.py create mode 100644 Lib/fontTools/pens/recordingPen.py create mode 100644 Lib/fontTools/pens/reverseContourPen.py create mode 100644 Lib/fontTools/pens/statisticsPen.py create mode 100644 Lib/fontTools/pens/svgPathPen.py create mode 100644 Lib/fontTools/pens/t2CharStringPen.py create mode 100644 Lib/fontTools/pens/teePen.py create mode 100644 Lib/fontTools/pens/ttGlyphPen.py create mode 100644 Lib/fontTools/pens/wxPen.py delete mode 100644 Lib/fontTools/subset.py create mode 100644 Lib/fontTools/subset/__init__.py create mode 100644 Lib/fontTools/subset/__main__.py create mode 100644 Lib/fontTools/svgLib/__init__.py create mode 100644 Lib/fontTools/svgLib/path/__init__.py create mode 100644 Lib/fontTools/svgLib/path/parser.py rename Lib/fontTools/{t1Lib.py => t1Lib/__init__.py} (87%) create mode 100644 Lib/fontTools/ttLib/tables/C_F_F__2.py create mode 100644 Lib/fontTools/ttLib/tables/F__e_a_t.py create mode 100644 Lib/fontTools/ttLib/tables/G__l_a_t.py create mode 100644 Lib/fontTools/ttLib/tables/G__l_o_c.py create mode 100644 Lib/fontTools/ttLib/tables/H_V_A_R_.py create mode 100644 Lib/fontTools/ttLib/tables/M_V_A_R_.py create mode 100644 Lib/fontTools/ttLib/tables/S_T_A_T_.py create mode 100644 Lib/fontTools/ttLib/tables/S__i_l_f.py create mode 100644 Lib/fontTools/ttLib/tables/S__i_l_l.py create mode 100644 Lib/fontTools/ttLib/tables/T_T_F_A_.py create mode 100644 Lib/fontTools/ttLib/tables/TupleVariation.py create mode 100644 Lib/fontTools/ttLib/tables/V_D_M_X_.py create mode 100644 Lib/fontTools/ttLib/tables/V_V_A_R_.py create mode 100644 Lib/fontTools/ttLib/tables/_a_n_k_r.py create mode 100644 Lib/fontTools/ttLib/tables/_a_v_a_r.py create mode 100644 Lib/fontTools/ttLib/tables/_b_s_l_n.py create mode 100644 Lib/fontTools/ttLib/tables/_c_i_d_g.py create mode 100644 Lib/fontTools/ttLib/tables/_c_v_a_r.py create mode 100644 Lib/fontTools/ttLib/tables/_f_e_a_t.py create mode 100644 Lib/fontTools/ttLib/tables/_f_v_a_r.py create mode 100644 Lib/fontTools/ttLib/tables/_g_c_i_d.py create mode 100644 Lib/fontTools/ttLib/tables/_g_v_a_r.py create mode 100644 Lib/fontTools/ttLib/tables/_l_c_a_r.py create mode 100644 Lib/fontTools/ttLib/tables/_l_t_a_g.py create mode 100644 Lib/fontTools/ttLib/tables/_m_e_t_a.py create mode 100644 Lib/fontTools/ttLib/tables/_m_o_r_t.py create mode 100644 Lib/fontTools/ttLib/tables/_m_o_r_x.py create mode 100644 Lib/fontTools/ttLib/tables/_o_p_b_d.py create mode 100644 Lib/fontTools/ttLib/tables/_p_r_o_p.py create mode 100644 Lib/fontTools/ttLib/tables/_t_r_a_k.py create mode 100644 Lib/fontTools/ttLib/tables/grUtils.py mode change 100644 => 100755 Lib/fontTools/ttLib/tables/otData.py delete mode 100644 Lib/fontTools/ttLib/tables/sbixBitmap.py delete mode 100644 Lib/fontTools/ttLib/tables/sbixBitmapSet.py create mode 100644 Lib/fontTools/ttLib/tables/sbixGlyph.py create mode 100644 Lib/fontTools/ttLib/tables/sbixStrike.py create mode 100644 Lib/fontTools/ttLib/ttCollection.py create mode 100644 Lib/fontTools/ttLib/ttFont.py create mode 100644 Lib/fontTools/ttLib/woff2.py create mode 100644 Lib/fontTools/unicodedata/Blocks.py create mode 100644 Lib/fontTools/unicodedata/OTTags.py create mode 100644 Lib/fontTools/unicodedata/ScriptExtensions.py create mode 100644 Lib/fontTools/unicodedata/Scripts.py create mode 100644 Lib/fontTools/unicodedata/__init__.py create mode 100644 Lib/fontTools/varLib/__init__.py create mode 100644 Lib/fontTools/varLib/__main__.py create mode 100644 Lib/fontTools/varLib/builder.py create mode 100644 Lib/fontTools/varLib/designspace.py create mode 100644 Lib/fontTools/varLib/featureVars.py create mode 100644 Lib/fontTools/varLib/interpolatable.py create mode 100644 Lib/fontTools/varLib/interpolate_layout.py create mode 100644 Lib/fontTools/varLib/iup.py create mode 100644 Lib/fontTools/varLib/merger.py create mode 100644 Lib/fontTools/varLib/models.py create mode 100644 Lib/fontTools/varLib/mutator.py create mode 100644 Lib/fontTools/varLib/mvar.py create mode 100644 Lib/fontTools/varLib/plot.py create mode 100644 Lib/fontTools/varLib/varStore.py create mode 100644 Lib/fontTools/voltLib/__init__.py create mode 100644 Lib/fontTools/voltLib/ast.py create mode 100644 Lib/fontTools/voltLib/error.py create mode 100644 Lib/fontTools/voltLib/lexer.py create mode 100644 Lib/fontTools/voltLib/parser.py create mode 100644 Lib/fonttools.egg-info/PKG-INFO create mode 100644 Lib/fonttools.egg-info/SOURCES.txt create mode 100644 Lib/fonttools.egg-info/dependency_links.txt create mode 100644 Lib/fonttools.egg-info/entry_points.txt create mode 100644 Lib/fonttools.egg-info/top_level.txt create mode 100644 METADATA create mode 100644 Makefile delete mode 100755 MetaTools/buildChangeLog.py create mode 100755 MetaTools/buildUCD.py create mode 100644 NEWS.rst create mode 100644 PKG-INFO delete mode 100644 README.md create mode 100644 README.rst delete mode 100644 README.version create mode 100644 Snippets/README.md create mode 100644 Snippets/checksum.py create mode 100755 Snippets/cmap-format.py create mode 100644 Snippets/dump_woff_metadata.py create mode 100644 Snippets/edit_raw_table_data.py create mode 100644 Snippets/fix-dflt-langsys.py create mode 100755 Snippets/interpolate.py create mode 100755 Snippets/layout-features.py create mode 100644 Snippets/merge_woff_metadata.py create mode 100755 Snippets/otf2ttf.py create mode 100755 Snippets/rename-fonts.py create mode 100755 Snippets/subset-fpgm.py create mode 100755 Snippets/svg2glif.py create mode 100755 Snippets/woff2_compress.py create mode 100755 Snippets/woff2_decompress.py create mode 100644 Tests/afmLib/afmLib_test.py create mode 100644 Tests/afmLib/data/TestAFM.afm create mode 100644 Tests/agl_test.py create mode 100644 Tests/cffLib/cffLib_test.py create mode 100644 Tests/cffLib/data/TestOTF.otf create mode 100644 Tests/cffLib/specializer_test.py create mode 100644 Tests/designspaceLib/data/test.designspace create mode 100644 Tests/designspaceLib/designspace_test.py create mode 100644 Tests/encodings/codecs_test.py create mode 100644 Tests/feaLib/__init__.py create mode 100644 Tests/feaLib/builder_test.py create mode 100644 Tests/feaLib/data/Attach.fea create mode 100644 Tests/feaLib/data/Attach.ttx create mode 100644 Tests/feaLib/data/GPOS_1.fea create mode 100644 Tests/feaLib/data/GPOS_1.ttx create mode 100644 Tests/feaLib/data/GPOS_1_zero.fea create mode 100644 Tests/feaLib/data/GPOS_1_zero.ttx create mode 100644 Tests/feaLib/data/GPOS_2.fea create mode 100644 Tests/feaLib/data/GPOS_2.ttx create mode 100644 Tests/feaLib/data/GPOS_2b.fea create mode 100644 Tests/feaLib/data/GPOS_2b.ttx create mode 100644 Tests/feaLib/data/GPOS_3.fea create mode 100644 Tests/feaLib/data/GPOS_3.ttx create mode 100644 Tests/feaLib/data/GPOS_4.fea create mode 100644 Tests/feaLib/data/GPOS_4.ttx create mode 100644 Tests/feaLib/data/GPOS_5.fea create mode 100644 Tests/feaLib/data/GPOS_5.ttx create mode 100644 Tests/feaLib/data/GPOS_6.fea create mode 100644 Tests/feaLib/data/GPOS_6.ttx create mode 100644 Tests/feaLib/data/GPOS_8.fea create mode 100644 Tests/feaLib/data/GPOS_8.ttx create mode 100644 Tests/feaLib/data/GSUB_2.fea create mode 100644 Tests/feaLib/data/GSUB_2.ttx create mode 100644 Tests/feaLib/data/GSUB_3.fea create mode 100644 Tests/feaLib/data/GSUB_3.ttx create mode 100644 Tests/feaLib/data/GSUB_6.fea create mode 100644 Tests/feaLib/data/GSUB_6.ttx create mode 100644 Tests/feaLib/data/GSUB_8.fea create mode 100644 Tests/feaLib/data/GSUB_8.ttx create mode 100644 Tests/feaLib/data/GlyphClassDef.fea create mode 100644 Tests/feaLib/data/GlyphClassDef.ttx create mode 100644 Tests/feaLib/data/LigatureCaretByIndex.fea create mode 100644 Tests/feaLib/data/LigatureCaretByIndex.ttx create mode 100644 Tests/feaLib/data/LigatureCaretByPos.fea create mode 100644 Tests/feaLib/data/LigatureCaretByPos.ttx create mode 100644 Tests/feaLib/data/PairPosSubtable.fea create mode 100644 Tests/feaLib/data/PairPosSubtable.ttx create mode 100644 Tests/feaLib/data/ZeroValue_ChainSinglePos_horizontal.fea create mode 100644 Tests/feaLib/data/ZeroValue_ChainSinglePos_horizontal.ttx create mode 100644 Tests/feaLib/data/ZeroValue_ChainSinglePos_vertical.fea create mode 100644 Tests/feaLib/data/ZeroValue_ChainSinglePos_vertical.ttx create mode 100644 Tests/feaLib/data/ZeroValue_PairPos_horizontal.fea create mode 100644 Tests/feaLib/data/ZeroValue_PairPos_horizontal.ttx create mode 100644 Tests/feaLib/data/ZeroValue_PairPos_vertical.fea create mode 100644 Tests/feaLib/data/ZeroValue_PairPos_vertical.ttx create mode 100644 Tests/feaLib/data/ZeroValue_SinglePos_horizontal.fea create mode 100644 Tests/feaLib/data/ZeroValue_SinglePos_horizontal.ttx create mode 100644 Tests/feaLib/data/ZeroValue_SinglePos_vertical.fea create mode 100644 Tests/feaLib/data/ZeroValue_SinglePos_vertical.ttx create mode 100644 Tests/feaLib/data/baseClass.fea create mode 100644 Tests/feaLib/data/baseClass.feax create mode 100644 Tests/feaLib/data/bug453.fea create mode 100644 Tests/feaLib/data/bug453.ttx create mode 100644 Tests/feaLib/data/bug457.fea create mode 100644 Tests/feaLib/data/bug457.ttx create mode 100644 Tests/feaLib/data/bug463.fea create mode 100644 Tests/feaLib/data/bug463.ttx create mode 100644 Tests/feaLib/data/bug501.fea create mode 100644 Tests/feaLib/data/bug501.ttx create mode 100644 Tests/feaLib/data/bug502.fea create mode 100644 Tests/feaLib/data/bug502.ttx create mode 100644 Tests/feaLib/data/bug504.fea create mode 100644 Tests/feaLib/data/bug504.ttx create mode 100644 Tests/feaLib/data/bug505.fea create mode 100644 Tests/feaLib/data/bug505.ttx create mode 100644 Tests/feaLib/data/bug506.fea create mode 100644 Tests/feaLib/data/bug506.ttx create mode 100644 Tests/feaLib/data/bug509.fea create mode 100644 Tests/feaLib/data/bug509.ttx create mode 100644 Tests/feaLib/data/bug512.fea create mode 100644 Tests/feaLib/data/bug512.ttx create mode 100644 Tests/feaLib/data/bug514.fea create mode 100644 Tests/feaLib/data/bug514.ttx create mode 100644 Tests/feaLib/data/bug568.fea create mode 100644 Tests/feaLib/data/bug568.ttx create mode 100644 Tests/feaLib/data/bug633.fea create mode 100644 Tests/feaLib/data/bug633.ttx create mode 100644 Tests/feaLib/data/enum.fea create mode 100644 Tests/feaLib/data/enum.ttx create mode 100644 Tests/feaLib/data/feature_aalt.fea create mode 100644 Tests/feaLib/data/feature_aalt.ttx create mode 100644 Tests/feaLib/data/ignore_pos.fea create mode 100644 Tests/feaLib/data/ignore_pos.ttx create mode 100644 Tests/feaLib/data/include/include1.fea create mode 100644 Tests/feaLib/data/include/include3.fea create mode 100644 Tests/feaLib/data/include/include4.fea create mode 100644 Tests/feaLib/data/include/include5.fea create mode 100644 Tests/feaLib/data/include/include6.fea create mode 100644 Tests/feaLib/data/include/includemissingfile.fea create mode 100644 Tests/feaLib/data/include/includeself.fea create mode 100644 Tests/feaLib/data/include/subdir/include2.fea create mode 100644 Tests/feaLib/data/include0.fea create mode 100644 Tests/feaLib/data/language_required.fea create mode 100644 Tests/feaLib/data/language_required.ttx create mode 100644 Tests/feaLib/data/lookup.fea create mode 100644 Tests/feaLib/data/lookup.ttx create mode 100644 Tests/feaLib/data/lookupflag.fea create mode 100644 Tests/feaLib/data/lookupflag.ttx create mode 100644 Tests/feaLib/data/markClass.fea create mode 100644 Tests/feaLib/data/markClass.ttx create mode 100644 Tests/feaLib/data/mini.fea create mode 100644 Tests/feaLib/data/multiple_feature_blocks.fea create mode 100644 Tests/feaLib/data/multiple_feature_blocks.ttx create mode 100644 Tests/feaLib/data/name.fea create mode 100644 Tests/feaLib/data/name.ttx create mode 100644 Tests/feaLib/data/omitted_GlyphClassDef.fea create mode 100644 Tests/feaLib/data/omitted_GlyphClassDef.ttx create mode 100644 Tests/feaLib/data/size.fea create mode 100644 Tests/feaLib/data/size.ttx create mode 100644 Tests/feaLib/data/size2.fea create mode 100644 Tests/feaLib/data/size2.ttx create mode 100644 Tests/feaLib/data/spec10.fea create mode 100644 Tests/feaLib/data/spec10.ttx create mode 100644 Tests/feaLib/data/spec4h1.fea create mode 100644 Tests/feaLib/data/spec4h1.ttx create mode 100644 Tests/feaLib/data/spec4h2.fea create mode 100644 Tests/feaLib/data/spec4h2.ttx create mode 100644 Tests/feaLib/data/spec5d1.fea create mode 100644 Tests/feaLib/data/spec5d1.ttx create mode 100644 Tests/feaLib/data/spec5d2.fea create mode 100644 Tests/feaLib/data/spec5d2.ttx create mode 100644 Tests/feaLib/data/spec5f_ii_1.fea create mode 100644 Tests/feaLib/data/spec5f_ii_1.ttx create mode 100644 Tests/feaLib/data/spec5f_ii_2.fea create mode 100644 Tests/feaLib/data/spec5f_ii_2.ttx create mode 100644 Tests/feaLib/data/spec5f_ii_3.fea create mode 100644 Tests/feaLib/data/spec5f_ii_3.ttx create mode 100644 Tests/feaLib/data/spec5f_ii_4.fea create mode 100644 Tests/feaLib/data/spec5f_ii_4.ttx create mode 100644 Tests/feaLib/data/spec5fi1.fea create mode 100644 Tests/feaLib/data/spec5fi1.ttx create mode 100644 Tests/feaLib/data/spec5fi2.fea create mode 100644 Tests/feaLib/data/spec5fi2.ttx create mode 100644 Tests/feaLib/data/spec5fi3.fea create mode 100644 Tests/feaLib/data/spec5fi3.ttx create mode 100644 Tests/feaLib/data/spec5fi4.fea create mode 100644 Tests/feaLib/data/spec5fi4.ttx create mode 100644 Tests/feaLib/data/spec5h1.fea create mode 100644 Tests/feaLib/data/spec5h1.ttx create mode 100644 Tests/feaLib/data/spec6b_ii.fea create mode 100644 Tests/feaLib/data/spec6b_ii.ttx create mode 100644 Tests/feaLib/data/spec6d2.fea create mode 100644 Tests/feaLib/data/spec6d2.ttx create mode 100644 Tests/feaLib/data/spec6e.fea create mode 100644 Tests/feaLib/data/spec6e.ttx create mode 100644 Tests/feaLib/data/spec6f.fea create mode 100644 Tests/feaLib/data/spec6f.ttx create mode 100644 Tests/feaLib/data/spec6h_ii.fea create mode 100644 Tests/feaLib/data/spec6h_ii.ttx create mode 100644 Tests/feaLib/data/spec6h_iii_1.fea create mode 100644 Tests/feaLib/data/spec6h_iii_1.ttx create mode 100644 Tests/feaLib/data/spec6h_iii_3d.fea create mode 100644 Tests/feaLib/data/spec6h_iii_3d.ttx create mode 100644 Tests/feaLib/data/spec8a.fea create mode 100644 Tests/feaLib/data/spec8a.ttx create mode 100644 Tests/feaLib/data/spec8b.fea create mode 100644 Tests/feaLib/data/spec8b.ttx create mode 100644 Tests/feaLib/data/spec8c.fea create mode 100644 Tests/feaLib/data/spec8c.ttx create mode 100644 Tests/feaLib/data/spec8d.fea create mode 100644 Tests/feaLib/data/spec8d.ttx create mode 100644 Tests/feaLib/data/spec9a.fea create mode 100644 Tests/feaLib/data/spec9a.ttx create mode 100644 Tests/feaLib/data/spec9b.fea create mode 100644 Tests/feaLib/data/spec9b.ttx create mode 100644 Tests/feaLib/data/spec9c1.fea create mode 100644 Tests/feaLib/data/spec9c1.ttx create mode 100644 Tests/feaLib/data/spec9c2.fea create mode 100644 Tests/feaLib/data/spec9c2.ttx create mode 100644 Tests/feaLib/data/spec9c3.fea create mode 100644 Tests/feaLib/data/spec9c3.ttx create mode 100644 Tests/feaLib/data/spec9d.fea create mode 100644 Tests/feaLib/data/spec9d.ttx create mode 100644 Tests/feaLib/data/spec9e.fea create mode 100644 Tests/feaLib/data/spec9e.ttx create mode 100644 Tests/feaLib/data/spec9f.fea create mode 100644 Tests/feaLib/data/spec9f.ttx create mode 100644 Tests/feaLib/data/spec9g.fea create mode 100644 Tests/feaLib/data/spec9g.ttx create mode 100644 Tests/feaLib/error_test.py create mode 100644 Tests/feaLib/lexer_test.py create mode 100644 Tests/feaLib/parser_test.py create mode 100644 Tests/merge_test.py create mode 100644 Tests/misc/arrayTools_test.py create mode 100644 Tests/misc/bezierTools_test.py create mode 100644 Tests/misc/classifyTools_test.py create mode 100644 Tests/misc/eexec_test.py create mode 100644 Tests/misc/encodingTools_test.py create mode 100644 Tests/misc/filenames_test.py create mode 100644 Tests/misc/fixedTools_test.py create mode 100644 Tests/misc/loggingTools_test.py create mode 100644 Tests/misc/macRes_test.py create mode 100644 Tests/misc/psCharStrings_test.py create mode 100644 Tests/misc/py23_test.py create mode 100644 Tests/misc/testTools_test.py create mode 100644 Tests/misc/textTools_test.py create mode 100644 Tests/misc/timeTools_test.py create mode 100644 Tests/misc/transform_test.py create mode 100644 Tests/misc/xmlReader_test.py create mode 100644 Tests/misc/xmlWriter_test.py create mode 100644 Tests/mtiLib/data/featurename-backward.ttx.GSUB create mode 100644 Tests/mtiLib/data/featurename-backward.txt create mode 100644 Tests/mtiLib/data/featurename-forward.ttx.GSUB create mode 100644 Tests/mtiLib/data/featurename-forward.txt create mode 100644 Tests/mtiLib/data/lookupnames-backward.ttx.GSUB create mode 100644 Tests/mtiLib/data/lookupnames-backward.txt create mode 100644 Tests/mtiLib/data/lookupnames-forward.ttx.GSUB create mode 100644 Tests/mtiLib/data/lookupnames-forward.txt create mode 100644 Tests/mtiLib/data/mixed-toplevels.ttx.GSUB create mode 100644 Tests/mtiLib/data/mixed-toplevels.txt create mode 100644 Tests/mtiLib/data/mti/README create mode 100644 Tests/mtiLib/data/mti/chained-glyph.ttx.GPOS create mode 100644 Tests/mtiLib/data/mti/chained-glyph.ttx.GSUB create mode 100644 Tests/mtiLib/data/mti/chained-glyph.txt create mode 100644 Tests/mtiLib/data/mti/chainedclass.ttx.GSUB create mode 100644 Tests/mtiLib/data/mti/chainedclass.txt create mode 100644 Tests/mtiLib/data/mti/chainedcoverage.ttx.GSUB create mode 100644 Tests/mtiLib/data/mti/chainedcoverage.txt create mode 100644 Tests/mtiLib/data/mti/cmap.ttx create mode 100644 Tests/mtiLib/data/mti/cmap.ttx.cmap create mode 100644 Tests/mtiLib/data/mti/cmap.txt create mode 100644 Tests/mtiLib/data/mti/context-glyph.txt create mode 100644 Tests/mtiLib/data/mti/contextclass.txt create mode 100644 Tests/mtiLib/data/mti/contextcoverage.txt create mode 100644 Tests/mtiLib/data/mti/featuretable.txt create mode 100644 Tests/mtiLib/data/mti/gdefattach.ttx.GDEF create mode 100644 Tests/mtiLib/data/mti/gdefattach.txt create mode 100644 Tests/mtiLib/data/mti/gdefclasses.ttx.GDEF create mode 100644 Tests/mtiLib/data/mti/gdefclasses.txt create mode 100644 Tests/mtiLib/data/mti/gdefligcaret.ttx.GDEF create mode 100644 Tests/mtiLib/data/mti/gdefligcaret.txt create mode 100644 Tests/mtiLib/data/mti/gdefmarkattach.ttx.GDEF create mode 100644 Tests/mtiLib/data/mti/gdefmarkattach.txt create mode 100644 Tests/mtiLib/data/mti/gdefmarkfilter.ttx.GDEF create mode 100644 Tests/mtiLib/data/mti/gdefmarkfilter.txt create mode 100644 Tests/mtiLib/data/mti/gposcursive.ttx.GPOS create mode 100644 Tests/mtiLib/data/mti/gposcursive.txt create mode 100644 Tests/mtiLib/data/mti/gposkernset.ttx.GPOS create mode 100644 Tests/mtiLib/data/mti/gposkernset.txt create mode 100644 Tests/mtiLib/data/mti/gposmarktobase.ttx.GPOS create mode 100644 Tests/mtiLib/data/mti/gposmarktobase.txt create mode 100644 Tests/mtiLib/data/mti/gpospairclass.ttx.GPOS create mode 100644 Tests/mtiLib/data/mti/gpospairclass.txt create mode 100644 Tests/mtiLib/data/mti/gpospairglyph.ttx.GPOS create mode 100644 Tests/mtiLib/data/mti/gpospairglyph.txt create mode 100644 Tests/mtiLib/data/mti/gpossingle.ttx.GPOS create mode 100644 Tests/mtiLib/data/mti/gpossingle.txt create mode 100644 Tests/mtiLib/data/mti/gsubalternate.ttx.GSUB create mode 100644 Tests/mtiLib/data/mti/gsubalternate.txt create mode 100644 Tests/mtiLib/data/mti/gsubligature.ttx.GSUB create mode 100644 Tests/mtiLib/data/mti/gsubligature.txt create mode 100644 Tests/mtiLib/data/mti/gsubmultiple.ttx.GSUB create mode 100644 Tests/mtiLib/data/mti/gsubmultiple.txt create mode 100644 Tests/mtiLib/data/mti/gsubreversechanined.ttx.GSUB create mode 100644 Tests/mtiLib/data/mti/gsubreversechanined.txt create mode 100644 Tests/mtiLib/data/mti/gsubsingle.ttx.GSUB create mode 100644 Tests/mtiLib/data/mti/gsubsingle.txt create mode 100644 Tests/mtiLib/data/mti/mark-to-ligature.ttx.GPOS create mode 100644 Tests/mtiLib/data/mti/mark-to-ligature.txt create mode 100644 Tests/mtiLib/data/mti/scripttable.ttx.GPOS create mode 100644 Tests/mtiLib/data/mti/scripttable.ttx.GSUB create mode 100644 Tests/mtiLib/data/mti/scripttable.txt create mode 100644 Tests/mtiLib/mti_test.py create mode 100644 Tests/otlLib/builder_test.py create mode 100644 Tests/pens/areaPen_test.py create mode 100644 Tests/pens/basePen_test.py create mode 100644 Tests/pens/boundsPen_test.py create mode 100644 Tests/pens/perimeterPen_test.py create mode 100644 Tests/pens/pointInsidePen_test.py create mode 100644 Tests/pens/recordingPen_test.py create mode 100644 Tests/pens/reverseContourPen_test.py create mode 100644 Tests/pens/t2CharStringPen_test.py create mode 100644 Tests/pens/ttGlyphPen_test.py create mode 100644 Tests/subset/data/Lobster.subset.ttx create mode 100644 Tests/subset/data/NotdefWidthCID-Regular.ttx create mode 100644 Tests/subset/data/TestANKR.ttx create mode 100644 Tests/subset/data/TestBSLN-0.ttx create mode 100644 Tests/subset/data/TestBSLN-1.ttx create mode 100644 Tests/subset/data/TestBSLN-2.ttx create mode 100644 Tests/subset/data/TestBSLN-3.ttx create mode 100644 Tests/subset/data/TestCID-Regular.ttx create mode 100644 Tests/subset/data/TestCLR-Regular.ttx create mode 100644 Tests/subset/data/TestGVAR.ttx create mode 100644 Tests/subset/data/TestLCAR-0.ttx create mode 100644 Tests/subset/data/TestLCAR-1.ttx create mode 100644 Tests/subset/data/TestMATH-Regular.ttx create mode 100644 Tests/subset/data/TestOPBD-0.ttx create mode 100644 Tests/subset/data/TestOPBD-1.ttx create mode 100644 Tests/subset/data/TestOTF-Regular.ttx create mode 100644 Tests/subset/data/TestPROP.ttx create mode 100644 Tests/subset/data/TestTTF-Regular.ttx create mode 100644 Tests/subset/data/TestTTF-Regular_non_BMP_char.ttx create mode 100644 Tests/subset/data/expect_ankr.ttx create mode 100644 Tests/subset/data/expect_bsln_0.ttx create mode 100644 Tests/subset/data/expect_bsln_1.ttx create mode 100644 Tests/subset/data/expect_bsln_2.ttx create mode 100644 Tests/subset/data/expect_bsln_3.ttx create mode 100644 Tests/subset/data/expect_desubroutinize_CFF.ttx create mode 100644 Tests/subset/data/expect_keep_colr.ttx create mode 100644 Tests/subset/data/expect_keep_gvar.ttx create mode 100644 Tests/subset/data/expect_keep_gvar_notdef_outline.ttx create mode 100644 Tests/subset/data/expect_keep_math.ttx create mode 100644 Tests/subset/data/expect_lcar_0.ttx create mode 100644 Tests/subset/data/expect_lcar_1.ttx create mode 100644 Tests/subset/data/expect_no_hinting_CFF.ttx create mode 100644 Tests/subset/data/expect_no_hinting_TTF.ttx create mode 100644 Tests/subset/data/expect_no_hinting_desubroutinize_CFF.ttx create mode 100644 Tests/subset/data/expect_no_notdef_outline_cid.ttx create mode 100644 Tests/subset/data/expect_no_notdef_outline_otf.ttx create mode 100644 Tests/subset/data/expect_no_notdef_outline_ttf.ttx create mode 100644 Tests/subset/data/expect_notdef_width_cid.ttx create mode 100644 Tests/subset/data/expect_opbd_0.ttx create mode 100644 Tests/subset/data/expect_opbd_1.ttx create mode 100644 Tests/subset/data/expect_prop_0.ttx create mode 100644 Tests/subset/data/expect_prop_1.ttx create mode 100644 Tests/subset/data/google_color.ttx create mode 100644 Tests/subset/subset_test.py create mode 100644 Tests/svgLib/path/__init__.py create mode 100644 Tests/svgLib/path/parser_test.py create mode 100644 Tests/svgLib/path/path_test.py create mode 100644 Tests/t1Lib/data/TestT1-Regular.lwfn create mode 100644 Tests/t1Lib/data/TestT1-Regular.pfa create mode 100644 Tests/t1Lib/data/TestT1-Regular.pfb create mode 100644 Tests/t1Lib/t1Lib_test.py create mode 100644 Tests/ttLib/data/TestOTF-Regular.otx create mode 100644 Tests/ttLib/data/TestTTF-Regular.ttx create mode 100644 Tests/ttLib/data/TestTTFComplex-Regular.ttx create mode 100644 Tests/ttLib/data/test_woff2_metadata.xml create mode 100644 Tests/ttLib/sfnt_test.py create mode 100644 Tests/ttLib/tables/C_F_F__2_test.py create mode 100644 Tests/ttLib/tables/C_F_F_test.py create mode 100644 Tests/ttLib/tables/C_P_A_L_test.py create mode 100644 Tests/ttLib/tables/M_V_A_R_test.py create mode 100644 Tests/ttLib/tables/O_S_2f_2_test.py create mode 100644 Tests/ttLib/tables/S_T_A_T_test.py create mode 100644 Tests/ttLib/tables/T_S_I__0_test.py create mode 100644 Tests/ttLib/tables/T_S_I__1_test.py create mode 100644 Tests/ttLib/tables/TupleVariation_test.py create mode 100644 Tests/ttLib/tables/_a_n_k_r_test.py create mode 100644 Tests/ttLib/tables/_a_v_a_r_test.py create mode 100644 Tests/ttLib/tables/_b_s_l_n_test.py create mode 100644 Tests/ttLib/tables/_c_i_d_g_test.py create mode 100644 Tests/ttLib/tables/_c_m_a_p_test.py create mode 100644 Tests/ttLib/tables/_c_v_a_r_test.py create mode 100644 Tests/ttLib/tables/_f_p_g_m_test.py create mode 100644 Tests/ttLib/tables/_f_v_a_r_test.py create mode 100644 Tests/ttLib/tables/_g_c_i_d_test.py create mode 100644 Tests/ttLib/tables/_g_l_y_f_test.py create mode 100644 Tests/ttLib/tables/_g_v_a_r_test.py create mode 100644 Tests/ttLib/tables/_h_h_e_a_test.py create mode 100644 Tests/ttLib/tables/_h_m_t_x_test.py create mode 100644 Tests/ttLib/tables/_k_e_r_n_test.py create mode 100644 Tests/ttLib/tables/_l_c_a_r_test.py create mode 100644 Tests/ttLib/tables/_l_t_a_g_test.py create mode 100644 Tests/ttLib/tables/_m_e_t_a_test.py create mode 100644 Tests/ttLib/tables/_m_o_r_t_test.py create mode 100644 Tests/ttLib/tables/_m_o_r_x_test.py create mode 100644 Tests/ttLib/tables/_n_a_m_e_test.py create mode 100644 Tests/ttLib/tables/_o_p_b_d_test.py create mode 100644 Tests/ttLib/tables/_p_r_o_p_test.py create mode 100644 Tests/ttLib/tables/_t_r_a_k_test.py create mode 100644 Tests/ttLib/tables/_v_h_e_a_test.py create mode 100644 Tests/ttLib/tables/_v_m_t_x_test.py create mode 100644 Tests/ttLib/tables/data/C_F_F_.bin create mode 100644 Tests/ttLib/tables/data/C_F_F_.ttx create mode 100644 Tests/ttLib/tables/data/C_F_F__2.bin create mode 100644 Tests/ttLib/tables/data/C_F_F__2.ttx create mode 100644 Tests/ttLib/tables/data/_h_h_e_a_recalc_OTF.ttx create mode 100644 Tests/ttLib/tables/data/_h_h_e_a_recalc_TTF.ttx create mode 100644 Tests/ttLib/tables/data/_h_h_e_a_recalc_empty.ttx create mode 100644 Tests/ttLib/tables/data/_v_h_e_a_recalc_OTF.ttx create mode 100644 Tests/ttLib/tables/data/_v_h_e_a_recalc_TTF.ttx create mode 100644 Tests/ttLib/tables/data/_v_h_e_a_recalc_empty.ttx create mode 100644 Tests/ttLib/tables/data/aots/README create mode 100644 Tests/ttLib/tables/data/aots/base.otf create mode 100644 Tests/ttLib/tables/data/aots/base.ttx.CFF create mode 100644 Tests/ttLib/tables/data/aots/base.ttx.OS_2 create mode 100644 Tests/ttLib/tables/data/aots/base.ttx.cmap create mode 100644 Tests/ttLib/tables/data/aots/base.ttx.head create mode 100644 Tests/ttLib/tables/data/aots/base.ttx.hhea create mode 100644 Tests/ttLib/tables/data/aots/base.ttx.hmtx create mode 100644 Tests/ttLib/tables/data/aots/base.ttx.maxp create mode 100644 Tests/ttLib/tables/data/aots/base.ttx.name create mode 100644 Tests/ttLib/tables/data/aots/base.ttx.post create mode 100644 Tests/ttLib/tables/data/aots/classdef1_font1.otf create mode 100644 Tests/ttLib/tables/data/aots/classdef1_font1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/classdef1_font2.otf create mode 100644 Tests/ttLib/tables/data/aots/classdef1_font2.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/classdef1_font3.otf create mode 100644 Tests/ttLib/tables/data/aots/classdef1_font3.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/classdef1_font4.otf create mode 100644 Tests/ttLib/tables/data/aots/classdef1_font4.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/classdef2_font1.otf create mode 100644 Tests/ttLib/tables/data/aots/classdef2_font1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/classdef2_font2.otf create mode 100644 Tests/ttLib/tables/data/aots/classdef2_font2.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/classdef2_font3.otf create mode 100644 Tests/ttLib/tables/data/aots/classdef2_font3.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/classdef2_font4.otf create mode 100644 Tests/ttLib/tables/data/aots/classdef2_font4.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/cmap0_font1.otf create mode 100644 Tests/ttLib/tables/data/aots/cmap0_font1.ttx.cmap create mode 100644 Tests/ttLib/tables/data/aots/cmap10_font1.otf create mode 100644 Tests/ttLib/tables/data/aots/cmap10_font1.ttx.cmap create mode 100644 Tests/ttLib/tables/data/aots/cmap10_font2.otf create mode 100644 Tests/ttLib/tables/data/aots/cmap10_font2.ttx.cmap create mode 100644 Tests/ttLib/tables/data/aots/cmap12_font1.otf create mode 100644 Tests/ttLib/tables/data/aots/cmap12_font1.ttx.cmap create mode 100644 Tests/ttLib/tables/data/aots/cmap14_font1.otf create mode 100644 Tests/ttLib/tables/data/aots/cmap14_font1.ttx.cmap create mode 100644 Tests/ttLib/tables/data/aots/cmap2_font1.otf create mode 100644 Tests/ttLib/tables/data/aots/cmap2_font1.ttx.cmap create mode 100644 Tests/ttLib/tables/data/aots/cmap4_font1.otf create mode 100644 Tests/ttLib/tables/data/aots/cmap4_font1.ttx.cmap create mode 100644 Tests/ttLib/tables/data/aots/cmap4_font2.otf create mode 100644 Tests/ttLib/tables/data/aots/cmap4_font2.ttx.cmap create mode 100644 Tests/ttLib/tables/data/aots/cmap4_font3.otf create mode 100644 Tests/ttLib/tables/data/aots/cmap4_font3.ttx.cmap create mode 100644 Tests/ttLib/tables/data/aots/cmap4_font4.otf create mode 100644 Tests/ttLib/tables/data/aots/cmap4_font4.ttx.cmap create mode 100644 Tests/ttLib/tables/data/aots/cmap6_font1.otf create mode 100644 Tests/ttLib/tables/data/aots/cmap6_font1.ttx.cmap create mode 100644 Tests/ttLib/tables/data/aots/cmap6_font2.otf create mode 100644 Tests/ttLib/tables/data/aots/cmap6_font2.ttx.cmap create mode 100644 Tests/ttLib/tables/data/aots/cmap8_font1.otf create mode 100644 Tests/ttLib/tables/data/aots/cmap8_font1.ttx.cmap create mode 100644 Tests/ttLib/tables/data/aots/cmap_composition_font1.otf create mode 100644 Tests/ttLib/tables/data/aots/cmap_composition_font1.ttx.cmap create mode 100644 Tests/ttLib/tables/data/aots/cmap_subtableselection_font1.otf create mode 100644 Tests/ttLib/tables/data/aots/cmap_subtableselection_font1.ttx.cmap create mode 100644 Tests/ttLib/tables/data/aots/cmap_subtableselection_font2.otf create mode 100644 Tests/ttLib/tables/data/aots/cmap_subtableselection_font2.ttx.cmap create mode 100644 Tests/ttLib/tables/data/aots/cmap_subtableselection_font3.otf create mode 100644 Tests/ttLib/tables/data/aots/cmap_subtableselection_font3.ttx.cmap create mode 100644 Tests/ttLib/tables/data/aots/cmap_subtableselection_font4.otf create mode 100644 Tests/ttLib/tables/data/aots/cmap_subtableselection_font4.ttx.cmap create mode 100644 Tests/ttLib/tables/data/aots/cmap_subtableselection_font5.otf create mode 100644 Tests/ttLib/tables/data/aots/cmap_subtableselection_font5.ttx.cmap create mode 100644 Tests/ttLib/tables/data/aots/gpos1_1_lookupflag_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos1_1_lookupflag_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos1_1_lookupflag_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos1_1_simple_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos1_1_simple_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos1_1_simple_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos1_1_simple_f2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos1_1_simple_f3.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos1_1_simple_f3.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos1_1_simple_f4.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos1_1_simple_f4.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos1_2_font1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos1_2_font1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos1_2_font2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos1_2_font2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos1_2_font2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos2_1_font6.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos2_1_font6.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos2_1_font7.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos2_1_font7.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos2_1_next_glyph_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos2_1_next_glyph_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos2_1_next_glyph_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos2_1_next_glyph_f2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos2_1_simple_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos2_1_simple_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos2_2_font1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos2_2_font1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos2_2_font2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos2_2_font2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos2_2_font2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos2_2_font3.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos2_2_font3.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos2_2_font3.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos2_2_font4.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos2_2_font4.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos2_2_font5.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos2_2_font5.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos3_font1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos3_font1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos3_font2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos3_font2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos3_font2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos3_font3.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos3_font3.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos3_font3.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos4_lookupflag_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos4_lookupflag_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos4_lookupflag_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos4_lookupflag_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos4_lookupflag_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos4_lookupflag_f2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos4_multiple_anchors_1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos4_multiple_anchors_1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos4_multiple_anchors_1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos4_simple_1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos4_simple_1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos4_simple_1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos5_font1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos5_font1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos5_font1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos5_font1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gpos6_font1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos6_font1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos6_font1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos7_1_font1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos7_1_font1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos9_font1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos9_font1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos9_font2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos9_font2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f3.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f3.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f3.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f4.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f4.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f4.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_lookupflag_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_lookupflag_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_lookupflag_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_next_glyph_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_next_glyph_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_next_glyph_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_successive_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_successive_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining1_successive_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f3.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f3.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f3.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f4.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f4.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f4.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_lookupflag_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_lookupflag_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_lookupflag_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_multiple_subrules_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_multiple_subrules_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_multiple_subrules_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_multiple_subrules_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_multiple_subrules_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_multiple_subrules_f2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_next_glyph_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_next_glyph_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_next_glyph_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_successive_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_successive_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining2_successive_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f3.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f3.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f3.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f4.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f4.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f4.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_lookupflag_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_lookupflag_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_lookupflag_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_next_glyph_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_next_glyph_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_next_glyph_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_successive_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_successive_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_chaining3_successive_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_boundary_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_boundary_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_boundary_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_boundary_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_boundary_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_boundary_f2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_expansion_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_expansion_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_expansion_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_next_glyph_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_next_glyph_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_next_glyph_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_simple_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_simple_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_simple_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_simple_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_simple_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_simple_f2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_successive_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_successive_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context1_successive_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_boundary_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_boundary_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_boundary_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_boundary_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_boundary_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_boundary_f2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_classes_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_classes_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_classes_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_classes_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_classes_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_classes_f2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_expansion_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_expansion_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_expansion_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_next_glyph_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_next_glyph_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_next_glyph_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_simple_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_simple_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_simple_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_simple_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_simple_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_simple_f2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_successive_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_successive_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context2_successive_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context3_boundary_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context3_boundary_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context3_boundary_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context3_boundary_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context3_boundary_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context3_boundary_f2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f2.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context3_next_glyph_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context3_next_glyph_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context3_next_glyph_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context3_simple_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context3_simple_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context3_simple_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gpos_context3_successive_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gpos_context3_successive_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gpos_context3_successive_f1.ttx.GPOS create mode 100644 Tests/ttLib/tables/data/aots/gsub1_1_lookupflag_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub1_1_lookupflag_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub1_1_lookupflag_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub1_1_modulo_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub1_1_modulo_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub1_1_simple_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub1_1_simple_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub1_2_lookupflag_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub1_2_lookupflag_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub1_2_lookupflag_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub1_2_simple_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub1_2_simple_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub2_1_lookupflag_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub2_1_lookupflag_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub2_1_lookupflag_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub2_1_multiple_sequences_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub2_1_multiple_sequences_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub2_1_simple_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub2_1_simple_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub3_1_lookupflag_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub3_1_lookupflag_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub3_1_lookupflag_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub3_1_multiple_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub3_1_multiple_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub3_1_simple_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub3_1_simple_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub4_1_lookupflag_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub4_1_lookupflag_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub4_1_lookupflag_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub4_1_multiple_ligatures_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub4_1_multiple_ligatures_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub4_1_multiple_ligatures_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub4_1_multiple_ligatures_f2.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub4_1_multiple_ligsets_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub4_1_multiple_ligsets_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub4_1_simple_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub4_1_simple_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub7_font1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub7_font1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub7_font2.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub7_font2.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f2.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f3.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f3.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f3.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f4.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f4.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f4.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_lookupflag_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_lookupflag_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_lookupflag_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_multiple_subrules_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_multiple_subrules_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_multiple_subrules_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_multiple_subrules_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_multiple_subrules_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_multiple_subrules_f2.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_next_glyph_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_next_glyph_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_next_glyph_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f2.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_successive_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_successive_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining1_successive_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f2.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f3.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f3.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f3.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f4.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f4.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f4.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_lookupflag_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_lookupflag_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_lookupflag_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f2.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_next_glyph_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_next_glyph_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_next_glyph_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f2.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_successive_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_successive_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining2_successive_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f2.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f3.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f3.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f3.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f4.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f4.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f4.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_lookupflag_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_lookupflag_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_lookupflag_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_next_glyph_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_next_glyph_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_next_glyph_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f2.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_successive_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_successive_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_chaining3_successive_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_boundary_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_boundary_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_boundary_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_boundary_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_boundary_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_boundary_f2.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_expansion_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_expansion_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_expansion_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f2.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f2.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_next_glyph_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_next_glyph_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_next_glyph_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_simple_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_simple_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_simple_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_simple_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_simple_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_simple_f2.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_successive_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_successive_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context1_successive_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_boundary_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_boundary_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_boundary_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_boundary_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_boundary_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_boundary_f2.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_classes_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_classes_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_classes_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_classes_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_classes_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_classes_f2.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_expansion_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_expansion_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_expansion_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f2.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f2.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_next_glyph_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_next_glyph_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_next_glyph_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_simple_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_simple_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_simple_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_simple_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_simple_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_simple_f2.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_successive_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_successive_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context2_successive_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context3_boundary_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context3_boundary_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context3_boundary_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context3_boundary_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context3_boundary_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context3_boundary_f2.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context3_lookupflag_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context3_lookupflag_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context3_lookupflag_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context3_lookupflag_f2.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context3_lookupflag_f2.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context3_lookupflag_f2.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context3_next_glyph_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context3_next_glyph_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context3_next_glyph_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context3_simple_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context3_simple_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context3_simple_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/gsub_context3_successive_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/gsub_context3_successive_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/gsub_context3_successive_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/lookupflag_ignore_attach_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/lookupflag_ignore_attach_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/lookupflag_ignore_attach_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/lookupflag_ignore_base_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/lookupflag_ignore_base_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/lookupflag_ignore_base_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/lookupflag_ignore_combination_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/lookupflag_ignore_combination_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/lookupflag_ignore_combination_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/lookupflag_ignore_ligatures_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/lookupflag_ignore_ligatures_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/lookupflag_ignore_ligatures_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/aots/lookupflag_ignore_marks_f1.otf create mode 100644 Tests/ttLib/tables/data/aots/lookupflag_ignore_marks_f1.ttx.GDEF create mode 100644 Tests/ttLib/tables/data/aots/lookupflag_ignore_marks_f1.ttx.GSUB create mode 100644 Tests/ttLib/tables/data/graphite/graphite_tests.ttf create mode 100644 Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Feat create mode 100644 Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Glat create mode 100644 Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Glat.setup create mode 100644 Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Silf create mode 100644 Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Silf.setup create mode 100644 Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Sill create mode 100644 Tests/ttLib/tables/data/ttProgram.ttx create mode 100644 Tests/ttLib/tables/otBase_test.py create mode 100644 Tests/ttLib/tables/otConverters_test.py create mode 100644 Tests/ttLib/tables/otTables_test.py create mode 100644 Tests/ttLib/tables/tables_test.py create mode 100644 Tests/ttLib/tables/ttProgram_test.py create mode 100644 Tests/ttLib/woff2_test.py create mode 100644 Tests/ttx/data/TestBOM.ttx create mode 100644 Tests/ttx/data/TestDFONT.dfont create mode 100644 Tests/ttx/data/TestNoSFNT.ttx create mode 100644 Tests/ttx/data/TestNoXML.ttx create mode 100644 Tests/ttx/data/TestOTF.otf create mode 100644 Tests/ttx/data/TestOTF.ttx create mode 100644 Tests/ttx/data/TestTTC.ttc create mode 100644 Tests/ttx/data/TestTTF.ttf create mode 100644 Tests/ttx/data/TestTTF.ttx create mode 100644 Tests/ttx/data/TestWOFF.woff create mode 100644 Tests/ttx/data/TestWOFF2.woff2 create mode 100644 Tests/ttx/ttx_test.py create mode 100644 Tests/unicodedata_test.py create mode 100644 Tests/varLib/__init__.py create mode 100644 Tests/varLib/builder_test.py create mode 100644 Tests/varLib/data/Build.designspace create mode 100644 Tests/varLib/data/BuildAvarEmptyAxis.designspace create mode 100644 Tests/varLib/data/BuildAvarIdentityMaps.designspace create mode 100644 Tests/varLib/data/BuildAvarSingleAxis.designspace create mode 100644 Tests/varLib/data/Designspace.designspace create mode 100644 Tests/varLib/data/Designspace2.designspace create mode 100644 Tests/varLib/data/InterpolateLayout.designspace create mode 100644 Tests/varLib/data/InterpolateLayout2.designspace create mode 100644 Tests/varLib/data/InterpolateLayout3.designspace create mode 100644 Tests/varLib/data/master_ttx_interpolatable_otf/TestFamily2-Master0.ttx create mode 100644 Tests/varLib/data/master_ttx_interpolatable_otf/TestFamily2-Master1.ttx create mode 100644 Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master0.ttx create mode 100644 Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master1.ttx create mode 100644 Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master2.ttx create mode 100644 Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master3.ttx create mode 100644 Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master4.ttx create mode 100644 Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily2-Master0.ttx create mode 100644 Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily2-Master1.ttx create mode 100644 Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Bold.ttx create mode 100644 Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Condensed.ttx create mode 100644 Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-CondensedBold.ttx create mode 100644 Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-CondensedLight.ttx create mode 100644 Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-CondensedSemiBold.ttx create mode 100644 Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Light.ttx create mode 100644 Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Regular.ttx create mode 100644 Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-SemiBold.ttx create mode 100755 Tests/varLib/data/master_ttx_varfont_ttf/Mutator_IUP.ttx create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/features.fea create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/fontinfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/A_.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/_notdef.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/a.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/contents.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/dollar.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/dollar.nostroke.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/space.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/lib.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/metainfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/features.fea create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/fontinfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/A_.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/_notdef.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/a.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/contents.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/dollar.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/dollar.nostroke.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/space.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/lib.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/metainfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/features.fea create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/fontinfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/A_.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/_notdef.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/a.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/contents.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/dollar.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/dollar.nostroke.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/space.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/lib.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/metainfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/features.fea create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/fontinfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/A_.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/_notdef.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/a.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/contents.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/dollar.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/dollar.nostroke.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/space.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/lib.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/metainfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/features.fea create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/fontinfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/A_.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/_notdef.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/a.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/contents.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/dollar.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/dollar.nostroke.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/space.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/lib.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/metainfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/features.fea create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/fontinfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/A_.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/A_.sc.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/_notdef.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/a.alt.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/a.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/ampersand.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/atilde.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/circledotted.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/contents.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/d.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/dieresisbelowcmb.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/dieresiscmb.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/f.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/f_t.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/n.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/space.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/t.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/tildebelowcmb.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/tildecmb.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/lib.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/metainfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/features.fea create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/fontinfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/A_.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/A_.sc.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/_notdef.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/a.alt.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/a.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/ampersand.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/atilde.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/circledotted.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/contents.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/d.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/dieresisbelowcmb.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/dieresiscmb.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/f.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/f_t.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/n.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/space.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/t.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/tildebelowcmb.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/tildecmb.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/lib.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/metainfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/fontinfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/F_.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/T_.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/contents.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/l.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/layerinfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/n.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/o.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/s.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/t.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/groups.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/kerning.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/layercontents.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/lib.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/metainfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/fontinfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/F_.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/T_.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/contents.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/l.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/layerinfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/n.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/o.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/s.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/t.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/groups.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/kerning.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/layercontents.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/lib.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/metainfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/fontinfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/F_.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/T_.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/contents.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/l.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/layerinfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/n.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/o.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/s.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/t.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/groups.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/kerning.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/layercontents.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/lib.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/metainfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/fontinfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/F_.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/T_.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/contents.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/l.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/layerinfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/n.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/o.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/s.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/t.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/groups.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/kerning.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/layercontents.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/lib.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/metainfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/fontinfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/F_.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/T_.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/contents.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/l.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/layerinfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/n.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/o.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/s.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/t.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/groups.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/kerning.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/layercontents.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/lib.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/metainfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/fontinfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/F_.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/T_.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/contents.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/l.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/layerinfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/n.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/o.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/s.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/t.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/groups.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/kerning.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/layercontents.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/lib.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/metainfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/fontinfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/F_.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/T_.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/contents.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/l.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/layerinfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/n.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/o.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/s.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/t.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/groups.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/kerning.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/layercontents.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/lib.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/metainfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/fontinfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/F_.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/T_.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/contents.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/l.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/layerinfo.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/n.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/o.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/s.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/t.glif create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/groups.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/kerning.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/layercontents.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/lib.plist create mode 100644 Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/metainfo.plist create mode 100644 Tests/varLib/data/test_results/Build.ttx create mode 100644 Tests/varLib/data/test_results/Build3.ttx create mode 100644 Tests/varLib/data/test_results/BuildAvarEmptyAxis.ttx create mode 100644 Tests/varLib/data/test_results/BuildAvarIdentityMaps.ttx create mode 100644 Tests/varLib/data/test_results/BuildAvarSingleAxis.ttx create mode 100644 Tests/varLib/data/test_results/BuildMain.ttx create mode 100644 Tests/varLib/data/test_results/InterpolateLayout.ttx create mode 100644 Tests/varLib/data/test_results/InterpolateLayout2.ttx create mode 100644 Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_diff.ttx create mode 100644 Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_diff2.ttx create mode 100644 Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_same.ttx create mode 100644 Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_class_diff.ttx create mode 100644 Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_class_diff2.ttx create mode 100644 Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_class_same.ttx create mode 100644 Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_spec_diff.ttx create mode 100644 Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_spec_diff2.ttx create mode 100644 Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_spec_same.ttx create mode 100644 Tests/varLib/data/test_results/InterpolateLayoutGPOS_3_diff.ttx create mode 100644 Tests/varLib/data/test_results/InterpolateLayoutGPOS_3_same.ttx create mode 100644 Tests/varLib/data/test_results/InterpolateLayoutGPOS_4_diff.ttx create mode 100644 Tests/varLib/data/test_results/InterpolateLayoutGPOS_4_same.ttx create mode 100644 Tests/varLib/data/test_results/InterpolateLayoutGPOS_5_diff.ttx create mode 100644 Tests/varLib/data/test_results/InterpolateLayoutGPOS_5_same.ttx create mode 100644 Tests/varLib/data/test_results/InterpolateLayoutGPOS_6_diff.ttx create mode 100644 Tests/varLib/data/test_results/InterpolateLayoutGPOS_6_same.ttx create mode 100644 Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx create mode 100644 Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_same.ttx create mode 100644 Tests/varLib/data/test_results/InterpolateLayoutGPOS_size_feat_same.ttx create mode 100644 Tests/varLib/data/test_results/InterpolateLayoutMain.ttx create mode 100644 Tests/varLib/data/test_results/Mutator.ttx create mode 100755 Tests/varLib/data/test_results/Mutator_IUP-instance.ttx create mode 100644 Tests/varLib/designspace_test.py create mode 100644 Tests/varLib/interpolatable_test.py create mode 100644 Tests/varLib/interpolate_layout_test.py create mode 100644 Tests/varLib/models_test.py create mode 100644 Tests/varLib/mutator_test.py create mode 100644 Tests/varLib/varLib_test.py create mode 100644 Tests/voltLib/lexer_test.py create mode 100644 Tests/voltLib/parser_test.py delete mode 120000 Tools/fontTools delete mode 100755 Tools/pyftinspect delete mode 100755 Tools/pyftmerge delete mode 100755 Tools/pyftsubset delete mode 100755 Tools/ttx delete mode 100644 Windows/README.TXT delete mode 100644 Windows/fonttools-win-setup.iss delete mode 100644 Windows/fonttools-win-setup.txt delete mode 100755 Windows/mcmillan.bat delete mode 100644 Windows/ttx.ico create mode 100644 dev-requirements.txt create mode 100755 fonttools create mode 100644 requirements.txt create mode 100755 run-tests.sh create mode 100644 setup.cfg create mode 100644 tox.ini diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 0000000..f04db29 --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,62 @@ +environment: + matrix: + - JOB: "2.7 32-bit" + PYTHON_HOME: "C:\\Python27" + TOXENV: "py27-cov" + TOXPYTHON: "C:\\Python27\\python.exe" + + - JOB: "3.6 32-bit" + PYTHON_HOME: "C:\\Python36" + TOXENV: "py36-cov" + TOXPYTHON: "C:\\Python36\\python.exe" + + - JOB: "2.7 64-bit" + PYTHON_HOME: "C:\\Python27-x64" + TOXENV: "py27-cov" + TOXPYTHON: "C:\\Python27-x64\\python.exe" + + - JOB: "3.6 64-bit" + PYTHON_HOME: "C:\\Python36-x64" + TOXENV: "py36-cov" + TOXPYTHON: "C:\\Python36-x64\\python.exe" + +install: + # If there is a newer build queued for the same PR, cancel this one. + # The AppVeyor 'rollout builds' option is supposed to serve the same + # purpose but it is problematic because it tends to cancel builds pushed + # directly to master instead of just PR builds (or the converse). + # credits: JuliaLang developers. + - ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod ` + https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | ` + Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { ` + throw "There are newer queued builds for this pull request, failing early." } + + # Prepend Python to the PATH of this build + - "SET PATH=%PYTHON_HOME%;%PYTHON_HOME%\\Scripts;%PATH%" + + # check that we have the expected version and architecture for Python + - "python --version" + - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" + + # upgrade pip and setuptools to avoid out-of-date warnings + - "python -m pip install --disable-pip-version-check --user --upgrade pip setuptools" + + # install the dependencies to run the tests + - "python -m pip install tox" + + +build: false + +test_script: + - "tox" + +after_test: + - "tox -e codecov" + +notifications: + - provider: Email + to: + - fonttools-dev@googlegroups.com + on_build_success: false + on_build_failure: true + on_build_status_changed: true diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..5e7474d --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,5 @@ +comment: false +coverage: + status: + project: off + patch: off diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..16b5756 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,34 @@ +[run] +# measure 'branch' coverage in addition to 'statement' coverage +# See: http://coverage.readthedocs.org/en/coverage-4.0.3/branch.html#branch +branch = True + +# list of directories or packages to measure +source = fontTools + +# these are treated as equivalent when combining data +[paths] +source = + Lib/fontTools + .tox/*/lib/python*/site-packages/fontTools + .tox/pypy*/site-packages/fontTools + +[report] +# Regexes for lines to exclude from consideration +exclude_lines = + # keywords to use in inline comments to skip coverage + pragma: no cover + + # don't complain if tests don't hit defensive assertion code + raise AssertionError + raise NotImplementedError + + # don't complain if non-runnable code isn't run + if 0: + if __name__ == .__main__.: + +# ignore source code that can’t be found +ignore_errors = True + +# when running a summary report, show missing lines +show_missing = True diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a1cd77d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,104 @@ +sudo: false + +language: python +python: 3.5 + +# empty "env:" is needed for 'allow_failures' +# https://docs.travis-ci.com/user/customizing-the-build/#Rows-that-are-Allowed-to-Fail +env: + +matrix: + fast_finish: true + exclude: + # Exclude the default Python 3.5 build + - python: 3.5 + include: + - python: 2.7 + env: TOXENV=py27-cov + - python: 3.4 + env: TOXENV=py34-cov + - python: 3.5 + env: TOXENV=py35-cov + - python: 3.6 + env: + - TOXENV=py36-cov + - BUILD_DIST=true + - python: pypy2.7-5.8.0 + # disable coverage.py on pypy because of performance problems + env: TOXENV=pypy-nocov + - language: generic + os: osx + env: TOXENV=py27-cov + - language: generic + os: osx + env: + - TOXENV=py36-cov + - HOMEBREW_NO_AUTO_UPDATE=1 + - env: + - TOXENV=py27-nocov + - PYENV_VERSION='2.7.6' + - PYENV_VERSION_STRING='Python 2.7.6' + - PYENV_ROOT=$HOME/.travis-pyenv + - TRAVIS_PYENV_VERSION='0.4.0' + allow_failures: + # We use fast_finish + allow_failures because OSX builds take forever + # https://blog.travis-ci.com/2013-11-27-fast-finishing-builds + - language: generic + os: osx + env: TOXENV=py27-cov + - language: generic + os: osx + env: + - TOXENV=py36-cov + - HOMEBREW_NO_AUTO_UPDATE=1 + +cache: + - pip + - directories: + - $HOME/.pyenv_cache + +before_install: + - source ./.travis/before_install.sh + +install: + - ./.travis/install.sh + +script: + - ./.travis/run.sh + +after_success: + - ./.travis/after_success.sh + +before_deploy: + - ./.travis/before_deploy.sh + +notifications: + irc: "irc.freenode.org##fonts" + email: fonttools-dev@googlegroups.com + +deploy: + # deploy to Github Releases on tags + - provider: releases + api_key: + secure: KEcWhJxMcnKay7wmWJCpg2W5GWHTQ+LaRbqGM11IKGcQuEOFxWuG7W1xjGpVdKPj/MQ+cG0b9hGUFpls1hwseOA1HANMv4xjCgYkuvT1OdpX/KOcZ7gfe/qaovzVxHyP9xwohnHSJMb790t37fmDfFUSROx3iEexIX09LLoDjO8= + skip_cleanup: true + file_glob: true + file: "dist/*" + on: + tags: true + repo: fonttools/fonttools + all_branches: true + condition: "$BUILD_DIST == true" + # deploy to PyPI on tags + - provider: pypi + server: https://upload.pypi.org/legacy/ + user: anthrotype + password: + secure: Dz3x8kh4ergBV6qZUgcGVDOEzjoCEFzzQiO5WVw4Zfi04DD8+d1ghmMz2BY4UvoVKSsFrfKDuEB3MCWyqewJsf/zoZQczk/vnWVFjERROieyO1Ckzpz/WkCvbjtniIE0lxzB7zorSV+kGI9VigGAaRlXJyU7mCFojeAFqD6cjS4= + skip_cleanup: true + distributions: pass + on: + tags: true + repo: fonttools/fonttools + all_branches: true + condition: "$BUILD_DIST == true" diff --git a/.travis/after_success.sh b/.travis/after_success.sh new file mode 100755 index 0000000..d113fe7 --- /dev/null +++ b/.travis/after_success.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e +set -x + +if [ "$TRAVIS_OS_NAME" == "osx" ]; then + source .venv/bin/activate +fi + +# upload coverage data to Codecov.io +[[ ${TOXENV} == *"-cov"* ]] && tox -e codecov diff --git a/.travis/before_deploy.sh b/.travis/before_deploy.sh new file mode 100755 index 0000000..1ded8f0 --- /dev/null +++ b/.travis/before_deploy.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -e +set -x + +# build sdist and wheel distribution packages in ./dist folder. +# Travis runs the `before_deploy` stage before each deployment, but +# we only want to build them once, as we want to use the same +# files for both Github and PyPI +if $(ls ./dist/fonttools*.zip > /dev/null 2>&1) && \ + $(ls ./dist/fonttools*.whl > /dev/null 2>&1); then + echo "Distribution packages already exists; skipping" +else + tox -e bdist +fi diff --git a/.travis/before_install.sh b/.travis/before_install.sh new file mode 100755 index 0000000..8cc4edb --- /dev/null +++ b/.travis/before_install.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +if [[ -n "$PYENV_VERSION" ]]; then + wget https://github.com/praekeltfoundation/travis-pyenv/releases/download/${TRAVIS_PYENV_VERSION}/setup-pyenv.sh + source setup-pyenv.sh +fi diff --git a/.travis/install.sh b/.travis/install.sh new file mode 100755 index 0000000..03cc0b3 --- /dev/null +++ b/.travis/install.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +set -e +set -x + +ci_requirements="pip setuptools tox" + +if [ "$TRAVIS_OS_NAME" == "osx" ]; then + if [[ ${TOXENV} == *"py27"* ]]; then + # install pip on the system python + curl -O https://bootstrap.pypa.io/get-pip.py + python get-pip.py --user + # install virtualenv and create virtual environment + python -m pip install --user virtualenv + python -m virtualenv .venv/ + elif [[ ${TOXENV} == *"py3"* ]]; then + # install/upgrade current python3 with homebrew + if brew list --versions python3 > /dev/null; then + brew upgrade python3 + else + brew install python3 + fi + # create virtual environment + python3 -m venv .venv/ + else + echo "unsupported $TOXENV: "${TOXENV} + exit 1 + fi + # activate virtual environment + source .venv/bin/activate +fi + +python -m pip install $ci_requirements diff --git a/.travis/run.sh b/.travis/run.sh new file mode 100755 index 0000000..6804f7d --- /dev/null +++ b/.travis/run.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e +set -x + +if [ "$TRAVIS_OS_NAME" == "osx" ]; then + source .venv/bin/activate +fi + +tox diff --git a/Doc/Makefile b/Doc/Makefile new file mode 100644 index 0000000..af18dd8 --- /dev/null +++ b/Doc/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = fontTools +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/Doc/changes.txt b/Doc/changes.txt deleted file mode 100644 index 22bcd5b..0000000 --- a/Doc/changes.txt +++ /dev/null @@ -1,161 +0,0 @@ -TTX/FontTools Version 2.4 -- Option to write to arbitrary files -- Better dump format for DSIG -- Better detection of OTF XML -- Fix issue with Apple's kern table format -- Fix mangling of TT glyph programs -- Fix issues related to mona.ttf -- Fix Windows Installer instructions -- Fix some modern MacOS issues -- Fix minor issues and typos - -TTX/FontTools Version 2.3 - -- TrueType Collection (TTC) support -- Python 2.6 support -- Update Unicode data to 5.2.0 -- Couple of bug fixes - -TTX/FontTools Version 2.2 - -- ClearType support -- cmap format 1 support -- PFA font support -- Switched from Numeric to numpy -- Update Unicode data to 5.1.0 -- Update AGLFN data to 1.6 -- Many bug fixes - -TTX/FontTools Version 2.1 - -- Many years worth of fixes and features - -TTX/FontTools Version 2.0 beta 2 (released ??? 2002) - -- Be "forgiving" when interpreting the maxp table version field: - interpret any value as 1.0 if it's not 0.5. Fixes dumping of these - GPL fonts: http://www.freebsd.org/cgi/pds.cgi?ports/chinese/wangttf -- Fixed ttx -l: it turned out this part of the code didn't work with - Python 2.2.1 and earlier. My bad to do most of my testing with a - different version than I shipped TTX with :-( -- Fixed bug in ClassDef format 1 subtable (Andreas Seidel bumped into - this one). - -TTX/FontTools Version 2.0 beta 1 (released September 10 2002) - -- Fixed embarrassing bug: the master checksum in the head table is now - calculated correctly even on little-endian platforms (such as Intel). -- Made the cmap format 4 compiler smarter: the binary data it creates is - now more or less as compact as possible. TTX now makes more compact - data than in any shipping font I've tested it with. -- Dump glyph names as a separate "GlyphOrder" pseudo table as opposed to - as part of the glyf table (obviously needed for CFF-OTF's). -- Added proper support for the CFF table. -- Don't barf on empty tables (questionable, but "there are font out there...") -- When writing TT glyf data, align glyphs on 4-byte boundaries. This seems - to be the current recommendation by MS. Also: don't barf on fonts which - are already 4-byte aligned. -- Windows installer contributed bu Adam Twardoch! Yay! -- Changed the command line interface again, now by creating one new tool - replacing the old ones: ttx - It dumps and compiles, depending on input file types. The options have - changed somewhat. - - The -d option is back (output dir) - - ttcompile's -i options is now called -m (as in "merge"), to avoid clash - with dump's -i. - - The -s option ("split tables") no longer creates a directory, - but instead outputs a small .ttx file containing references to the - individual table files. This is not a true link, it's a simple file - name, and the referenced file should be in the same directory so - ttcompile can find them. - - compile no longer accepts a directory as input argument. Instead it - can parse the new "mini-ttx" format as output by "ttx -s". - - all arguments are input files -- Renamed the command line programs and moved them to the Tools - subdirectory. They are now installed by the setup.py install script. -- Added OpenType support. BASE, GDEF, GPOS, GSUB and JSTF are (almost) - fully supported. The XML output is not yet final, as I'm still - considering to output certain subtables in a more human-friendly - manner. -- Fixed 'kern' table to correctly accept subtables it doesn't know about, - as well as interpreting Apple's definition of the 'kern' table headers - correctly. -- Fixed bug where glyphnames were not calculated from 'cmap' if it was - (one of the) first tables to be decompiled. More specifically: it cmap - was the first to ask for a glyphID -> glyphName mapping. -- Switched XML parsers: use expat instead of xmlproc. Should be faster. -- Removed my UnicodeString object: I now require Python 2.0 or up, which - has unicode support built in. -- Removed assert in glyf table: redundant data at the end of the table - is now ignored instead of raising an error. Should become a warning. -- Fixed bug in hmtx/vmtx code that only occured if all advances were equal. -- Fixed subtle bug in TT instruction disassembler. -- Couple of fixes to the 'post' table. -- Updated OS/2 table to latest spec. - -TTX/FontTools Version 1.0 beta 1 (released August 10 2001) - -- Reorganized the command line interface for ttDump.py and ttCompile.py, - they now behave more like "normal" command line tool, in that they accept - multiple input files for batch processing. -- ttDump.py and ttCompile.py don't silently override files anymore, but ask - before doing so. Can be overridden by -f. -- Added -d option to both ttDump.py and ttCompile.py. -- Installation is now done with distutils. (Needs work for environments without - compilers.) -- Updated installation instructions. -- Added some workarounds so as to handle certain buggy fonts more gracefully. -- Updated Unicode table to Unicode 3.0 (Thanks Antoine!) -- Included a Python script by Adam Twardoch that adds some useful stuff to the - Windows registry. -- Moved the project to SourceForge. - -TTX/FontTools Version 1.0 alpha 6 (released March 15 2000) - -- Big reorganization: made ttLib a subpackage of the new fontTools package, - changed several module names. Called the entire suite "FontTools" -- Added several submodules to fontTools, some new, some older. -- Added experimental CFF/GPOS/GSUB support to ttLib, read-only (but XML dumping - of GPOS/GSUB is for now disabled) -- Fixed hdmx endian bug -- Added -b option to ttCompile.py, it disables recalculation of bounding boxes, - as requested by Werner Lemberg. -- Renamed tt2xml.pt to ttDump.py and xml2tt.py to ttCompile.py -- Use ".ttx" as file extension instead of ".xml". -- TTX is now the name of the XML-based *format* for TT fonts, and not just - an application. - -Version 1.0 alpha 5 (never released) - -- More tables supported: hdmx, vhea, vmtx - -Version 1.0 alpha 3 & 4 (never released) - -- fixed most portability issues -- retracted the "Euro_or_currency" change from 1.0a2: it was nonsense! - -Version 1.0 alpha 2 (released as binary for MacOS, 2 May 1999) - -- genenates full FOND resources: including width table, PS - font name info and kern table if applicable. -- added cmap format 4 support. Extra: dumps Unicode char names as XML comments! -- added cmap format 6 support -- now accepts true type files starting with "true" - (instead of just 0x00010000 and "OTTO") -- 'glyf' table support is now complete: I added support for composite scale, - xy-scale and two-by-two for the 'glyf' table. For now, component offset scale - behaviour defaults to Apple-style. This only affects the (re)calculation of - the glyph bounding box. -- changed "Euro" to "Euro_or_currency" in the Standard Apple Glyph order list, - since we cannot tell from the 'post' table which is meant. I should probably - doublecheck with a Unicode encoding if available. (This does not affect the - output!) - -Fixed bugs: -- 'hhea' table is now recalculated correctly -- fixed wrong assumption about sfnt resource names - -Version 1.0 alpha 1 (27 Apr 1999) - -- initial binary release for MacOS - diff --git a/Doc/documentation.html b/Doc/documentation.html deleted file mode 100644 index e2821fe..0000000 --- a/Doc/documentation.html +++ /dev/null @@ -1,104 +0,0 @@ - - - -TTX Documentation - - - - - -

TTX -- From OpenType and TrueType to XML and Back

- -TTX is a tool for manipulating TrueType and OpenType fonts. It is written in Python and has a BSD-style, open-source licence -- see LICENSE.txt. Among other things this means you can use it free of charge. It's hosted at sourceforge.net. - -

-TTX can dump TrueType and OpenType fonts to an XML-based text format, which is also called TTX. TTX files have a .ttx file extension. - -

How to use TTX

- -The TTX application works can be used in two ways, depending on what platform you run it on: - - - -

-TTX detects what kind of files it is fed: it will output a .ttx file when it sees a .ttf or .otf, and it will compile a .ttf or .otf when the input file is a .ttx file. By default, the output file is created in the same folder as the input file, and will have the same name as the input file but with a different extension. TTX will never overwrite existing files, but if necessary will append a unique number to the output filename (before the extension), eg.: "Arial#1.ttf". - -

-When using TTX from the command line there are a bunch of extra options, these are explained in the help text, as displayed when typing "ttx -h" at the command prompt. These additional options include: -

- -

The TTX file format

- -The following tables are currently supported: -
- -BASE, CBDT, CBLC, CFF, COLR, CPAL, DSIG, EBDT, EBLC, FFTM, GDEF, GMAP, GPKG, GPOS, GSUB, JSTF, LTSH, META, OS/2, SING, SVG, TSI0, TSI1, TSI2, TSI3, TSI5, TSIB, TSID, TSIJ, TSIP, TSIS, TSIV, VORG, cmap, cvt, fpgm, gasp, glyf, hdmx, head, hhea, hmtx, kern, loca, maxp, name, post, prep, sbix, vhea and vmtx - -
-Other tables are dumped as hexadecimal data. - -

-TrueType fonts use glyph indices (GlyphID's) to refer to glyphs in most places. -While this is fine in binary form, it is really hard to work with for -humans. Therefore we use names instead. - -

The glyph names are either extracted from the 'CFF ' table or the 'post' table, -or are derived from a Unicode 'cmap' table. In the latter case the Adobe Glyph List -is used to calculate names based on Unicode values. If all of these mthods fail, -names are invented based on GlyphID (eg. "glyph00142"). - -

It is possible that different glyphs use the same name. If this happens, -we force the names to be unique by appending "#n" to the name (n being an -integer number). The original names are being kept, so this has no influence -on a "round tripped" font. - -

Because the order in which glyphs are stored inside the TT font is -important, we maintain an ordered list of glyph names in the font. - - -

Development and feedback

- -TTX/FontTools development is ongoing, but often goes in spurts. Feature requests and bug reports are always welcome. The best place for these is currently the fonttools-discussion mailing list at SourceForge. This list is both for discussion TTX from an end-user perspective as well as TTX/FontTools development. Subscription info can be found if you follow the "Mailing Lists" link at the SourceForge project page. You can also email me directly at just@letterror.com. - -

-Let me take this opportunity to mention that if you have special needs (eg. custom font monipulators, dufferent table formats, etc.): I am available for contracting. - -

Credits

- -Windows setup script: Adam Twardoch -
Icon: Hannes Famira - -

Acknowledgements

- -(in alphabetical order) -Erik van Blokland, Petr van Blokland, Jelle Bosma, Vincent Connare, -Simon Daniels, Hannes Famira, Yannis Haralambous, Greg Hitchcock, John Hudson, -Jack Jansen, Tom Kacvinsky, Antoine Leca, Werner Lemberg, Tal Leming, -Peter Lofting, Dave Opstad, Laurence Penney, Read Roberts, Guido van Rossum, Andreas Seidel, Adam Twardoch. - -

Copyrights

- -FontTools/TTX -
1999-2003 Just van Rossum; LettError (just@letterror.com). See LICENSE.txt for the full license. -

-Python -
Copyright (c) 2001-2003 Python Software Foundation. All Rights Reserved. -
Copyright (c) 2000 BeOpen.com. All Rights Reserved. -
Copyright (c) 1995-2001 Corporation for National Research Initiatives. All Rights Reserved. -
Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam. All Rights Reserved. -

-Numeric Python (NumPy) -
Copyright (c) 1996. The Regents of the University of California. All rights reserved. - - - diff --git a/Doc/install.txt b/Doc/install.txt deleted file mode 100644 index f2f3b0c..0000000 --- a/Doc/install.txt +++ /dev/null @@ -1,114 +0,0 @@ -TTX/FontTools - -TTX/FontTools is a suite of tools for manipulating fonts. It is written in -Python and has a BSD-style, open-source licence -- see LICENSE.txt. -It's hosted at http://sourceforge.net/. - -The flagship is TTX, a tool to convert OpenType and TrueType font files to -an XML-based format (also called TTX), and back. This lets you edit TTF or -OTF files with any text editor. - -The FontTools library currently reads and writes TrueType font files, reads -PostScript Type 1 fonts and more. - - -Scope - -TTX/FontTools' functionality is aimed towards font developers and font tool -developers. It can of course be used to just access fonts (outlines, -metrics, etc.) but it is not optimized for that. It will be further -developed so it can be the core of any font editor. And that's exactly -what it will be for our upcoming major rewrite of RoboFog, our (commercial) -PythonPowered font editor for MacOS. - - -Installation - -For Windows and MacOS there are easy-to-use TTX installers. The rest if this -document is meant for people who want to use TTX/FontTools from the source. - -You need the following software: - -Python - The fresh versions as well as older versions (You need 2.0 or higher) - can be downloaded from - http://www.python.org/download/ - or here - http://sourceforge.net/projects/python/ - - Windows: grab the Windows installer, run the full install. - Un*x: follow the build instructions. - MacOS: grab the installer, run "Easy Install" - -The numpy extension - See http://numpy.scipy.org/ - -Now run the "setup.py" script from the FontTools archive. This will install -all the modules in the right places, as well as tries to compile the one -(optional) C extension contained in FontTools. On Unix it also installs the -"ttx" command line tool. This tool can also be used on Windows, but might -need some fiddling. - -For instructions how to build a standalone Windows installer, see -Windows/README.TXT. Thanks a LOT to Adam Twardoch for this essential -contribution. - -For TTX usage instructions, see the file "documentation.html". - - -Feedback - -Please join the fonttools-discussion mailing list at SourceForge. Subscription -info can be found if you follow the "Mailing Lists" link at the SourceForge -project page: - http://sourceforge.net/projects/fonttools/ -You can also email me directly at just@letterror.com. - -If you want to follow the development of FontTools closely, or would like to -contribute, you can also subscribe to the fonttools-checkins mailing list. - - -Anonymous VCS access - -The FontTools sources are also accessible here: - http://sourceforge.net/projects/fonttools/ -Let me know if you'd like to become a co-developer. - - -Developer documentation - -Sorry, documentation beyond doc strings in the source code is still on my to-do list... -Below follows a brief overview of what's there. - - -The library - - Cross-platform - fontTools.t1Lib -- Provides a Type 1 font reader. Writing is a planned feature. - fontTools.ttLib -- Extensive TrueType tools. Reads and writes. This is the flagship - of FontTools, it's by far the most mature component. Contains a completely modular - TTF table converter architecture. See ttLib/tables/table_API_readme.txt. - fontTools.afmLib -- And AFM file reader/writer. - fontTools.cffLib -- Reads CFF fonts. Writing is a planned feature. - fontTools.unicode -- A simple (but large) module that translates - Unicode values to their descriptive names. Still Unicode 2.0. - fontTools.agl -- Interface to the Adobe Glyph List: maps unicode values - to glyph names and back. - - Mac-specific - fontTools.fondLib -- A reader/writer class for Mac FOND resources. - fontTools.nfntLib -- Reads Mac NFNT bitmap font resources. - - -Thank-you's - -(in alphabetical order) -Erik van Blokland, Petr van Blokland, Jelle Bosma, Vincent Connare, -Simon Daniels, Hannes Famira, Greg Hitchcock, John Hudson, Jack Jansen, -Antoine Leca, Werner Lemberg, Peter Lofting, Dave Opstad, Laurence Penney, -Guido van Rossum, Adam Twardoch. - -Copyrights - -FontTools/TTX -- 1999-2002 Just van Rossum; Letterror (just@letterror.com) -See LICENCE.txt for the full license. diff --git a/Doc/make.bat b/Doc/make.bat new file mode 100644 index 0000000..81a851a --- /dev/null +++ b/Doc/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build +set SPHINXPROJ=fontTools + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/Doc/ttx.1 b/Doc/man/man1/ttx.1 similarity index 98% rename from Doc/ttx.1 rename to Doc/man/man1/ttx.1 index 24ce9c1..bba23b5 100644 --- a/Doc/ttx.1 +++ b/Doc/man/man1/ttx.1 @@ -220,6 +220,6 @@ restrictions. .\" For Emacs: .\" Local Variables: .\" fill-column: 72 -.\" sentence-end: "[.?!][]\"')}]*\\($\\| $\\| \\| \\)[ \n]*" +.\" sentence-end: "[.?!][]\"')}]*\\($\\| $\\| \\| \\)[ \n]*" .\" sentence-end-double-space: t -.\" End: +.\" End: \ No newline at end of file diff --git a/Doc/source/afmLib.rst b/Doc/source/afmLib.rst new file mode 100644 index 0000000..f56d3c1 --- /dev/null +++ b/Doc/source/afmLib.rst @@ -0,0 +1,7 @@ +###### +afmLib +###### + +.. automodule:: fontTools.afmLib + :members: + :undoc-members: diff --git a/Doc/source/agl.rst b/Doc/source/agl.rst new file mode 100644 index 0000000..0ecf14d --- /dev/null +++ b/Doc/source/agl.rst @@ -0,0 +1,7 @@ +### +agl +### + +.. automodule:: fontTools.agl + :members: + :undoc-members: diff --git a/Doc/source/cffLib.rst b/Doc/source/cffLib.rst new file mode 100644 index 0000000..364824f --- /dev/null +++ b/Doc/source/cffLib.rst @@ -0,0 +1,7 @@ +###### +cffLib +###### + +.. automodule:: fontTools.cffLib + :members: + :undoc-members: diff --git a/Doc/source/conf.py b/Doc/source/conf.py new file mode 100644 index 0000000..a3b2be2 --- /dev/null +++ b/Doc/source/conf.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- +# +# fontTools documentation build configuration file, created by +# sphinx-quickstart on Thu Apr 20 11:07:39 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +needs_sphinx = '1.3' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] + +autodoc_mock_imports = ['gtk'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'fontTools' +copyright = u'2017, Just van Rossum, Behdad Esfahbod et al.' +author = u'Just van Rossum, Behdad Esfahbod et al.' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'3.10' +# The full version, including alpha/beta/rc tags. +release = u'3.10' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'classic' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'fontToolsDoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'fontTools.tex', u'fontTools Documentation', + u'Just van Rossum, Behdad Esfahbod et al.', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'fonttools', u'fontTools Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'fontTools', u'fontTools Documentation', + author, 'fontTools', 'A library for manipulating fonts, written in Python.', + 'Typography'), +] + diff --git a/Doc/source/designspaceLib/index.rst b/Doc/source/designspaceLib/index.rst new file mode 100644 index 0000000..2fb4e52 --- /dev/null +++ b/Doc/source/designspaceLib/index.rst @@ -0,0 +1,17 @@ +############## +designspaceLib +############## + +MutatorMath started out with its own reader and writer for designspaces. +Since then the use of designspace has broadened and it would be useful +to have a reader and writer that are independent of a specific system. + +.. toctree:: + :maxdepth: 1 + + readme + scripting + +.. automodule:: fontTools.designspaceLib + :members: + :undoc-members: diff --git a/Doc/source/designspaceLib/readme.rst b/Doc/source/designspaceLib/readme.rst new file mode 100644 index 0000000..5af5d63 --- /dev/null +++ b/Doc/source/designspaceLib/readme.rst @@ -0,0 +1,1052 @@ +################################# +DesignSpaceDocument Specification +################################# + +An object to read, write and edit interpolation systems for typefaces. + +- the format was originally written for MutatorMath. +- the format is now also used in fontTools.varlib. +- Define sources, axes and instances. +- Not all values might be required by all applications. + +A couple of differences between things that use designspaces: + +- Varlib does not support anisotropic interpolations. +- MutatorMath and Superpolator will extrapolate over the boundaries of + the axes. Varlib can not (at the moment). +- Varlib requires much less data to define an instance than + MutatorMath. +- The goals of Varlib and MutatorMath are different, so not all + attributes are always needed. +- Need to expand the description of FDK use of designspace files. + +The DesignSpaceDocument object can read and write ``.designspace`` data. +It imports the axes, sources and instances to very basic **descriptor** +objects that store the data in attributes. Data is added to the document +by creating such descriptor objects, filling them with data and then +adding them to the document. This makes it easy to integrate this object +in different contexts. + +The **DesignSpaceDocument** object can be subclassed to work with +different objects, as long as they have the same attributes. + +.. code:: python + + from designSpaceDocument import DesignSpaceDocument + doc = DesignSpaceDocument() + doc.read("some/path/to/my.designspace") + doc.axes + doc.sources + doc.instances + +********** +Validation +********** + +Some validation is done when reading. + +Axes +==== + +- If the ``axes`` element is available in the document then all + locations will check their dimensions against the defined axes. If a + location uses an axis that is not defined it will be ignored. +- If there are no ``axes`` in the document, locations will accept all + axis names, so that we can.. +- Use ``doc.checkAxes()`` to reconstruct axes definitions based on the + ``source.location`` values. If you save the document the axes will be + there. + +Default font +============ + +- The source with the ``copyInfo`` flag indicates this is the default + font. +- In mutatorMath the default font is selected automatically. A warning + is printed if the mutatorMath default selection differs from the one + set by ``copyInfo``. But the ``copyInfo`` source will be used. +- If no source has a ``copyInfo`` flag, mutatorMath will be used to + select one. This source gets its ``copyInfo`` flag set. If you save + the document this flag will be set. +- Use ``doc.checkDefault()`` to set the default font. + +************ +Localisation +************ + +Some of the descriptors support localised names. The names are stored in +dictionaries using the language code as key. That means that there are +now two places to store names: the old attribute and the new localised +dictionary, ``obj.stylename`` and ``obj.localisedStyleName['en']``. + +***** +Rules +***** + +Rules describe designspace areas in which one glyph should be replaced by another. +A rule has a name and a number of conditionsets. The rule also contains a list of +glyphname pairs: the glyphs that need to be substituted. For a rule to be triggered +**only one** of the conditionsets needs to be true, ``OR``. Within a conditionset +**all** conditions need to be true, ``AND``. + +The ``sub`` element contains a pair of glyphnames. The ``name`` attribute is the glyph that should be visible when the rule evaluates to **False**. The ``with`` attribute is the glyph that should be visible when the rule evaluates to **True**. + +UFO instances +============= + +- When making instances as UFOs however, we need to swap the glyphs so + that the original shape is still available. For instance, if a rule + swaps ``a`` for ``a.alt``, but a glyph that references ``a`` in a + component would then show the new ``a.alt``. +- But that can lead to unexpected results. So, if there are no rules + for ``adieresis`` (assuming it references ``a``) then that glyph + **should not change appearance**. That means that when the rule swaps + ``a`` and ``a.alt`` it also swaps all components that reference these + glyphs so they keep their appearance. +- The swap function also needs to take care of swapping the names in + kerning data. + +********** +Python API +********** + +SourceDescriptor object +======================= + +Attributes +---------- + +- ``filename``: string. A relative path to the source file, **as it is + in the document**. MutatorMath + Varlib. +- ``path``: string. Absolute path to the source file, calculated from + the document path and the string in the filename attr. MutatorMath + + Varlib. +- ``layerName``: string. The name of the layer in the source to look for + outline data. Default ``None`` which means ``foreground``. +- ``font``: Any Python object. Optional. Points to a representation of + this source font that is loaded in memory, as a Python object + (e.g. a ``defcon.Font`` or a ``fontTools.ttFont.TTFont``). The default + document reader will not fill-in this attribute, and the default + writer will not use this attribute. It is up to the user of + ``designspaceLib`` to either load the resource identified by ``filename`` + and store it in this field, or write the contents of this field to the + disk and make ``filename`` point to that. +- ``name``: string. Optional. Unique identifier name for this source, + if there is one or more ``instance.glyph`` elements in the document. + MutatorMath. +- ``location``: dict. Axis values for this source. MutatorMath + Varlib +- ``copyLib``: bool. Indicates if the contents of the font.lib need to + be copied to the instances. MutatorMath. +- ``copyInfo`` bool. Indicates if the non-interpolating font.info needs + to be copied to the instances. Also indicates this source is expected + to be the default font. MutatorMath + Varlib +- ``copyGroups`` bool. Indicates if the groups need to be copied to the + instances. MutatorMath. +- ``copyFeatures`` bool. Indicates if the feature text needs to be + copied to the instances. MutatorMath. +- ``muteKerning``: bool. Indicates if the kerning data from this source + needs to be muted (i.e. not be part of the calculations). + MutatorMath. +- ``muteInfo``: bool. Indicated if the interpolating font.info data for + this source needs to be muted. MutatorMath. +- ``mutedGlyphNames``: list. Glyphnames that need to be muted in the + instances. MutatorMath. +- ``familyName``: string. Family name of this source. Though this data + can be extracted from the font, it can be efficient to have it right + here. Varlib. +- ``styleName``: string. Style name of this source. Though this data + can be extracted from the font, it can be efficient to have it right + here. Varlib. + +.. code:: python + + doc = DesignSpaceDocument() + s1 = SourceDescriptor() + s1.path = masterPath1 + s1.name = "master.ufo1" + s1.font = defcon.Font("master.ufo1") + s1.copyLib = True + s1.copyInfo = True + s1.copyFeatures = True + s1.location = dict(weight=0) + s1.familyName = "MasterFamilyName" + s1.styleName = "MasterStyleNameOne" + s1.mutedGlyphNames.append("A") + s1.mutedGlyphNames.append("Z") + doc.addSource(s1) + +.. _instance-descriptor-object: + +InstanceDescriptor object +========================= + +.. attributes-1: + +Attributes +---------- + +- ``filename``: string. Relative path to the instance file, **as it is + in the document**. The file may or may not exist. MutatorMath. +- ``path``: string. Absolute path to the source file, calculated from + the document path and the string in the filename attr. The file may + or may not exist. MutatorMath. +- ``name``: string. Unique identifier name of the instance, used to + identify it if it needs to be referenced from elsewhere in the + document. +- ``location``: dict. Axis values for this source. MutatorMath + + Varlib. +- ``familyName``: string. Family name of this instance. MutatorMath + + Varlib. +- ``localisedFamilyName``: dict. A dictionary of localised family name + strings, keyed by language code. +- ``styleName``: string. Style name of this source. MutatorMath + + Varlib. +- ``localisedStyleName``: dict. A dictionary of localised stylename + strings, keyed by language code. +- ``postScriptFontName``: string. Postscript fontname for this + instance. MutatorMath. +- ``styleMapFamilyName``: string. StyleMap familyname for this + instance. MutatorMath. +- ``localisedStyleMapFamilyName``: A dictionary of localised style map + familyname strings, keyed by language code. +- ``localisedStyleMapStyleName``: A dictionary of localised style map + stylename strings, keyed by language code. +- ``styleMapStyleName``: string. StyleMap stylename for this instance. + MutatorMath. +- ``glyphs``: dict for special master definitions for glyphs. If glyphs + need special masters (to record the results of executed rules for + example). MutatorMath. +- ``mutedGlyphNames``: list of glyphnames that should be suppressed in + the generation of this instance. +- ``kerning``: bool. Indicates if this instance needs its kerning + calculated. MutatorMath. +- ``info``: bool. Indicated if this instance needs the interpolating + font.info calculated. +- ``lib``: dict. Custom data associated with this instance. + +Methods +------- + +These methods give easier access to the localised names. + +- ``setStyleName(styleName, languageCode="en")`` +- ``getStyleName(languageCode="en")`` +- ``setFamilyName(familyName, languageCode="en")`` +- ``getFamilyName(self, languageCode="en")`` +- ``setStyleMapStyleName(styleMapStyleName, languageCode="en")`` +- ``getStyleMapStyleName(languageCode="en")`` +- ``setStyleMapFamilyName(styleMapFamilyName, languageCode="en")`` +- ``getStyleMapFamilyName(languageCode="en")`` + +Example +------- + +.. code:: python + + i2 = InstanceDescriptor() + i2.path = instancePath2 + i2.familyName = "InstanceFamilyName" + i2.styleName = "InstanceStyleName" + i2.name = "instance.ufo2" + # anisotropic location + i2.location = dict(weight=500, width=(400,300)) + i2.postScriptFontName = "InstancePostscriptName" + i2.styleMapFamilyName = "InstanceStyleMapFamilyName" + i2.styleMapStyleName = "InstanceStyleMapStyleName" + glyphMasters = [dict(font="master.ufo1", glyphName="BB", location=dict(width=20,weight=20)), dict(font="master.ufo2", glyphName="CC", location=dict(width=900,weight=900))] + glyphData = dict(name="arrow", unicodeValue=1234) + glyphData['masters'] = glyphMasters + glyphData['note'] = "A note about this glyph" + glyphData['instanceLocation'] = dict(width=100, weight=120) + i2.glyphs['arrow'] = glyphData + i2.glyphs['arrow2'] = dict(mute=False) + i2.lib['com.coolDesignspaceApp.specimenText'] = 'Hamburgerwhatever' + doc.addInstance(i2) + +.. _axis-descriptor-object: + +AxisDescriptor object +===================== + +- ``tag``: string. Four letter tag for this axis. Some might be + registered at the `OpenType + specification `__. + Privately-defined axis tags must begin with an uppercase letter and + use only uppercase letters or digits. +- ``name``: string. Name of the axis as it is used in the location + dicts. MutatorMath + Varlib. +- ``labelNames``: dict. When defining a non-registered axis, it will be + necessary to define user-facing readable names for the axis. Keyed by + xml:lang code. Varlib. +- ``minimum``: number. The minimum value for this axis. MutatorMath + + Varlib. +- ``maximum``: number. The maximum value for this axis. MutatorMath + + Varlib. +- ``default``: number. The default value for this axis, i.e. when a new + location is created, this is the value this axis will get. + MutatorMath + Varlib. +- ``map``: list of input / output values that can describe a warp of + user space to designspace coordinates. If no map values are present, + it is assumed it is [(minimum, minimum), (maximum, maximum)]. Varlib. + +.. code:: python + + a1 = AxisDescriptor() + a1.minimum = 1 + a1.maximum = 1000 + a1.default = 400 + a1.name = "weight" + a1.tag = "wght" + a1.labelNames[u'fa-IR'] = u"قطر" + a1.labelNames[u'en'] = u"Wéíght" + a1.map = [(1.0, 10.0), (400.0, 66.0), (1000.0, 990.0)] + +RuleDescriptor object +===================== + +- ``name``: string. Unique name for this rule. Can be used to + reference this rule data. +- ``conditionSets``: a list of conditionsets +- Each conditionset is a list of conditions. +- Each condition is a dict with ``name``, ``minimum`` and ``maximum`` keys. +- ``subs``: list of substitutions +- Each substitution is stored as tuples of glyphnames, e.g. ("a", "a.alt"). + +.. code:: python + + r1 = RuleDescriptor() + r1.name = "unique.rule.name" + r1.conditionsSets.append([dict(name="weight", minimum=-10, maximum=10), dict(...)]) + r1.conditionsSets.append([dict(...), dict(...)]) + r1.subs.append(("a", "a.alt")) + +.. _subclassing-descriptors: + +Subclassing descriptors +======================= + +The DesignSpaceDocument can take subclassed Reader and Writer objects. +This allows you to work with your own descriptors. You could subclass +the descriptors. But as long as they have the basic attributes the +descriptor does not need to be a subclass. + +.. code:: python + + class MyDocReader(BaseDocReader): + ruleDescriptorClass = MyRuleDescriptor + axisDescriptorClass = MyAxisDescriptor + sourceDescriptorClass = MySourceDescriptor + instanceDescriptorClass = MyInstanceDescriptor + + class MyDocWriter(BaseDocWriter): + ruleDescriptorClass = MyRuleDescriptor + axisDescriptorClass = MyAxisDescriptor + sourceDescriptorClass = MySourceDescriptor + instanceDescriptorClass = MyInstanceDescriptor + + myDoc = DesignSpaceDocument(KeyedDocReader, KeyedDocWriter) + +********************** +Document xml structure +********************** + +- The ``axes`` element contains one or more ``axis`` elements. +- The ``sources`` element contains one or more ``source`` elements. +- The ``instances`` element contains one or more ``instance`` elements. +- The ``rules`` element contains one or more ``rule`` elements. +- The ``lib`` element contains arbitrary data. + +.. code:: xml + + + + + + + + + + + + + + + + + + + + + + + + + + +.. 1-axis-element: + +1. axis element +=============== + +- Define a single axis +- Child element of ``axes`` + +.. attributes-2: + +Attributes +---------- + +- ``name``: required, string. Name of the axis that is used in the + location elements. +- ``tag``: required, string, 4 letters. Some axis tags are registered + in the OpenType Specification. +- ``minimum``: required, number. The minimum value for this axis. +- ``maximum``: required, number. The maximum value for this axis. +- ``default``: required, number. The default value for this axis. +- ``hidden``: optional, 0 or 1. Records whether this axis needs to be + hidden in interfaces. + +.. code:: xml + + + +.. 11-labelname-element: + +1.1 labelname element +===================== + +- Defines a human readable name for UI use. +- Optional for non-registered axis names. +- Can be localised with ``xml:lang`` +- Child element of ``axis`` + +.. attributes-3: + +Attributes +---------- + +- ``xml:lang``: required, string. `XML language + definition `__ + +Value +----- + +- The natural language name of this axis. + +.. example-1: + +Example +------- + +.. code:: xml + + قطر + Wéíght + +.. 12-map-element: + +1.2 map element +=============== + +- Defines a single node in a series of input value / output value + pairs. +- Together these values transform the designspace. +- Child of ``axis`` element. + +.. example-2: + +Example +------- + +.. code:: xml + + + + + +Example of all axis elements together: +-------------------------------------- + +.. code:: xml + + + + قطر + Wéíght + + + + + + + + +.. 2-location-element: + +2. location element +=================== + +- Defines a coordinate in the design space. +- Dictionary of axisname: axisvalue +- Used in ``source``, ``instance`` and ``glyph`` elements. + +.. 21-dimension-element: + +2.1 dimension element +===================== + +- Child element of ``location`` + +.. attributes-4: + +Attributes +---------- + +- ``name``: required, string. Name of the axis. +- ``xvalue``: required, number. The value on this axis. +- ``yvalue``: optional, number. Separate value for anisotropic + interpolations. + +.. example-3: + +Example +------- + +.. code:: xml + + + + + + +.. 3-source-element: + +3. source element +================= + +- Defines a single font that contributes to the designspace. +- Child element of ``sources`` + +.. attributes-5: + +Attributes +---------- + +- ``familyname``: optional, string. The family name of the source font. + While this could be extracted from the font data itself, it can be + more efficient to add it here. +- ``stylename``: optional, string. The style name of the source font. +- ``name``: required, string. A unique name that can be used to + identify this font if it needs to be referenced elsewhere. +- ``filename``: required, string. A path to the source file, relative + to the root path of this document. The path can be at the same level + as the document or lower. +- ``layer``: optional, string. The name of the layer in the source file. + If no layer attribute is given assume the foreground layer should be used. + +.. 31-lib-element: + +3.1 lib element +=============== + +There are two meanings for the ``lib`` element: + +1. Source lib + - Example: ```` + - Child element of ``source`` + - Defines if the instances can inherit the data in the lib of this + source. + - MutatorMath only + +2. Document and instance lib + - Example: + + .. code:: xml + + + + ... + The contents use the PLIST format. + + + + - Child element of ``designspace`` and ``instance`` + - Contains arbitrary data about the whole document or about a specific + instance. + - Items in the dict need to use **reverse domain name notation** __ + +.. 32-info-element: + +3.2 info element +================ + +- ```` +- Child element of ``source`` +- Defines if the instances can inherit the non-interpolating font info + from this source. +- MutatorMath + Varlib +- NOTE: **This presence of this element indicates this source is to be + the default font.** + +.. 33-features-element: + +3.3 features element +==================== + +- ```` +- Defines if the instances can inherit opentype feature text from this + source. +- Child element of ``source`` +- MutatorMath only + +.. 34-glyph-element: + +3.4 glyph element +================= + +- Can appear in ``source`` as well as in ``instance`` elements. +- In a ``source`` element this states if a glyph is to be excluded from + the calculation. +- MutatorMath only + +.. attributes-6: + +Attributes +---------- + +- ``mute``: optional attribute, number 1 or 0. Indicate if this glyph + should be ignored as a master. +- ```` +- MutatorMath only + +.. 35-kerning-element: + +3.5 kerning element +=================== + +- ```` +- Can appear in ``source`` as well as in ``instance`` elements. + +.. attributes-7: + +Attributes +---------- + +- ``mute``: required attribute, number 1 or 0. Indicate if the kerning + data from this source is to be excluded from the calculation. +- If the kerning element is not present, assume ``mute=0``, yes, + include the kerning of this source in the calculation. +- MutatorMath only + +.. example-4: + +Example +------- + +.. code:: xml + + + + + + + + + + + + + +.. 4-instance-element: + +4. instance element +=================== + +- Defines a single font that can be calculated with the designspace. +- Child element of ``instances`` +- For use in Varlib the instance element really only needs the names + and the location. The ``glyphs`` element is not required. +- MutatorMath uses the ``glyphs`` element to describe how certain + glyphs need different masters, mainly to describe the effects of + conditional rules in Superpolator. + +.. attributes-8: + +Attributes +---------- + +- ``familyname``: required, string. The family name of the instance + font. Corresponds with ``font.info.familyName`` +- ``stylename``: required, string. The style name of the instance font. + Corresponds with ``font.info.styleName`` +- ``name``: required, string. A unique name that can be used to + identify this font if it needs to be referenced elsewhere. +- ``filename``: string. Required for MutatorMath. A path to the + instance file, relative to the root path of this document. The path + can be at the same level as the document or lower. +- ``postscriptfontname``: string. Optional for MutatorMath. Corresponds + with ``font.info.postscriptFontName`` +- ``stylemapfamilyname``: string. Optional for MutatorMath. Corresponds + with ``styleMapFamilyName`` +- ``stylemapstylename``: string. Optional for MutatorMath. Corresponds + with ``styleMapStyleName`` + +Example for varlib +------------------ + +.. code:: xml + + + + + + + + + + + com.coolDesignspaceApp.specimenText + Hamburgerwhatever + + + + +.. 41-glyphs-element: + +4.1 glyphs element +================== + +- Container for ``glyph`` elements. +- Optional +- MutatorMath only. + +.. 42-glyph-element: + +4.2 glyph element +================= + +- Child element of ``glyphs`` +- May contain a ``location`` element. + +.. attributes-9: + +Attributes +---------- + +- ``name``: string. The name of the glyph. +- ``unicode``: string. Unicode values for this glyph, in hexadecimal. + Multiple values should be separated with a space. +- ``mute``: optional attribute, number 1 or 0. Indicate if this glyph + should be supressed in the output. + +.. 421-note-element: + +4.2.1 note element +================== + +- String. The value corresponds to glyph.note in UFO. + +.. 422-masters-element: + +4.2.2 masters element +===================== + +- Container for ``master`` elements +- These ``master`` elements define an alternative set of glyph masters + for this glyph. + +.. 4221-master-element: + +4.2.2.1 master element +====================== + +- Defines a single alternative master for this glyph. + +4.3 Localised names for instances +================================= + +Localised names for instances can be included with these simple elements +with an ``xml:lang`` attribute: +`XML language definition `__ + +- stylename +- familyname +- stylemapstylename +- stylemapfamilyname + +.. example-5: + +Example +------- + +.. code:: xml + + Demigras + 半ば + Montserrat + モンセラート + Standard + Montserrat Halbfett + モンセラート SemiBold + +.. attributes-10: + +Attributes +---------- + +- ``glyphname``: the name of the alternate master glyph. +- ``source``: the identifier name of the source this master glyph needs + to be loaded from + +.. example-6: + +Example +------- + +.. code:: xml + + + + + + + + + + + + + + A note about this glyph + + + + + + + + + + + + + + + com.coolDesignspaceApp.specimenText + Hamburgerwhatever + + + + +.. 50-rules-element: + +5.0 rules element +================= + +- Container for ``rule`` elements +- The rules are evaluated in this order. + +.. 51-rule-element: + +5.1 rule element +================ + +- Defines a named rule. +- Each ``rule`` element contains one or more ``conditionset`` elements. +- Only one ``conditionset`` needs to be true to trigger the rule. +- All conditions in a ``conditionset`` must be true to make the ``conditionset`` true. +- For backwards compatibility a ``rule`` can contain ``condition`` elements outside of a conditionset. These are then understood to be part of a single, implied, ``conditionset``. Note: these conditions should be written wrapped in a conditionset. +- A rule element needs to contain one or more ``sub`` elements in order to be compiled to a variable font. +- Rules without sub elements should be ignored when compiling a font. +- For authoring tools it might be necessary to save designspace files without ``sub`` elements just because the work is incomplete. + +.. attributes-11: + +Attributes +---------- + +- ``name``: optional, string. A unique name that can be used to + identify this rule if it needs to be referenced elsewhere. The name + is not important for compiling variable fonts. + +5.1.1 conditionset element +======================= + +- Child element of ``rule`` +- Contains one or more ``condition`` elements. + +.. 512-condition-element: + +5.1.2 condition element +======================= + +- Child element of ``conditionset`` +- Between the ``minimum`` and ``maximum`` this rule is ``True``. +- If ``minimum`` is not available, assume it is ``axis.minimum``. +- If ``maximum`` is not available, assume it is ``axis.maximum``. +- The condition must contain at least a minimum or maximum or both. + +.. attributes-12: + +Attributes +---------- + +- ``name``: string, required. Must match one of the defined ``axis`` + name attributes. +- ``minimum``: number, required*. The low value. +- ``maximum``: number, required*. The high value. + +.. 513-sub-element: + +5.1.3 sub element +================= + +- Child element of ``rule``. +- Defines which glyph to replace when the rule evaluates to **True**. + +.. attributes-13: + +Attributes +---------- + +- ``name``: string, required. The name of the glyph this rule looks + for. +- ``with``: string, required. The name of the glyph it is replaced + with. + +.. example-7: + +Example +------- + +Example with an implied ``conditionset``. Here the conditions are not +contained in a conditionset. + +.. code:: xml + + + + + + + + + +Example with ``conditionsets``. All conditions in a conditionset must be true. + +.. code:: xml + + + + + + + + + + + + + + + +.. 6-notes: + +6 Notes +======= + +Paths and filenames +------------------- + +A designspace file needs to store many references to UFO files. + +- designspace files can be part of versioning systems and appear on + different computers. This means it is not possible to store absolute + paths. +- So, all paths are relative to the designspace document path. +- Using relative paths allows designspace files and UFO files to be + **near** each other, and that they can be **found** without enforcing + one particular structure. +- The **filename** attribute in the ``SourceDescriptor`` and + ``InstanceDescriptor`` classes stores the preferred relative path. +- The **path** attribute in these objects stores the absolute path. It + is calculated from the document path and the relative path in the + filename attribute when the object is created. +- Only the **filename** attribute is written to file. +- Both **filename** and **path** must use forward slashes (``/``) as + path separators, even on Windows. + +Right before we save we need to identify and respond to the following +situations: + +In each descriptor, we have to do the right thing for the filename +attribute. Before writing to file, the ``documentObject.updatePaths()`` +method prepares the paths as follows: + +**Case 1** + +:: + + descriptor.filename == None + descriptor.path == None + +**Action** + +- write as is, descriptors will not have a filename attr. Useless, but + no reason to interfere. + +**Case 2** + +:: + + descriptor.filename == "../something" + descriptor.path == None + +**Action** + +- write as is. The filename attr should not be touched. + +**Case 3** + +:: + + descriptor.filename == None + descriptor.path == "~/absolute/path/there" + +**Action** + +- calculate the relative path for filename. We're not overwriting some + other value for filename, it should be fine. + +**Case 4** + +:: + + descriptor.filename == '../somewhere' + descriptor.path == "~/absolute/path/there" + +**Action** + +- There is a conflict between the given filename, and the path. The + difference could have happened for any number of reasons. Assuming + the values were not in conflict when the object was created, either + could have changed. We can't guess. +- Assume the path attribute is more up to date. Calculate a new value + for filename based on the path and the document path. + +Recommendation for editors +-------------------------- + +- If you want to explicitly set the **filename** attribute, leave the + path attribute empty. +- If you want to explicitly set the **path** attribute, leave the + filename attribute empty. It will be recalculated. +- Use ``documentObject.updateFilenameFromPath()`` to explicitly set the + **filename** attributes for all instance and source descriptors. + +.. 7-this-document: + +7 This document +=============== + +- The package is rather new and changes are to be expected. diff --git a/Doc/source/designspaceLib/scripting.rst b/Doc/source/designspaceLib/scripting.rst new file mode 100644 index 0000000..2bd4a0a --- /dev/null +++ b/Doc/source/designspaceLib/scripting.rst @@ -0,0 +1,253 @@ +####################### +Scripting a designspace +####################### + +It can be useful to build a designspace with a script rather than +construct one with an interface like +`Superpolator `__ or +`DesignSpaceEditor `__. + +`fontTools.designspaceLib` offers a some tools for building designspaces in +Python. This document shows an example. + +******************************** +Filling-in a DesignSpaceDocument +******************************** + +So, suppose you installed the `fontTools` package through your favorite +``git`` client. + +The ``DesignSpaceDocument`` object represents the document, whether it +already exists or not. Make a new one: + +.. code:: python + + from fontTools.designspaceLib import (DesignSpaceDocument, AxisDescriptor, + SourceDescriptor, InstanceDescriptor) + doc = DesignSpaceDocument() + +We want to create definitions for axes, sources and instances. That +means there are a lot of attributes to set. The **DesignSpaceDocument +object** uses objects to describe the axes, sources and instances. These +are relatively simple objects, think of these as collections of +attributes. + +- Attributes of the :ref:`source-descriptor-object` +- Attributes of the :ref:`instance-descriptor-object` +- Attributes of the :ref:`axis-descriptor-object` +- Read about :ref:`subclassing-descriptors` + +Make an axis object +=================== + +Make a descriptor object and add it to the document. + +.. code:: python + + a1 = AxisDescriptor() + a1.maximum = 1000 + a1.minimum = 0 + a1.default = 0 + a1.name = "weight" + a1.tag = "wght" + doc.addAxis(a1) + +- You can add as many axes as you need. OpenType has a maximum of + around 64K. DesignSpaceEditor has a maximum of 5. +- The ``name`` attribute is the name you'll be using as the axis name + in the locations. +- The ``tag`` attribute is the one of the registered `OpenType + Variation Axis + Tags `__ +- The default master is expected at the intersection of all + default values of all axes. + +Option: add label names +----------------------- + +The **labelnames** attribute is intended to store localisable, human +readable names for this axis if this is not an axis that is registered +by OpenType. Think "The label next to the slider". The attribute is a +dictionary. The key is the `xml language +tag `__, the +value is a utf-8 string with the name. Whether or not this attribute is +used depends on the font building tool, the operating system and the +authoring software. This, at least, is the place to record it. + +.. code:: python + + a1.labelNames['fa-IR'] = u"قطر" + a1.labelNames['en'] = u"Wéíght" + +Option: add a map +----------------- + +The **map** attribute is a list of (input, output) mapping values +intended for `axis variations table of +OpenType `__. + +.. code:: python + + a1.map = [(0.0, 10.0), (401.0, 66.0), (1000.0, 990.0)] + +Make a source object +==================== + +A **source** is an object that points to a UFO file. It provides the +outline geometry, kerning and font.info that we want to work with. + +.. code:: python + + s0 = SourceDescriptor() + s0.path = "my/path/to/thin.ufo" + s0.name = "master.thin" + s0.location = dict(weight=0) + doc.addSource(s0) + +- You'll need to have at least 2 sources in your document, so go ahead + and add another one. +- The **location** attribute is a dictionary with the designspace + location for this master. +- The axis names in the location have to match one of the ``axis.name`` + values you defined before. +- The **path** attribute is the absolute path to an existing UFO. +- The **name** attribute is a unique name for this source used to keep + track it. +- The **layerName** attribute is the name of the UFO3 layer. Default None for ``foreground``. + +So go ahead and add another master: + +.. code:: python + + s1 = SourceDescriptor() + s1.path = "my/path/to/bold.ufo" + s1.name = "master.bold" + s1.location = dict(weight=1000) + doc.addSource(s1) + + +Option: exclude glyphs +---------------------- + +By default all glyphs in a source will be processed. If you want to +exclude certain glyphs, add their names to the ``mutedGlyphNames`` list. + +.. code:: python + + s1.mutedGlyphNames = ["A.test", "A.old"] + +Make an instance object +======================= + +An **instance** is description of a UFO that you want to generate with +the designspace. For an instance you can define more things. If you want +to generate UFO instances with MutatorMath then you can define different +names and set flags for if you want to generate kerning and font info +and so on. You can also set a path where to generate the instance. + +.. code:: python + + i0 = InstanceDescriptor() + i0.familyName = "MyVariableFontPrototype" + i0.styleName = "Medium" + i0.path = os.path.join(root, "instances","MyVariableFontPrototype-Medium.ufo") + i0.location = dict(weight=500) + i0.kerning = True + i0.info = True + doc.addInstance(i0) + +- The ``path`` attribute needs to be the absolute (real or intended) + path for the instance. When the document is saved this path will + written as relative to the path of the document. +- instance paths should be on the same level as the document, or in a + level below. +- Instances for MutatorMath will generate to UFO. +- Instances for variable fonts become **named instances**. + +Option: add more names +---------------------- + +If you want you can add a PostScript font name, a stylemap familyName +and a stylemap styleName. + +.. code:: python + + i0.postScriptFontName = "MyVariableFontPrototype-Medium" + i0.styleMapFamilyName = "MyVarProtoMedium" + i0.styleMapStyleName = "regular" + +Option: add glyph specific masters +---------------------------------- + +This bit is not supported by OpenType variable fonts, but it is needed +for some designspaces intended for generating instances with +MutatorMath. The code becomes a bit verbose, so you're invited to wrap +this into something clever. + +.. code:: python + + # we're making a dict with all sorts of + #(optional) settings for a glyph. + #In this example: the dollar. + glyphData = dict(name="dollar", unicodeValue=0x24) + + # you can specify a different location for a glyph + glyphData['instanceLocation'] = dict(weight=500) + + # You can specify different masters + # for this specific glyph. + # You can also give those masters new + # locations. It's a miniature designspace. + # Remember the "name" attribute we assigned to the sources? + glyphData['masters'] = [ + dict(font="master.thin", + glyphName="dollar.nostroke", + location=dict(weight=0)), + dict(font="master.bold", + glyphName="dollar.nostroke", + location=dict(weight=1000)), + ] + + # With all of that set up, store it in the instance. + i4.glyphs['dollar'] = glyphData + +****** +Saving +****** + +.. code:: python + + path = "myprototype.designspace" + doc.write(path) + +************************ +Reading old designspaces +************************ + +Old designspace files might not contain ``axes`` definitions. This is +how you reconstruct the axes from the extremes of the source locations + +.. code:: python + + doc.checkAxes() + +This is how you check the default font. + +.. code:: python + + doc.checkDefault() + +*********** +Generating? +*********** + +You can generate the UFO's with MutatorMath: + +.. code:: python + + from mutatorMath.ufo import build + build("whatevs/myprototype.designspace") + +- Assuming the outline data in the masters is compatible. + +Or you can use the file in making a **variable font** with varlib. diff --git a/Doc/source/encodings.rst b/Doc/source/encodings.rst new file mode 100644 index 0000000..8bcd38a --- /dev/null +++ b/Doc/source/encodings.rst @@ -0,0 +1,14 @@ +######### +encodings +######### + +.. automodule:: fontTools.encodings + :members: + :undoc-members: + +codecs +------ + +.. automodule:: fontTools.encodings.codecs + :members: + :undoc-members: diff --git a/Doc/source/feaLib.rst b/Doc/source/feaLib.rst new file mode 100644 index 0000000..ad69217 --- /dev/null +++ b/Doc/source/feaLib.rst @@ -0,0 +1,43 @@ +###### +feaLib +###### + +.. automodule:: fontTools.feaLib + :members: + :undoc-members: + +ast +--- + +.. automodule:: fontTools.feaLib.ast + :members: + :undoc-members: + +builder +------- + +.. automodule:: fontTools.feaLib.builder + :members: + :undoc-members: + +error +----- + +.. automodule:: fontTools.feaLib.parser + :members: + :undoc-members: + +lexer +----- + +.. automodule:: fontTools.feaLib.lexer + :members: + :undoc-members: + +parser +------ + +.. automodule:: fontTools.feaLib.parser + :members: + :undoc-members: + diff --git a/Doc/source/index.rst b/Doc/source/index.rst new file mode 100644 index 0000000..67ec88e --- /dev/null +++ b/Doc/source/index.rst @@ -0,0 +1,29 @@ +fontTools Docs +============== + +.. toctree:: + :maxdepth: 1 + + afmLib + agl + cffLib + designspaceLib/index + inspect + encodings + feaLib + merge + misc/index + pens/index + subset + t1Lib + ttLib/index + ttx + varLib/index + voltLib + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/Doc/source/inspect.rst b/Doc/source/inspect.rst new file mode 100644 index 0000000..e0f65c2 --- /dev/null +++ b/Doc/source/inspect.rst @@ -0,0 +1,7 @@ +####### +inspect +####### + +.. automodule:: fontTools.inspect + :members: + :undoc-members: diff --git a/Doc/source/merge.rst b/Doc/source/merge.rst new file mode 100644 index 0000000..74cf9ad --- /dev/null +++ b/Doc/source/merge.rst @@ -0,0 +1,7 @@ +##### +merge +##### + +.. automodule:: fontTools.merge + :members: + :undoc-members: diff --git a/Doc/source/misc/arrayTools.rst b/Doc/source/misc/arrayTools.rst new file mode 100644 index 0000000..acb2a51 --- /dev/null +++ b/Doc/source/misc/arrayTools.rst @@ -0,0 +1,7 @@ +########## +arrayTools +########## + +.. automodule:: fontTools.misc.arrayTools + :members: + :undoc-members: diff --git a/Doc/source/misc/bezierTools.rst b/Doc/source/misc/bezierTools.rst new file mode 100644 index 0000000..c5b4d2a --- /dev/null +++ b/Doc/source/misc/bezierTools.rst @@ -0,0 +1,7 @@ +########### +bezierTools +########### + +.. automodule:: fontTools.misc.bezierTools + :members: + :undoc-members: diff --git a/Doc/source/misc/classifyTools.rst b/Doc/source/misc/classifyTools.rst new file mode 100644 index 0000000..b02b350 --- /dev/null +++ b/Doc/source/misc/classifyTools.rst @@ -0,0 +1,7 @@ +############# +classifyTools +############# + +.. automodule:: fontTools.misc.classifyTools + :members: + :undoc-members: diff --git a/Doc/source/misc/eexec.rst b/Doc/source/misc/eexec.rst new file mode 100644 index 0000000..8506f86 --- /dev/null +++ b/Doc/source/misc/eexec.rst @@ -0,0 +1,7 @@ +##### +eexec +##### + +.. automodule:: fontTools.misc.eexec + :members: + :undoc-members: diff --git a/Doc/source/misc/encodingTools.rst b/Doc/source/misc/encodingTools.rst new file mode 100644 index 0000000..ff29f66 --- /dev/null +++ b/Doc/source/misc/encodingTools.rst @@ -0,0 +1,7 @@ +############# +encodingTools +############# + +.. automodule:: fontTools.misc.encodingTools + :members: + :undoc-members: diff --git a/Doc/source/misc/fixedTools.rst b/Doc/source/misc/fixedTools.rst new file mode 100644 index 0000000..30a1f02 --- /dev/null +++ b/Doc/source/misc/fixedTools.rst @@ -0,0 +1,7 @@ +########## +fixedTools +########## + +.. automodule:: fontTools.misc.fixedTools + :members: + :undoc-members: diff --git a/Doc/source/misc/index.rst b/Doc/source/misc/index.rst new file mode 100644 index 0000000..29a7245 --- /dev/null +++ b/Doc/source/misc/index.rst @@ -0,0 +1,23 @@ +#### +misc +#### + +.. toctree:: + :maxdepth: 2 + + arrayTools + bezierTools + classifyTools + eexec + encodingTools + fixedTools + loggingTools + sstruct + psCharStrings + testTools + textTools + timeTools + transform + xmlReader + xmlWriter + diff --git a/Doc/source/misc/loggingTools.rst b/Doc/source/misc/loggingTools.rst new file mode 100644 index 0000000..fb8eab5 --- /dev/null +++ b/Doc/source/misc/loggingTools.rst @@ -0,0 +1,7 @@ +############ +loggingTools +############ + +.. automodule:: fontTools.misc.loggingTools + :members: + :undoc-members: diff --git a/Doc/source/misc/psCharStrings.rst b/Doc/source/misc/psCharStrings.rst new file mode 100644 index 0000000..3dc0dce --- /dev/null +++ b/Doc/source/misc/psCharStrings.rst @@ -0,0 +1,7 @@ +############# +psCharStrings +############# + +.. automodule:: fontTools.misc.psCharStrings + :members: + :undoc-members: diff --git a/Doc/source/misc/sstruct.rst b/Doc/source/misc/sstruct.rst new file mode 100644 index 0000000..482aa23 --- /dev/null +++ b/Doc/source/misc/sstruct.rst @@ -0,0 +1,7 @@ +####### +sstruct +####### + +.. automodule:: fontTools.misc.sstruct + :members: + :undoc-members: diff --git a/Doc/source/misc/testTools.rst b/Doc/source/misc/testTools.rst new file mode 100644 index 0000000..b3215ac --- /dev/null +++ b/Doc/source/misc/testTools.rst @@ -0,0 +1,7 @@ +######### +testTools +######### + +.. automodule:: fontTools.misc.testTools + :members: + :undoc-members: diff --git a/Doc/source/misc/textTools.rst b/Doc/source/misc/textTools.rst new file mode 100644 index 0000000..754eb67 --- /dev/null +++ b/Doc/source/misc/textTools.rst @@ -0,0 +1,7 @@ +######### +textTools +######### + +.. automodule:: fontTools.misc.textTools + :members: + :undoc-members: diff --git a/Doc/source/misc/timeTools.rst b/Doc/source/misc/timeTools.rst new file mode 100644 index 0000000..f8d1508 --- /dev/null +++ b/Doc/source/misc/timeTools.rst @@ -0,0 +1,7 @@ +######### +timeTools +######### + +.. automodule:: fontTools.misc.timeTools + :members: + :undoc-members: diff --git a/Doc/source/misc/transform.rst b/Doc/source/misc/transform.rst new file mode 100644 index 0000000..9517fe0 --- /dev/null +++ b/Doc/source/misc/transform.rst @@ -0,0 +1,7 @@ +######### +transform +######### + +.. automodule:: fontTools.misc.transform + :members: + :undoc-members: diff --git a/Doc/source/misc/xmlReader.rst b/Doc/source/misc/xmlReader.rst new file mode 100644 index 0000000..6e09354 --- /dev/null +++ b/Doc/source/misc/xmlReader.rst @@ -0,0 +1,7 @@ +######### +xmlReader +######### + +.. automodule:: fontTools.misc.xmlReader + :members: + :undoc-members: diff --git a/Doc/source/misc/xmlWriter.rst b/Doc/source/misc/xmlWriter.rst new file mode 100644 index 0000000..f488183 --- /dev/null +++ b/Doc/source/misc/xmlWriter.rst @@ -0,0 +1,7 @@ +######### +xmlWriter +######### + +.. automodule:: fontTools.misc.xmlWriter + :members: + :undoc-members: diff --git a/Doc/source/pens/areaPen.rst b/Doc/source/pens/areaPen.rst new file mode 100644 index 0000000..03a751b --- /dev/null +++ b/Doc/source/pens/areaPen.rst @@ -0,0 +1,7 @@ +####### +areaPen +####### + +.. automodule:: fontTools.pens.areaPen + :members: + :undoc-members: diff --git a/Doc/source/pens/basePen.rst b/Doc/source/pens/basePen.rst new file mode 100644 index 0000000..f5965b1 --- /dev/null +++ b/Doc/source/pens/basePen.rst @@ -0,0 +1,7 @@ +####### +basePen +####### + +.. automodule:: fontTools.pens.basePen + :members: + :undoc-members: diff --git a/Doc/source/pens/boundsPen.rst b/Doc/source/pens/boundsPen.rst new file mode 100644 index 0000000..8de5620 --- /dev/null +++ b/Doc/source/pens/boundsPen.rst @@ -0,0 +1,7 @@ +######### +boundsPen +######### + +.. automodule:: fontTools.pens.boundsPen + :members: + :undoc-members: diff --git a/Doc/source/pens/filterPen.rst b/Doc/source/pens/filterPen.rst new file mode 100644 index 0000000..0b484a4 --- /dev/null +++ b/Doc/source/pens/filterPen.rst @@ -0,0 +1,7 @@ +######### +filterPen +######### + +.. automodule:: fontTools.pens.filterPen + :members: + :undoc-members: diff --git a/Doc/source/pens/index.rst b/Doc/source/pens/index.rst new file mode 100644 index 0000000..7e5a192 --- /dev/null +++ b/Doc/source/pens/index.rst @@ -0,0 +1,18 @@ +#### +pens +#### + +.. toctree:: + :maxdepth: 1 + + basePen + boundsPen + pointInsidePen + filterPen + transformPen + t2CharStringPen + statisticsPen + recordingPen + teePen + areaPen + perimeterPen diff --git a/Doc/source/pens/perimeterPen.rst b/Doc/source/pens/perimeterPen.rst new file mode 100644 index 0000000..97fecca --- /dev/null +++ b/Doc/source/pens/perimeterPen.rst @@ -0,0 +1,7 @@ +############ +perimeterPen +############ + +.. automodule:: fontTools.pens.perimeterPen + :members: + :undoc-members: diff --git a/Doc/source/pens/pointInsidePen.rst b/Doc/source/pens/pointInsidePen.rst new file mode 100644 index 0000000..9954e47 --- /dev/null +++ b/Doc/source/pens/pointInsidePen.rst @@ -0,0 +1,7 @@ +############## +pointInsidePen +############## + +.. automodule:: fontTools.pens.pointInsidePen + :members: + :undoc-members: diff --git a/Doc/source/pens/recordingPen.rst b/Doc/source/pens/recordingPen.rst new file mode 100644 index 0000000..69e7ceb --- /dev/null +++ b/Doc/source/pens/recordingPen.rst @@ -0,0 +1,7 @@ +############ +recordingPen +############ + +.. automodule:: fontTools.pens.recordingPen + :members: + :undoc-members: diff --git a/Doc/source/pens/statisticsPen.rst b/Doc/source/pens/statisticsPen.rst new file mode 100644 index 0000000..efa1608 --- /dev/null +++ b/Doc/source/pens/statisticsPen.rst @@ -0,0 +1,7 @@ +############# +statisticsPen +############# + +.. automodule:: fontTools.pens.statisticsPen + :members: + :undoc-members: diff --git a/Doc/source/pens/t2CharStringPen.rst b/Doc/source/pens/t2CharStringPen.rst new file mode 100644 index 0000000..d58c67a --- /dev/null +++ b/Doc/source/pens/t2CharStringPen.rst @@ -0,0 +1,7 @@ +############### +t2CharStringPen +############### + +.. automodule:: fontTools.pens.t2CharStringPen + :members: + :undoc-members: diff --git a/Doc/source/pens/teePen.rst b/Doc/source/pens/teePen.rst new file mode 100644 index 0000000..7a0313c --- /dev/null +++ b/Doc/source/pens/teePen.rst @@ -0,0 +1,7 @@ +###### +teePen +###### + +.. automodule:: fontTools.pens.teePen + :members: + :undoc-members: diff --git a/Doc/source/pens/transformPen.rst b/Doc/source/pens/transformPen.rst new file mode 100644 index 0000000..5b414f8 --- /dev/null +++ b/Doc/source/pens/transformPen.rst @@ -0,0 +1,7 @@ +############ +transformPen +############ + +.. automodule:: fontTools.pens.transformPen + :members: + :undoc-members: diff --git a/Doc/source/subset.rst b/Doc/source/subset.rst new file mode 100644 index 0000000..a8fff95 --- /dev/null +++ b/Doc/source/subset.rst @@ -0,0 +1,7 @@ +###### +subset +###### + +.. automodule:: fontTools.subset + :members: + :undoc-members: diff --git a/Doc/source/t1Lib.rst b/Doc/source/t1Lib.rst new file mode 100644 index 0000000..dcc0438 --- /dev/null +++ b/Doc/source/t1Lib.rst @@ -0,0 +1,7 @@ +##### +t1Lib +##### + +.. automodule:: fontTools.t1Lib + :members: + :undoc-members: diff --git a/Doc/source/ttLib/index.rst b/Doc/source/ttLib/index.rst new file mode 100644 index 0000000..d273934 --- /dev/null +++ b/Doc/source/ttLib/index.rst @@ -0,0 +1,15 @@ +##### +ttLib +##### + +.. toctree:: + :maxdepth: 1 + + macUtils + sfnt + tables + woff2 + +.. automodule:: fontTools.ttLib + :members: + :undoc-members: diff --git a/Doc/source/ttLib/macUtils.rst b/Doc/source/ttLib/macUtils.rst new file mode 100644 index 0000000..cb014d7 --- /dev/null +++ b/Doc/source/ttLib/macUtils.rst @@ -0,0 +1,7 @@ +######## +macUtils +######## + +.. automodule:: fontTools.ttLib.macUtils + :members: + :undoc-members: diff --git a/Doc/source/ttLib/sfnt.rst b/Doc/source/ttLib/sfnt.rst new file mode 100644 index 0000000..41e3032 --- /dev/null +++ b/Doc/source/ttLib/sfnt.rst @@ -0,0 +1,7 @@ +#### +sfnt +#### + +.. automodule:: fontTools.ttLib.sfnt + :members: + :undoc-members: diff --git a/Doc/source/ttLib/tables.rst b/Doc/source/ttLib/tables.rst new file mode 100644 index 0000000..6ae705f --- /dev/null +++ b/Doc/source/ttLib/tables.rst @@ -0,0 +1,506 @@ +###### +tables +###### + +.. automodule:: fontTools.ttLib.tables + :members: + :undoc-members: + +_a_v_a_r +-------- + +.. automodule:: fontTools.ttLib.tables._a_v_a_r + :members: + :undoc-members: + +_c_m_a_p +-------- + +.. automodule:: fontTools.ttLib.tables._c_m_a_p + :members: + :undoc-members: + +_c_v_a_r +-------- + +.. automodule:: fontTools.ttLib.tables._c_v_a_r + :members: + :undoc-members: + +_c_v_t +------ + +.. automodule:: fontTools.ttLib.tables._c_v_t + :members: + :undoc-members: + +_f_e_a_t +-------- + +.. automodule:: fontTools.ttLib.tables._f_e_a_t + :members: + :undoc-members: + +_f_p_g_m +-------- + +.. automodule:: fontTools.ttLib.tables._f_p_g_m + :members: + :undoc-members: + +_f_v_a_r +-------- + +.. automodule:: fontTools.ttLib.tables._f_v_a_r + :members: + :undoc-members: + +_g_a_s_p +-------- + +.. automodule:: fontTools.ttLib.tables._g_a_s_p + :members: + :undoc-members: + +_g_l_y_f +-------- + +.. automodule:: fontTools.ttLib.tables._g_l_y_f + :members: + :undoc-members: + +_g_v_a_r +-------- + +.. automodule:: fontTools.ttLib.tables._g_v_a_r + :members: + :undoc-members: + +_h_d_m_x +-------- + +.. automodule:: fontTools.ttLib.tables._h_d_m_x + :members: + :undoc-members: + +_h_e_a_d +-------- + +.. automodule:: fontTools.ttLib.tables._h_e_a_d + :members: + :undoc-members: + +_h_h_e_a +-------- + +.. automodule:: fontTools.ttLib.tables._h_h_e_a + :members: + :undoc-members: + +_h_m_t_x +-------- + +.. automodule:: fontTools.ttLib.tables._h_m_t_x + :members: + :undoc-members: + +_k_e_r_n +-------- + +.. automodule:: fontTools.ttLib.tables._k_e_r_n + :members: + :undoc-members: + +_l_o_c_a +-------- + +.. automodule:: fontTools.ttLib.tables._l_o_c_a + :members: + :undoc-members: + +_l_t_a_g +-------- + +.. automodule:: fontTools.ttLib.tables._l_t_a_g + :members: + :undoc-members: + +_m_a_x_p +-------- + +.. automodule:: fontTools.ttLib.tables._m_a_x_p + :members: + :undoc-members: + +_m_e_t_a +-------- + +.. automodule:: fontTools.ttLib.tables._m_e_t_a + :members: + :undoc-members: + +_n_a_m_e +-------- + +.. automodule:: fontTools.ttLib.tables._n_a_m_e + :members: + :undoc-members: + +_p_o_s_t +-------- + +.. automodule:: fontTools.ttLib.tables._p_o_s_t + :members: + :undoc-members: + +_p_r_e_p +-------- + +.. automodule:: fontTools.ttLib.tables._p_r_e_p + :members: + :undoc-members: + +_s_b_i_x +-------- + +.. automodule:: fontTools.ttLib.tables._s_b_i_x + :members: + :undoc-members: + +_t_r_a_k +-------- + +.. automodule:: fontTools.ttLib.tables._t_r_a_k + :members: + :undoc-members: + +_v_h_e_a +-------- + +.. automodule:: fontTools.ttLib.tables._v_h_e_a + :members: + :undoc-members: + +_v_m_t_x +-------- + +.. automodule:: fontTools.ttLib.tables._v_m_t_x + :members: + :undoc-members: + +asciiTable +---------- + +.. automodule:: fontTools.ttLib.tables.asciiTable + :members: + :undoc-members: + +B_A_S_E_ +-------- + +.. automodule:: fontTools.ttLib.tables.B_A_S_E_ + :members: + :undoc-members: + +BitmapGlyphMetrics +------------------ + +.. automodule:: fontTools.ttLib.tables.BitmapGlyphMetrics + :members: + :undoc-members: + +C_B_D_T_ +-------- + +.. automodule:: fontTools.ttLib.tables.C_B_D_T_ + :members: + :undoc-members: + +C_B_L_C_ +-------- + +.. automodule:: fontTools.ttLib.tables.C_B_L_C_ + :members: + :undoc-members: + +C_F_F_ +------ + +.. automodule:: fontTools.ttLib.tables.C_F_F_ + :members: + :undoc-members: + +C_F_F__2 +-------- + +.. automodule:: fontTools.ttLib.tables.C_F_F__2 + :members: + :undoc-members: + +C_O_L_R_ +-------- + +.. automodule:: fontTools.ttLib.tables.C_O_L_R_ + :members: + :undoc-members: + +C_P_A_L_ +-------- + +.. automodule:: fontTools.ttLib.tables.C_P_A_L_ + :members: + :undoc-members: + +D_S_I_G_ +-------- + +.. automodule:: fontTools.ttLib.tables.D_S_I_G_ + :members: + :undoc-members: + +DefaultTable +------------ + +.. automodule:: fontTools.ttLib.tables.DefaultTable + :members: + :undoc-members: + +E_B_D_T_ +-------- + +.. automodule:: fontTools.ttLib.tables.E_B_D_T_ + :members: + :undoc-members: + +E_B_L_C_ +-------- + +.. automodule:: fontTools.ttLib.tables.E_B_L_C_ + :members: + :undoc-members: + +F_F_T_M_ +-------- + +.. automodule:: fontTools.ttLib.tables.F_F_T_M_ + :members: + :undoc-members: + +G_D_E_F_ +-------- + +.. automodule:: fontTools.ttLib.tables.G_D_E_F_ + :members: + :undoc-members: + +G_M_A_P_ +-------- + +.. automodule:: fontTools.ttLib.tables.G_M_A_P_ + :members: + :undoc-members: + +G_P_K_G_ +-------- + +.. automodule:: fontTools.ttLib.tables.G_P_K_G_ + :members: + :undoc-members: + +G_P_O_S_ +-------- + +.. automodule:: fontTools.ttLib.tables.G_P_O_S_ + :members: + :undoc-members: + +G_S_U_B_ +-------- + +.. automodule:: fontTools.ttLib.tables.G_S_U_B_ + :members: + :undoc-members: + +H_V_A_R_ +-------- + +.. automodule:: fontTools.ttLib.tables.H_V_A_R_ + :members: + :undoc-members: + +J_S_T_F_ +-------- + +.. automodule:: fontTools.ttLib.tables.J_S_T_F_ + :members: + :undoc-members: + +L_T_S_H_ +-------- + +.. automodule:: fontTools.ttLib.tables.L_T_S_H_ + :members: + :undoc-members: + +M_A_T_H_ +-------- + +.. automodule:: fontTools.ttLib.tables.M_A_T_H_ + :members: + :undoc-members: + +M_E_T_A_ +-------- + +.. automodule:: fontTools.ttLib.tables.M_E_T_A_ + :members: + :undoc-members: + +M_V_A_R_ +-------- + +.. automodule:: fontTools.ttLib.tables.M_V_A_R_ + :members: + :undoc-members: + +O_S_2f_2 +-------- + +.. automodule:: fontTools.ttLib.tables.O_S_2f_2 + :members: + :undoc-members: + +otBase +------ + +.. automodule:: fontTools.ttLib.tables.otBase + :members: + :undoc-members: + +otConverters +------------ + +.. automodule:: fontTools.ttLib.tables.otConverters + :members: + :undoc-members: + +otData +------ + +.. automodule:: fontTools.ttLib.tables.otData + :members: + :undoc-members: + +otTables +-------- + +.. automodule:: fontTools.ttLib.tables.otTables + :members: + :undoc-members: + +S_I_N_G_ +-------- + +.. automodule:: fontTools.ttLib.tables.S_I_N_G_ + :members: + :undoc-members: + +S_T_A_T_ +-------- + +.. automodule:: fontTools.ttLib.tables.S_T_A_T_ + :members: + :undoc-members: + +S_V_G_ +------ + +.. automodule:: fontTools.ttLib.tables.S_V_G_ + :members: + :undoc-members: + +sbixGlyph +--------- + +.. automodule:: fontTools.ttLib.tables.sbixGlyph + :members: + :undoc-members: + +sbixStrike +---------- + +.. automodule:: fontTools.ttLib.tables.sbixStrike + :members: + :undoc-members: + +T_S_I__0 +-------- + +.. automodule:: fontTools.ttLib.tables.T_S_I__0 + :members: + :undoc-members: + +T_S_I__1 +-------- + +.. automodule:: fontTools.ttLib.tables.T_S_I__1 + :members: + :undoc-members: + +T_S_I__2 +-------- + +.. automodule:: fontTools.ttLib.tables.T_S_I__2 + :members: + :undoc-members: + +T_S_I__3 +-------- + +.. automodule:: fontTools.ttLib.tables.T_S_I__3 + :members: + :undoc-members: + +T_S_I__5 +-------- + +.. automodule:: fontTools.ttLib.tables.T_S_I__5 + :members: + :undoc-members: + +ttProgram +--------- + +.. automodule:: fontTools.ttLib.tables.ttProgram + :members: + :undoc-members: + +TupleVariation +-------------- + +.. automodule:: fontTools.ttLib.tables.TupleVariation + :members: + :undoc-members: + +V_D_M_X_ +-------- + +.. automodule:: fontTools.ttLib.tables.V_D_M_X_ + :members: + :undoc-members: + +V_O_R_G_ +-------- + +.. automodule:: fontTools.ttLib.tables.V_O_R_G_ + :members: + :undoc-members: + +V_V_A_R_ +-------- + +.. automodule:: fontTools.ttLib.tables.V_V_A_R_ + :members: + :undoc-members: + + diff --git a/Doc/source/ttLib/woff2.rst b/Doc/source/ttLib/woff2.rst new file mode 100644 index 0000000..0c464be --- /dev/null +++ b/Doc/source/ttLib/woff2.rst @@ -0,0 +1,7 @@ +##### +woff2 +##### + +.. automodule:: fontTools.ttLib.woff2 + :members: + :undoc-members: diff --git a/Doc/source/ttx.rst b/Doc/source/ttx.rst new file mode 100644 index 0000000..1c90901 --- /dev/null +++ b/Doc/source/ttx.rst @@ -0,0 +1,7 @@ +### +ttx +### + +.. automodule:: fontTools.ttx + :members: + :undoc-members: diff --git a/Doc/source/varLib/designspace.rst b/Doc/source/varLib/designspace.rst new file mode 100644 index 0000000..e3bbdf7 --- /dev/null +++ b/Doc/source/varLib/designspace.rst @@ -0,0 +1,7 @@ +########### +designspace +########### + +.. automodule:: fontTools.varLib.designspace + :members: + :undoc-members: diff --git a/Doc/source/varLib/index.rst b/Doc/source/varLib/index.rst new file mode 100644 index 0000000..0e9f17b --- /dev/null +++ b/Doc/source/varLib/index.rst @@ -0,0 +1,17 @@ +###### +varLib +###### + +.. toctree:: + :maxdepth: 2 + + designspace + interpolatable + interpolate_layout + merger + models + mutator + +.. automodule:: fontTools.varLib + :members: + :undoc-members: diff --git a/Doc/source/varLib/interpolatable.rst b/Doc/source/varLib/interpolatable.rst new file mode 100644 index 0000000..969fb61 --- /dev/null +++ b/Doc/source/varLib/interpolatable.rst @@ -0,0 +1,7 @@ +############## +interpolatable +############## + +.. automodule:: fontTools.varLib.interpolatable + :members: + :undoc-members: diff --git a/Doc/source/varLib/interpolate_layout.rst b/Doc/source/varLib/interpolate_layout.rst new file mode 100644 index 0000000..752f748 --- /dev/null +++ b/Doc/source/varLib/interpolate_layout.rst @@ -0,0 +1,7 @@ +################## +interpolate_layout +################## + +.. automodule:: fontTools.varLib.interpolate_layout + :members: + :undoc-members: diff --git a/Doc/source/varLib/merger.rst b/Doc/source/varLib/merger.rst new file mode 100644 index 0000000..37383aa --- /dev/null +++ b/Doc/source/varLib/merger.rst @@ -0,0 +1,7 @@ +###### +merger +###### + +.. automodule:: fontTools.varLib.merger + :members: + :undoc-members: diff --git a/Doc/source/varLib/models.rst b/Doc/source/varLib/models.rst new file mode 100644 index 0000000..e6c7fa8 --- /dev/null +++ b/Doc/source/varLib/models.rst @@ -0,0 +1,7 @@ +###### +models +###### + +.. automodule:: fontTools.varLib.models + :members: + :undoc-members: diff --git a/Doc/source/varLib/mutator.rst b/Doc/source/varLib/mutator.rst new file mode 100644 index 0000000..e606ab8 --- /dev/null +++ b/Doc/source/varLib/mutator.rst @@ -0,0 +1,7 @@ +####### +mutator +####### + +.. automodule:: fontTools.varLib.mutator + :members: + :undoc-members: diff --git a/Doc/source/voltLib.rst b/Doc/source/voltLib.rst new file mode 100644 index 0000000..5906c4c --- /dev/null +++ b/Doc/source/voltLib.rst @@ -0,0 +1,35 @@ +####### +voltLib +####### + +.. automodule:: fontTools.voltLib + :members: + :undoc-members: + +ast +--- + +.. automodule:: fontTools.voltLib.ast + :members: + :undoc-members: + +error +----- + +.. automodule:: fontTools.voltLib.parser + :members: + :undoc-members: + +lexer +----- + +.. automodule:: fontTools.voltLib.lexer + :members: + :undoc-members: + +parser +------ + +.. automodule:: fontTools.voltLib.parser + :members: + :undoc-members: diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cc63390 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Just van Rossum + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LICENSE.external b/LICENSE.external new file mode 100644 index 0000000..4abc7d5 --- /dev/null +++ b/LICENSE.external @@ -0,0 +1,148 @@ +FontTools includes the following font projects for testing purposes, which are +under SIL Open Font License, Version 1.1: + +Lobster + Copyright (c) 2010, Pablo Impallari (www.impallari.com|impallari@gmail.com), + with Reserved Font Name Lobster. + This Font Software is licensed under the SIL Open Font License, Version 1.1. + +Noto Fonts + This Font Software is licensed under the SIL Open Font License, Version 1.1. + +XITS font project + Copyright (c) 2001-2010 by the STI Pub Companies, consisting of the American + Institute of Physics, the American Chemical Society, the American + Mathematical Society, the American Physical Society, Elsevier, Inc., and The + Institute of Electrical and Electronic Engineers, Inc. (www.stixfonts.org), + with Reserved Font Name STIX Fonts, STIX Fonts (TM) is a trademark of The + Institute of Electrical and Electronics Engineers, Inc. + + Portions copyright (c) 1998-2003 by MicroPress, Inc. + (www.micropress-inc.com), with Reserved Font Name TM Math. To obtain + additional mathematical fonts, please contact MicroPress, Inc., 68-30 Harrow + Street, Forest Hills, NY 11375, USA, Phone: (718) 575-1816. + + Portions copyright (c) 1990 by Elsevier, Inc. + + This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font +creation efforts of academic and linguistic communities, and to +provide a free and open framework in which fonts may be shared and +improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply to +any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software +components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, +deleting, or substituting -- in part or in whole -- any of the +components of the Original Version, by changing formats or by porting +the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, +modify, redistribute, and sell modified and unmodified copies of the +Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in +Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the +corresponding Copyright Holder. This restriction only applies to the +primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created using +the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +===== + +FontTools includes Adobe AGL & AGLFN, which is under 3-clauses BSD license: + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +Neither the name of Adobe Systems Incorporated nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index 2c90134..0000000 --- a/LICENSE.txt +++ /dev/null @@ -1,25 +0,0 @@ -Copyright 1999-2004 -by Just van Rossum, Letterror, The Netherlands. - - All Rights Reserved - -Permission to use, copy, modify, and distribute this software and -its documentation for any purpose and without fee is hereby granted, -provided that the above copyright notice appear in all copies and -that both that copyright notice and this permission notice appear -in supporting documentation, and that the names of Just van Rossum -or Letterror not be used in advertising or publicity pertaining to -distribution of the software without specific, written prior -permission. - -JUST VAN ROSSUM AND LETTERROR DISCLAIM ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL JUST VAN ROSSUM OR -LETTERROR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL -DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR -PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER -TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. - - -just@letterror.com diff --git a/Lib/fontTools/Android.bp b/Lib/fontTools/Android.bp index 7767914..34ca987 100644 --- a/Lib/fontTools/Android.bp +++ b/Lib/fontTools/Android.bp @@ -11,7 +11,6 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - python_defaults { name: "fonttools_default", version: { @@ -25,15 +24,11 @@ python_defaults { }, }, } - python_library_host { name: "fontTools", defaults: ["fonttools_default"], pkg_path: "fontTools", srcs: [ - "*.py", - "misc/*.py", - "ttLib/*.py", - "ttLib/tables/*.py", + "**/*.py", ], } diff --git a/Lib/fontTools/__init__.py b/Lib/fontTools/__init__.py index ea07e8d..fb2b35c 100644 --- a/Lib/fontTools/__init__.py +++ b/Lib/fontTools/__init__.py @@ -1 +1,10 @@ -version = "2.4" +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +import logging +from fontTools.misc.loggingTools import configLogger + +log = logging.getLogger(__name__) + +version = __version__ = "3.28.0" + +__all__ = ["version", "log", "configLogger"] diff --git a/Lib/fontTools/__main__.py b/Lib/fontTools/__main__.py new file mode 100644 index 0000000..7d45751 --- /dev/null +++ b/Lib/fontTools/__main__.py @@ -0,0 +1,33 @@ +from __future__ import print_function, division, absolute_import +import sys + + +def main(args=None): + if args is None: + args = sys.argv[1:] + + # TODO Add help output, --help, etc. + + # TODO Handle library-wide options. Eg.: + # --unicodedata + # --verbose / other logging stuff + + # TODO Allow a way to run arbitrary modules? Useful for setting + # library-wide options and calling another library. Eg.: + # + # $ fonttools --unicodedata=... fontmake ... + # + # This allows for a git-like command where thirdparty commands + # can be added. Should we just try importing the fonttools + # module first and try without if it fails? + + mod = 'fontTools.'+sys.argv[1] + sys.argv[1] = sys.argv[0] + ' ' + sys.argv[1] + del sys.argv[0] + + import runpy + runpy.run_module(mod, run_name='__main__') + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/Lib/fontTools/afmLib.py b/Lib/fontTools/afmLib.py index e679770..e0ccafe 100644 --- a/Lib/fontTools/afmLib.py +++ b/Lib/fontTools/afmLib.py @@ -14,47 +14,47 @@ identifierRE = re.compile("^([A-Za-z]+).*") # regular expression to parse char lines charRE = re.compile( "(-?\d+)" # charnum - "\s*;\s*WX\s+" # ; WX + "\s*;\s*WX\s+" # ; WX "(-?\d+)" # width - "\s*;\s*N\s+" # ; N - "([.A-Za-z0-9_]+)" # charname - "\s*;\s*B\s+" # ; B + "\s*;\s*N\s+" # ; N + "([.A-Za-z0-9_]+)" # charname + "\s*;\s*B\s+" # ; B "(-?\d+)" # left - "\s+" # + "\s+" "(-?\d+)" # bottom - "\s+" # + "\s+" "(-?\d+)" # right - "\s+" # + "\s+" "(-?\d+)" # top - "\s*;\s*" # ; + "\s*;\s*" # ; ) # regular expression to parse kerning lines kernRE = re.compile( - "([.A-Za-z0-9_]+)" # leftchar - "\s+" # - "([.A-Za-z0-9_]+)" # rightchar - "\s+" # + "([.A-Za-z0-9_]+)" # leftchar + "\s+" + "([.A-Za-z0-9_]+)" # rightchar + "\s+" "(-?\d+)" # value - "\s*" # + "\s*" ) # regular expressions to parse composite info lines of the form: # Aacute 2 ; PCC A 0 0 ; PCC acute 182 211 ; compositeRE = re.compile( - "([.A-Za-z0-9_]+)" # char name - "\s+" # + "([.A-Za-z0-9_]+)" # char name + "\s+" "(\d+)" # number of parts - "\s*;\s*" # + "\s*;\s*" ) componentRE = re.compile( "PCC\s+" # PPC - "([.A-Za-z0-9_]+)" # base char name - "\s+" # + "([.A-Za-z0-9_]+)" # base char name + "\s+" "(-?\d+)" # x offset - "\s+" # + "\s+" "(-?\d+)" # y offset - "\s*;\s*" # + "\s*;\s*" ) preferredAttributeOrder = [ @@ -77,13 +77,14 @@ preferredAttributeOrder = [ ] -class error(Exception): pass +class error(Exception): + pass class AFM(object): - + _attrs = None - + _keywords = ['StartFontMetrics', 'EndFontMetrics', 'StartCharMetrics', @@ -95,7 +96,7 @@ class AFM(object): 'StartComposites', 'EndComposites', ] - + def __init__(self, path=None): self._attrs = {} self._chars = {} @@ -105,7 +106,7 @@ class AFM(object): self._composites = {} if path is not None: self.read(path) - + def read(self, path): lines = readlines(path) for line in lines: @@ -114,7 +115,7 @@ class AFM(object): m = identifierRE.match(line) if m is None: raise error("syntax error in AFM file: " + repr(line)) - + pos = m.regs[1][1] word = line[:pos] rest = line[pos:].strip() @@ -128,7 +129,7 @@ class AFM(object): self.parsecomposite(rest) else: self.parseattr(word, rest) - + def parsechar(self, rest): m = charRE.match(rest) if m is None: @@ -140,7 +141,7 @@ class AFM(object): del things[2] charnum, width, l, b, r, t = (int(thing) for thing in things) self._chars[charname] = charnum, width, (l, b, r, t) - + def parsekernpair(self, rest): m = kernRE.match(rest) if m is None: @@ -151,7 +152,7 @@ class AFM(object): leftchar, rightchar, value = things value = int(value) self._kerning[(leftchar, rightchar)] = value - + def parseattr(self, word, rest): if word == "FontBBox": l, b, r, t = [int(thing) for thing in rest.split()] @@ -165,7 +166,7 @@ class AFM(object): self._attrs[word] = rest else: self._attrs[word] = value - + def parsecomposite(self, rest): m = compositeRE.match(rest) if m is None: @@ -187,19 +188,19 @@ class AFM(object): break assert len(components) == ncomponents self._composites[charname] = components - + def write(self, path, sep='\r'): import time lines = [ "StartFontMetrics 2.0", "Comment Generated by afmLib; at %s" % ( - time.strftime("%m/%d/%Y %H:%M:%S", + time.strftime("%m/%d/%Y %H:%M:%S", time.localtime(time.time())))] - + # write comments, assuming (possibly wrongly!) they should # all appear at the top for comment in self._comments: lines.append("Comment " + comment) - + # write attributes, first the ones we know about, in # a preferred order attrs = self._attrs @@ -216,24 +217,24 @@ class AFM(object): if attr in preferredAttributeOrder: continue lines.append(attr + " " + str(value)) - + # write char metrics lines.append("StartCharMetrics " + repr(len(self._chars))) items = [(charnum, (charname, width, box)) for charname, (charnum, width, box) in self._chars.items()] - + def myKey(a): - """Custom key function to make sure unencoded chars (-1) + """Custom key function to make sure unencoded chars (-1) end up at the end of the list after sorting.""" if a[0] == -1: a = (0xffff,) + a[1:] # 0xffff is an arbitrary large number return a items.sort(key=myKey) - + for charnum, (charname, width, (l, b, r, t)) in items: lines.append("C %d ; WX %d ; N %s ; B %d %d %d %d ;" % (charnum, width, charname, l, b, r, t)) lines.append("EndCharMetrics") - + # write kerning info lines.append("StartKernData") lines.append("StartKernPairs " + repr(len(self._kerning))) @@ -242,7 +243,7 @@ class AFM(object): lines.append("KPX %s %s %d" % (leftchar, rightchar, value)) lines.append("EndKernPairs") lines.append("EndKernData") - + if self._composites: composites = sorted(self._composites.items()) lines.append("StartComposites %s" % len(self._composites)) @@ -252,45 +253,45 @@ class AFM(object): line = line + " PCC %s %s %s ;" % (basechar, xoffset, yoffset) lines.append(line) lines.append("EndComposites") - + lines.append("EndFontMetrics") - + writelines(path, lines, sep) - + def has_kernpair(self, pair): return pair in self._kerning - + def kernpairs(self): return list(self._kerning.keys()) - + def has_char(self, char): return char in self._chars - + def chars(self): return list(self._chars.keys()) - + def comments(self): return self._comments - + def addComment(self, comment): self._comments.append(comment) - + def addComposite(self, glyphName, components): self._composites[glyphName] = components - + def __getattr__(self, attr): if attr in self._attrs: return self._attrs[attr] else: raise AttributeError(attr) - + def __setattr__(self, attr, value): # all attrs *not* starting with "_" are consider to be AFM keywords if attr[:1] == "_": self.__dict__[attr] = value else: self._attrs[attr] = value - + def __delattr__(self, attr): # all attrs *not* starting with "_" are consider to be AFM keywords if attr[:1] == "_": @@ -303,7 +304,7 @@ class AFM(object): del self._attrs[attr] except KeyError: raise AttributeError(attr) - + def __getitem__(self, key): if isinstance(key, tuple): # key is a tuple, return the kernpair @@ -311,7 +312,7 @@ class AFM(object): else: # return the metrics instead return self._chars[key] - + def __setitem__(self, key, value): if isinstance(key, tuple): # key is a tuple, set kernpair @@ -319,7 +320,7 @@ class AFM(object): else: # set char metrics self._chars[key] = value - + def __delitem__(self, key): if isinstance(key, tuple): # key is a tuple, del kernpair @@ -327,7 +328,7 @@ class AFM(object): else: # del char metrics del self._chars[key] - + def __repr__(self): if hasattr(self, "FullName"): return '' % self.FullName @@ -336,24 +337,14 @@ class AFM(object): def readlines(path): - f = open(path, 'rb') - data = f.read() - f.close() - # read any text file, regardless whether it's formatted for Mac, Unix or Dos - sep = "" - if '\r' in data: - sep = sep + '\r' # mac or dos - if '\n' in data: - sep = sep + '\n' # unix or dos - return data.split(sep) + with open(path, "r", encoding="ascii") as f: + data = f.read() + return data.splitlines() def writelines(path, lines, sep='\r'): - f = open(path, 'wb') - for line in lines: - f.write(line + sep) - f.close() - - + with open(path, "w", encoding="ascii", newline=sep) as f: + f.write("\n".join(lines) + "\n") + if __name__ == "__main__": import EasyDialogs @@ -374,4 +365,3 @@ if __name__ == "__main__": #print afm.kernpairs() print(afm) afm.write(path + ".muck") - diff --git a/Lib/fontTools/agl.py b/Lib/fontTools/agl.py index 5f20f51..ec1a1b0 100644 --- a/Lib/fontTools/agl.py +++ b/Lib/fontTools/agl.py @@ -1,8 +1,12 @@ +# -*- coding: utf-8 -*- # The table below is taken from # http://www.adobe.com/devnet/opentype/archives/aglfn.txt -from __future__ import print_function, division, absolute_import +from __future__ import (print_function, division, absolute_import, + unicode_literals) from fontTools.misc.py23 import * +import re + _aglText = """\ # ----------------------------------------------------------- @@ -705,18 +709,19 @@ _aglText = """\ """ -AGLError = "AGLError" +class AGLError(Exception): + pass AGL2UV = {} UV2AGL = {} def _builddicts(): import re - + lines = _aglText.splitlines() - + parseAGL_RE = re.compile("([0-9A-F]{4});([A-Za-z_0-9.]+);.*?$") - + for line in lines: if not line or line[:1] == '#': continue @@ -726,12 +731,145 @@ def _builddicts(): unicode = m.group(1) assert len(unicode) == 4 unicode = int(unicode, 16) - glyphName = m.group(2) + glyphName = tostr(m.group(2)) if glyphName in AGL2UV: # the above table contains identical duplicates assert AGL2UV[glyphName] == unicode else: AGL2UV[glyphName] = unicode UV2AGL[unicode] = glyphName - + _builddicts() + + +def toUnicode(glyph, isZapfDingbats=False): + """Convert glyph names to Unicode, such as 'longs_t.oldstyle' --> u'ſt' + + If isZapfDingbats is True, the implementation recognizes additional + glyph names (as required by the AGL specification). + """ + # https://github.com/adobe-type-tools/agl-specification#2-the-mapping + # + # 1. Drop all the characters from the glyph name starting with + # the first occurrence of a period (U+002E; FULL STOP), if any. + glyph = glyph.split(".", 1)[0] + + # 2. Split the remaining string into a sequence of components, + # using underscore (U+005F; LOW LINE) as the delimiter. + components = glyph.split("_") + + # 3. Map each component to a character string according to the + # procedure below, and concatenate those strings; the result + # is the character string to which the glyph name is mapped. + result = [_glyphComponentToUnicode(c, isZapfDingbats) + for c in components] + return "".join(result) + + +def _glyphComponentToUnicode(component, isZapfDingbats): + # If the font is Zapf Dingbats (PostScript FontName: ZapfDingbats), + # and the component is in the ITC Zapf Dingbats Glyph List, then + # map it to the corresponding character in that list. + dingbat = _zapfDingbatsToUnicode(component) if isZapfDingbats else None + if dingbat: + return dingbat + + # Otherwise, if the component is in AGL, then map it + # to the corresponding character in that list. + # + # TODO: We currently use the AGLFN (Adobe glyph list for new fonts), + # although the spec actually mandates the legacy AGL which is + # a superset of the AGLFN. + # https://github.com/fonttools/fonttools/issues/775 + uchar = AGL2UV.get(component) + if uchar: + return unichr(uchar) + + # Otherwise, if the component is of the form "uni" (U+0075, + # U+006E, and U+0069) followed by a sequence of uppercase + # hexadecimal digits (0–9 and A–F, meaning U+0030 through + # U+0039 and U+0041 through U+0046), if the length of that + # sequence is a multiple of four, and if each group of four + # digits represents a value in the ranges 0000 through D7FF + # or E000 through FFFF, then interpret each as a Unicode scalar + # value and map the component to the string made of those + # scalar values. Note that the range and digit-length + # restrictions mean that the "uni" glyph name prefix can be + # used only with UVs in the Basic Multilingual Plane (BMP). + uni = _uniToUnicode(component) + if uni: + return uni + + # Otherwise, if the component is of the form "u" (U+0075) + # followed by a sequence of four to six uppercase hexadecimal + # digits (0–9 and A–F, meaning U+0030 through U+0039 and + # U+0041 through U+0046), and those digits represents a value + # in the ranges 0000 through D7FF or E000 through 10FFFF, then + # interpret it as a Unicode scalar value and map the component + # to the string made of this scalar value. + uni = _uToUnicode(component) + if uni: + return uni + + # Otherwise, map the component to an empty string. + return '' + + +# https://github.com/adobe-type-tools/agl-aglfn/blob/master/zapfdingbats.txt +_AGL_ZAPF_DINGBATS = ( + " ✁✂✄☎✆✝✞✟✠✡☛☞✌✍✎✏✑✒✓✔✕✖✗✘✙✚✛✜✢✣✤✥✦✧★✩✪✫✬✭✮✯✰✱✲✳✴✵✶✷✸✹✺✻✼✽✾✿❀" + "❁❂❃❄❅❆❇❈❉❊❋●❍■❏❑▲▼◆❖ ◗❘❙❚❯❱❲❳❨❩❬❭❪❫❴❵❛❜❝❞❡❢❣❤✐❥❦❧♠♥♦♣ ✉✈✇" + "①②③④⑤⑥⑦⑧⑨⑩❶❷❸❹❺❻❼❽❾❿➀➁➂➃➄➅➆➇➈➉➊➋➌➍➎➏➐➑➒➓➔→➣↔" + "↕➙➛➜➝➞➟➠➡➢➤➥➦➧➨➩➫➭➯➲➳➵➸➺➻➼➽➾➚➪➶➹➘➴➷➬➮➱✃❐❒❮❰") + + +def _zapfDingbatsToUnicode(glyph): + """Helper for toUnicode().""" + if len(glyph) < 2 or glyph[0] != 'a': + return None + try: + gid = int(glyph[1:]) + except ValueError: + return None + if gid < 0 or gid >= len(_AGL_ZAPF_DINGBATS): + return None + uchar = _AGL_ZAPF_DINGBATS[gid] + return uchar if uchar != ' ' else None + + +_re_uni = re.compile("^uni([0-9A-F]+)$") + + +def _uniToUnicode(component): + """Helper for toUnicode() to handle "uniABCD" components.""" + match = _re_uni.match(component) + if match is None: + return None + digits = match.group(1) + if len(digits) % 4 != 0: + return None + chars = [int(digits[i : i + 4], 16) + for i in range(0, len(digits), 4)] + if any(c >= 0xD800 and c <= 0xDFFF for c in chars): + # The AGL specification explicitly excluded surrogate pairs. + return None + return ''.join([unichr(c) for c in chars]) + + +_re_u = re.compile("^u([0-9A-F]{4,6})$") + + +def _uToUnicode(component): + """Helper for toUnicode() to handle "u1ABCD" components.""" + match = _re_u.match(component) + if match is None: + return None + digits = match.group(1) + try: + value = int(digits, 16) + except ValueError: + return None + if ((value >= 0x0000 and value <= 0xD7FF) or + (value >= 0xE000 and value <= 0x10FFFF)): + return unichr(value) + return None diff --git a/Lib/fontTools/cffLib.py b/Lib/fontTools/cffLib/__init__.py similarity index 51% rename from Lib/fontTools/cffLib.py rename to Lib/fontTools/cffLib/__init__.py index cd5a92f..83526ed 100644 --- a/Lib/fontTools/cffLib.py +++ b/Lib/fontTools/cffLib/__init__.py @@ -4,106 +4,203 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.misc import sstruct from fontTools.misc import psCharStrings +from fontTools.misc.arrayTools import unionRect, intRect from fontTools.misc.textTools import safeEval +from fontTools.ttLib import TTFont +from fontTools.ttLib.tables.otBase import OTTableWriter +from fontTools.ttLib.tables.otBase import OTTableReader +from fontTools.ttLib.tables import otTables as ot import struct +import logging +import re -DEBUG = 0 - +# mute cffLib debug messages when running ttx in verbose mode +DEBUG = logging.DEBUG - 1 +log = logging.getLogger(__name__) cffHeaderFormat = """ major: B minor: B hdrSize: B - offSize: B """ +maxStackLimit = 513 +# maxstack operator has been deprecated. max stack is now always 513. + + class CFFFontSet(object): - - def __init__(self): - pass - - def decompile(self, file, otFont): - sstruct.unpack(cffHeaderFormat, file.read(4), self) - assert self.major == 1 and self.minor == 0, \ - "unknown CFF format: %d.%d" % (self.major, self.minor) - - file.seek(self.hdrSize) - self.fontNames = list(Index(file)) - self.topDictIndex = TopDictIndex(file) - self.strings = IndexedStrings(file) - self.GlobalSubrs = GlobalSubrsIndex(file) + + def decompile(self, file, otFont, isCFF2=None): + self.otFont = otFont + sstruct.unpack(cffHeaderFormat, file.read(3), self) + if isCFF2 is not None: + # called from ttLib: assert 'major' as read from file matches the + # expected version + expected_major = (2 if isCFF2 else 1) + if self.major != expected_major: + raise ValueError( + "Invalid CFF 'major' version: expected %d, found %d" % + (expected_major, self.major)) + else: + # use 'major' version from file to determine if isCFF2 + assert self.major in (1, 2), "Unknown CFF format" + isCFF2 = self.major == 2 + if not isCFF2: + self.offSize = struct.unpack("B", file.read(1))[0] + file.seek(self.hdrSize) + self.fontNames = list(tostr(s) for s in Index(file, isCFF2=isCFF2)) + self.topDictIndex = TopDictIndex(file, isCFF2=isCFF2) + self.strings = IndexedStrings(file) + else: # isCFF2 + self.topDictSize = struct.unpack(">H", file.read(2))[0] + file.seek(self.hdrSize) + self.fontNames = ["CFF2Font"] + cff2GetGlyphOrder = otFont.getGlyphOrder + # in CFF2, offsetSize is the size of the TopDict data. + self.topDictIndex = TopDictIndex( + file, cff2GetGlyphOrder, self.topDictSize, isCFF2=isCFF2) + self.strings = None + self.GlobalSubrs = GlobalSubrsIndex(file, isCFF2=isCFF2) self.topDictIndex.strings = self.strings self.topDictIndex.GlobalSubrs = self.GlobalSubrs - + def __len__(self): return len(self.fontNames) - + def keys(self): return list(self.fontNames) - + def values(self): return self.topDictIndex - - def __getitem__(self, name): - try: - index = self.fontNames.index(name) - except ValueError: - raise KeyError(name) + + def __getitem__(self, nameOrIndex): + """ Return TopDict instance identified by name (str) or index (int + or any object that implements `__index__`). + """ + if hasattr(nameOrIndex, "__index__"): + index = nameOrIndex.__index__() + elif isinstance(nameOrIndex, basestring): + name = nameOrIndex + try: + index = self.fontNames.index(name) + except ValueError: + raise KeyError(nameOrIndex) + else: + raise TypeError(nameOrIndex) return self.topDictIndex[index] - - def compile(self, file, otFont): - strings = IndexedStrings() - writer = CFFWriter() - writer.add(sstruct.pack(cffHeaderFormat, self)) - fontNames = Index() - for name in self.fontNames: - fontNames.append(name) - writer.add(fontNames.getCompiler(strings, None)) - topCompiler = self.topDictIndex.getCompiler(strings, None) + + def compile(self, file, otFont, isCFF2=None): + self.otFont = otFont + if isCFF2 is not None: + # called from ttLib: assert 'major' value matches expected version + expected_major = (2 if isCFF2 else 1) + if self.major != expected_major: + raise ValueError( + "Invalid CFF 'major' version: expected %d, found %d" % + (expected_major, self.major)) + else: + # use current 'major' value to determine output format + assert self.major in (1, 2), "Unknown CFF format" + isCFF2 = self.major == 2 + + if otFont.recalcBBoxes and not isCFF2: + for topDict in self.topDictIndex: + topDict.recalcFontBBox() + + if not isCFF2: + strings = IndexedStrings() + else: + strings = None + writer = CFFWriter(isCFF2) + topCompiler = self.topDictIndex.getCompiler(strings, self, isCFF2=isCFF2) + if isCFF2: + self.hdrSize = 5 + writer.add(sstruct.pack(cffHeaderFormat, self)) + # Note: topDictSize will most likely change in CFFWriter.toFile(). + self.topDictSize = topCompiler.getDataLength() + writer.add(struct.pack(">H", self.topDictSize)) + else: + self.hdrSize = 4 + self.offSize = 4 # will most likely change in CFFWriter.toFile(). + writer.add(sstruct.pack(cffHeaderFormat, self)) + writer.add(struct.pack("B", self.offSize)) + if not isCFF2: + fontNames = Index() + for name in self.fontNames: + fontNames.append(name) + writer.add(fontNames.getCompiler(strings, self, isCFF2=isCFF2)) writer.add(topCompiler) - writer.add(strings.getCompiler()) - writer.add(self.GlobalSubrs.getCompiler(strings, None)) - + if not isCFF2: + writer.add(strings.getCompiler()) + writer.add(self.GlobalSubrs.getCompiler(strings, self, isCFF2=isCFF2)) + for topDict in self.topDictIndex: if not hasattr(topDict, "charset") or topDict.charset is None: charset = otFont.getGlyphOrder() topDict.charset = charset - - for child in topCompiler.getChildren(strings): + children = topCompiler.getChildren(strings) + for child in children: writer.add(child) - + writer.toFile(file) - - def toXML(self, xmlWriter, progress=None): + + def toXML(self, xmlWriter): + xmlWriter.simpletag("major", value=self.major) + xmlWriter.newline() + xmlWriter.simpletag("minor", value=self.minor) + xmlWriter.newline() for fontName in self.fontNames: xmlWriter.begintag("CFFFont", name=tostr(fontName)) xmlWriter.newline() font = self[fontName] - font.toXML(xmlWriter, progress) + font.toXML(xmlWriter) xmlWriter.endtag("CFFFont") xmlWriter.newline() xmlWriter.newline() xmlWriter.begintag("GlobalSubrs") xmlWriter.newline() - self.GlobalSubrs.toXML(xmlWriter, progress) + self.GlobalSubrs.toXML(xmlWriter) xmlWriter.endtag("GlobalSubrs") xmlWriter.newline() - - def fromXML(self, name, attrs, content): - if not hasattr(self, "GlobalSubrs"): - self.GlobalSubrs = GlobalSubrsIndex() + + def fromXML(self, name, attrs, content, otFont=None): + self.otFont = otFont + + # set defaults. These will be replaced if there are entries for them + # in the XML file. + if not hasattr(self, "major"): self.major = 1 + if not hasattr(self, "minor"): self.minor = 0 - self.hdrSize = 4 - self.offSize = 4 # XXX ?? + if name == "CFFFont": - if not hasattr(self, "fontNames"): - self.fontNames = [] - self.topDictIndex = TopDictIndex() - fontName = attrs["name"] - topDict = TopDict(GlobalSubrs=self.GlobalSubrs) - topDict.charset = None # gets filled in later - self.fontNames.append(fontName) + if self.major == 1: + if not hasattr(self, "offSize"): + # this will be recalculated when the cff is compiled. + self.offSize = 4 + if not hasattr(self, "hdrSize"): + self.hdrSize = 4 + if not hasattr(self, "GlobalSubrs"): + self.GlobalSubrs = GlobalSubrsIndex() + if not hasattr(self, "fontNames"): + self.fontNames = [] + self.topDictIndex = TopDictIndex() + fontName = attrs["name"] + self.fontNames.append(fontName) + topDict = TopDict(GlobalSubrs=self.GlobalSubrs) + topDict.charset = None # gets filled in later + elif self.major == 2: + if not hasattr(self, "hdrSize"): + self.hdrSize = 5 + if not hasattr(self, "GlobalSubrs"): + self.GlobalSubrs = GlobalSubrsIndex() + if not hasattr(self, "fontNames"): + self.fontNames = ["CFF2Font"] + cff2GetGlyphOrder = self.otFont.getGlyphOrder + topDict = TopDict( + GlobalSubrs=self.GlobalSubrs, + cff2GetGlyphOrder=cff2GetGlyphOrder) + self.topDictIndex = TopDictIndex(None, cff2GetGlyphOrder, None) self.topDictIndex.append(topDict) for element in content: if isinstance(element, basestring): @@ -111,35 +208,120 @@ class CFFFontSet(object): name, attrs, content = element topDict.fromXML(name, attrs, content) elif name == "GlobalSubrs": + subrCharStringClass = psCharStrings.T2CharString + if not hasattr(self, "GlobalSubrs"): + self.GlobalSubrs = GlobalSubrsIndex() for element in content: if isinstance(element, basestring): continue name, attrs, content = element - subr = psCharStrings.T2CharString() + subr = subrCharStringClass() subr.fromXML(name, attrs, content) self.GlobalSubrs.append(subr) + elif name == "major": + self.major = int(attrs['value']) + elif name == "minor": + self.minor = int(attrs['value']) + + def convertCFFToCFF2(self, otFont): + # This assumes a decompiled CFF table. + self.major = 2 + cff2GetGlyphOrder = self.otFont.getGlyphOrder + topDictData = TopDictIndex(None, cff2GetGlyphOrder, None) + topDictData.items = self.topDictIndex.items + self.topDictIndex = topDictData + topDict = topDictData[0] + if hasattr(topDict, 'Private'): + privateDict = topDict.Private + else: + privateDict = None + opOrder = buildOrder(topDictOperators2) + topDict.order = opOrder + topDict.cff2GetGlyphOrder = cff2GetGlyphOrder + for entry in topDictOperators: + key = entry[1] + if key not in opOrder: + if key in topDict.rawDict: + del topDict.rawDict[key] + if hasattr(topDict, key): + delattr(topDict, key) + + if not hasattr(topDict, "FDArray"): + fdArray = topDict.FDArray = FDArrayIndex() + fdArray.strings = None + fdArray.GlobalSubrs = topDict.GlobalSubrs + topDict.GlobalSubrs.fdArray = fdArray + charStrings = topDict.CharStrings + if charStrings.charStringsAreIndexed: + charStrings.charStringsIndex.fdArray = fdArray + else: + charStrings.fdArray = fdArray + fontDict = FontDict() + fontDict.setCFF2(True) + fdArray.append(fontDict) + fontDict.Private = privateDict + privateOpOrder = buildOrder(privateDictOperators2) + for entry in privateDictOperators: + key = entry[1] + if key not in privateOpOrder: + if key in privateDict.rawDict: + # print "Removing private dict", key + del privateDict.rawDict[key] + if hasattr(privateDict, key): + delattr(privateDict, key) + # print "Removing privateDict attr", key + else: + # clean up the PrivateDicts in the fdArray + fdArray = topDict.FDArray + privateOpOrder = buildOrder(privateDictOperators2) + for fontDict in fdArray: + fontDict.setCFF2(True) + for key in fontDict.rawDict.keys(): + if key not in fontDict.order: + del fontDict.rawDict[key] + if hasattr(fontDict, key): + delattr(fontDict, key) + + privateDict = fontDict.Private + for entry in privateDictOperators: + key = entry[1] + if key not in privateOpOrder: + if key in privateDict.rawDict: + # print "Removing private dict", key + del privateDict.rawDict[key] + if hasattr(privateDict, key): + delattr(privateDict, key) + # print "Removing privateDict attr", key + # At this point, the Subrs and Charstrings are all still T2Charstring class + # easiest to fix this by compiling, then decompiling again + file = BytesIO() + self.compile(file, otFont, isCFF2=True) + file.seek(0) + self.decompile(file, otFont, isCFF2=True) class CFFWriter(object): - - def __init__(self): + + def __init__(self, isCFF2): self.data = [] - + self.isCFF2 = isCFF2 + def add(self, table): self.data.append(table) - + def toFile(self, file): lastPosList = None count = 1 while True: - if DEBUG: - print("CFFWriter.toFile() iteration:", count) + log.log(DEBUG, "CFFWriter.toFile() iteration: %d", count) count = count + 1 pos = 0 posList = [pos] for item in self.data: if hasattr(item, "getDataLength"): endPos = pos + item.getDataLength() + if isinstance(item, TopDictIndexCompiler) and item.isCFF2: + self.topDictSize = item.getDataLength() else: endPos = pos + len(item) if hasattr(item, "setPos"): @@ -149,9 +331,13 @@ class CFFWriter(object): if posList == lastPosList: break lastPosList = posList - if DEBUG: - print("CFFWriter.toFile() writing to file.") + log.log(DEBUG, "CFFWriter.toFile() writing to file.") begin = file.tell() + if self.isCFF2: + self.data[1] = struct.pack(">H", self.topDictSize) + else: + self.offSize = calcOffSize(lastPosList[-1]) + self.data[1] = struct.pack("B", self.offSize) posList = [0] for item in self.data: if hasattr(item, "toFile"): @@ -175,83 +361,127 @@ def calcOffSize(largestOffset): class IndexCompiler(object): - - def __init__(self, items, strings, parent): + + def __init__(self, items, strings, parent, isCFF2=None): + if isCFF2 is None and hasattr(parent, "isCFF2"): + isCFF2 = parent.isCFF2 + assert isCFF2 is not None + self.isCFF2 = isCFF2 self.items = self.getItems(items, strings) self.parent = parent - + def getItems(self, items, strings): return items - + def getOffsets(self): - pos = 1 - offsets = [pos] - for item in self.items: - if hasattr(item, "getDataLength"): - pos = pos + item.getDataLength() - else: - pos = pos + len(item) - offsets.append(pos) + # An empty INDEX contains only the count field. + if self.items: + pos = 1 + offsets = [pos] + for item in self.items: + if hasattr(item, "getDataLength"): + pos = pos + item.getDataLength() + else: + pos = pos + len(item) + offsets.append(pos) + else: + offsets = [] return offsets - + def getDataLength(self): - lastOffset = self.getOffsets()[-1] - offSize = calcOffSize(lastOffset) - dataLength = ( - 2 + # count - 1 + # offSize - (len(self.items) + 1) * offSize + # the offsets - lastOffset - 1 # size of object data - ) + if self.isCFF2: + countSize = 4 + else: + countSize = 2 + + if self.items: + lastOffset = self.getOffsets()[-1] + offSize = calcOffSize(lastOffset) + dataLength = ( + countSize + # count + 1 + # offSize + (len(self.items) + 1) * offSize + # the offsets + lastOffset - 1 # size of object data + ) + else: + # count. For empty INDEX tables, this is the only entry. + dataLength = countSize + return dataLength - + def toFile(self, file): offsets = self.getOffsets() - writeCard16(file, len(self.items)) - offSize = calcOffSize(offsets[-1]) - writeCard8(file, offSize) - offSize = -offSize - pack = struct.pack - for offset in offsets: - binOffset = pack(">l", offset)[offSize:] - assert len(binOffset) == -offSize - file.write(binOffset) - for item in self.items: - if hasattr(item, "toFile"): - item.toFile(file) - else: - file.write(tobytes(item, encoding="latin1")) + if self.isCFF2: + writeCard32(file, len(self.items)) + else: + writeCard16(file, len(self.items)) + # An empty INDEX contains only the count field. + if self.items: + offSize = calcOffSize(offsets[-1]) + writeCard8(file, offSize) + offSize = -offSize + pack = struct.pack + for offset in offsets: + binOffset = pack(">l", offset)[offSize:] + assert len(binOffset) == -offSize + file.write(binOffset) + for item in self.items: + if hasattr(item, "toFile"): + item.toFile(file) + else: + data = tobytes(item, encoding="latin1") + file.write(data) class IndexedStringsCompiler(IndexCompiler): - + def getItems(self, items, strings): return items.strings class TopDictIndexCompiler(IndexCompiler): - + def getItems(self, items, strings): out = [] for item in items: out.append(item.getCompiler(strings, self)) return out - + def getChildren(self, strings): children = [] for topDict in self.items: children.extend(topDict.getChildren(strings)) return children + def getOffsets(self): + if self.isCFF2: + offsets = [0, self.items[0].getDataLength()] + return offsets + else: + return super(TopDictIndexCompiler, self).getOffsets() + + def getDataLength(self): + if self.isCFF2: + dataLength = self.items[0].getDataLength() + return dataLength + else: + return super(TopDictIndexCompiler, self).getDataLength() + + def toFile(self, file): + if self.isCFF2: + self.items[0].toFile(file) + else: + super(TopDictIndexCompiler, self).toFile(file) + class FDArrayIndexCompiler(IndexCompiler): - + def getItems(self, items, strings): out = [] for item in items: out.append(item.getCompiler(strings, self)) return out - + def getChildren(self, strings): children = [] for fontDict in self.items: @@ -260,7 +490,10 @@ class FDArrayIndexCompiler(IndexCompiler): def toFile(self, file): offsets = self.getOffsets() - writeCard16(file, len(self.items)) + if self.isCFF2: + writeCard32(file, len(self.items)) + else: + writeCard16(file, len(self.items)) offSize = calcOffSize(offsets[-1]) writeCard8(file, offSize) offSize = -offSize @@ -280,110 +513,139 @@ class FDArrayIndexCompiler(IndexCompiler): class GlobalSubrsCompiler(IndexCompiler): + def getItems(self, items, strings): out = [] for cs in items: - cs.compile() + cs.compile(self.isCFF2) out.append(cs.bytecode) return out + class SubrsCompiler(GlobalSubrsCompiler): + def setPos(self, pos, endPos): offset = pos - self.parent.pos self.parent.rawDict["Subrs"] = offset + class CharStringsCompiler(GlobalSubrsCompiler): + + def getItems(self, items, strings): + out = [] + for cs in items: + cs.compile(self.isCFF2) + out.append(cs.bytecode) + return out + def setPos(self, pos, endPos): self.parent.rawDict["CharStrings"] = pos class Index(object): - + """This class represents what the CFF spec calls an INDEX.""" - + compilerClass = IndexCompiler - - def __init__(self, file=None): + + def __init__(self, file=None, isCFF2=None): + assert (isCFF2 is None) == (file is None) + self.items = [] name = self.__class__.__name__ if file is None: - self.items = [] return - if DEBUG: - print("loading %s at %s" % (name, file.tell())) + self._isCFF2 = isCFF2 + log.log(DEBUG, "loading %s at %s", name, file.tell()) self.file = file - count = readCard16(file) - self.count = count - self.items = [None] * count + if isCFF2: + count = readCard32(file) + else: + count = readCard16(file) if count == 0: - self.items = [] return + self.items = [None] * count offSize = readCard8(file) - if DEBUG: - print(" index count: %s offSize: %s" % (count, offSize)) + log.log(DEBUG, " index count: %s offSize: %s", count, offSize) assert offSize <= 4, "offSize too large: %s" % offSize self.offsets = offsets = [] pad = b'\0' * (4 - offSize) - for index in range(count+1): + for index in range(count + 1): chunk = file.read(offSize) chunk = pad + chunk offset, = struct.unpack(">L", chunk) offsets.append(int(offset)) self.offsetBase = file.tell() - 1 file.seek(self.offsetBase + offsets[-1]) # pretend we've read the whole lot - if DEBUG: - print(" end of %s at %s" % (name, file.tell())) - + log.log(DEBUG, " end of %s at %s", name, file.tell()) + def __len__(self): return len(self.items) - + def __getitem__(self, index): item = self.items[index] if item is not None: return item offset = self.offsets[index] + self.offsetBase - size = self.offsets[index+1] - self.offsets[index] + size = self.offsets[index + 1] - self.offsets[index] file = self.file file.seek(offset) data = file.read(size) assert len(data) == size - item = self.produceItem(index, data, file, offset, size) + item = self.produceItem(index, data, file, offset) self.items[index] = item return item - - def produceItem(self, index, data, file, offset, size): + + def __setitem__(self, index, item): + self.items[index] = item + + def produceItem(self, index, data, file, offset): return data - + def append(self, item): self.items.append(item) - - def getCompiler(self, strings, parent): - return self.compilerClass(self, strings, parent) + + def getCompiler(self, strings, parent, isCFF2=None): + return self.compilerClass(self, strings, parent, isCFF2=isCFF2) class GlobalSubrsIndex(Index): - + compilerClass = GlobalSubrsCompiler - - def __init__(self, file=None, globalSubrs=None, private=None, fdSelect=None, fdArray=None): - Index.__init__(self, file) + subrClass = psCharStrings.T2CharString + charStringClass = psCharStrings.T2CharString + + def __init__(self, file=None, globalSubrs=None, private=None, + fdSelect=None, fdArray=None, isCFF2=None): + super(GlobalSubrsIndex, self).__init__(file, isCFF2=isCFF2) self.globalSubrs = globalSubrs self.private = private if fdSelect: self.fdSelect = fdSelect if fdArray: self.fdArray = fdArray - - def produceItem(self, index, data, file, offset, size): + if isCFF2: + # CFF2Subr's can have numeric arguments on the stack after the last operator. + self.subrClass = psCharStrings.CFF2Subr + self.charStringClass = psCharStrings.CFF2Subr + + + def produceItem(self, index, data, file, offset): if self.private is not None: private = self.private elif hasattr(self, 'fdArray') and self.fdArray is not None: - private = self.fdArray[self.fdSelect[index]].Private + if hasattr(self, 'fdSelect') and self.fdSelect is not None: + fdIndex = self.fdSelect[index] + else: + fdIndex = 0 + private = self.fdArray[fdIndex].Private else: private = None - return psCharStrings.T2CharString(data, private=private, globalSubrs=self.globalSubrs) - - def toXML(self, xmlWriter, progress): - xmlWriter.comment("The 'index' attribute is only for humans; it is ignored when parsed.") + return self.subrClass(data, private=private, globalSubrs=self.globalSubrs) + + def toXML(self, xmlWriter): + xmlWriter.comment( + "The 'index' attribute is only for humans; " + "it is ignored when parsed.") xmlWriter.newline() for i in range(len(self)): subr = self[i] @@ -395,14 +657,14 @@ class GlobalSubrsIndex(Index): subr.toXML(xmlWriter) xmlWriter.endtag("CharString") xmlWriter.newline() - + def fromXML(self, name, attrs, content): if name != "CharString": return - subr = psCharStrings.T2CharString() + subr = self.subrClass() subr.fromXML(name, attrs, content) self.append(subr) - + def getItemAndSelector(self, index): sel = None if hasattr(self, 'fdSelect'): @@ -415,27 +677,64 @@ class SubrsIndex(GlobalSubrsIndex): class TopDictIndex(Index): - + compilerClass = TopDictIndexCompiler - - def produceItem(self, index, data, file, offset, size): - top = TopDict(self.strings, file, offset, self.GlobalSubrs) + + def __init__(self, file=None, cff2GetGlyphOrder=None, topSize=0, + isCFF2=None): + assert (isCFF2 is None) == (file is None) + self.cff2GetGlyphOrder = cff2GetGlyphOrder + if file is not None and isCFF2: + self._isCFF2 = isCFF2 + self.items = [] + name = self.__class__.__name__ + log.log(DEBUG, "loading %s at %s", name, file.tell()) + self.file = file + count = 1 + self.items = [None] * count + self.offsets = [0, topSize] + self.offsetBase = file.tell() + # pretend we've read the whole lot + file.seek(self.offsetBase + topSize) + log.log(DEBUG, " end of %s at %s", name, file.tell()) + else: + super(TopDictIndex, self).__init__(file, isCFF2=isCFF2) + + def produceItem(self, index, data, file, offset): + top = TopDict( + self.strings, file, offset, self.GlobalSubrs, + self.cff2GetGlyphOrder, isCFF2=self._isCFF2) top.decompile(data) return top - - def toXML(self, xmlWriter, progress): + + def toXML(self, xmlWriter): for i in range(len(self)): xmlWriter.begintag("FontDict", index=i) xmlWriter.newline() - self[i].toXML(xmlWriter, progress) + self[i].toXML(xmlWriter) xmlWriter.endtag("FontDict") xmlWriter.newline() -class FDArrayIndex(TopDictIndex): - +class FDArrayIndex(Index): + compilerClass = FDArrayIndexCompiler + def toXML(self, xmlWriter): + for i in range(len(self)): + xmlWriter.begintag("FontDict", index=i) + xmlWriter.newline() + self[i].toXML(xmlWriter) + xmlWriter.endtag("FontDict") + xmlWriter.newline() + + def produceItem(self, index, data, file, offset): + fontDict = FontDict( + self.strings, file, offset, self.GlobalSubrs, isCFF2=self._isCFF2, + vstore=self.vstore) + fontDict.decompile(data) + return fontDict + def fromXML(self, name, attrs, content): if name != "FontDict": return @@ -448,8 +747,62 @@ class FDArrayIndex(TopDictIndex): self.append(fontDict) -class FDSelect: - def __init__(self, file = None, numGlyphs = None, format=None): +class VarStoreData(object): + + def __init__(self, file=None, otVarStore=None): + self.file = file + self.data = None + self.otVarStore = otVarStore + self.font = TTFont() # dummy font for the decompile function. + + def decompile(self): + if self.file: + class GlobalState(object): + def __init__(self, tableType, cachingStats): + self.tableType = tableType + self.cachingStats = cachingStats + globalState = GlobalState(tableType="VarStore", cachingStats={}) + # read data in from file. Assume position is correct. + length = readCard16(self.file) + self.data = self.file.read(length) + globalState = {} + reader = OTTableReader(self.data, globalState) + self.otVarStore = ot.VarStore() + self.otVarStore.decompile(reader, self.font) + return self + + def compile(self): + writer = OTTableWriter() + self.otVarStore.compile(writer, self.font) + # Note that this omits the initial Card16 length from the CFF2 + # VarStore data block + self.data = writer.getAllData() + + def writeXML(self, xmlWriter, name): + self.otVarStore.toXML(xmlWriter, self.font) + + def xmlRead(self, name, attrs, content, parent): + self.otVarStore = ot.VarStore() + for element in content: + if isinstance(element, tuple): + name, attrs, content = element + self.otVarStore.fromXML(name, attrs, content, self.font) + else: + pass + return None + + def __len__(self): + return len(self.data) + + def getNumRegions(self, vsIndex): + varData = self.otVarStore.VarData[vsIndex] + numRegions = varData.VarRegionCount + return numRegions + + +class FDSelect(object): + + def __init__(self, file=None, numGlyphs=None, format=None): if file: # read data in from file self.format = readCard8(file) @@ -459,6 +812,7 @@ class FDSelect: elif self.format == 3: gidArray = [None] * numGlyphs nRanges = readCard16(file) + fd = None prev = None for i in range(nRanges): first = readCard16(file) @@ -475,88 +829,94 @@ class FDSelect: else: assert False, "unsupported FDSelect format: %s" % format else: - # reading from XML. Make empty gidArray,, and leave format as passed in. + # reading from XML. Make empty gidArray, and leave format as passed in. # format is None will result in the smallest representation being used. self.format = format self.gidArray = [] - def __len__(self): return len(self.gidArray) - + def __getitem__(self, index): return self.gidArray[index] - + def __setitem__(self, index, fdSelectValue): self.gidArray[index] = fdSelectValue def append(self, fdSelectValue): self.gidArray.append(fdSelectValue) - + class CharStrings(object): - - def __init__(self, file, charset, globalSubrs, private, fdSelect, fdArray): + + def __init__(self, file, charset, globalSubrs, private, fdSelect, fdArray, + isCFF2=None): + self.globalSubrs = globalSubrs if file is not None: - self.charStringsIndex = SubrsIndex(file, globalSubrs, private, fdSelect, fdArray) + self.charStringsIndex = SubrsIndex( + file, globalSubrs, private, fdSelect, fdArray, isCFF2=isCFF2) self.charStrings = charStrings = {} for i in range(len(charset)): charStrings[charset[i]] = i + # read from OTF file: charStrings.values() are indices into + # charStringsIndex. self.charStringsAreIndexed = 1 else: self.charStrings = {} + # read from ttx file: charStrings.values() are actual charstrings self.charStringsAreIndexed = 0 - self.globalSubrs = globalSubrs self.private = private if fdSelect is not None: self.fdSelect = fdSelect if fdArray is not None: self.fdArray = fdArray - + def keys(self): return list(self.charStrings.keys()) - + def values(self): if self.charStringsAreIndexed: return self.charStringsIndex else: return list(self.charStrings.values()) - + def has_key(self, name): return name in self.charStrings - + + __contains__ = has_key + def __len__(self): return len(self.charStrings) - + def __getitem__(self, name): charString = self.charStrings[name] if self.charStringsAreIndexed: charString = self.charStringsIndex[charString] return charString - + def __setitem__(self, name, charString): if self.charStringsAreIndexed: index = self.charStrings[name] self.charStringsIndex[index] = charString else: self.charStrings[name] = charString - + def getItemAndSelector(self, name): if self.charStringsAreIndexed: index = self.charStrings[name] return self.charStringsIndex.getItemAndSelector(index) else: - if hasattr(self, 'fdSelect'): - sel = self.fdSelect[index] # index is not defined at this point. Read R. ? + if hasattr(self, 'fdArray'): + if hasattr(self, 'fdSelect'): + sel = self.charStrings[name].fdSelectIndex + else: + sel = 0 else: - raise KeyError("fdSelect array not yet defined.") + sel = None return self.charStrings[name], sel - - def toXML(self, xmlWriter, progress): + + def toXML(self, xmlWriter): names = sorted(self.keys()) - i = 0 - step = 10 - numGlyphs = len(names) for name in names: charStr, fdSelectIndex = self.getItemAndSelector(name) if charStr.needsDecompilation(): @@ -566,17 +926,14 @@ class CharStrings(object): if fdSelectIndex is None: xmlWriter.begintag("CharString", [('name', name)] + raw) else: - xmlWriter.begintag("CharString", - [('name', name), ('fdSelectIndex', fdSelectIndex)] + raw) + xmlWriter.begintag( + "CharString", + [('name', name), ('fdSelectIndex', fdSelectIndex)] + raw) xmlWriter.newline() charStr.toXML(xmlWriter) xmlWriter.endtag("CharString") xmlWriter.newline() - if not i % step and progress is not None: - progress.setLabel("Dumping 'CFF ' table... (%s)" % name) - progress.increment(step / numGlyphs) - i = i + 1 - + def fromXML(self, name, attrs, content): for element in content: if isinstance(element, basestring): @@ -586,13 +943,17 @@ class CharStrings(object): continue fdID = -1 if hasattr(self, "fdArray"): - fdID = safeEval(attrs["fdSelectIndex"]) + try: + fdID = safeEval(attrs["fdSelectIndex"]) + except KeyError: + fdID = 0 private = self.fdArray[fdID].Private else: private = self.private - + glyphName = attrs["name"] - charString = psCharStrings.T2CharString( + charStringClass = psCharStrings.T2CharString + charString = charStringClass( private=private, globalSubrs=self.globalSubrs) charString.fromXML(name, attrs, content) @@ -604,28 +965,44 @@ class CharStrings(object): def readCard8(file): return byteord(file.read(1)) + def readCard16(file): value, = struct.unpack(">H", file.read(2)) return value + +def readCard32(file): + value, = struct.unpack(">L", file.read(4)) + return value + + def writeCard8(file, value): file.write(bytechr(value)) + def writeCard16(file, value): file.write(struct.pack(">H", value)) + +def writeCard32(file, value): + file.write(struct.pack(">L", value)) + + def packCard8(value): return bytechr(value) + def packCard16(value): return struct.pack(">H", value) + def buildOperatorDict(table): d = {} for op, name, arg, default, conv in table: d[op] = (name, arg) return d + def buildOpcodeDict(table): d = {} for op, name, arg, default, conv in table: @@ -636,12 +1013,14 @@ def buildOpcodeDict(table): d[name] = (op, arg) return d + def buildOrder(table): l = [] for op, name, arg, default, conv in table: l.append(name) return l + def buildDefaults(table): d = {} for op, name, arg, default, conv in table: @@ -649,6 +1028,7 @@ def buildDefaults(table): d[name] = default return d + def buildConverters(table): d = {} for op, name, arg, default, conv in table: @@ -657,35 +1037,62 @@ def buildConverters(table): class SimpleConverter(object): + def read(self, parent, value): + if not hasattr(parent, "file"): + return self._read(parent, value) + file = parent.file + pos = file.tell() + try: + return self._read(parent, value) + finally: + file.seek(pos) + + def _read(self, parent, value): return value + def write(self, parent, value): return value - def xmlWrite(self, xmlWriter, name, value, progress): + + def xmlWrite(self, xmlWriter, name, value): xmlWriter.simpletag(name, value=value) xmlWriter.newline() + def xmlRead(self, name, attrs, content, parent): return attrs["value"] + class ASCIIConverter(SimpleConverter): - def read(self, parent, value): + + def _read(self, parent, value): return tostr(value, encoding='ascii') + def write(self, parent, value): return tobytes(value, encoding='ascii') - def xmlWrite(self, xmlWriter, name, value, progress): - xmlWriter.simpletag(name, value=tostr(value, encoding="ascii")) + + def xmlWrite(self, xmlWriter, name, value): + xmlWriter.simpletag(name, value=tounicode(value, encoding="ascii")) xmlWriter.newline() + def xmlRead(self, name, attrs, content, parent): return tobytes(attrs["value"], encoding=("ascii")) + class Latin1Converter(SimpleConverter): - def read(self, parent, value): + + def _read(self, parent, value): return tostr(value, encoding='latin1') + def write(self, parent, value): return tobytes(value, encoding='latin1') - def xmlWrite(self, xmlWriter, name, value, progress): - xmlWriter.simpletag(name, value=tostr(value, encoding="latin1")) + + def xmlWrite(self, xmlWriter, name, value): + value = tounicode(value, encoding="latin1") + if name in ['Notice', 'Copyright']: + value = re.sub(r"[\r\n]\s+", " ", value) + xmlWriter.simpletag(name, value=value) xmlWriter.newline() + def xmlRead(self, name, attrs, content, parent): return tobytes(attrs["value"], encoding=("latin1")) @@ -697,26 +1104,83 @@ def parseNum(s): value = float(s) return value + +def parseBlendList(s): + valueList = [] + for element in s: + if isinstance(element, basestring): + continue + name, attrs, content = element + blendList = attrs["value"].split() + blendList = [eval(val) for val in blendList] + valueList.append(blendList) + if len(valueList) == 1: + valueList = valueList[0] + return valueList + + class NumberConverter(SimpleConverter): + def xmlWrite(self, xmlWriter, name, value): + if isinstance(value, list): + xmlWriter.begintag(name) + xmlWriter.newline() + xmlWriter.indent() + blendValue = " ".join([str(val) for val in value]) + xmlWriter.simpletag(kBlendDictOpName, value=blendValue) + xmlWriter.newline() + xmlWriter.dedent() + xmlWriter.endtag(name) + xmlWriter.newline() + else: + xmlWriter.simpletag(name, value=value) + xmlWriter.newline() + def xmlRead(self, name, attrs, content, parent): - return parseNum(attrs["value"]) + valueString = attrs.get("value", None) + if valueString is None: + value = parseBlendList(content) + else: + value = parseNum(attrs["value"]) + return value + class ArrayConverter(SimpleConverter): - def xmlWrite(self, xmlWriter, name, value, progress): - value = " ".join(map(str, value)) - xmlWriter.simpletag(name, value=value) - xmlWriter.newline() + def xmlWrite(self, xmlWriter, name, value): + if value and isinstance(value[0], list): + xmlWriter.begintag(name) + xmlWriter.newline() + xmlWriter.indent() + for valueList in value: + blendValue = " ".join([str(val) for val in valueList]) + xmlWriter.simpletag(kBlendDictOpName, value=blendValue) + xmlWriter.newline() + xmlWriter.dedent() + xmlWriter.endtag(name) + xmlWriter.newline() + else: + value = " ".join([str(val) for val in value]) + xmlWriter.simpletag(name, value=value) + xmlWriter.newline() + def xmlRead(self, name, attrs, content, parent): - values = attrs["value"].split() - return [parseNum(value) for value in values] + valueString = attrs.get("value", None) + if valueString is None: + valueList = parseBlendList(content) + else: + values = valueString.split() + valueList = [parseNum(value) for value in values] + return valueList + class TableConverter(SimpleConverter): - def xmlWrite(self, xmlWriter, name, value, progress): + + def xmlWrite(self, xmlWriter, name, value): xmlWriter.begintag(name) xmlWriter.newline() - value.toXML(xmlWriter, progress) + value.toXML(xmlWriter) xmlWriter.endtag(name) xmlWriter.newline() + def xmlRead(self, name, attrs, content, parent): ob = self.getClass()() for element in content: @@ -726,66 +1190,101 @@ class TableConverter(SimpleConverter): ob.fromXML(name, attrs, content) return ob + class PrivateDictConverter(TableConverter): + def getClass(self): return PrivateDict - def read(self, parent, value): + + def _read(self, parent, value): size, offset = value file = parent.file - priv = PrivateDict(parent.strings, file, offset) + isCFF2 = parent._isCFF2 + try: + vstore = parent.vstore + except AttributeError: + vstore = None + priv = PrivateDict( + parent.strings, file, offset, isCFF2=isCFF2, vstore=vstore) file.seek(offset) data = file.read(size) assert len(data) == size priv.decompile(data) return priv + def write(self, parent, value): return (0, 0) # dummy value + class SubrsConverter(TableConverter): + def getClass(self): return SubrsIndex - def read(self, parent, value): + + def _read(self, parent, value): file = parent.file + isCFF2 = parent._isCFF2 file.seek(parent.offset + value) # Offset(self) - return SubrsIndex(file) + return SubrsIndex(file, isCFF2=isCFF2) + def write(self, parent, value): return 0 # dummy value + class CharStringsConverter(TableConverter): - def read(self, parent, value): + + def _read(self, parent, value): file = parent.file + isCFF2 = parent._isCFF2 charset = parent.charset globalSubrs = parent.GlobalSubrs - if hasattr(parent, "ROS"): - fdSelect, fdArray = parent.FDSelect, parent.FDArray + if hasattr(parent, "FDArray"): + fdArray = parent.FDArray + if hasattr(parent, "FDSelect"): + fdSelect = parent.FDSelect + else: + fdSelect = None private = None else: fdSelect, fdArray = None, None private = parent.Private file.seek(value) # Offset(0) - return CharStrings(file, charset, globalSubrs, private, fdSelect, fdArray) + charStrings = CharStrings( + file, charset, globalSubrs, private, fdSelect, fdArray, isCFF2=isCFF2) + return charStrings + def write(self, parent, value): return 0 # dummy value + def xmlRead(self, name, attrs, content, parent): - if hasattr(parent, "ROS"): - # if it is a CID-keyed font, then the private Dict is extracted from the parent.FDArray - private, fdSelect, fdArray = None, parent.FDSelect, parent.FDArray + if hasattr(parent, "FDArray"): + # if it is a CID-keyed font, then the private Dict is extracted from the + # parent.FDArray + fdArray = parent.FDArray + if hasattr(parent, "FDSelect"): + fdSelect = parent.FDSelect + else: + fdSelect = None + private = None else: - # if it is a name-keyed font, then the private dict is in the top dict, and there is no fdArray. + # if it is a name-keyed font, then the private dict is in the top dict, + # and + # there is no fdArray. private, fdSelect, fdArray = parent.Private, None, None - charStrings = CharStrings(None, None, parent.GlobalSubrs, private, fdSelect, fdArray) + charStrings = CharStrings( + None, None, parent.GlobalSubrs, private, fdSelect, fdArray) charStrings.fromXML(name, attrs, content) return charStrings -class CharsetConverter(object): - def read(self, parent, value): + +class CharsetConverter(SimpleConverter): + def _read(self, parent, value): isCID = hasattr(parent, "ROS") if value > 2: numGlyphs = parent.numGlyphs file = parent.file file.seek(value) - if DEBUG: - print("loading charset at %s" % value) + log.log(DEBUG, "loading charset at %s", value) format = readCard8(file) if format == 0: charset = parseCharset0(numGlyphs, file, parent.strings, isCID) @@ -794,11 +1293,12 @@ class CharsetConverter(object): else: raise NotImplementedError assert len(charset) == numGlyphs - if DEBUG: - print(" charset end at %s" % file.tell()) - else: # offset == 0 -> no charset data. - if isCID or "CharStrings" not in parent.rawDict: - assert value == 0 # We get here only when processing fontDicts from the FDArray of CFF-CID fonts. Only the real topDict references the chrset. + log.log(DEBUG, " charset end at %s", file.tell()) + else: # offset == 0 -> no charset data. + if isCID or "CharStrings" not in parent.rawDict: + # We get here only when processing fontDicts from the FDArray of + # CFF-CID fonts. Only the real topDict references the chrset. + assert value == 0 charset = None elif value == 0: charset = cffISOAdobeStrings @@ -806,23 +1306,26 @@ class CharsetConverter(object): charset = cffIExpertStrings elif value == 2: charset = cffExpertSubsetStrings + if charset and (len(charset) != parent.numGlyphs): + charset = charset[:parent.numGlyphs] return charset def write(self, parent, value): return 0 # dummy value - def xmlWrite(self, xmlWriter, name, value, progress): + + def xmlWrite(self, xmlWriter, name, value): # XXX only write charset when not in OT/TTX context, where we # dump charset as a separate "GlyphOrder" table. - ##xmlWriter.simpletag("charset") + # # xmlWriter.simpletag("charset") xmlWriter.comment("charset is dumped separately as the 'GlyphOrder' element") xmlWriter.newline() + def xmlRead(self, name, attrs, content, parent): - if 0: - return safeEval(attrs["value"]) + pass class CharsetCompiler(object): - + def __init__(self, strings, charset, parent): assert charset[0] == '.notdef' isCID = hasattr(parent.dictObj, "ROS") @@ -833,23 +1336,46 @@ class CharsetCompiler(object): else: self.data = data0 self.parent = parent - + def setPos(self, pos, endPos): self.parent.rawDict["charset"] = pos - + def getDataLength(self): return len(self.data) - + def toFile(self, file): file.write(self.data) +def getStdCharSet(charset): + # check to see if we can use a predefined charset value. + predefinedCharSetVal = None + predefinedCharSets = [ + (cffISOAdobeStringCount, cffISOAdobeStrings, 0), + (cffExpertStringCount, cffIExpertStrings, 1), + (cffExpertSubsetStringCount, cffExpertSubsetStrings, 2)] + lcs = len(charset) + for cnt, pcs, csv in predefinedCharSets: + if predefinedCharSetVal is not None: + break + if lcs > cnt: + continue + predefinedCharSetVal = csv + for i in range(lcs): + if charset[i] != pcs[i]: + predefinedCharSetVal = None + break + return predefinedCharSetVal + + def getCIDfromName(name, strings): return int(name[3:]) + def getSIDfromName(name, strings): return strings.getSID(name) + def packCharset0(charset, isCID, strings): fmt = 0 data = [packCard8(fmt)] @@ -859,7 +1385,7 @@ def packCharset0(charset, isCID, strings): getNameID = getSIDfromName for name in charset[1:]: - data.append(packCard16(getNameID(name,strings))) + data.append(packCard16(getNameID(name, strings))) return bytesjoin(data) @@ -872,7 +1398,7 @@ def packCharset(charset, isCID, strings): getNameID = getCIDfromName else: getNameID = getSIDfromName - + for name in charset[1:]: SID = getNameID(name, strings) if first is None: @@ -884,11 +1410,12 @@ def packCharset(charset, isCID, strings): ranges.append((first, nLeft)) first = SID end = SID - nLeft = end - first - if nLeft > 255: - fmt = 2 - ranges.append((first, nLeft)) - + if end: + nLeft = end - first + if nLeft > 255: + fmt = 2 + ranges.append((first, nLeft)) + data = [packCard8(fmt)] if fmt == 1: nLeftFunc = packCard8 @@ -898,6 +1425,7 @@ def packCharset(charset, isCID, strings): data.append(packCard16(first) + nLeftFunc(nLeft)) return bytesjoin(data) + def parseCharset0(numGlyphs, file, strings, isCID): charset = [".notdef"] if isCID: @@ -910,6 +1438,7 @@ def parseCharset0(numGlyphs, file, strings, isCID): charset.append(strings[SID]) return charset + def parseCharset(numGlyphs, file, strings, isCID, fmt): charset = ['.notdef'] count = 1 @@ -921,10 +1450,10 @@ def parseCharset(numGlyphs, file, strings, isCID, fmt): first = readCard16(file) nLeft = nLeftFunc(file) if isCID: - for CID in range(first, first+nLeft+1): + for CID in range(first, first + nLeft + 1): charset.append("cid" + str(CID).zfill(5)) else: - for SID in range(first, first+nLeft+1): + for SID in range(first, first + nLeft + 1): charset.append(strings[SID]) count = count + nLeft + 1 return charset @@ -944,17 +1473,17 @@ class EncodingCompiler(object): def setPos(self, pos, endPos): self.parent.rawDict["Encoding"] = pos - + def getDataLength(self): return len(self.data) - + def toFile(self, file): file.write(self.data) class EncodingConverter(SimpleConverter): - def read(self, parent, value): + def _read(self, parent, value): if value == 0: return "StandardEncoding" elif value == 1: @@ -963,8 +1492,7 @@ class EncodingConverter(SimpleConverter): assert value > 1 file = parent.file file.seek(value) - if DEBUG: - print("loading Encoding at %s" % value) + log.log(DEBUG, "loading Encoding at %s", value) fmt = readCard8(file) haveSupplement = fmt & 0x80 if haveSupplement: @@ -985,7 +1513,7 @@ class EncodingConverter(SimpleConverter): return 1 return 0 # dummy value - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): if value in ("StandardEncoding", "ExpertEncoding"): xmlWriter.simpletag(name, name=value) xmlWriter.newline() @@ -1023,6 +1551,7 @@ def parseEncoding0(charset, file, haveSupplement, strings): encoding[code] = charset[glyphID] return encoding + def parseEncoding1(charset, file, haveSupplement, strings): nRanges = readCard8(file) encoding = [".notdef"] * 256 @@ -1036,6 +1565,7 @@ def parseEncoding1(charset, file, haveSupplement, strings): glyphID = glyphID + 1 return encoding + def packEncoding0(charset, encoding, strings): fmt = 0 m = {} @@ -1047,7 +1577,7 @@ def packEncoding0(charset, encoding, strings): for name in charset[1:]: code = m.get(name) codes.append(code) - + while codes and codes[-1] is None: codes.pop() @@ -1058,6 +1588,7 @@ def packEncoding0(charset, encoding, strings): data.append(packCard8(code)) return bytesjoin(data) + def packEncoding1(charset, encoding, strings): fmt = 1 m = {} @@ -1079,7 +1610,7 @@ def packEncoding1(charset, encoding, strings): end = code nLeft = end - first ranges.append((first, nLeft)) - + # remove unencoded glyphs at the end. while ranges and ranges[-1][0] == -1: ranges.pop() @@ -1094,10 +1625,16 @@ def packEncoding1(charset, encoding, strings): class FDArrayConverter(TableConverter): - def read(self, parent, value): + def _read(self, parent, value): + try: + vstore = parent.VarStore + except AttributeError: + vstore = None file = parent.file + isCFF2 = parent._isCFF2 file.seek(value) - fdArray = FDArrayIndex(file) + fdArray = FDArrayIndex(file, isCFF2=isCFF2) + fdArray.vstore = vstore fdArray.strings = parent.strings fdArray.GlobalSubrs = parent.GlobalSubrs return fdArray @@ -1115,20 +1652,20 @@ class FDArrayConverter(TableConverter): return fdArray -class FDSelectConverter(object): +class FDSelectConverter(SimpleConverter): - def read(self, parent, value): + def _read(self, parent, value): file = parent.file file.seek(value) fdSelect = FDSelect(file, parent.numGlyphs) - return fdSelect + return fdSelect def write(self, parent, value): return 0 # dummy value # The FDSelect glyph data is written out to XML in the charstring keys, # so we write out only the format selector - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): xmlWriter.simpletag(name, [('format', value.format)]) xmlWriter.newline() @@ -1138,7 +1675,28 @@ class FDSelectConverter(object): numGlyphs = None fdSelect = FDSelect(file, numGlyphs, fmt) return fdSelect - + + +class VarStoreConverter(SimpleConverter): + + def _read(self, parent, value): + file = parent.file + file.seek(value) + varStore = VarStoreData(file) + varStore.decompile() + return varStore + + def write(self, parent, value): + return 0 # dummy value + + def xmlWrite(self, xmlWriter, name, value): + value.writeXML(xmlWriter, name) + + def xmlRead(self, name, attrs, content, parent): + varStore = VarStoreData() + varStore.xmlRead(name, attrs, content, parent) + return varStore + def packFDSelect0(fdSelectArray): fmt = 0 @@ -1151,8 +1709,6 @@ def packFDSelect0(fdSelectArray): def packFDSelect3(fdSelectArray): fmt = 3 fdRanges = [] - first = None - end = 0 lenArray = len(fdSelectArray) lastFDIndex = -1 for i in range(lenArray): @@ -1161,9 +1717,9 @@ def packFDSelect3(fdSelectArray): fdRanges.append([i, fdIndex]) lastFDIndex = fdIndex sentinelGID = i + 1 - + data = [packCard8(fmt)] - data.append(packCard16( len(fdRanges) )) + data.append(packCard16(len(fdRanges))) for fdRange in fdRanges: data.append(packCard16(fdRange[0])) data.append(packCard8(fdRange[1])) @@ -1172,7 +1728,7 @@ def packFDSelect3(fdSelectArray): class FDSelectCompiler(object): - + def __init__(self, fdSelect, parent): fmt = fdSelect.format fdSelectArray = fdSelect.gidArray @@ -1192,96 +1748,156 @@ class FDSelectCompiler(object): fdSelect.format = 3 self.parent = parent - + def setPos(self, pos, endPos): self.parent.rawDict["FDSelect"] = pos - + def getDataLength(self): return len(self.data) - + + def toFile(self, file): + file.write(self.data) + + +class VarStoreCompiler(object): + + def __init__(self, varStoreData, parent): + self.parent = parent + if not varStoreData.data: + varStoreData.compile() + data = [ + packCard16(len(varStoreData.data)), + varStoreData.data + ] + self.data = bytesjoin(data) + + def setPos(self, pos, endPos): + self.parent.rawDict["VarStore"] = pos + + def getDataLength(self): + return len(self.data) + def toFile(self, file): file.write(self.data) class ROSConverter(SimpleConverter): - def xmlWrite(self, xmlWriter, name, value, progress): + def xmlWrite(self, xmlWriter, name, value): registry, order, supplement = value - xmlWriter.simpletag(name, [('Registry', tostr(registry)), ('Order', tostr(order)), - ('Supplement', supplement)]) + xmlWriter.simpletag( + name, + [ + ('Registry', tostr(registry)), + ('Order', tostr(order)), + ('Supplement', supplement) + ]) xmlWriter.newline() def xmlRead(self, name, attrs, content, parent): return (attrs['Registry'], attrs['Order'], safeEval(attrs['Supplement'])) - - topDictOperators = [ -# opcode name argument type default converter - ((12, 30), 'ROS', ('SID','SID','number'), None, ROSConverter()), - ((12, 20), 'SyntheticBase', 'number', None, None), - (0, 'version', 'SID', None, None), - (1, 'Notice', 'SID', None, Latin1Converter()), - ((12, 0), 'Copyright', 'SID', None, Latin1Converter()), - (2, 'FullName', 'SID', None, None), - ((12, 38), 'FontName', 'SID', None, None), - (3, 'FamilyName', 'SID', None, None), - (4, 'Weight', 'SID', None, None), - ((12, 1), 'isFixedPitch', 'number', 0, None), - ((12, 2), 'ItalicAngle', 'number', 0, None), - ((12, 3), 'UnderlinePosition', 'number', None, None), - ((12, 4), 'UnderlineThickness', 'number', 50, None), - ((12, 5), 'PaintType', 'number', 0, None), - ((12, 6), 'CharstringType', 'number', 2, None), - ((12, 7), 'FontMatrix', 'array', [0.001,0,0,0.001,0,0], None), - (13, 'UniqueID', 'number', None, None), - (5, 'FontBBox', 'array', [0,0,0,0], None), - ((12, 8), 'StrokeWidth', 'number', 0, None), - (14, 'XUID', 'array', None, None), - ((12, 21), 'PostScript', 'SID', None, None), - ((12, 22), 'BaseFontName', 'SID', None, None), - ((12, 23), 'BaseFontBlend', 'delta', None, None), - ((12, 31), 'CIDFontVersion', 'number', 0, None), - ((12, 32), 'CIDFontRevision', 'number', 0, None), - ((12, 33), 'CIDFontType', 'number', 0, None), - ((12, 34), 'CIDCount', 'number', 8720, None), - (15, 'charset', 'number', 0, CharsetConverter()), - ((12, 35), 'UIDBase', 'number', None, None), - (16, 'Encoding', 'number', 0, EncodingConverter()), - (18, 'Private', ('number','number'), None, PrivateDictConverter()), - ((12, 37), 'FDSelect', 'number', None, FDSelectConverter()), - ((12, 36), 'FDArray', 'number', None, FDArrayConverter()), - (17, 'CharStrings', 'number', None, CharStringsConverter()), +# opcode name argument type default converter + (25, 'maxstack', 'number', None, None), + ((12, 30), 'ROS', ('SID', 'SID', 'number'), None, ROSConverter()), + ((12, 20), 'SyntheticBase', 'number', None, None), + (0, 'version', 'SID', None, None), + (1, 'Notice', 'SID', None, Latin1Converter()), + ((12, 0), 'Copyright', 'SID', None, Latin1Converter()), + (2, 'FullName', 'SID', None, None), + ((12, 38), 'FontName', 'SID', None, None), + (3, 'FamilyName', 'SID', None, None), + (4, 'Weight', 'SID', None, None), + ((12, 1), 'isFixedPitch', 'number', 0, None), + ((12, 2), 'ItalicAngle', 'number', 0, None), + ((12, 3), 'UnderlinePosition', 'number', -100, None), + ((12, 4), 'UnderlineThickness', 'number', 50, None), + ((12, 5), 'PaintType', 'number', 0, None), + ((12, 6), 'CharstringType', 'number', 2, None), + ((12, 7), 'FontMatrix', 'array', [0.001, 0, 0, 0.001, 0, 0], None), + (13, 'UniqueID', 'number', None, None), + (5, 'FontBBox', 'array', [0, 0, 0, 0], None), + ((12, 8), 'StrokeWidth', 'number', 0, None), + (14, 'XUID', 'array', None, None), + ((12, 21), 'PostScript', 'SID', None, None), + ((12, 22), 'BaseFontName', 'SID', None, None), + ((12, 23), 'BaseFontBlend', 'delta', None, None), + ((12, 31), 'CIDFontVersion', 'number', 0, None), + ((12, 32), 'CIDFontRevision', 'number', 0, None), + ((12, 33), 'CIDFontType', 'number', 0, None), + ((12, 34), 'CIDCount', 'number', 8720, None), + (15, 'charset', 'number', None, CharsetConverter()), + ((12, 35), 'UIDBase', 'number', None, None), + (16, 'Encoding', 'number', 0, EncodingConverter()), + (18, 'Private', ('number', 'number'), None, PrivateDictConverter()), + ((12, 37), 'FDSelect', 'number', None, FDSelectConverter()), + ((12, 36), 'FDArray', 'number', None, FDArrayConverter()), + (17, 'CharStrings', 'number', None, CharStringsConverter()), + (24, 'VarStore', 'number', None, VarStoreConverter()), +] + +topDictOperators2 = [ +# opcode name argument type default converter + (25, 'maxstack', 'number', None, None), + ((12, 7), 'FontMatrix', 'array', [0.001, 0, 0, 0.001, 0, 0], None), + ((12, 37), 'FDSelect', 'number', None, FDSelectConverter()), + ((12, 36), 'FDArray', 'number', None, FDArrayConverter()), + (17, 'CharStrings', 'number', None, CharStringsConverter()), + (24, 'VarStore', 'number', None, VarStoreConverter()), ] # Note! FDSelect and FDArray must both preceed CharStrings in the output XML build order, # in order for the font to compile back from xml. +kBlendDictOpName = "blend" +blendOp = 23 privateDictOperators = [ -# opcode name argument type default converter - (6, 'BlueValues', 'delta', None, None), - (7, 'OtherBlues', 'delta', None, None), - (8, 'FamilyBlues', 'delta', None, None), - (9, 'FamilyOtherBlues', 'delta', None, None), - ((12, 9), 'BlueScale', 'number', 0.039625, None), - ((12, 10), 'BlueShift', 'number', 7, None), - ((12, 11), 'BlueFuzz', 'number', 1, None), - (10, 'StdHW', 'number', None, None), - (11, 'StdVW', 'number', None, None), - ((12, 12), 'StemSnapH', 'delta', None, None), - ((12, 13), 'StemSnapV', 'delta', None, None), - ((12, 14), 'ForceBold', 'number', 0, None), - ((12, 15), 'ForceBoldThreshold', 'number', None, None), # deprecated - ((12, 16), 'lenIV', 'number', None, None), # deprecated - ((12, 17), 'LanguageGroup', 'number', 0, None), - ((12, 18), 'ExpansionFactor', 'number', 0.06, None), - ((12, 19), 'initialRandomSeed', 'number', 0, None), - (20, 'defaultWidthX', 'number', 0, None), - (21, 'nominalWidthX', 'number', 0, None), - (19, 'Subrs', 'number', None, SubrsConverter()), +# opcode name argument type default converter + (22, "vsindex", 'number', None, None), + (blendOp, kBlendDictOpName, 'blendList', None, None), # This is for reading to/from XML: it not written to CFF. + (6, 'BlueValues', 'delta', None, None), + (7, 'OtherBlues', 'delta', None, None), + (8, 'FamilyBlues', 'delta', None, None), + (9, 'FamilyOtherBlues', 'delta', None, None), + ((12, 9), 'BlueScale', 'number', 0.039625, None), + ((12, 10), 'BlueShift', 'number', 7, None), + ((12, 11), 'BlueFuzz', 'number', 1, None), + (10, 'StdHW', 'number', None, None), + (11, 'StdVW', 'number', None, None), + ((12, 12), 'StemSnapH', 'delta', None, None), + ((12, 13), 'StemSnapV', 'delta', None, None), + ((12, 14), 'ForceBold', 'number', 0, None), + ((12, 15), 'ForceBoldThreshold', 'number', None, None), # deprecated + ((12, 16), 'lenIV', 'number', None, None), # deprecated + ((12, 17), 'LanguageGroup', 'number', 0, None), + ((12, 18), 'ExpansionFactor', 'number', 0.06, None), + ((12, 19), 'initialRandomSeed', 'number', 0, None), + (20, 'defaultWidthX', 'number', 0, None), + (21, 'nominalWidthX', 'number', 0, None), + (19, 'Subrs', 'number', None, SubrsConverter()), +] + +privateDictOperators2 = [ +# opcode name argument type default converter + (22, "vsindex", 'number', None, None), + (blendOp, kBlendDictOpName, 'blendList', None, None), # This is for reading to/from XML: it not written to CFF. + (6, 'BlueValues', 'delta', None, None), + (7, 'OtherBlues', 'delta', None, None), + (8, 'FamilyBlues', 'delta', None, None), + (9, 'FamilyOtherBlues', 'delta', None, None), + ((12, 9), 'BlueScale', 'number', 0.039625, None), + ((12, 10), 'BlueShift', 'number', 7, None), + ((12, 11), 'BlueFuzz', 'number', 1, None), + (10, 'StdHW', 'number', None, None), + (11, 'StdVW', 'number', None, None), + ((12, 12), 'StemSnapH', 'delta', None, None), + ((12, 13), 'StemSnapV', 'delta', None, None), + (19, 'Subrs', 'number', None, SubrsConverter()), ] + def addConverters(table): for i in range(len(table)): op, name, arg, default, conv = table[i] @@ -1293,10 +1909,13 @@ def addConverters(table): conv = NumberConverter() elif arg == "SID": conv = ASCIIConverter() + elif arg == 'blendList': + conv = None else: assert False table[i] = op, name, arg, default, conv + addConverters(privateDictOperators) addConverters(topDictOperators) @@ -1310,9 +1929,15 @@ class PrivateDictDecompiler(psCharStrings.DictDecompiler): class DictCompiler(object): - - def __init__(self, dictObj, strings, parent): - assert isinstance(strings, IndexedStrings) + maxBlendStack = 0 + + def __init__(self, dictObj, strings, parent, isCFF2=None): + if strings: + assert isinstance(strings, IndexedStrings) + if isCFF2 is None and hasattr(parent, "isCFF2"): + isCFF2 = parent.isCFF2 + assert isCFF2 is not None + self.isCFF2 = isCFF2 self.dictObj = dictObj self.strings = strings self.parent = parent @@ -1327,17 +1952,15 @@ class DictCompiler(object): continue rawDict[name] = value self.rawDict = rawDict - + def setPos(self, pos, endPos): pass - + def getDataLength(self): return len(self.compile("getDataLength")) - + def compile(self, reason): - if DEBUG: - print("-- compiling %s for %s" % (self.__class__.__name__, reason)) - print("in baseDict: ", self) + log.log(DEBUG, "-- compiling %s for %s", self.__class__.__name__, reason) rawDict = self.rawDict data = [] for name in self.dictObj.order: @@ -1357,32 +1980,115 @@ class DictCompiler(object): arghandler = getattr(self, "arg_" + argType) data.append(arghandler(value)) data.append(op) - return bytesjoin(data) - + data = bytesjoin(data) + return data + def toFile(self, file): - file.write(self.compile("toFile")) - + data = self.compile("toFile") + file.write(data) + def arg_number(self, num): - return encodeNumber(num) + if isinstance(num, list): + data = [encodeNumber(val) for val in num] + data.append(encodeNumber(1)) + data.append(bytechr(blendOp)) + datum = bytesjoin(data) + else: + datum = encodeNumber(num) + return datum + def arg_SID(self, s): return psCharStrings.encodeIntCFF(self.strings.getSID(s)) + def arg_array(self, value): data = [] for num in value: - data.append(encodeNumber(num)) + data.append(self.arg_number(num)) return bytesjoin(data) + def arg_delta(self, value): - out = [] - last = 0 - for v in value: - out.append(v - last) - last = v - data = [] - for num in out: - data.append(encodeNumber(num)) + if not value: + return b"" + val0 = value[0] + if isinstance(val0, list): + data = self.arg_delta_blend(value) + else: + out = [] + last = 0 + for v in value: + out.append(v - last) + last = v + data = [] + for num in out: + data.append(encodeNumber(num)) return bytesjoin(data) + def arg_delta_blend(self, value): + """ A delta list with blend lists has to be *all* blend lists. + The value is a list is arranged as follows. + [ + [V0, d0..dn] + [V1, d0..dn] + ... + [Vm, d0..dn] + ] + V is the absolute coordinate value from the default font, and d0-dn are + the delta values from the n regions. Each V is an absolute coordinate + from the default font. + We want to return a list: + [ + [v0, v1..vm] + [d0..dn] + ... + [d0..dn] + numBlends + blendOp + ] + where each v is relative to the previous default font value. + """ + numMasters = len(value[0]) + numBlends = len(value) + numStack = (numBlends * numMasters) + 1 + if numStack > self.maxBlendStack: + # Figure out the max number of value we can blend + # and divide this list up into chunks of that size. + + numBlendValues = int((self.maxBlendStack - 1) / numMasters) + out = [] + while True: + numVal = min(len(value), numBlendValues) + if numVal == 0: + break + valList = value[0:numVal] + out1 = self.arg_delta_blend(valList) + out.extend(out1) + value = value[numVal:] + else: + firstList = [0] * numBlends + deltaList = [None] * numBlends + i = 0 + prevVal = 0 + while i < numBlends: + # For PrivateDict BlueValues, the default font + # values are absolute, not relative. + # Must convert these back to relative coordinates + # befor writing to CFF2. + defaultValue = value[i][0] + firstList[i] = defaultValue - prevVal + prevVal = defaultValue + deltaList[i] = value[i][1:] + i += 1 + + relValueList = firstList + for blendList in deltaList: + relValueList.extend(blendList) + out = [encodeNumber(val) for val in relValueList] + out.append(encodeNumber(numBlends)) + out.append(bytechr(blendOp)) + return out + + def encodeNumber(num): if isinstance(num, float): return psCharStrings.encodeFloat(num) @@ -1391,24 +2097,39 @@ def encodeNumber(num): class TopDictCompiler(DictCompiler): - + opcodes = buildOpcodeDict(topDictOperators) - + def getChildren(self, strings): + isCFF2 = self.isCFF2 children = [] - if hasattr(self.dictObj, "charset") and self.dictObj.charset: - children.append(CharsetCompiler(strings, self.dictObj.charset, self)) - if hasattr(self.dictObj, "Encoding"): - encoding = self.dictObj.Encoding - if not isinstance(encoding, basestring): - children.append(EncodingCompiler(strings, encoding, self)) + if self.dictObj.cff2GetGlyphOrder is None: + if hasattr(self.dictObj, "charset") and self.dictObj.charset: + if hasattr(self.dictObj, "ROS"): # aka isCID + charsetCode = None + else: + charsetCode = getStdCharSet(self.dictObj.charset) + if charsetCode is None: + children.append(CharsetCompiler(strings, self.dictObj.charset, self)) + else: + self.rawDict["charset"] = charsetCode + if hasattr(self.dictObj, "Encoding") and self.dictObj.Encoding: + encoding = self.dictObj.Encoding + if not isinstance(encoding, basestring): + children.append(EncodingCompiler(strings, encoding, self)) + else: + if hasattr(self.dictObj, "VarStore"): + varStoreData = self.dictObj.VarStore + varStoreComp = VarStoreCompiler(varStoreData, self) + children.append(varStoreComp) if hasattr(self.dictObj, "FDSelect"): - # I have not yet supported merging a ttx CFF-CID font, as there are interesting - # issues about merging the FDArrays. Here I assume that - # either the font was read from XML, and teh FDSelect indices are all + # I have not yet supported merging a ttx CFF-CID font, as there are + # interesting issues about merging the FDArrays. Here I assume that + # either the font was read from XML, and the FDSelect indices are all # in the charstring data, or the FDSelect array is already fully defined. fdSelect = self.dictObj.FDSelect - if len(fdSelect) == 0: # probably read in from XML; assume fdIndex in CharString data + # probably read in from XML; assume fdIndex in CharString data + if len(fdSelect) == 0: charStrings = self.dictObj.CharStrings for name in self.dictObj.charset: fdSelect.append(charStrings[name].fdSelectIndex) @@ -1419,12 +2140,13 @@ class TopDictCompiler(DictCompiler): charStrings = self.dictObj.CharStrings for name in self.dictObj.charset: items.append(charStrings[name]) - charStringsComp = CharStringsCompiler(items, strings, self) + charStringsComp = CharStringsCompiler( + items, strings, self, isCFF2=isCFF2) children.append(charStringsComp) if hasattr(self.dictObj, "FDArray"): - # I have not yet supported merging a ttx CFF-CID font, as there are interesting - # issues about merging the FDArrays. Here I assume that the FDArray info is correct - # and complete. + # I have not yet supported merging a ttx CFF-CID font, as there are + # interesting issues about merging the FDArrays. Here I assume that the + # FDArray info is correct and complete. fdArrayIndexComp = self.dictObj.FDArray.getCompiler(strings, self) children.append(fdArrayIndexComp) children.extend(fdArrayIndexComp.getChildren(strings)) @@ -1436,9 +2158,40 @@ class TopDictCompiler(DictCompiler): class FontDictCompiler(DictCompiler): - opcodes = buildOpcodeDict(topDictOperators) - + + def __init__(self, dictObj, strings, parent, isCFF2=None): + super(FontDictCompiler, self).__init__(dictObj, strings, parent, isCFF2=isCFF2) + # + # We now take some effort to detect if there were any key/value pairs + # supplied that were ignored in the FontDict context, and issue a warning + # for those cases. + # + ignoredNames = [] + dictObj = self.dictObj + for name in sorted(set(dictObj.converters) - set(dictObj.order)): + if name in dictObj.rawDict: + # The font was directly read from binary. In this + # case, we want to report *all* "useless" key/value + # pairs that are in the font, not just the ones that + # are different from the default. + ignoredNames.append(name) + else: + # The font was probably read from a TTX file. We only + # warn about keys whos value is not the default. The + # ones that have the default value will not be written + # to binary anyway. + default = dictObj.defaults.get(name) + if default is not None: + conv = dictObj.converters[name] + default = conv.read(dictObj, default) + if getattr(dictObj, name, None) != default: + ignoredNames.append(name) + if ignoredNames: + log.warning( + "Some CFF FDArray/FontDict keys were ignored upon compile: " + + " ".join(sorted(ignoredNames))) + def getChildren(self, strings): children = [] if hasattr(self.dictObj, "Private"): @@ -1449,14 +2202,15 @@ class FontDictCompiler(DictCompiler): class PrivateDictCompiler(DictCompiler): - + + maxBlendStack = maxStackLimit opcodes = buildOpcodeDict(privateDictOperators) - + def setPos(self, pos, endPos): size = endPos - pos self.parent.rawDict["Private"] = size, pos self.pos = pos - + def getChildren(self, strings): children = [] if hasattr(self.dictObj, "Subrs"): @@ -1465,32 +2219,35 @@ class PrivateDictCompiler(DictCompiler): class BaseDict(object): - - def __init__(self, strings=None, file=None, offset=None): + + def __init__(self, strings=None, file=None, offset=None, isCFF2=None): + assert (isCFF2 is None) == (file is None) self.rawDict = {} - if DEBUG: - print("loading %s at %s" % (self.__class__.__name__, offset)) - self.file = file - self.offset = offset - self.strings = strings self.skipNames = [] - + self.strings = strings + if file is None: + return + self._isCFF2 = isCFF2 + self.file = file + if offset is not None: + log.log(DEBUG, "loading %s at %s", self.__class__.__name__, offset) + self.offset = offset + def decompile(self, data): - if DEBUG: - print(" length %s is %s" % (self.__class__.__name__, len(data))) - dec = self.decompilerClass(self.strings) + log.log(DEBUG, " length %s is %d", self.__class__.__name__, len(data)) + dec = self.decompilerClass(self.strings, self) dec.decompile(data) self.rawDict = dec.getDict() self.postDecompile() - + def postDecompile(self): pass - - def getCompiler(self, strings, parent): - return self.compilerClass(self, strings, parent) - + + def getCompiler(self, strings, parent, isCFF2=None): + return self.compilerClass(self, strings, parent, isCFF2=isCFF2) + def __getattr__(self, name): - value = self.rawDict.get(name) + value = self.rawDict.get(name, None) if value is None: value = self.defaults.get(name) if value is None: @@ -1499,17 +2256,31 @@ class BaseDict(object): value = conv.read(self, value) setattr(self, name, value) return value - - def toXML(self, xmlWriter, progress): + + def toXML(self, xmlWriter): for name in self.order: if name in self.skipNames: continue value = getattr(self, name, None) - if value is None: + # XXX For "charset" we never skip calling xmlWrite even if the + # value is None, so we always write the following XML comment: + # + # + # + # Charset is None when 'CFF ' table is imported from XML into an + # empty TTFont(). By writing this comment all the time, we obtain + # the same XML output whether roundtripping XML-to-XML or + # dumping binary-to-XML + if value is None and name != "charset": continue conv = self.converters[name] - conv.xmlWrite(xmlWriter, name, value, progress) - + conv.xmlWrite(xmlWriter, name, value) + ignoredNames = set(self.rawDict) - set(self.order) + if ignoredNames: + xmlWriter.comment( + "some keys were ignored: %s" % " ".join(sorted(ignoredNames))) + xmlWriter.newline() + def fromXML(self, name, attrs, content): conv = self.converters[name] value = conv.xmlRead(name, attrs, content, self) @@ -1517,75 +2288,126 @@ class BaseDict(object): class TopDict(BaseDict): - + defaults = buildDefaults(topDictOperators) converters = buildConverters(topDictOperators) + compilerClass = TopDictCompiler order = buildOrder(topDictOperators) decompilerClass = TopDictDecompiler - compilerClass = TopDictCompiler - - def __init__(self, strings=None, file=None, offset=None, GlobalSubrs=None): - BaseDict.__init__(self, strings, file, offset) + + def __init__(self, strings=None, file=None, offset=None, + GlobalSubrs=None, cff2GetGlyphOrder=None, isCFF2=None): + super(TopDict, self).__init__(strings, file, offset, isCFF2=isCFF2) + self.cff2GetGlyphOrder = cff2GetGlyphOrder self.GlobalSubrs = GlobalSubrs - + if isCFF2: + self.defaults = buildDefaults(topDictOperators2) + self.charset = cff2GetGlyphOrder() + self.order = buildOrder(topDictOperators2) + else: + self.defaults = buildDefaults(topDictOperators) + self.order = buildOrder(topDictOperators) + def getGlyphOrder(self): return self.charset - + def postDecompile(self): offset = self.rawDict.get("CharStrings") if offset is None: return # get the number of glyphs beforehand. self.file.seek(offset) - self.numGlyphs = readCard16(self.file) - - def toXML(self, xmlWriter, progress): + if self._isCFF2: + self.numGlyphs = readCard32(self.file) + else: + self.numGlyphs = readCard16(self.file) + + def toXML(self, xmlWriter): if hasattr(self, "CharStrings"): - self.decompileAllCharStrings(progress) + self.decompileAllCharStrings() if hasattr(self, "ROS"): self.skipNames = ['Encoding'] if not hasattr(self, "ROS") or not hasattr(self, "CharStrings"): # these values have default values, but I only want them to show up # in CID fonts. - self.skipNames = ['CIDFontVersion', 'CIDFontRevision', 'CIDFontType', - 'CIDCount'] - BaseDict.toXML(self, xmlWriter, progress) - - def decompileAllCharStrings(self, progress): - # XXX only when doing ttdump -i? - i = 0 + self.skipNames = [ + 'CIDFontVersion', 'CIDFontRevision', 'CIDFontType', 'CIDCount'] + BaseDict.toXML(self, xmlWriter) + + def decompileAllCharStrings(self): + # Make sure that all the Private Dicts have been instantiated. for charString in self.CharStrings.values(): try: charString.decompile() except: - print("Error in charstring ", i) - import sys - typ, value = sys.exc_info()[0:2] - raise typ(value) - if not i % 30 and progress: - progress.increment(0) # update - i = i + 1 + log.error("Error in charstring %s", i) + raise + + def recalcFontBBox(self): + fontBBox = None + for charString in self.CharStrings.values(): + bounds = charString.calcBounds(self.CharStrings) + if bounds is not None: + if fontBBox is not None: + fontBBox = unionRect(fontBBox, bounds) + else: + fontBBox = bounds + + if fontBBox is None: + self.FontBBox = self.defaults['FontBBox'][:] + else: + self.FontBBox = list(intRect(fontBBox)) class FontDict(BaseDict): - - defaults = buildDefaults(topDictOperators) + # + # Since fonttools used to pass a lot of fields that are not relevant in the FDArray + # FontDict, there are 'ttx' files in the wild that contain all these. These got in + # the ttx files because fonttools writes explicit values for all the TopDict default + # values. These are not actually illegal in the context of an FDArray FontDict - you + # can legally, per spec, put any arbitrary key/value pair in a FontDict - but are + # useless since current major company CFF interpreters ignore anything but the set + # listed in this file. So, we just silently skip them. An exception is Weight: this + # is not used by any interpreter, but some foundries have asked that this be + # supported in FDArray FontDicts just to preserve information about the design when + # the font is being inspected. + # + # On top of that, there are fonts out there that contain such useless FontDict values. + # + # By subclassing TopDict, we *allow* all key/values from TopDict, both when reading + # from binary or when reading from XML, but by overriding `order` with a limited + # list of names, we ensure that only the useful names ever get exported to XML and + # ever get compiled into the binary font. + # + # We override compilerClass so we can warn about "useless" key/value pairs, either + # from the original binary font or from TTX input. + # + # See: + # - https://github.com/fonttools/fonttools/issues/740 + # - https://github.com/fonttools/fonttools/issues/601 + # - https://github.com/adobe-type-tools/afdko/issues/137 + # + defaults = {} converters = buildConverters(topDictOperators) - order = buildOrder(topDictOperators) - decompilerClass = None compilerClass = FontDictCompiler - - def __init__(self, strings=None, file=None, offset=None, GlobalSubrs=None): - BaseDict.__init__(self, strings, file, offset) - self.GlobalSubrs = GlobalSubrs - - def getGlyphOrder(self): - return self.charset - - def toXML(self, xmlWriter, progress): - self.skipNames = ['Encoding'] - BaseDict.toXML(self, xmlWriter, progress) - + orderCFF = ['FontName', 'FontMatrix', 'Weight', 'Private'] + orderCFF2 = ['Private'] + decompilerClass = TopDictDecompiler + + def __init__(self, strings=None, file=None, offset=None, + GlobalSubrs=None, isCFF2=None, vstore=None): + super(FontDict, self).__init__(strings, file, offset, isCFF2=isCFF2) + self.vstore = vstore + self.setCFF2(isCFF2) + + def setCFF2(self, isCFF2): + # isCFF2 may be None. + if isCFF2: + self.order = self.orderCFF2 + self._isCFF2 = True + else: + self.order = self.orderCFF + self._isCFF2 = False class PrivateDict(BaseDict): @@ -1595,33 +2417,58 @@ class PrivateDict(BaseDict): decompilerClass = PrivateDictDecompiler compilerClass = PrivateDictCompiler + def __init__(self, strings=None, file=None, offset=None, isCFF2=None, + vstore=None): + super(PrivateDict, self).__init__(strings, file, offset, isCFF2=isCFF2) + self.vstore = vstore + if isCFF2: + self.defaults = buildDefaults(privateDictOperators2) + self.order = buildOrder(privateDictOperators2) + else: + self.defaults = buildDefaults(privateDictOperators) + self.order = buildOrder(privateDictOperators) + + def getNumRegions(self, vi=None): # called from misc/psCharStrings.py + # if getNumRegions is being called, we can assume that VarStore exists. + if vi is None: + if hasattr(self, 'vsindex'): + vi = self.vsindex + else: + vi = 0 + numRegions = self.vstore.getNumRegions(vi) + return numRegions + class IndexedStrings(object): - + """SID -> string mapping.""" - + def __init__(self, file=None): if file is None: strings = [] else: - strings = [tostr(s, encoding="latin1") for s in Index(file)] + strings = [ + tostr(s, encoding="latin1") + for s in Index(file, isCFF2=False) + ] self.strings = strings - + def getCompiler(self): - return IndexedStringsCompiler(self, None, None) - + return IndexedStringsCompiler(self, None, self, isCFF2=False) + def __len__(self): return len(self.strings) - + def __getitem__(self, SID): if SID < cffStandardStringCount: return cffStandardStrings[SID] else: return self.strings[SID - cffStandardStringCount] - + def getSID(self, s): if not hasattr(self, "stringMapping"): self.buildStringMapping() + s = tostr(s, encoding="latin1") if s in cffStandardStringMapping: SID = cffStandardStringMapping[s] elif s in self.stringMapping: @@ -1631,10 +2478,10 @@ class IndexedStrings(object): self.strings.append(s) self.stringMapping[s] = SID return SID - + def getStrings(self): return self.strings - + def buildStringMapping(self): self.stringMapping = {} for index in range(len(self.strings)): @@ -1644,68 +2491,68 @@ class IndexedStrings(object): # The 391 Standard Strings as used in the CFF format. # from Adobe Technical None #5176, version 1.0, 18 March 1998 -cffStandardStrings = ['.notdef', 'space', 'exclam', 'quotedbl', 'numbersign', - 'dollar', 'percent', 'ampersand', 'quoteright', 'parenleft', 'parenright', - 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash', 'zero', 'one', - 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon', - 'semicolon', 'less', 'equal', 'greater', 'question', 'at', 'A', 'B', 'C', - 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', - 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', - 'bracketright', 'asciicircum', 'underscore', 'quoteleft', 'a', 'b', 'c', - 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', - 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', - 'asciitilde', 'exclamdown', 'cent', 'sterling', 'fraction', 'yen', 'florin', - 'section', 'currency', 'quotesingle', 'quotedblleft', 'guillemotleft', - 'guilsinglleft', 'guilsinglright', 'fi', 'fl', 'endash', 'dagger', - 'daggerdbl', 'periodcentered', 'paragraph', 'bullet', 'quotesinglbase', - 'quotedblbase', 'quotedblright', 'guillemotright', 'ellipsis', 'perthousand', - 'questiondown', 'grave', 'acute', 'circumflex', 'tilde', 'macron', 'breve', - 'dotaccent', 'dieresis', 'ring', 'cedilla', 'hungarumlaut', 'ogonek', 'caron', - 'emdash', 'AE', 'ordfeminine', 'Lslash', 'Oslash', 'OE', 'ordmasculine', 'ae', - 'dotlessi', 'lslash', 'oslash', 'oe', 'germandbls', 'onesuperior', - 'logicalnot', 'mu', 'trademark', 'Eth', 'onehalf', 'plusminus', 'Thorn', - 'onequarter', 'divide', 'brokenbar', 'degree', 'thorn', 'threequarters', - 'twosuperior', 'registered', 'minus', 'eth', 'multiply', 'threesuperior', - 'copyright', 'Aacute', 'Acircumflex', 'Adieresis', 'Agrave', 'Aring', - 'Atilde', 'Ccedilla', 'Eacute', 'Ecircumflex', 'Edieresis', 'Egrave', - 'Iacute', 'Icircumflex', 'Idieresis', 'Igrave', 'Ntilde', 'Oacute', - 'Ocircumflex', 'Odieresis', 'Ograve', 'Otilde', 'Scaron', 'Uacute', - 'Ucircumflex', 'Udieresis', 'Ugrave', 'Yacute', 'Ydieresis', 'Zcaron', - 'aacute', 'acircumflex', 'adieresis', 'agrave', 'aring', 'atilde', 'ccedilla', - 'eacute', 'ecircumflex', 'edieresis', 'egrave', 'iacute', 'icircumflex', - 'idieresis', 'igrave', 'ntilde', 'oacute', 'ocircumflex', 'odieresis', - 'ograve', 'otilde', 'scaron', 'uacute', 'ucircumflex', 'udieresis', 'ugrave', - 'yacute', 'ydieresis', 'zcaron', 'exclamsmall', 'Hungarumlautsmall', - 'dollaroldstyle', 'dollarsuperior', 'ampersandsmall', 'Acutesmall', - 'parenleftsuperior', 'parenrightsuperior', 'twodotenleader', 'onedotenleader', - 'zerooldstyle', 'oneoldstyle', 'twooldstyle', 'threeoldstyle', 'fouroldstyle', - 'fiveoldstyle', 'sixoldstyle', 'sevenoldstyle', 'eightoldstyle', - 'nineoldstyle', 'commasuperior', 'threequartersemdash', 'periodsuperior', - 'questionsmall', 'asuperior', 'bsuperior', 'centsuperior', 'dsuperior', - 'esuperior', 'isuperior', 'lsuperior', 'msuperior', 'nsuperior', 'osuperior', - 'rsuperior', 'ssuperior', 'tsuperior', 'ff', 'ffi', 'ffl', 'parenleftinferior', - 'parenrightinferior', 'Circumflexsmall', 'hyphensuperior', 'Gravesmall', - 'Asmall', 'Bsmall', 'Csmall', 'Dsmall', 'Esmall', 'Fsmall', 'Gsmall', 'Hsmall', - 'Ismall', 'Jsmall', 'Ksmall', 'Lsmall', 'Msmall', 'Nsmall', 'Osmall', 'Psmall', - 'Qsmall', 'Rsmall', 'Ssmall', 'Tsmall', 'Usmall', 'Vsmall', 'Wsmall', 'Xsmall', - 'Ysmall', 'Zsmall', 'colonmonetary', 'onefitted', 'rupiah', 'Tildesmall', - 'exclamdownsmall', 'centoldstyle', 'Lslashsmall', 'Scaronsmall', 'Zcaronsmall', - 'Dieresissmall', 'Brevesmall', 'Caronsmall', 'Dotaccentsmall', 'Macronsmall', - 'figuredash', 'hypheninferior', 'Ogoneksmall', 'Ringsmall', 'Cedillasmall', - 'questiondownsmall', 'oneeighth', 'threeeighths', 'fiveeighths', 'seveneighths', - 'onethird', 'twothirds', 'zerosuperior', 'foursuperior', 'fivesuperior', - 'sixsuperior', 'sevensuperior', 'eightsuperior', 'ninesuperior', 'zeroinferior', - 'oneinferior', 'twoinferior', 'threeinferior', 'fourinferior', 'fiveinferior', - 'sixinferior', 'seveninferior', 'eightinferior', 'nineinferior', 'centinferior', - 'dollarinferior', 'periodinferior', 'commainferior', 'Agravesmall', - 'Aacutesmall', 'Acircumflexsmall', 'Atildesmall', 'Adieresissmall', 'Aringsmall', - 'AEsmall', 'Ccedillasmall', 'Egravesmall', 'Eacutesmall', 'Ecircumflexsmall', - 'Edieresissmall', 'Igravesmall', 'Iacutesmall', 'Icircumflexsmall', - 'Idieresissmall', 'Ethsmall', 'Ntildesmall', 'Ogravesmall', 'Oacutesmall', - 'Ocircumflexsmall', 'Otildesmall', 'Odieresissmall', 'OEsmall', 'Oslashsmall', - 'Ugravesmall', 'Uacutesmall', 'Ucircumflexsmall', 'Udieresissmall', - 'Yacutesmall', 'Thornsmall', 'Ydieresissmall', '001.000', '001.001', '001.002', - '001.003', 'Black', 'Bold', 'Book', 'Light', 'Medium', 'Regular', 'Roman', +cffStandardStrings = ['.notdef', 'space', 'exclam', 'quotedbl', 'numbersign', + 'dollar', 'percent', 'ampersand', 'quoteright', 'parenleft', 'parenright', + 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash', 'zero', 'one', + 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon', + 'semicolon', 'less', 'equal', 'greater', 'question', 'at', 'A', 'B', 'C', + 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', + 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', + 'bracketright', 'asciicircum', 'underscore', 'quoteleft', 'a', 'b', 'c', + 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', + 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', + 'asciitilde', 'exclamdown', 'cent', 'sterling', 'fraction', 'yen', 'florin', + 'section', 'currency', 'quotesingle', 'quotedblleft', 'guillemotleft', + 'guilsinglleft', 'guilsinglright', 'fi', 'fl', 'endash', 'dagger', + 'daggerdbl', 'periodcentered', 'paragraph', 'bullet', 'quotesinglbase', + 'quotedblbase', 'quotedblright', 'guillemotright', 'ellipsis', 'perthousand', + 'questiondown', 'grave', 'acute', 'circumflex', 'tilde', 'macron', 'breve', + 'dotaccent', 'dieresis', 'ring', 'cedilla', 'hungarumlaut', 'ogonek', 'caron', + 'emdash', 'AE', 'ordfeminine', 'Lslash', 'Oslash', 'OE', 'ordmasculine', 'ae', + 'dotlessi', 'lslash', 'oslash', 'oe', 'germandbls', 'onesuperior', + 'logicalnot', 'mu', 'trademark', 'Eth', 'onehalf', 'plusminus', 'Thorn', + 'onequarter', 'divide', 'brokenbar', 'degree', 'thorn', 'threequarters', + 'twosuperior', 'registered', 'minus', 'eth', 'multiply', 'threesuperior', + 'copyright', 'Aacute', 'Acircumflex', 'Adieresis', 'Agrave', 'Aring', + 'Atilde', 'Ccedilla', 'Eacute', 'Ecircumflex', 'Edieresis', 'Egrave', + 'Iacute', 'Icircumflex', 'Idieresis', 'Igrave', 'Ntilde', 'Oacute', + 'Ocircumflex', 'Odieresis', 'Ograve', 'Otilde', 'Scaron', 'Uacute', + 'Ucircumflex', 'Udieresis', 'Ugrave', 'Yacute', 'Ydieresis', 'Zcaron', + 'aacute', 'acircumflex', 'adieresis', 'agrave', 'aring', 'atilde', 'ccedilla', + 'eacute', 'ecircumflex', 'edieresis', 'egrave', 'iacute', 'icircumflex', + 'idieresis', 'igrave', 'ntilde', 'oacute', 'ocircumflex', 'odieresis', + 'ograve', 'otilde', 'scaron', 'uacute', 'ucircumflex', 'udieresis', 'ugrave', + 'yacute', 'ydieresis', 'zcaron', 'exclamsmall', 'Hungarumlautsmall', + 'dollaroldstyle', 'dollarsuperior', 'ampersandsmall', 'Acutesmall', + 'parenleftsuperior', 'parenrightsuperior', 'twodotenleader', 'onedotenleader', + 'zerooldstyle', 'oneoldstyle', 'twooldstyle', 'threeoldstyle', 'fouroldstyle', + 'fiveoldstyle', 'sixoldstyle', 'sevenoldstyle', 'eightoldstyle', + 'nineoldstyle', 'commasuperior', 'threequartersemdash', 'periodsuperior', + 'questionsmall', 'asuperior', 'bsuperior', 'centsuperior', 'dsuperior', + 'esuperior', 'isuperior', 'lsuperior', 'msuperior', 'nsuperior', 'osuperior', + 'rsuperior', 'ssuperior', 'tsuperior', 'ff', 'ffi', 'ffl', 'parenleftinferior', + 'parenrightinferior', 'Circumflexsmall', 'hyphensuperior', 'Gravesmall', + 'Asmall', 'Bsmall', 'Csmall', 'Dsmall', 'Esmall', 'Fsmall', 'Gsmall', 'Hsmall', + 'Ismall', 'Jsmall', 'Ksmall', 'Lsmall', 'Msmall', 'Nsmall', 'Osmall', 'Psmall', + 'Qsmall', 'Rsmall', 'Ssmall', 'Tsmall', 'Usmall', 'Vsmall', 'Wsmall', 'Xsmall', + 'Ysmall', 'Zsmall', 'colonmonetary', 'onefitted', 'rupiah', 'Tildesmall', + 'exclamdownsmall', 'centoldstyle', 'Lslashsmall', 'Scaronsmall', 'Zcaronsmall', + 'Dieresissmall', 'Brevesmall', 'Caronsmall', 'Dotaccentsmall', 'Macronsmall', + 'figuredash', 'hypheninferior', 'Ogoneksmall', 'Ringsmall', 'Cedillasmall', + 'questiondownsmall', 'oneeighth', 'threeeighths', 'fiveeighths', 'seveneighths', + 'onethird', 'twothirds', 'zerosuperior', 'foursuperior', 'fivesuperior', + 'sixsuperior', 'sevensuperior', 'eightsuperior', 'ninesuperior', 'zeroinferior', + 'oneinferior', 'twoinferior', 'threeinferior', 'fourinferior', 'fiveinferior', + 'sixinferior', 'seveninferior', 'eightinferior', 'nineinferior', 'centinferior', + 'dollarinferior', 'periodinferior', 'commainferior', 'Agravesmall', + 'Aacutesmall', 'Acircumflexsmall', 'Atildesmall', 'Adieresissmall', 'Aringsmall', + 'AEsmall', 'Ccedillasmall', 'Egravesmall', 'Eacutesmall', 'Ecircumflexsmall', + 'Edieresissmall', 'Igravesmall', 'Iacutesmall', 'Icircumflexsmall', + 'Idieresissmall', 'Ethsmall', 'Ntildesmall', 'Ogravesmall', 'Oacutesmall', + 'Ocircumflexsmall', 'Otildesmall', 'Odieresissmall', 'OEsmall', 'Oslashsmall', + 'Ugravesmall', 'Uacutesmall', 'Ucircumflexsmall', 'Udieresissmall', + 'Yacutesmall', 'Thornsmall', 'Ydieresissmall', '001.000', '001.001', '001.002', + '001.003', 'Black', 'Bold', 'Book', 'Light', 'Medium', 'Regular', 'Roman', 'Semibold' ] diff --git a/Lib/fontTools/cffLib/specializer.py b/Lib/fontTools/cffLib/specializer.py new file mode 100644 index 0000000..7f19faf --- /dev/null +++ b/Lib/fontTools/cffLib/specializer.py @@ -0,0 +1,551 @@ +# -*- coding: utf-8 -*- + +"""T2CharString operator specializer and generalizer.""" + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * + + +def stringToProgram(string): + if isinstance(string, basestring): + string = string.split() + program = [] + for token in string: + try: + token = int(token) + except ValueError: + try: + token = float(token) + except ValueError: + pass + program.append(token) + return program + +def programToString(program): + return ' '.join(str(x) for x in program) + + +def programToCommands(program): + """Takes a T2CharString program list and returns list of commands. + Each command is a two-tuple of commandname,arg-list. The commandname might + be empty string if no commandname shall be emitted (used for glyph width, + hintmask/cntrmask argument, as well as stray arguments at the end of the + program (¯\_(ツ)_/¯).""" + + width = None + commands = [] + stack = [] + it = iter(program) + for token in it: + if not isinstance(token, basestring): + stack.append(token) + continue + + if width is None and token in {'hstem', 'hstemhm', 'vstem', 'vstemhm', + 'cntrmask', 'hintmask', + 'hmoveto', 'vmoveto', 'rmoveto', + 'endchar'}: + parity = token in {'hmoveto', 'vmoveto'} + if stack and (len(stack) % 2) ^ parity: + width = stack.pop(0) + commands.append(('', [width])) + + if token in {'hintmask', 'cntrmask'}: + if stack: + commands.append(('', stack)) + commands.append((token, [])) + commands.append(('', [next(it)])) + else: + commands.append((token,stack)) + stack = [] + if stack: + commands.append(('', stack)) + return commands + +def commandsToProgram(commands): + """Takes a commands list as returned by programToCommands() and converts + it back to a T2CharString program list.""" + program = [] + for op,args in commands: + program.extend(args) + if op: + program.append(op) + return program + + +def _everyN(el, n): + """Group the list el into groups of size n""" + if len(el) % n != 0: raise ValueError(el) + for i in range(0, len(el), n): + yield el[i:i+n] + + +class _GeneralizerDecombinerCommandsMap(object): + + @staticmethod + def rmoveto(args): + if len(args) != 2: raise ValueError(args) + yield ('rmoveto', args) + @staticmethod + def hmoveto(args): + if len(args) != 1: raise ValueError(args) + yield ('rmoveto', [args[0], 0]) + @staticmethod + def vmoveto(args): + if len(args) != 1: raise ValueError(args) + yield ('rmoveto', [0, args[0]]) + + @staticmethod + def rlineto(args): + if not args: raise ValueError(args) + for args in _everyN(args, 2): + yield ('rlineto', args) + @staticmethod + def hlineto(args): + if not args: raise ValueError(args) + it = iter(args) + try: + while True: + yield ('rlineto', [next(it), 0]) + yield ('rlineto', [0, next(it)]) + except StopIteration: + pass + @staticmethod + def vlineto(args): + if not args: raise ValueError(args) + it = iter(args) + try: + while True: + yield ('rlineto', [0, next(it)]) + yield ('rlineto', [next(it), 0]) + except StopIteration: + pass + @staticmethod + def rrcurveto(args): + if not args: raise ValueError(args) + for args in _everyN(args, 6): + yield ('rrcurveto', args) + @staticmethod + def hhcurveto(args): + if len(args) < 4 or len(args) % 4 > 1: raise ValueError(args) + if len(args) % 2 == 1: + yield ('rrcurveto', [args[1], args[0], args[2], args[3], args[4], 0]) + args = args[5:] + for args in _everyN(args, 4): + yield ('rrcurveto', [args[0], 0, args[1], args[2], args[3], 0]) + @staticmethod + def vvcurveto(args): + if len(args) < 4 or len(args) % 4 > 1: raise ValueError(args) + if len(args) % 2 == 1: + yield ('rrcurveto', [args[0], args[1], args[2], args[3], 0, args[4]]) + args = args[5:] + for args in _everyN(args, 4): + yield ('rrcurveto', [0, args[0], args[1], args[2], 0, args[3]]) + @staticmethod + def hvcurveto(args): + if len(args) < 4 or len(args) % 8 not in {0,1,4,5}: raise ValueError(args) + last_args = None + if len(args) % 2 == 1: + lastStraight = len(args) % 8 == 5 + args, last_args = args[:-5], args[-5:] + it = _everyN(args, 4) + try: + while True: + args = next(it) + yield ('rrcurveto', [args[0], 0, args[1], args[2], 0, args[3]]) + args = next(it) + yield ('rrcurveto', [0, args[0], args[1], args[2], args[3], 0]) + except StopIteration: + pass + if last_args: + args = last_args + if lastStraight: + yield ('rrcurveto', [args[0], 0, args[1], args[2], args[4], args[3]]) + else: + yield ('rrcurveto', [0, args[0], args[1], args[2], args[3], args[4]]) + @staticmethod + def vhcurveto(args): + if len(args) < 4 or len(args) % 8 not in {0,1,4,5}: raise ValueError(args) + last_args = None + if len(args) % 2 == 1: + lastStraight = len(args) % 8 == 5 + args, last_args = args[:-5], args[-5:] + it = _everyN(args, 4) + try: + while True: + args = next(it) + yield ('rrcurveto', [0, args[0], args[1], args[2], args[3], 0]) + args = next(it) + yield ('rrcurveto', [args[0], 0, args[1], args[2], 0, args[3]]) + except StopIteration: + pass + if last_args: + args = last_args + if lastStraight: + yield ('rrcurveto', [0, args[0], args[1], args[2], args[3], args[4]]) + else: + yield ('rrcurveto', [args[0], 0, args[1], args[2], args[4], args[3]]) + + @staticmethod + def rcurveline(args): + if len(args) < 8 or len(args) % 6 != 2: raise ValueError(args) + args, last_args = args[:-2], args[-2:] + for args in _everyN(args, 6): + yield ('rrcurveto', args) + yield ('rlineto', last_args) + @staticmethod + def rlinecurve(args): + if len(args) < 8 or len(args) % 2 != 0: raise ValueError(args) + args, last_args = args[:-6], args[-6:] + for args in _everyN(args, 2): + yield ('rlineto', args) + yield ('rrcurveto', last_args) + + +def generalizeCommands(commands, ignoreErrors=False): + result = [] + mapping = _GeneralizerDecombinerCommandsMap + for op,args in commands: + func = getattr(mapping, op, None) + if not func: + result.append((op,args)) + continue + try: + for command in func(args): + result.append(command) + except ValueError: + if ignoreErrors: + # Store op as data, such that consumers of commands do not have to + # deal with incorrect number of arguments. + result.append(('', args)) + result.append(('', [op])) + else: + raise + return result + +def generalizeProgram(program, **kwargs): + return commandsToProgram(generalizeCommands(programToCommands(program), **kwargs)) + + +def _categorizeVector(v): + """ + Takes X,Y vector v and returns one of r, h, v, or 0 depending on which + of X and/or Y are zero, plus tuple of nonzero ones. If both are zero, + it returns a single zero still. + + >>> _categorizeVector((0,0)) + ('0', (0,)) + >>> _categorizeVector((1,0)) + ('h', (1,)) + >>> _categorizeVector((0,2)) + ('v', (2,)) + >>> _categorizeVector((1,2)) + ('r', (1, 2)) + """ + if not v[0]: + if not v[1]: + return '0', v[:1] + else: + return 'v', v[1:] + else: + if not v[1]: + return 'h', v[:1] + else: + return 'r', v + +def _mergeCategories(a, b): + if a == '0': return b + if b == '0': return a + if a == b: return a + return None + +def _negateCategory(a): + if a == 'h': return 'v' + if a == 'v': return 'h' + assert a in '0r' + return a + +def specializeCommands(commands, + ignoreErrors=False, + generalizeFirst=True, + preserveTopology=False, + maxstack=48): + + # We perform several rounds of optimizations. They are carefully ordered and are: + # + # 0. Generalize commands. + # This ensures that they are in our expected simple form, with each line/curve only + # having arguments for one segment, and using the generic form (rlineto/rrcurveto). + # If caller is sure the input is in this form, they can turn off generalization to + # save time. + # + # 1. Combine successive rmoveto operations. + # + # 2. Specialize rmoveto/rlineto/rrcurveto operators into horizontal/vertical variants. + # We specialize into some, made-up, variants as well, which simplifies following + # passes. + # + # 3. Merge or delete redundant operations, to the extent requested. + # OpenType spec declares point numbers in CFF undefined. As such, we happily + # change topology. If client relies on point numbers (in GPOS anchors, or for + # hinting purposes(what?)) they can turn this off. + # + # 4. Peephole optimization to revert back some of the h/v variants back into their + # original "relative" operator (rline/rrcurveto) if that saves a byte. + # + # 5. Combine adjacent operators when possible, minding not to go over max stack size. + # + # 6. Resolve any remaining made-up operators into real operators. + # + # I have convinced myself that this produces optimal bytecode (except for, possibly + # one byte each time maxstack size prohibits combining.) YMMV, but you'd be wrong. :-) + # A dynamic-programming approach can do the same but would be significantly slower. + + + # 0. Generalize commands. + if generalizeFirst: + commands = generalizeCommands(commands, ignoreErrors=ignoreErrors) + else: + commands = list(commands) # Make copy since we modify in-place later. + + # 1. Combine successive rmoveto operations. + for i in range(len(commands)-1, 0, -1): + if 'rmoveto' == commands[i][0] == commands[i-1][0]: + v1, v2 = commands[i-1][1], commands[i][1] + commands[i-1] = ('rmoveto', [v1[0]+v2[0], v1[1]+v2[1]]) + del commands[i] + + # 2. Specialize rmoveto/rlineto/rrcurveto operators into horizontal/vertical variants. + # + # We, in fact, specialize into more, made-up, variants that special-case when both + # X and Y components are zero. This simplifies the following optimization passes. + # This case is rare, but OCD does not let me skip it. + # + # After this round, we will have four variants that use the following mnemonics: + # + # - 'r' for relative, ie. non-zero X and non-zero Y, + # - 'h' for horizontal, ie. zero X and non-zero Y, + # - 'v' for vertical, ie. non-zero X and zero Y, + # - '0' for zeros, ie. zero X and zero Y. + # + # The '0' pseudo-operators are not part of the spec, but help simplify the following + # optimization rounds. We resolve them at the end. So, after this, we will have four + # moveto and four lineto variants: + # + # - 0moveto, 0lineto + # - hmoveto, hlineto + # - vmoveto, vlineto + # - rmoveto, rlineto + # + # and sixteen curveto variants. For example, a '0hcurveto' operator means a curve + # dx0,dy0,dx1,dy1,dx2,dy2,dx3,dy3 where dx0, dx1, and dy3 are zero but not dx3. + # An 'rvcurveto' means dx3 is zero but not dx0,dy0,dy3. + # + # There are nine different variants of curves without the '0'. Those nine map exactly + # to the existing curve variants in the spec: rrcurveto, and the four variants hhcurveto, + # vvcurveto, hvcurveto, and vhcurveto each cover two cases, one with an odd number of + # arguments and one without. Eg. an hhcurveto with an extra argument (odd number of + # arguments) is in fact an rhcurveto. The operators in the spec are designed such that + # all four of rhcurveto, rvcurveto, hrcurveto, and vrcurveto are encodable for one curve. + # + # Of the curve types with '0', the 00curveto is equivalent to a lineto variant. The rest + # of the curve types with a 0 need to be encoded as a h or v variant. Ie. a '0' can be + # thought of a "don't care" and can be used as either an 'h' or a 'v'. As such, we always + # encode a number 0 as argument when we use a '0' variant. Later on, we can just substitute + # the '0' with either 'h' or 'v' and it works. + # + # When we get to curve splines however, things become more complicated... XXX finish this. + # There's one more complexity with splines. If one side of the spline is not horizontal or + # vertical (or zero), ie. if it's 'r', then it limits which spline types we can encode. + # Only hhcurveto and vvcurveto operators can encode a spline starting with 'r', and + # only hvcurveto and vhcurveto operators can encode a spline ending with 'r'. + # This limits our merge opportunities later. + # + for i in range(len(commands)): + op,args = commands[i] + + if op in {'rmoveto', 'rlineto'}: + c, args = _categorizeVector(args) + commands[i] = c+op[1:], args + continue + + if op == 'rrcurveto': + c1, args1 = _categorizeVector(args[:2]) + c2, args2 = _categorizeVector(args[-2:]) + commands[i] = c1+c2+'curveto', args1+args[2:4]+args2 + continue + + # 3. Merge or delete redundant operations, to the extent requested. + # + # TODO + # A 0moveto that comes before all other path operations can be removed. + # though I find conflicting evidence for this. + # + # TODO + # "If hstem and vstem hints are both declared at the beginning of a + # CharString, and this sequence is followed directly by the hintmask or + # cntrmask operators, then the vstem hint operator (or, if applicable, + # the vstemhm operator) need not be included." + # + # "The sequence and form of a CFF2 CharString program may be represented as: + # {hs* vs* cm* hm* mt subpath}? {mt subpath}*" + # + # https://www.microsoft.com/typography/otspec/cff2charstr.htm#section3.1 + # + # For Type2 CharStrings the sequence is: + # w? {hs* vs* cm* hm* mt subpath}? {mt subpath}* endchar" + + + # Some other redundancies change topology (point numbers). + if not preserveTopology: + for i in range(len(commands)-1, -1, -1): + op, args = commands[i] + + # A 00curveto is demoted to a (specialized) lineto. + if op == '00curveto': + assert len(args) == 4 + c, args = _categorizeVector(args[1:3]) + op = c+'lineto' + commands[i] = op, args + # and then... + + # A 0lineto can be deleted. + if op == '0lineto': + del commands[i] + continue + + # Merge adjacent hlineto's and vlineto's. + if i and op in {'hlineto', 'vlineto'} and op == commands[i-1][0]: + _, other_args = commands[i-1] + assert len(args) == 1 and len(other_args) == 1 + commands[i-1] = (op, [other_args[0]+args[0]]) + del commands[i] + continue + + # 4. Peephole optimization to revert back some of the h/v variants back into their + # original "relative" operator (rline/rrcurveto) if that saves a byte. + for i in range(1, len(commands)-1): + op,args = commands[i] + prv,nxt = commands[i-1][0], commands[i+1][0] + + if op in {'0lineto', 'hlineto', 'vlineto'} and prv == nxt == 'rlineto': + assert len(args) == 1 + args = [0, args[0]] if op[0] == 'v' else [args[0], 0] + commands[i] = ('rlineto', args) + continue + + if op[2:] == 'curveto' and len(args) == 5 and prv == nxt == 'rrcurveto': + assert (op[0] == 'r') ^ (op[1] == 'r') + if op[0] == 'v': + pos = 0 + elif op[0] != 'r': + pos = 1 + elif op[1] == 'v': + pos = 4 + else: + pos = 5 + # Insert, while maintaining the type of args (can be tuple or list). + args = args[:pos] + type(args)((0,)) + args[pos:] + commands[i] = ('rrcurveto', args) + continue + + # 5. Combine adjacent operators when possible, minding not to go over max stack size. + for i in range(len(commands)-1, 0, -1): + op1,args1 = commands[i-1] + op2,args2 = commands[i] + new_op = None + + # Merge logic... + if {op1, op2} <= {'rlineto', 'rrcurveto'}: + if op1 == op2: + new_op = op1 + else: + if op2 == 'rrcurveto' and len(args2) == 6: + new_op = 'rlinecurve' + elif len(args2) == 2: + new_op = 'rcurveline' + + elif (op1, op2) in {('rlineto', 'rlinecurve'), ('rrcurveto', 'rcurveline')}: + new_op = op2 + + elif {op1, op2} == {'vlineto', 'hlineto'}: + new_op = op1 + + elif 'curveto' == op1[2:] == op2[2:]: + d0, d1 = op1[:2] + d2, d3 = op2[:2] + + if d1 == 'r' or d2 == 'r' or d0 == d3 == 'r': + continue + + d = _mergeCategories(d1, d2) + if d is None: continue + if d0 == 'r': + d = _mergeCategories(d, d3) + if d is None: continue + new_op = 'r'+d+'curveto' + elif d3 == 'r': + d0 = _mergeCategories(d0, _negateCategory(d)) + if d0 is None: continue + new_op = d0+'r'+'curveto' + else: + d0 = _mergeCategories(d0, d3) + if d0 is None: continue + new_op = d0+d+'curveto' + + if new_op and len(args1) + len(args2) <= maxstack: + commands[i-1] = (new_op, args1+args2) + del commands[i] + + # 6. Resolve any remaining made-up operators into real operators. + for i in range(len(commands)): + op,args = commands[i] + + if op in {'0moveto', '0lineto'}: + commands[i] = 'h'+op[1:], args + continue + + if op[2:] == 'curveto' and op[:2] not in {'rr', 'hh', 'vv', 'vh', 'hv'}: + op0, op1 = op[:2] + if (op0 == 'r') ^ (op1 == 'r'): + assert len(args) % 2 == 1 + if op0 == '0': op0 = 'h' + if op1 == '0': op1 = 'h' + if op0 == 'r': op0 = op1 + if op1 == 'r': op1 = _negateCategory(op0) + assert {op0,op1} <= {'h','v'}, (op0, op1) + + if len(args) % 2: + if op0 != op1: # vhcurveto / hvcurveto + if (op0 == 'h') ^ (len(args) % 8 == 1): + # Swap last two args order + args = args[:-2]+args[-1:]+args[-2:-1] + else: # hhcurveto / vvcurveto + if op0 == 'h': # hhcurveto + # Swap first two args order + args = args[1:2]+args[:1]+args[2:] + + commands[i] = op0+op1+'curveto', args + continue + + return commands + +def specializeProgram(program, **kwargs): + return commandsToProgram(specializeCommands(programToCommands(program), **kwargs)) + + +if __name__ == '__main__': + import sys + if len(sys.argv) == 1: + import doctest + sys.exit(doctest.testmod().failed) + program = stringToProgram(sys.argv[1:]) + print("Program:"); print(programToString(program)) + commands = programToCommands(program) + print("Commands:"); print(commands) + program2 = commandsToProgram(commands) + print("Program from commands:"); print(programToString(program2)) + assert program == program2 + print("Generalized program:"); print(programToString(generalizeProgram(program))) + print("Specialized program:"); print(programToString(specializeProgram(program))) + diff --git a/Lib/fontTools/cffLib/width.py b/Lib/fontTools/cffLib/width.py new file mode 100644 index 0000000..a9e5532 --- /dev/null +++ b/Lib/fontTools/cffLib/width.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- + +"""T2CharString glyph width optimizer.""" + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.ttLib import TTFont, getTableClass +from collections import defaultdict +from operator import add +from functools import partial, reduce + + +class missingdict(dict): + def __init__(self, missing_func): + self.missing_func = missing_func + def __missing__(self, v): + return self.missing_func(v) + +def cumSum(f, op=add, start=0, decreasing=False): + + keys = sorted(f.keys()) + minx, maxx = keys[0], keys[-1] + + total = reduce(op, f.values(), start) + + if decreasing: + missing = lambda x: start if x > maxx else total + domain = range(maxx, minx - 1, -1) + else: + missing = lambda x: start if x < minx else total + domain = range(minx, maxx + 1) + + out = missingdict(missing) + + v = start + for x in domain: + v = op(v, f[x]) + out[x] = v + + return out + +def byteCost(widths, default, nominal): + + if not hasattr(widths, 'items'): + d = defaultdict(int) + for w in widths: + d[w] += 1 + widths = d + + cost = 0 + for w,freq in widths.items(): + if w == default: continue + diff = abs(w - nominal) + if diff <= 107: + cost += freq + elif diff <= 1131: + cost += freq * 2 + else: + cost += freq * 5 + return cost + + +def optimizeWidthsBruteforce(widths): + """Bruteforce version. Veeeeeeeeeeeeeeeeery slow. Only works for smallests of fonts.""" + + d = defaultdict(int) + for w in widths: + d[w] += 1 + + # Maximum number of bytes using default can possibly save + maxDefaultAdvantage = 5 * max(d.values()) + + minw, maxw = min(widths), max(widths) + domain = list(range(minw, maxw+1)) + + bestCostWithoutDefault = min(byteCost(widths, None, nominal) for nominal in domain) + + bestCost = len(widths) * 5 + 1 + for nominal in domain: + if byteCost(widths, None, nominal) > bestCost + maxDefaultAdvantage: + continue + for default in domain: + cost = byteCost(widths, default, nominal) + if cost < bestCost: + bestCost = cost + bestDefault = default + bestNominal = nominal + + return bestDefault, bestNominal + + +def optimizeWidths(widths): + """Given a list of glyph widths, or dictionary mapping glyph width to number of + glyphs having that, returns a tuple of best CFF default and nominal glyph widths. + + This algorithm is linear in UPEM+numGlyphs.""" + + if not hasattr(widths, 'items'): + d = defaultdict(int) + for w in widths: + d[w] += 1 + widths = d + + keys = sorted(widths.keys()) + minw, maxw = keys[0], keys[-1] + domain = list(range(minw, maxw+1)) + + # Cumulative sum/max forward/backward. + cumFrqU = cumSum(widths, op=add) + cumMaxU = cumSum(widths, op=max) + cumFrqD = cumSum(widths, op=add, decreasing=True) + cumMaxD = cumSum(widths, op=max, decreasing=True) + + # Cost per nominal choice, without default consideration. + nomnCostU = missingdict(lambda x: cumFrqU[x] + cumFrqU[x-108] + cumFrqU[x-1132]*3) + nomnCostD = missingdict(lambda x: cumFrqD[x] + cumFrqD[x+108] + cumFrqD[x+1132]*3) + nomnCost = missingdict(lambda x: nomnCostU[x] + nomnCostD[x] - widths[x]) + + # Cost-saving per nominal choice, by best default choice. + dfltCostU = missingdict(lambda x: max(cumMaxU[x], cumMaxU[x-108]*2, cumMaxU[x-1132]*5)) + dfltCostD = missingdict(lambda x: max(cumMaxD[x], cumMaxD[x+108]*2, cumMaxD[x+1132]*5)) + dfltCost = missingdict(lambda x: max(dfltCostU[x], dfltCostD[x])) + + # Combined cost per nominal choice. + bestCost = missingdict(lambda x: nomnCost[x] - dfltCost[x]) + + # Best nominal. + nominal = min(domain, key=lambda x: bestCost[x]) + + # Work back the best default. + bestC = bestCost[nominal] + dfltC = nomnCost[nominal] - bestCost[nominal] + ends = [] + if dfltC == dfltCostU[nominal]: + starts = [nominal, nominal-108, nominal-1131] + for start in starts: + while cumMaxU[start] and cumMaxU[start] == cumMaxU[start-1]: + start -= 1 + ends.append(start) + else: + starts = [nominal, nominal+108, nominal+1131] + for start in starts: + while cumMaxD[start] and cumMaxD[start] == cumMaxD[start+1]: + start += 1 + ends.append(start) + default = min(ends, key=lambda default: byteCost(widths, default, nominal)) + + return default, nominal + + +if __name__ == '__main__': + import sys + if len(sys.argv) == 1: + import doctest + sys.exit(doctest.testmod().failed) + for fontfile in sys.argv[1:]: + font = TTFont(fontfile) + hmtx = font['hmtx'] + widths = [m[0] for m in hmtx.metrics.values()] + default, nominal = optimizeWidths(widths) + print("glyphs=%d default=%d nominal=%d byteCost=%d" % (len(widths), default, nominal, byteCost(widths, default, nominal))) + #default, nominal = optimizeWidthsBruteforce(widths) + #print("glyphs=%d default=%d nominal=%d byteCost=%d" % (len(widths), default, nominal, byteCost(widths, default, nominal))) diff --git a/Lib/fontTools/designspaceLib/__init__.py b/Lib/fontTools/designspaceLib/__init__.py new file mode 100644 index 0000000..8063ac5 --- /dev/null +++ b/Lib/fontTools/designspaceLib/__init__.py @@ -0,0 +1,1252 @@ +# -*- coding: utf-8 -*- + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.loggingTools import LogMixin +import collections +import os +import posixpath +import plistlib + +try: + import xml.etree.cElementTree as ET +except ImportError: + import xml.etree.ElementTree as ET + +""" + designSpaceDocument + + - read and write designspace files +""" + +__all__ = [ + 'DesignSpaceDocumentError', 'DesignSpaceDocument', 'SourceDescriptor', + 'InstanceDescriptor', 'AxisDescriptor', 'RuleDescriptor', 'BaseDocReader', + 'BaseDocWriter' +] + +# ElementTree allows to find namespace-prefixed elements, but not attributes +# so we have to do it ourselves for 'xml:lang' +XML_NS = "{http://www.w3.org/XML/1998/namespace}" +XML_LANG = XML_NS + "lang" + + +def to_plist(value): + try: + # Python 2 + string = plistlib.writePlistToString(value) + except AttributeError: + # Python 3 + string = plistlib.dumps(value).decode() + return ET.fromstring(string)[0] + + +def from_plist(element): + if element is None: + return {} + plist = ET.Element('plist') + plist.append(element) + string = ET.tostring(plist) + try: + # Python 2 + return plistlib.readPlistFromString(string) + except AttributeError: + # Python 3 + return plistlib.loads(string, fmt=plistlib.FMT_XML) + + +def posix(path): + """Normalize paths using forward slash to work also on Windows.""" + new_path = posixpath.join(*path.split(os.path.sep)) + if path.startswith('/'): + # The above transformation loses absolute paths + new_path = '/' + new_path + return new_path + + +def posixpath_property(private_name): + def getter(self): + # Normal getter + return getattr(self, private_name) + + def setter(self, value): + # The setter rewrites paths using forward slashes + if value is not None: + value = posix(value) + setattr(self, private_name, value) + + return property(getter, setter) + + +class DesignSpaceDocumentError(Exception): + def __init__(self, msg, obj=None): + self.msg = msg + self.obj = obj + + def __str__(self): + return str(self.msg) + ( + ": %r" % self.obj if self.obj is not None else "") + + +def _indent(elem, whitespace=" ", level=0): + # taken from http://effbot.org/zone/element-lib.htm#prettyprint + i = "\n" + level * whitespace + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + whitespace + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + _indent(elem, whitespace, level+1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + + +class SimpleDescriptor(object): + """ Containers for a bunch of attributes""" + + # XXX this is ugly. The 'print' is inappropriate here, and instead of + # assert, it should simply return True/False + def compare(self, other): + # test if this object contains the same data as the other + for attr in self._attrs: + try: + assert(getattr(self, attr) == getattr(other, attr)) + except AssertionError: + print("failed attribute", attr, getattr(self, attr), "!=", getattr(other, attr)) + + +class SourceDescriptor(SimpleDescriptor): + """Simple container for data related to the source""" + flavor = "source" + _attrs = ['filename', 'path', 'name', 'layerName', + 'location', 'copyLib', + 'copyGroups', 'copyFeatures', + 'muteKerning', 'muteInfo', + 'mutedGlyphNames', + 'familyName', 'styleName'] + + def __init__(self): + self.filename = None + """The original path as found in the document.""" + + self.path = None + """The absolute path, calculated from filename.""" + + self.font = None + """Any Python object. Optional. Points to a representation of this + source font that is loaded in memory, as a Python object (e.g. a + ``defcon.Font`` or a ``fontTools.ttFont.TTFont``). + + The default document reader will not fill-in this attribute, and the + default writer will not use this attribute. It is up to the user of + ``designspaceLib`` to either load the resource identified by + ``filename`` and store it in this field, or write the contents of + this field to the disk and make ```filename`` point to that. + """ + + self.name = None + self.location = None + self.layerName = None + self.copyLib = False + self.copyInfo = False + self.copyGroups = False + self.copyFeatures = False + self.muteKerning = False + self.muteInfo = False + self.mutedGlyphNames = [] + self.familyName = None + self.styleName = None + + path = posixpath_property("_path") + filename = posixpath_property("_filename") + + +class RuleDescriptor(SimpleDescriptor): + """ + + + + + + + + + + + + """ + _attrs = ['name', 'conditionSets', 'subs'] # what do we need here + + def __init__(self): + self.name = None + self.conditionSets = [] # list of list of dict(name='aaaa', minimum=0, maximum=1000) + self.subs = [] # list of substitutions stored as tuples of glyphnames ("a", "a.alt") + + +def evaluateRule(rule, location): + """ Return True if any of the rule's conditionsets matches the given location.""" + return any(evaluateConditions(c, location) for c in rule.conditionSets) + + +def evaluateConditions(conditions, location): + """ Return True if all the conditions matches the given location. + If a condition has no minimum, check for < maximum. + If a condition has no maximum, check for > minimum. + """ + for cd in conditions: + value = location[cd['name']] + if cd.get('minimum') is None: + if value > cd['maximum']: + return False + elif cd.get('maximum') is None: + if cd['minimum'] > value: + return False + elif not cd['minimum'] <= value <= cd['maximum']: + return False + return True + + +def processRules(rules, location, glyphNames): + """ Apply these rules at this location to these glyphnames.minimum + - rule order matters + """ + newNames = [] + for rule in rules: + if evaluateRule(rule, location): + for name in glyphNames: + swap = False + for a, b in rule.subs: + if name == a: + swap = True + break + if swap: + newNames.append(b) + else: + newNames.append(name) + glyphNames = newNames + newNames = [] + return glyphNames + + +class InstanceDescriptor(SimpleDescriptor): + """Simple container for data related to the instance""" + flavor = "instance" + _defaultLanguageCode = "en" + _attrs = ['path', + 'name', + 'location', + 'familyName', + 'styleName', + 'postScriptFontName', + 'styleMapFamilyName', + 'styleMapStyleName', + 'kerning', + 'info', + 'lib'] + + def __init__(self): + self.filename = None # the original path as found in the document + self.path = None # the absolute path, calculated from filename + self.name = None + self.location = None + self.familyName = None + self.styleName = None + self.postScriptFontName = None + self.styleMapFamilyName = None + self.styleMapStyleName = None + self.localisedStyleName = {} + self.localisedFamilyName = {} + self.localisedStyleMapStyleName = {} + self.localisedStyleMapFamilyName = {} + self.glyphs = {} + self.mutedGlyphNames = [] + self.kerning = True + self.info = True + + self.lib = {} + """Custom data associated with this instance.""" + + path = posixpath_property("_path") + filename = posixpath_property("_filename") + + def setStyleName(self, styleName, languageCode="en"): + self.localisedStyleName[languageCode] = styleName + + def getStyleName(self, languageCode="en"): + return self.localisedStyleName.get(languageCode) + + def setFamilyName(self, familyName, languageCode="en"): + self.localisedFamilyName[languageCode] = familyName + + def getFamilyName(self, languageCode="en"): + return self.localisedFamilyName.get(languageCode) + + def setStyleMapStyleName(self, styleMapStyleName, languageCode="en"): + self.localisedStyleMapStyleName[languageCode] = styleMapStyleName + + def getStyleMapStyleName(self, languageCode="en"): + return self.localisedStyleMapStyleName.get(languageCode) + + def setStyleMapFamilyName(self, styleMapFamilyName, languageCode="en"): + self.localisedStyleMapFamilyName[languageCode] = styleMapFamilyName + + def getStyleMapFamilyName(self, languageCode="en"): + return self.localisedStyleMapFamilyName.get(languageCode) + + +def tagForAxisName(name): + # try to find or make a tag name for this axis name + names = { + 'weight': ('wght', dict(en = 'Weight')), + 'width': ('wdth', dict(en = 'Width')), + 'optical': ('opsz', dict(en = 'Optical Size')), + 'slant': ('slnt', dict(en = 'Slant')), + 'italic': ('ital', dict(en = 'Italic')), + } + if name.lower() in names: + return names[name.lower()] + if len(name) < 4: + tag = name + "*" * (4 - len(name)) + else: + tag = name[:4] + return tag, dict(en=name) + + +class AxisDescriptor(SimpleDescriptor): + """ Simple container for the axis data + Add more localisations? + """ + flavor = "axis" + _attrs = ['tag', 'name', 'maximum', 'minimum', 'default', 'map'] + + def __init__(self): + self.tag = None # opentype tag for this axis + self.name = None # name of the axis used in locations + self.labelNames = {} # names for UI purposes, if this is not a standard axis, + self.minimum = None + self.maximum = None + self.default = None + self.hidden = False + self.map = [] + + def serialize(self): + # output to a dict, used in testing + return dict( + tag=self.tag, + name=self.name, + labelNames=self.labelNames, + maximum=self.maximum, + minimum=self.minimum, + default=self.default, + hidden=self.hidden, + map=self.map, + ) + + +class BaseDocWriter(object): + _whiteSpace = " " + ruleDescriptorClass = RuleDescriptor + axisDescriptorClass = AxisDescriptor + sourceDescriptorClass = SourceDescriptor + instanceDescriptorClass = InstanceDescriptor + + @classmethod + def getAxisDecriptor(cls): + return cls.axisDescriptorClass() + + @classmethod + def getSourceDescriptor(cls): + return cls.sourceDescriptorClass() + + @classmethod + def getInstanceDescriptor(cls): + return cls.instanceDescriptorClass() + + @classmethod + def getRuleDescriptor(cls): + return cls.ruleDescriptorClass() + + def __init__(self, documentPath, documentObject): + self.path = documentPath + self.documentObject = documentObject + self.documentVersion = "4.0" + self.root = ET.Element("designspace") + self.root.attrib['format'] = self.documentVersion + self._axes = [] # for use by the writer only + self._rules = [] # for use by the writer only + + def write(self, pretty=True): + if self.documentObject.axes: + self.root.append(ET.Element("axes")) + for axisObject in self.documentObject.axes: + self._addAxis(axisObject) + + if self.documentObject.rules: + self.root.append(ET.Element("rules")) + for ruleObject in self.documentObject.rules: + self._addRule(ruleObject) + + if self.documentObject.sources: + self.root.append(ET.Element("sources")) + for sourceObject in self.documentObject.sources: + self._addSource(sourceObject) + + if self.documentObject.instances: + self.root.append(ET.Element("instances")) + for instanceObject in self.documentObject.instances: + self._addInstance(instanceObject) + + if self.documentObject.lib: + self._addLib(self.documentObject.lib) + + if pretty: + _indent(self.root, whitespace=self._whiteSpace) + tree = ET.ElementTree(self.root) + tree.write(self.path, encoding="utf-8", method='xml', xml_declaration=True) + + def _makeLocationElement(self, locationObject, name=None): + """ Convert Location dict to a locationElement.""" + locElement = ET.Element("location") + if name is not None: + locElement.attrib['name'] = name + validatedLocation = self.documentObject.newDefaultLocation() + for axisName, axisValue in locationObject.items(): + if axisName in validatedLocation: + # only accept values we know + validatedLocation[axisName] = axisValue + for dimensionName, dimensionValue in validatedLocation.items(): + dimElement = ET.Element('dimension') + dimElement.attrib['name'] = dimensionName + if type(dimensionValue) == tuple: + dimElement.attrib['xvalue'] = self.intOrFloat(dimensionValue[0]) + dimElement.attrib['yvalue'] = self.intOrFloat(dimensionValue[1]) + else: + dimElement.attrib['xvalue'] = self.intOrFloat(dimensionValue) + locElement.append(dimElement) + return locElement, validatedLocation + + def intOrFloat(self, num): + if int(num) == num: + return "%d" % num + return "%f" % num + + def _addRule(self, ruleObject): + # if none of the conditions have minimum or maximum values, do not add the rule. + self._rules.append(ruleObject) + ruleElement = ET.Element('rule') + if ruleObject.name is not None: + ruleElement.attrib['name'] = ruleObject.name + for conditions in ruleObject.conditionSets: + conditionsetElement = ET.Element('conditionset') + for cond in conditions: + if cond.get('minimum') is None and cond.get('maximum') is None: + # neither is defined, don't add this condition + continue + conditionElement = ET.Element('condition') + conditionElement.attrib['name'] = cond.get('name') + if cond.get('minimum') is not None: + conditionElement.attrib['minimum'] = self.intOrFloat(cond.get('minimum')) + if cond.get('maximum') is not None: + conditionElement.attrib['maximum'] = self.intOrFloat(cond.get('maximum')) + conditionsetElement.append(conditionElement) + if len(conditionsetElement): + ruleElement.append(conditionsetElement) + for sub in ruleObject.subs: + subElement = ET.Element('sub') + subElement.attrib['name'] = sub[0] + subElement.attrib['with'] = sub[1] + ruleElement.append(subElement) + if len(ruleElement): + self.root.findall('.rules')[0].append(ruleElement) + + def _addAxis(self, axisObject): + self._axes.append(axisObject) + axisElement = ET.Element('axis') + axisElement.attrib['tag'] = axisObject.tag + axisElement.attrib['name'] = axisObject.name + axisElement.attrib['minimum'] = self.intOrFloat(axisObject.minimum) + axisElement.attrib['maximum'] = self.intOrFloat(axisObject.maximum) + axisElement.attrib['default'] = self.intOrFloat(axisObject.default) + if axisObject.hidden: + axisElement.attrib['hidden'] = "1" + for languageCode, labelName in sorted(axisObject.labelNames.items()): + languageElement = ET.Element('labelname') + languageElement.attrib[u'xml:lang'] = languageCode + languageElement.text = labelName + axisElement.append(languageElement) + if axisObject.map: + for inputValue, outputValue in axisObject.map: + mapElement = ET.Element('map') + mapElement.attrib['input'] = self.intOrFloat(inputValue) + mapElement.attrib['output'] = self.intOrFloat(outputValue) + axisElement.append(mapElement) + self.root.findall('.axes')[0].append(axisElement) + + def _addInstance(self, instanceObject): + instanceElement = ET.Element('instance') + if instanceObject.name is not None: + instanceElement.attrib['name'] = instanceObject.name + if instanceObject.familyName is not None: + instanceElement.attrib['familyname'] = instanceObject.familyName + if instanceObject.styleName is not None: + instanceElement.attrib['stylename'] = instanceObject.styleName + # add localisations + if instanceObject.localisedStyleName: + languageCodes = list(instanceObject.localisedStyleName.keys()) + languageCodes.sort() + for code in languageCodes: + if code == "en": + continue # already stored in the element attribute + localisedStyleNameElement = ET.Element('stylename') + localisedStyleNameElement.attrib["xml:lang"] = code + localisedStyleNameElement.text = instanceObject.getStyleName(code) + instanceElement.append(localisedStyleNameElement) + if instanceObject.localisedFamilyName: + languageCodes = list(instanceObject.localisedFamilyName.keys()) + languageCodes.sort() + for code in languageCodes: + if code == "en": + continue # already stored in the element attribute + localisedFamilyNameElement = ET.Element('familyname') + localisedFamilyNameElement.attrib["xml:lang"] = code + localisedFamilyNameElement.text = instanceObject.getFamilyName(code) + instanceElement.append(localisedFamilyNameElement) + if instanceObject.localisedStyleMapStyleName: + languageCodes = list(instanceObject.localisedStyleMapStyleName.keys()) + languageCodes.sort() + for code in languageCodes: + if code == "en": + continue + localisedStyleMapStyleNameElement = ET.Element('stylemapstylename') + localisedStyleMapStyleNameElement.attrib["xml:lang"] = code + localisedStyleMapStyleNameElement.text = instanceObject.getStyleMapStyleName(code) + instanceElement.append(localisedStyleMapStyleNameElement) + if instanceObject.localisedStyleMapFamilyName: + languageCodes = list(instanceObject.localisedStyleMapFamilyName.keys()) + languageCodes.sort() + for code in languageCodes: + if code == "en": + continue + localisedStyleMapFamilyNameElement = ET.Element('stylemapfamilyname') + localisedStyleMapFamilyNameElement.attrib["xml:lang"] = code + localisedStyleMapFamilyNameElement.text = instanceObject.getStyleMapFamilyName(code) + instanceElement.append(localisedStyleMapFamilyNameElement) + + if instanceObject.location is not None: + locationElement, instanceObject.location = self._makeLocationElement(instanceObject.location) + instanceElement.append(locationElement) + if instanceObject.filename is not None: + instanceElement.attrib['filename'] = instanceObject.filename + if instanceObject.postScriptFontName is not None: + instanceElement.attrib['postscriptfontname'] = instanceObject.postScriptFontName + if instanceObject.styleMapFamilyName is not None: + instanceElement.attrib['stylemapfamilyname'] = instanceObject.styleMapFamilyName + if instanceObject.styleMapStyleName is not None: + instanceElement.attrib['stylemapstylename'] = instanceObject.styleMapStyleName + if instanceObject.glyphs: + if instanceElement.findall('.glyphs') == []: + glyphsElement = ET.Element('glyphs') + instanceElement.append(glyphsElement) + glyphsElement = instanceElement.findall('.glyphs')[0] + for glyphName, data in sorted(instanceObject.glyphs.items()): + glyphElement = self._writeGlyphElement(instanceElement, instanceObject, glyphName, data) + glyphsElement.append(glyphElement) + if instanceObject.kerning: + kerningElement = ET.Element('kerning') + instanceElement.append(kerningElement) + if instanceObject.info: + infoElement = ET.Element('info') + instanceElement.append(infoElement) + if instanceObject.lib: + libElement = ET.Element('lib') + libElement.append(to_plist(instanceObject.lib)) + instanceElement.append(libElement) + self.root.findall('.instances')[0].append(instanceElement) + + def _addSource(self, sourceObject): + sourceElement = ET.Element("source") + if sourceObject.filename is not None: + sourceElement.attrib['filename'] = sourceObject.filename + if sourceObject.name is not None: + if sourceObject.name.find("temp_master") != 0: + # do not save temporary source names + sourceElement.attrib['name'] = sourceObject.name + if sourceObject.familyName is not None: + sourceElement.attrib['familyname'] = sourceObject.familyName + if sourceObject.styleName is not None: + sourceElement.attrib['stylename'] = sourceObject.styleName + if sourceObject.layerName is not None: + sourceElement.attrib['layer'] = sourceObject.layerName + if sourceObject.copyLib: + libElement = ET.Element('lib') + libElement.attrib['copy'] = "1" + sourceElement.append(libElement) + if sourceObject.copyGroups: + groupsElement = ET.Element('groups') + groupsElement.attrib['copy'] = "1" + sourceElement.append(groupsElement) + if sourceObject.copyFeatures: + featuresElement = ET.Element('features') + featuresElement.attrib['copy'] = "1" + sourceElement.append(featuresElement) + if sourceObject.copyInfo or sourceObject.muteInfo: + infoElement = ET.Element('info') + if sourceObject.copyInfo: + infoElement.attrib['copy'] = "1" + if sourceObject.muteInfo: + infoElement.attrib['mute'] = "1" + sourceElement.append(infoElement) + if sourceObject.muteKerning: + kerningElement = ET.Element("kerning") + kerningElement.attrib["mute"] = '1' + sourceElement.append(kerningElement) + if sourceObject.mutedGlyphNames: + for name in sourceObject.mutedGlyphNames: + glyphElement = ET.Element("glyph") + glyphElement.attrib["name"] = name + glyphElement.attrib["mute"] = '1' + sourceElement.append(glyphElement) + locationElement, sourceObject.location = self._makeLocationElement(sourceObject.location) + sourceElement.append(locationElement) + self.root.findall('.sources')[0].append(sourceElement) + + def _addLib(self, dict): + libElement = ET.Element('lib') + libElement.append(to_plist(dict)) + self.root.append(libElement) + + def _writeGlyphElement(self, instanceElement, instanceObject, glyphName, data): + glyphElement = ET.Element('glyph') + if data.get('mute'): + glyphElement.attrib['mute'] = "1" + if data.get('unicodes') is not None: + glyphElement.attrib['unicode'] = " ".join([hex(u) for u in data.get('unicodes')]) + if data.get('instanceLocation') is not None: + locationElement, data['instanceLocation'] = self._makeLocationElement(data.get('instanceLocation')) + glyphElement.append(locationElement) + if glyphName is not None: + glyphElement.attrib['name'] = glyphName + if data.get('note') is not None: + noteElement = ET.Element('note') + noteElement.text = data.get('note') + glyphElement.append(noteElement) + if data.get('masters') is not None: + mastersElement = ET.Element("masters") + for m in data.get('masters'): + masterElement = ET.Element("master") + if m.get('glyphName') is not None: + masterElement.attrib['glyphname'] = m.get('glyphName') + if m.get('font') is not None: + masterElement.attrib['source'] = m.get('font') + if m.get('location') is not None: + locationElement, m['location'] = self._makeLocationElement(m.get('location')) + masterElement.append(locationElement) + mastersElement.append(masterElement) + glyphElement.append(mastersElement) + return glyphElement + + +class BaseDocReader(LogMixin): + ruleDescriptorClass = RuleDescriptor + axisDescriptorClass = AxisDescriptor + sourceDescriptorClass = SourceDescriptor + instanceDescriptorClass = InstanceDescriptor + + def __init__(self, documentPath, documentObject): + self.path = documentPath + self.documentObject = documentObject + tree = ET.parse(self.path) + self.root = tree.getroot() + self.documentObject.formatVersion = self.root.attrib.get("format", "3.0") + self._axes = [] + self.rules = [] + self.sources = [] + self.instances = [] + self.axisDefaults = {} + self._strictAxisNames = True + + def read(self): + self.readAxes() + self.readRules() + self.readSources() + self.readInstances() + self.readLib() + + def getSourcePaths(self, makeGlyphs=True, makeKerning=True, makeInfo=True): + paths = [] + for name in self.documentObject.sources.keys(): + paths.append(self.documentObject.sources[name][0].path) + return paths + + def readRules(self): + # we also need to read any conditions that are outside of a condition set. + rules = [] + for ruleElement in self.root.findall(".rules/rule"): + ruleObject = self.ruleDescriptorClass() + ruleName = ruleObject.name = ruleElement.attrib.get("name") + # read any stray conditions outside a condition set + externalConditions = self._readConditionElements( + ruleElement, + ruleName, + ) + if externalConditions: + ruleObject.conditionSets.append(externalConditions) + self.log.info( + "Found stray rule conditions outside a conditionset. " + "Wrapped them in a new conditionset." + ) + # read the conditionsets + for conditionSetElement in ruleElement.findall('.conditionset'): + conditionSet = self._readConditionElements( + conditionSetElement, + ruleName, + ) + if conditionSet is not None: + ruleObject.conditionSets.append(conditionSet) + for subElement in ruleElement.findall('.sub'): + a = subElement.attrib['name'] + b = subElement.attrib['with'] + ruleObject.subs.append((a, b)) + rules.append(ruleObject) + self.documentObject.rules = rules + + def _readConditionElements(self, parentElement, ruleName=None): + cds = [] + for conditionElement in parentElement.findall('.condition'): + cd = {} + cdMin = conditionElement.attrib.get("minimum") + if cdMin is not None: + cd['minimum'] = float(cdMin) + else: + # will allow these to be None, assume axis.minimum + cd['minimum'] = None + cdMax = conditionElement.attrib.get("maximum") + if cdMax is not None: + cd['maximum'] = float(cdMax) + else: + # will allow these to be None, assume axis.maximum + cd['maximum'] = None + cd['name'] = conditionElement.attrib.get("name") + # # test for things + if cd.get('minimum') is None and cd.get('maximum') is None: + raise DesignSpaceDocumentError( + "condition missing required minimum or maximum in rule" + + (" '%s'" % ruleName if ruleName is not None else "")) + cds.append(cd) + return cds + + def readAxes(self): + # read the axes elements, including the warp map. + if len(self.root.findall(".axes/axis")) == 0: + self._strictAxisNames = False + return + for axisElement in self.root.findall(".axes/axis"): + axisObject = self.axisDescriptorClass() + axisObject.name = axisElement.attrib.get("name") + axisObject.minimum = float(axisElement.attrib.get("minimum")) + axisObject.maximum = float(axisElement.attrib.get("maximum")) + if axisElement.attrib.get('hidden', False): + axisObject.hidden = True + axisObject.default = float(axisElement.attrib.get("default")) + axisObject.tag = axisElement.attrib.get("tag") + for mapElement in axisElement.findall('map'): + a = float(mapElement.attrib['input']) + b = float(mapElement.attrib['output']) + axisObject.map.append((a, b)) + for labelNameElement in axisElement.findall('labelname'): + # Note: elementtree reads the xml:lang attribute name as + # '{http://www.w3.org/XML/1998/namespace}lang' + for key, lang in labelNameElement.items(): + if key == XML_LANG: + labelName = labelNameElement.text + axisObject.labelNames[lang] = labelName + self.documentObject.axes.append(axisObject) + self.axisDefaults[axisObject.name] = axisObject.default + self.documentObject.defaultLoc = self.axisDefaults + + def readSources(self): + for sourceCount, sourceElement in enumerate(self.root.findall(".sources/source")): + filename = sourceElement.attrib.get('filename') + if filename is not None and self.path is not None: + sourcePath = os.path.abspath(os.path.join(os.path.dirname(self.path), filename)) + else: + sourcePath = None + sourceName = sourceElement.attrib.get('name') + if sourceName is None: + # add a temporary source name + sourceName = "temp_master.%d" % (sourceCount) + sourceObject = self.sourceDescriptorClass() + sourceObject.path = sourcePath # absolute path to the ufo source + sourceObject.filename = filename # path as it is stored in the document + sourceObject.name = sourceName + familyName = sourceElement.attrib.get("familyname") + if familyName is not None: + sourceObject.familyName = familyName + styleName = sourceElement.attrib.get("stylename") + if styleName is not None: + sourceObject.styleName = styleName + sourceObject.location = self.locationFromElement(sourceElement) + layerName = sourceElement.attrib.get('layer') + if layerName is not None: + sourceObject.layerName = layerName + for libElement in sourceElement.findall('.lib'): + if libElement.attrib.get('copy') == '1': + sourceObject.copyLib = True + for groupsElement in sourceElement.findall('.groups'): + if groupsElement.attrib.get('copy') == '1': + sourceObject.copyGroups = True + for infoElement in sourceElement.findall(".info"): + if infoElement.attrib.get('copy') == '1': + sourceObject.copyInfo = True + if infoElement.attrib.get('mute') == '1': + sourceObject.muteInfo = True + for featuresElement in sourceElement.findall(".features"): + if featuresElement.attrib.get('copy') == '1': + sourceObject.copyFeatures = True + for glyphElement in sourceElement.findall(".glyph"): + glyphName = glyphElement.attrib.get('name') + if glyphName is None: + continue + if glyphElement.attrib.get('mute') == '1': + sourceObject.mutedGlyphNames.append(glyphName) + for kerningElement in sourceElement.findall(".kerning"): + if kerningElement.attrib.get('mute') == '1': + sourceObject.muteKerning = True + self.documentObject.sources.append(sourceObject) + + def locationFromElement(self, element): + elementLocation = None + for locationElement in element.findall('.location'): + elementLocation = self.readLocationElement(locationElement) + break + return elementLocation + + def readLocationElement(self, locationElement): + """ Format 0 location reader """ + if not self.documentObject.axes: + raise DesignSpaceDocumentError("No axes defined") + loc = {} + for dimensionElement in locationElement.findall(".dimension"): + dimName = dimensionElement.attrib.get("name") + if self._strictAxisNames and dimName not in self.axisDefaults: + # In case the document contains no axis definitions, + self.log.warning("Location with undefined axis: \"%s\".", dimName) + continue + xValue = yValue = None + try: + xValue = dimensionElement.attrib.get('xvalue') + xValue = float(xValue) + except ValueError: + self.log.warning("KeyError in readLocation xValue %3.3f", xValue) + try: + yValue = dimensionElement.attrib.get('yvalue') + if yValue is not None: + yValue = float(yValue) + except ValueError: + pass + if yValue is not None: + loc[dimName] = (xValue, yValue) + else: + loc[dimName] = xValue + return loc + + def readInstances(self, makeGlyphs=True, makeKerning=True, makeInfo=True): + instanceElements = self.root.findall('.instances/instance') + for instanceElement in instanceElements: + self._readSingleInstanceElement(instanceElement, makeGlyphs=makeGlyphs, makeKerning=makeKerning, makeInfo=makeInfo) + + def _readSingleInstanceElement(self, instanceElement, makeGlyphs=True, makeKerning=True, makeInfo=True): + filename = instanceElement.attrib.get('filename') + if filename is not None: + instancePath = os.path.join(os.path.dirname(self.documentObject.path), filename) + else: + instancePath = None + instanceObject = self.instanceDescriptorClass() + instanceObject.path = instancePath # absolute path to the instance + instanceObject.filename = filename # path as it is stored in the document + name = instanceElement.attrib.get("name") + if name is not None: + instanceObject.name = name + familyname = instanceElement.attrib.get('familyname') + if familyname is not None: + instanceObject.familyName = familyname + stylename = instanceElement.attrib.get('stylename') + if stylename is not None: + instanceObject.styleName = stylename + postScriptFontName = instanceElement.attrib.get('postscriptfontname') + if postScriptFontName is not None: + instanceObject.postScriptFontName = postScriptFontName + styleMapFamilyName = instanceElement.attrib.get('stylemapfamilyname') + if styleMapFamilyName is not None: + instanceObject.styleMapFamilyName = styleMapFamilyName + styleMapStyleName = instanceElement.attrib.get('stylemapstylename') + if styleMapStyleName is not None: + instanceObject.styleMapStyleName = styleMapStyleName + # read localised names + for styleNameElement in instanceElement.findall('stylename'): + for key, lang in styleNameElement.items(): + if key == XML_LANG: + styleName = styleNameElement.text + instanceObject.setStyleName(styleName, lang) + for familyNameElement in instanceElement.findall('familyname'): + for key, lang in familyNameElement.items(): + if key == XML_LANG: + familyName = familyNameElement.text + instanceObject.setFamilyName(familyName, lang) + for styleMapStyleNameElement in instanceElement.findall('stylemapstylename'): + for key, lang in styleMapStyleNameElement.items(): + if key == XML_LANG: + styleMapStyleName = styleMapStyleNameElement.text + instanceObject.setStyleMapStyleName(styleMapStyleName, lang) + for styleMapFamilyNameElement in instanceElement.findall('stylemapfamilyname'): + for key, lang in styleMapFamilyNameElement.items(): + if key == XML_LANG: + styleMapFamilyName = styleMapFamilyNameElement.text + instanceObject.setStyleMapFamilyName(styleMapFamilyName, lang) + instanceLocation = self.locationFromElement(instanceElement) + if instanceLocation is not None: + instanceObject.location = instanceLocation + for glyphElement in instanceElement.findall('.glyphs/glyph'): + self.readGlyphElement(glyphElement, instanceObject) + for infoElement in instanceElement.findall("info"): + self.readInfoElement(infoElement, instanceObject) + for libElement in instanceElement.findall('lib'): + self.readLibElement(libElement, instanceObject) + self.documentObject.instances.append(instanceObject) + + def readLibElement(self, libElement, instanceObject): + """Read the lib element for the given instance.""" + instanceObject.lib = from_plist(libElement[0]) + + def readInfoElement(self, infoElement, instanceObject): + """ Read the info element.""" + instanceObject.info = True + + def readKerningElement(self, kerningElement, instanceObject): + """ Read the kerning element.""" + kerningLocation = self.locationFromElement(kerningElement) + instanceObject.addKerning(kerningLocation) + + def readGlyphElement(self, glyphElement, instanceObject): + """ + Read the glyph element. + + + + + + + This is an instance from an anisotropic interpolation. + + + """ + glyphData = {} + glyphName = glyphElement.attrib.get('name') + if glyphName is None: + raise DesignSpaceDocumentError("Glyph object without name attribute") + mute = glyphElement.attrib.get("mute") + if mute == "1": + glyphData['mute'] = True + # unicode + unicodes = glyphElement.attrib.get('unicode') + if unicodes is not None: + try: + unicodes = [int(u, 16) for u in unicodes.split(" ")] + glyphData['unicodes'] = unicodes + except ValueError: + raise DesignSpaceDocumentError("unicode values %s are not integers" % unicodes) + + for noteElement in glyphElement.findall('.note'): + glyphData['note'] = noteElement.text + break + instanceLocation = self.locationFromElement(glyphElement) + if instanceLocation is not None: + glyphData['instanceLocation'] = instanceLocation + glyphSources = None + for masterElement in glyphElement.findall('.masters/master'): + fontSourceName = masterElement.attrib.get('source') + sourceLocation = self.locationFromElement(masterElement) + masterGlyphName = masterElement.attrib.get('glyphname') + if masterGlyphName is None: + # if we don't read a glyphname, use the one we have + masterGlyphName = glyphName + d = dict(font=fontSourceName, + location=sourceLocation, + glyphName=masterGlyphName) + if glyphSources is None: + glyphSources = [] + glyphSources.append(d) + if glyphSources is not None: + glyphData['masters'] = glyphSources + instanceObject.glyphs[glyphName] = glyphData + + def readLib(self): + """Read the lib element for the whole document.""" + for libElement in self.root.findall(".lib"): + self.documentObject.lib = from_plist(libElement[0]) + + +class DesignSpaceDocument(LogMixin): + """ Read, write data from the designspace file""" + def __init__(self, readerClass=None, writerClass=None): + self.path = None + self.filename = None + """String, optional. When the document is read from the disk, this is + its original file name, i.e. the last part of its path. + + When the document is produced by a Python script and still only exists + in memory, the producing script can write here an indication of a + possible "good" filename, in case one wants to save the file somewhere. + """ + + self.formatVersion = None + self.sources = [] + self.instances = [] + self.axes = [] + self.rules = [] + self.default = None # name of the default master + self.defaultLoc = None + + self.lib = {} + """Custom data associated with the whole document.""" + + # + if readerClass is not None: + self.readerClass = readerClass + else: + self.readerClass = BaseDocReader + if writerClass is not None: + self.writerClass = writerClass + else: + self.writerClass = BaseDocWriter + + def read(self, path): + self.path = path + self.filename = os.path.basename(path) + reader = self.readerClass(path, self) + reader.read() + if self.sources: + self.findDefault() + + def write(self, path): + self.path = path + self.filename = os.path.basename(path) + self.updatePaths() + writer = self.writerClass(path, self) + writer.write() + + def _posixRelativePath(self, otherPath): + relative = os.path.relpath(otherPath, os.path.dirname(self.path)) + return posix(relative) + + def updatePaths(self): + """ + Right before we save we need to identify and respond to the following situations: + In each descriptor, we have to do the right thing for the filename attribute. + + case 1. + descriptor.filename == None + descriptor.path == None + + -- action: + write as is, descriptors will not have a filename attr. + useless, but no reason to interfere. + + + case 2. + descriptor.filename == "../something" + descriptor.path == None + + -- action: + write as is. The filename attr should not be touched. + + + case 3. + descriptor.filename == None + descriptor.path == "~/absolute/path/there" + + -- action: + calculate the relative path for filename. + We're not overwriting some other value for filename, it should be fine + + + case 4. + descriptor.filename == '../somewhere' + descriptor.path == "~/absolute/path/there" + + -- action: + there is a conflict between the given filename, and the path. + So we know where the file is relative to the document. + Can't guess why they're different, we just choose for path to be correct and update filename. + + + """ + for descriptor in self.sources + self.instances: + # check what the relative path really should be? + expectedFilename = None + if descriptor.path is not None and self.path is not None: + expectedFilename = self._posixRelativePath(descriptor.path) + + # 3 + if descriptor.filename is None and descriptor.path is not None and self.path is not None: + descriptor.filename = self._posixRelativePath(descriptor.path) + continue + + # 4 + if descriptor.filename is not None and descriptor.path is not None and self.path is not None: + if descriptor.filename is not expectedFilename: + descriptor.filename = expectedFilename + + def addSource(self, sourceDescriptor): + self.sources.append(sourceDescriptor) + + def addInstance(self, instanceDescriptor): + self.instances.append(instanceDescriptor) + + def addAxis(self, axisDescriptor): + self.axes.append(axisDescriptor) + + def addRule(self, ruleDescriptor): + self.rules.append(ruleDescriptor) + + def newDefaultLocation(self): + # Without OrderedDict, output XML would be non-deterministic. + # https://github.com/LettError/designSpaceDocument/issues/10 + loc = collections.OrderedDict() + for axisDescriptor in self.axes: + loc[axisDescriptor.name] = axisDescriptor.default + return loc + + def updateFilenameFromPath(self, masters=True, instances=True, force=False): + # set a descriptor filename attr from the path and this document path + # if the filename attribute is not None: skip it. + if masters: + for descriptor in self.sources: + if descriptor.filename is not None and not force: + continue + if self.path is not None: + descriptor.filename = self._posixRelativePath(descriptor.path) + if instances: + for descriptor in self.instances: + if descriptor.filename is not None and not force: + continue + if self.path is not None: + descriptor.filename = self._posixRelativePath(descriptor.path) + + def newAxisDescriptor(self): + # Ask the writer class to make us a new axisDescriptor + return self.writerClass.getAxisDecriptor() + + def newSourceDescriptor(self): + # Ask the writer class to make us a new sourceDescriptor + return self.writerClass.getSourceDescriptor() + + def newInstanceDescriptor(self): + # Ask the writer class to make us a new instanceDescriptor + return self.writerClass.getInstanceDescriptor() + + def getAxisOrder(self): + names = [] + for axisDescriptor in self.axes: + names.append(axisDescriptor.name) + return names + + def getAxis(self, name): + for axisDescriptor in self.axes: + if axisDescriptor.name == name: + return axisDescriptor + return None + + def findDefault(self): + # new default finder + # take the sourcedescriptor with the location at all the defaults + # if we can't find it, return None, let someone else figure it out + self.default = None + for sourceDescriptor in self.sources: + if sourceDescriptor.location == self.defaultLoc: + # we choose you! + self.default = sourceDescriptor + return sourceDescriptor + return None + + def normalizeLocation(self, location): + # adapted from fontTools.varlib.models.normalizeLocation because: + # - this needs to work with axis names, not tags + # - this needs to accomodate anisotropic locations + # - the axes are stored differently here, it's just math + new = {} + for axis in self.axes: + if axis.name not in location: + # skipping this dimension it seems + continue + v = location.get(axis.name, axis.default) + if type(v) == tuple: + v = v[0] + if v == axis.default: + v = 0.0 + elif v < axis.default: + if axis.default == axis.minimum: + v = 0.0 + else: + v = (max(v, axis.minimum) - axis.default) / (axis.default - axis.minimum) + else: + if axis.default == axis.maximum: + v = 0.0 + else: + v = (min(v, axis.maximum) - axis.default) / (axis.maximum - axis.default) + new[axis.name] = v + return new + + def normalize(self): + # Normalise the geometry of this designspace: + # scale all the locations of all masters and instances to the -1 - 0 - 1 value. + # we need the axis data to do the scaling, so we do those last. + # masters + for item in self.sources: + item.location = self.normalizeLocation(item.location) + # instances + for item in self.instances: + # glyph masters for this instance + for _, glyphData in item.glyphs.items(): + glyphData['instanceLocation'] = self.normalizeLocation(glyphData['instanceLocation']) + for glyphMaster in glyphData['masters']: + glyphMaster['location'] = self.normalizeLocation(glyphMaster['location']) + item.location = self.normalizeLocation(item.location) + # the axes + for axis in self.axes: + # scale the map first + newMap = [] + for inputValue, outputValue in axis.map: + newOutputValue = self.normalizeLocation({axis.name: outputValue}).get(axis.name) + newMap.append((inputValue, newOutputValue)) + if newMap: + axis.map = newMap + # finally the axis values + minimum = self.normalizeLocation({axis.name: axis.minimum}).get(axis.name) + maximum = self.normalizeLocation({axis.name: axis.maximum}).get(axis.name) + default = self.normalizeLocation({axis.name: axis.default}).get(axis.name) + # and set them in the axis.minimum + axis.minimum = minimum + axis.maximum = maximum + axis.default = default + # now the rules + for rule in self.rules: + newConditionSets = [] + for conditions in rule.conditionSets: + newConditions = [] + for cond in conditions: + if cond.get('minimum') is not None: + minimum = self.normalizeLocation({cond['name']: cond['minimum']}).get(cond['name']) + else: + minimum = None + if cond.get('maximum') is not None: + maximum = self.normalizeLocation({cond['name']: cond['maximum']}).get(cond['name']) + else: + maximum = None + newConditions.append(dict(name=cond['name'], minimum=minimum, maximum=maximum)) + newConditionSets.append(newConditions) + rule.conditionSets = newConditionSets diff --git a/Lib/fontTools/encodings/MacRoman.py b/Lib/fontTools/encodings/MacRoman.py index bfeb0d5..43c58eb 100644 --- a/Lib/fontTools/encodings/MacRoman.py +++ b/Lib/fontTools/encodings/MacRoman.py @@ -1,37 +1,39 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * + MacRoman = [ - 'NUL', 'Eth', 'eth', 'Lslash', 'lslash', 'Scaron', 'scaron', 'Yacute', - 'yacute', 'HT', 'LF', 'Thorn', 'thorn', 'CR', 'Zcaron', 'zcaron', 'DLE', 'DC1', - 'DC2', 'DC3', 'DC4', 'onehalf', 'onequarter', 'onesuperior', 'threequarters', - 'threesuperior', 'twosuperior', 'brokenbar', 'minus', 'multiply', 'RS', 'US', - 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent', 'ampersand', - 'quotesingle', 'parenleft', 'parenright', 'asterisk', 'plus', 'comma', - 'hyphen', 'period', 'slash', 'zero', 'one', 'two', 'three', 'four', 'five', - 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less', 'equal', - 'greater', 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', - 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', - 'bracketleft', 'backslash', 'bracketright', 'asciicircum', 'underscore', - 'grave', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', - 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', - 'braceright', 'asciitilde', 'DEL', 'Adieresis', 'Aring', 'Ccedilla', 'Eacute', - 'Ntilde', 'Odieresis', 'Udieresis', 'aacute', 'agrave', 'acircumflex', - 'adieresis', 'atilde', 'aring', 'ccedilla', 'eacute', 'egrave', 'ecircumflex', - 'edieresis', 'iacute', 'igrave', 'icircumflex', 'idieresis', 'ntilde', - 'oacute', 'ograve', 'ocircumflex', 'odieresis', 'otilde', 'uacute', 'ugrave', - 'ucircumflex', 'udieresis', 'dagger', 'degree', 'cent', 'sterling', 'section', - 'bullet', 'paragraph', 'germandbls', 'registered', 'copyright', 'trademark', - 'acute', 'dieresis', 'notequal', 'AE', 'Oslash', 'infinity', 'plusminus', - 'lessequal', 'greaterequal', 'yen', 'mu', 'partialdiff', 'summation', - 'product', 'pi', 'integral', 'ordfeminine', 'ordmasculine', 'Omega', 'ae', - 'oslash', 'questiondown', 'exclamdown', 'logicalnot', 'radical', 'florin', - 'approxequal', 'Delta', 'guillemotleft', 'guillemotright', 'ellipsis', - 'nbspace', 'Agrave', 'Atilde', 'Otilde', 'OE', 'oe', 'endash', 'emdash', - 'quotedblleft', 'quotedblright', 'quoteleft', 'quoteright', 'divide', 'lozenge', - 'ydieresis', 'Ydieresis', 'fraction', 'currency', 'guilsinglleft', - 'guilsinglright', 'fi', 'fl', 'daggerdbl', 'periodcentered', 'quotesinglbase', - 'quotedblbase', 'perthousand', 'Acircumflex', 'Ecircumflex', 'Aacute', - 'Edieresis', 'Egrave', 'Iacute', 'Icircumflex', 'Idieresis', 'Igrave', 'Oacute', - 'Ocircumflex', 'apple', 'Ograve', 'Uacute', 'Ucircumflex', 'Ugrave', 'dotlessi', - 'circumflex', 'tilde', 'macron', 'breve', 'dotaccent', 'ring', 'cedilla', + 'NUL', 'Eth', 'eth', 'Lslash', 'lslash', 'Scaron', 'scaron', 'Yacute', + 'yacute', 'HT', 'LF', 'Thorn', 'thorn', 'CR', 'Zcaron', 'zcaron', 'DLE', 'DC1', + 'DC2', 'DC3', 'DC4', 'onehalf', 'onequarter', 'onesuperior', 'threequarters', + 'threesuperior', 'twosuperior', 'brokenbar', 'minus', 'multiply', 'RS', 'US', + 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent', 'ampersand', + 'quotesingle', 'parenleft', 'parenright', 'asterisk', 'plus', 'comma', + 'hyphen', 'period', 'slash', 'zero', 'one', 'two', 'three', 'four', 'five', + 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less', 'equal', + 'greater', 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'bracketleft', 'backslash', 'bracketright', 'asciicircum', 'underscore', + 'grave', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', + 'braceright', 'asciitilde', 'DEL', 'Adieresis', 'Aring', 'Ccedilla', 'Eacute', + 'Ntilde', 'Odieresis', 'Udieresis', 'aacute', 'agrave', 'acircumflex', + 'adieresis', 'atilde', 'aring', 'ccedilla', 'eacute', 'egrave', 'ecircumflex', + 'edieresis', 'iacute', 'igrave', 'icircumflex', 'idieresis', 'ntilde', + 'oacute', 'ograve', 'ocircumflex', 'odieresis', 'otilde', 'uacute', 'ugrave', + 'ucircumflex', 'udieresis', 'dagger', 'degree', 'cent', 'sterling', 'section', + 'bullet', 'paragraph', 'germandbls', 'registered', 'copyright', 'trademark', + 'acute', 'dieresis', 'notequal', 'AE', 'Oslash', 'infinity', 'plusminus', + 'lessequal', 'greaterequal', 'yen', 'mu', 'partialdiff', 'summation', + 'product', 'pi', 'integral', 'ordfeminine', 'ordmasculine', 'Omega', 'ae', + 'oslash', 'questiondown', 'exclamdown', 'logicalnot', 'radical', 'florin', + 'approxequal', 'Delta', 'guillemotleft', 'guillemotright', 'ellipsis', + 'nbspace', 'Agrave', 'Atilde', 'Otilde', 'OE', 'oe', 'endash', 'emdash', + 'quotedblleft', 'quotedblright', 'quoteleft', 'quoteright', 'divide', 'lozenge', + 'ydieresis', 'Ydieresis', 'fraction', 'currency', 'guilsinglleft', + 'guilsinglright', 'fi', 'fl', 'daggerdbl', 'periodcentered', 'quotesinglbase', + 'quotedblbase', 'perthousand', 'Acircumflex', 'Ecircumflex', 'Aacute', + 'Edieresis', 'Egrave', 'Iacute', 'Icircumflex', 'Idieresis', 'Igrave', 'Oacute', + 'Ocircumflex', 'apple', 'Ograve', 'Uacute', 'Ucircumflex', 'Ugrave', 'dotlessi', + 'circumflex', 'tilde', 'macron', 'breve', 'dotaccent', 'ring', 'cedilla', 'hungarumlaut', 'ogonek', 'caron' ] - diff --git a/Lib/fontTools/encodings/StandardEncoding.py b/Lib/fontTools/encodings/StandardEncoding.py index 810b2a0..dc01ef8 100644 --- a/Lib/fontTools/encodings/StandardEncoding.py +++ b/Lib/fontTools/encodings/StandardEncoding.py @@ -1,3 +1,6 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * + StandardEncoding = [ '.notdef', '.notdef', '.notdef', '.notdef', '.notdef', '.notdef', '.notdef', '.notdef', '.notdef', '.notdef', diff --git a/Lib/fontTools/encodings/__init__.py b/Lib/fontTools/encodings/__init__.py index e001bb2..3f9abc9 100644 --- a/Lib/fontTools/encodings/__init__.py +++ b/Lib/fontTools/encodings/__init__.py @@ -1,3 +1,4 @@ -"""Empty __init__.py file to signal Python this directory is a package. -(It can't be completely empty since WinZip seems to skip empty files.) -""" +"""Empty __init__.py file to signal Python this directory is a package.""" + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * diff --git a/Lib/fontTools/encodings/codecs.py b/Lib/fontTools/encodings/codecs.py new file mode 100644 index 0000000..30e4691 --- /dev/null +++ b/Lib/fontTools/encodings/codecs.py @@ -0,0 +1,135 @@ +"""Extend the Python codecs module with a few encodings that are used in OpenType (name table) +but missing from Python. See https://github.com/behdad/fonttools/issues/236 for details.""" + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +import codecs +import encodings + +class ExtendCodec(codecs.Codec): + + def __init__(self, name, base_encoding, mapping): + self.name = name + self.base_encoding = base_encoding + self.mapping = mapping + self.reverse = {v:k for k,v in mapping.items()} + self.max_len = max(len(v) for v in mapping.values()) + self.info = codecs.CodecInfo(name=self.name, encode=self.encode, decode=self.decode) + codecs.register_error(name, self.error) + + def encode(self, input, errors='strict'): + assert errors == 'strict' + #return codecs.encode(input, self.base_encoding, self.name), len(input) + + # The above line could totally be all we needed, relying on the error + # handling to replace the unencodable Unicode characters with our extended + # byte sequences. + # + # However, there seems to be a design bug in Python (probably intentional): + # the error handler for encoding is supposed to return a **Unicode** character, + # that then needs to be encodable itself... Ugh. + # + # So we implement what codecs.encode() should have been doing: which is expect + # error handler to return bytes() to be added to the output. + # + # This seems to have been fixed in Python 3.3. We should try using that and + # use fallback only if that failed. + # https://docs.python.org/3.3/library/codecs.html#codecs.register_error + + length = len(input) + out = b'' + while input: + try: + part = codecs.encode(input, self.base_encoding) + out += part + input = '' # All converted + except UnicodeEncodeError as e: + # Convert the correct part + out += codecs.encode(input[:e.start], self.base_encoding) + replacement, pos = self.error(e) + out += replacement + input = input[pos:] + return out, length + + def decode(self, input, errors='strict'): + assert errors == 'strict' + return codecs.decode(input, self.base_encoding, self.name), len(input) + + def error(self, e): + if isinstance(e, UnicodeDecodeError): + for end in range(e.start + 1, e.end + 1): + s = e.object[e.start:end] + if s in self.mapping: + return self.mapping[s], end + elif isinstance(e, UnicodeEncodeError): + for end in range(e.start + 1, e.start + self.max_len + 1): + s = e.object[e.start:end] + if s in self.reverse: + return self.reverse[s], end + e.encoding = self.name + raise e + + +_extended_encodings = { + "x_mac_japanese_ttx": ("shift_jis", { + b"\xFC": unichr(0x007C), + b"\x7E": unichr(0x007E), + b"\x80": unichr(0x005C), + b"\xA0": unichr(0x00A0), + b"\xFD": unichr(0x00A9), + b"\xFE": unichr(0x2122), + b"\xFF": unichr(0x2026), + }), + "x_mac_trad_chinese_ttx": ("big5", { + b"\x80": unichr(0x005C), + b"\xA0": unichr(0x00A0), + b"\xFD": unichr(0x00A9), + b"\xFE": unichr(0x2122), + b"\xFF": unichr(0x2026), + }), + "x_mac_korean_ttx": ("euc_kr", { + b"\x80": unichr(0x00A0), + b"\x81": unichr(0x20A9), + b"\x82": unichr(0x2014), + b"\x83": unichr(0x00A9), + b"\xFE": unichr(0x2122), + b"\xFF": unichr(0x2026), + }), + "x_mac_simp_chinese_ttx": ("gb2312", { + b"\x80": unichr(0x00FC), + b"\xA0": unichr(0x00A0), + b"\xFD": unichr(0x00A9), + b"\xFE": unichr(0x2122), + b"\xFF": unichr(0x2026), + }), +} + +_cache = {} + +def search_function(name): + name = encodings.normalize_encoding(name) # Rather undocumented... + if name in _extended_encodings: + if name not in _cache: + base_encoding, mapping = _extended_encodings[name] + assert(name[-4:] == "_ttx") + # Python 2 didn't have any of the encodings that we are implementing + # in this file. Python 3 added aliases for the East Asian ones, mapping + # them "temporarily" to the same base encoding as us, with a comment + # suggesting that full implementation will appear some time later. + # As such, try the Python version of the x_mac_... first, if that is found, + # use *that* as our base encoding. This would make our encoding upgrade + # to the full encoding when and if Python finally implements that. + # http://bugs.python.org/issue24041 + base_encodings = [name[:-4], base_encoding] + for base_encoding in base_encodings: + try: + codecs.lookup(base_encoding) + except LookupError: + continue + _cache[name] = ExtendCodec(name, base_encoding, mapping) + break + return _cache[name].info + + return None + +codecs.register(search_function) diff --git a/Lib/fontTools/feaLib/__init__.py b/Lib/fontTools/feaLib/__init__.py new file mode 100644 index 0000000..ae532cd --- /dev/null +++ b/Lib/fontTools/feaLib/__init__.py @@ -0,0 +1,4 @@ +"""fontTools.feaLib -- a package for dealing with OpenType feature files.""" + +# The structure of OpenType feature files is defined here: +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html diff --git a/Lib/fontTools/feaLib/__main__.py b/Lib/fontTools/feaLib/__main__.py new file mode 100644 index 0000000..e446db6 --- /dev/null +++ b/Lib/fontTools/feaLib/__main__.py @@ -0,0 +1,42 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.ttLib import TTFont +from fontTools.feaLib.builder import addOpenTypeFeatures +from fontTools import configLogger +from fontTools.misc.cliTools import makeOutputFileName +import sys +import argparse +import logging + + +log = logging.getLogger("fontTools.feaLib") + + +def main(args=None): + parser = argparse.ArgumentParser( + description="Use fontTools to compile OpenType feature files (*.fea).") + parser.add_argument( + "input_fea", metavar="FEATURES", help="Path to the feature file") + parser.add_argument( + "input_font", metavar="INPUT_FONT", help="Path to the input font") + parser.add_argument( + "-o", "--output", dest="output_font", metavar="OUTPUT_FONT", + help="Path to the output font.") + parser.add_argument( + "-v", "--verbose", help="increase the logger verbosity. Multiple -v " + "options are allowed.", action="count", default=0) + options = parser.parse_args(args) + + levels = ["WARNING", "INFO", "DEBUG"] + configLogger(level=levels[min(len(levels) - 1, options.verbose)]) + + output_font = options.output_font or makeOutputFileName(options.input_font) + log.info("Compiling features to '%s'" % (output_font)) + + font = TTFont(options.input_font) + addOpenTypeFeatures(font, options.input_fea) + font.save(output_font) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py new file mode 100644 index 0000000..3c2c5f6 --- /dev/null +++ b/Lib/fontTools/feaLib/ast.py @@ -0,0 +1,1346 @@ +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.misc.py23 import * +from fontTools.feaLib.error import FeatureLibError +from fontTools.misc.encodingTools import getEncoding +from collections import OrderedDict +import itertools + +SHIFT = " " * 4 + +__all__ = [ + 'AlternateSubstStatement', + 'Anchor', + 'AnchorDefinition', + 'AnonymousBlock', + 'AttachStatement', + 'BaseAxis', + 'Block', + 'BytesIO', + 'CVParametersNameStatement', + 'ChainContextPosStatement', + 'ChainContextSubstStatement', + 'CharacterStatement', + 'Comment', + 'CursivePosStatement', + 'Element', + 'Expression', + 'FeatureBlock', + 'FeatureFile', + 'FeatureLibError', + 'FeatureNameStatement', + 'FeatureReferenceStatement', + 'FontRevisionStatement', + 'GlyphClass', + 'GlyphClassDefStatement', + 'GlyphClassDefinition', + 'GlyphClassName', + 'GlyphName', + 'HheaField', + 'IgnorePosStatement', + 'IgnoreSubstStatement', + 'IncludeStatement', + 'LanguageStatement', + 'LanguageSystemStatement', + 'LigatureCaretByIndexStatement', + 'LigatureCaretByPosStatement', + 'LigatureSubstStatement', + 'LookupBlock', + 'LookupFlagStatement', + 'LookupReferenceStatement', + 'MarkBasePosStatement', + 'MarkClass', + 'MarkClassDefinition', + 'MarkClassName', + 'MarkLigPosStatement', + 'MarkMarkPosStatement', + 'MultipleSubstStatement', + 'NameRecord', + 'NestedBlock', + 'OS2Field', + 'OrderedDict', + 'PairPosStatement', + 'Py23Error', + 'ReverseChainSingleSubstStatement', + 'ScriptStatement', + 'SimpleNamespace', + 'SinglePosStatement', + 'SingleSubstStatement', + 'SizeParameters', + 'Statement', + 'StringIO', + 'SubtableStatement', + 'TableBlock', + 'Tag', + 'UnicodeIO', + 'ValueRecord', + 'ValueRecordDefinition', + 'VheaField', +] + + +def deviceToString(device): + if device is None: + return "" + else: + return "" % ", ".join("%d %d" % t for t in device) + + +fea_keywords = set([ + "anchor", "anchordef", "anon", "anonymous", + "by", + "contour", "cursive", + "device", + "enum", "enumerate", "excludedflt", "exclude_dflt", + "feature", "from", + "ignore", "ignorebaseglyphs", "ignoreligatures", "ignoremarks", + "include", "includedflt", "include_dflt", + "language", "languagesystem", "lookup", "lookupflag", + "mark", "markattachmenttype", "markclass", + "nameid", "null", + "parameters", "pos", "position", + "required", "righttoleft", "reversesub", "rsub", + "script", "sub", "substitute", "subtable", + "table", + "usemarkfilteringset", "useextension", "valuerecorddef"] +) + + +def asFea(g): + if hasattr(g, 'asFea'): + return g.asFea() + elif isinstance(g, tuple) and len(g) == 2: + return asFea(g[0]) + "-" + asFea(g[1]) # a range + elif g.lower() in fea_keywords: + return "\\" + g + else: + return g + + +class Element(object): + + def __init__(self, location=None): + self.location = location + + def build(self, builder): + pass + + def asFea(self, indent=""): + raise NotImplementedError + + def __str__(self): + return self.asFea() + + +class Statement(Element): + pass + + +class Expression(Element): + pass + + +class Comment(Element): + def __init__(self, text, location=None): + super(Comment, self).__init__(location) + self.text = text + + def asFea(self, indent=""): + return self.text + + +class GlyphName(Expression): + """A single glyph name, such as cedilla.""" + def __init__(self, glyph, location=None): + Expression.__init__(self, location) + self.glyph = glyph + + def glyphSet(self): + return (self.glyph,) + + def asFea(self, indent=""): + return str(self.glyph) + + +class GlyphClass(Expression): + """A glyph class, such as [acute cedilla grave].""" + def __init__(self, glyphs=None, location=None): + Expression.__init__(self, location) + self.glyphs = glyphs if glyphs is not None else [] + self.original = [] + self.curr = 0 + + def glyphSet(self): + return tuple(self.glyphs) + + def asFea(self, indent=""): + if len(self.original): + if self.curr < len(self.glyphs): + self.original.extend(self.glyphs[self.curr:]) + self.curr = len(self.glyphs) + return "[" + " ".join(map(asFea, self.original)) + "]" + else: + return "[" + " ".join(map(asFea, self.glyphs)) + "]" + + def extend(self, glyphs): + self.glyphs.extend(glyphs) + + def append(self, glyph): + self.glyphs.append(glyph) + + def add_range(self, start, end, glyphs): + if self.curr < len(self.glyphs): + self.original.extend(self.glyphs[self.curr:]) + self.original.append((start, end)) + self.glyphs.extend(glyphs) + self.curr = len(self.glyphs) + + def add_cid_range(self, start, end, glyphs): + if self.curr < len(self.glyphs): + self.original.extend(self.glyphs[self.curr:]) + self.original.append(("cid{:05d}".format(start), "cid{:05d}".format(end))) + self.glyphs.extend(glyphs) + self.curr = len(self.glyphs) + + def add_class(self, gc): + if self.curr < len(self.glyphs): + self.original.extend(self.glyphs[self.curr:]) + self.original.append(gc) + self.glyphs.extend(gc.glyphSet()) + self.curr = len(self.glyphs) + + +class GlyphClassName(Expression): + """A glyph class name, such as @FRENCH_MARKS.""" + def __init__(self, glyphclass, location=None): + Expression.__init__(self, location) + assert isinstance(glyphclass, GlyphClassDefinition) + self.glyphclass = glyphclass + + def glyphSet(self): + return tuple(self.glyphclass.glyphSet()) + + def asFea(self, indent=""): + return "@" + self.glyphclass.name + + +class MarkClassName(Expression): + """A mark class name, such as @FRENCH_MARKS defined with markClass.""" + def __init__(self, markClass, location=None): + Expression.__init__(self, location) + assert isinstance(markClass, MarkClass) + self.markClass = markClass + + def glyphSet(self): + return self.markClass.glyphSet() + + def asFea(self, indent=""): + return "@" + self.markClass.name + + +class AnonymousBlock(Statement): + def __init__(self, tag, content, location=None): + Statement.__init__(self, location) + self.tag, self.content = tag, content + + def asFea(self, indent=""): + res = "anon {} {{\n".format(self.tag) + res += self.content + res += "}} {};\n\n".format(self.tag) + return res + + +class Block(Statement): + def __init__(self, location=None): + Statement.__init__(self, location) + self.statements = [] + + def build(self, builder): + for s in self.statements: + s.build(builder) + + def asFea(self, indent=""): + indent += SHIFT + return indent + ("\n" + indent).join( + [s.asFea(indent=indent) for s in self.statements]) + "\n" + + +class FeatureFile(Block): + def __init__(self): + Block.__init__(self, location=None) + self.markClasses = {} # name --> ast.MarkClass + + def asFea(self, indent=""): + return "\n".join(s.asFea(indent=indent) for s in self.statements) + + +class FeatureBlock(Block): + def __init__(self, name, use_extension=False, location=None): + Block.__init__(self, location) + self.name, self.use_extension = name, use_extension + + def build(self, builder): + # TODO(sascha): Handle use_extension. + builder.start_feature(self.location, self.name) + # language exclude_dflt statements modify builder.features_ + # limit them to this block with temporary builder.features_ + features = builder.features_ + builder.features_ = {} + Block.build(self, builder) + for key, value in builder.features_.items(): + features.setdefault(key, []).extend(value) + builder.features_ = features + builder.end_feature() + + def asFea(self, indent=""): + res = indent + "feature %s {\n" % self.name.strip() + res += Block.asFea(self, indent=indent) + res += indent + "} %s;\n" % self.name.strip() + return res + + +class NestedBlock(Block): + def __init__(self, tag, block_name, location=None): + Block.__init__(self, location) + self.tag = tag + self.block_name = block_name + + def build(self, builder): + Block.build(self, builder) + if self.block_name == "ParamUILabelNameID": + builder.add_to_cv_num_named_params(self.tag) + + def asFea(self, indent=""): + res = "{}{} {{\n".format(indent, self.block_name) + res += Block.asFea(self, indent=indent) + res += "{}}};\n".format(indent) + return res + + +class LookupBlock(Block): + def __init__(self, name, use_extension=False, location=None): + Block.__init__(self, location) + self.name, self.use_extension = name, use_extension + + def build(self, builder): + # TODO(sascha): Handle use_extension. + builder.start_lookup_block(self.location, self.name) + Block.build(self, builder) + builder.end_lookup_block() + + def asFea(self, indent=""): + res = "lookup {} {{\n".format(self.name) + res += Block.asFea(self, indent=indent) + res += "{}}} {};\n".format(indent, self.name) + return res + + +class TableBlock(Block): + def __init__(self, name, location=None): + Block.__init__(self, location) + self.name = name + + def asFea(self, indent=""): + res = "table {} {{\n".format(self.name.strip()) + res += super(TableBlock, self).asFea(indent=indent) + res += "}} {};\n".format(self.name.strip()) + return res + + +class GlyphClassDefinition(Statement): + """Example: @UPPERCASE = [A-Z];""" + def __init__(self, name, glyphs, location=None): + Statement.__init__(self, location) + self.name = name + self.glyphs = glyphs + + def glyphSet(self): + return tuple(self.glyphs.glyphSet()) + + def asFea(self, indent=""): + return "@" + self.name + " = " + self.glyphs.asFea() + ";" + + +class GlyphClassDefStatement(Statement): + """Example: GlyphClassDef @UPPERCASE, [B], [C], [D];""" + def __init__(self, baseGlyphs, markGlyphs, ligatureGlyphs, + componentGlyphs, location=None): + Statement.__init__(self, location) + self.baseGlyphs, self.markGlyphs = (baseGlyphs, markGlyphs) + self.ligatureGlyphs = ligatureGlyphs + self.componentGlyphs = componentGlyphs + + def build(self, builder): + base = self.baseGlyphs.glyphSet() if self.baseGlyphs else tuple() + liga = self.ligatureGlyphs.glyphSet() \ + if self.ligatureGlyphs else tuple() + mark = self.markGlyphs.glyphSet() if self.markGlyphs else tuple() + comp = (self.componentGlyphs.glyphSet() + if self.componentGlyphs else tuple()) + builder.add_glyphClassDef(self.location, base, liga, mark, comp) + + def asFea(self, indent=""): + return "GlyphClassDef {}, {}, {}, {};".format( + self.baseGlyphs.asFea() if self.baseGlyphs else "", + self.ligatureGlyphs.asFea() if self.ligatureGlyphs else "", + self.markGlyphs.asFea() if self.markGlyphs else "", + self.componentGlyphs.asFea() if self.componentGlyphs else "") + + +# While glyph classes can be defined only once, the feature file format +# allows expanding mark classes with multiple definitions, each using +# different glyphs and anchors. The following are two MarkClassDefinitions +# for the same MarkClass: +# markClass [acute grave] @FRENCH_ACCENTS; +# markClass [cedilla] @FRENCH_ACCENTS; +class MarkClass(object): + def __init__(self, name): + self.name = name + self.definitions = [] + self.glyphs = OrderedDict() # glyph --> ast.MarkClassDefinitions + + def addDefinition(self, definition): + assert isinstance(definition, MarkClassDefinition) + self.definitions.append(definition) + for glyph in definition.glyphSet(): + if glyph in self.glyphs: + otherLoc = self.glyphs[glyph].location + if otherLoc is None: + end = "" + else: + end = " at %s:%d:%d" % ( + otherLoc[0], otherLoc[1], otherLoc[2]) + raise FeatureLibError( + "Glyph %s already defined%s" % (glyph, end), + definition.location) + self.glyphs[glyph] = definition + + def glyphSet(self): + return tuple(self.glyphs.keys()) + + def asFea(self, indent=""): + res = "\n".join(d.asFea(indent=indent) for d in self.definitions) + return res + + +class MarkClassDefinition(Statement): + def __init__(self, markClass, anchor, glyphs, location=None): + Statement.__init__(self, location) + assert isinstance(markClass, MarkClass) + assert isinstance(anchor, Anchor) and isinstance(glyphs, Expression) + self.markClass, self.anchor, self.glyphs = markClass, anchor, glyphs + + def glyphSet(self): + return self.glyphs.glyphSet() + + def asFea(self, indent=""): + return "{}markClass {} {} @{};".format( + indent, self.glyphs.asFea(), self.anchor.asFea(), + self.markClass.name) + + +class AlternateSubstStatement(Statement): + def __init__(self, prefix, glyph, suffix, replacement, location=None): + Statement.__init__(self, location) + self.prefix, self.glyph, self.suffix = (prefix, glyph, suffix) + self.replacement = replacement + + def build(self, builder): + glyph = self.glyph.glyphSet() + assert len(glyph) == 1, glyph + glyph = list(glyph)[0] + prefix = [p.glyphSet() for p in self.prefix] + suffix = [s.glyphSet() for s in self.suffix] + replacement = self.replacement.glyphSet() + builder.add_alternate_subst(self.location, prefix, glyph, suffix, + replacement) + + def asFea(self, indent=""): + res = "sub " + if len(self.prefix) or len(self.suffix): + if len(self.prefix): + res += " ".join(map(asFea, self.prefix)) + " " + res += asFea(self.glyph) + "'" # even though we really only use 1 + if len(self.suffix): + res += " " + " ".join(map(asFea, self.suffix)) + else: + res += asFea(self.glyph) + res += " from " + res += asFea(self.replacement) + res += ";" + return res + + +class Anchor(Expression): + def __init__(self, x, y, name=None, contourpoint=None, + xDeviceTable=None, yDeviceTable=None, location=None): + Expression.__init__(self, location) + self.name = name + self.x, self.y, self.contourpoint = x, y, contourpoint + self.xDeviceTable, self.yDeviceTable = xDeviceTable, yDeviceTable + + def asFea(self, indent=""): + if self.name is not None: + return "".format(self.name) + res = "" + exit = self.exitAnchor.asFea() if self.exitAnchor else "" + return "pos cursive {} {} {};".format(self.glyphclass.asFea(), entry, exit) + + +class FeatureReferenceStatement(Statement): + """Example: feature salt;""" + def __init__(self, featureName, location=None): + Statement.__init__(self, location) + self.location, self.featureName = (location, featureName) + + def build(self, builder): + builder.add_feature_reference(self.location, self.featureName) + + def asFea(self, indent=""): + return "feature {};".format(self.featureName) + + +class IgnorePosStatement(Statement): + def __init__(self, chainContexts, location=None): + Statement.__init__(self, location) + self.chainContexts = chainContexts + + def build(self, builder): + for prefix, glyphs, suffix in self.chainContexts: + prefix = [p.glyphSet() for p in prefix] + glyphs = [g.glyphSet() for g in glyphs] + suffix = [s.glyphSet() for s in suffix] + builder.add_chain_context_pos( + self.location, prefix, glyphs, suffix, []) + + def asFea(self, indent=""): + contexts = [] + for prefix, glyphs, suffix in self.chainContexts: + res = "" + if len(prefix) or len(suffix): + if len(prefix): + res += " ".join(map(asFea, prefix)) + " " + res += " ".join(g.asFea() + "'" for g in glyphs) + if len(suffix): + res += " " + " ".join(map(asFea, suffix)) + else: + res += " ".join(map(asFea, glyphs)) + contexts.append(res) + return "ignore pos " + ", ".join(contexts) + ";" + + +class IgnoreSubstStatement(Statement): + def __init__(self, chainContexts, location=None): + Statement.__init__(self, location) + self.chainContexts = chainContexts + + def build(self, builder): + for prefix, glyphs, suffix in self.chainContexts: + prefix = [p.glyphSet() for p in prefix] + glyphs = [g.glyphSet() for g in glyphs] + suffix = [s.glyphSet() for s in suffix] + builder.add_chain_context_subst( + self.location, prefix, glyphs, suffix, []) + + def asFea(self, indent=""): + contexts = [] + for prefix, glyphs, suffix in self.chainContexts: + res = "" + if len(prefix) or len(suffix): + if len(prefix): + res += " ".join(map(asFea, prefix)) + " " + res += " ".join(g.asFea() + "'" for g in glyphs) + if len(suffix): + res += " " + " ".join(map(asFea, suffix)) + else: + res += " ".join(map(asFea, glyphs)) + contexts.append(res) + return "ignore sub " + ", ".join(contexts) + ";" + + +class IncludeStatement(Statement): + def __init__(self, filename, location=None): + super(IncludeStatement, self).__init__(location) + self.filename = filename + + def build(self): + # TODO: consider lazy-loading the including parser/lexer? + raise FeatureLibError( + "Building an include statement is not implemented yet. " + "Instead, use Parser(..., followIncludes=True) for building.", + self.location) + + def asFea(self, indent=""): + return indent + "include(%s);" % self.filename + + +class LanguageStatement(Statement): + def __init__(self, language, include_default=True, required=False, + location=None): + Statement.__init__(self, location) + assert(len(language) == 4) + self.language = language + self.include_default = include_default + self.required = required + + def build(self, builder): + builder.set_language(location=self.location, language=self.language, + include_default=self.include_default, + required=self.required) + + def asFea(self, indent=""): + res = "language {}".format(self.language.strip()) + if not self.include_default: + res += " exclude_dflt" + if self.required: + res += " required" + res += ";" + return res + + +class LanguageSystemStatement(Statement): + def __init__(self, script, language, location=None): + Statement.__init__(self, location) + self.script, self.language = (script, language) + + def build(self, builder): + builder.add_language_system(self.location, self.script, self.language) + + def asFea(self, indent=""): + return "languagesystem {} {};".format(self.script, self.language.strip()) + + +class FontRevisionStatement(Statement): + def __init__(self, revision, location=None): + Statement.__init__(self, location) + self.revision = revision + + def build(self, builder): + builder.set_font_revision(self.location, self.revision) + + def asFea(self, indent=""): + return "FontRevision {:.3f};".format(self.revision) + + +class LigatureCaretByIndexStatement(Statement): + def __init__(self, glyphs, carets, location=None): + Statement.__init__(self, location) + self.glyphs, self.carets = (glyphs, carets) + + def build(self, builder): + glyphs = self.glyphs.glyphSet() + builder.add_ligatureCaretByIndex_(self.location, glyphs, set(self.carets)) + + def asFea(self, indent=""): + return "LigatureCaretByIndex {} {};".format( + self.glyphs.asFea(), " ".join(str(x) for x in self.carets)) + + +class LigatureCaretByPosStatement(Statement): + def __init__(self, glyphs, carets, location=None): + Statement.__init__(self, location) + self.glyphs, self.carets = (glyphs, carets) + + def build(self, builder): + glyphs = self.glyphs.glyphSet() + builder.add_ligatureCaretByPos_(self.location, glyphs, set(self.carets)) + + def asFea(self, indent=""): + return "LigatureCaretByPos {} {};".format( + self.glyphs.asFea(), " ".join(str(x) for x in self.carets)) + + +class LigatureSubstStatement(Statement): + def __init__(self, prefix, glyphs, suffix, replacement, + forceChain, location=None): + Statement.__init__(self, location) + self.prefix, self.glyphs, self.suffix = (prefix, glyphs, suffix) + self.replacement, self.forceChain = replacement, forceChain + + def build(self, builder): + prefix = [p.glyphSet() for p in self.prefix] + glyphs = [g.glyphSet() for g in self.glyphs] + suffix = [s.glyphSet() for s in self.suffix] + builder.add_ligature_subst( + self.location, prefix, glyphs, suffix, self.replacement, + self.forceChain) + + def asFea(self, indent=""): + res = "sub " + if len(self.prefix) or len(self.suffix) or self.forceChain: + if len(self.prefix): + res += " ".join(g.asFea() for g in self.prefix) + " " + res += " ".join(g.asFea() + "'" for g in self.glyphs) + if len(self.suffix): + res += " " + " ".join(g.asFea() for g in self.suffix) + else: + res += " ".join(g.asFea() for g in self.glyphs) + res += " by " + res += asFea(self.replacement) + res += ";" + return res + + +class LookupFlagStatement(Statement): + def __init__(self, value=0, markAttachment=None, markFilteringSet=None, + location=None): + Statement.__init__(self, location) + self.value = value + self.markAttachment = markAttachment + self.markFilteringSet = markFilteringSet + + def build(self, builder): + markAttach = None + if self.markAttachment is not None: + markAttach = self.markAttachment.glyphSet() + markFilter = None + if self.markFilteringSet is not None: + markFilter = self.markFilteringSet.glyphSet() + builder.set_lookup_flag(self.location, self.value, + markAttach, markFilter) + + def asFea(self, indent=""): + res = "lookupflag" + flags = ["RightToLeft", "IgnoreBaseGlyphs", "IgnoreLigatures", "IgnoreMarks"] + curr = 1 + for i in range(len(flags)): + if self.value & curr != 0: + res += " " + flags[i] + curr = curr << 1 + if self.markAttachment is not None: + res += " MarkAttachmentType {}".format(self.markAttachment.asFea()) + if self.markFilteringSet is not None: + res += " UseMarkFilteringSet {}".format(self.markFilteringSet.asFea()) + res += ";" + return res + + +class LookupReferenceStatement(Statement): + def __init__(self, lookup, location=None): + Statement.__init__(self, location) + self.location, self.lookup = (location, lookup) + + def build(self, builder): + builder.add_lookup_call(self.lookup.name) + + def asFea(self, indent=""): + return "lookup {};".format(self.lookup.name) + + +class MarkBasePosStatement(Statement): + def __init__(self, base, marks, location=None): + Statement.__init__(self, location) + self.base, self.marks = base, marks + + def build(self, builder): + builder.add_mark_base_pos(self.location, self.base.glyphSet(), self.marks) + + def asFea(self, indent=""): + res = "pos base {}".format(self.base.asFea()) + for a, m in self.marks: + res += " {} mark @{}".format(a.asFea(), m.name) + res += ";" + return res + + +class MarkLigPosStatement(Statement): + def __init__(self, ligatures, marks, location=None): + Statement.__init__(self, location) + self.ligatures, self.marks = ligatures, marks + + def build(self, builder): + builder.add_mark_lig_pos(self.location, self.ligatures.glyphSet(), self.marks) + + def asFea(self, indent=""): + res = "pos ligature {}".format(self.ligatures.asFea()) + ligs = [] + for l in self.marks: + temp = "" + if l is None or not len(l): + temp = " " + else: + for a, m in l: + temp += " {} mark @{}".format(a.asFea(), m.name) + ligs.append(temp) + res += ("\n" + indent + SHIFT + "ligComponent").join(ligs) + res += ";" + return res + + +class MarkMarkPosStatement(Statement): + def __init__(self, baseMarks, marks, location=None): + Statement.__init__(self, location) + self.baseMarks, self.marks = baseMarks, marks + + def build(self, builder): + builder.add_mark_mark_pos(self.location, self.baseMarks.glyphSet(), self.marks) + + def asFea(self, indent=""): + res = "pos mark {}".format(self.baseMarks.asFea()) + for a, m in self.marks: + res += " {} mark @{}".format(a.asFea(), m.name) + res += ";" + return res + + +class MultipleSubstStatement(Statement): + def __init__(self, prefix, glyph, suffix, replacement, location=None): + Statement.__init__(self, location) + self.prefix, self.glyph, self.suffix = prefix, glyph, suffix + self.replacement = replacement + + def build(self, builder): + prefix = [p.glyphSet() for p in self.prefix] + suffix = [s.glyphSet() for s in self.suffix] + builder.add_multiple_subst( + self.location, prefix, self.glyph, suffix, self.replacement) + + def asFea(self, indent=""): + res = "sub " + if len(self.prefix) or len(self.suffix): + if len(self.prefix): + res += " ".join(map(asFea, self.prefix)) + " " + res += asFea(self.glyph) + "'" + if len(self.suffix): + res += " " + " ".join(map(asFea, self.suffix)) + else: + res += asFea(self.glyph) + res += " by " + res += " ".join(map(asFea, self.replacement)) + res += ";" + return res + + +class PairPosStatement(Statement): + def __init__(self, glyphs1, valuerecord1, glyphs2, valuerecord2, + enumerated=False, location=None): + Statement.__init__(self, location) + self.enumerated = enumerated + self.glyphs1, self.valuerecord1 = glyphs1, valuerecord1 + self.glyphs2, self.valuerecord2 = glyphs2, valuerecord2 + + def build(self, builder): + if self.enumerated: + g = [self.glyphs1.glyphSet(), self.glyphs2.glyphSet()] + for glyph1, glyph2 in itertools.product(*g): + builder.add_specific_pair_pos( + self.location, glyph1, self.valuerecord1, + glyph2, self.valuerecord2) + return + + is_specific = (isinstance(self.glyphs1, GlyphName) and + isinstance(self.glyphs2, GlyphName)) + if is_specific: + builder.add_specific_pair_pos( + self.location, self.glyphs1.glyph, self.valuerecord1, + self.glyphs2.glyph, self.valuerecord2) + else: + builder.add_class_pair_pos( + self.location, self.glyphs1.glyphSet(), self.valuerecord1, + self.glyphs2.glyphSet(), self.valuerecord2) + + def asFea(self, indent=""): + res = "enum " if self.enumerated else "" + if self.valuerecord2: + res += "pos {} {} {} {};".format( + self.glyphs1.asFea(), self.valuerecord1.makeString(), + self.glyphs2.asFea(), self.valuerecord2.makeString()) + else: + res += "pos {} {} {};".format( + self.glyphs1.asFea(), self.glyphs2.asFea(), + self.valuerecord1.makeString()) + return res + + +class ReverseChainSingleSubstStatement(Statement): + def __init__(self, old_prefix, old_suffix, glyphs, replacements, + location=None): + Statement.__init__(self, location) + self.old_prefix, self.old_suffix = old_prefix, old_suffix + self.glyphs = glyphs + self.replacements = replacements + + def build(self, builder): + prefix = [p.glyphSet() for p in self.old_prefix] + suffix = [s.glyphSet() for s in self.old_suffix] + originals = self.glyphs[0].glyphSet() + replaces = self.replacements[0].glyphSet() + if len(replaces) == 1: + replaces = replaces * len(originals) + builder.add_reverse_chain_single_subst( + self.location, prefix, suffix, dict(zip(originals, replaces))) + + def asFea(self, indent=""): + res = "rsub " + if len(self.old_prefix) or len(self.old_suffix): + if len(self.old_prefix): + res += " ".join(asFea(g) for g in self.old_prefix) + " " + res += " ".join(asFea(g) + "'" for g in self.glyphs) + if len(self.old_suffix): + res += " " + " ".join(asFea(g) for g in self.old_suffix) + else: + res += " ".join(map(asFea, self.glyphs)) + res += " by {};".format(" ".join(asFea(g) for g in self.replacements)) + return res + + +class SingleSubstStatement(Statement): + def __init__(self, glyphs, replace, prefix, suffix, forceChain, + location=None): + Statement.__init__(self, location) + self.prefix, self.suffix = prefix, suffix + self.forceChain = forceChain + self.glyphs = glyphs + self.replacements = replace + + def build(self, builder): + prefix = [p.glyphSet() for p in self.prefix] + suffix = [s.glyphSet() for s in self.suffix] + originals = self.glyphs[0].glyphSet() + replaces = self.replacements[0].glyphSet() + if len(replaces) == 1: + replaces = replaces * len(originals) + builder.add_single_subst(self.location, prefix, suffix, + OrderedDict(zip(originals, replaces)), + self.forceChain) + + def asFea(self, indent=""): + res = "sub " + if len(self.prefix) or len(self.suffix) or self.forceChain: + if len(self.prefix): + res += " ".join(asFea(g) for g in self.prefix) + " " + res += " ".join(asFea(g) + "'" for g in self.glyphs) + if len(self.suffix): + res += " " + " ".join(asFea(g) for g in self.suffix) + else: + res += " ".join(asFea(g) for g in self.glyphs) + res += " by {};".format(" ".join(asFea(g) for g in self.replacements)) + return res + + +class ScriptStatement(Statement): + def __init__(self, script, location=None): + Statement.__init__(self, location) + self.script = script + + def build(self, builder): + builder.set_script(self.location, self.script) + + def asFea(self, indent=""): + return "script {};".format(self.script.strip()) + + +class SinglePosStatement(Statement): + def __init__(self, pos, prefix, suffix, forceChain, location=None): + Statement.__init__(self, location) + self.pos, self.prefix, self.suffix = pos, prefix, suffix + self.forceChain = forceChain + + def build(self, builder): + prefix = [p.glyphSet() for p in self.prefix] + suffix = [s.glyphSet() for s in self.suffix] + pos = [(g.glyphSet(), value) for g, value in self.pos] + builder.add_single_pos(self.location, prefix, suffix, + pos, self.forceChain) + + def asFea(self, indent=""): + res = "pos " + if len(self.prefix) or len(self.suffix) or self.forceChain: + if len(self.prefix): + res += " ".join(map(asFea, self.prefix)) + " " + res += " ".join([asFea(x[0]) + "'" + ( + (" " + x[1].makeString()) if x[1] else "") for x in self.pos]) + if len(self.suffix): + res += " " + " ".join(map(asFea, self.suffix)) + else: + res += " ".join([asFea(x[0]) + " " + + (x[1].makeString() if x[1] else "") for x in self.pos]) + res += ";" + return res + + +class SubtableStatement(Statement): + def __init__(self, location=None): + Statement.__init__(self, location) + + def asFea(self, indent=""): + return indent + "subtable;" + + +class ValueRecord(Expression): + def __init__(self, xPlacement=None, yPlacement=None, + xAdvance=None, yAdvance=None, + xPlaDevice=None, yPlaDevice=None, + xAdvDevice=None, yAdvDevice=None, + vertical=False, location=None): + Expression.__init__(self, location) + self.xPlacement, self.yPlacement = (xPlacement, yPlacement) + self.xAdvance, self.yAdvance = (xAdvance, yAdvance) + self.xPlaDevice, self.yPlaDevice = (xPlaDevice, yPlaDevice) + self.xAdvDevice, self.yAdvDevice = (xAdvDevice, yAdvDevice) + self.vertical = vertical + + def __eq__(self, other): + return (self.xPlacement == other.xPlacement and + self.yPlacement == other.yPlacement and + self.xAdvance == other.xAdvance and + self.yAdvance == other.yAdvance and + self.xPlaDevice == other.xPlaDevice and + self.xAdvDevice == other.xAdvDevice) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return (hash(self.xPlacement) ^ hash(self.yPlacement) ^ + hash(self.xAdvance) ^ hash(self.yAdvance) ^ + hash(self.xPlaDevice) ^ hash(self.yPlaDevice) ^ + hash(self.xAdvDevice) ^ hash(self.yAdvDevice)) + + def makeString(self, vertical=None): + x, y = self.xPlacement, self.yPlacement + xAdvance, yAdvance = self.xAdvance, self.yAdvance + xPlaDevice, yPlaDevice = self.xPlaDevice, self.yPlaDevice + xAdvDevice, yAdvDevice = self.xAdvDevice, self.yAdvDevice + if vertical is None: + vertical = self.vertical + + # Try format A, if possible. + if x is None and y is None: + if xAdvance is None and vertical: + return str(yAdvance) + elif yAdvance is None and not vertical: + return str(xAdvance) + + # Try format B, if possible. + if (xPlaDevice is None and yPlaDevice is None and + xAdvDevice is None and yAdvDevice is None): + return "<%s %s %s %s>" % (x, y, xAdvance, yAdvance) + + # Last resort is format C. + return "<%s %s %s %s %s %s %s %s>" % ( + x, y, xAdvance, yAdvance, + deviceToString(xPlaDevice), deviceToString(yPlaDevice), + deviceToString(xAdvDevice), deviceToString(yAdvDevice)) + + +class ValueRecordDefinition(Statement): + def __init__(self, name, value, location=None): + Statement.__init__(self, location) + self.name = name + self.value = value + + def asFea(self, indent=""): + return "valueRecordDef {} {};".format(self.value.asFea(), self.name) + + +def simplify_name_attributes(pid, eid, lid): + if pid == 3 and eid == 1 and lid == 1033: + return "" + elif pid == 1 and eid == 0 and lid == 0: + return "1" + else: + return "{} {} {}".format(pid, eid, lid) + + +class NameRecord(Statement): + def __init__(self, nameID, platformID, platEncID, langID, string, + location=None): + Statement.__init__(self, location) + self.nameID = nameID + self.platformID = platformID + self.platEncID = platEncID + self.langID = langID + self.string = string + + def build(self, builder): + builder.add_name_record( + self.location, self.nameID, self.platformID, + self.platEncID, self.langID, self.string) + + def asFea(self, indent=""): + def escape(c, escape_pattern): + # Also escape U+0022 QUOTATION MARK and U+005C REVERSE SOLIDUS + if c >= 0x20 and c <= 0x7E and c not in (0x22, 0x5C): + return unichr(c) + else: + return escape_pattern % c + encoding = getEncoding(self.platformID, self.platEncID, self.langID) + if encoding is None: + raise FeatureLibError("Unsupported encoding", self.location) + s = tobytes(self.string, encoding=encoding) + if encoding == "utf_16_be": + escaped_string = "".join([ + escape(byteord(s[i]) * 256 + byteord(s[i + 1]), r"\%04x") + for i in range(0, len(s), 2)]) + else: + escaped_string = "".join([escape(byteord(b), r"\%02x") for b in s]) + plat = simplify_name_attributes( + self.platformID, self.platEncID, self.langID) + if plat != "": + plat += " " + return "nameid {} {}\"{}\";".format(self.nameID, plat, escaped_string) + + +class FeatureNameStatement(NameRecord): + def build(self, builder): + NameRecord.build(self, builder) + builder.add_featureName(self.nameID) + + def asFea(self, indent=""): + if self.nameID == "size": + tag = "sizemenuname" + else: + tag = "name" + plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID) + if plat != "": + plat += " " + return "{} {}\"{}\";".format(tag, plat, self.string) + + +class SizeParameters(Statement): + def __init__(self, DesignSize, SubfamilyID, RangeStart, RangeEnd, + location=None): + Statement.__init__(self, location) + self.DesignSize = DesignSize + self.SubfamilyID = SubfamilyID + self.RangeStart = RangeStart + self.RangeEnd = RangeEnd + + def build(self, builder): + builder.set_size_parameters(self.location, self.DesignSize, + self.SubfamilyID, self.RangeStart, self.RangeEnd) + + def asFea(self, indent=""): + res = "parameters {:.1f} {}".format(self.DesignSize, self.SubfamilyID) + if self.RangeStart != 0 or self.RangeEnd != 0: + res += " {} {}".format(int(self.RangeStart * 10), int(self.RangeEnd * 10)) + return res + ";" + + +class CVParametersNameStatement(NameRecord): + def __init__(self, nameID, platformID, platEncID, langID, string, + block_name, location=None): + NameRecord.__init__(self, nameID, platformID, platEncID, langID, + string, location=location) + self.block_name = block_name + + def build(self, builder): + item = "" + if self.block_name == "ParamUILabelNameID": + item = "_{}".format(builder.cv_num_named_params_.get(self.nameID, 0)) + builder.add_cv_parameter(self.nameID) + self.nameID = (self.nameID, self.block_name + item) + NameRecord.build(self, builder) + + def asFea(self, indent=""): + plat = simplify_name_attributes(self.platformID, self.platEncID, + self.langID) + if plat != "": + plat += " " + return "name {}\"{}\";".format(plat, self.string) + + +class CharacterStatement(Statement): + """ + Statement used in cvParameters blocks of Character Variant features (cvXX). + The Unicode value may be written with either decimal or hexadecimal + notation. The value must be preceded by '0x' if it is a hexadecimal value. + The largest Unicode value allowed is 0xFFFFFF. + """ + def __init__(self, character, tag, location=None): + Statement.__init__(self, location) + self.character = character + self.tag = tag + + def build(self, builder): + builder.add_cv_character(self.character, self.tag) + + def asFea(self, indent=""): + return "Character {:#x};".format(self.character) + + +class BaseAxis(Statement): + def __init__(self, bases, scripts, vertical, location=None): + Statement.__init__(self, location) + self.bases = bases + self.scripts = scripts + self.vertical = vertical + + def build(self, builder): + builder.set_base_axis(self.bases, self.scripts, self.vertical) + + def asFea(self, indent=""): + direction = "Vert" if self.vertical else "Horiz" + scripts = ["{} {} {}".format(a[0], a[1], " ".join(map(str, a[2]))) for a in self.scripts] + return "{}Axis.BaseTagList {};\n{}{}Axis.BaseScriptList {};".format( + direction, " ".join(self.bases), indent, direction, ", ".join(scripts)) + + +class OS2Field(Statement): + def __init__(self, key, value, location=None): + Statement.__init__(self, location) + self.key = key + self.value = value + + def build(self, builder): + builder.add_os2_field(self.key, self.value) + + def asFea(self, indent=""): + def intarr2str(x): + return " ".join(map(str, x)) + numbers = ("FSType", "TypoAscender", "TypoDescender", "TypoLineGap", + "winAscent", "winDescent", "XHeight", "CapHeight", + "WeightClass", "WidthClass", "LowerOpSize", "UpperOpSize") + ranges = ("UnicodeRange", "CodePageRange") + keywords = dict([(x.lower(), [x, str]) for x in numbers]) + keywords.update([(x.lower(), [x, intarr2str]) for x in ranges]) + keywords["panose"] = ["Panose", intarr2str] + keywords["vendor"] = ["Vendor", lambda y: '"{}"'.format(y)] + if self.key in keywords: + return "{} {};".format(keywords[self.key][0], keywords[self.key][1](self.value)) + return "" # should raise exception + + +class HheaField(Statement): + def __init__(self, key, value, location=None): + Statement.__init__(self, location) + self.key = key + self.value = value + + def build(self, builder): + builder.add_hhea_field(self.key, self.value) + + def asFea(self, indent=""): + fields = ("CaretOffset", "Ascender", "Descender", "LineGap") + keywords = dict([(x.lower(), x) for x in fields]) + return "{} {};".format(keywords[self.key], self.value) + + +class VheaField(Statement): + def __init__(self, key, value, location=None): + Statement.__init__(self, location) + self.key = key + self.value = value + + def build(self, builder): + builder.add_vhea_field(self.key, self.value) + + def asFea(self, indent=""): + fields = ("VertTypoAscender", "VertTypoDescender", "VertTypoLineGap") + keywords = dict([(x.lower(), x) for x in fields]) + return "{} {};".format(keywords[self.key], self.value) diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py new file mode 100644 index 0000000..447ded5 --- /dev/null +++ b/Lib/fontTools/feaLib/builder.py @@ -0,0 +1,1588 @@ +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc import sstruct +from fontTools.misc.textTools import binary2num, safeEval +from fontTools.feaLib.error import FeatureLibError +from fontTools.feaLib.parser import Parser +from fontTools.feaLib.ast import FeatureFile +from fontTools.otlLib import builder as otl +from fontTools.ttLib import newTable, getTableModule +from fontTools.ttLib.tables import otBase, otTables +from collections import defaultdict +import itertools +import logging + + +log = logging.getLogger(__name__) + + +def addOpenTypeFeatures(font, featurefile, tables=None): + builder = Builder(font, featurefile) + builder.build(tables=tables) + + +def addOpenTypeFeaturesFromString(font, features, filename=None, tables=None): + featurefile = UnicodeIO(tounicode(features)) + if filename: + # the directory containing 'filename' is used as the root of relative + # include paths; if None is provided, the current directory is assumed + featurefile.name = filename + addOpenTypeFeatures(font, featurefile, tables=tables) + + +class Builder(object): + + supportedTables = frozenset(Tag(tag) for tag in [ + "BASE", + "GDEF", + "GPOS", + "GSUB", + "OS/2", + "head", + "hhea", + "name", + "vhea", + ]) + + def __init__(self, font, featurefile): + self.font = font + # 'featurefile' can be either a path or file object (in which case we + # parse it into an AST), or a pre-parsed AST instance + if isinstance(featurefile, FeatureFile): + self.parseTree, self.file = featurefile, None + else: + self.parseTree, self.file = None, featurefile + self.glyphMap = font.getReverseGlyphMap() + self.default_language_systems_ = set() + self.script_ = None + self.lookupflag_ = 0 + self.lookupflag_markFilterSet_ = None + self.language_systems = set() + self.named_lookups_ = {} + self.cur_lookup_ = None + self.cur_lookup_name_ = None + self.cur_feature_name_ = None + self.lookups_ = [] + self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*] + self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp' + # for feature 'aalt' + self.aalt_features_ = [] # [(location, featureName)*], for 'aalt' + self.aalt_location_ = None + self.aalt_alternates_ = {} + # for 'featureNames' + self.featureNames_ = set() + self.featureNames_ids_ = {} + # for 'cvParameters' + self.cv_parameters_ = set() + self.cv_parameters_ids_ = {} + self.cv_num_named_params_ = {} + self.cv_characters_ = defaultdict(list) + # for feature 'size' + self.size_parameters_ = None + # for table 'head' + self.fontRevision_ = None # 2.71 + # for table 'name' + self.names_ = [] + # for table 'BASE' + self.base_horiz_axis_ = None + self.base_vert_axis_ = None + # for table 'GDEF' + self.attachPoints_ = {} # "a" --> {3, 7} + self.ligCaretCoords_ = {} # "f_f_i" --> {300, 600} + self.ligCaretPoints_ = {} # "f_f_i" --> {3, 7} + self.glyphClassDefs_ = {} # "fi" --> (2, (file, line, column)) + self.markAttach_ = {} # "acute" --> (4, (file, line, column)) + self.markAttachClassID_ = {} # frozenset({"acute", "grave"}) --> 4 + self.markFilterSets_ = {} # frozenset({"acute", "grave"}) --> 4 + # for table 'OS/2' + self.os2_ = {} + # for table 'hhea' + self.hhea_ = {} + # for table 'vhea' + self.vhea_ = {} + + def build(self, tables=None): + if self.parseTree is None: + self.parseTree = Parser(self.file, self.glyphMap).parse() + self.parseTree.build(self) + # by default, build all the supported tables + if tables is None: + tables = self.supportedTables + else: + tables = frozenset(tables) + unsupported = tables - self.supportedTables + assert not unsupported, unsupported + if "GSUB" in tables: + self.build_feature_aalt_() + if "head" in tables: + self.build_head() + if "hhea" in tables: + self.build_hhea() + if "vhea" in tables: + self.build_vhea() + if "name" in tables: + self.build_name() + if "OS/2" in tables: + self.build_OS_2() + for tag in ('GPOS', 'GSUB'): + if tag not in tables: + continue + table = self.makeTable(tag) + if (table.ScriptList.ScriptCount > 0 or + table.FeatureList.FeatureCount > 0 or + table.LookupList.LookupCount > 0): + fontTable = self.font[tag] = newTable(tag) + fontTable.table = table + elif tag in self.font: + del self.font[tag] + if "GDEF" in tables: + gdef = self.buildGDEF() + if gdef: + self.font["GDEF"] = gdef + elif "GDEF" in self.font: + del self.font["GDEF"] + if "BASE" in tables: + base = self.buildBASE() + if base: + self.font["BASE"] = base + elif "BASE" in self.font: + del self.font["BASE"] + + def get_chained_lookup_(self, location, builder_class): + result = builder_class(self.font, location) + result.lookupflag = self.lookupflag_ + result.markFilterSet = self.lookupflag_markFilterSet_ + self.lookups_.append(result) + return result + + def add_lookup_to_feature_(self, lookup, feature_name): + for script, lang in self.language_systems: + key = (script, lang, feature_name) + self.features_.setdefault(key, []).append(lookup) + + def get_lookup_(self, location, builder_class): + if (self.cur_lookup_ and + type(self.cur_lookup_) == builder_class and + self.cur_lookup_.lookupflag == self.lookupflag_ and + self.cur_lookup_.markFilterSet == + self.lookupflag_markFilterSet_): + return self.cur_lookup_ + if self.cur_lookup_name_ and self.cur_lookup_: + raise FeatureLibError( + "Within a named lookup block, all rules must be of " + "the same lookup type and flag", location) + self.cur_lookup_ = builder_class(self.font, location) + self.cur_lookup_.lookupflag = self.lookupflag_ + self.cur_lookup_.markFilterSet = self.lookupflag_markFilterSet_ + self.lookups_.append(self.cur_lookup_) + if self.cur_lookup_name_: + # We are starting a lookup rule inside a named lookup block. + self.named_lookups_[self.cur_lookup_name_] = self.cur_lookup_ + if self.cur_feature_name_: + # We are starting a lookup rule inside a feature. This includes + # lookup rules inside named lookups inside features. + self.add_lookup_to_feature_(self.cur_lookup_, + self.cur_feature_name_) + return self.cur_lookup_ + + def build_feature_aalt_(self): + if not self.aalt_features_ and not self.aalt_alternates_: + return + alternates = {g: set(a) for g, a in self.aalt_alternates_.items()} + for location, name in self.aalt_features_ + [(None, "aalt")]: + feature = [(script, lang, feature, lookups) + for (script, lang, feature), lookups + in self.features_.items() + if feature == name] + # "aalt" does not have to specify its own lookups, but it might. + if not feature and name != "aalt": + raise FeatureLibError("Feature %s has not been defined" % name, + location) + for script, lang, feature, lookups in feature: + for lookup in lookups: + for glyph, alts in lookup.getAlternateGlyphs().items(): + alternates.setdefault(glyph, set()).update(alts) + single = {glyph: list(repl)[0] for glyph, repl in alternates.items() + if len(repl) == 1} + # TODO: Figure out the glyph alternate ordering used by makeotf. + # https://github.com/fonttools/fonttools/issues/836 + multi = {glyph: sorted(repl, key=self.font.getGlyphID) + for glyph, repl in alternates.items() + if len(repl) > 1} + if not single and not multi: + return + self.features_ = {(script, lang, feature): lookups + for (script, lang, feature), lookups + in self.features_.items() + if feature != "aalt"} + old_lookups = self.lookups_ + self.lookups_ = [] + self.start_feature(self.aalt_location_, "aalt") + if single: + single_lookup = self.get_lookup_(location, SingleSubstBuilder) + single_lookup.mapping = single + if multi: + multi_lookup = self.get_lookup_(location, AlternateSubstBuilder) + multi_lookup.alternates = multi + self.end_feature() + self.lookups_.extend(old_lookups) + + def build_head(self): + if not self.fontRevision_: + return + table = self.font.get("head") + if not table: # this only happens for unit tests + table = self.font["head"] = newTable("head") + table.decompile(b"\0" * 54, self.font) + table.tableVersion = 1.0 + table.created = table.modified = 3406620153 # 2011-12-13 11:22:33 + table.fontRevision = self.fontRevision_ + + def build_hhea(self): + if not self.hhea_: + return + table = self.font.get("hhea") + if not table: # this only happens for unit tests + table = self.font["hhea"] = newTable("hhea") + table.decompile(b"\0" * 36, self.font) + table.tableVersion = 0x00010000 + if "caretoffset" in self.hhea_: + table.caretOffset = self.hhea_["caretoffset"] + if "ascender" in self.hhea_: + table.ascent = self.hhea_["ascender"] + if "descender" in self.hhea_: + table.descent = self.hhea_["descender"] + if "linegap" in self.hhea_: + table.lineGap = self.hhea_["linegap"] + + def build_vhea(self): + if not self.vhea_: + return + table = self.font.get("vhea") + if not table: # this only happens for unit tests + table = self.font["vhea"] = newTable("vhea") + table.decompile(b"\0" * 36, self.font) + table.tableVersion = 0x00011000 + if "verttypoascender" in self.vhea_: + table.ascent = self.vhea_["verttypoascender"] + if "verttypodescender" in self.vhea_: + table.descent = self.vhea_["verttypodescender"] + if "verttypolinegap" in self.vhea_: + table.lineGap = self.vhea_["verttypolinegap"] + + def get_user_name_id(self, table): + # Try to find first unused font-specific name id + nameIDs = [name.nameID for name in table.names] + for user_name_id in range(256, 32767): + if user_name_id not in nameIDs: + return user_name_id + + def buildFeatureParams(self, tag): + params = None + if tag == "size": + params = otTables.FeatureParamsSize() + params.DesignSize, params.SubfamilyID, params.RangeStart, \ + params.RangeEnd = self.size_parameters_ + if tag in self.featureNames_ids_: + params.SubfamilyNameID = self.featureNames_ids_[tag] + else: + params.SubfamilyNameID = 0 + elif tag in self.featureNames_: + assert tag in self.featureNames_ids_ + params = otTables.FeatureParamsStylisticSet() + params.Version = 0 + params.UINameID = self.featureNames_ids_[tag] + elif tag in self.cv_parameters_: + params = otTables.FeatureParamsCharacterVariants() + params.Format = 0 + params.FeatUILabelNameID = self.cv_parameters_ids_.get( + (tag, 'FeatUILabelNameID'), 0) + params.FeatUITooltipTextNameID = self.cv_parameters_ids_.get( + (tag, 'FeatUITooltipTextNameID'), 0) + params.SampleTextNameID = self.cv_parameters_ids_.get( + (tag, 'SampleTextNameID'), 0) + params.NumNamedParameters = self.cv_num_named_params_.get(tag, 0) + params.FirstParamUILabelNameID = self.cv_parameters_ids_.get( + (tag, 'ParamUILabelNameID_0'), 0) + params.CharCount = len(self.cv_characters_[tag]) + params.Character = self.cv_characters_[tag] + return params + + def build_name(self): + if not self.names_: + return + table = self.font.get("name") + if not table: # this only happens for unit tests + table = self.font["name"] = newTable("name") + table.names = [] + for name in self.names_: + nameID, platformID, platEncID, langID, string = name + # For featureNames block, nameID is 'feature tag' + # For cvParameters blocks, nameID is ('feature tag', 'block name') + if not isinstance(nameID, int): + tag = nameID + if tag in self.featureNames_: + if tag not in self.featureNames_ids_: + self.featureNames_ids_[tag] = self.get_user_name_id(table) + assert self.featureNames_ids_[tag] is not None + nameID = self.featureNames_ids_[tag] + elif tag[0] in self.cv_parameters_: + if tag not in self.cv_parameters_ids_: + self.cv_parameters_ids_[tag] = self.get_user_name_id(table) + assert self.cv_parameters_ids_[tag] is not None + nameID = self.cv_parameters_ids_[tag] + table.setName(string, nameID, platformID, platEncID, langID) + + def build_OS_2(self): + if not self.os2_: + return + table = self.font.get("OS/2") + if not table: # this only happens for unit tests + table = self.font["OS/2"] = newTable("OS/2") + data = b"\0" * sstruct.calcsize(getTableModule("OS/2").OS2_format_0) + table.decompile(data, self.font) + version = 0 + if "fstype" in self.os2_: + table.fsType = self.os2_["fstype"] + if "panose" in self.os2_: + panose = getTableModule("OS/2").Panose() + panose.bFamilyType, panose.bSerifStyle, panose.bWeight,\ + panose.bProportion, panose.bContrast, panose.bStrokeVariation,\ + panose.bArmStyle, panose.bLetterForm, panose.bMidline, \ + panose.bXHeight = self.os2_["panose"] + table.panose = panose + if "typoascender" in self.os2_: + table.sTypoAscender = self.os2_["typoascender"] + if "typodescender" in self.os2_: + table.sTypoDescender = self.os2_["typodescender"] + if "typolinegap" in self.os2_: + table.sTypoLineGap = self.os2_["typolinegap"] + if "winascent" in self.os2_: + table.usWinAscent = self.os2_["winascent"] + if "windescent" in self.os2_: + table.usWinDescent = self.os2_["windescent"] + if "vendor" in self.os2_: + table.achVendID = safeEval("'''" + self.os2_["vendor"] + "'''") + if "weightclass" in self.os2_: + table.usWeightClass = self.os2_["weightclass"] + if "widthclass" in self.os2_: + table.usWidthClass = self.os2_["widthclass"] + if "unicoderange" in self.os2_: + table.setUnicodeRanges(self.os2_["unicoderange"]) + if "codepagerange" in self.os2_: + pages = self.build_codepages_(self.os2_["codepagerange"]) + table.ulCodePageRange1, table.ulCodePageRange2 = pages + version = 1 + if "xheight" in self.os2_: + table.sxHeight = self.os2_["xheight"] + version = 2 + if "capheight" in self.os2_: + table.sCapHeight = self.os2_["capheight"] + version = 2 + if "loweropsize" in self.os2_: + table.usLowerOpticalPointSize = self.os2_["loweropsize"] + version = 5 + if "upperopsize" in self.os2_: + table.usUpperOpticalPointSize = self.os2_["upperopsize"] + version = 5 + def checkattr(table, attrs): + for attr in attrs: + if not hasattr(table, attr): + setattr(table, attr, 0) + table.version = max(version, table.version) + # this only happens for unit tests + if version >= 1: + checkattr(table, ("ulCodePageRange1", "ulCodePageRange2")) + if version >= 2: + checkattr(table, ("sxHeight", "sCapHeight", "usDefaultChar", + "usBreakChar", "usMaxContext")) + if version >= 5: + checkattr(table, ("usLowerOpticalPointSize", + "usUpperOpticalPointSize")) + + def build_codepages_(self, pages): + pages2bits = { + 1252: 0, 1250: 1, 1251: 2, 1253: 3, 1254: 4, 1255: 5, 1256: 6, + 1257: 7, 1258: 8, 874: 16, 932: 17, 936: 18, 949: 19, 950: 20, + 1361: 21, 869: 48, 866: 49, 865: 50, 864: 51, 863: 52, 862: 53, + 861: 54, 860: 55, 857: 56, 855: 57, 852: 58, 775: 59, 737: 60, + 708: 61, 850: 62, 437: 63, + } + bits = [pages2bits[p] for p in pages if p in pages2bits] + pages = [] + for i in range(2): + pages.append("") + for j in range(i * 32, (i + 1) * 32): + if j in bits: + pages[i] += "1" + else: + pages[i] += "0" + return [binary2num(p[::-1]) for p in pages] + + def buildBASE(self): + if not self.base_horiz_axis_ and not self.base_vert_axis_: + return None + base = otTables.BASE() + base.Version = 0x00010000 + base.HorizAxis = self.buildBASEAxis(self.base_horiz_axis_) + base.VertAxis = self.buildBASEAxis(self.base_vert_axis_) + + result = newTable("BASE") + result.table = base + return result + + def buildBASEAxis(self, axis): + if not axis: + return + bases, scripts = axis + axis = otTables.Axis() + axis.BaseTagList = otTables.BaseTagList() + axis.BaseTagList.BaselineTag = bases + axis.BaseTagList.BaseTagCount = len(bases) + axis.BaseScriptList = otTables.BaseScriptList() + axis.BaseScriptList.BaseScriptRecord = [] + axis.BaseScriptList.BaseScriptCount = len(scripts) + for script in sorted(scripts): + record = otTables.BaseScriptRecord() + record.BaseScriptTag = script[0] + record.BaseScript = otTables.BaseScript() + record.BaseScript.BaseLangSysCount = 0 + record.BaseScript.BaseValues = otTables.BaseValues() + record.BaseScript.BaseValues.DefaultIndex = bases.index(script[1]) + record.BaseScript.BaseValues.BaseCoord = [] + record.BaseScript.BaseValues.BaseCoordCount = len(script[2]) + for c in script[2]: + coord = otTables.BaseCoord() + coord.Format = 1 + coord.Coordinate = c + record.BaseScript.BaseValues.BaseCoord.append(coord) + axis.BaseScriptList.BaseScriptRecord.append(record) + return axis + + def buildGDEF(self): + gdef = otTables.GDEF() + gdef.GlyphClassDef = self.buildGDEFGlyphClassDef_() + gdef.AttachList = \ + otl.buildAttachList(self.attachPoints_, self.glyphMap) + gdef.LigCaretList = \ + otl.buildLigCaretList(self.ligCaretCoords_, self.ligCaretPoints_, + self.glyphMap) + gdef.MarkAttachClassDef = self.buildGDEFMarkAttachClassDef_() + gdef.MarkGlyphSetsDef = self.buildGDEFMarkGlyphSetsDef_() + gdef.Version = 0x00010002 if gdef.MarkGlyphSetsDef else 0x00010000 + if any((gdef.GlyphClassDef, gdef.AttachList, gdef.LigCaretList, + gdef.MarkAttachClassDef, gdef.MarkGlyphSetsDef)): + result = newTable("GDEF") + result.table = gdef + return result + else: + return None + + def buildGDEFGlyphClassDef_(self): + if self.glyphClassDefs_: + classes = {g: c for (g, (c, _)) in self.glyphClassDefs_.items()} + else: + classes = {} + for lookup in self.lookups_: + classes.update(lookup.inferGlyphClasses()) + for markClass in self.parseTree.markClasses.values(): + for markClassDef in markClass.definitions: + for glyph in markClassDef.glyphSet(): + classes[glyph] = 3 + if classes: + result = otTables.GlyphClassDef() + result.classDefs = classes + return result + else: + return None + + def buildGDEFMarkAttachClassDef_(self): + classDefs = {g: c for g, (c, _) in self.markAttach_.items()} + if not classDefs: + return None + result = otTables.MarkAttachClassDef() + result.classDefs = classDefs + return result + + def buildGDEFMarkGlyphSetsDef_(self): + sets = [] + for glyphs, id_ in sorted(self.markFilterSets_.items(), + key=lambda item: item[1]): + sets.append(glyphs) + return otl.buildMarkGlyphSetsDef(sets, self.glyphMap) + + def buildLookups_(self, tag): + assert tag in ('GPOS', 'GSUB'), tag + for lookup in self.lookups_: + lookup.lookup_index = None + lookups = [] + for lookup in self.lookups_: + if lookup.table != tag: + continue + lookup.lookup_index = len(lookups) + lookups.append(lookup) + return [l.build() for l in lookups] + + def makeTable(self, tag): + table = getattr(otTables, tag, None)() + table.Version = 0x00010000 + table.ScriptList = otTables.ScriptList() + table.ScriptList.ScriptRecord = [] + table.FeatureList = otTables.FeatureList() + table.FeatureList.FeatureRecord = [] + table.LookupList = otTables.LookupList() + table.LookupList.Lookup = self.buildLookups_(tag) + + # Build a table for mapping (tag, lookup_indices) to feature_index. + # For example, ('liga', (2,3,7)) --> 23. + feature_indices = {} + required_feature_indices = {} # ('latn', 'DEU') --> 23 + scripts = {} # 'latn' --> {'DEU': [23, 24]} for feature #23,24 + # Sort the feature table by feature tag: + # https://github.com/behdad/fonttools/issues/568 + sortFeatureTag = lambda f: (f[0][2], f[0][1], f[0][0], f[1]) + for key, lookups in sorted(self.features_.items(), key=sortFeatureTag): + script, lang, feature_tag = key + # l.lookup_index will be None when a lookup is not needed + # for the table under construction. For example, substitution + # rules will have no lookup_index while building GPOS tables. + lookup_indices = tuple([l.lookup_index for l in lookups + if l.lookup_index is not None]) + + size_feature = (tag == "GPOS" and feature_tag == "size") + if len(lookup_indices) == 0 and not size_feature: + continue + + feature_key = (feature_tag, lookup_indices) + feature_index = feature_indices.get(feature_key) + if feature_index is None: + feature_index = len(table.FeatureList.FeatureRecord) + frec = otTables.FeatureRecord() + frec.FeatureTag = feature_tag + frec.Feature = otTables.Feature() + frec.Feature.FeatureParams = self.buildFeatureParams( + feature_tag) + frec.Feature.LookupListIndex = list(lookup_indices) + frec.Feature.LookupCount = len(lookup_indices) + table.FeatureList.FeatureRecord.append(frec) + feature_indices[feature_key] = feature_index + scripts.setdefault(script, {}).setdefault(lang, []).append( + feature_index) + if self.required_features_.get((script, lang)) == feature_tag: + required_feature_indices[(script, lang)] = feature_index + + # Build ScriptList. + for script, lang_features in sorted(scripts.items()): + srec = otTables.ScriptRecord() + srec.ScriptTag = script + srec.Script = otTables.Script() + srec.Script.DefaultLangSys = None + srec.Script.LangSysRecord = [] + for lang, feature_indices in sorted(lang_features.items()): + langrec = otTables.LangSysRecord() + langrec.LangSys = otTables.LangSys() + langrec.LangSys.LookupOrder = None + + req_feature_index = \ + required_feature_indices.get((script, lang)) + if req_feature_index is None: + langrec.LangSys.ReqFeatureIndex = 0xFFFF + else: + langrec.LangSys.ReqFeatureIndex = req_feature_index + + langrec.LangSys.FeatureIndex = [i for i in feature_indices + if i != req_feature_index] + langrec.LangSys.FeatureCount = \ + len(langrec.LangSys.FeatureIndex) + + if lang == "dflt": + srec.Script.DefaultLangSys = langrec.LangSys + else: + langrec.LangSysTag = lang + srec.Script.LangSysRecord.append(langrec) + srec.Script.LangSysCount = len(srec.Script.LangSysRecord) + table.ScriptList.ScriptRecord.append(srec) + + table.ScriptList.ScriptCount = len(table.ScriptList.ScriptRecord) + table.FeatureList.FeatureCount = len(table.FeatureList.FeatureRecord) + table.LookupList.LookupCount = len(table.LookupList.Lookup) + return table + + def add_language_system(self, location, script, language): + # OpenType Feature File Specification, section 4.b.i + if (script == "DFLT" and language == "dflt" and + self.default_language_systems_): + raise FeatureLibError( + 'If "languagesystem DFLT dflt" is present, it must be ' + 'the first of the languagesystem statements', location) + if (script, language) in self.default_language_systems_: + raise FeatureLibError( + '"languagesystem %s %s" has already been specified' % + (script.strip(), language.strip()), location) + self.default_language_systems_.add((script, language)) + + def get_default_language_systems_(self): + # OpenType Feature File specification, 4.b.i. languagesystem: + # If no "languagesystem" statement is present, then the + # implementation must behave exactly as though the following + # statement were present at the beginning of the feature file: + # languagesystem DFLT dflt; + if self.default_language_systems_: + return frozenset(self.default_language_systems_) + else: + return frozenset({('DFLT', 'dflt')}) + + def start_feature(self, location, name): + self.language_systems = self.get_default_language_systems_() + self.script_ = 'DFLT' + self.cur_lookup_ = None + self.cur_feature_name_ = name + self.lookupflag_ = 0 + self.lookupflag_markFilterSet_ = None + if name == "aalt": + self.aalt_location_ = location + + def end_feature(self): + assert self.cur_feature_name_ is not None + self.cur_feature_name_ = None + self.language_systems = None + self.cur_lookup_ = None + self.lookupflag_ = 0 + self.lookupflag_markFilterSet_ = None + + def start_lookup_block(self, location, name): + if name in self.named_lookups_: + raise FeatureLibError( + 'Lookup "%s" has already been defined' % name, location) + if self.cur_feature_name_ == "aalt": + raise FeatureLibError( + "Lookup blocks cannot be placed inside 'aalt' features; " + "move it out, and then refer to it with a lookup statement", + location) + self.cur_lookup_name_ = name + self.named_lookups_[name] = None + self.cur_lookup_ = None + self.lookupflag_ = 0 + self.lookupflag_markFilterSet_ = None + + def end_lookup_block(self): + assert self.cur_lookup_name_ is not None + self.cur_lookup_name_ = None + self.cur_lookup_ = None + self.lookupflag_ = 0 + self.lookupflag_markFilterSet_ = None + + def add_lookup_call(self, lookup_name): + assert lookup_name in self.named_lookups_, lookup_name + self.cur_lookup_ = None + lookup = self.named_lookups_[lookup_name] + self.add_lookup_to_feature_(lookup, self.cur_feature_name_) + + def set_font_revision(self, location, revision): + self.fontRevision_ = revision + + def set_language(self, location, language, include_default, required): + assert(len(language) == 4) + if self.cur_feature_name_ in ('aalt', 'size'): + raise FeatureLibError( + "Language statements are not allowed " + "within \"feature %s\"" % self.cur_feature_name_, location) + if language != 'dflt' and self.script_ == 'DFLT': + raise FeatureLibError("Need non-DFLT script when using non-dflt " + "language (was: \"%s\")" % language, location) + self.cur_lookup_ = None + + key = (self.script_, language, self.cur_feature_name_) + if not include_default: + # don't include any lookups added by script DFLT in this feature + self.features_[key] = [] + elif language != 'dflt': + # add rules defined between script statement and its first following + # language statement to each of its explicitly specified languages: + # http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html#4.b.ii + lookups = self.features_.get((key[0], 'dflt', key[2])) + dflt_lookups = self.features_.get(('DFLT', 'dflt', key[2]), []) + if lookups: + if key[:2] in self.get_default_language_systems_(): + lookups = [l for l in lookups if l not in dflt_lookups] + self.features_.setdefault(key, []).extend(lookups) + if self.script_ == 'DFLT': + langsys = set(self.get_default_language_systems_()) + else: + langsys = set() + langsys.add((self.script_, language)) + self.language_systems = frozenset(langsys) + + if required: + key = (self.script_, language) + if key in self.required_features_: + raise FeatureLibError( + "Language %s (script %s) has already " + "specified feature %s as its required feature" % ( + language.strip(), self.script_.strip(), + self.required_features_[key].strip()), + location) + self.required_features_[key] = self.cur_feature_name_ + + def getMarkAttachClass_(self, location, glyphs): + glyphs = frozenset(glyphs) + id_ = self.markAttachClassID_.get(glyphs) + if id_ is not None: + return id_ + id_ = len(self.markAttachClassID_) + 1 + self.markAttachClassID_[glyphs] = id_ + for glyph in glyphs: + if glyph in self.markAttach_: + _, loc = self.markAttach_[glyph] + raise FeatureLibError( + "Glyph %s already has been assigned " + "a MarkAttachmentType at %s:%d:%d" % ( + glyph, loc[0], loc[1], loc[2]), + location) + self.markAttach_[glyph] = (id_, location) + return id_ + + def getMarkFilterSet_(self, location, glyphs): + glyphs = frozenset(glyphs) + id_ = self.markFilterSets_.get(glyphs) + if id_ is not None: + return id_ + id_ = len(self.markFilterSets_) + self.markFilterSets_[glyphs] = id_ + return id_ + + def set_lookup_flag(self, location, value, markAttach, markFilter): + value = value & 0xFF + if markAttach: + markAttachClass = self.getMarkAttachClass_(location, markAttach) + value = value | (markAttachClass << 8) + if markFilter: + markFilterSet = self.getMarkFilterSet_(location, markFilter) + value = value | 0x10 + self.lookupflag_markFilterSet_ = markFilterSet + else: + self.lookupflag_markFilterSet_ = None + self.lookupflag_ = value + + def set_script(self, location, script): + if self.cur_feature_name_ in ('aalt', 'size'): + raise FeatureLibError( + "Script statements are not allowed " + "within \"feature %s\"" % self.cur_feature_name_, location) + self.cur_lookup_ = None + self.script_ = script + self.lookupflag_ = 0 + self.lookupflag_markFilterSet_ = None + self.set_language(location, "dflt", + include_default=True, required=False) + + def find_lookup_builders_(self, lookups): + """Helper for building chain contextual substitutions + + Given a list of lookup names, finds the LookupBuilder for each name. + If an input name is None, it gets mapped to a None LookupBuilder. + """ + lookup_builders = [] + for lookup in lookups: + if lookup is not None: + lookup_builders.append(self.named_lookups_.get(lookup.name)) + else: + lookup_builders.append(None) + return lookup_builders + + def add_attach_points(self, location, glyphs, contourPoints): + for glyph in glyphs: + self.attachPoints_.setdefault(glyph, set()).update(contourPoints) + + def add_chain_context_pos(self, location, prefix, glyphs, suffix, lookups): + lookup = self.get_lookup_(location, ChainContextPosBuilder) + lookup.rules.append((prefix, glyphs, suffix, + self.find_lookup_builders_(lookups))) + + def add_chain_context_subst(self, location, + prefix, glyphs, suffix, lookups): + lookup = self.get_lookup_(location, ChainContextSubstBuilder) + lookup.substitutions.append((prefix, glyphs, suffix, + self.find_lookup_builders_(lookups))) + + def add_alternate_subst(self, location, + prefix, glyph, suffix, replacement): + if self.cur_feature_name_ == "aalt": + alts = self.aalt_alternates_.setdefault(glyph, set()) + alts.update(replacement) + return + if prefix or suffix: + chain = self.get_lookup_(location, ChainContextSubstBuilder) + lookup = self.get_chained_lookup_(location, AlternateSubstBuilder) + chain.substitutions.append((prefix, [glyph], suffix, [lookup])) + else: + lookup = self.get_lookup_(location, AlternateSubstBuilder) + if glyph in lookup.alternates: + raise FeatureLibError( + 'Already defined alternates for glyph "%s"' % glyph, + location) + lookup.alternates[glyph] = replacement + + def add_feature_reference(self, location, featureName): + if self.cur_feature_name_ != "aalt": + raise FeatureLibError( + 'Feature references are only allowed inside "feature aalt"', + location) + self.aalt_features_.append((location, featureName)) + + def add_featureName(self, tag): + self.featureNames_.add(tag) + + def add_cv_parameter(self, tag): + self.cv_parameters_.add(tag) + + def add_to_cv_num_named_params(self, tag): + """Adds new items to self.cv_num_named_params_ + or increments the count of existing items.""" + if tag in self.cv_num_named_params_: + self.cv_num_named_params_[tag] += 1 + else: + self.cv_num_named_params_[tag] = 1 + + def add_cv_character(self, character, tag): + self.cv_characters_[tag].append(character) + + def set_base_axis(self, bases, scripts, vertical): + if vertical: + self.base_vert_axis_ = (bases, scripts) + else: + self.base_horiz_axis_ = (bases, scripts) + + def set_size_parameters(self, location, DesignSize, SubfamilyID, + RangeStart, RangeEnd): + if self.cur_feature_name_ != 'size': + raise FeatureLibError( + "Parameters statements are not allowed " + "within \"feature %s\"" % self.cur_feature_name_, location) + self.size_parameters_ = [DesignSize, SubfamilyID, RangeStart, RangeEnd] + for script, lang in self.language_systems: + key = (script, lang, self.cur_feature_name_) + self.features_.setdefault(key, []) + + def add_ligature_subst(self, location, + prefix, glyphs, suffix, replacement, forceChain): + if prefix or suffix or forceChain: + chain = self.get_lookup_(location, ChainContextSubstBuilder) + lookup = self.get_chained_lookup_(location, LigatureSubstBuilder) + chain.substitutions.append((prefix, glyphs, suffix, [lookup])) + else: + lookup = self.get_lookup_(location, LigatureSubstBuilder) + + # OpenType feature file syntax, section 5.d, "Ligature substitution": + # "Since the OpenType specification does not allow ligature + # substitutions to be specified on target sequences that contain + # glyph classes, the implementation software will enumerate + # all specific glyph sequences if glyph classes are detected" + for g in sorted(itertools.product(*glyphs)): + lookup.ligatures[g] = replacement + + def add_multiple_subst(self, location, + prefix, glyph, suffix, replacements): + if prefix or suffix: + chain = self.get_lookup_(location, ChainContextSubstBuilder) + sub = self.get_chained_lookup_(location, MultipleSubstBuilder) + sub.mapping[glyph] = replacements + chain.substitutions.append((prefix, [{glyph}], suffix, [sub])) + return + lookup = self.get_lookup_(location, MultipleSubstBuilder) + if glyph in lookup.mapping: + raise FeatureLibError( + 'Already defined substitution for glyph "%s"' % glyph, + location) + lookup.mapping[glyph] = replacements + + def add_reverse_chain_single_subst(self, location, old_prefix, + old_suffix, mapping): + lookup = self.get_lookup_(location, ReverseChainSingleSubstBuilder) + lookup.substitutions.append((old_prefix, old_suffix, mapping)) + + def add_single_subst(self, location, prefix, suffix, mapping, forceChain): + if self.cur_feature_name_ == "aalt": + for (from_glyph, to_glyph) in mapping.items(): + alts = self.aalt_alternates_.setdefault(from_glyph, set()) + alts.add(to_glyph) + return + if prefix or suffix or forceChain: + self.add_single_subst_chained_(location, prefix, suffix, mapping) + return + lookup = self.get_lookup_(location, SingleSubstBuilder) + for (from_glyph, to_glyph) in mapping.items(): + if from_glyph in lookup.mapping: + raise FeatureLibError( + 'Already defined rule for replacing glyph "%s" by "%s"' % + (from_glyph, lookup.mapping[from_glyph]), + location) + lookup.mapping[from_glyph] = to_glyph + + def find_chainable_SingleSubst_(self, chain, glyphs): + """Helper for add_single_subst_chained_()""" + for _, _, _, substitutions in chain.substitutions: + for sub in substitutions: + if (isinstance(sub, SingleSubstBuilder) and + not any(g in glyphs for g in sub.mapping.keys())): + return sub + return None + + def add_single_subst_chained_(self, location, prefix, suffix, mapping): + # https://github.com/behdad/fonttools/issues/512 + chain = self.get_lookup_(location, ChainContextSubstBuilder) + sub = self.find_chainable_SingleSubst_(chain, set(mapping.keys())) + if sub is None: + sub = self.get_chained_lookup_(location, SingleSubstBuilder) + sub.mapping.update(mapping) + chain.substitutions.append((prefix, [mapping.keys()], suffix, [sub])) + + def add_cursive_pos(self, location, glyphclass, entryAnchor, exitAnchor): + lookup = self.get_lookup_(location, CursivePosBuilder) + lookup.add_attachment( + location, glyphclass, + makeOpenTypeAnchor(entryAnchor), + makeOpenTypeAnchor(exitAnchor)) + + def add_marks_(self, location, lookupBuilder, marks): + """Helper for add_mark_{base,liga,mark}_pos.""" + for _, markClass in marks: + for markClassDef in markClass.definitions: + for mark in markClassDef.glyphs.glyphSet(): + if mark not in lookupBuilder.marks: + otMarkAnchor = makeOpenTypeAnchor(markClassDef.anchor) + lookupBuilder.marks[mark] = ( + markClass.name, otMarkAnchor) + else: + existingMarkClass = lookupBuilder.marks[mark][0] + if markClass.name != existingMarkClass: + raise FeatureLibError( + "Glyph %s cannot be in both @%s and @%s" % ( + mark, existingMarkClass, markClass.name), + location) + + def add_mark_base_pos(self, location, bases, marks): + builder = self.get_lookup_(location, MarkBasePosBuilder) + self.add_marks_(location, builder, marks) + for baseAnchor, markClass in marks: + otBaseAnchor = makeOpenTypeAnchor(baseAnchor) + for base in bases: + builder.bases.setdefault(base, {})[markClass.name] = ( + otBaseAnchor) + + def add_mark_lig_pos(self, location, ligatures, components): + builder = self.get_lookup_(location, MarkLigPosBuilder) + componentAnchors = [] + for marks in components: + anchors = {} + self.add_marks_(location, builder, marks) + for ligAnchor, markClass in marks: + anchors[markClass.name] = makeOpenTypeAnchor(ligAnchor) + componentAnchors.append(anchors) + for glyph in ligatures: + builder.ligatures[glyph] = componentAnchors + + def add_mark_mark_pos(self, location, baseMarks, marks): + builder = self.get_lookup_(location, MarkMarkPosBuilder) + self.add_marks_(location, builder, marks) + for baseAnchor, markClass in marks: + otBaseAnchor = makeOpenTypeAnchor(baseAnchor) + for baseMark in baseMarks: + builder.baseMarks.setdefault(baseMark, {})[markClass.name] = ( + otBaseAnchor) + + def add_class_pair_pos(self, location, glyphclass1, value1, + glyphclass2, value2): + lookup = self.get_lookup_(location, PairPosBuilder) + lookup.addClassPair(location, glyphclass1, value1, glyphclass2, value2) + + def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2): + lookup = self.get_lookup_(location, PairPosBuilder) + lookup.addGlyphPair(location, glyph1, value1, glyph2, value2) + + def add_single_pos(self, location, prefix, suffix, pos, forceChain): + if prefix or suffix or forceChain: + self.add_single_pos_chained_(location, prefix, suffix, pos) + else: + lookup = self.get_lookup_(location, SinglePosBuilder) + for glyphs, value in pos: + for glyph in glyphs: + lookup.add_pos(location, glyph, value) + + def find_chainable_SinglePos_(self, lookups, glyphs, value): + """Helper for add_single_pos_chained_()""" + for look in lookups: + if all(look.can_add(glyph, value) for glyph in glyphs): + return look + return None + + def add_single_pos_chained_(self, location, prefix, suffix, pos): + # https://github.com/fonttools/fonttools/issues/514 + chain = self.get_lookup_(location, ChainContextPosBuilder) + targets = [] + for _, _, _, lookups in chain.rules: + for lookup in lookups: + if isinstance(lookup, SinglePosBuilder): + targets.append(lookup) + subs = [] + for glyphs, value in pos: + if value is None: + subs.append(None) + continue + otValue, _ = makeOpenTypeValueRecord(value, pairPosContext=False) + sub = self.find_chainable_SinglePos_(targets, glyphs, otValue) + if sub is None: + sub = self.get_chained_lookup_(location, SinglePosBuilder) + targets.append(sub) + for glyph in glyphs: + sub.add_pos(location, glyph, value) + subs.append(sub) + assert len(pos) == len(subs), (pos, subs) + chain.rules.append( + (prefix, [g for g, v in pos], suffix, subs)) + + def setGlyphClass_(self, location, glyph, glyphClass): + oldClass, oldLocation = self.glyphClassDefs_.get(glyph, (None, None)) + if oldClass and oldClass != glyphClass: + raise FeatureLibError( + "Glyph %s was assigned to a different class at %s:%s:%s" % + (glyph, oldLocation[0], oldLocation[1], oldLocation[2]), + location) + self.glyphClassDefs_[glyph] = (glyphClass, location) + + def add_glyphClassDef(self, location, baseGlyphs, ligatureGlyphs, + markGlyphs, componentGlyphs): + for glyph in baseGlyphs: + self.setGlyphClass_(location, glyph, 1) + for glyph in ligatureGlyphs: + self.setGlyphClass_(location, glyph, 2) + for glyph in markGlyphs: + self.setGlyphClass_(location, glyph, 3) + for glyph in componentGlyphs: + self.setGlyphClass_(location, glyph, 4) + + def add_ligatureCaretByIndex_(self, location, glyphs, carets): + for glyph in glyphs: + self.ligCaretPoints_.setdefault(glyph, set()).update(carets) + + def add_ligatureCaretByPos_(self, location, glyphs, carets): + for glyph in glyphs: + self.ligCaretCoords_.setdefault(glyph, set()).update(carets) + + def add_name_record(self, location, nameID, platformID, platEncID, + langID, string): + self.names_.append([nameID, platformID, platEncID, langID, string]) + + def add_os2_field(self, key, value): + self.os2_[key] = value + + def add_hhea_field(self, key, value): + self.hhea_[key] = value + + def add_vhea_field(self, key, value): + self.vhea_[key] = value + + +def makeOpenTypeAnchor(anchor): + """ast.Anchor --> otTables.Anchor""" + if anchor is None: + return None + deviceX, deviceY = None, None + if anchor.xDeviceTable is not None: + deviceX = otl.buildDevice(dict(anchor.xDeviceTable)) + if anchor.yDeviceTable is not None: + deviceY = otl.buildDevice(dict(anchor.yDeviceTable)) + return otl.buildAnchor(anchor.x, anchor.y, anchor.contourpoint, + deviceX, deviceY) + + +_VALUEREC_ATTRS = { + name[0].lower() + name[1:]: (name, isDevice) + for _, name, isDevice, _ in otBase.valueRecordFormat + if not name.startswith("Reserved") +} + + +def makeOpenTypeValueRecord(v, pairPosContext): + """ast.ValueRecord --> (otBase.ValueRecord, int ValueFormat)""" + if v is None: + return None, 0 + + vr = {} + for astName, (otName, isDevice) in _VALUEREC_ATTRS.items(): + val = getattr(v, astName, None) + if val: + vr[otName] = otl.buildDevice(dict(val)) if isDevice else val + if pairPosContext and not vr: + vr = {"YAdvance": 0} if v.vertical else {"XAdvance": 0} + valRec = otl.buildValue(vr) + return valRec, valRec.getFormat() + + +class LookupBuilder(object): + def __init__(self, font, location, table, lookup_type): + self.font = font + self.glyphMap = font.getReverseGlyphMap() + self.location = location + self.table, self.lookup_type = table, lookup_type + self.lookupflag = 0 + self.markFilterSet = None + self.lookup_index = None # assigned when making final tables + assert table in ('GPOS', 'GSUB') + + def equals(self, other): + return (isinstance(other, self.__class__) and + self.table == other.table and + self.lookupflag == other.lookupflag and + self.markFilterSet == other.markFilterSet) + + def inferGlyphClasses(self): + """Infers glyph glasses for the GDEF table, such as {"cedilla":3}.""" + return {} + + def getAlternateGlyphs(self): + """Helper for building 'aalt' features.""" + return {} + + def buildLookup_(self, subtables): + return otl.buildLookup(subtables, self.lookupflag, self.markFilterSet) + + def buildMarkClasses_(self, marks): + """{"cedilla": ("BOTTOM", ast.Anchor), ...} --> {"BOTTOM":0, "TOP":1} + + Helper for MarkBasePostBuilder, MarkLigPosBuilder, and + MarkMarkPosBuilder. Seems to return the same numeric IDs + for mark classes as the AFDKO makeotf tool. + """ + ids = {} + for mark in sorted(marks.keys(), key=self.font.getGlyphID): + markClassName, _markAnchor = marks[mark] + if markClassName not in ids: + ids[markClassName] = len(ids) + return ids + + def setBacktrackCoverage_(self, prefix, subtable): + subtable.BacktrackGlyphCount = len(prefix) + subtable.BacktrackCoverage = [] + for p in reversed(prefix): + coverage = otl.buildCoverage(p, self.glyphMap) + subtable.BacktrackCoverage.append(coverage) + + def setLookAheadCoverage_(self, suffix, subtable): + subtable.LookAheadGlyphCount = len(suffix) + subtable.LookAheadCoverage = [] + for s in suffix: + coverage = otl.buildCoverage(s, self.glyphMap) + subtable.LookAheadCoverage.append(coverage) + + def setInputCoverage_(self, glyphs, subtable): + subtable.InputGlyphCount = len(glyphs) + subtable.InputCoverage = [] + for g in glyphs: + coverage = otl.buildCoverage(g, self.glyphMap) + subtable.InputCoverage.append(coverage) + + +class AlternateSubstBuilder(LookupBuilder): + def __init__(self, font, location): + LookupBuilder.__init__(self, font, location, 'GSUB', 3) + self.alternates = {} + + def equals(self, other): + return (LookupBuilder.equals(self, other) and + self.alternates == other.alternates) + + def build(self): + subtable = otl.buildAlternateSubstSubtable(self.alternates) + return self.buildLookup_([subtable]) + + def getAlternateGlyphs(self): + return self.alternates + + +class ChainContextPosBuilder(LookupBuilder): + def __init__(self, font, location): + LookupBuilder.__init__(self, font, location, 'GPOS', 8) + self.rules = [] # (prefix, input, suffix, lookups) + + def equals(self, other): + return (LookupBuilder.equals(self, other) and + self.rules == other.rules) + + def build(self): + subtables = [] + for (prefix, glyphs, suffix, lookups) in self.rules: + st = otTables.ChainContextPos() + subtables.append(st) + st.Format = 3 + self.setBacktrackCoverage_(prefix, st) + self.setLookAheadCoverage_(suffix, st) + self.setInputCoverage_(glyphs, st) + + st.PosCount = len([l for l in lookups if l is not None]) + st.PosLookupRecord = [] + for sequenceIndex, l in enumerate(lookups): + if l is not None: + rec = otTables.PosLookupRecord() + rec.SequenceIndex = sequenceIndex + rec.LookupListIndex = l.lookup_index + st.PosLookupRecord.append(rec) + return self.buildLookup_(subtables) + + +class ChainContextSubstBuilder(LookupBuilder): + def __init__(self, font, location): + LookupBuilder.__init__(self, font, location, 'GSUB', 6) + self.substitutions = [] # (prefix, input, suffix, lookups) + + def equals(self, other): + return (LookupBuilder.equals(self, other) and + self.substitutions == other.substitutions) + + def build(self): + subtables = [] + for (prefix, input, suffix, lookups) in self.substitutions: + st = otTables.ChainContextSubst() + subtables.append(st) + st.Format = 3 + self.setBacktrackCoverage_(prefix, st) + self.setLookAheadCoverage_(suffix, st) + self.setInputCoverage_(input, st) + + st.SubstCount = len([l for l in lookups if l is not None]) + st.SubstLookupRecord = [] + for sequenceIndex, l in enumerate(lookups): + if l is not None: + rec = otTables.SubstLookupRecord() + rec.SequenceIndex = sequenceIndex + rec.LookupListIndex = l.lookup_index + st.SubstLookupRecord.append(rec) + return self.buildLookup_(subtables) + + def getAlternateGlyphs(self): + result = {} + for (_prefix, _input, _suffix, lookups) in self.substitutions: + for lookup in lookups: + alts = lookup.getAlternateGlyphs() + for glyph, replacements in alts.items(): + result.setdefault(glyph, set()).update(replacements) + return result + + +class LigatureSubstBuilder(LookupBuilder): + def __init__(self, font, location): + LookupBuilder.__init__(self, font, location, 'GSUB', 4) + self.ligatures = {} # {('f','f','i'): 'f_f_i'} + + def equals(self, other): + return (LookupBuilder.equals(self, other) and + self.ligatures == other.ligatures) + + def build(self): + subtable = otl.buildLigatureSubstSubtable(self.ligatures) + return self.buildLookup_([subtable]) + + +class MultipleSubstBuilder(LookupBuilder): + def __init__(self, font, location): + LookupBuilder.__init__(self, font, location, 'GSUB', 2) + self.mapping = {} + + def equals(self, other): + return (LookupBuilder.equals(self, other) and + self.mapping == other.mapping) + + def build(self): + subtable = otl.buildMultipleSubstSubtable(self.mapping) + return self.buildLookup_([subtable]) + + +class CursivePosBuilder(LookupBuilder): + def __init__(self, font, location): + LookupBuilder.__init__(self, font, location, 'GPOS', 3) + self.attachments = {} + + def equals(self, other): + return (LookupBuilder.equals(self, other) and + self.attachments == other.attachments) + + def add_attachment(self, location, glyphs, entryAnchor, exitAnchor): + for glyph in glyphs: + self.attachments[glyph] = (entryAnchor, exitAnchor) + + def build(self): + st = otl.buildCursivePosSubtable(self.attachments, self.glyphMap) + return self.buildLookup_([st]) + + +class MarkBasePosBuilder(LookupBuilder): + def __init__(self, font, location): + LookupBuilder.__init__(self, font, location, 'GPOS', 4) + self.marks = {} # glyphName -> (markClassName, anchor) + self.bases = {} # glyphName -> {markClassName: anchor} + + def equals(self, other): + return (LookupBuilder.equals(self, other) and + self.marks == other.marks and + self.bases == other.bases) + + def inferGlyphClasses(self): + result = {glyph: 1 for glyph in self.bases} + result.update({glyph: 3 for glyph in self.marks}) + return result + + def build(self): + markClasses = self.buildMarkClasses_(self.marks) + marks = {mark: (markClasses[mc], anchor) + for mark, (mc, anchor) in self.marks.items()} + bases = {} + for glyph, anchors in self.bases.items(): + bases[glyph] = {markClasses[mc]: anchor + for (mc, anchor) in anchors.items()} + subtables = otl.buildMarkBasePos(marks, bases, self.glyphMap) + return self.buildLookup_(subtables) + + +class MarkLigPosBuilder(LookupBuilder): + def __init__(self, font, location): + LookupBuilder.__init__(self, font, location, 'GPOS', 5) + self.marks = {} # glyphName -> (markClassName, anchor) + self.ligatures = {} # glyphName -> [{markClassName: anchor}, ...] + + def equals(self, other): + return (LookupBuilder.equals(self, other) and + self.marks == other.marks and + self.ligatures == other.ligatures) + + def inferGlyphClasses(self): + result = {glyph: 2 for glyph in self.ligatures} + result.update({glyph: 3 for glyph in self.marks}) + return result + + def build(self): + markClasses = self.buildMarkClasses_(self.marks) + marks = {mark: (markClasses[mc], anchor) + for mark, (mc, anchor) in self.marks.items()} + ligs = {} + for lig, components in self.ligatures.items(): + ligs[lig] = [] + for c in components: + ligs[lig].append({markClasses[mc]: a for mc, a in c.items()}) + subtables = otl.buildMarkLigPos(marks, ligs, self.glyphMap) + return self.buildLookup_(subtables) + + +class MarkMarkPosBuilder(LookupBuilder): + def __init__(self, font, location): + LookupBuilder.__init__(self, font, location, 'GPOS', 6) + self.marks = {} # glyphName -> (markClassName, anchor) + self.baseMarks = {} # glyphName -> {markClassName: anchor} + + def equals(self, other): + return (LookupBuilder.equals(self, other) and + self.marks == other.marks and + self.baseMarks == other.baseMarks) + + def inferGlyphClasses(self): + result = {glyph: 3 for glyph in self.baseMarks} + result.update({glyph: 3 for glyph in self.marks}) + return result + + def build(self): + markClasses = self.buildMarkClasses_(self.marks) + markClassList = sorted(markClasses.keys(), key=markClasses.get) + marks = {mark: (markClasses[mc], anchor) + for mark, (mc, anchor) in self.marks.items()} + + st = otTables.MarkMarkPos() + st.Format = 1 + st.ClassCount = len(markClasses) + st.Mark1Coverage = otl.buildCoverage(marks, self.glyphMap) + st.Mark2Coverage = otl.buildCoverage(self.baseMarks, self.glyphMap) + st.Mark1Array = otl.buildMarkArray(marks, self.glyphMap) + st.Mark2Array = otTables.Mark2Array() + st.Mark2Array.Mark2Count = len(st.Mark2Coverage.glyphs) + st.Mark2Array.Mark2Record = [] + for base in st.Mark2Coverage.glyphs: + anchors = [self.baseMarks[base].get(mc) for mc in markClassList] + st.Mark2Array.Mark2Record.append(otl.buildMark2Record(anchors)) + return self.buildLookup_([st]) + + +class ReverseChainSingleSubstBuilder(LookupBuilder): + def __init__(self, font, location): + LookupBuilder.__init__(self, font, location, 'GSUB', 8) + self.substitutions = [] # (prefix, suffix, mapping) + + def equals(self, other): + return (LookupBuilder.equals(self, other) and + self.substitutions == other.substitutions) + + def build(self): + subtables = [] + for prefix, suffix, mapping in self.substitutions: + st = otTables.ReverseChainSingleSubst() + st.Format = 1 + self.setBacktrackCoverage_(prefix, st) + self.setLookAheadCoverage_(suffix, st) + st.Coverage = otl.buildCoverage(mapping.keys(), self.glyphMap) + st.GlyphCount = len(mapping) + st.Substitute = [mapping[g] for g in st.Coverage.glyphs] + subtables.append(st) + return self.buildLookup_(subtables) + + +class SingleSubstBuilder(LookupBuilder): + def __init__(self, font, location): + LookupBuilder.__init__(self, font, location, 'GSUB', 1) + self.mapping = {} + + def equals(self, other): + return (LookupBuilder.equals(self, other) and + self.mapping == other.mapping) + + def build(self): + subtable = otl.buildSingleSubstSubtable(self.mapping) + return self.buildLookup_([subtable]) + + def getAlternateGlyphs(self): + return {glyph: set([repl]) for glyph, repl in self.mapping.items()} + + +class ClassPairPosSubtableBuilder(object): + def __init__(self, builder, valueFormat1, valueFormat2): + self.builder_ = builder + self.classDef1_, self.classDef2_ = None, None + self.values_ = {} # (glyphclass1, glyphclass2) --> (value1, value2) + self.valueFormat1_, self.valueFormat2_ = valueFormat1, valueFormat2 + self.forceSubtableBreak_ = False + self.subtables_ = [] + + def addPair(self, gc1, value1, gc2, value2): + mergeable = (not self.forceSubtableBreak_ and + self.classDef1_ is not None and + self.classDef1_.canAdd(gc1) and + self.classDef2_ is not None and + self.classDef2_.canAdd(gc2)) + if not mergeable: + self.flush_() + self.classDef1_ = otl.ClassDefBuilder(useClass0=True) + self.classDef2_ = otl.ClassDefBuilder(useClass0=False) + self.values_ = {} + self.classDef1_.add(gc1) + self.classDef2_.add(gc2) + self.values_[(gc1, gc2)] = (value1, value2) + + def addSubtableBreak(self): + self.forceSubtableBreak_ = True + + def subtables(self): + self.flush_() + return self.subtables_ + + def flush_(self): + if self.classDef1_ is None or self.classDef2_ is None: + return + st = otl.buildPairPosClassesSubtable(self.values_, + self.builder_.glyphMap) + self.subtables_.append(st) + + +class PairPosBuilder(LookupBuilder): + SUBTABLE_BREAK_ = "SUBTABLE_BREAK" + + def __init__(self, font, location): + LookupBuilder.__init__(self, font, location, 'GPOS', 2) + self.pairs = [] # [(gc1, value1, gc2, value2)*] + self.glyphPairs = {} # (glyph1, glyph2) --> (value1, value2) + self.locations = {} # (gc1, gc2) --> (filepath, line, column) + + def addClassPair(self, location, glyphclass1, value1, glyphclass2, value2): + self.pairs.append((glyphclass1, value1, glyphclass2, value2)) + + def addGlyphPair(self, location, glyph1, value1, glyph2, value2): + key = (glyph1, glyph2) + oldValue = self.glyphPairs.get(key, None) + if oldValue is not None: + # the Feature File spec explicitly allows specific pairs generated + # by an 'enum' rule to be overridden by preceding single pairs; + # we emit a warning and use the previously defined value + otherLoc = self.locations[key] + log.warning( + 'Already defined position for pair %s %s at %s:%d:%d; ' + 'choosing the first value', + glyph1, glyph2, otherLoc[0], otherLoc[1], otherLoc[2]) + else: + val1, _ = makeOpenTypeValueRecord(value1, pairPosContext=True) + val2, _ = makeOpenTypeValueRecord(value2, pairPosContext=True) + self.glyphPairs[key] = (val1, val2) + self.locations[key] = location + + def add_subtable_break(self, location): + self.pairs.append((self.SUBTABLE_BREAK_, self.SUBTABLE_BREAK_, + self.SUBTABLE_BREAK_, self.SUBTABLE_BREAK_)) + + def equals(self, other): + return (LookupBuilder.equals(self, other) and + self.glyphPairs == other.glyphPairs and + self.pairs == other.pairs) + + def build(self): + builders = {} + builder = None + for glyphclass1, value1, glyphclass2, value2 in self.pairs: + if glyphclass1 is self.SUBTABLE_BREAK_: + if builder is not None: + builder.addSubtableBreak() + continue + val1, valFormat1 = makeOpenTypeValueRecord( + value1, pairPosContext=True) + val2, valFormat2 = makeOpenTypeValueRecord( + value2, pairPosContext=True) + builder = builders.get((valFormat1, valFormat2)) + if builder is None: + builder = ClassPairPosSubtableBuilder( + self, valFormat1, valFormat2) + builders[(valFormat1, valFormat2)] = builder + builder.addPair(glyphclass1, val1, glyphclass2, val2) + subtables = [] + if self.glyphPairs: + subtables.extend( + otl.buildPairPosGlyphs(self.glyphPairs, self.glyphMap)) + for key in sorted(builders.keys()): + subtables.extend(builders[key].subtables()) + return self.buildLookup_(subtables) + + +class SinglePosBuilder(LookupBuilder): + def __init__(self, font, location): + LookupBuilder.__init__(self, font, location, 'GPOS', 1) + self.locations = {} # glyph -> (filename, line, column) + self.mapping = {} # glyph -> otTables.ValueRecord + + def add_pos(self, location, glyph, valueRecord): + otValueRecord, _ = makeOpenTypeValueRecord( + valueRecord, pairPosContext=False) + if not self.can_add(glyph, otValueRecord): + otherLoc = self.locations[glyph] + raise FeatureLibError( + 'Already defined different position for glyph "%s" at %s:%d:%d' + % (glyph, otherLoc[0], otherLoc[1], otherLoc[2]), + location) + if otValueRecord: + self.mapping[glyph] = otValueRecord + self.locations[glyph] = location + + def can_add(self, glyph, value): + assert isinstance(value, otl.ValueRecord) + curValue = self.mapping.get(glyph) + return curValue is None or curValue == value + + def equals(self, other): + return (LookupBuilder.equals(self, other) and + self.mapping == other.mapping) + + def build(self): + subtables = otl.buildSinglePos(self.mapping, self.glyphMap) + return self.buildLookup_(subtables) diff --git a/Lib/fontTools/feaLib/error.py b/Lib/fontTools/feaLib/error.py new file mode 100644 index 0000000..8281f95 --- /dev/null +++ b/Lib/fontTools/feaLib/error.py @@ -0,0 +1,20 @@ +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals + + +class FeatureLibError(Exception): + def __init__(self, message, location): + Exception.__init__(self, message) + self.location = location + + def __str__(self): + message = Exception.__str__(self) + if self.location: + path, line, column = self.location + return "%s:%d:%d: %s" % (path, line, column, message) + else: + return message + + +class IncludedFeaNotFound(FeatureLibError): + pass diff --git a/Lib/fontTools/feaLib/lexer.py b/Lib/fontTools/feaLib/lexer.py new file mode 100644 index 0000000..18849ef --- /dev/null +++ b/Lib/fontTools/feaLib/lexer.py @@ -0,0 +1,261 @@ +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.misc.py23 import * +from fontTools.feaLib.error import FeatureLibError, IncludedFeaNotFound +import re +import os + + +class Lexer(object): + NUMBER = "NUMBER" + FLOAT = "FLOAT" + STRING = "STRING" + NAME = "NAME" + FILENAME = "FILENAME" + GLYPHCLASS = "GLYPHCLASS" + CID = "CID" + SYMBOL = "SYMBOL" + COMMENT = "COMMENT" + NEWLINE = "NEWLINE" + ANONYMOUS_BLOCK = "ANONYMOUS_BLOCK" + + CHAR_WHITESPACE_ = " \t" + CHAR_NEWLINE_ = "\r\n" + CHAR_SYMBOL_ = ",;:-+'{}[]<>()=" + CHAR_DIGIT_ = "0123456789" + CHAR_HEXDIGIT_ = "0123456789ABCDEFabcdef" + CHAR_LETTER_ = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + CHAR_NAME_START_ = CHAR_LETTER_ + "_+*:.^~!\\" + CHAR_NAME_CONTINUATION_ = CHAR_LETTER_ + CHAR_DIGIT_ + "_.+*:^~!/-" + + RE_GLYPHCLASS = re.compile(r"^[A-Za-z_0-9.]+$") + + MODE_NORMAL_ = "NORMAL" + MODE_FILENAME_ = "FILENAME" + + def __init__(self, text, filename): + self.filename_ = filename + self.line_ = 1 + self.pos_ = 0 + self.line_start_ = 0 + self.text_ = text + self.text_length_ = len(text) + self.mode_ = Lexer.MODE_NORMAL_ + + def __iter__(self): + return self + + def next(self): # Python 2 + return self.__next__() + + def __next__(self): # Python 3 + while True: + token_type, token, location = self.next_() + if token_type != Lexer.NEWLINE: + return (token_type, token, location) + + def location_(self): + column = self.pos_ - self.line_start_ + 1 + return (self.filename_ or "", self.line_, column) + + def next_(self): + self.scan_over_(Lexer.CHAR_WHITESPACE_) + location = self.location_() + start = self.pos_ + text = self.text_ + limit = len(text) + if start >= limit: + raise StopIteration() + cur_char = text[start] + next_char = text[start + 1] if start + 1 < limit else None + + if cur_char == "\n": + self.pos_ += 1 + self.line_ += 1 + self.line_start_ = self.pos_ + return (Lexer.NEWLINE, None, location) + if cur_char == "\r": + self.pos_ += (2 if next_char == "\n" else 1) + self.line_ += 1 + self.line_start_ = self.pos_ + return (Lexer.NEWLINE, None, location) + if cur_char == "#": + self.scan_until_(Lexer.CHAR_NEWLINE_) + return (Lexer.COMMENT, text[start:self.pos_], location) + + if self.mode_ is Lexer.MODE_FILENAME_: + if cur_char != "(": + raise FeatureLibError("Expected '(' before file name", + location) + self.scan_until_(")") + cur_char = text[self.pos_] if self.pos_ < limit else None + if cur_char != ")": + raise FeatureLibError("Expected ')' after file name", + location) + self.pos_ += 1 + self.mode_ = Lexer.MODE_NORMAL_ + return (Lexer.FILENAME, text[start + 1:self.pos_ - 1], location) + + if cur_char == "\\" and next_char in Lexer.CHAR_DIGIT_: + self.pos_ += 1 + self.scan_over_(Lexer.CHAR_DIGIT_) + return (Lexer.CID, int(text[start + 1:self.pos_], 10), location) + if cur_char == "@": + self.pos_ += 1 + self.scan_over_(Lexer.CHAR_NAME_CONTINUATION_) + glyphclass = text[start + 1:self.pos_] + if len(glyphclass) < 1: + raise FeatureLibError("Expected glyph class name", location) + if len(glyphclass) > 63: + raise FeatureLibError( + "Glyph class names must not be longer than 63 characters", + location) + if not Lexer.RE_GLYPHCLASS.match(glyphclass): + raise FeatureLibError( + "Glyph class names must consist of letters, digits, " + "underscore, or period", location) + return (Lexer.GLYPHCLASS, glyphclass, location) + if cur_char in Lexer.CHAR_NAME_START_: + self.pos_ += 1 + self.scan_over_(Lexer.CHAR_NAME_CONTINUATION_) + token = text[start:self.pos_] + if token == "include": + self.mode_ = Lexer.MODE_FILENAME_ + return (Lexer.NAME, token, location) + if cur_char == "0" and next_char in "xX": + self.pos_ += 2 + self.scan_over_(Lexer.CHAR_HEXDIGIT_) + return (Lexer.NUMBER, int(text[start:self.pos_], 16), location) + if cur_char in Lexer.CHAR_DIGIT_: + self.scan_over_(Lexer.CHAR_DIGIT_) + if self.pos_ >= limit or text[self.pos_] != ".": + return (Lexer.NUMBER, int(text[start:self.pos_], 10), location) + self.scan_over_(".") + self.scan_over_(Lexer.CHAR_DIGIT_) + return (Lexer.FLOAT, float(text[start:self.pos_]), location) + if cur_char == "-" and next_char in Lexer.CHAR_DIGIT_: + self.pos_ += 1 + self.scan_over_(Lexer.CHAR_DIGIT_) + if self.pos_ >= limit or text[self.pos_] != ".": + return (Lexer.NUMBER, int(text[start:self.pos_], 10), location) + self.scan_over_(".") + self.scan_over_(Lexer.CHAR_DIGIT_) + return (Lexer.FLOAT, float(text[start:self.pos_]), location) + if cur_char in Lexer.CHAR_SYMBOL_: + self.pos_ += 1 + return (Lexer.SYMBOL, cur_char, location) + if cur_char == '"': + self.pos_ += 1 + self.scan_until_('"') + if self.pos_ < self.text_length_ and self.text_[self.pos_] == '"': + self.pos_ += 1 + # strip newlines embedded within a string + string = re.sub("[\r\n]", "", text[start + 1:self.pos_ - 1]) + return (Lexer.STRING, string, location) + else: + raise FeatureLibError("Expected '\"' to terminate string", + location) + raise FeatureLibError("Unexpected character: %r" % cur_char, + location) + + def scan_over_(self, valid): + p = self.pos_ + while p < self.text_length_ and self.text_[p] in valid: + p += 1 + self.pos_ = p + + def scan_until_(self, stop_at): + p = self.pos_ + while p < self.text_length_ and self.text_[p] not in stop_at: + p += 1 + self.pos_ = p + + def scan_anonymous_block(self, tag): + location = self.location_() + tag = tag.strip() + self.scan_until_(Lexer.CHAR_NEWLINE_) + self.scan_over_(Lexer.CHAR_NEWLINE_) + regexp = r'}\s*' + tag + r'\s*;' + split = re.split(regexp, self.text_[self.pos_:], maxsplit=1) + if len(split) != 2: + raise FeatureLibError( + "Expected '} %s;' to terminate anonymous block" % tag, + location) + self.pos_ += len(split[0]) + return (Lexer.ANONYMOUS_BLOCK, split[0], location) + + +class IncludingLexer(object): + def __init__(self, featurefile): + self.lexers_ = [self.make_lexer_(featurefile)] + self.featurefilepath = self.lexers_[0].filename_ + + def __iter__(self): + return self + + def next(self): # Python 2 + return self.__next__() + + def __next__(self): # Python 3 + while self.lexers_: + lexer = self.lexers_[-1] + try: + token_type, token, location = next(lexer) + except StopIteration: + self.lexers_.pop() + continue + if token_type is Lexer.NAME and token == "include": + fname_type, fname_token, fname_location = lexer.next() + if fname_type is not Lexer.FILENAME: + raise FeatureLibError("Expected file name", fname_location) + #semi_type, semi_token, semi_location = lexer.next() + #if semi_type is not Lexer.SYMBOL or semi_token != ";": + # raise FeatureLibError("Expected ';'", semi_location) + if os.path.isabs(fname_token): + path = fname_token + else: + if self.featurefilepath is not None: + curpath = os.path.dirname(self.featurefilepath) + else: + # if the IncludingLexer was initialized from an in-memory + # file-like stream, it doesn't have a 'name' pointing to + # its filesystem path, therefore we fall back to using the + # current working directory to resolve relative includes + curpath = os.getcwd() + path = os.path.join(curpath, fname_token) + if len(self.lexers_) >= 5: + raise FeatureLibError("Too many recursive includes", + fname_location) + try: + self.lexers_.append(self.make_lexer_(path)) + except IOError as err: + # FileNotFoundError does not exist on Python < 3.3 + import errno + if err.errno == errno.ENOENT: + raise IncludedFeaNotFound(fname_token, fname_location) + raise # pragma: no cover + else: + return (token_type, token, location) + raise StopIteration() + + @staticmethod + def make_lexer_(file_or_path): + if hasattr(file_or_path, "read"): + fileobj, closing = file_or_path, False + else: + filename, closing = file_or_path, True + fileobj = open(filename, "r", encoding="utf-8") + data = fileobj.read() + filename = getattr(fileobj, "name", None) + if closing: + fileobj.close() + return Lexer(data, filename) + + def scan_anonymous_block(self, tag): + return self.lexers_[-1].scan_anonymous_block(tag) + + +class NonIncludingLexer(IncludingLexer): + """Lexer that does not follow `include` statements, emits them as-is.""" + def __next__(self): # Python 3 + return next(self.lexers_[0]) diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py new file mode 100644 index 0000000..2b2f2d4 --- /dev/null +++ b/Lib/fontTools/feaLib/parser.py @@ -0,0 +1,1694 @@ +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.feaLib.error import FeatureLibError +from fontTools.feaLib.lexer import Lexer, IncludingLexer, NonIncludingLexer +from fontTools.misc.encodingTools import getEncoding +from fontTools.misc.py23 import * +import fontTools.feaLib.ast as ast +import logging +import os +import re + + +log = logging.getLogger(__name__) + + +class Parser(object): + extensions = {} + ast = ast + SS_FEATURE_TAGS = {"ss%02d" % i for i in range(1, 20+1)} + CV_FEATURE_TAGS = {"cv%02d" % i for i in range(1, 99+1)} + + def __init__(self, featurefile, glyphNames=(), followIncludes=True, + **kwargs): + if "glyphMap" in kwargs: + from fontTools.misc.loggingTools import deprecateArgument + deprecateArgument("glyphMap", "use 'glyphNames' (iterable) instead") + if glyphNames: + raise TypeError("'glyphNames' and (deprecated) 'glyphMap' are " + "mutually exclusive") + glyphNames = kwargs.pop("glyphMap") + if kwargs: + raise TypeError("unsupported keyword argument%s: %s" + % ("" if len(kwargs) == 1 else "s", + ", ".join(repr(k) for k in kwargs))) + + self.glyphNames_ = set(glyphNames) + self.doc_ = self.ast.FeatureFile() + self.anchors_ = SymbolTable() + self.glyphclasses_ = SymbolTable() + self.lookups_ = SymbolTable() + self.valuerecords_ = SymbolTable() + self.symbol_tables_ = { + self.anchors_, self.valuerecords_ + } + self.next_token_type_, self.next_token_ = (None, None) + self.cur_comments_ = [] + self.next_token_location_ = None + lexerClass = IncludingLexer if followIncludes else NonIncludingLexer + self.lexer_ = lexerClass(featurefile) + self.advance_lexer_(comments=True) + + def parse(self): + statements = self.doc_.statements + while self.next_token_type_ is not None or self.cur_comments_: + self.advance_lexer_(comments=True) + if self.cur_token_type_ is Lexer.COMMENT: + statements.append( + self.ast.Comment(self.cur_token_, + location=self.cur_token_location_)) + elif self.is_cur_keyword_("include"): + statements.append(self.parse_include_()) + elif self.cur_token_type_ is Lexer.GLYPHCLASS: + statements.append(self.parse_glyphclass_definition_()) + elif self.is_cur_keyword_(("anon", "anonymous")): + statements.append(self.parse_anonymous_()) + elif self.is_cur_keyword_("anchorDef"): + statements.append(self.parse_anchordef_()) + elif self.is_cur_keyword_("languagesystem"): + statements.append(self.parse_languagesystem_()) + elif self.is_cur_keyword_("lookup"): + statements.append(self.parse_lookup_(vertical=False)) + elif self.is_cur_keyword_("markClass"): + statements.append(self.parse_markClass_()) + elif self.is_cur_keyword_("feature"): + statements.append(self.parse_feature_block_()) + elif self.is_cur_keyword_("table"): + statements.append(self.parse_table_()) + elif self.is_cur_keyword_("valueRecordDef"): + statements.append( + self.parse_valuerecord_definition_(vertical=False)) + elif self.cur_token_type_ is Lexer.NAME and self.cur_token_ in self.extensions: + statements.append(self.extensions[self.cur_token_](self)) + elif self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == ";": + continue + else: + raise FeatureLibError( + "Expected feature, languagesystem, lookup, markClass, " + "table, or glyph class definition, got {} \"{}\"".format(self.cur_token_type_, self.cur_token_), + self.cur_token_location_) + return self.doc_ + + def parse_anchor_(self): + self.expect_symbol_("<") + self.expect_keyword_("anchor") + location = self.cur_token_location_ + + if self.next_token_ == "NULL": + self.expect_keyword_("NULL") + self.expect_symbol_(">") + return None + + if self.next_token_type_ == Lexer.NAME: + name = self.expect_name_() + anchordef = self.anchors_.resolve(name) + if anchordef is None: + raise FeatureLibError( + 'Unknown anchor "%s"' % name, + self.cur_token_location_) + self.expect_symbol_(">") + return self.ast.Anchor(anchordef.x, anchordef.y, + name=name, + contourpoint=anchordef.contourpoint, + xDeviceTable=None, yDeviceTable=None, + location=location) + + x, y = self.expect_number_(), self.expect_number_() + + contourpoint = None + if self.next_token_ == "contourpoint": + self.expect_keyword_("contourpoint") + contourpoint = self.expect_number_() + + if self.next_token_ == "<": + xDeviceTable = self.parse_device_() + yDeviceTable = self.parse_device_() + else: + xDeviceTable, yDeviceTable = None, None + + self.expect_symbol_(">") + return self.ast.Anchor(x, y, name=None, + contourpoint=contourpoint, + xDeviceTable=xDeviceTable, + yDeviceTable=yDeviceTable, + location=location) + + def parse_anchor_marks_(self): + """Parses a sequence of [ mark @MARKCLASS]*.""" + anchorMarks = [] # [(self.ast.Anchor, markClassName)*] + while self.next_token_ == "<": + anchor = self.parse_anchor_() + if anchor is None and self.next_token_ != "mark": + continue # without mark, eg. in GPOS type 5 + self.expect_keyword_("mark") + markClass = self.expect_markClass_reference_() + anchorMarks.append((anchor, markClass)) + return anchorMarks + + def parse_anchordef_(self): + assert self.is_cur_keyword_("anchorDef") + location = self.cur_token_location_ + x, y = self.expect_number_(), self.expect_number_() + contourpoint = None + if self.next_token_ == "contourpoint": + self.expect_keyword_("contourpoint") + contourpoint = self.expect_number_() + name = self.expect_name_() + self.expect_symbol_(";") + anchordef = self.ast.AnchorDefinition(name, x, y, + contourpoint=contourpoint, + location=location) + self.anchors_.define(name, anchordef) + return anchordef + + def parse_anonymous_(self): + assert self.is_cur_keyword_(("anon", "anonymous")) + tag = self.expect_tag_() + _, content, location = self.lexer_.scan_anonymous_block(tag) + self.advance_lexer_() + self.expect_symbol_('}') + end_tag = self.expect_tag_() + assert tag == end_tag, "bad splitting in Lexer.scan_anonymous_block()" + self.expect_symbol_(';') + return self.ast.AnonymousBlock(tag, content, location=location) + + def parse_attach_(self): + assert self.is_cur_keyword_("Attach") + location = self.cur_token_location_ + glyphs = self.parse_glyphclass_(accept_glyphname=True) + contourPoints = {self.expect_number_()} + while self.next_token_ != ";": + contourPoints.add(self.expect_number_()) + self.expect_symbol_(";") + return self.ast.AttachStatement(glyphs, contourPoints, + location=location) + + def parse_enumerate_(self, vertical): + assert self.cur_token_ in {"enumerate", "enum"} + self.advance_lexer_() + return self.parse_position_(enumerated=True, vertical=vertical) + + def parse_GlyphClassDef_(self): + """Parses 'GlyphClassDef @BASE, @LIGATURES, @MARKS, @COMPONENTS;'""" + assert self.is_cur_keyword_("GlyphClassDef") + location = self.cur_token_location_ + if self.next_token_ != ",": + baseGlyphs = self.parse_glyphclass_(accept_glyphname=False) + else: + baseGlyphs = None + self.expect_symbol_(",") + if self.next_token_ != ",": + ligatureGlyphs = self.parse_glyphclass_(accept_glyphname=False) + else: + ligatureGlyphs = None + self.expect_symbol_(",") + if self.next_token_ != ",": + markGlyphs = self.parse_glyphclass_(accept_glyphname=False) + else: + markGlyphs = None + self.expect_symbol_(",") + if self.next_token_ != ";": + componentGlyphs = self.parse_glyphclass_(accept_glyphname=False) + else: + componentGlyphs = None + self.expect_symbol_(";") + return self.ast.GlyphClassDefStatement(baseGlyphs, markGlyphs, + ligatureGlyphs, componentGlyphs, + location=location) + + def parse_glyphclass_definition_(self): + """Parses glyph class definitions such as '@UPPERCASE = [A-Z];'""" + location, name = self.cur_token_location_, self.cur_token_ + self.expect_symbol_("=") + glyphs = self.parse_glyphclass_(accept_glyphname=False) + self.expect_symbol_(";") + glyphclass = self.ast.GlyphClassDefinition(name, glyphs, + location=location) + self.glyphclasses_.define(name, glyphclass) + return glyphclass + + def split_glyph_range_(self, name, location): + # Since v1.20, the OpenType Feature File specification allows + # for dashes in glyph names. A sequence like "a-b-c-d" could + # therefore mean a single glyph whose name happens to be + # "a-b-c-d", or it could mean a range from glyph "a" to glyph + # "b-c-d", or a range from glyph "a-b" to glyph "c-d", or a + # range from glyph "a-b-c" to glyph "d".Technically, this + # example could be resolved because the (pretty complex) + # definition of glyph ranges renders most of these splits + # invalid. But the specification does not say that a compiler + # should try to apply such fancy heuristics. To encourage + # unambiguous feature files, we therefore try all possible + # splits and reject the feature file if there are multiple + # splits possible. It is intentional that we don't just emit a + # warning; warnings tend to get ignored. To fix the problem, + # font designers can trivially add spaces around the intended + # split point, and we emit a compiler error that suggests + # how exactly the source should be rewritten to make things + # unambiguous. + parts = name.split("-") + solutions = [] + for i in range(len(parts)): + start, limit = "-".join(parts[0:i]), "-".join(parts[i:]) + if start in self.glyphNames_ and limit in self.glyphNames_: + solutions.append((start, limit)) + if len(solutions) == 1: + start, limit = solutions[0] + return start, limit + elif len(solutions) == 0: + raise FeatureLibError( + "\"%s\" is not a glyph in the font, and it can not be split " + "into a range of known glyphs" % name, location) + else: + ranges = " or ".join(["\"%s - %s\"" % (s, l) for s, l in solutions]) + raise FeatureLibError( + "Ambiguous glyph range \"%s\"; " + "please use %s to clarify what you mean" % (name, ranges), + location) + + def parse_glyphclass_(self, accept_glyphname): + if (accept_glyphname and + self.next_token_type_ in (Lexer.NAME, Lexer.CID)): + glyph = self.expect_glyph_() + return self.ast.GlyphName(glyph, location=self.cur_token_location_) + if self.next_token_type_ is Lexer.GLYPHCLASS: + self.advance_lexer_() + gc = self.glyphclasses_.resolve(self.cur_token_) + if gc is None: + raise FeatureLibError( + "Unknown glyph class @%s" % self.cur_token_, + self.cur_token_location_) + if isinstance(gc, self.ast.MarkClass): + return self.ast.MarkClassName( + gc, location=self.cur_token_location_) + else: + return self.ast.GlyphClassName( + gc, location=self.cur_token_location_) + + self.expect_symbol_("[") + location = self.cur_token_location_ + glyphs = self.ast.GlyphClass(location=location) + while self.next_token_ != "]": + if self.next_token_type_ is Lexer.NAME: + glyph = self.expect_glyph_() + location = self.cur_token_location_ + if '-' in glyph and glyph not in self.glyphNames_: + start, limit = self.split_glyph_range_(glyph, location) + glyphs.add_range( + start, limit, + self.make_glyph_range_(location, start, limit)) + elif self.next_token_ == "-": + start = glyph + self.expect_symbol_("-") + limit = self.expect_glyph_() + glyphs.add_range( + start, limit, + self.make_glyph_range_(location, start, limit)) + else: + glyphs.append(glyph) + elif self.next_token_type_ is Lexer.CID: + glyph = self.expect_glyph_() + if self.next_token_ == "-": + range_location = self.cur_token_location_ + range_start = self.cur_token_ + self.expect_symbol_("-") + range_end = self.expect_cid_() + glyphs.add_cid_range(range_start, range_end, + self.make_cid_range_(range_location, + range_start, range_end)) + else: + glyphs.append("cid%05d" % self.cur_token_) + elif self.next_token_type_ is Lexer.GLYPHCLASS: + self.advance_lexer_() + gc = self.glyphclasses_.resolve(self.cur_token_) + if gc is None: + raise FeatureLibError( + "Unknown glyph class @%s" % self.cur_token_, + self.cur_token_location_) + if isinstance(gc, self.ast.MarkClass): + gc = self.ast.MarkClassName( + gc, location=self.cur_token_location_) + else: + gc = self.ast.GlyphClassName( + gc, location=self.cur_token_location_) + glyphs.add_class(gc) + else: + raise FeatureLibError( + "Expected glyph name, glyph range, " + "or glyph class reference", + self.next_token_location_) + self.expect_symbol_("]") + return glyphs + + def parse_class_name_(self): + name = self.expect_class_name_() + gc = self.glyphclasses_.resolve(name) + if gc is None: + raise FeatureLibError( + "Unknown glyph class @%s" % name, + self.cur_token_location_) + if isinstance(gc, self.ast.MarkClass): + return self.ast.MarkClassName( + gc, location=self.cur_token_location_) + else: + return self.ast.GlyphClassName( + gc, location=self.cur_token_location_) + + def parse_glyph_pattern_(self, vertical): + prefix, glyphs, lookups, values, suffix = ([], [], [], [], []) + hasMarks = False + while self.next_token_ not in {"by", "from", ";", ","}: + gc = self.parse_glyphclass_(accept_glyphname=True) + marked = False + if self.next_token_ == "'": + self.expect_symbol_("'") + hasMarks = marked = True + if marked: + if suffix: + # makeotf also reports this as an error, while FontForge + # silently inserts ' in all the intervening glyphs. + # https://github.com/fonttools/fonttools/pull/1096 + raise FeatureLibError( + "Unsupported contextual target sequence: at most " + "one run of marked (') glyph/class names allowed", + self.cur_token_location_) + glyphs.append(gc) + elif glyphs: + suffix.append(gc) + else: + prefix.append(gc) + + if self.is_next_value_(): + values.append(self.parse_valuerecord_(vertical)) + else: + values.append(None) + + lookup = None + if self.next_token_ == "lookup": + self.expect_keyword_("lookup") + if not marked: + raise FeatureLibError( + "Lookups can only follow marked glyphs", + self.cur_token_location_) + lookup_name = self.expect_name_() + lookup = self.lookups_.resolve(lookup_name) + if lookup is None: + raise FeatureLibError( + 'Unknown lookup "%s"' % lookup_name, + self.cur_token_location_) + if marked: + lookups.append(lookup) + + if not glyphs and not suffix: # eg., "sub f f i by" + assert lookups == [] + return ([], prefix, [None] * len(prefix), values, [], hasMarks) + else: + assert not any(values[:len(prefix)]), values + values = values[len(prefix):][:len(glyphs)] + return (prefix, glyphs, lookups, values, suffix, hasMarks) + + def parse_chain_context_(self): + location = self.cur_token_location_ + prefix, glyphs, lookups, values, suffix, hasMarks = \ + self.parse_glyph_pattern_(vertical=False) + chainContext = [(prefix, glyphs, suffix)] + hasLookups = any(lookups) + while self.next_token_ == ",": + self.expect_symbol_(",") + prefix, glyphs, lookups, values, suffix, hasMarks = \ + self.parse_glyph_pattern_(vertical=False) + chainContext.append((prefix, glyphs, suffix)) + hasLookups = hasLookups or any(lookups) + self.expect_symbol_(";") + return chainContext, hasLookups + + def parse_ignore_(self): + assert self.is_cur_keyword_("ignore") + location = self.cur_token_location_ + self.advance_lexer_() + if self.cur_token_ in ["substitute", "sub"]: + chainContext, hasLookups = self.parse_chain_context_() + if hasLookups: + raise FeatureLibError( + "No lookups can be specified for \"ignore sub\"", + location) + return self.ast.IgnoreSubstStatement(chainContext, + location=location) + if self.cur_token_ in ["position", "pos"]: + chainContext, hasLookups = self.parse_chain_context_() + if hasLookups: + raise FeatureLibError( + "No lookups can be specified for \"ignore pos\"", + location) + return self.ast.IgnorePosStatement(chainContext, + location=location) + raise FeatureLibError( + "Expected \"substitute\" or \"position\"", + self.cur_token_location_) + + def parse_include_(self): + assert self.cur_token_ == "include" + location = self.cur_token_location_ + filename = self.expect_filename_() + # self.expect_symbol_(";") + return ast.IncludeStatement(filename, location=location) + + def parse_language_(self): + assert self.is_cur_keyword_("language") + location = self.cur_token_location_ + language = self.expect_language_tag_() + include_default, required = (True, False) + if self.next_token_ in {"exclude_dflt", "include_dflt"}: + include_default = (self.expect_name_() == "include_dflt") + if self.next_token_ == "required": + self.expect_keyword_("required") + required = True + self.expect_symbol_(";") + return self.ast.LanguageStatement(language, + include_default, required, + location=location) + + def parse_ligatureCaretByIndex_(self): + assert self.is_cur_keyword_("LigatureCaretByIndex") + location = self.cur_token_location_ + glyphs = self.parse_glyphclass_(accept_glyphname=True) + carets = [self.expect_number_()] + while self.next_token_ != ";": + carets.append(self.expect_number_()) + self.expect_symbol_(";") + return self.ast.LigatureCaretByIndexStatement(glyphs, carets, + location=location) + + def parse_ligatureCaretByPos_(self): + assert self.is_cur_keyword_("LigatureCaretByPos") + location = self.cur_token_location_ + glyphs = self.parse_glyphclass_(accept_glyphname=True) + carets = [self.expect_number_()] + while self.next_token_ != ";": + carets.append(self.expect_number_()) + self.expect_symbol_(";") + return self.ast.LigatureCaretByPosStatement(glyphs, carets, + location=location) + + def parse_lookup_(self, vertical): + assert self.is_cur_keyword_("lookup") + location, name = self.cur_token_location_, self.expect_name_() + + if self.next_token_ == ";": + lookup = self.lookups_.resolve(name) + if lookup is None: + raise FeatureLibError("Unknown lookup \"%s\"" % name, + self.cur_token_location_) + self.expect_symbol_(";") + return self.ast.LookupReferenceStatement(lookup, + location=location) + + use_extension = False + if self.next_token_ == "useExtension": + self.expect_keyword_("useExtension") + use_extension = True + + block = self.ast.LookupBlock(name, use_extension, location=location) + self.parse_block_(block, vertical) + self.lookups_.define(name, block) + return block + + def parse_lookupflag_(self): + assert self.is_cur_keyword_("lookupflag") + location = self.cur_token_location_ + + # format B: "lookupflag 6;" + if self.next_token_type_ == Lexer.NUMBER: + value = self.expect_number_() + self.expect_symbol_(";") + return self.ast.LookupFlagStatement(value, location=location) + + # format A: "lookupflag RightToLeft MarkAttachmentType @M;" + value, markAttachment, markFilteringSet = 0, None, None + flags = { + "RightToLeft": 1, "IgnoreBaseGlyphs": 2, + "IgnoreLigatures": 4, "IgnoreMarks": 8 + } + seen = set() + while self.next_token_ != ";": + if self.next_token_ in seen: + raise FeatureLibError( + "%s can be specified only once" % self.next_token_, + self.next_token_location_) + seen.add(self.next_token_) + if self.next_token_ == "MarkAttachmentType": + self.expect_keyword_("MarkAttachmentType") + markAttachment = self.parse_class_name_() + elif self.next_token_ == "UseMarkFilteringSet": + self.expect_keyword_("UseMarkFilteringSet") + markFilteringSet = self.parse_class_name_() + elif self.next_token_ in flags: + value = value | flags[self.expect_name_()] + else: + raise FeatureLibError( + '"%s" is not a recognized lookupflag' % self.next_token_, + self.next_token_location_) + self.expect_symbol_(";") + return self.ast.LookupFlagStatement(value, + markAttachment=markAttachment, + markFilteringSet=markFilteringSet, + location=location) + + def parse_markClass_(self): + assert self.is_cur_keyword_("markClass") + location = self.cur_token_location_ + glyphs = self.parse_glyphclass_(accept_glyphname=True) + anchor = self.parse_anchor_() + name = self.expect_class_name_() + self.expect_symbol_(";") + markClass = self.doc_.markClasses.get(name) + if markClass is None: + markClass = self.ast.MarkClass(name) + self.doc_.markClasses[name] = markClass + self.glyphclasses_.define(name, markClass) + mcdef = self.ast.MarkClassDefinition(markClass, anchor, glyphs, + location=location) + markClass.addDefinition(mcdef) + return mcdef + + def parse_position_(self, enumerated, vertical): + assert self.cur_token_ in {"position", "pos"} + if self.next_token_ == "cursive": # GPOS type 3 + return self.parse_position_cursive_(enumerated, vertical) + elif self.next_token_ == "base": # GPOS type 4 + return self.parse_position_base_(enumerated, vertical) + elif self.next_token_ == "ligature": # GPOS type 5 + return self.parse_position_ligature_(enumerated, vertical) + elif self.next_token_ == "mark": # GPOS type 6 + return self.parse_position_mark_(enumerated, vertical) + + location = self.cur_token_location_ + prefix, glyphs, lookups, values, suffix, hasMarks = \ + self.parse_glyph_pattern_(vertical) + self.expect_symbol_(";") + + if any(lookups): + # GPOS type 8: Chaining contextual positioning; explicit lookups + if any(values): + raise FeatureLibError( + "If \"lookup\" is present, no values must be specified", + location) + return self.ast.ChainContextPosStatement( + prefix, glyphs, suffix, lookups, location=location) + + # Pair positioning, format A: "pos V 10 A -10;" + # Pair positioning, format B: "pos V A -20;" + if not prefix and not suffix and len(glyphs) == 2 and not hasMarks: + if values[0] is None: # Format B: "pos V A -20;" + values.reverse() + return self.ast.PairPosStatement( + glyphs[0], values[0], glyphs[1], values[1], + enumerated=enumerated, + location=location) + + if enumerated: + raise FeatureLibError( + '"enumerate" is only allowed with pair positionings', location) + return self.ast.SinglePosStatement(list(zip(glyphs, values)), + prefix, suffix, forceChain=hasMarks, + location=location) + + def parse_position_cursive_(self, enumerated, vertical): + location = self.cur_token_location_ + self.expect_keyword_("cursive") + if enumerated: + raise FeatureLibError( + '"enumerate" is not allowed with ' + 'cursive attachment positioning', + location) + glyphclass = self.parse_glyphclass_(accept_glyphname=True) + entryAnchor = self.parse_anchor_() + exitAnchor = self.parse_anchor_() + self.expect_symbol_(";") + return self.ast.CursivePosStatement( + glyphclass, entryAnchor, exitAnchor, location=location) + + def parse_position_base_(self, enumerated, vertical): + location = self.cur_token_location_ + self.expect_keyword_("base") + if enumerated: + raise FeatureLibError( + '"enumerate" is not allowed with ' + 'mark-to-base attachment positioning', + location) + base = self.parse_glyphclass_(accept_glyphname=True) + marks = self.parse_anchor_marks_() + self.expect_symbol_(";") + return self.ast.MarkBasePosStatement(base, marks, location=location) + + def parse_position_ligature_(self, enumerated, vertical): + location = self.cur_token_location_ + self.expect_keyword_("ligature") + if enumerated: + raise FeatureLibError( + '"enumerate" is not allowed with ' + 'mark-to-ligature attachment positioning', + location) + ligatures = self.parse_glyphclass_(accept_glyphname=True) + marks = [self.parse_anchor_marks_()] + while self.next_token_ == "ligComponent": + self.expect_keyword_("ligComponent") + marks.append(self.parse_anchor_marks_()) + self.expect_symbol_(";") + return self.ast.MarkLigPosStatement(ligatures, marks, location=location) + + def parse_position_mark_(self, enumerated, vertical): + location = self.cur_token_location_ + self.expect_keyword_("mark") + if enumerated: + raise FeatureLibError( + '"enumerate" is not allowed with ' + 'mark-to-mark attachment positioning', + location) + baseMarks = self.parse_glyphclass_(accept_glyphname=True) + marks = self.parse_anchor_marks_() + self.expect_symbol_(";") + return self.ast.MarkMarkPosStatement(baseMarks, marks, + location=location) + + def parse_script_(self): + assert self.is_cur_keyword_("script") + location, script = self.cur_token_location_, self.expect_script_tag_() + self.expect_symbol_(";") + return self.ast.ScriptStatement(script, location=location) + + def parse_substitute_(self): + assert self.cur_token_ in {"substitute", "sub", "reversesub", "rsub"} + location = self.cur_token_location_ + reverse = self.cur_token_ in {"reversesub", "rsub"} + old_prefix, old, lookups, values, old_suffix, hasMarks = \ + self.parse_glyph_pattern_(vertical=False) + if any(values): + raise FeatureLibError( + "Substitution statements cannot contain values", location) + new = [] + if self.next_token_ == "by": + keyword = self.expect_keyword_("by") + while self.next_token_ != ";": + gc = self.parse_glyphclass_(accept_glyphname=True) + new.append(gc) + elif self.next_token_ == "from": + keyword = self.expect_keyword_("from") + new = [self.parse_glyphclass_(accept_glyphname=False)] + else: + keyword = None + self.expect_symbol_(";") + if len(new) is 0 and not any(lookups): + raise FeatureLibError( + 'Expected "by", "from" or explicit lookup references', + self.cur_token_location_) + + # GSUB lookup type 3: Alternate substitution. + # Format: "substitute a from [a.1 a.2 a.3];" + if keyword == "from": + if reverse: + raise FeatureLibError( + 'Reverse chaining substitutions do not support "from"', + location) + if len(old) != 1 or len(old[0].glyphSet()) != 1: + raise FeatureLibError( + 'Expected a single glyph before "from"', + location) + if len(new) != 1: + raise FeatureLibError( + 'Expected a single glyphclass after "from"', + location) + return self.ast.AlternateSubstStatement( + old_prefix, old[0], old_suffix, new[0], location=location) + + num_lookups = len([l for l in lookups if l is not None]) + + # GSUB lookup type 1: Single substitution. + # Format A: "substitute a by a.sc;" + # Format B: "substitute [one.fitted one.oldstyle] by one;" + # Format C: "substitute [a-d] by [A.sc-D.sc];" + if (not reverse and len(old) == 1 and len(new) == 1 and + num_lookups == 0): + glyphs = list(old[0].glyphSet()) + replacements = list(new[0].glyphSet()) + if len(replacements) == 1: + replacements = replacements * len(glyphs) + if len(glyphs) != len(replacements): + raise FeatureLibError( + 'Expected a glyph class with %d elements after "by", ' + 'but found a glyph class with %d elements' % + (len(glyphs), len(replacements)), location) + return self.ast.SingleSubstStatement( + old, new, + old_prefix, old_suffix, + forceChain=hasMarks, + location=location + ) + + # GSUB lookup type 2: Multiple substitution. + # Format: "substitute f_f_i by f f i;" + if (not reverse and + len(old) == 1 and len(old[0].glyphSet()) == 1 and + len(new) > 1 and max([len(n.glyphSet()) for n in new]) == 1 and + num_lookups == 0): + return self.ast.MultipleSubstStatement( + old_prefix, tuple(old[0].glyphSet())[0], old_suffix, + tuple([list(n.glyphSet())[0] for n in new]), location=location) + + # GSUB lookup type 4: Ligature substitution. + # Format: "substitute f f i by f_f_i;" + if (not reverse and + len(old) > 1 and len(new) == 1 and + len(new[0].glyphSet()) == 1 and + num_lookups == 0): + return self.ast.LigatureSubstStatement( + old_prefix, old, old_suffix, + list(new[0].glyphSet())[0], forceChain=hasMarks, + location=location) + + # GSUB lookup type 8: Reverse chaining substitution. + if reverse: + if len(old) != 1: + raise FeatureLibError( + "In reverse chaining single substitutions, " + "only a single glyph or glyph class can be replaced", + location) + if len(new) != 1: + raise FeatureLibError( + 'In reverse chaining single substitutions, ' + 'the replacement (after "by") must be a single glyph ' + 'or glyph class', location) + if num_lookups != 0: + raise FeatureLibError( + "Reverse chaining substitutions cannot call named lookups", + location) + glyphs = sorted(list(old[0].glyphSet())) + replacements = sorted(list(new[0].glyphSet())) + if len(replacements) == 1: + replacements = replacements * len(glyphs) + if len(glyphs) != len(replacements): + raise FeatureLibError( + 'Expected a glyph class with %d elements after "by", ' + 'but found a glyph class with %d elements' % + (len(glyphs), len(replacements)), location) + return self.ast.ReverseChainSingleSubstStatement( + old_prefix, old_suffix, old, new, location=location) + + if len(old) > 1 and len(new) > 1: + raise FeatureLibError( + 'Direct substitution of multiple glyphs by multiple glyphs ' + 'is not supported', + location) + + # GSUB lookup type 6: Chaining contextual substitution. + assert len(new) == 0, new + rule = self.ast.ChainContextSubstStatement( + old_prefix, old, old_suffix, lookups, location=location) + return rule + + def parse_subtable_(self): + assert self.is_cur_keyword_("subtable") + location = self.cur_token_location_ + self.expect_symbol_(";") + return self.ast.SubtableStatement(location=location) + + def parse_size_parameters_(self): + assert self.is_cur_keyword_("parameters") + location = self.cur_token_location_ + DesignSize = self.expect_decipoint_() + SubfamilyID = self.expect_number_() + RangeStart = 0 + RangeEnd = 0 + if self.next_token_type_ in (Lexer.NUMBER, Lexer.FLOAT) or \ + SubfamilyID != 0: + RangeStart = self.expect_decipoint_() + RangeEnd = self.expect_decipoint_() + + self.expect_symbol_(";") + return self.ast.SizeParameters(DesignSize, SubfamilyID, + RangeStart, RangeEnd, + location=location) + + def parse_size_menuname_(self): + assert self.is_cur_keyword_("sizemenuname") + location = self.cur_token_location_ + platformID, platEncID, langID, string = self.parse_name_() + return self.ast.FeatureNameStatement("size", platformID, + platEncID, langID, string, + location=location) + + def parse_table_(self): + assert self.is_cur_keyword_("table") + location, name = self.cur_token_location_, self.expect_tag_() + table = self.ast.TableBlock(name, location=location) + self.expect_symbol_("{") + handler = { + "GDEF": self.parse_table_GDEF_, + "head": self.parse_table_head_, + "hhea": self.parse_table_hhea_, + "vhea": self.parse_table_vhea_, + "name": self.parse_table_name_, + "BASE": self.parse_table_BASE_, + "OS/2": self.parse_table_OS_2_, + }.get(name) + if handler: + handler(table) + else: + raise FeatureLibError('"table %s" is not supported' % name.strip(), + location) + self.expect_symbol_("}") + end_tag = self.expect_tag_() + if end_tag != name: + raise FeatureLibError('Expected "%s"' % name.strip(), + self.cur_token_location_) + self.expect_symbol_(";") + return table + + def parse_table_GDEF_(self, table): + statements = table.statements + while self.next_token_ != "}" or self.cur_comments_: + self.advance_lexer_(comments=True) + if self.cur_token_type_ is Lexer.COMMENT: + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) + elif self.is_cur_keyword_("Attach"): + statements.append(self.parse_attach_()) + elif self.is_cur_keyword_("GlyphClassDef"): + statements.append(self.parse_GlyphClassDef_()) + elif self.is_cur_keyword_("LigatureCaretByIndex"): + statements.append(self.parse_ligatureCaretByIndex_()) + elif self.is_cur_keyword_("LigatureCaretByPos"): + statements.append(self.parse_ligatureCaretByPos_()) + elif self.cur_token_ == ";": + continue + else: + raise FeatureLibError( + "Expected Attach, LigatureCaretByIndex, " + "or LigatureCaretByPos", + self.cur_token_location_) + + def parse_table_head_(self, table): + statements = table.statements + while self.next_token_ != "}" or self.cur_comments_: + self.advance_lexer_(comments=True) + if self.cur_token_type_ is Lexer.COMMENT: + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) + elif self.is_cur_keyword_("FontRevision"): + statements.append(self.parse_FontRevision_()) + elif self.cur_token_ == ";": + continue + else: + raise FeatureLibError("Expected FontRevision", + self.cur_token_location_) + + def parse_table_hhea_(self, table): + statements = table.statements + fields = ("CaretOffset", "Ascender", "Descender", "LineGap") + while self.next_token_ != "}" or self.cur_comments_: + self.advance_lexer_(comments=True) + if self.cur_token_type_ is Lexer.COMMENT: + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) + elif self.cur_token_type_ is Lexer.NAME and self.cur_token_ in fields: + key = self.cur_token_.lower() + value = self.expect_number_() + statements.append( + self.ast.HheaField(key, value, + location=self.cur_token_location_)) + if self.next_token_ != ";": + raise FeatureLibError("Incomplete statement", self.next_token_location_) + elif self.cur_token_ == ";": + continue + else: + raise FeatureLibError("Expected CaretOffset, Ascender, " + "Descender or LineGap", + self.cur_token_location_) + + def parse_table_vhea_(self, table): + statements = table.statements + fields = ("VertTypoAscender", "VertTypoDescender", "VertTypoLineGap") + while self.next_token_ != "}" or self.cur_comments_: + self.advance_lexer_(comments=True) + if self.cur_token_type_ is Lexer.COMMENT: + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) + elif self.cur_token_type_ is Lexer.NAME and self.cur_token_ in fields: + key = self.cur_token_.lower() + value = self.expect_number_() + statements.append( + self.ast.VheaField(key, value, + location=self.cur_token_location_)) + if self.next_token_ != ";": + raise FeatureLibError("Incomplete statement", self.next_token_location_) + elif self.cur_token_ == ";": + continue + else: + raise FeatureLibError("Expected VertTypoAscender, " + "VertTypoDescender or VertTypoLineGap", + self.cur_token_location_) + + def parse_table_name_(self, table): + statements = table.statements + while self.next_token_ != "}" or self.cur_comments_: + self.advance_lexer_(comments=True) + if self.cur_token_type_ is Lexer.COMMENT: + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) + elif self.is_cur_keyword_("nameid"): + statement = self.parse_nameid_() + if statement: + statements.append(statement) + elif self.cur_token_ == ";": + continue + else: + raise FeatureLibError("Expected nameid", + self.cur_token_location_) + + def parse_name_(self): + platEncID = None + langID = None + if self.next_token_type_ == Lexer.NUMBER: + platformID = self.expect_number_() + location = self.cur_token_location_ + if platformID not in (1, 3): + raise FeatureLibError("Expected platform id 1 or 3", location) + if self.next_token_type_ == Lexer.NUMBER: + platEncID = self.expect_number_() + langID = self.expect_number_() + else: + platformID = 3 + location = self.cur_token_location_ + + if platformID == 1: # Macintosh + platEncID = platEncID or 0 # Roman + langID = langID or 0 # English + else: # 3, Windows + platEncID = platEncID or 1 # Unicode + langID = langID or 0x0409 # English + + string = self.expect_string_() + self.expect_symbol_(";") + + encoding = getEncoding(platformID, platEncID, langID) + if encoding is None: + raise FeatureLibError("Unsupported encoding", location) + unescaped = self.unescape_string_(string, encoding) + return platformID, platEncID, langID, unescaped + + def parse_nameid_(self): + assert self.cur_token_ == "nameid", self.cur_token_ + location, nameID = self.cur_token_location_, self.expect_number_() + if nameID > 32767: + raise FeatureLibError("Name id value cannot be greater than 32767", + self.cur_token_location_) + if 1 <= nameID <= 6: + log.warning("Name id %d cannot be set from the feature file. " + "Ignoring record" % nameID) + self.parse_name_() # skip to the next record + return None + + platformID, platEncID, langID, string = self.parse_name_() + return self.ast.NameRecord(nameID, platformID, platEncID, + langID, string, location=location) + + def unescape_string_(self, string, encoding): + if encoding == "utf_16_be": + s = re.sub(r"\\[0-9a-fA-F]{4}", self.unescape_unichr_, string) + else: + unescape = lambda m: self.unescape_byte_(m, encoding) + s = re.sub(r"\\[0-9a-fA-F]{2}", unescape, string) + # We now have a Unicode string, but it might contain surrogate pairs. + # We convert surrogates to actual Unicode by round-tripping through + # Python's UTF-16 codec in a special mode. + utf16 = tobytes(s, "utf_16_be", "surrogatepass") + return tounicode(utf16, "utf_16_be") + + @staticmethod + def unescape_unichr_(match): + n = match.group(0)[1:] + return unichr(int(n, 16)) + + @staticmethod + def unescape_byte_(match, encoding): + n = match.group(0)[1:] + return bytechr(int(n, 16)).decode(encoding) + + def parse_table_BASE_(self, table): + statements = table.statements + while self.next_token_ != "}" or self.cur_comments_: + self.advance_lexer_(comments=True) + if self.cur_token_type_ is Lexer.COMMENT: + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) + elif self.is_cur_keyword_("HorizAxis.BaseTagList"): + horiz_bases = self.parse_base_tag_list_() + elif self.is_cur_keyword_("HorizAxis.BaseScriptList"): + horiz_scripts = self.parse_base_script_list_(len(horiz_bases)) + statements.append( + self.ast.BaseAxis(horiz_bases, + horiz_scripts, False, + location=self.cur_token_location_)) + elif self.is_cur_keyword_("VertAxis.BaseTagList"): + vert_bases = self.parse_base_tag_list_() + elif self.is_cur_keyword_("VertAxis.BaseScriptList"): + vert_scripts = self.parse_base_script_list_(len(vert_bases)) + statements.append( + self.ast.BaseAxis(vert_bases, + vert_scripts, True, + location=self.cur_token_location_)) + elif self.cur_token_ == ";": + continue + + def parse_table_OS_2_(self, table): + statements = table.statements + numbers = ("FSType", "TypoAscender", "TypoDescender", "TypoLineGap", + "winAscent", "winDescent", "XHeight", "CapHeight", + "WeightClass", "WidthClass", "LowerOpSize", "UpperOpSize") + ranges = ("UnicodeRange", "CodePageRange") + while self.next_token_ != "}" or self.cur_comments_: + self.advance_lexer_(comments=True) + if self.cur_token_type_ is Lexer.COMMENT: + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) + elif self.cur_token_type_ is Lexer.NAME: + key = self.cur_token_.lower() + value = None + if self.cur_token_ in numbers: + value = self.expect_number_() + elif self.is_cur_keyword_("Panose"): + value = [] + for i in range(10): + value.append(self.expect_number_()) + elif self.cur_token_ in ranges: + value = [] + while self.next_token_ != ";": + value.append(self.expect_number_()) + elif self.is_cur_keyword_("Vendor"): + value = self.expect_string_() + statements.append( + self.ast.OS2Field(key, value, + location=self.cur_token_location_)) + elif self.cur_token_ == ";": + continue + + def parse_base_tag_list_(self): + assert self.cur_token_ in ("HorizAxis.BaseTagList", + "VertAxis.BaseTagList"), self.cur_token_ + bases = [] + while self.next_token_ != ";": + bases.append(self.expect_script_tag_()) + self.expect_symbol_(";") + return bases + + def parse_base_script_list_(self, count): + assert self.cur_token_ in ("HorizAxis.BaseScriptList", + "VertAxis.BaseScriptList"), self.cur_token_ + scripts = [(self.parse_base_script_record_(count))] + while self.next_token_ == ",": + self.expect_symbol_(",") + scripts.append(self.parse_base_script_record_(count)) + self.expect_symbol_(";") + return scripts + + def parse_base_script_record_(self, count): + script_tag = self.expect_script_tag_() + base_tag = self.expect_script_tag_() + coords = [self.expect_number_() for i in range(count)] + return script_tag, base_tag, coords + + def parse_device_(self): + result = None + self.expect_symbol_("<") + self.expect_keyword_("device") + if self.next_token_ == "NULL": + self.expect_keyword_("NULL") + else: + result = [(self.expect_number_(), self.expect_number_())] + while self.next_token_ == ",": + self.expect_symbol_(",") + result.append((self.expect_number_(), self.expect_number_())) + result = tuple(result) # make it hashable + self.expect_symbol_(">") + return result + + def is_next_value_(self): + return self.next_token_type_ is Lexer.NUMBER or self.next_token_ == "<" + + def parse_valuerecord_(self, vertical): + if self.next_token_type_ is Lexer.NUMBER: + number, location = self.expect_number_(), self.cur_token_location_ + if vertical: + val = self.ast.ValueRecord(yAdvance=number, + vertical=vertical, + location=location) + else: + val = self.ast.ValueRecord(xAdvance=number, + vertical=vertical, + location=location) + return val + self.expect_symbol_("<") + location = self.cur_token_location_ + if self.next_token_type_ is Lexer.NAME: + name = self.expect_name_() + if name == "NULL": + self.expect_symbol_(">") + return None + vrd = self.valuerecords_.resolve(name) + if vrd is None: + raise FeatureLibError("Unknown valueRecordDef \"%s\"" % name, + self.cur_token_location_) + value = vrd.value + xPlacement, yPlacement = (value.xPlacement, value.yPlacement) + xAdvance, yAdvance = (value.xAdvance, value.yAdvance) + else: + xPlacement, yPlacement, xAdvance, yAdvance = ( + self.expect_number_(), self.expect_number_(), + self.expect_number_(), self.expect_number_()) + + if self.next_token_ == "<": + xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice = ( + self.parse_device_(), self.parse_device_(), + self.parse_device_(), self.parse_device_()) + allDeltas = sorted([ + delta + for size, delta + in (xPlaDevice if xPlaDevice else ()) + + (yPlaDevice if yPlaDevice else ()) + + (xAdvDevice if xAdvDevice else ()) + + (yAdvDevice if yAdvDevice else ())]) + if allDeltas[0] < -128 or allDeltas[-1] > 127: + raise FeatureLibError( + "Device value out of valid range (-128..127)", + self.cur_token_location_) + else: + xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice = ( + None, None, None, None) + + self.expect_symbol_(">") + return self.ast.ValueRecord( + xPlacement, yPlacement, xAdvance, yAdvance, + xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice, + vertical=vertical, location=location) + + def parse_valuerecord_definition_(self, vertical): + assert self.is_cur_keyword_("valueRecordDef") + location = self.cur_token_location_ + value = self.parse_valuerecord_(vertical) + name = self.expect_name_() + self.expect_symbol_(";") + vrd = self.ast.ValueRecordDefinition(name, value, location=location) + self.valuerecords_.define(name, vrd) + return vrd + + def parse_languagesystem_(self): + assert self.cur_token_ == "languagesystem" + location = self.cur_token_location_ + script = self.expect_script_tag_() + language = self.expect_language_tag_() + self.expect_symbol_(";") + if script == "DFLT" and language != "dflt": + raise FeatureLibError( + 'For script "DFLT", the language must be "dflt"', + self.cur_token_location_) + return self.ast.LanguageSystemStatement(script, language, + location=location) + + def parse_feature_block_(self): + assert self.cur_token_ == "feature" + location = self.cur_token_location_ + tag = self.expect_tag_() + vertical = (tag in {"vkrn", "vpal", "vhal", "valt"}) + + stylisticset = None + cv_feature = None + size_feature = False + if tag in self.SS_FEATURE_TAGS: + stylisticset = tag + elif tag in self.CV_FEATURE_TAGS: + cv_feature = tag + elif tag == "size": + size_feature = True + + use_extension = False + if self.next_token_ == "useExtension": + self.expect_keyword_("useExtension") + use_extension = True + + block = self.ast.FeatureBlock(tag, use_extension=use_extension, + location=location) + self.parse_block_(block, vertical, stylisticset, size_feature, + cv_feature) + return block + + def parse_feature_reference_(self): + assert self.cur_token_ == "feature", self.cur_token_ + location = self.cur_token_location_ + featureName = self.expect_tag_() + self.expect_symbol_(";") + return self.ast.FeatureReferenceStatement(featureName, + location=location) + + def parse_featureNames_(self, tag): + assert self.cur_token_ == "featureNames", self.cur_token_ + block = self.ast.NestedBlock(tag, self.cur_token_, + location=self.cur_token_location_) + self.expect_symbol_("{") + for symtab in self.symbol_tables_: + symtab.enter_scope() + while self.next_token_ != "}" or self.cur_comments_: + self.advance_lexer_(comments=True) + if self.cur_token_type_ is Lexer.COMMENT: + block.statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) + elif self.is_cur_keyword_("name"): + location = self.cur_token_location_ + platformID, platEncID, langID, string = self.parse_name_() + block.statements.append( + self.ast.FeatureNameStatement(tag, platformID, + platEncID, langID, string, + location=location)) + elif self.cur_token_ == ";": + continue + else: + raise FeatureLibError('Expected "name"', + self.cur_token_location_) + self.expect_symbol_("}") + for symtab in self.symbol_tables_: + symtab.exit_scope() + self.expect_symbol_(";") + return block + + def parse_cvParameters_(self, tag): + assert self.cur_token_ == "cvParameters", self.cur_token_ + block = self.ast.NestedBlock(tag, self.cur_token_, + location=self.cur_token_location_) + self.expect_symbol_("{") + for symtab in self.symbol_tables_: + symtab.enter_scope() + + statements = block.statements + while self.next_token_ != "}" or self.cur_comments_: + self.advance_lexer_(comments=True) + if self.cur_token_type_ is Lexer.COMMENT: + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) + elif self.is_cur_keyword_({"FeatUILabelNameID", + "FeatUITooltipTextNameID", + "SampleTextNameID", + "ParamUILabelNameID"}): + statements.append(self.parse_cvNameIDs_(tag, self.cur_token_)) + elif self.is_cur_keyword_("Character"): + statements.append(self.parse_cvCharacter_(tag)) + elif self.cur_token_ == ";": + continue + else: + raise FeatureLibError( + "Expected statement: got {} {}".format( + self.cur_token_type_, self.cur_token_), + self.cur_token_location_) + + self.expect_symbol_("}") + for symtab in self.symbol_tables_: + symtab.exit_scope() + self.expect_symbol_(";") + return block + + def parse_cvNameIDs_(self, tag, block_name): + assert self.cur_token_ == block_name, self.cur_token_ + block = self.ast.NestedBlock(tag, block_name, + location=self.cur_token_location_) + self.expect_symbol_("{") + for symtab in self.symbol_tables_: + symtab.enter_scope() + while self.next_token_ != "}" or self.cur_comments_: + self.advance_lexer_(comments=True) + if self.cur_token_type_ is Lexer.COMMENT: + block.statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) + elif self.is_cur_keyword_("name"): + location = self.cur_token_location_ + platformID, platEncID, langID, string = self.parse_name_() + block.statements.append( + self.ast.CVParametersNameStatement( + tag, platformID, platEncID, langID, string, + block_name, location=location)) + elif self.cur_token_ == ";": + continue + else: + raise FeatureLibError('Expected "name"', + self.cur_token_location_) + self.expect_symbol_("}") + for symtab in self.symbol_tables_: + symtab.exit_scope() + self.expect_symbol_(";") + return block + + def parse_cvCharacter_(self, tag): + assert self.cur_token_ == "Character", self.cur_token_ + location, character = self.cur_token_location_, self.expect_decimal_or_hexadecimal_() + self.expect_symbol_(";") + if not (0xFFFFFF >= character >= 0): + raise FeatureLibError("Character value must be between " + "{:#x} and {:#x}".format(0, 0xFFFFFF), + location) + return self.ast.CharacterStatement(character, tag, location=location) + + def parse_FontRevision_(self): + assert self.cur_token_ == "FontRevision", self.cur_token_ + location, version = self.cur_token_location_, self.expect_float_() + self.expect_symbol_(";") + if version <= 0: + raise FeatureLibError("Font revision numbers must be positive", + location) + return self.ast.FontRevisionStatement(version, location=location) + + def parse_block_(self, block, vertical, stylisticset=None, + size_feature=False, cv_feature=None): + self.expect_symbol_("{") + for symtab in self.symbol_tables_: + symtab.enter_scope() + + statements = block.statements + while self.next_token_ != "}" or self.cur_comments_: + self.advance_lexer_(comments=True) + if self.cur_token_type_ is Lexer.COMMENT: + statements.append(self.ast.Comment( + self.cur_token_, location=self.cur_token_location_)) + elif self.cur_token_type_ is Lexer.GLYPHCLASS: + statements.append(self.parse_glyphclass_definition_()) + elif self.is_cur_keyword_("anchorDef"): + statements.append(self.parse_anchordef_()) + elif self.is_cur_keyword_({"enum", "enumerate"}): + statements.append(self.parse_enumerate_(vertical=vertical)) + elif self.is_cur_keyword_("feature"): + statements.append(self.parse_feature_reference_()) + elif self.is_cur_keyword_("ignore"): + statements.append(self.parse_ignore_()) + elif self.is_cur_keyword_("language"): + statements.append(self.parse_language_()) + elif self.is_cur_keyword_("lookup"): + statements.append(self.parse_lookup_(vertical)) + elif self.is_cur_keyword_("lookupflag"): + statements.append(self.parse_lookupflag_()) + elif self.is_cur_keyword_("markClass"): + statements.append(self.parse_markClass_()) + elif self.is_cur_keyword_({"pos", "position"}): + statements.append( + self.parse_position_(enumerated=False, vertical=vertical)) + elif self.is_cur_keyword_("script"): + statements.append(self.parse_script_()) + elif (self.is_cur_keyword_({"sub", "substitute", + "rsub", "reversesub"})): + statements.append(self.parse_substitute_()) + elif self.is_cur_keyword_("subtable"): + statements.append(self.parse_subtable_()) + elif self.is_cur_keyword_("valueRecordDef"): + statements.append(self.parse_valuerecord_definition_(vertical)) + elif stylisticset and self.is_cur_keyword_("featureNames"): + statements.append(self.parse_featureNames_(stylisticset)) + elif cv_feature and self.is_cur_keyword_("cvParameters"): + statements.append(self.parse_cvParameters_(cv_feature)) + elif size_feature and self.is_cur_keyword_("parameters"): + statements.append(self.parse_size_parameters_()) + elif size_feature and self.is_cur_keyword_("sizemenuname"): + statements.append(self.parse_size_menuname_()) + elif self.cur_token_type_ is Lexer.NAME and self.cur_token_ in self.extensions: + statements.append(self.extensions[self.cur_token_](self)) + elif self.cur_token_ == ";": + continue + else: + raise FeatureLibError( + "Expected glyph class definition or statement: got {} {}".format(self.cur_token_type_, self.cur_token_), + self.cur_token_location_) + + self.expect_symbol_("}") + for symtab in self.symbol_tables_: + symtab.exit_scope() + + name = self.expect_name_() + if name != block.name.strip(): + raise FeatureLibError("Expected \"%s\"" % block.name.strip(), + self.cur_token_location_) + self.expect_symbol_(";") + + # A multiple substitution may have a single destination, in which case + # it will look just like a single substitution. So if there are both + # multiple and single substitutions, upgrade all the single ones to + # multiple substitutions. + + # Check if we have a mix of non-contextual singles and multiples. + has_single = False + has_multiple = False + for s in statements: + if isinstance(s, self.ast.SingleSubstStatement): + has_single = not any([s.prefix, s.suffix, s.forceChain]) + elif isinstance(s, self.ast.MultipleSubstStatement): + has_multiple = not any([s.prefix, s.suffix]) + + # Upgrade all single substitutions to multiple substitutions. + if has_single and has_multiple: + for i, s in enumerate(statements): + if isinstance(s, self.ast.SingleSubstStatement): + statements[i] = self.ast.MultipleSubstStatement( + s.prefix, s.glyphs[0].glyphSet()[0], s.suffix, + [r.glyphSet()[0] for r in s.replacements], + location=s.location) + + def is_cur_keyword_(self, k): + if self.cur_token_type_ is Lexer.NAME: + if isinstance(k, type("")): # basestring is gone in Python3 + return self.cur_token_ == k + else: + return self.cur_token_ in k + return False + + def expect_class_name_(self): + self.advance_lexer_() + if self.cur_token_type_ is not Lexer.GLYPHCLASS: + raise FeatureLibError("Expected @NAME", self.cur_token_location_) + return self.cur_token_ + + def expect_cid_(self): + self.advance_lexer_() + if self.cur_token_type_ is Lexer.CID: + return self.cur_token_ + raise FeatureLibError("Expected a CID", self.cur_token_location_) + + def expect_filename_(self): + self.advance_lexer_() + if self.cur_token_type_ is not Lexer.FILENAME: + raise FeatureLibError("Expected file name", + self.cur_token_location_) + return self.cur_token_ + + def expect_glyph_(self): + self.advance_lexer_() + if self.cur_token_type_ is Lexer.NAME: + self.cur_token_ = self.cur_token_.lstrip("\\") + if len(self.cur_token_) > 63: + raise FeatureLibError( + "Glyph names must not be longer than 63 characters", + self.cur_token_location_) + return self.cur_token_ + elif self.cur_token_type_ is Lexer.CID: + return "cid%05d" % self.cur_token_ + raise FeatureLibError("Expected a glyph name or CID", + self.cur_token_location_) + + def expect_markClass_reference_(self): + name = self.expect_class_name_() + mc = self.glyphclasses_.resolve(name) + if mc is None: + raise FeatureLibError("Unknown markClass @%s" % name, + self.cur_token_location_) + if not isinstance(mc, self.ast.MarkClass): + raise FeatureLibError("@%s is not a markClass" % name, + self.cur_token_location_) + return mc + + def expect_tag_(self): + self.advance_lexer_() + if self.cur_token_type_ is not Lexer.NAME: + raise FeatureLibError("Expected a tag", self.cur_token_location_) + if len(self.cur_token_) > 4: + raise FeatureLibError("Tags can not be longer than 4 characters", + self.cur_token_location_) + return (self.cur_token_ + " ")[:4] + + def expect_script_tag_(self): + tag = self.expect_tag_() + if tag == "dflt": + raise FeatureLibError( + '"dflt" is not a valid script tag; use "DFLT" instead', + self.cur_token_location_) + return tag + + def expect_language_tag_(self): + tag = self.expect_tag_() + if tag == "DFLT": + raise FeatureLibError( + '"DFLT" is not a valid language tag; use "dflt" instead', + self.cur_token_location_) + return tag + + def expect_symbol_(self, symbol): + self.advance_lexer_() + if self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == symbol: + return symbol + raise FeatureLibError("Expected '%s'" % symbol, + self.cur_token_location_) + + def expect_keyword_(self, keyword): + self.advance_lexer_() + if self.cur_token_type_ is Lexer.NAME and self.cur_token_ == keyword: + return self.cur_token_ + raise FeatureLibError("Expected \"%s\"" % keyword, + self.cur_token_location_) + + def expect_name_(self): + self.advance_lexer_() + if self.cur_token_type_ is Lexer.NAME: + return self.cur_token_ + raise FeatureLibError("Expected a name", self.cur_token_location_) + + # TODO: Don't allow this method to accept hexadecimal values + def expect_number_(self): + self.advance_lexer_() + if self.cur_token_type_ is Lexer.NUMBER: + return self.cur_token_ + raise FeatureLibError("Expected a number", self.cur_token_location_) + + def expect_float_(self): + self.advance_lexer_() + if self.cur_token_type_ is Lexer.FLOAT: + return self.cur_token_ + raise FeatureLibError("Expected a floating-point number", + self.cur_token_location_) + + # TODO: Don't allow this method to accept hexadecimal values + def expect_decipoint_(self): + if self.next_token_type_ == Lexer.FLOAT: + return self.expect_float_() + elif self.next_token_type_ is Lexer.NUMBER: + return self.expect_number_() / 10 + else: + raise FeatureLibError("Expected an integer or floating-point number", + self.cur_token_location_) + + def expect_decimal_or_hexadecimal_(self): + # the lexer returns the same token type 'NUMBER' for either decimal or + # hexadecimal integers, and casts them both to a `int` type, so it's + # impossible to distinguish the two here. This method is implemented + # the same as `expect_number_`, only it gives a more informative + # error message + self.advance_lexer_() + if self.cur_token_type_ is Lexer.NUMBER: + return self.cur_token_ + raise FeatureLibError("Expected a decimal or hexadecimal number", + self.cur_token_location_) + + def expect_string_(self): + self.advance_lexer_() + if self.cur_token_type_ is Lexer.STRING: + return self.cur_token_ + raise FeatureLibError("Expected a string", self.cur_token_location_) + + def advance_lexer_(self, comments=False): + if comments and self.cur_comments_: + self.cur_token_type_ = Lexer.COMMENT + self.cur_token_, self.cur_token_location_ = self.cur_comments_.pop(0) + return + else: + self.cur_token_type_, self.cur_token_, self.cur_token_location_ = ( + self.next_token_type_, self.next_token_, self.next_token_location_) + while True: + try: + (self.next_token_type_, self.next_token_, + self.next_token_location_) = next(self.lexer_) + except StopIteration: + self.next_token_type_, self.next_token_ = (None, None) + if self.next_token_type_ != Lexer.COMMENT: + break + self.cur_comments_.append((self.next_token_, self.next_token_location_)) + + @staticmethod + def reverse_string_(s): + """'abc' --> 'cba'""" + return ''.join(reversed(list(s))) + + def make_cid_range_(self, location, start, limit): + """(location, 999, 1001) --> ["cid00999", "cid01000", "cid01001"]""" + result = list() + if start > limit: + raise FeatureLibError( + "Bad range: start should be less than limit", location) + for cid in range(start, limit + 1): + result.append("cid%05d" % cid) + return result + + def make_glyph_range_(self, location, start, limit): + """(location, "a.sc", "d.sc") --> ["a.sc", "b.sc", "c.sc", "d.sc"]""" + result = list() + if len(start) != len(limit): + raise FeatureLibError( + "Bad range: \"%s\" and \"%s\" should have the same length" % + (start, limit), location) + + rev = self.reverse_string_ + prefix = os.path.commonprefix([start, limit]) + suffix = rev(os.path.commonprefix([rev(start), rev(limit)])) + if len(suffix) > 0: + start_range = start[len(prefix):-len(suffix)] + limit_range = limit[len(prefix):-len(suffix)] + else: + start_range = start[len(prefix):] + limit_range = limit[len(prefix):] + + if start_range >= limit_range: + raise FeatureLibError( + "Start of range must be smaller than its end", + location) + + uppercase = re.compile(r'^[A-Z]$') + if uppercase.match(start_range) and uppercase.match(limit_range): + for c in range(ord(start_range), ord(limit_range) + 1): + result.append("%s%c%s" % (prefix, c, suffix)) + return result + + lowercase = re.compile(r'^[a-z]$') + if lowercase.match(start_range) and lowercase.match(limit_range): + for c in range(ord(start_range), ord(limit_range) + 1): + result.append("%s%c%s" % (prefix, c, suffix)) + return result + + digits = re.compile(r'^[0-9]{1,3}$') + if digits.match(start_range) and digits.match(limit_range): + for i in range(int(start_range, 10), int(limit_range, 10) + 1): + number = ("000" + str(i))[-len(start_range):] + result.append("%s%s%s" % (prefix, number, suffix)) + return result + + raise FeatureLibError("Bad range: \"%s-%s\"" % (start, limit), + location) + + +class SymbolTable(object): + def __init__(self): + self.scopes_ = [{}] + + def enter_scope(self): + self.scopes_.append({}) + + def exit_scope(self): + self.scopes_.pop() + + def define(self, name, item): + self.scopes_[-1][name] = item + + def resolve(self, name): + for scope in reversed(self.scopes_): + item = scope.get(name) + if item: + return item + return None diff --git a/Lib/fontTools/inspect.py b/Lib/fontTools/inspect.py index 875736d..bdacadf 100644 --- a/Lib/fontTools/inspect.py +++ b/Lib/fontTools/inspect.py @@ -8,13 +8,18 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools import misc, ttLib, cffLib -import pygtk -pygtk.require('2.0') +try: + from gi import pygtkcompat +except ImportError: + pygtkcompat = None + +if pygtkcompat is not None: + pygtkcompat.enable() + pygtkcompat.enable_gtk(version='3.0') import gtk import sys - class Row(object): def __init__(self, parent, index, key, value, font): self._parent = parent @@ -74,7 +79,7 @@ class Row(object): def _add_object(self, key, value): # Make sure item is decompiled try: - value["asdf"] + value.asdf # Any better way?! except (AttributeError, KeyError, TypeError, ttLib.TTLibError): pass if isinstance(value, ttLib.getTableModule('glyf').Glyph): @@ -110,8 +115,7 @@ class Row(object): if len(value) and len(value) <= 32: self._value_str = str(value) else: - self._value_str = '%s of %d items' % (value.__class__.__name__, - len(value)) + self._value_str = '%s of %d items' % (value.__class__.__name__, len(value)) self._items = list(enumerate(value)) def __len__(self): @@ -253,13 +257,15 @@ class Inspect(object): self.scrolled_window.add(self.treeview) self.window.show_all() -def main(args): +def main(args=None): + if args is None: + args = sys.argv[1:] if len(args) < 1: print("usage: pyftinspect font...", file=sys.stderr) - sys.exit(1) + return 1 for arg in args: Inspect(arg) gtk.main() if __name__ == "__main__": - main(sys.argv[1:]) + sys.exit(main()) diff --git a/Lib/fontTools/merge.py b/Lib/fontTools/merge.py index 54e91e3..7dcb8b5 100644 --- a/Lib/fontTools/merge.py +++ b/Lib/fontTools/merge.py @@ -7,13 +7,21 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * +from fontTools.misc.timeTools import timestampNow from fontTools import ttLib, cffLib from fontTools.ttLib.tables import otTables, _h_e_a_d from fontTools.ttLib.tables.DefaultTable import DefaultTable +from fontTools.misc.loggingTools import Timer +from fontTools.pens.recordingPen import DecomposingRecordingPen from functools import reduce import sys import time import operator +import logging + + +log = logging.getLogger("fontTools.merge") +timer = Timer(logger=logging.getLogger(__name__+".timer"), level=logging.INFO) def _add_method(*clazzes, **kwargs): @@ -21,11 +29,13 @@ def _add_method(*clazzes, **kwargs): more classes.""" allowDefault = kwargs.get('allowDefaultTable', False) def wrapper(method): + done = [] for clazz in clazzes: + if clazz in done: continue # Support multiple names of a clazz + done.append(clazz) assert allowDefault or clazz != DefaultTable, 'Oops, table class not found.' assert method.__name__ not in clazz.__dict__, \ - "Oops, class '%s' has method '%s'." % (clazz.__name__, - method.__name__) + "Oops, class '%s' has method '%s'." % (clazz.__name__, method.__name__) setattr(clazz, method.__name__, method) return None return wrapper @@ -46,7 +56,7 @@ def recalculate(lst): return NotImplemented def current_time(lst): - return int(time.time() - _h_e_a_d.mac_epoch_diff) + return timestampNow() def bitwise_and(lst): return reduce(operator.and_, lst) @@ -141,7 +151,7 @@ def mergeBits(bitmap): @_add_method(DefaultTable, allowDefaultTable=True) def merge(self, m, tables): if not hasattr(self, 'mergeMap'): - m.log("Don't know how to merge '%s'." % self.tableTag) + log.info("Don't know how to merge '%s'.", self.tableTag) return NotImplemented logic = self.mergeMap @@ -307,12 +317,6 @@ ttLib.getTableClass('vmtx').mergeMap = ttLib.getTableClass('hmtx').mergeMap = { 'metrics': sumDicts, } -ttLib.getTableClass('gasp').mergeMap = { - 'tableTag': equal, - 'version': max, - 'gaspRange': first, # FIXME? Appears irreconcilable -} - ttLib.getTableClass('name').mergeMap = { 'tableTag': equal, 'names': first, # FIXME? Does mixing name records make sense? @@ -346,48 +350,186 @@ def merge(self, m, tables): ttLib.getTableClass('prep').mergeMap = lambda self, lst: first(lst) ttLib.getTableClass('fpgm').mergeMap = lambda self, lst: first(lst) ttLib.getTableClass('cvt ').mergeMap = lambda self, lst: first(lst) +ttLib.getTableClass('gasp').mergeMap = lambda self, lst: first(lst) # FIXME? Appears irreconcilable + +def _glyphsAreSame(glyphSet1, glyphSet2, glyph1, glyph2): + pen1 = DecomposingRecordingPen(glyphSet1) + pen2 = DecomposingRecordingPen(glyphSet2) + g1 = glyphSet1[glyph1] + g2 = glyphSet2[glyph2] + g1.draw(pen1) + g2.draw(pen2) + return (pen1.value == pen2.value and + g1.width == g2.width and + (not hasattr(g1, 'height') or g1.height == g2.height)) @_add_method(ttLib.getTableClass('cmap')) def merge(self, m, tables): # TODO Handle format=14. - cmapTables = [(t,fontIdx) for fontIdx,table in enumerate(tables) for t in table.tables - if t.isUnicode()] - # TODO Better handle format-4 and format-12 coexisting in same font. - # TODO Insert both a format-4 and format-12 if needed. - module = ttLib.getTableModule('cmap') - assert all(t.format in [4, 12] for t,_ in cmapTables) - format = max(t.format for t,_ in cmapTables) - cmapTable = module.cmap_classes[format](format) - cmapTable.cmap = {} - cmapTable.platformID = 3 - cmapTable.platEncID = max(t.platEncID for t,_ in cmapTables) - cmapTable.language = 0 - cmap = cmapTable.cmap + # Only merges 4/3/1 and 12/3/10 subtables, ignores all other subtables + # If there is a format 12 table for the same font, ignore the format 4 table + cmapTables = [] + for fontIdx,table in enumerate(tables): + format4 = None + format12 = None + for subtable in table.tables: + properties = (subtable.format, subtable.platformID, subtable.platEncID) + if properties == (4,3,1): + format4 = subtable + elif properties == (12,3,10): + format12 = subtable + if format12 is not None: + cmapTables.append((format12, fontIdx)) + elif format4 is not None: + cmapTables.append((format4, fontIdx)) + + # Build a unicode mapping, then decide which format is needed to store it. + cmap = {} + fontIndexForGlyph = {} + glyphSets = [None for f in m.fonts] if hasattr(m, 'fonts') else None for table,fontIdx in cmapTables: - # TODO handle duplicates. + # handle duplicates for uni,gid in table.cmap.items(): oldgid = cmap.get(uni, None) if oldgid is None: cmap[uni] = gid + fontIndexForGlyph[gid] = fontIdx elif oldgid != gid: # Char previously mapped to oldgid, now to gid. # Record, to fix up in GSUB 'locl' later. - assert m.duplicateGlyphsPerFont[fontIdx].get(oldgid, gid) == gid - m.duplicateGlyphsPerFont[fontIdx][oldgid] = gid + if m.duplicateGlyphsPerFont[fontIdx].get(oldgid) is None: + if glyphSets is not None: + oldFontIdx = fontIndexForGlyph[oldgid] + for idx in (fontIdx, oldFontIdx): + if glyphSets[idx] is None: + glyphSets[idx] = m.fonts[idx].getGlyphSet() + if _glyphsAreSame(glyphSets[oldFontIdx], glyphSets[fontIdx], oldgid, gid): + continue + m.duplicateGlyphsPerFont[fontIdx][oldgid] = gid + elif m.duplicateGlyphsPerFont[fontIdx][oldgid] != gid: + # Char previously mapped to oldgid but oldgid is already remapped to a different + # gid, because of another Unicode character. + # TODO: Try harder to do something about these. + log.warning("Dropped mapping from codepoint %#06X to glyphId '%s'", uni, gid) + + cmapBmpOnly = {uni: gid for uni,gid in cmap.items() if uni <= 0xFFFF} + self.tables = [] + module = ttLib.getTableModule('cmap') + if len(cmapBmpOnly) != len(cmap): + # format-12 required. + cmapTable = module.cmap_classes[12](12) + cmapTable.platformID = 3 + cmapTable.platEncID = 10 + cmapTable.language = 0 + cmapTable.cmap = cmap + self.tables.append(cmapTable) + # always create format-4 + cmapTable = module.cmap_classes[4](4) + cmapTable.platformID = 3 + cmapTable.platEncID = 1 + cmapTable.language = 0 + cmapTable.cmap = cmapBmpOnly + # ordered by platform then encoding + self.tables.insert(0, cmapTable) self.tableVersion = 0 - self.tables = [cmapTable] self.numSubTables = len(self.tables) return self +def mergeLookupLists(lst): + # TODO Do smarter merge. + return sumLists(lst) + +def mergeFeatures(lst): + assert lst + self = otTables.Feature() + self.FeatureParams = None + self.LookupListIndex = mergeLookupLists([l.LookupListIndex for l in lst if l.LookupListIndex]) + self.LookupCount = len(self.LookupListIndex) + return self + +def mergeFeatureLists(lst): + d = {} + for l in lst: + for f in l: + tag = f.FeatureTag + if tag not in d: + d[tag] = [] + d[tag].append(f.Feature) + ret = [] + for tag in sorted(d.keys()): + rec = otTables.FeatureRecord() + rec.FeatureTag = tag + rec.Feature = mergeFeatures(d[tag]) + ret.append(rec) + return ret + +def mergeLangSyses(lst): + assert lst + + # TODO Support merging ReqFeatureIndex + assert all(l.ReqFeatureIndex == 0xFFFF for l in lst) + + self = otTables.LangSys() + self.LookupOrder = None + self.ReqFeatureIndex = 0xFFFF + self.FeatureIndex = mergeFeatureLists([l.FeatureIndex for l in lst if l.FeatureIndex]) + self.FeatureCount = len(self.FeatureIndex) + return self + +def mergeScripts(lst): + assert lst + + if len(lst) == 1: + return lst[0] + langSyses = {} + for sr in lst: + for lsr in sr.LangSysRecord: + if lsr.LangSysTag not in langSyses: + langSyses[lsr.LangSysTag] = [] + langSyses[lsr.LangSysTag].append(lsr.LangSys) + lsrecords = [] + for tag, langSys_list in sorted(langSyses.items()): + lsr = otTables.LangSysRecord() + lsr.LangSys = mergeLangSyses(langSys_list) + lsr.LangSysTag = tag + lsrecords.append(lsr) + + self = otTables.Script() + self.LangSysRecord = lsrecords + self.LangSysCount = len(lsrecords) + self.DefaultLangSys = mergeLangSyses([s.DefaultLangSys for s in lst if s.DefaultLangSys]) + return self + +def mergeScriptRecords(lst): + d = {} + for l in lst: + for s in l: + tag = s.ScriptTag + if tag not in d: + d[tag] = [] + d[tag].append(s.Script) + ret = [] + for tag in sorted(d.keys()): + rec = otTables.ScriptRecord() + rec.ScriptTag = tag + rec.Script = mergeScripts(d[tag]) + ret.append(rec) + return ret + otTables.ScriptList.mergeMap = { - 'ScriptCount': sum, - 'ScriptRecord': lambda lst: sorted(sumLists(lst), key=lambda s: s.ScriptTag), + 'ScriptCount': lambda lst: None, # TODO + 'ScriptRecord': mergeScriptRecords, +} +otTables.BaseScriptList.mergeMap = { + 'BaseScriptCount': lambda lst: None, # TODO + # TODO: Merge duplicate entries + 'BaseScriptRecord': lambda lst: sorted(sumLists(lst), key=lambda s: s.BaseScriptTag), } otTables.FeatureList.mergeMap = { 'FeatureCount': sum, - 'FeatureRecord': sumLists, + 'FeatureRecord': lambda lst: sorted(sumLists(lst), key=lambda s: s.FeatureTag), } otTables.LookupList.mergeMap = { @@ -396,10 +538,12 @@ otTables.LookupList.mergeMap = { } otTables.Coverage.mergeMap = { + 'Format': min, 'glyphs': sumLists, } otTables.ClassDef.mergeMap = { + 'Format': min, 'classDefs': sumDicts, } @@ -422,12 +566,23 @@ otTables.MarkGlyphSetsDef.mergeMap = { 'Coverage': sumLists, } -otTables.GDEF.mergeMap = { +otTables.Axis.mergeMap = { '*': mergeObjects, - 'Version': max, } -otTables.GSUB.mergeMap = otTables.GPOS.mergeMap = { +# XXX Fix BASE table merging +otTables.BaseTagList.mergeMap = { + 'BaseTagCount': sum, + 'BaselineTag': sumLists, +} + +otTables.GDEF.mergeMap = \ +otTables.GSUB.mergeMap = \ +otTables.GPOS.mergeMap = \ +otTables.BASE.mergeMap = \ +otTables.JSTF.mergeMap = \ +otTables.MATH.mergeMap = \ +{ '*': mergeObjects, 'Version': max, } @@ -449,15 +604,14 @@ def merge(self, m, tables): assert len(tables) == len(m.duplicateGlyphsPerFont) for i,(table,dups) in enumerate(zip(tables, m.duplicateGlyphsPerFont)): if not dups: continue - assert (table is not None and table is not NotImplemented), "Have duplicates to resolve for font %d but no GSUB" % (i + 1) - lookupMap = dict((id(v),v) for v in table.table.LookupList.Lookup) - featureMap = dict((id(v),v) for v in table.table.FeatureList.FeatureRecord) + assert (table is not None and table is not NotImplemented), "Have duplicates to resolve for font %d but no GSUB: %s" % (i + 1, dups) synthFeature = None synthLookup = None for script in table.table.ScriptList.ScriptRecord: if script.ScriptTag == 'DFLT': continue # XXX for langsys in [script.Script.DefaultLangSys] + [l.LangSys for l in script.Script.LangSysRecord]: - feature = [featureMap[v] for v in langsys.FeatureIndex if featureMap[v].FeatureTag == 'locl'] + if langsys is None: continue # XXX Create! + feature = [v for v in langsys.FeatureIndex if v.FeatureTag == 'locl'] assert len(feature) <= 1 if feature: feature = feature[0] @@ -469,9 +623,8 @@ def merge(self, m, tables): f.FeatureParams = None f.LookupCount = 0 f.LookupListIndex = [] - langsys.FeatureIndex.append(id(synthFeature)) - featureMap[id(synthFeature)] = synthFeature - langsys.FeatureIndex.sort(key=lambda v: featureMap[v].FeatureTag) + langsys.FeatureIndex.append(synthFeature) + langsys.FeatureIndex.sort(key=lambda v: v.FeatureTag) table.table.FeatureList.FeatureRecord.append(synthFeature) table.table.FeatureList.FeatureCount += 1 feature = synthFeature @@ -484,98 +637,110 @@ def merge(self, m, tables): synthLookup.LookupType = 1 synthLookup.SubTableCount = 1 synthLookup.SubTable = [subtable] + if table.table.LookupList is None: + # mtiLib uses None as default value for LookupList, + # while feaLib points to an empty array with count 0 + # TODO: make them do the same + table.table.LookupList = otTables.LookupList() + table.table.LookupList.Lookup = [] + table.table.LookupList.LookupCount = 0 table.table.LookupList.Lookup.append(synthLookup) table.table.LookupList.LookupCount += 1 - feature.Feature.LookupListIndex[:0] = [id(synthLookup)] + feature.Feature.LookupListIndex[:0] = [synthLookup] feature.Feature.LookupCount += 1 - DefaultTable.merge(self, m, tables) return self - - @_add_method(otTables.SingleSubst, - otTables.MultipleSubst, - otTables.AlternateSubst, - otTables.LigatureSubst, - otTables.ReverseChainSingleSubst, - otTables.SinglePos, - otTables.PairPos, - otTables.CursivePos, - otTables.MarkBasePos, - otTables.MarkLigPos, - otTables.MarkMarkPos) + otTables.MultipleSubst, + otTables.AlternateSubst, + otTables.LigatureSubst, + otTables.ReverseChainSingleSubst, + otTables.SinglePos, + otTables.PairPos, + otTables.CursivePos, + otTables.MarkBasePos, + otTables.MarkLigPos, + otTables.MarkMarkPos) def mapLookups(self, lookupMap): - pass + pass # Copied and trimmed down from subset.py @_add_method(otTables.ContextSubst, - otTables.ChainContextSubst, - otTables.ContextPos, - otTables.ChainContextPos) -def __classify_context(self): - - class ContextHelper(object): - def __init__(self, klass, Format): - if klass.__name__.endswith('Subst'): - Typ = 'Sub' - Type = 'Subst' - else: - Typ = 'Pos' - Type = 'Pos' - if klass.__name__.startswith('Chain'): - Chain = 'Chain' - else: - Chain = '' - ChainTyp = Chain+Typ - - self.Typ = Typ - self.Type = Type - self.Chain = Chain - self.ChainTyp = ChainTyp - - self.LookupRecord = Type+'LookupRecord' - - if Format == 1: - self.Rule = ChainTyp+'Rule' - self.RuleSet = ChainTyp+'RuleSet' - elif Format == 2: - self.Rule = ChainTyp+'ClassRule' - self.RuleSet = ChainTyp+'ClassSet' - - if self.Format not in [1, 2, 3]: - return None # Don't shoot the messenger; let it go - if not hasattr(self.__class__, "__ContextHelpers"): - self.__class__.__ContextHelpers = {} - if self.Format not in self.__class__.__ContextHelpers: - helper = ContextHelper(self.__class__, self.Format) - self.__class__.__ContextHelpers[self.Format] = helper - return self.__class__.__ContextHelpers[self.Format] + otTables.ChainContextSubst, + otTables.ContextPos, + otTables.ChainContextPos) +def __merge_classify_context(self): + + class ContextHelper(object): + def __init__(self, klass, Format): + if klass.__name__.endswith('Subst'): + Typ = 'Sub' + Type = 'Subst' + else: + Typ = 'Pos' + Type = 'Pos' + if klass.__name__.startswith('Chain'): + Chain = 'Chain' + else: + Chain = '' + ChainTyp = Chain+Typ + + self.Typ = Typ + self.Type = Type + self.Chain = Chain + self.ChainTyp = ChainTyp + + self.LookupRecord = Type+'LookupRecord' + + if Format == 1: + self.Rule = ChainTyp+'Rule' + self.RuleSet = ChainTyp+'RuleSet' + elif Format == 2: + self.Rule = ChainTyp+'ClassRule' + self.RuleSet = ChainTyp+'ClassSet' + + if self.Format not in [1, 2, 3]: + return None # Don't shoot the messenger; let it go + if not hasattr(self.__class__, "__ContextHelpers"): + self.__class__.__ContextHelpers = {} + if self.Format not in self.__class__.__ContextHelpers: + helper = ContextHelper(self.__class__, self.Format) + self.__class__.__ContextHelpers[self.Format] = helper + return self.__class__.__ContextHelpers[self.Format] @_add_method(otTables.ContextSubst, - otTables.ChainContextSubst, - otTables.ContextPos, - otTables.ChainContextPos) + otTables.ChainContextSubst, + otTables.ContextPos, + otTables.ChainContextPos) +def mapLookups(self, lookupMap): + c = self.__merge_classify_context() + + if self.Format in [1, 2]: + for rs in getattr(self, c.RuleSet): + if not rs: continue + for r in getattr(rs, c.Rule): + if not r: continue + for ll in getattr(r, c.LookupRecord): + if not ll: continue + ll.LookupListIndex = lookupMap[ll.LookupListIndex] + elif self.Format == 3: + for ll in getattr(self, c.LookupRecord): + if not ll: continue + ll.LookupListIndex = lookupMap[ll.LookupListIndex] + else: + assert 0, "unknown format: %s" % self.Format + +@_add_method(otTables.ExtensionSubst, + otTables.ExtensionPos) def mapLookups(self, lookupMap): - c = self.__classify_context() - - if self.Format in [1, 2]: - for rs in getattr(self, c.RuleSet): - if not rs: continue - for r in getattr(rs, c.Rule): - if not r: continue - for ll in getattr(r, c.LookupRecord): - if not ll: continue - ll.LookupListIndex = lookupMap[ll.LookupListIndex] - elif self.Format == 3: - for ll in getattr(self, c.LookupRecord): - if not ll: continue - ll.LookupListIndex = lookupMap[ll.LookupListIndex] - else: - assert 0, "unknown format: %s" % self.Format + if self.Format == 1: + self.ExtSubTable.mapLookups(lookupMap) + else: + assert 0, "unknown format: %s" % self.Format @_add_method(otTables.Lookup) def mapLookups(self, lookupMap): @@ -600,7 +765,7 @@ def mapLookups(self, lookupMap): f.Feature.mapLookups(lookupMap) @_add_method(otTables.DefaultLangSys, - otTables.LangSys) + otTables.LangSys) def mapFeatures(self, featureMap): self.FeatureIndex = [featureMap[i] for i in self.FeatureIndex] if self.ReqFeatureIndex != 65535: @@ -623,91 +788,135 @@ def mapFeatures(self, featureMap): class Options(object): - class UnknownOptionError(Exception): - pass - - def __init__(self, **kwargs): - - self.set(**kwargs) - - def set(self, **kwargs): - for k,v in kwargs.items(): - if not hasattr(self, k): - raise self.UnknownOptionError("Unknown option '%s'" % k) - setattr(self, k, v) - - def parse_opts(self, argv, ignore_unknown=False): - ret = [] - opts = {} - for a in argv: - orig_a = a - if not a.startswith('--'): - ret.append(a) - continue - a = a[2:] - i = a.find('=') - op = '=' - if i == -1: - if a.startswith("no-"): - k = a[3:] - v = False - else: - k = a - v = True - else: - k = a[:i] - if k[-1] in "-+": - op = k[-1]+'=' # Ops is '-=' or '+=' now. - k = k[:-1] - v = a[i+1:] - k = k.replace('-', '_') - if not hasattr(self, k): - if ignore_unknown == True or k in ignore_unknown: - ret.append(orig_a) - continue - else: - raise self.UnknownOptionError("Unknown option '%s'" % a) - - ov = getattr(self, k) - if isinstance(ov, bool): - v = bool(v) - elif isinstance(ov, int): - v = int(v) - elif isinstance(ov, list): - vv = v.split(',') - if vv == ['']: - vv = [] - vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv] - if op == '=': - v = vv - elif op == '+=': - v = ov - v.extend(vv) - elif op == '-=': - v = ov - for x in vv: - if x in v: - v.remove(x) - else: - assert 0 - - opts[k] = v - self.set(**opts) - - return ret + class UnknownOptionError(Exception): + pass + def __init__(self, **kwargs): + + self.verbose = False + self.timing = False + + self.set(**kwargs) + + def set(self, **kwargs): + for k,v in kwargs.items(): + if not hasattr(self, k): + raise self.UnknownOptionError("Unknown option '%s'" % k) + setattr(self, k, v) + + def parse_opts(self, argv, ignore_unknown=[]): + ret = [] + opts = {} + for a in argv: + orig_a = a + if not a.startswith('--'): + ret.append(a) + continue + a = a[2:] + i = a.find('=') + op = '=' + if i == -1: + if a.startswith("no-"): + k = a[3:] + v = False + else: + k = a + v = True + else: + k = a[:i] + if k[-1] in "-+": + op = k[-1]+'=' # Ops is '-=' or '+=' now. + k = k[:-1] + v = a[i+1:] + k = k.replace('-', '_') + if not hasattr(self, k): + if ignore_unknown is True or k in ignore_unknown: + ret.append(orig_a) + continue + else: + raise self.UnknownOptionError("Unknown option '%s'" % a) + + ov = getattr(self, k) + if isinstance(ov, bool): + v = bool(v) + elif isinstance(ov, int): + v = int(v) + elif isinstance(ov, list): + vv = v.split(',') + if vv == ['']: + vv = [] + vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv] + if op == '=': + v = vv + elif op == '+=': + v = ov + v.extend(vv) + elif op == '-=': + v = ov + for x in vv: + if x in v: + v.remove(x) + else: + assert 0 + + opts[k] = v + self.set(**opts) + + return ret + +class _AttendanceRecordingIdentityDict(object): + """A dictionary-like object that records indices of items actually accessed + from a list.""" + + def __init__(self, lst): + self.l = lst + self.d = {id(v):i for i,v in enumerate(lst)} + self.s = set() + + def __getitem__(self, v): + self.s.add(self.d[id(v)]) + return v + +class _GregariousIdentityDict(object): + """A dictionary-like object that welcomes guests without reservations and + adds them to the end of the guest list.""" + + def __init__(self, lst): + self.l = lst + self.s = set(id(v) for v in lst) + + def __getitem__(self, v): + if id(v) not in self.s: + self.s.add(id(v)) + self.l.append(v) + return v + +class _NonhashableDict(object): + """A dictionary-like object mapping objects to values.""" + + def __init__(self, keys, values=None): + if values is None: + self.d = {id(v):i for i,v in enumerate(keys)} + else: + self.d = {id(k):v for k,v in zip(keys, values)} + + def __getitem__(self, k): + return self.d[id(k)] + + def __setitem__(self, k, v): + self.d[id(k)] = v + + def __delitem__(self, k): + del self.d[id(k)] class Merger(object): - def __init__(self, options=None, log=None): + def __init__(self, options=None): - if not log: - log = Logger() if not options: options = Options() self.options = options - self.log = log def merge(self, fontfiles): @@ -730,29 +939,37 @@ class Merger(object): for font in fonts: self._preMerge(font) + self.fonts = fonts self.duplicateGlyphsPerFont = [{} for f in fonts] allTags = reduce(set.union, (list(font.keys()) for font in fonts), set()) allTags.remove('GlyphOrder') - allTags.remove('cmap') - allTags.remove('GSUB') - allTags = ['cmap', 'GSUB'] + list(allTags) - for tag in allTags: - tables = [font.get(tag, NotImplemented) for font in fonts] + # Make sure we process cmap before GSUB as we have a dependency there. + if 'GSUB' in allTags: + allTags.remove('GSUB') + allTags = ['GSUB'] + list(allTags) + if 'cmap' in allTags: + allTags.remove('cmap') + allTags = ['cmap'] + list(allTags) - clazz = ttLib.getTableClass(tag) - table = clazz(tag).merge(self, tables) - # XXX Clean this up and use: table = mergeObjects(tables) + for tag in allTags: + with timer("merge '%s'" % tag): + tables = [font.get(tag, NotImplemented) for font in fonts] - if table is not NotImplemented and table is not False: - mega[tag] = table - self.log("Merged '%s'." % tag) - else: - self.log("Dropped '%s'." % tag) - self.log.lapse("merge '%s'" % tag) + log.info("Merging '%s'.", tag) + clazz = ttLib.getTableClass(tag) + table = clazz(tag).merge(self, tables) + # XXX Clean this up and use: table = mergeObjects(tables) + + if table is not NotImplemented and table is not False: + mega[tag] = table + log.info("Merged '%s'.", tag) + else: + log.info("Dropped '%s'.", tag) del self.duplicateGlyphsPerFont + del self.fonts self._postMerge(mega) @@ -784,7 +1001,7 @@ class Merger(object): try: mergeLogic = logic['*'] except KeyError: - raise Exception("Don't know how to merge key %s of class %s" % + raise Exception("Don't know how to merge key %s of class %s" % (key, returnTable.__class__.__name__)) if mergeLogic is NotImplemented: continue @@ -806,14 +1023,12 @@ class Merger(object): if not t: continue if t.table.LookupList: - lookupMap = dict((i,id(v)) for i,v in enumerate(t.table.LookupList.Lookup)) + lookupMap = {i:v for i,v in enumerate(t.table.LookupList.Lookup)} t.table.LookupList.mapLookups(lookupMap) - if t.table.FeatureList: - # XXX Handle present FeatureList but absent LookupList - t.table.FeatureList.mapLookups(lookupMap) + t.table.FeatureList.mapLookups(lookupMap) if t.table.FeatureList and t.table.ScriptList: - featureMap = dict((i,id(v)) for i,v in enumerate(t.table.FeatureList.FeatureRecord)) + featureMap = {i:v for i,v in enumerate(t.table.FeatureList.FeatureRecord)} t.table.ScriptList.mapFeatures(featureMap) # TODO GDEF/Lookup MarkFilteringSets @@ -830,92 +1045,85 @@ class Merger(object): for t in [GSUB, GPOS]: if not t: continue + if t.table.FeatureList and t.table.ScriptList: + + # Collect unregistered (new) features. + featureMap = _GregariousIdentityDict(t.table.FeatureList.FeatureRecord) + t.table.ScriptList.mapFeatures(featureMap) + + # Record used features. + featureMap = _AttendanceRecordingIdentityDict(t.table.FeatureList.FeatureRecord) + t.table.ScriptList.mapFeatures(featureMap) + usedIndices = featureMap.s + + # Remove unused features + t.table.FeatureList.FeatureRecord = [f for i,f in enumerate(t.table.FeatureList.FeatureRecord) if i in usedIndices] + + # Map back to indices. + featureMap = _NonhashableDict(t.table.FeatureList.FeatureRecord) + t.table.ScriptList.mapFeatures(featureMap) + + t.table.FeatureList.FeatureCount = len(t.table.FeatureList.FeatureRecord) + if t.table.LookupList: - lookupMap = dict((id(v),i) for i,v in enumerate(t.table.LookupList.Lookup)) + + # Collect unregistered (new) lookups. + lookupMap = _GregariousIdentityDict(t.table.LookupList.Lookup) + t.table.FeatureList.mapLookups(lookupMap) t.table.LookupList.mapLookups(lookupMap) - if t.table.FeatureList: - # XXX Handle present FeatureList but absent LookupList - t.table.FeatureList.mapLookups(lookupMap) - if t.table.FeatureList and t.table.ScriptList: - # XXX Handle present ScriptList but absent FeatureList - featureMap = dict((id(v),i) for i,v in enumerate(t.table.FeatureList.FeatureRecord)) - t.table.ScriptList.mapFeatures(featureMap) + # Record used lookups. + lookupMap = _AttendanceRecordingIdentityDict(t.table.LookupList.Lookup) + t.table.FeatureList.mapLookups(lookupMap) + t.table.LookupList.mapLookups(lookupMap) + usedIndices = lookupMap.s - # TODO GDEF/Lookup MarkFilteringSets - # TODO FeatureParams nameIDs + # Remove unused lookups + t.table.LookupList.Lookup = [l for i,l in enumerate(t.table.LookupList.Lookup) if i in usedIndices] + # Map back to indices. + lookupMap = _NonhashableDict(t.table.LookupList.Lookup) + t.table.FeatureList.mapLookups(lookupMap) + t.table.LookupList.mapLookups(lookupMap) -class Logger(object): - - def __init__(self, verbose=False, xml=False, timing=False): - self.verbose = verbose - self.xml = xml - self.timing = timing - self.last_time = self.start_time = time.time() - - def parse_opts(self, argv): - argv = argv[:] - for v in ['verbose', 'xml', 'timing']: - if "--"+v in argv: - setattr(self, v, True) - argv.remove("--"+v) - return argv - - def __call__(self, *things): - if not self.verbose: - return - print(' '.join(str(x) for x in things)) - - def lapse(self, *things): - if not self.timing: - return - new_time = time.time() - print("Took %0.3fs to %s" %(new_time - self.last_time, - ' '.join(str(x) for x in things))) - self.last_time = new_time - - def font(self, font, file=sys.stdout): - if not self.xml: - return - from fontTools.misc import xmlWriter - writer = xmlWriter.XMLWriter(file) - font.disassembleInstructions = False # Work around ttLib bug - for tag in font.keys(): - writer.begintag(tag) - writer.newline() - font[tag].toXML(writer, font) - writer.endtag(tag) - writer.newline() + t.table.LookupList.LookupCount = len(t.table.LookupList.Lookup) + + # TODO GDEF/Lookup MarkFilteringSets + # TODO FeatureParams nameIDs __all__ = [ - 'Options', - 'Merger', - 'Logger', - 'main' + 'Options', + 'Merger', + 'main' ] -def main(args): +@timer("make one with everything (TOTAL TIME)") +def main(args=None): + from fontTools import configLogger - log = Logger() - args = log.parse_opts(args) + if args is None: + args = sys.argv[1:] options = Options() args = options.parse_opts(args) if len(args) < 1: print("usage: pyftmerge font...", file=sys.stderr) - sys.exit(1) + return 1 + + configLogger(level=logging.INFO if options.verbose else logging.WARNING) + if options.timing: + timer.logger.setLevel(logging.DEBUG) + else: + timer.logger.disabled = True - merger = Merger(options=options, log=log) + merger = Merger(options=options) font = merger.merge(args) outfile = 'merged.ttf' - font.save(outfile) - log.lapse("compile and save font") + with timer("compile and save font"): + font.save(outfile) - log.last_time = log.start_time - log.lapse("make one with everything(TOTAL TIME)") if __name__ == "__main__": - main(sys.argv[1:]) + sys.exit(main()) diff --git a/Lib/fontTools/misc/__init__.py b/Lib/fontTools/misc/__init__.py index e001bb2..3f9abc9 100644 --- a/Lib/fontTools/misc/__init__.py +++ b/Lib/fontTools/misc/__init__.py @@ -1,3 +1,4 @@ -"""Empty __init__.py file to signal Python this directory is a package. -(It can't be completely empty since WinZip seems to skip empty files.) -""" +"""Empty __init__.py file to signal Python this directory is a package.""" + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * diff --git a/Lib/fontTools/misc/arrayTools.py b/Lib/fontTools/misc/arrayTools.py index 0daabd9..f2cfac8 100644 --- a/Lib/fontTools/misc/arrayTools.py +++ b/Lib/fontTools/misc/arrayTools.py @@ -6,7 +6,9 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * +from numbers import Number import math +import operator def calcBounds(array): """Return the bounding rectangle of a 2D points array as a tuple: @@ -21,13 +23,9 @@ def calcBounds(array): def calcIntBounds(array): """Return the integer bounding rectangle of a 2D points array as a tuple: (xMin, yMin, xMax, yMax) + Values are rounded to closest integer. """ - xMin, yMin, xMax, yMax = calcBounds(array) - xMin = int(math.floor(xMin)) - xMax = int(math.ceil(xMax)) - yMin = int(math.floor(yMin)) - yMax = int(math.ceil(yMax)) - return xMin, yMin, xMax, yMax + return tuple(round(v) for v in calcBounds(array)) def updateBounds(bounds, p, min=min, max=max): @@ -43,7 +41,7 @@ def pointInRect(p, rect): return (xMin <= x <= xMax) and (yMin <= y <= yMax) def pointsInRect(array, rect): - """Find out which points or array are inside rect. + """Find out which points or array are inside rect. Returns an array with a boolean for each point. """ if len(array) < 1: @@ -59,7 +57,7 @@ def vectorLength(vector): def asInt16(array): """Round and cast to 16 bit integer.""" return [int(math.floor(i+0.5)) for i in array] - + def normRect(rect): """Normalize the rectangle so that the following holds: @@ -124,6 +122,136 @@ def intRect(rect1): return (xMin, yMin, xMax, yMax) +class Vector(object): + """A math-like vector.""" + + def __init__(self, values, keep=False): + self.values = values if keep else list(values) + + def __getitem__(self, index): + return self.values[index] + + def __len__(self): + return len(self.values) + + def __repr__(self): + return "Vector(%s)" % self.values + + def _vectorOp(self, other, op): + if isinstance(other, Vector): + assert len(self.values) == len(other.values) + a = self.values + b = other.values + return [op(a[i], b[i]) for i in range(len(self.values))] + if isinstance(other, Number): + return [op(v, other) for v in self.values] + raise NotImplementedError + + def _scalarOp(self, other, op): + if isinstance(other, Number): + return [op(v, other) for v in self.values] + raise NotImplementedError + + def _unaryOp(self, op): + return [op(v) for v in self.values] + + def __add__(self, other): + return Vector(self._vectorOp(other, operator.add), keep=True) + def __iadd__(self, other): + self.values = self._vectorOp(other, operator.add) + return self + __radd__ = __add__ + + def __sub__(self, other): + return Vector(self._vectorOp(other, operator.sub), keep=True) + def __isub__(self, other): + self.values = self._vectorOp(other, operator.sub) + return self + def __rsub__(self, other): + return other + (-self) + + def __mul__(self, other): + return Vector(self._scalarOp(other, operator.mul), keep=True) + def __imul__(self, other): + self.values = self._scalarOp(other, operator.mul) + return self + __rmul__ = __mul__ + + def __truediv__(self, other): + return Vector(self._scalarOp(other, operator.div), keep=True) + def __itruediv__(self, other): + self.values = self._scalarOp(other, operator.div) + return self + + def __pos__(self): + return Vector(self._unaryOp(operator.pos), keep=True) + def __neg__(self): + return Vector(self._unaryOp(operator.neg), keep=True) + def __round__(self): + return Vector(self._unaryOp(round), keep=True) + def toInt(self): + return self.__round__() + + def __eq__(self, other): + if type(other) == Vector: + return self.values == other.values + else: + return self.values == other + def __ne__(self, other): + return not self.__eq__(other) + + def __bool__(self): + return any(self.values) + __nonzero__ = __bool__ + + def __abs__(self): + return math.sqrt(sum([x*x for x in self.values])) + def dot(self, other): + a = self.values + b = other.values if type(other) == Vector else b + assert len(a) == len(b) + return sum([a[i] * b[i] for i in range(len(a))]) + + +def pairwise(iterable, reverse=False): + """Iterate over current and next items in iterable, optionally in + reverse order. + + >>> tuple(pairwise([])) + () + >>> tuple(pairwise([], reverse=True)) + () + >>> tuple(pairwise([0])) + ((0, 0),) + >>> tuple(pairwise([0], reverse=True)) + ((0, 0),) + >>> tuple(pairwise([0, 1])) + ((0, 1), (1, 0)) + >>> tuple(pairwise([0, 1], reverse=True)) + ((1, 0), (0, 1)) + >>> tuple(pairwise([0, 1, 2])) + ((0, 1), (1, 2), (2, 0)) + >>> tuple(pairwise([0, 1, 2], reverse=True)) + ((2, 1), (1, 0), (0, 2)) + >>> tuple(pairwise(['a', 'b', 'c', 'd'])) + (('a', 'b'), ('b', 'c'), ('c', 'd'), ('d', 'a')) + >>> tuple(pairwise(['a', 'b', 'c', 'd'], reverse=True)) + (('d', 'c'), ('c', 'b'), ('b', 'a'), ('a', 'd')) + """ + if not iterable: + return + if reverse: + it = reversed(iterable) + else: + it = iter(iterable) + first = next(it, None) + a = first + for b in it: + yield (a, b) + a = b + yield (a, first) + + def _test(): """ >>> import math @@ -180,5 +308,6 @@ def _test(): """ if __name__ == "__main__": + import sys import doctest - doctest.testmod() + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/misc/bezierTools.py b/Lib/fontTools/misc/bezierTools.py index 6d9f8ce..7305305 100644 --- a/Lib/fontTools/misc/bezierTools.py +++ b/Lib/fontTools/misc/bezierTools.py @@ -1,12 +1,24 @@ +# -*- coding: utf-8 -*- """fontTools.misc.bezierTools.py -- tools for working with bezier path segments. """ from __future__ import print_function, division, absolute_import +from fontTools.misc.arrayTools import calcBounds from fontTools.misc.py23 import * +import math + __all__ = [ - "calcQuadraticBounds", + "approximateCubicArcLength", + "approximateCubicArcLengthC", + "approximateQuadraticArcLength", + "approximateQuadraticArcLengthC", + "calcCubicArcLength", + "calcCubicArcLengthC", + "calcQuadraticArcLength", + "calcQuadraticArcLengthC", "calcCubicBounds", + "calcQuadraticBounds", "splitLine", "splitQuadratic", "splitCubic", @@ -16,9 +28,121 @@ __all__ = [ "solveCubic", ] -from fontTools.misc.arrayTools import calcBounds -epsilon = 1e-12 +def calcCubicArcLength(pt1, pt2, pt3, pt4, tolerance=0.005): + """Return the arc length for a cubic bezier segment.""" + return calcCubicArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3), complex(*pt4), tolerance) + + +def _split_cubic_into_two(p0, p1, p2, p3): + mid = (p0 + 3 * (p1 + p2) + p3) * .125 + deriv3 = (p3 + p2 - p1 - p0) * .125 + return ((p0, (p0 + p1) * .5, mid - deriv3, mid), + (mid, mid + deriv3, (p2 + p3) * .5, p3)) + +def _calcCubicArcLengthCRecurse(mult, p0, p1, p2, p3): + arch = abs(p0-p3) + box = abs(p0-p1) + abs(p1-p2) + abs(p2-p3) + if arch * mult >= box: + return (arch + box) * .5 + else: + one,two = _split_cubic_into_two(p0,p1,p2,p3) + return _calcCubicArcLengthCRecurse(mult, *one) + _calcCubicArcLengthCRecurse(mult, *two) + +def calcCubicArcLengthC(pt1, pt2, pt3, pt4, tolerance=0.005): + """Return the arc length for a cubic bezier segment using complex points.""" + mult = 1. + 1.5 * tolerance # The 1.5 is a empirical hack; no math + return _calcCubicArcLengthCRecurse(mult, pt1, pt2, pt3, pt4) + + +epsilonDigits = 6 +epsilon = 1e-10 + + +def _dot(v1, v2): + return (v1 * v2.conjugate()).real + + +def _intSecAtan(x): + # In : sympy.integrate(sp.sec(sp.atan(x))) + # Out: x*sqrt(x**2 + 1)/2 + asinh(x)/2 + return x * math.sqrt(x**2 + 1)/2 + math.asinh(x)/2 + + +def calcQuadraticArcLength(pt1, pt2, pt3): + """Return the arc length for a qudratic bezier segment. + pt1 and pt3 are the "anchor" points, pt2 is the "handle". + + >>> calcQuadraticArcLength((0, 0), (0, 0), (0, 0)) # empty segment + 0.0 + >>> calcQuadraticArcLength((0, 0), (50, 0), (80, 0)) # collinear points + 80.0 + >>> calcQuadraticArcLength((0, 0), (0, 50), (0, 80)) # collinear points vertical + 80.0 + >>> calcQuadraticArcLength((0, 0), (50, 20), (100, 40)) # collinear points + 107.70329614269008 + >>> calcQuadraticArcLength((0, 0), (0, 100), (100, 0)) + 154.02976155645263 + >>> calcQuadraticArcLength((0, 0), (0, 50), (100, 0)) + 120.21581243984076 + >>> calcQuadraticArcLength((0, 0), (50, -10), (80, 50)) + 102.53273816445825 + >>> calcQuadraticArcLength((0, 0), (40, 0), (-40, 0)) # collinear points, control point outside + 66.66666666666667 + >>> calcQuadraticArcLength((0, 0), (40, 0), (0, 0)) # collinear points, looping back + 40.0 + """ + return calcQuadraticArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3)) + + +def calcQuadraticArcLengthC(pt1, pt2, pt3): + """Return the arc length for a qudratic bezier segment using complex points. + pt1 and pt3 are the "anchor" points, pt2 is the "handle".""" + + # Analytical solution to the length of a quadratic bezier. + # I'll explain how I arrived at this later. + d0 = pt2 - pt1 + d1 = pt3 - pt2 + d = d1 - d0 + n = d * 1j + scale = abs(n) + if scale == 0.: + return abs(pt3-pt1) + origDist = _dot(n,d0) + if abs(origDist) < epsilon: + if _dot(d0,d1) >= 0: + return abs(pt3-pt1) + a, b = abs(d0), abs(d1) + return (a*a + b*b) / (a+b) + x0 = _dot(d,d0) / origDist + x1 = _dot(d,d1) / origDist + Len = abs(2 * (_intSecAtan(x1) - _intSecAtan(x0)) * origDist / (scale * (x1 - x0))) + return Len + + +def approximateQuadraticArcLength(pt1, pt2, pt3): + # Approximate length of quadratic Bezier curve using Gauss-Legendre quadrature + # with n=3 points. + return approximateQuadraticArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3)) + + +def approximateQuadraticArcLengthC(pt1, pt2, pt3): + # Approximate length of quadratic Bezier curve using Gauss-Legendre quadrature + # with n=3 points for complex points. + # + # This, essentially, approximates the length-of-derivative function + # to be integrated with the best-matching fifth-degree polynomial + # approximation of it. + # + #https://en.wikipedia.org/wiki/Gaussian_quadrature#Gauss.E2.80.93Legendre_quadrature + + # abs(BezierCurveC[2].diff(t).subs({t:T})) for T in sorted(.5, .5±sqrt(3/5)/2), + # weighted 5/18, 8/18, 5/18 respectively. + v0 = abs(-0.492943519233745*pt1 + 0.430331482911935*pt2 + 0.0626120363218102*pt3) + v1 = abs(pt3-pt1)*0.4444444444444444 + v2 = abs(-0.0626120363218102*pt1 - 0.430331482911935*pt2 + 0.492943519233745*pt3) + + return v0 + v1 + v2 def calcQuadraticBounds(pt1, pt2, pt3): @@ -42,6 +166,50 @@ def calcQuadraticBounds(pt1, pt2, pt3): return calcBounds(points) +def approximateCubicArcLength(pt1, pt2, pt3, pt4): + """Return the approximate arc length for a cubic bezier segment. + pt1 and pt4 are the "anchor" points, pt2 and pt3 are the "handles". + + >>> approximateCubicArcLength((0, 0), (25, 100), (75, 100), (100, 0)) + 190.04332968932817 + >>> approximateCubicArcLength((0, 0), (50, 0), (100, 50), (100, 100)) + 154.8852074945903 + >>> approximateCubicArcLength((0, 0), (50, 0), (100, 0), (150, 0)) # line; exact result should be 150. + 149.99999999999991 + >>> approximateCubicArcLength((0, 0), (50, 0), (100, 0), (-50, 0)) # cusp; exact result should be 150. + 136.9267662156362 + >>> approximateCubicArcLength((0, 0), (50, 0), (100, -50), (-50, 0)) # cusp + 154.80848416537057 + """ + # Approximate length of cubic Bezier curve using Gauss-Lobatto quadrature + # with n=5 points. + return approximateCubicArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3), complex(*pt4)) + + +def approximateCubicArcLengthC(pt1, pt2, pt3, pt4): + """Return the approximate arc length for a cubic bezier segment of complex points. + pt1 and pt4 are the "anchor" points, pt2 and pt3 are the "handles".""" + + # Approximate length of cubic Bezier curve using Gauss-Lobatto quadrature + # with n=5 points for complex points. + # + # This, essentially, approximates the length-of-derivative function + # to be integrated with the best-matching seventh-degree polynomial + # approximation of it. + # + # https://en.wikipedia.org/wiki/Gaussian_quadrature#Gauss.E2.80.93Lobatto_rules + + # abs(BezierCurveC[3].diff(t).subs({t:T})) for T in sorted(0, .5±(3/7)**.5/2, .5, 1), + # weighted 1/20, 49/180, 32/90, 49/180, 1/20 respectively. + v0 = abs(pt2-pt1)*.15 + v1 = abs(-0.558983582205757*pt1 + 0.325650248872424*pt2 + 0.208983582205757*pt3 + 0.024349751127576*pt4) + v2 = abs(pt4-pt1+pt3-pt2)*0.26666666666666666 + v3 = abs(-0.024349751127576*pt1 - 0.208983582205757*pt2 - 0.325650248872424*pt3 + 0.558983582205757*pt4) + v4 = abs(pt4-pt3)*.15 + + return v0 + v1 + v2 + v3 + v4 + + def calcCubicBounds(pt1, pt2, pt3, pt4): """Return the bounding rectangle for a cubic bezier segment. pt1 and pt4 are the "anchor" points, pt2 and pt3 are the "handles". @@ -50,7 +218,7 @@ def calcCubicBounds(pt1, pt2, pt3, pt4): (0, 0, 100, 75.0) >>> calcCubicBounds((0, 0), (50, 0), (100, 50), (100, 100)) (0.0, 0.0, 100, 100) - >>> print "%f %f %f %f" % calcCubicBounds((50, 0), (0, 100), (100, 100), (50, 0)) + >>> print("%f %f %f %f" % calcCubicBounds((50, 0), (0, 100), (100, 100), (50, 0))) 35.566243 0.000000 64.433757 75.000000 """ (ax, ay), (bx, by), (cx, cy), (dx, dy) = calcCubicParameters(pt1, pt2, pt3, pt4) @@ -62,7 +230,7 @@ def calcCubicBounds(pt1, pt2, pt3, pt4): xRoots = [t for t in solveQuadratic(ax3, bx2, cx) if 0 <= t < 1] yRoots = [t for t in solveQuadratic(ay3, by2, cy) if 0 <= t < 1] roots = xRoots + yRoots - + points = [(ax*t*t*t + bx*t*t + cx * t + dx, ay*t*t*t + by*t*t + cy * t + dy) for t in roots] + [pt1, pt4] return calcBounds(points) @@ -75,16 +243,22 @@ def splitLine(pt1, pt2, where, isHorizontal): line. >>> printSegments(splitLine((0, 0), (100, 100), 50, True)) - ((0, 0), (50.0, 50.0)) - ((50.0, 50.0), (100, 100)) + ((0, 0), (50, 50)) + ((50, 50), (100, 100)) >>> printSegments(splitLine((0, 0), (100, 100), 100, True)) ((0, 0), (100, 100)) >>> printSegments(splitLine((0, 0), (100, 100), 0, True)) - ((0, 0), (0.0, 0.0)) - ((0.0, 0.0), (100, 100)) + ((0, 0), (0, 0)) + ((0, 0), (100, 100)) >>> printSegments(splitLine((0, 0), (100, 100), 0, False)) - ((0, 0), (0.0, 0.0)) - ((0.0, 0.0), (100, 100)) + ((0, 0), (0, 0)) + ((0, 0), (100, 100)) + >>> printSegments(splitLine((100, 0), (0, 0), 50, False)) + ((100, 0), (50, 0)) + ((50, 0), (0, 0)) + >>> printSegments(splitLine((0, 100), (0, 0), 50, True)) + ((0, 100), (0, 50)) + ((0, 50), (0, 0)) """ pt1x, pt1y = pt1 pt2x, pt2y = pt2 @@ -95,10 +269,11 @@ def splitLine(pt1, pt2, where, isHorizontal): bx = pt1x by = pt1y - if ax == 0: - return [(pt1, pt2)] + a = (ax, ay)[isHorizontal] - t = (where - (bx, by)[isHorizontal]) / ax + if a == 0: + return [(pt1, pt2)] + t = (where - (bx, by)[isHorizontal]) / a if 0 <= t < 1: midPt = ax * t + bx, ay * t + by return [(pt1, midPt), (midPt, pt2)] @@ -114,20 +289,20 @@ def splitQuadratic(pt1, pt2, pt3, where, isHorizontal): >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 150, False)) ((0, 0), (50, 100), (100, 0)) >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 50, False)) - ((0.0, 0.0), (25.0, 50.0), (50.0, 50.0)) - ((50.0, 50.0), (75.0, 50.0), (100.0, 0.0)) + ((0, 0), (25, 50), (50, 50)) + ((50, 50), (75, 50), (100, 0)) >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 25, False)) - ((0.0, 0.0), (12.5, 25.0), (25.0, 37.5)) - ((25.0, 37.5), (62.5, 75.0), (100.0, 0.0)) + ((0, 0), (12.5, 25), (25, 37.5)) + ((25, 37.5), (62.5, 75), (100, 0)) >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 25, True)) - ((0.0, 0.0), (7.32233047034, 14.6446609407), (14.6446609407, 25.0)) - ((14.6446609407, 25.0), (50.0, 75.0), (85.3553390593, 25.0)) - ((85.3553390593, 25.0), (92.6776695297, 14.6446609407), (100.0, -7.1054273576e-15)) + ((0, 0), (7.32233, 14.6447), (14.6447, 25)) + ((14.6447, 25), (50, 75), (85.3553, 25)) + ((85.3553, 25), (92.6777, 14.6447), (100, -7.10543e-15)) >>> # XXX I'm not at all sure if the following behavior is desirable: >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 50, True)) - ((0.0, 0.0), (25.0, 50.0), (50.0, 50.0)) - ((50.0, 50.0), (50.0, 50.0), (50.0, 50.0)) - ((50.0, 50.0), (75.0, 50.0), (100.0, 0.0)) + ((0, 0), (25, 50), (50, 50)) + ((50, 50), (50, 50), (50, 50)) + ((50, 50), (75, 50), (100, 0)) """ a, b, c = calcQuadraticParameters(pt1, pt2, pt3) solutions = solveQuadratic(a[isHorizontal], b[isHorizontal], @@ -146,12 +321,12 @@ def splitCubic(pt1, pt2, pt3, pt4, where, isHorizontal): >>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 150, False)) ((0, 0), (25, 100), (75, 100), (100, 0)) >>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 50, False)) - ((0.0, 0.0), (12.5, 50.0), (31.25, 75.0), (50.0, 75.0)) - ((50.0, 75.0), (68.75, 75.0), (87.5, 50.0), (100.0, 0.0)) + ((0, 0), (12.5, 50), (31.25, 75), (50, 75)) + ((50, 75), (68.75, 75), (87.5, 50), (100, 0)) >>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 25, True)) - ((0.0, 0.0), (2.2937927384, 9.17517095361), (4.79804488188, 17.5085042869), (7.47413641001, 25.0)) - ((7.47413641001, 25.0), (31.2886200204, 91.6666666667), (68.7113799796, 91.6666666667), (92.52586359, 25.0)) - ((92.52586359, 25.0), (95.2019551181, 17.5085042869), (97.7062072616, 9.17517095361), (100.0, 1.7763568394e-15)) + ((0, 0), (2.29379, 9.17517), (4.79804, 17.5085), (7.47414, 25)) + ((7.47414, 25), (31.2886, 91.6667), (68.7114, 91.6667), (92.5259, 25)) + ((92.5259, 25), (95.202, 17.5085), (97.7062, 9.17517), (100, 1.77636e-15)) """ a, b, c, d = calcCubicParameters(pt1, pt2, pt3, pt4) solutions = solveCubic(a[isHorizontal], b[isHorizontal], c[isHorizontal], @@ -167,12 +342,12 @@ def splitQuadraticAtT(pt1, pt2, pt3, *ts): values of t. Return a list of curve segments. >>> printSegments(splitQuadraticAtT((0, 0), (50, 100), (100, 0), 0.5)) - ((0.0, 0.0), (25.0, 50.0), (50.0, 50.0)) - ((50.0, 50.0), (75.0, 50.0), (100.0, 0.0)) + ((0, 0), (25, 50), (50, 50)) + ((50, 50), (75, 50), (100, 0)) >>> printSegments(splitQuadraticAtT((0, 0), (50, 100), (100, 0), 0.5, 0.75)) - ((0.0, 0.0), (25.0, 50.0), (50.0, 50.0)) - ((50.0, 50.0), (62.5, 50.0), (75.0, 37.5)) - ((75.0, 37.5), (87.5, 25.0), (100.0, 0.0)) + ((0, 0), (25, 50), (50, 50)) + ((50, 50), (62.5, 50), (75, 37.5)) + ((75, 37.5), (87.5, 25), (100, 0)) """ a, b, c = calcQuadraticParameters(pt1, pt2, pt3) return _splitQuadraticAtT(a, b, c, *ts) @@ -183,12 +358,12 @@ def splitCubicAtT(pt1, pt2, pt3, pt4, *ts): values of t. Return a list of curve segments. >>> printSegments(splitCubicAtT((0, 0), (25, 100), (75, 100), (100, 0), 0.5)) - ((0.0, 0.0), (12.5, 50.0), (31.25, 75.0), (50.0, 75.0)) - ((50.0, 75.0), (68.75, 75.0), (87.5, 50.0), (100.0, 0.0)) + ((0, 0), (12.5, 50), (31.25, 75), (50, 75)) + ((50, 75), (68.75, 75), (87.5, 50), (100, 0)) >>> printSegments(splitCubicAtT((0, 0), (25, 100), (75, 100), (100, 0), 0.5, 0.75)) - ((0.0, 0.0), (12.5, 50.0), (31.25, 75.0), (50.0, 75.0)) - ((50.0, 75.0), (59.375, 75.0), (68.75, 68.75), (77.34375, 56.25)) - ((77.34375, 56.25), (85.9375, 43.75), (93.75, 25.0), (100.0, 0.0)) + ((0, 0), (12.5, 50), (31.25, 75), (50, 75)) + ((50, 75), (59.375, 75), (68.75, 68.75), (77.3438, 56.25)) + ((77.3438, 56.25), (85.9375, 43.75), (93.75, 25), (100, 0)) """ a, b, c, d = calcCubicParameters(pt1, pt2, pt3, pt4) return _splitCubicAtT(a, b, c, d, *ts) @@ -207,13 +382,15 @@ def _splitQuadraticAtT(a, b, c, *ts): t2 = ts[i+1] delta = (t2 - t1) # calc new a, b and c - a1x = ax * delta**2 - a1y = ay * delta**2 + delta_2 = delta*delta + a1x = ax * delta_2 + a1y = ay * delta_2 b1x = (2*ax*t1 + bx) * delta b1y = (2*ay*t1 + by) * delta - c1x = ax*t1**2 + bx*t1 + cx - c1y = ay*t1**2 + by*t1 + cy - + t1_2 = t1*t1 + c1x = ax*t1_2 + bx*t1 + cx + c1y = ay*t1_2 + by*t1 + cy + pt1, pt2, pt3 = calcQuadraticPoints((a1x, a1y), (b1x, b1y), (c1x, c1y)) segments.append((pt1, pt2, pt3)) return segments @@ -232,15 +409,21 @@ def _splitCubicAtT(a, b, c, d, *ts): t1 = ts[i] t2 = ts[i+1] delta = (t2 - t1) + + delta_2 = delta*delta + delta_3 = delta*delta_2 + t1_2 = t1*t1 + t1_3 = t1*t1_2 + # calc new a, b, c and d - a1x = ax * delta**3 - a1y = ay * delta**3 - b1x = (3*ax*t1 + bx) * delta**2 - b1y = (3*ay*t1 + by) * delta**2 - c1x = (2*bx*t1 + cx + 3*ax*t1**2) * delta - c1y = (2*by*t1 + cy + 3*ay*t1**2) * delta - d1x = ax*t1**3 + bx*t1**2 + cx*t1 + dx - d1y = ay*t1**3 + by*t1**2 + cy*t1 + dy + a1x = ax * delta_3 + a1y = ay * delta_3 + b1x = (3*ax*t1 + bx) * delta_2 + b1y = (3*ay*t1 + by) * delta_2 + c1x = (2*bx*t1 + cx + 3*ax*t1_2) * delta + c1y = (2*by*t1 + cy + 3*ay*t1_2) * delta + d1x = ax*t1_3 + bx*t1_2 + cx*t1 + dx + d1y = ay*t1_3 + by*t1_2 + cy*t1 + dy pt1, pt2, pt3, pt4 = calcCubicPoints((a1x, a1y), (b1x, b1y), (c1x, c1y), (d1x, d1y)) segments.append((pt1, pt2, pt3, pt4)) return segments @@ -284,6 +467,21 @@ def solveCubic(a, b, c, d): a*x*x*x + b*x*x + c*x + d = 0 This function returns a list of roots. Note that the returned list is neither guaranteed to be sorted nor to contain unique values! + + >>> solveCubic(1, 1, -6, 0) + [-3.0, -0.0, 2.0] + >>> solveCubic(-10.0, -9.0, 48.0, -29.0) + [-2.9, 1.0, 1.0] + >>> solveCubic(-9.875, -9.0, 47.625, -28.75) + [-2.911392, 1.0, 1.0] + >>> solveCubic(1.0, -4.5, 6.75, -3.375) + [1.5, 1.5, 1.5] + >>> solveCubic(-12.0, 18.0, -9.0, 1.50023651123) + [0.5, 0.5, 0.5] + >>> solveCubic( + ... 9.0, 0.0, 0.0, -7.62939453125e-05 + ... ) == [-0.0, -0.0, -0.0] + True """ # # adapted from: @@ -299,27 +497,49 @@ def solveCubic(a, b, c, d): a1 = b/a a2 = c/a a3 = d/a - + Q = (a1*a1 - 3.0*a2)/9.0 R = (2.0*a1*a1*a1 - 9.0*a1*a2 + 27.0*a3)/54.0 - R2_Q3 = R*R - Q*Q*Q - if R2_Q3 < 0: - theta = acos(R/sqrt(Q*Q*Q)) + R2 = R*R + Q3 = Q*Q*Q + R2 = 0 if R2 < epsilon else R2 + Q3 = 0 if abs(Q3) < epsilon else Q3 + + R2_Q3 = R2 - Q3 + + if R2 == 0. and Q3 == 0.: + x = round(-a1/3.0, epsilonDigits) + return [x, x, x] + elif R2_Q3 <= epsilon * .5: + # The epsilon * .5 above ensures that Q3 is not zero. + theta = acos(max(min(R/sqrt(Q3), 1.0), -1.0)) rQ2 = -2.0*sqrt(Q) - x0 = rQ2*cos(theta/3.0) - a1/3.0 - x1 = rQ2*cos((theta+2.0*pi)/3.0) - a1/3.0 - x2 = rQ2*cos((theta+4.0*pi)/3.0) - a1/3.0 + a1_3 = a1/3.0 + x0 = rQ2*cos(theta/3.0) - a1_3 + x1 = rQ2*cos((theta+2.0*pi)/3.0) - a1_3 + x2 = rQ2*cos((theta+4.0*pi)/3.0) - a1_3 + x0, x1, x2 = sorted([x0, x1, x2]) + # Merge roots that are close-enough + if x1 - x0 < epsilon and x2 - x1 < epsilon: + x0 = x1 = x2 = round((x0 + x1 + x2) / 3., epsilonDigits) + elif x1 - x0 < epsilon: + x0 = x1 = round((x0 + x1) / 2., epsilonDigits) + x2 = round(x2, epsilonDigits) + elif x2 - x1 < epsilon: + x0 = round(x0, epsilonDigits) + x1 = x2 = round((x1 + x2) / 2., epsilonDigits) + else: + x0 = round(x0, epsilonDigits) + x1 = round(x1, epsilonDigits) + x2 = round(x2, epsilonDigits) return [x0, x1, x2] else: - if Q == 0 and R == 0: - x = 0 - else: - x = pow(sqrt(R2_Q3)+abs(R), 1/3.0) - x = x + Q/x + x = pow(sqrt(R2_Q3)+abs(R), 1/3.0) + x = x + Q/x if R >= 0.0: x = -x - x = x - a1/3.0 + x = round(x - a1/3.0, epsilonDigits) return [x] @@ -389,7 +609,7 @@ def _segmentrepr(obj): try: it = iter(obj) except TypeError: - return str(obj) + return "%g" % obj else: return "(%s)" % ", ".join([_segmentrepr(x) for x in it]) @@ -402,5 +622,6 @@ def printSegments(segments): print(_segmentrepr(segment)) if __name__ == "__main__": + import sys import doctest - doctest.testmod() + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/misc/classifyTools.py b/Lib/fontTools/misc/classifyTools.py new file mode 100644 index 0000000..64bcdc8 --- /dev/null +++ b/Lib/fontTools/misc/classifyTools.py @@ -0,0 +1,173 @@ +""" fontTools.misc.classifyTools.py -- tools for classifying things. +""" + +from __future__ import print_function, absolute_import +from fontTools.misc.py23 import * + +class Classifier(object): + + """ + Main Classifier object, used to classify things into similar sets. + """ + + def __init__(self, sort=True): + + self._things = set() # set of all things known so far + self._sets = [] # list of class sets produced so far + self._mapping = {} # map from things to their class set + self._dirty = False + self._sort = sort + + def add(self, set_of_things): + """ + Add a set to the classifier. Any iterable is accepted. + """ + if not set_of_things: + return + + self._dirty = True + + things, sets, mapping = self._things, self._sets, self._mapping + + s = set(set_of_things) + intersection = s.intersection(things) # existing things + s.difference_update(intersection) # new things + difference = s + del s + + # Add new class for new things + if difference: + things.update(difference) + sets.append(difference) + for thing in difference: + mapping[thing] = difference + del difference + + while intersection: + # Take one item and process the old class it belongs to + old_class = mapping[next(iter(intersection))] + old_class_intersection = old_class.intersection(intersection) + + # Update old class to remove items from new set + old_class.difference_update(old_class_intersection) + + # Remove processed items from todo list + intersection.difference_update(old_class_intersection) + + # Add new class for the intersection with old class + sets.append(old_class_intersection) + for thing in old_class_intersection: + mapping[thing] = old_class_intersection + del old_class_intersection + + def update(self, list_of_sets): + """ + Add a a list of sets to the classifier. Any iterable of iterables is accepted. + """ + for s in list_of_sets: + self.add(s) + + def _process(self): + if not self._dirty: + return + + # Do any deferred processing + sets = self._sets + self._sets = [s for s in sets if s] + + if self._sort: + self._sets = sorted(self._sets, key=lambda s: (-len(s), sorted(s))) + + self._dirty = False + + # Output methods + + def getThings(self): + """Returns the set of all things known so far. + + The return value belongs to the Classifier object and should NOT + be modified while the classifier is still in use. + """ + self._process() + return self._things + + def getMapping(self): + """Returns the mapping from things to their class set. + + The return value belongs to the Classifier object and should NOT + be modified while the classifier is still in use. + """ + self._process() + return self._mapping + + def getClasses(self): + """Returns the list of class sets. + + The return value belongs to the Classifier object and should NOT + be modified while the classifier is still in use. + """ + self._process() + return self._sets + + +def classify(list_of_sets, sort=True): + """ + Takes a iterable of iterables (list of sets from here on; but any + iterable works.), and returns the smallest list of sets such that + each set, is either a subset, or is disjoint from, each of the input + sets. + + In other words, this function classifies all the things present in + any of the input sets, into similar classes, based on which sets + things are a member of. + + If sort=True, return class sets are sorted by decreasing size and + their natural sort order within each class size. Otherwise, class + sets are returned in the order that they were identified, which is + generally not significant. + + >>> classify([]) == ([], {}) + True + >>> classify([[]]) == ([], {}) + True + >>> classify([[], []]) == ([], {}) + True + >>> classify([[1]]) == ([{1}], {1: {1}}) + True + >>> classify([[1,2]]) == ([{1, 2}], {1: {1, 2}, 2: {1, 2}}) + True + >>> classify([[1],[2]]) == ([{1}, {2}], {1: {1}, 2: {2}}) + True + >>> classify([[1,2],[2]]) == ([{1}, {2}], {1: {1}, 2: {2}}) + True + >>> classify([[1,2],[2,4]]) == ([{1}, {2}, {4}], {1: {1}, 2: {2}, 4: {4}}) + True + >>> classify([[1,2],[2,4,5]]) == ( + ... [{4, 5}, {1}, {2}], {1: {1}, 2: {2}, 4: {4, 5}, 5: {4, 5}}) + True + >>> classify([[1,2],[2,4,5]], sort=False) == ( + ... [{1}, {4, 5}, {2}], {1: {1}, 2: {2}, 4: {4, 5}, 5: {4, 5}}) + True + >>> classify([[1,2,9],[2,4,5]], sort=False) == ( + ... [{1, 9}, {4, 5}, {2}], {1: {1, 9}, 2: {2}, 4: {4, 5}, 5: {4, 5}, + ... 9: {1, 9}}) + True + >>> classify([[1,2,9,15],[2,4,5]], sort=False) == ( + ... [{1, 9, 15}, {4, 5}, {2}], {1: {1, 9, 15}, 2: {2}, 4: {4, 5}, + ... 5: {4, 5}, 9: {1, 9, 15}, 15: {1, 9, 15}}) + True + >>> classes, mapping = classify([[1,2,9,15],[2,4,5],[15,5]], sort=False) + >>> set([frozenset(c) for c in classes]) == set( + ... [frozenset(s) for s in ({1, 9}, {4}, {2}, {5}, {15})]) + True + >>> mapping == {1: {1, 9}, 2: {2}, 4: {4}, 5: {5}, 9: {1, 9}, 15: {15}} + True + """ + classifier = Classifier(sort=sort) + classifier.update(list_of_sets) + return classifier.getClasses(), classifier.getMapping() + + +if __name__ == "__main__": + import sys, doctest + sys.exit(doctest.testmod(optionflags=doctest.ELLIPSIS).failed) diff --git a/Lib/fontTools/misc/cliTools.py b/Lib/fontTools/misc/cliTools.py new file mode 100644 index 0000000..59ac3be --- /dev/null +++ b/Lib/fontTools/misc/cliTools.py @@ -0,0 +1,26 @@ +"""Collection of utilities for command-line interfaces and console scripts.""" +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +import os +import re + + +numberAddedRE = re.compile("#\d+$") + + +def makeOutputFileName(input, outputDir=None, extension=None, overWrite=False): + dirName, fileName = os.path.split(input) + fileName, ext = os.path.splitext(fileName) + if outputDir: + dirName = outputDir + fileName = numberAddedRE.split(fileName)[0] + if extension is None: + extension = os.path.splitext(input)[1] + output = os.path.join(dirName, fileName + extension) + n = 1 + if not overWrite: + while os.path.exists(output): + output = os.path.join( + dirName, fileName + "#" + repr(n) + extension) + n += 1 + return output diff --git a/Lib/fontTools/misc/eexec.py b/Lib/fontTools/misc/eexec.py index b7656d7..0efa6f5 100644 --- a/Lib/fontTools/misc/eexec.py +++ b/Lib/fontTools/misc/eexec.py @@ -1,4 +1,4 @@ -"""fontTools.misc.eexec.py -- Module implementing the eexec and +"""fontTools.misc.eexec.py -- Module implementing the eexec and charstring encryption algorithm as used by PostScript Type 1 fonts. """ @@ -19,19 +19,35 @@ def _encryptChar(plain, R): def decrypt(cipherstring, R): + r""" + >>> testStr = b"\0\0asdadads asds\265" + >>> decryptedStr, R = decrypt(testStr, 12321) + >>> decryptedStr == b'0d\nh\x15\xe8\xc4\xb2\x15\x1d\x108\x1a<6\xa1' + True + >>> R == 36142 + True + """ plainList = [] for cipher in cipherstring: plain, R = _decryptChar(cipher, R) plainList.append(plain) - plainstring = strjoin(plainList) + plainstring = bytesjoin(plainList) return plainstring, int(R) def encrypt(plainstring, R): + r""" + >>> testStr = b'0d\nh\x15\xe8\xc4\xb2\x15\x1d\x108\x1a<6\xa1' + >>> encryptedStr, R = encrypt(testStr, 12321) + >>> encryptedStr == b"\0\0asdadads asds\265" + True + >>> R == 36142 + True + """ cipherList = [] for plain in plainstring: cipher, R = _encryptChar(plain, R) cipherList.append(cipher) - cipherstring = strjoin(cipherList) + cipherstring = bytesjoin(cipherList) return cipherstring, int(R) @@ -41,15 +57,11 @@ def hexString(s): def deHexString(h): import binascii - h = strjoin(h.split()) + h = bytesjoin(h.split()) return binascii.unhexlify(h) -def _test(): - testStr = "\0\0asdadads asds\265" - print(decrypt, decrypt(testStr, 12321)) - print(encrypt, encrypt(testStr, 12321)) - - if __name__ == "__main__": - _test() + import sys + import doctest + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/misc/encodingTools.py b/Lib/fontTools/misc/encodingTools.py new file mode 100644 index 0000000..275ae9f --- /dev/null +++ b/Lib/fontTools/misc/encodingTools.py @@ -0,0 +1,73 @@ +"""fontTools.misc.encodingTools.py -- tools for working with OpenType encodings. +""" + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +import fontTools.encodings.codecs + +# Map keyed by platformID, then platEncID, then possibly langID +_encodingMap = { + 0: { # Unicode + 0: 'utf_16_be', + 1: 'utf_16_be', + 2: 'utf_16_be', + 3: 'utf_16_be', + 4: 'utf_16_be', + 5: 'utf_16_be', + 6: 'utf_16_be', + }, + 1: { # Macintosh + # See + # https://github.com/behdad/fonttools/issues/236 + 0: { # Macintosh, platEncID==0, keyed by langID + 15: "mac_iceland", + 17: "mac_turkish", + 18: "mac_croatian", + 24: "mac_latin2", + 25: "mac_latin2", + 26: "mac_latin2", + 27: "mac_latin2", + 28: "mac_latin2", + 36: "mac_latin2", + 37: "mac_romanian", + 38: "mac_latin2", + 39: "mac_latin2", + 40: "mac_latin2", + Ellipsis: 'mac_roman', # Other + }, + 1: 'x_mac_japanese_ttx', + 2: 'x_mac_trad_chinese_ttx', + 3: 'x_mac_korean_ttx', + 6: 'mac_greek', + 7: 'mac_cyrillic', + 25: 'x_mac_simp_chinese_ttx', + 29: 'mac_latin2', + 35: 'mac_turkish', + 37: 'mac_iceland', + }, + 2: { # ISO + 0: 'ascii', + 1: 'utf_16_be', + 2: 'latin1', + }, + 3: { # Microsoft + 0: 'utf_16_be', + 1: 'utf_16_be', + 2: 'shift_jis', + 3: 'gb2312', + 4: 'big5', + 5: 'euc_kr', + 6: 'johab', + 10: 'utf_16_be', + }, +} + +def getEncoding(platformID, platEncID, langID, default=None): + """Returns the Python encoding name for OpenType platformID/encodingID/langID + triplet. If encoding for these values is not known, by default None is + returned. That can be overriden by passing a value to the default argument. + """ + encoding = _encodingMap.get(platformID, {}).get(platEncID, default) + if isinstance(encoding, dict): + encoding = encoding.get(langID, encoding[Ellipsis]) + return encoding diff --git a/Lib/fontTools/misc/filenames.py b/Lib/fontTools/misc/filenames.py new file mode 100644 index 0000000..6cf02e3 --- /dev/null +++ b/Lib/fontTools/misc/filenames.py @@ -0,0 +1,224 @@ +""" +User name to file name conversion based on the UFO 3 spec: +http://unifiedfontobject.org/versions/ufo3/conventions/ + +The code was copied from: +https://github.com/unified-font-object/ufoLib/blob/8747da7/Lib/ufoLib/filenames.py + +Author: Tal Leming +Copyright (c) 2005-2016, The RoboFab Developers: + Erik van Blokland + Tal Leming + Just van Rossum +""" +from __future__ import unicode_literals +from fontTools.misc.py23 import basestring, unicode + + +illegalCharacters = "\" * + / : < > ? [ \ ] | \0".split(" ") +illegalCharacters += [chr(i) for i in range(1, 32)] +illegalCharacters += [chr(0x7F)] +reservedFileNames = "CON PRN AUX CLOCK$ NUL A:-Z: COM1".lower().split(" ") +reservedFileNames += "LPT1 LPT2 LPT3 COM2 COM3 COM4".lower().split(" ") +maxFileNameLength = 255 + + +class NameTranslationError(Exception): + pass + + +def userNameToFileName(userName, existing=[], prefix="", suffix=""): + """ + existing should be a case-insensitive list + of all existing file names. + + >>> userNameToFileName("a") == "a" + True + >>> userNameToFileName("A") == "A_" + True + >>> userNameToFileName("AE") == "A_E_" + True + >>> userNameToFileName("Ae") == "A_e" + True + >>> userNameToFileName("ae") == "ae" + True + >>> userNameToFileName("aE") == "aE_" + True + >>> userNameToFileName("a.alt") == "a.alt" + True + >>> userNameToFileName("A.alt") == "A_.alt" + True + >>> userNameToFileName("A.Alt") == "A_.A_lt" + True + >>> userNameToFileName("A.aLt") == "A_.aL_t" + True + >>> userNameToFileName(u"A.alT") == "A_.alT_" + True + >>> userNameToFileName("T_H") == "T__H_" + True + >>> userNameToFileName("T_h") == "T__h" + True + >>> userNameToFileName("t_h") == "t_h" + True + >>> userNameToFileName("F_F_I") == "F__F__I_" + True + >>> userNameToFileName("f_f_i") == "f_f_i" + True + >>> userNameToFileName("Aacute_V.swash") == "A_acute_V_.swash" + True + >>> userNameToFileName(".notdef") == "_notdef" + True + >>> userNameToFileName("con") == "_con" + True + >>> userNameToFileName("CON") == "C_O_N_" + True + >>> userNameToFileName("con.alt") == "_con.alt" + True + >>> userNameToFileName("alt.con") == "alt._con" + True + """ + # the incoming name must be a unicode string + if not isinstance(userName, unicode): + raise ValueError("The value for userName must be a unicode string.") + # establish the prefix and suffix lengths + prefixLength = len(prefix) + suffixLength = len(suffix) + # replace an initial period with an _ + # if no prefix is to be added + if not prefix and userName[0] == ".": + userName = "_" + userName[1:] + # filter the user name + filteredUserName = [] + for character in userName: + # replace illegal characters with _ + if character in illegalCharacters: + character = "_" + # add _ to all non-lower characters + elif character != character.lower(): + character += "_" + filteredUserName.append(character) + userName = "".join(filteredUserName) + # clip to 255 + sliceLength = maxFileNameLength - prefixLength - suffixLength + userName = userName[:sliceLength] + # test for illegal files names + parts = [] + for part in userName.split("."): + if part.lower() in reservedFileNames: + part = "_" + part + parts.append(part) + userName = ".".join(parts) + # test for clash + fullName = prefix + userName + suffix + if fullName.lower() in existing: + fullName = handleClash1(userName, existing, prefix, suffix) + # finished + return fullName + +def handleClash1(userName, existing=[], prefix="", suffix=""): + """ + existing should be a case-insensitive list + of all existing file names. + + >>> prefix = ("0" * 5) + "." + >>> suffix = "." + ("0" * 10) + >>> existing = ["a" * 5] + + >>> e = list(existing) + >>> handleClash1(userName="A" * 5, existing=e, + ... prefix=prefix, suffix=suffix) == ( + ... '00000.AAAAA000000000000001.0000000000') + True + + >>> e = list(existing) + >>> e.append(prefix + "aaaaa" + "1".zfill(15) + suffix) + >>> handleClash1(userName="A" * 5, existing=e, + ... prefix=prefix, suffix=suffix) == ( + ... '00000.AAAAA000000000000002.0000000000') + True + + >>> e = list(existing) + >>> e.append(prefix + "AAAAA" + "2".zfill(15) + suffix) + >>> handleClash1(userName="A" * 5, existing=e, + ... prefix=prefix, suffix=suffix) == ( + ... '00000.AAAAA000000000000001.0000000000') + True + """ + # if the prefix length + user name length + suffix length + 15 is at + # or past the maximum length, silce 15 characters off of the user name + prefixLength = len(prefix) + suffixLength = len(suffix) + if prefixLength + len(userName) + suffixLength + 15 > maxFileNameLength: + l = (prefixLength + len(userName) + suffixLength + 15) + sliceLength = maxFileNameLength - l + userName = userName[:sliceLength] + finalName = None + # try to add numbers to create a unique name + counter = 1 + while finalName is None: + name = userName + str(counter).zfill(15) + fullName = prefix + name + suffix + if fullName.lower() not in existing: + finalName = fullName + break + else: + counter += 1 + if counter >= 999999999999999: + break + # if there is a clash, go to the next fallback + if finalName is None: + finalName = handleClash2(existing, prefix, suffix) + # finished + return finalName + +def handleClash2(existing=[], prefix="", suffix=""): + """ + existing should be a case-insensitive list + of all existing file names. + + >>> prefix = ("0" * 5) + "." + >>> suffix = "." + ("0" * 10) + >>> existing = [prefix + str(i) + suffix for i in range(100)] + + >>> e = list(existing) + >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( + ... '00000.100.0000000000') + True + + >>> e = list(existing) + >>> e.remove(prefix + "1" + suffix) + >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( + ... '00000.1.0000000000') + True + + >>> e = list(existing) + >>> e.remove(prefix + "2" + suffix) + >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( + ... '00000.2.0000000000') + True + """ + # calculate the longest possible string + maxLength = maxFileNameLength - len(prefix) - len(suffix) + maxValue = int("9" * maxLength) + # try to find a number + finalName = None + counter = 1 + while finalName is None: + fullName = prefix + str(counter) + suffix + if fullName.lower() not in existing: + finalName = fullName + break + else: + counter += 1 + if counter >= maxValue: + break + # raise an error if nothing has been found + if finalName is None: + raise NameTranslationError("No unique name could be found.") + # finished + return finalName + +if __name__ == "__main__": + import doctest + import sys + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/misc/fixedTools.py b/Lib/fontTools/misc/fixedTools.py index 59c55dd..c5119ab 100644 --- a/Lib/fontTools/misc/fixedTools.py +++ b/Lib/fontTools/misc/fixedTools.py @@ -3,63 +3,93 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * +import math +import logging + +log = logging.getLogger(__name__) __all__ = [ - "fixedToFloat", - "floatToFixed", + "otRound", + "fixedToFloat", + "floatToFixed", + "floatToFixedToFloat", + "ensureVersionIsLong", + "versionToFixed", ] + +def otRound(value): + """Round float value to nearest integer towards +Infinity. + For fractional values of 0.5 and higher, take the next higher integer; + for other fractional values, truncate. + + https://docs.microsoft.com/en-us/typography/opentype/spec/otvaroverview + https://github.com/fonttools/fonttools/issues/1248#issuecomment-383198166 + """ + return int(math.floor(value + 0.5)) + + def fixedToFloat(value, precisionBits): """Converts a fixed-point number to a float, choosing the float that has the shortest decimal reprentation. Eg. to convert a fixed number in a 2.14 format, use precisionBits=14. This is pretty slow compared to a simple division. Use sporadically. - - >>> fixedToFloat(13107, 14) - 0.8 - >>> fixedToFloat(0, 14) - 0.0 - >>> fixedToFloat(0x4000, 14) - 1.0 - """ + precisionBits is only supported up to 16. + """ if not value: return 0.0 scale = 1 << precisionBits value /= scale eps = .5 / scale - digits = (precisionBits + 2) // 3 - fmt = "%%.%df" % digits - lo = fmt % (value - eps) - hi = fmt % (value + eps) - out = [] - length = min(len(lo), len(hi)) - for i in range(length): + lo = value - eps + hi = value + eps + # If the range of valid choices spans an integer, return the integer. + if int(lo) != int(hi): + return float(round(value)) + fmt = "%.8f" + lo = fmt % lo + hi = fmt % hi + assert len(lo) == len(hi) and lo != hi + for i in range(len(lo)): if lo[i] != hi[i]: - break; - out.append(lo[i]) - outlen = len(out) - if outlen < length: - out.append(max(lo[outlen], hi[outlen])) - return float(strjoin(out)) + break + period = lo.find('.') + assert period < i + fmt = "%%.%df" % (i - period) + value = fmt % value + return float(value) def floatToFixed(value, precisionBits): """Converts a float to a fixed-point number given the number of - precisionBits. Ie. int(round(value * (1<>> floatToFixed(0.8, 14) - 13107 - >>> floatToFixed(1.0, 14) - 16384 - >>> floatToFixed(1, 14) - 16384 - >>> floatToFixed(0, 14) - 0 +def floatToFixedToFloat(value, precisionBits): + """Converts a float to a fixed-point number given the number of + precisionBits, round it, then convert it back to float again. + Ie. round(value * (1<>> import sys + >>> handler = logging.StreamHandler(sys.stdout) + >>> formatter = LevelFormatter( + ... fmt={ + ... '*': '[%(levelname)s] %(message)s', + ... 'DEBUG': '%(name)s [%(levelname)s] %(message)s', + ... 'INFO': '%(message)s', + ... }) + >>> handler.setFormatter(formatter) + >>> log = logging.getLogger('test') + >>> log.setLevel(logging.DEBUG) + >>> log.addHandler(handler) + >>> log.debug('this uses a custom format string') + test [DEBUG] this uses a custom format string + >>> log.info('this also uses a custom format string') + this also uses a custom format string + >>> log.warning("this one uses the default format string") + [WARNING] this one uses the default format string + """ + + def __init__(self, fmt=None, datefmt=None, style="%"): + if style != '%': + raise ValueError( + "only '%' percent style is supported in both python 2 and 3") + if fmt is None: + fmt = DEFAULT_FORMATS + if isinstance(fmt, basestring): + default_format = fmt + custom_formats = {} + elif isinstance(fmt, collections.Mapping): + custom_formats = dict(fmt) + default_format = custom_formats.pop("*", None) + else: + raise TypeError('fmt must be a str or a dict of str: %r' % fmt) + super(LevelFormatter, self).__init__(default_format, datefmt) + self.default_format = self._fmt + self.custom_formats = {} + for level, fmt in custom_formats.items(): + level = logging._checkLevel(level) + self.custom_formats[level] = fmt + + def format(self, record): + if self.custom_formats: + fmt = self.custom_formats.get(record.levelno, self.default_format) + if self._fmt != fmt: + self._fmt = fmt + # for python >= 3.2, _style needs to be set if _fmt changes + if PercentStyle: + self._style = PercentStyle(fmt) + return super(LevelFormatter, self).format(record) + + +def configLogger(**kwargs): + """ Do basic configuration for the logging system. This is more or less + the same as logging.basicConfig with some additional options and defaults. + + The default behaviour is to create a StreamHandler which writes to + sys.stderr, set a formatter using the DEFAULT_FORMATS strings, and add + the handler to the top-level library logger ("fontTools"). + + A number of optional keyword arguments may be specified, which can alter + the default behaviour. + + logger Specifies the logger name or a Logger instance to be configured. + (it defaults to "fontTools" logger). Unlike basicConfig, this + function can be called multiple times to reconfigure a logger. + If the logger or any of its children already exists before the + call is made, they will be reset before the new configuration + is applied. + filename Specifies that a FileHandler be created, using the specified + filename, rather than a StreamHandler. + filemode Specifies the mode to open the file, if filename is specified + (if filemode is unspecified, it defaults to 'a'). + format Use the specified format string for the handler. This argument + also accepts a dictionary of format strings keyed by level name, + to allow customising the records appearance for specific levels. + The special '*' key is for 'any other' level. + datefmt Use the specified date/time format. + level Set the logger level to the specified level. + stream Use the specified stream to initialize the StreamHandler. Note + that this argument is incompatible with 'filename' - if both + are present, 'stream' is ignored. + handlers If specified, this should be an iterable of already created + handlers, which will be added to the logger. Any handler + in the list which does not have a formatter assigned will be + assigned the formatter created in this function. + filters If specified, this should be an iterable of already created + filters, which will be added to the handler(s), if the latter + do(es) not already have filters assigned. + propagate All loggers have a "propagate" attribute initially set to True, + which determines whether to continue searching for handlers up + the logging hierarchy. By default, this arguments sets the + "propagate" attribute to False. + """ + # using kwargs to enforce keyword-only arguments in py2. + handlers = kwargs.pop("handlers", None) + if handlers is None: + if "stream" in kwargs and "filename" in kwargs: + raise ValueError("'stream' and 'filename' should not be " + "specified together") + else: + if "stream" in kwargs or "filename" in kwargs: + raise ValueError("'stream' or 'filename' should not be " + "specified together with 'handlers'") + if handlers is None: + filename = kwargs.pop("filename", None) + mode = kwargs.pop("filemode", 'a') + if filename: + h = logging.FileHandler(filename, mode) + else: + stream = kwargs.pop("stream", None) + h = logging.StreamHandler(stream) + handlers = [h] + # By default, the top-level library logger is configured. + logger = kwargs.pop("logger", "fontTools") + if not logger or isinstance(logger, basestring): + # empty "" or None means the 'root' logger + logger = logging.getLogger(logger) + # before (re)configuring, reset named logger and its children (if exist) + _resetExistingLoggers(parent=logger.name) + # use DEFAULT_FORMATS if 'format' is None + fs = kwargs.pop("format", None) + dfs = kwargs.pop("datefmt", None) + # XXX: '%' is the only format style supported on both py2 and 3 + style = kwargs.pop("style", '%') + fmt = LevelFormatter(fs, dfs, style) + filters = kwargs.pop("filters", []) + for h in handlers: + if h.formatter is None: + h.setFormatter(fmt) + if not h.filters: + for f in filters: + h.addFilter(f) + logger.addHandler(h) + if logger.name != "root": + # stop searching up the hierarchy for handlers + logger.propagate = kwargs.pop("propagate", False) + # set a custom severity level + level = kwargs.pop("level", None) + if level is not None: + logger.setLevel(level) + if kwargs: + keys = ', '.join(kwargs.keys()) + raise ValueError('Unrecognised argument(s): %s' % keys) + + +def _resetExistingLoggers(parent="root"): + """ Reset the logger named 'parent' and all its children to their initial + state, if they already exist in the current configuration. + """ + root = logging.root + # get sorted list of all existing loggers + existing = sorted(root.manager.loggerDict.keys()) + if parent == "root": + # all the existing loggers are children of 'root' + loggers_to_reset = [parent] + existing + elif parent not in existing: + # nothing to do + return + elif parent in existing: + loggers_to_reset = [parent] + # collect children, starting with the entry after parent name + i = existing.index(parent) + 1 + prefixed = parent + "." + pflen = len(prefixed) + num_existing = len(existing) + while i < num_existing: + if existing[i][:pflen] == prefixed: + loggers_to_reset.append(existing[i]) + i += 1 + for name in loggers_to_reset: + if name == "root": + root.setLevel(logging.WARNING) + for h in root.handlers[:]: + root.removeHandler(h) + for f in root.filters[:]: + root.removeFilters(f) + root.disabled = False + else: + logger = root.manager.loggerDict[name] + logger.level = logging.NOTSET + logger.handlers = [] + logger.filters = [] + logger.propagate = True + logger.disabled = False + + +class Timer(object): + """ Keeps track of overall time and split/lap times. + + >>> import time + >>> timer = Timer() + >>> time.sleep(0.01) + >>> print("First lap:", timer.split()) + First lap: ... + >>> time.sleep(0.02) + >>> print("Second lap:", timer.split()) + Second lap: ... + >>> print("Overall time:", timer.time()) + Overall time: ... + + Can be used as a context manager inside with-statements. + + >>> with Timer() as t: + ... time.sleep(0.01) + >>> print("%0.3f seconds" % t.elapsed) + 0... seconds + + If initialised with a logger, it can log the elapsed time automatically + upon exiting the with-statement. + + >>> import logging + >>> log = logging.getLogger("fontTools") + >>> configLogger(level="DEBUG", format="%(message)s", stream=sys.stdout) + >>> with Timer(log, 'do something'): + ... time.sleep(0.01) + Took ... to do something + + The same Timer instance, holding a reference to a logger, can be reused + in multiple with-statements, optionally with different messages or levels. + + >>> timer = Timer(log) + >>> with timer(): + ... time.sleep(0.01) + elapsed time: ...s + >>> with timer('redo it', level=logging.INFO): + ... time.sleep(0.02) + Took ... to redo it + + It can also be used as a function decorator to log the time elapsed to run + the decorated function. + + >>> @timer() + ... def test1(): + ... time.sleep(0.01) + >>> @timer('run test 2', level=logging.INFO) + ... def test2(): + ... time.sleep(0.02) + >>> test1() + Took ... to run 'test1' + >>> test2() + Took ... to run test 2 + """ + + # timeit.default_timer choses the most accurate clock for each platform + _time = timeit.default_timer + default_msg = "elapsed time: %(time).3fs" + default_format = "Took %(time).3fs to %(msg)s" + + def __init__(self, logger=None, msg=None, level=None, start=None): + self.reset(start) + if logger is None: + for arg in ('msg', 'level'): + if locals().get(arg) is not None: + raise ValueError( + "'%s' can't be specified without a 'logger'" % arg) + self.logger = logger + self.level = level if level is not None else TIME_LEVEL + self.msg = msg + + def reset(self, start=None): + """ Reset timer to 'start_time' or the current time. """ + if start is None: + self.start = self._time() + else: + self.start = start + self.last = self.start + self.elapsed = 0.0 + + def time(self): + """ Return the overall time (in seconds) since the timer started. """ + return self._time() - self.start + + def split(self): + """ Split and return the lap time (in seconds) in between splits. """ + current = self._time() + self.elapsed = current - self.last + self.last = current + return self.elapsed + + def formatTime(self, msg, time): + """ Format 'time' value in 'msg' and return formatted string. + If 'msg' contains a '%(time)' format string, try to use that. + Otherwise, use the predefined 'default_format'. + If 'msg' is empty or None, fall back to 'default_msg'. + """ + if not msg: + msg = self.default_msg + if msg.find("%(time)") < 0: + msg = self.default_format % {"msg": msg, "time": time} + else: + try: + msg = msg % {"time": time} + except (KeyError, ValueError): + pass # skip if the format string is malformed + return msg + + def __enter__(self): + """ Start a new lap """ + self.last = self._time() + self.elapsed = 0.0 + return self + + def __exit__(self, exc_type, exc_value, traceback): + """ End the current lap. If timer has a logger, log the time elapsed, + using the format string in self.msg (or the default one). + """ + time = self.split() + if self.logger is None or exc_type: + # if there's no logger attached, or if any exception occurred in + # the with-statement, exit without logging the time + return + message = self.formatTime(self.msg, time) + # Allow log handlers to see the individual parts to facilitate things + # like a server accumulating aggregate stats. + msg_parts = { 'msg': self.msg, 'time': time } + self.logger.log(self.level, message, msg_parts) + + def __call__(self, func_or_msg=None, **kwargs): + """ If the first argument is a function, return a decorator which runs + the wrapped function inside Timer's context manager. + Otherwise, treat the first argument as a 'msg' string and return an updated + Timer instance, referencing the same logger. + A 'level' keyword can also be passed to override self.level. + """ + if isinstance(func_or_msg, collections.Callable): + func = func_or_msg + # use the function name when no explicit 'msg' is provided + if not self.msg: + self.msg = "run '%s'" % func.__name__ + + @wraps(func) + def wrapper(*args, **kwds): + with self: + return func(*args, **kwds) + return wrapper + else: + msg = func_or_msg or kwargs.get("msg") + level = kwargs.get("level", self.level) + return self.__class__(self.logger, msg, level) + + def __float__(self): + return self.elapsed + + def __int__(self): + return int(self.elapsed) + + def __str__(self): + return "%.3f" % self.elapsed + + +class ChannelsFilter(logging.Filter): + """ Filter out records emitted from a list of enabled channel names, + including their children. It works the same as the logging.Filter class, + but allows to specify multiple channel names. + + >>> import sys + >>> handler = logging.StreamHandler(sys.stdout) + >>> handler.setFormatter(logging.Formatter("%(message)s")) + >>> filter = ChannelsFilter("A.B", "C.D") + >>> handler.addFilter(filter) + >>> root = logging.getLogger() + >>> root.addHandler(handler) + >>> root.setLevel(level=logging.DEBUG) + >>> logging.getLogger('A.B').debug('this record passes through') + this record passes through + >>> logging.getLogger('A.B.C').debug('records from children also pass') + records from children also pass + >>> logging.getLogger('C.D').debug('this one as well') + this one as well + >>> logging.getLogger('A.B.').debug('also this one') + also this one + >>> logging.getLogger('A.F').debug('but this one does not!') + >>> logging.getLogger('C.DE').debug('neither this one!') + """ + + def __init__(self, *names): + self.names = names + self.num = len(names) + self.lenghts = {n: len(n) for n in names} + + def filter(self, record): + if self.num == 0: + return True + for name in self.names: + nlen = self.lenghts[name] + if name == record.name: + return True + elif (record.name.find(name, 0, nlen) == 0 + and record.name[nlen] == "."): + return True + return False + + +class CapturingLogHandler(logging.Handler): + def __init__(self, logger, level): + self.records = [] + self.level = logging._checkLevel(level) + if isinstance(logger, basestring): + self.logger = logging.getLogger(logger) + else: + self.logger = logger + + def __enter__(self): + self.original_disabled = self.logger.disabled + self.original_level = self.logger.level + + self.logger.addHandler(self) + self.logger.level = self.level + self.logger.disabled = False + + return self + + def __exit__(self, type, value, traceback): + self.logger.removeHandler(self) + self.logger.level = self.original_level + self.logger.disabled = self.logger.disabled + return self + + def handle(self, record): + self.records.append(record) + + def emit(self, record): + pass + + def createLock(self): + self.lock = None + + def assertRegex(self, regexp): + import re + pattern = re.compile(regexp) + for r in self.records: + if pattern.search(r.getMessage()): + return True + assert 0, "Pattern '%s' not found in logger records" % regexp + + +class LogMixin(object): + """ Mixin class that adds logging functionality to another class. + You can define a new class that subclasses from LogMixin as well as + other base classes through multiple inheritance. + All instances of that class will have a 'log' property that returns + a logging.Logger named after their respective .. + For example: + + >>> class BaseClass(object): + ... pass + >>> class MyClass(LogMixin, BaseClass): + ... pass + >>> a = MyClass() + >>> isinstance(a.log, logging.Logger) + True + >>> print(a.log.name) + fontTools.misc.loggingTools.MyClass + >>> class AnotherClass(MyClass): + ... pass + >>> b = AnotherClass() + >>> isinstance(b.log, logging.Logger) + True + >>> print(b.log.name) + fontTools.misc.loggingTools.AnotherClass + """ + + @property + def log(self): + if not hasattr(self, "_log"): + name = ".".join( + (self.__class__.__module__, self.__class__.__name__) + ) + self._log = logging.getLogger(name) + return self._log + + +def deprecateArgument(name, msg, category=UserWarning): + """ Raise a warning about deprecated function argument 'name'. """ + warnings.warn( + "%r is deprecated; %s" % (name, msg), category=category, stacklevel=3) + + +def deprecateFunction(msg, category=UserWarning): + """ Decorator to raise a warning when a deprecated function is called. """ + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + warnings.warn( + "%r is deprecated; %s" % (func.__name__, msg), + category=category, stacklevel=2) + return func(*args, **kwargs) + return wrapper + return decorator + + +class LastResortLogger(logging.Logger): + """ Adds support for 'lastResort' handler introduced in Python 3.2. + It allows to print messages to sys.stderr even when no explicit handler + was configured. + To enable it, you can do: + + import logging + logging.lastResort = StderrHandler(logging.WARNING) + logging.setLoggerClass(LastResortLogger) + """ + + def callHandlers(self, record): + # this is the same as Python 3.5's logging.Logger.callHandlers + c = self + found = 0 + while c: + for hdlr in c.handlers: + found = found + 1 + if record.levelno >= hdlr.level: + hdlr.handle(record) + if not c.propagate: + c = None # break out + else: + c = c.parent + if found == 0: + if logging.lastResort: + if record.levelno >= logging.lastResort.level: + logging.lastResort.handle(record) + elif ( + logging.raiseExceptions + and not self.manager.emittedNoHandlerWarning + ): + sys.stderr.write( + "No handlers could be found for logger" + ' "%s"\n' % self.name + ) + self.manager.emittedNoHandlerWarning = True + + +class StderrHandler(logging.StreamHandler): + """ This class is like a StreamHandler using sys.stderr, but always uses + whateve sys.stderr is currently set to rather than the value of + sys.stderr at handler construction time. + """ + + def __init__(self, level=logging.NOTSET): + """ + Initialize the handler. + """ + logging.Handler.__init__(self, level) + + @property + def stream(self): + # the try/execept avoids failures during interpreter shutdown, when + # globals are set to None + try: + return sys.stderr + except AttributeError: + return __import__("sys").stderr + + +if __name__ == "__main__": + import doctest + sys.exit(doctest.testmod(optionflags=doctest.ELLIPSIS).failed) diff --git a/Lib/fontTools/misc/macCreatorType.py b/Lib/fontTools/misc/macCreatorType.py index 5f2e18a..2b33e89 100644 --- a/Lib/fontTools/misc/macCreatorType.py +++ b/Lib/fontTools/misc/macCreatorType.py @@ -1,11 +1,15 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * import sys +try: + import xattr +except ImportError: + xattr = None try: import MacOS except ImportError: MacOS = None -from .py23 import * + def _reverseString(s): s = list(s) @@ -14,11 +18,21 @@ def _reverseString(s): def getMacCreatorAndType(path): + if xattr is not None: + try: + finderInfo = xattr.getxattr(path, 'com.apple.FinderInfo') + except (KeyError, IOError): + pass + else: + fileType = Tag(finderInfo[:4]) + fileCreator = Tag(finderInfo[4:8]) + return fileCreator, fileType if MacOS is not None: fileCreator, fileType = MacOS.GetCreatorAndType(path) - if sys.byteorder == "little": + if sys.version_info[:2] < (2, 7) and sys.byteorder == "little": # work around bug in MacOS.GetCreatorAndType() on intel: # http://bugs.python.org/issue1594 + # (fixed with Python 2.7) fileCreator = _reverseString(fileCreator) fileType = _reverseString(fileType) return fileCreator, fileType @@ -27,10 +41,11 @@ def getMacCreatorAndType(path): def setMacCreatorAndType(path, fileCreator, fileType): + if xattr is not None: + from fontTools.misc.textTools import pad + if not all(len(s) == 4 for s in (fileCreator, fileType)): + raise TypeError('arg must be string of 4 chars') + finderInfo = pad(bytesjoin([fileType, fileCreator]), 32) + xattr.setxattr(path, 'com.apple.FinderInfo', finderInfo) if MacOS is not None: - if sys.byteorder == "little": - # work around bug in MacOS.SetCreatorAndType() on intel: - # http://bugs.python.org/issue1594 - fileCreator = _reverseString(fileCreator) - fileType = _reverseString(fileType) MacOS.SetCreatorAndType(path, fileCreator, fileType) diff --git a/Lib/fontTools/misc/macRes.py b/Lib/fontTools/misc/macRes.py new file mode 100644 index 0000000..20bfa71 --- /dev/null +++ b/Lib/fontTools/misc/macRes.py @@ -0,0 +1,233 @@ +""" Tools for reading Mac resource forks. """ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +import struct +from fontTools.misc import sstruct +from collections import OrderedDict +try: + from collections.abc import MutableMapping +except ImportError: + from UserDict import DictMixin as MutableMapping + + +class ResourceError(Exception): + pass + + +class ResourceReader(MutableMapping): + + def __init__(self, fileOrPath): + self._resources = OrderedDict() + if hasattr(fileOrPath, 'read'): + self.file = fileOrPath + else: + try: + # try reading from the resource fork (only works on OS X) + self.file = self.openResourceFork(fileOrPath) + self._readFile() + return + except (ResourceError, IOError): + # if it fails, use the data fork + self.file = self.openDataFork(fileOrPath) + self._readFile() + + @staticmethod + def openResourceFork(path): + with open(path + '/..namedfork/rsrc', 'rb') as resfork: + data = resfork.read() + infile = BytesIO(data) + infile.name = path + return infile + + @staticmethod + def openDataFork(path): + with open(path, 'rb') as datafork: + data = datafork.read() + infile = BytesIO(data) + infile.name = path + return infile + + def _readFile(self): + self._readHeaderAndMap() + self._readTypeList() + + def _read(self, numBytes, offset=None): + if offset is not None: + try: + self.file.seek(offset) + except OverflowError: + raise ResourceError("Failed to seek offset ('offset' is too large)") + if self.file.tell() != offset: + raise ResourceError('Failed to seek offset (reached EOF)') + try: + data = self.file.read(numBytes) + except OverflowError: + raise ResourceError("Cannot read resource ('numBytes' is too large)") + if len(data) != numBytes: + raise ResourceError('Cannot read resource (not enough data)') + return data + + def _readHeaderAndMap(self): + self.file.seek(0) + headerData = self._read(ResourceForkHeaderSize) + sstruct.unpack(ResourceForkHeader, headerData, self) + # seek to resource map, skip reserved + mapOffset = self.mapOffset + 22 + resourceMapData = self._read(ResourceMapHeaderSize, mapOffset) + sstruct.unpack(ResourceMapHeader, resourceMapData, self) + self.absTypeListOffset = self.mapOffset + self.typeListOffset + self.absNameListOffset = self.mapOffset + self.nameListOffset + + def _readTypeList(self): + absTypeListOffset = self.absTypeListOffset + numTypesData = self._read(2, absTypeListOffset) + self.numTypes, = struct.unpack('>H', numTypesData) + absTypeListOffset2 = absTypeListOffset + 2 + for i in range(self.numTypes + 1): + resTypeItemOffset = absTypeListOffset2 + ResourceTypeItemSize * i + resTypeItemData = self._read(ResourceTypeItemSize, resTypeItemOffset) + item = sstruct.unpack(ResourceTypeItem, resTypeItemData) + resType = tostr(item['type'], encoding='mac-roman') + refListOffset = absTypeListOffset + item['refListOffset'] + numRes = item['numRes'] + 1 + resources = self._readReferenceList(resType, refListOffset, numRes) + self._resources[resType] = resources + + def _readReferenceList(self, resType, refListOffset, numRes): + resources = [] + for i in range(numRes): + refOffset = refListOffset + ResourceRefItemSize * i + refData = self._read(ResourceRefItemSize, refOffset) + res = Resource(resType) + res.decompile(refData, self) + resources.append(res) + return resources + + def __getitem__(self, resType): + return self._resources[resType] + + def __delitem__(self, resType): + del self._resources[resType] + + def __setitem__(self, resType, resources): + self._resources[resType] = resources + + def __len__(self): + return len(self._resources) + + def __iter__(self): + return iter(self._resources) + + def keys(self): + return self._resources.keys() + + @property + def types(self): + return list(self._resources.keys()) + + def countResources(self, resType): + """Return the number of resources of a given type.""" + try: + return len(self[resType]) + except KeyError: + return 0 + + def getIndices(self, resType): + numRes = self.countResources(resType) + if numRes: + return list(range(1, numRes+1)) + else: + return [] + + def getNames(self, resType): + """Return list of names of all resources of a given type.""" + return [res.name for res in self.get(resType, []) if res.name is not None] + + def getIndResource(self, resType, index): + """Return resource of given type located at an index ranging from 1 + to the number of resources for that type, or None if not found. + """ + if index < 1: + return None + try: + res = self[resType][index-1] + except (KeyError, IndexError): + return None + return res + + def getNamedResource(self, resType, name): + """Return the named resource of given type, else return None.""" + name = tostr(name, encoding='mac-roman') + for res in self.get(resType, []): + if res.name == name: + return res + return None + + def close(self): + if not self.file.closed: + self.file.close() + + +class Resource(object): + + def __init__(self, resType=None, resData=None, resID=None, resName=None, + resAttr=None): + self.type = resType + self.data = resData + self.id = resID + self.name = resName + self.attr = resAttr + + def decompile(self, refData, reader): + sstruct.unpack(ResourceRefItem, refData, self) + # interpret 3-byte dataOffset as (padded) ULONG to unpack it with struct + self.dataOffset, = struct.unpack('>L', bytesjoin([b"\0", self.dataOffset])) + absDataOffset = reader.dataOffset + self.dataOffset + dataLength, = struct.unpack(">L", reader._read(4, absDataOffset)) + self.data = reader._read(dataLength) + if self.nameOffset == -1: + return + absNameOffset = reader.absNameListOffset + self.nameOffset + nameLength, = struct.unpack('B', reader._read(1, absNameOffset)) + name, = struct.unpack('>%ss' % nameLength, reader._read(nameLength)) + self.name = tostr(name, encoding='mac-roman') + + +ResourceForkHeader = """ + > # big endian + dataOffset: L + mapOffset: L + dataLen: L + mapLen: L +""" + +ResourceForkHeaderSize = sstruct.calcsize(ResourceForkHeader) + +ResourceMapHeader = """ + > # big endian + attr: H + typeListOffset: H + nameListOffset: H +""" + +ResourceMapHeaderSize = sstruct.calcsize(ResourceMapHeader) + +ResourceTypeItem = """ + > # big endian + type: 4s + numRes: H + refListOffset: H +""" + +ResourceTypeItemSize = sstruct.calcsize(ResourceTypeItem) + +ResourceRefItem = """ + > # big endian + id: h + nameOffset: h + attr: B + dataOffset: 3s + reserved: L +""" + +ResourceRefItemSize = sstruct.calcsize(ResourceRefItem) diff --git a/Lib/fontTools/misc/psCharStrings.py b/Lib/fontTools/misc/psCharStrings.py index 6ffdb99..ff78271 100644 --- a/Lib/fontTools/misc/psCharStrings.py +++ b/Lib/fontTools/misc/psCharStrings.py @@ -1,79 +1,97 @@ -"""psCharStrings.py -- module implementing various kinds of CharStrings: +"""psCharStrings.py -- module implementing various kinds of CharStrings: CFF dictionary data and Type1/Type2 CharStrings. """ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import fixedToFloat, otRound +from fontTools.pens.boundsPen import BoundsPen import struct +import logging -DEBUG = 0 +log = logging.getLogger(__name__) + + +def read_operator(self, b0, data, index): + if b0 == 12: + op = (b0, byteord(data[index])) + index = index+1 + else: + op = b0 + try: + operator = self.operators[op] + except KeyError: + return None, index + value = self.handle_operator(operator) + return value, index + +def read_byte(self, b0, data, index): + return b0 - 139, index + +def read_smallInt1(self, b0, data, index): + b1 = byteord(data[index]) + return (b0-247)*256 + b1 + 108, index+1 + +def read_smallInt2(self, b0, data, index): + b1 = byteord(data[index]) + return -(b0-251)*256 - b1 - 108, index+1 + +def read_shortInt(self, b0, data, index): + value, = struct.unpack(">h", data[index:index+2]) + return value, index+2 + +def read_longInt(self, b0, data, index): + value, = struct.unpack(">l", data[index:index+4]) + return value, index+4 + +def read_fixed1616(self, b0, data, index): + value, = struct.unpack(">l", data[index:index+4]) + return fixedToFloat(value, precisionBits=16), index+4 + +def read_reserved(self, b0, data, index): + assert NotImplementedError + return NotImplemented, index + +def read_realNumber(self, b0, data, index): + number = '' + while True: + b = byteord(data[index]) + index = index + 1 + nibble0 = (b & 0xf0) >> 4 + nibble1 = b & 0x0f + if nibble0 == 0xf: + break + number = number + realNibbles[nibble0] + if nibble1 == 0xf: + break + number = number + realNibbles[nibble1] + return float(number), index t1OperandEncoding = [None] * 256 -t1OperandEncoding[0:32] = (32) * ["do_operator"] -t1OperandEncoding[32:247] = (247 - 32) * ["read_byte"] -t1OperandEncoding[247:251] = (251 - 247) * ["read_smallInt1"] -t1OperandEncoding[251:255] = (255 - 251) * ["read_smallInt2"] -t1OperandEncoding[255] = "read_longInt" +t1OperandEncoding[0:32] = (32) * [read_operator] +t1OperandEncoding[32:247] = (247 - 32) * [read_byte] +t1OperandEncoding[247:251] = (251 - 247) * [read_smallInt1] +t1OperandEncoding[251:255] = (255 - 251) * [read_smallInt2] +t1OperandEncoding[255] = read_longInt assert len(t1OperandEncoding) == 256 t2OperandEncoding = t1OperandEncoding[:] -t2OperandEncoding[28] = "read_shortInt" -t2OperandEncoding[255] = "read_fixed1616" +t2OperandEncoding[28] = read_shortInt +t2OperandEncoding[255] = read_fixed1616 cffDictOperandEncoding = t2OperandEncoding[:] -cffDictOperandEncoding[29] = "read_longInt" -cffDictOperandEncoding[30] = "read_realNumber" -cffDictOperandEncoding[255] = "reserved" +cffDictOperandEncoding[29] = read_longInt +cffDictOperandEncoding[30] = read_realNumber +cffDictOperandEncoding[255] = read_reserved -realNibbles = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', +realNibbles = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', 'E', 'E-', None, '-'] -realNibblesDict = {} -for _i in range(len(realNibbles)): - realNibblesDict[realNibbles[_i]] = _i - +realNibblesDict = {v:i for i,v in enumerate(realNibbles)} -class ByteCodeBase(object): - - def read_byte(self, b0, data, index): - return b0 - 139, index - - def read_smallInt1(self, b0, data, index): - b1 = byteord(data[index]) - return (b0-247)*256 + b1 + 108, index+1 - - def read_smallInt2(self, b0, data, index): - b1 = byteord(data[index]) - return -(b0-251)*256 - b1 - 108, index+1 - - def read_shortInt(self, b0, data, index): - value, = struct.unpack(">h", data[index:index+2]) - return value, index+2 - - def read_longInt(self, b0, data, index): - value, = struct.unpack(">l", data[index:index+4]) - return value, index+4 - - def read_fixed1616(self, b0, data, index): - value, = struct.unpack(">l", data[index:index+4]) - return value / 65536, index+4 - - def read_realNumber(self, b0, data, index): - number = '' - while True: - b = byteord(data[index]) - index = index + 1 - nibble0 = (b & 0xf0) >> 4 - nibble1 = b & 0x0f - if nibble0 == 0xf: - break - number = number + realNibbles[nibble0] - if nibble1 == 0xf: - break - number = number + realNibbles[nibble1] - return float(number), index +maxOpStack = 193 def buildOperatorDict(operatorList): @@ -92,63 +110,63 @@ def buildOperatorDict(operatorList): t2Operators = [ -# opcode name - (1, 'hstem'), - (3, 'vstem'), - (4, 'vmoveto'), - (5, 'rlineto'), - (6, 'hlineto'), - (7, 'vlineto'), - (8, 'rrcurveto'), - (10, 'callsubr'), - (11, 'return'), - (14, 'endchar'), - (16, 'blend'), - (18, 'hstemhm'), - (19, 'hintmask'), - (20, 'cntrmask'), - (21, 'rmoveto'), - (22, 'hmoveto'), - (23, 'vstemhm'), - (24, 'rcurveline'), - (25, 'rlinecurve'), - (26, 'vvcurveto'), - (27, 'hhcurveto'), -# (28, 'shortint'), # not really an operator - (29, 'callgsubr'), - (30, 'vhcurveto'), - (31, 'hvcurveto'), - ((12, 0), 'ignore'), # dotsection. Yes, there a few very early OTF/CFF - # fonts with this deprecated operator. Just ignore it. - ((12, 3), 'and'), - ((12, 4), 'or'), - ((12, 5), 'not'), - ((12, 8), 'store'), - ((12, 9), 'abs'), - ((12, 10), 'add'), - ((12, 11), 'sub'), - ((12, 12), 'div'), - ((12, 13), 'load'), - ((12, 14), 'neg'), - ((12, 15), 'eq'), - ((12, 18), 'drop'), - ((12, 20), 'put'), - ((12, 21), 'get'), - ((12, 22), 'ifelse'), - ((12, 23), 'random'), - ((12, 24), 'mul'), - ((12, 26), 'sqrt'), - ((12, 27), 'dup'), - ((12, 28), 'exch'), - ((12, 29), 'index'), - ((12, 30), 'roll'), - ((12, 34), 'hflex'), - ((12, 35), 'flex'), - ((12, 36), 'hflex1'), - ((12, 37), 'flex1'), +# opcode name + (1, 'hstem'), + (3, 'vstem'), + (4, 'vmoveto'), + (5, 'rlineto'), + (6, 'hlineto'), + (7, 'vlineto'), + (8, 'rrcurveto'), + (10, 'callsubr'), + (11, 'return'), + (14, 'endchar'), + (15, 'vsindex'), + (16, 'blend'), + (18, 'hstemhm'), + (19, 'hintmask'), + (20, 'cntrmask'), + (21, 'rmoveto'), + (22, 'hmoveto'), + (23, 'vstemhm'), + (24, 'rcurveline'), + (25, 'rlinecurve'), + (26, 'vvcurveto'), + (27, 'hhcurveto'), +# (28, 'shortint'), # not really an operator + (29, 'callgsubr'), + (30, 'vhcurveto'), + (31, 'hvcurveto'), + ((12, 0), 'ignore'), # dotsection. Yes, there a few very early OTF/CFF + # fonts with this deprecated operator. Just ignore it. + ((12, 3), 'and'), + ((12, 4), 'or'), + ((12, 5), 'not'), + ((12, 8), 'store'), + ((12, 9), 'abs'), + ((12, 10), 'add'), + ((12, 11), 'sub'), + ((12, 12), 'div'), + ((12, 13), 'load'), + ((12, 14), 'neg'), + ((12, 15), 'eq'), + ((12, 18), 'drop'), + ((12, 20), 'put'), + ((12, 21), 'get'), + ((12, 22), 'ifelse'), + ((12, 23), 'random'), + ((12, 24), 'mul'), + ((12, 26), 'sqrt'), + ((12, 27), 'dup'), + ((12, 28), 'exch'), + ((12, 29), 'index'), + ((12, 30), 'roll'), + ((12, 34), 'hflex'), + ((12, 35), 'flex'), + ((12, 36), 'hflex1'), + ((12, 37), 'flex1'), ] - def getIntEncoder(format): if format == "cff": fourByteOp = bytechr(29) @@ -157,7 +175,7 @@ def getIntEncoder(format): else: assert format == "t2" fourByteOp = None - + def encodeInt(value, fourByteOp=fourByteOp, bytechr=bytechr, pack=struct.pack, unpack=struct.unpack): if -107 <= value <= 107: @@ -189,7 +207,7 @@ def getIntEncoder(format): else: code = fourByteOp + pack(">l", value) return code - + return encodeInt @@ -199,7 +217,7 @@ encodeIntT2 = getIntEncoder("t2") def encodeFixed(f, pack=struct.pack): # For T2 only - return b"\xff" + pack(">l", int(round(f * 65536))) + return b"\xff" + pack(">l", otRound(f * 65536)) def encodeFloat(f): # For CFF only, used in cffLib @@ -228,275 +246,39 @@ def encodeFloat(f): class CharStringCompileError(Exception): pass -class T2CharString(ByteCodeBase): - - operandEncoding = t2OperandEncoding - operators, opcodes = buildOperatorDict(t2Operators) - - def __init__(self, bytecode=None, program=None, private=None, globalSubrs=None): - if program is None: - program = [] - self.bytecode = bytecode - self.program = program - self.private = private - self.globalSubrs = globalSubrs if globalSubrs is not None else [] - - def __repr__(self): - if self.bytecode is None: - return "<%s (source) at %x>" % (self.__class__.__name__, id(self)) - else: - return "<%s (bytecode) at %x>" % (self.__class__.__name__, id(self)) - - def getIntEncoder(self): - return encodeIntT2 - - def getFixedEncoder(self): - return encodeFixed - - def decompile(self): - if not self.needsDecompilation(): - return - subrs = getattr(self.private, "Subrs", []) - decompiler = SimpleT2Decompiler(subrs, self.globalSubrs) - decompiler.execute(self) - - def draw(self, pen): - subrs = getattr(self.private, "Subrs", []) - extractor = T2OutlineExtractor(pen, subrs, self.globalSubrs, - self.private.nominalWidthX, self.private.defaultWidthX) - extractor.execute(self) - self.width = extractor.width - - def compile(self): - if self.bytecode is not None: - return - assert self.program, "illegal CharString: decompiled to empty program" - assert self.program[-1] in ("endchar", "return", "callsubr", "callgsubr", - "seac"), "illegal CharString" - bytecode = [] - opcodes = self.opcodes - program = self.program - encodeInt = self.getIntEncoder() - encodeFixed = self.getFixedEncoder() - i = 0 - end = len(program) - while i < end: - token = program[i] - i = i + 1 - tp = type(token) - if issubclass(tp, basestring): - try: - bytecode.extend(bytechr(b) for b in opcodes[token]) - except KeyError: - raise CharStringCompileError("illegal operator: %s" % token) - if token in ('hintmask', 'cntrmask'): - bytecode.append(program[i]) # hint mask - i = i + 1 - elif tp == int: - bytecode.append(encodeInt(token)) - elif tp == float: - bytecode.append(encodeFixed(token)) - else: - assert 0, "unsupported type: %s" % tp - try: - bytecode = bytesjoin(bytecode) - except TypeError: - print(bytecode) - raise - self.setBytecode(bytecode) - - def needsDecompilation(self): - return self.bytecode is not None - - def setProgram(self, program): - self.program = program - self.bytecode = None - - def setBytecode(self, bytecode): - self.bytecode = bytecode - self.program = None - - def getToken(self, index, - len=len, byteord=byteord, getattr=getattr, type=type, StringType=str): - if self.bytecode is not None: - if index >= len(self.bytecode): - return None, 0, 0 - b0 = byteord(self.bytecode[index]) - index = index + 1 - code = self.operandEncoding[b0] - handler = getattr(self, code) - token, index = handler(b0, self.bytecode, index) - else: - if index >= len(self.program): - return None, 0, 0 - token = self.program[index] - index = index + 1 - isOperator = isinstance(token, StringType) - return token, isOperator, index - - def getBytes(self, index, nBytes): - if self.bytecode is not None: - newIndex = index + nBytes - bytes = self.bytecode[index:newIndex] - index = newIndex - else: - bytes = self.program[index] - index = index + 1 - assert len(bytes) == nBytes - return bytes, index - - def do_operator(self, b0, data, index): - if b0 == 12: - op = (b0, byteord(data[index])) - index = index+1 - else: - op = b0 - operator = self.operators[op] - return operator, index - - def toXML(self, xmlWriter): - from fontTools.misc.textTools import num2binary - if self.bytecode is not None: - xmlWriter.dumphex(self.bytecode) - else: - index = 0 - args = [] - while True: - token, isOperator, index = self.getToken(index) - if token is None: - break - if isOperator: - args = [str(arg) for arg in args] - if token in ('hintmask', 'cntrmask'): - hintMask, isOperator, index = self.getToken(index) - bits = [] - for byte in hintMask: - bits.append(num2binary(byteord(byte), 8)) - hintMask = strjoin(bits) - line = ' '.join(args + [token, hintMask]) - else: - line = ' '.join(args + [token]) - xmlWriter.write(line) - xmlWriter.newline() - args = [] - else: - args.append(token) - - def fromXML(self, name, attrs, content): - from fontTools.misc.textTools import binary2num, readHex - if attrs.get("raw"): - self.setBytecode(readHex(content)) - return - content = strjoin(content) - content = content.split() - program = [] - end = len(content) - i = 0 - while i < end: - token = content[i] - i = i + 1 - try: - token = int(token) - except ValueError: - try: - token = float(token) - except ValueError: - program.append(token) - if token in ('hintmask', 'cntrmask'): - mask = content[i] - maskBytes = b"" - for j in range(0, len(mask), 8): - maskBytes = maskBytes + bytechr(binary2num(mask[j:j+8])) - program.append(maskBytes) - i = i + 1 - else: - program.append(token) - else: - program.append(token) - self.setProgram(program) - - -t1Operators = [ -# opcode name - (1, 'hstem'), - (3, 'vstem'), - (4, 'vmoveto'), - (5, 'rlineto'), - (6, 'hlineto'), - (7, 'vlineto'), - (8, 'rrcurveto'), - (9, 'closepath'), - (10, 'callsubr'), - (11, 'return'), - (13, 'hsbw'), - (14, 'endchar'), - (21, 'rmoveto'), - (22, 'hmoveto'), - (30, 'vhcurveto'), - (31, 'hvcurveto'), - ((12, 0), 'dotsection'), - ((12, 1), 'vstem3'), - ((12, 2), 'hstem3'), - ((12, 6), 'seac'), - ((12, 7), 'sbw'), - ((12, 12), 'div'), - ((12, 16), 'callothersubr'), - ((12, 17), 'pop'), - ((12, 33), 'setcurrentpoint'), -] - -class T1CharString(T2CharString): - - operandEncoding = t1OperandEncoding - operators, opcodes = buildOperatorDict(t1Operators) - - def __init__(self, bytecode=None, program=None, subrs=None): - if program is None: - program = [] - self.bytecode = bytecode - self.program = program - self.subrs = subrs - - def getIntEncoder(self): - return encodeIntT1 - - def getFixedEncoder(self): - def encodeFixed(value): - raise TypeError("Type 1 charstrings don't support floating point operands") - - def decompile(self): - if self.bytecode is None: - return - program = [] - index = 0 - while True: - token, isOperator, index = self.getToken(index) - if token is None: - break - program.append(token) - self.setProgram(program) - - def draw(self, pen): - extractor = T1OutlineExtractor(pen, self.subrs) - extractor.execute(self) - self.width = extractor.width - - class SimpleT2Decompiler(object): - - def __init__(self, localSubrs, globalSubrs): + + def __init__(self, localSubrs, globalSubrs, private=None): self.localSubrs = localSubrs self.localBias = calcSubrBias(localSubrs) self.globalSubrs = globalSubrs self.globalBias = calcSubrBias(globalSubrs) + self.private = private self.reset() - + def reset(self): self.callingStack = [] self.operandStack = [] self.hintCount = 0 self.hintMaskBytes = 0 - + self.numRegions = 0 + + def check_program(self, program): + if not hasattr(self, 'private') or self.private is None: + # Type 1 charstrings don't have self.private. + # Type2 CFF charstrings may have self.private == None. + # In both cases, they are not CFF2 charstrings + isCFF2 = False + else: + isCFF2 = self.private._isCFF2 + if isCFF2: + if program: + assert program[-1] not in ("seac",), "illegal CharString Terminator" + else: + assert program, "illegal CharString: decompiled to empty program" + assert program[-1] in ("endchar", "return", "callsubr", "callgsubr", + "seac"), "illegal CharString" + def execute(self, charString): self.callingStack.append(charString) needsDecompilation = charString.needsDecompilation() @@ -514,8 +296,8 @@ class SimpleT2Decompiler(object): pushToProgram(token) if isOperator: handlerName = "op_" + token - if hasattr(self, handlerName): - handler = getattr(self, handlerName) + handler = getattr(self, handlerName, None) + if handler is not None: rv = handler(index) if rv: hintMaskBytes, index = rv @@ -525,29 +307,27 @@ class SimpleT2Decompiler(object): else: pushToStack(token) if needsDecompilation: - assert program, "illegal CharString: decompiled to empty program" - assert program[-1] in ("endchar", "return", "callsubr", "callgsubr", - "seac"), "illegal CharString" + self.check_program(program) charString.setProgram(program) del self.callingStack[-1] - + def pop(self): value = self.operandStack[-1] del self.operandStack[-1] return value - + def popall(self): stack = self.operandStack[:] self.operandStack[:] = [] return stack - + def push(self, value): self.operandStack.append(value) - + def op_return(self, index): if self.operandStack: pass - + def op_endchar(self, index): pass @@ -558,12 +338,12 @@ class SimpleT2Decompiler(object): subrIndex = self.pop() subr = self.localSubrs[subrIndex+self.localBias] self.execute(subr) - + def op_callgsubr(self, index): subrIndex = self.pop() subr = self.globalSubrs[subrIndex+self.globalBias] self.execute(subr) - + def op_hstem(self, index): self.countHints() def op_vstem(self, index): @@ -572,16 +352,16 @@ class SimpleT2Decompiler(object): self.countHints() def op_vstemhm(self, index): self.countHints() - + def op_hintmask(self, index): if not self.hintMaskBytes: self.countHints() self.hintMaskBytes = (self.hintCount + 7) // 8 hintMaskBytes, index = self.callingStack[-1].getBytes(index, self.hintMaskBytes) return hintMaskBytes, index - + op_cntrmask = op_hintmask - + def countHints(self): args = self.popall() self.hintCount = self.hintCount + len(args) // 2 @@ -632,28 +412,109 @@ class SimpleT2Decompiler(object): def op_roll(self, index): raise NotImplementedError -class T2OutlineExtractor(SimpleT2Decompiler): - - def __init__(self, pen, localSubrs, globalSubrs, nominalWidthX, defaultWidthX): + # TODO(behdad): move to T2OutlineExtractor and add a 'setVariation' + # method that takes VarStoreData and a location + def op_blend(self, index): + if self.numRegions == 0: + self.numRegions = self.private.getNumRegions() + numBlends = self.pop() + numOps = numBlends * (self.numRegions + 1) + blendArgs = self.operandStack[-numOps:] + del self.operandStack[:-(numOps-numBlends)] # Leave the default operands on the stack. + + def op_vsindex(self, index): + vi = self.pop() + self.numRegions = self.private.getNumRegions(vi) + + +t1Operators = [ +# opcode name + (1, 'hstem'), + (3, 'vstem'), + (4, 'vmoveto'), + (5, 'rlineto'), + (6, 'hlineto'), + (7, 'vlineto'), + (8, 'rrcurveto'), + (9, 'closepath'), + (10, 'callsubr'), + (11, 'return'), + (13, 'hsbw'), + (14, 'endchar'), + (21, 'rmoveto'), + (22, 'hmoveto'), + (30, 'vhcurveto'), + (31, 'hvcurveto'), + ((12, 0), 'dotsection'), + ((12, 1), 'vstem3'), + ((12, 2), 'hstem3'), + ((12, 6), 'seac'), + ((12, 7), 'sbw'), + ((12, 12), 'div'), + ((12, 16), 'callothersubr'), + ((12, 17), 'pop'), + ((12, 33), 'setcurrentpoint'), +] + + +class T2WidthExtractor(SimpleT2Decompiler): + + def __init__(self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX): SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs) - self.pen = pen self.nominalWidthX = nominalWidthX self.defaultWidthX = defaultWidthX - + def reset(self): SimpleT2Decompiler.reset(self) - self.hints = [] self.gotWidth = 0 self.width = 0 + + def popallWidth(self, evenOdd=0): + args = self.popall() + if not self.gotWidth: + if evenOdd ^ (len(args) % 2): + self.width = self.nominalWidthX + args[0] + args = args[1:] + else: + self.width = self.defaultWidthX + self.gotWidth = 1 + return args + + def countHints(self): + args = self.popallWidth() + self.hintCount = self.hintCount + len(args) // 2 + + def op_rmoveto(self, index): + self.popallWidth() + + def op_hmoveto(self, index): + self.popallWidth(1) + + def op_vmoveto(self, index): + self.popallWidth(1) + + def op_endchar(self, index): + self.popallWidth() + + +class T2OutlineExtractor(T2WidthExtractor): + + def __init__(self, pen, localSubrs, globalSubrs, nominalWidthX, defaultWidthX): + T2WidthExtractor.__init__( + self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX) + self.pen = pen + + def reset(self): + T2WidthExtractor.reset(self) self.currentPoint = (0, 0) self.sawMoveTo = 0 - + def _nextPoint(self, point): x, y = self.currentPoint point = x + point[0], y + point[1] self.currentPoint = point return point - + def rMoveTo(self, point): self.pen.moveTo(self._nextPoint(point)) self.sawMoveTo = 1 @@ -668,32 +529,17 @@ class T2OutlineExtractor(SimpleT2Decompiler): self.rMoveTo((0, 0)) nextPoint = self._nextPoint self.pen.curveTo(nextPoint(pt1), nextPoint(pt2), nextPoint(pt3)) - + def closePath(self): if self.sawMoveTo: self.pen.closePath() self.sawMoveTo = 0 - + def endPath(self): # In T2 there are no open paths, so always do a closePath when # finishing a sub path. self.closePath() - def popallWidth(self, evenOdd=0): - args = self.popall() - if not self.gotWidth: - if evenOdd ^ (len(args) % 2): - self.width = self.nominalWidthX + args[0] - args = args[1:] - else: - self.width = self.defaultWidthX - self.gotWidth = 1 - return args - - def countHints(self): - args = self.popallWidth() - self.hintCount = self.hintCount + len(args) // 2 - # # hint operators # @@ -709,7 +555,7 @@ class T2OutlineExtractor(SimpleT2Decompiler): # self.countHints() #def op_cntrmask(self, index): # self.countHints() - + # # path constructors, moveto # @@ -734,7 +580,7 @@ class T2OutlineExtractor(SimpleT2Decompiler): self.pen.addComponent(baseGlyph, (1, 0, 0, 1, 0, 0)) accentGlyph = StandardEncoding[achar] self.pen.addComponent(accentGlyph, (1, 0, 0, 1, adx, ady)) - + # # path constructors, lines # @@ -743,12 +589,12 @@ class T2OutlineExtractor(SimpleT2Decompiler): for i in range(0, len(args), 2): point = args[i:i+2] self.rLineTo(point) - + def op_hlineto(self, index): self.alternatingLineto(1) def op_vlineto(self, index): self.alternatingLineto(0) - + # # path constructors, curves # @@ -758,7 +604,7 @@ class T2OutlineExtractor(SimpleT2Decompiler): for i in range(0, len(args), 6): dxa, dya, dxb, dyb, dxc, dyc, = args[i:i+6] self.rCurveTo((dxa, dya), (dxb, dyb), (dxc, dyc)) - + def op_rcurveline(self, index): """{dxa dya dxb dyb dxc dyc}+ dxd dyd rcurveline""" args = self.popall() @@ -766,7 +612,7 @@ class T2OutlineExtractor(SimpleT2Decompiler): dxb, dyb, dxc, dyc, dxd, dyd = args[i:i+6] self.rCurveTo((dxb, dyb), (dxc, dyc), (dxd, dyd)) self.rLineTo(args[-2:]) - + def op_rlinecurve(self, index): """{dxa dya}+ dxb dyb dxc dyc dxd dyd rlinecurve""" args = self.popall() @@ -775,7 +621,7 @@ class T2OutlineExtractor(SimpleT2Decompiler): self.rLineTo(lineArgs[i:i+2]) dxb, dyb, dxc, dyc, dxd, dyd = args[-6:] self.rCurveTo((dxb, dyb), (dxc, dyc), (dxd, dyd)) - + def op_vvcurveto(self, index): "dx1? {dya dxb dyb dyc}+ vvcurveto" args = self.popall() @@ -788,7 +634,7 @@ class T2OutlineExtractor(SimpleT2Decompiler): dya, dxb, dyb, dyc = args[i:i+4] self.rCurveTo((dx1, dya), (dxb, dyb), (0, dyc)) dx1 = 0 - + def op_hhcurveto(self, index): """dy1? {dxa dxb dyb dxc}+ hhcurveto""" args = self.popall() @@ -801,7 +647,7 @@ class T2OutlineExtractor(SimpleT2Decompiler): dxa, dxb, dyb, dxc = args[i:i+4] self.rCurveTo((dxa, dy1), (dxb, dyb), (dxc, 0)) dy1 = 0 - + def op_vhcurveto(self, index): """dy1 dx2 dy2 dx3 {dxa dxb dyb dyc dyd dxe dye dxf}* dyf? vhcurveto (30) {dya dxb dyb dxc dxd dxe dye dyf}+ dxf? vhcurveto @@ -811,7 +657,7 @@ class T2OutlineExtractor(SimpleT2Decompiler): args = self.vcurveto(args) if args: args = self.hcurveto(args) - + def op_hvcurveto(self, index): """dx1 dx2 dy2 dy3 {dya dxb dyb dxc dxd dxe dye dyf}* dxf? {dxa dxb dyb dyc dyd dxe dye dxf}+ dyf? @@ -821,7 +667,7 @@ class T2OutlineExtractor(SimpleT2Decompiler): args = self.hcurveto(args) if args: args = self.vcurveto(args) - + # # path constructors, flex # @@ -854,13 +700,13 @@ class T2OutlineExtractor(SimpleT2Decompiler): dy6 = d6 self.rCurveTo((dx1, dy1), (dx2, dy2), (dx3, dy3)) self.rCurveTo((dx4, dy4), (dx5, dy5), (dx6, dy6)) - + # # MultipleMaster. Well... # def op_blend(self, index): self.popall() - + # misc def op_and(self, index): raise NotImplementedError @@ -913,7 +759,7 @@ class T2OutlineExtractor(SimpleT2Decompiler): raise NotImplementedError def op_roll(self, index): raise NotImplementedError - + # # miscellaneous helpers # @@ -926,7 +772,7 @@ class T2OutlineExtractor(SimpleT2Decompiler): point = (0, arg) self.rLineTo(point) isHorizontal = not isHorizontal - + def vcurveto(self, args): dya, dxb, dyb, dxc = args[:4] args = args[4:] @@ -937,7 +783,7 @@ class T2OutlineExtractor(SimpleT2Decompiler): dyc = 0 self.rCurveTo((0, dya), (dxb, dyb), (dxc, dyc)) return args - + def hcurveto(self, args): dxa, dxb, dyb, dyc = args[:4] args = args[4:] @@ -949,20 +795,19 @@ class T2OutlineExtractor(SimpleT2Decompiler): self.rCurveTo((dxa, 0), (dxb, dyb), (dxc, dyc)) return args - class T1OutlineExtractor(T2OutlineExtractor): - + def __init__(self, pen, subrs): self.pen = pen self.subrs = subrs self.reset() - + def reset(self): self.flexing = 0 self.width = 0 self.sbx = 0 T2OutlineExtractor.reset(self) - + def endPath(self): if self.sawMoveTo: self.pen.endPath() @@ -970,11 +815,11 @@ class T1OutlineExtractor(T2OutlineExtractor): def popallWidth(self, evenOdd=0): return self.popall() - + def exch(self): stack = self.operandStack stack[-1], stack[-2] = stack[-2], stack[-1] - + # # path constructors # @@ -1004,10 +849,10 @@ class T1OutlineExtractor(T2OutlineExtractor): args = self.popall() x, y = args self.currentPoint = x, y - + def op_endchar(self, index): self.endPath() - + def op_hsbw(self, index): sbx, wx = self.popall() self.width = wx @@ -1015,7 +860,7 @@ class T1OutlineExtractor(T2OutlineExtractor): self.currentPoint = sbx, self.currentPoint[1] def op_sbw(self, index): self.popall() # XXX - + # def op_callsubr(self, index): subrIndex = self.pop() @@ -1033,12 +878,12 @@ class T1OutlineExtractor(T2OutlineExtractor): # ignore... def op_pop(self, index): pass # ignore... - + def doFlex(self): finaly = self.pop() finalx = self.pop() self.pop() # flex height is unused - + p3y = self.pop() p3x = self.pop() bcp4y = self.pop() @@ -1053,7 +898,7 @@ class T1OutlineExtractor(T2OutlineExtractor): bcp1x = self.pop() rpy = self.pop() rpx = self.pop() - + # call rrcurveto self.push(bcp1x+rpx) self.push(bcp1y+rpy) @@ -1062,7 +907,7 @@ class T1OutlineExtractor(T2OutlineExtractor): self.push(p2x) self.push(p2y) self.op_rrcurveto(None) - + # call rrcurveto self.push(bcp3x) self.push(bcp3y) @@ -1071,11 +916,11 @@ class T1OutlineExtractor(T2OutlineExtractor): self.push(p3x) self.push(p3y) self.op_rrcurveto(None) - + # Push back final coords so subr 0 can find them self.push(finalx) self.push(finaly) - + def op_dotsection(self, index): self.popall() # XXX def op_hstem3(self, index): @@ -1092,20 +937,267 @@ class T1OutlineExtractor(T2OutlineExtractor): def op_vstem3(self, index): self.popall() # XXX +class T2CharString(object): -class DictDecompiler(ByteCodeBase): + operandEncoding = t2OperandEncoding + operators, opcodes = buildOperatorDict(t2Operators) + decompilerClass = SimpleT2Decompiler + outlineExtractor = T2OutlineExtractor + isCFF2 = False + def __init__(self, bytecode=None, program=None, private=None, globalSubrs=None): + if program is None: + program = [] + self.bytecode = bytecode + self.program = program + self.private = private + self.globalSubrs = globalSubrs if globalSubrs is not None else [] + + def __repr__(self): + if self.bytecode is None: + return "<%s (source) at %x>" % (self.__class__.__name__, id(self)) + else: + return "<%s (bytecode) at %x>" % (self.__class__.__name__, id(self)) + + def getIntEncoder(self): + return encodeIntT2 + + def getFixedEncoder(self): + return encodeFixed + + def decompile(self): + if not self.needsDecompilation(): + return + subrs = getattr(self.private, "Subrs", []) + decompiler = self.decompilerClass(subrs, self.globalSubrs, self.private) + decompiler.execute(self) + + def draw(self, pen): + subrs = getattr(self.private, "Subrs", []) + extractor = self.outlineExtractor(pen, subrs, self.globalSubrs, + self.private.nominalWidthX, self.private.defaultWidthX) + extractor.execute(self) + self.width = extractor.width + + def calcBounds(self, glyphSet): + boundsPen = BoundsPen(glyphSet) + self.draw(boundsPen) + return boundsPen.bounds + + def check_program(self, program, isCFF2=False): + if isCFF2: + if self.program: + assert self.program[-1] not in ("seac",), "illegal CFF2 CharString Termination" + else: + assert self.program, "illegal CharString: decompiled to empty program" + assert self.program[-1] in ("endchar", "return", "callsubr", "callgsubr", "seac"), "illegal CharString" + + def compile(self, isCFF2=False): + if self.bytecode is not None: + return + opcodes = self.opcodes + program = self.program + self.check_program(program, isCFF2=isCFF2) + bytecode = [] + encodeInt = self.getIntEncoder() + encodeFixed = self.getFixedEncoder() + i = 0 + end = len(program) + while i < end: + token = program[i] + i = i + 1 + tp = type(token) + if issubclass(tp, basestring): + try: + bytecode.extend(bytechr(b) for b in opcodes[token]) + except KeyError: + raise CharStringCompileError("illegal operator: %s" % token) + if token in ('hintmask', 'cntrmask'): + bytecode.append(program[i]) # hint mask + i = i + 1 + elif tp == int: + bytecode.append(encodeInt(token)) + elif tp == float: + bytecode.append(encodeFixed(token)) + else: + assert 0, "unsupported type: %s" % tp + try: + bytecode = bytesjoin(bytecode) + except TypeError: + log.error(bytecode) + raise + self.setBytecode(bytecode) + + if isCFF2: + # If present, remove return and endchar operators. + if self.bytecode and (byteord(self.bytecode[-1]) in (11, 14)): + self.bytecode = self.bytecode[:-1] + + def needsDecompilation(self): + return self.bytecode is not None + + def setProgram(self, program): + self.program = program + self.bytecode = None + + def setBytecode(self, bytecode): + self.bytecode = bytecode + self.program = None + + def getToken(self, index, + len=len, byteord=byteord, basestring=basestring, + isinstance=isinstance): + if self.bytecode is not None: + if index >= len(self.bytecode): + return None, 0, 0 + b0 = byteord(self.bytecode[index]) + index = index + 1 + handler = self.operandEncoding[b0] + token, index = handler(self, b0, self.bytecode, index) + else: + if index >= len(self.program): + return None, 0, 0 + token = self.program[index] + index = index + 1 + isOperator = isinstance(token, basestring) + return token, isOperator, index + + def getBytes(self, index, nBytes): + if self.bytecode is not None: + newIndex = index + nBytes + bytes = self.bytecode[index:newIndex] + index = newIndex + else: + bytes = self.program[index] + index = index + 1 + assert len(bytes) == nBytes + return bytes, index + + def handle_operator(self, operator): + return operator + + def toXML(self, xmlWriter): + from fontTools.misc.textTools import num2binary + if self.bytecode is not None: + xmlWriter.dumphex(self.bytecode) + else: + index = 0 + args = [] + while True: + token, isOperator, index = self.getToken(index) + if token is None: + break + if isOperator: + args = [str(arg) for arg in args] + if token in ('hintmask', 'cntrmask'): + hintMask, isOperator, index = self.getToken(index) + bits = [] + for byte in hintMask: + bits.append(num2binary(byteord(byte), 8)) + hintMask = strjoin(bits) + line = ' '.join(args + [token, hintMask]) + else: + line = ' '.join(args + [token]) + xmlWriter.write(line) + xmlWriter.newline() + args = [] + else: + args.append(token) + if args: + if self.isCFF2: + # CFF2Subr's can have numeric arguments on the stack after the last operator. + args = [str(arg) for arg in args] + line = ' '.join(args) + xmlWriter.write(line) + else: + assert 0, "T2Charstring or Subr has items on the stack after last operator." + + def fromXML(self, name, attrs, content): + from fontTools.misc.textTools import binary2num, readHex + if attrs.get("raw"): + self.setBytecode(readHex(content)) + return + content = strjoin(content) + content = content.split() + program = [] + end = len(content) + i = 0 + while i < end: + token = content[i] + i = i + 1 + try: + token = int(token) + except ValueError: + try: + token = float(token) + except ValueError: + program.append(token) + if token in ('hintmask', 'cntrmask'): + mask = content[i] + maskBytes = b"" + for j in range(0, len(mask), 8): + maskBytes = maskBytes + bytechr(binary2num(mask[j:j+8])) + program.append(maskBytes) + i = i + 1 + else: + program.append(token) + else: + program.append(token) + self.setProgram(program) + +class CFF2Subr(T2CharString): + isCFF2 = True + +class T1CharString(T2CharString): + + operandEncoding = t1OperandEncoding + operators, opcodes = buildOperatorDict(t1Operators) + + def __init__(self, bytecode=None, program=None, subrs=None): + if program is None: + program = [] + self.bytecode = bytecode + self.program = program + self.subrs = subrs + + def getIntEncoder(self): + return encodeIntT1 + + def getFixedEncoder(self): + def encodeFixed(value): + raise TypeError("Type 1 charstrings don't support floating point operands") + + def decompile(self): + if self.bytecode is None: + return + program = [] + index = 0 + while True: + token, isOperator, index = self.getToken(index) + if token is None: + break + program.append(token) + self.setProgram(program) + + def draw(self, pen): + extractor = T1OutlineExtractor(pen, self.subrs) + extractor.execute(self) + self.width = extractor.width + +class DictDecompiler(object): + operandEncoding = cffDictOperandEncoding - - def __init__(self, strings): + + def __init__(self, strings, parent=None): self.stack = [] self.strings = strings self.dict = {} - + self.parent = parent + def getDict(self): assert len(self.stack) == 0, "non-empty stack" return self.dict - + def decompile(self, data): index = 0 lenData = len(data) @@ -1113,34 +1205,23 @@ class DictDecompiler(ByteCodeBase): while index < lenData: b0 = byteord(data[index]) index = index + 1 - code = self.operandEncoding[b0] - handler = getattr(self, code) - value, index = handler(b0, data, index) + handler = self.operandEncoding[b0] + value, index = handler(self, b0, data, index) if value is not None: push(value) - def pop(self): value = self.stack[-1] del self.stack[-1] return value - + def popall(self): args = self.stack[:] del self.stack[:] return args - - def do_operator(self, b0, data, index): - if b0 == 12: - op = (b0, byteord(data[index])) - index = index+1 - else: - op = b0 - operator, argType = self.operators[op] - self.handle_operator(operator, argType) - return None, index - - def handle_operator(self, operator, argType): - if isinstance(argType, type(())): + + def handle_operator(self, operator): + operator, argType = operator + if isinstance(argType, tuple): value = () for i in range(len(argType)-1, -1, -1): arg = argType[i] @@ -1149,20 +1230,74 @@ class DictDecompiler(ByteCodeBase): else: arghandler = getattr(self, "arg_" + argType) value = arghandler(operator) - self.dict[operator] = value - + if operator == "blend": + self.stack.extend(value) + else: + self.dict[operator] = value + def arg_number(self, name): - return self.pop() + if isinstance(self.stack[0], list): + out = self.arg_blend_number(self.stack) + else: + out = self.pop() + return out + + def arg_blend_number(self, name): + out = [] + blendArgs = self.pop() + numMasters = len(blendArgs) + out.append(blendArgs) + out.append("blend") + dummy = self.popall() + return blendArgs + def arg_SID(self, name): return self.strings[self.pop()] def arg_array(self, name): return self.popall() + def arg_blendList(self, name): + """ + There may be non-blend args at the top of the stack. We first calculate + where the blend args start in the stack. These are the last + numMasters*numBlends) +1 args. + The blend args starts with numMasters relative coordinate values, the BlueValues in the list from the default master font. This is followed by + numBlends list of values. Each of value in one of these lists is the + Variable Font delta for the matching region. + + We re-arrange this to be a list of numMaster entries. Each entry starts with the corresponding default font relative value, and is followed by + the delta values. We then convert the default values, the first item in each entry, to an absolute value. + """ + vsindex = self.dict.get('vsindex', 0) + numMasters = self.parent.getNumRegions(vsindex) + 1 # only a PrivateDict has blended ops. + numBlends = self.pop() + args = self.popall() + numArgs = len(args) + # The spec says that there should be no non-blended Blue Values,. + assert(numArgs == numMasters * numBlends) + value = [None]*numBlends + numDeltas = numMasters-1 + i = 0 + prevVal = 0 + while i < numBlends: + newVal = args[i] + prevVal + prevVal = newVal + masterOffset = numBlends + (i* numDeltas) + blendList = [newVal] + args[masterOffset:masterOffset+numDeltas] + value[i] = blendList + i += 1 + return value + def arg_delta(self, name): + valueList = self.popall() out = [] - current = 0 - for v in self.popall(): - current = current + v - out.append(current) + if valueList and isinstance(valueList[0], list): + # arg_blendList() has already converted these to absolute values. + out = valueList + else: + current = 0 + for v in valueList: + current = current + v + out.append(current) return out diff --git a/Lib/fontTools/misc/psLib.py b/Lib/fontTools/misc/psLib.py index 90faa90..aa01eed 100644 --- a/Lib/fontTools/misc/psLib.py +++ b/Lib/fontTools/misc/psLib.py @@ -5,17 +5,20 @@ from .psOperators import * import re import collections from string import whitespace +import logging -ps_special = '()<>[]{}%' # / is one too, but we take care of that one differently +log = logging.getLogger(__name__) -skipwhiteRE = re.compile("[%s]*" % whitespace) -endofthingPat = "[^][(){}<>/%%%s]*" % whitespace +ps_special = b'()<>[]{}%' # / is one too, but we take care of that one differently + +skipwhiteRE = re.compile(bytesjoin([b"[", whitespace, b"]*"])) +endofthingPat = bytesjoin([b"[^][(){}<>/%", whitespace, b"]*"]) endofthingRE = re.compile(endofthingPat) -commentRE = re.compile("%[^\n\r]*") +commentRE = re.compile(b"%[^\n\r]*") # XXX This not entirely correct as it doesn't allow *nested* embedded parens: -stringPat = r""" +stringPat = br""" \( ( ( @@ -29,17 +32,46 @@ stringPat = r""" [^()]* \) """ -stringPat = "".join(stringPat.split()) +stringPat = b"".join(stringPat.split()) stringRE = re.compile(stringPat) -hexstringRE = re.compile("<[%s0-9A-Fa-f]*>" % whitespace) +hexstringRE = re.compile(bytesjoin([b"<[", whitespace, b"0-9A-Fa-f]*>"])) class PSTokenError(Exception): pass class PSError(Exception): pass -class PSTokenizer(StringIO): - +class PSTokenizer(object): + + def __init__(self, buf=b'', encoding="ascii"): + # Force self.buf to be a byte string + buf = tobytes(buf) + self.buf = buf + self.len = len(buf) + self.pos = 0 + self.closed = False + self.encoding = encoding + + def read(self, n=-1): + """Read at most 'n' bytes from the buffer, or less if the read + hits EOF before obtaining 'n' bytes. + If 'n' is negative or omitted, read all data until EOF is reached. + """ + if self.closed: + raise ValueError("I/O operation on closed file") + if n is None or n < 0: + newpos = self.len + else: + newpos = min(self.pos+n, self.len) + r = self.buf[self.pos:newpos] + self.pos = newpos + return r + + def close(self): + if not self.closed: + self.closed = True + del self.buf, self.pos + def getnexttoken(self, # localize some stuff, for performance len=len, @@ -47,32 +79,30 @@ class PSTokenizer(StringIO): stringmatch=stringRE.match, hexstringmatch=hexstringRE.match, commentmatch=commentRE.match, - endmatch=endofthingRE.match, - whitematch=skipwhiteRE.match): - - _, nextpos = whitematch(self.buf, self.pos).span() - self.pos = nextpos + endmatch=endofthingRE.match): + + self.skipwhite() if self.pos >= self.len: return None, None pos = self.pos buf = self.buf - char = buf[pos] + char = bytechr(byteord(buf[pos])) if char in ps_special: - if char in '{}[]': + if char in b'{}[]': tokentype = 'do_special' token = char - elif char == '%': + elif char == b'%': tokentype = 'do_comment' _, nextpos = commentmatch(buf, pos).span() token = buf[pos:nextpos] - elif char == '(': + elif char == b'(': tokentype = 'do_string' m = stringmatch(buf, pos) if m is None: raise PSTokenError('bad string at character %d' % pos) _, nextpos = m.span() token = buf[pos:nextpos] - elif char == '<': + elif char == b'<': tokentype = 'do_hexstring' m = hexstringmatch(buf, pos) if m is None: @@ -82,7 +112,7 @@ class PSTokenizer(StringIO): else: raise PSTokenError('bad token at character %d' % pos) else: - if char == '/': + if char == b'/': tokentype = 'do_literal' m = endmatch(buf, pos+1) else: @@ -93,43 +123,39 @@ class PSTokenizer(StringIO): _, nextpos = m.span() token = buf[pos:nextpos] self.pos = pos + len(token) + token = tostr(token, encoding=self.encoding) return tokentype, token - + def skipwhite(self, whitematch=skipwhiteRE.match): _, nextpos = whitematch(self.buf, self.pos).span() self.pos = nextpos - + def starteexec(self): self.pos = self.pos + 1 - #self.skipwhite() self.dirtybuf = self.buf[self.pos:] self.buf, R = eexec.decrypt(self.dirtybuf, 55665) self.len = len(self.buf) self.pos = 4 - + def stopeexec(self): if not hasattr(self, 'dirtybuf'): return self.buf = self.dirtybuf del self.dirtybuf - - def flush(self): - if self.buflist: - self.buf = self.buf + "".join(self.buflist) - self.buflist = [] class PSInterpreter(PSOperators): - - def __init__(self): + + def __init__(self, encoding="ascii"): systemdict = {} userdict = {} + self.encoding = encoding self.dictstack = [systemdict, userdict] self.stack = [] self.proclevel = 0 self.procmark = ps_procmark() self.fillsystemdict() - + def fillsystemdict(self): systemdict = self.dictstack[0] systemdict['['] = systemdict['mark'] = self.mark = ps_mark() @@ -139,7 +165,7 @@ class PSInterpreter(PSOperators): systemdict['StandardEncoding'] = ps_array(ps_StandardEncoding) systemdict['FontDirectory'] = ps_dict({}) self.suckoperators(systemdict, self.__class__) - + def suckoperators(self, systemdict, klass): for name in dir(klass): attr = getattr(self, name) @@ -148,16 +174,15 @@ class PSInterpreter(PSOperators): systemdict[name] = ps_operator(name, attr) for baseclass in klass.__bases__: self.suckoperators(systemdict, baseclass) - - def interpret(self, data, getattr = getattr): - tokenizer = self.tokenizer = PSTokenizer(data) + + def interpret(self, data, getattr=getattr): + tokenizer = self.tokenizer = PSTokenizer(data, self.encoding) getnexttoken = tokenizer.getnexttoken do_token = self.do_token handle_object = self.handle_object try: while 1: tokentype, token = getnexttoken() - #print token if not token: break if tokentype: @@ -169,15 +194,19 @@ class PSInterpreter(PSOperators): handle_object(object) tokenizer.close() self.tokenizer = None - finally: + except: if self.tokenizer is not None: - if 0: - print('ps error:\n- - - - - - -') - print(self.tokenizer.buf[self.tokenizer.pos-50:self.tokenizer.pos]) - print('>>>') - print(self.tokenizer.buf[self.tokenizer.pos:self.tokenizer.pos+50]) - print('- - - - - - -') - + log.debug( + 'ps error:\n' + '- - - - - - -\n' + '%s\n' + '>>>\n' + '%s\n' + '- - - - - - -', + self.tokenizer.buf[self.tokenizer.pos-50:self.tokenizer.pos], + self.tokenizer.buf[self.tokenizer.pos:self.tokenizer.pos+50]) + raise + def handle_object(self, object): if not (self.proclevel or object.literal or object.type == 'proceduretype'): if object.type != 'operatortype': @@ -191,21 +220,21 @@ class PSInterpreter(PSOperators): object.function() else: self.push(object) - + def call_procedure(self, proc): handle_object = self.handle_object for item in proc.value: handle_object(item) - + def resolve_name(self, name): dictstack = self.dictstack for i in range(len(dictstack)-1, -1, -1): if name in dictstack[i]: return dictstack[i][name] raise PSError('name error: ' + str(name)) - + def do_token(self, token, - int=int, + int=int, float=float, ps_name=ps_name, ps_integer=ps_integer, @@ -231,16 +260,16 @@ class PSInterpreter(PSOperators): return ps_real(num) else: return ps_integer(num) - + def do_comment(self, token): pass - + def do_literal(self, token): return ps_literal(token[1:]) - + def do_string(self, token): return ps_string(token[1:-1]) - + def do_hexstring(self, token): hexStr = "".join(token[1:-1].split()) if len(hexStr) % 2: @@ -250,7 +279,7 @@ class PSInterpreter(PSOperators): cleanstr.append(chr(int(hexStr[i:i+2], 16))) cleanstr = "".join(cleanstr) return ps_string(cleanstr) - + def do_special(self, token): if token == '{': self.proclevel = self.proclevel + 1 @@ -271,10 +300,10 @@ class PSInterpreter(PSOperators): return ps_name(']') else: raise PSTokenError('huh?') - + def push(self, object): self.stack.append(object) - + def pop(self, *types): stack = self.stack if not stack: @@ -285,7 +314,7 @@ class PSInterpreter(PSOperators): raise PSError('typecheck, expected %s, found %s' % (repr(types), object.type)) del stack[-1] return object - + def do_makearray(self): array = [] while 1: @@ -295,7 +324,7 @@ class PSInterpreter(PSOperators): array.append(topobject) array.reverse() self.push(ps_array(array)) - + def close(self): """Remove circular references.""" del self.stack @@ -318,14 +347,13 @@ def unpack_item(item): newitem = item.value return newitem -def suckfont(data): - import re +def suckfont(data, encoding="ascii"): m = re.search(br"/FontName\s+/([^ \t\n\r]+)\s+def", data) if m: fontName = m.group(1) else: fontName = None - interpreter = PSInterpreter() + interpreter = PSInterpreter(encoding=encoding) interpreter.interpret(b"/Helvetica 4 dict dup /Encoding StandardEncoding put definefont pop") interpreter.interpret(data) fontdir = interpreter.dictstack[0]['FontDirectory'].value @@ -340,12 +368,3 @@ def suckfont(data): rawfont = fontdir[fontNames[0]] interpreter.close() return unpack_item(rawfont) - - -if __name__ == "__main__": - import EasyDialogs - path = EasyDialogs.AskFileForOpen() - if path: - from fontTools import t1Lib - data, kind = t1Lib.read(path) - font = suckfont(data) diff --git a/Lib/fontTools/misc/psOperators.py b/Lib/fontTools/misc/psOperators.py index 57cfbe8..4aa0281 100644 --- a/Lib/fontTools/misc/psOperators.py +++ b/Lib/fontTools/misc/psOperators.py @@ -4,24 +4,24 @@ from fontTools.misc.py23 import * _accessstrings = {0: "", 1: "readonly", 2: "executeonly", 3: "noaccess"} -class ps_object: - +class ps_object(object): + literal = 1 access = 0 value = None - + def __init__(self, value): self.value = value self.type = self.__class__.__name__[3:] + "type" - + def __repr__(self): return "<%s %s>" % (self.__class__.__name__[3:], repr(self.value)) class ps_operator(ps_object): - + literal = 0 - + def __init__(self, name, function): self.name = name self.function = function @@ -171,7 +171,7 @@ class ps_dict(ps_object): return "" class ps_mark(ps_object): - def __init__(self): + def __init__(self): self.value = 'mark' self.type = self.__class__.__name__[3:] + "type" @@ -204,18 +204,18 @@ class ps_real(ps_object): return repr(self.value) -class PSOperators: - +class PSOperators(object): + def ps_def(self): obj = self.pop() name = self.pop() self.dictstack[-1][name.value] = obj - + def ps_bind(self): proc = self.pop('proceduretype') self.proc_bind(proc) self.push(proc) - + def proc_bind(self, proc): for i in range(len(proc.value)): item = proc.value[i] @@ -230,7 +230,7 @@ class PSOperators: else: if obj.type == 'operatortype': proc.value[i] = obj - + def ps_exch(self): if len(self.stack) < 2: raise RuntimeError('stack underflow') @@ -238,49 +238,49 @@ class PSOperators: obj2 = self.pop() self.push(obj1) self.push(obj2) - + def ps_dup(self): if not self.stack: raise RuntimeError('stack underflow') self.push(self.stack[-1]) - + def ps_exec(self): obj = self.pop() if obj.type == 'proceduretype': self.call_procedure(obj) else: self.handle_object(obj) - + def ps_count(self): self.push(ps_integer(len(self.stack))) - + def ps_eq(self): any1 = self.pop() any2 = self.pop() self.push(ps_boolean(any1.value == any2.value)) - + def ps_ne(self): any1 = self.pop() any2 = self.pop() self.push(ps_boolean(any1.value != any2.value)) - + def ps_cvx(self): obj = self.pop() obj.literal = 0 self.push(obj) - + def ps_matrix(self): matrix = [ps_real(1.0), ps_integer(0), ps_integer(0), ps_real(1.0), ps_integer(0), ps_integer(0)] self.push(ps_array(matrix)) - + def ps_string(self): num = self.pop('integertype').value self.push(ps_string('\0' * num)) - + def ps_type(self): obj = self.pop() self.push(ps_string(obj.type)) - + def ps_store(self): value = self.pop() key = self.pop() @@ -290,41 +290,41 @@ class PSOperators: self.dictstack[i][name] = value break self.dictstack[-1][name] = value - + def ps_where(self): name = self.pop() # XXX self.push(ps_boolean(0)) - + def ps_systemdict(self): self.push(ps_dict(self.dictstack[0])) - + def ps_userdict(self): self.push(ps_dict(self.dictstack[1])) - + def ps_currentdict(self): self.push(ps_dict(self.dictstack[-1])) - + def ps_currentfile(self): self.push(ps_file(self.tokenizer)) - + def ps_eexec(self): f = self.pop('filetype').value f.starteexec() - + def ps_closefile(self): f = self.pop('filetype').value f.skipwhite() f.stopeexec() - + def ps_cleartomark(self): obj = self.pop() while obj != self.mark: obj = self.pop() - + def ps_readstring(self, - ps_boolean = ps_boolean, - len = len): + ps_boolean=ps_boolean, + len=len): s = self.pop('stringtype') oldstr = s.value f = self.pop('filetype') @@ -335,17 +335,17 @@ class PSOperators: s.value = newstr self.push(s) self.push(ps_boolean(len(oldstr) == len(newstr))) - + def ps_known(self): key = self.pop() d = self.pop('dicttype', 'fonttype') self.push(ps_boolean(key.value in d.value)) - + def ps_if(self): proc = self.pop('proceduretype') if self.pop('booleantype').value: self.call_procedure(proc) - + def ps_ifelse(self): proc2 = self.pop('proceduretype') proc1 = self.pop('proceduretype') @@ -353,36 +353,36 @@ class PSOperators: self.call_procedure(proc1) else: self.call_procedure(proc2) - + def ps_readonly(self): obj = self.pop() if obj.access < 1: obj.access = 1 self.push(obj) - + def ps_executeonly(self): obj = self.pop() if obj.access < 2: obj.access = 2 self.push(obj) - + def ps_noaccess(self): obj = self.pop() if obj.access < 3: obj.access = 3 self.push(obj) - + def ps_not(self): obj = self.pop('booleantype', 'integertype') if obj.type == 'booleantype': self.push(ps_boolean(not obj.value)) else: self.push(ps_integer(~obj.value)) - + def ps_print(self): str = self.pop('stringtype') print('PS output --->', str.value) - + def ps_anchorsearch(self): seek = self.pop('stringtype') s = self.pop('stringtype') @@ -394,22 +394,22 @@ class PSOperators: else: self.push(s) self.push(ps_boolean(0)) - + def ps_array(self): num = self.pop('integertype') array = ps_array([None] * num.value) self.push(array) - + def ps_astore(self): array = self.pop('arraytype') for i in range(len(array.value)-1, -1, -1): array.value[i] = self.pop() self.push(array) - + def ps_load(self): name = self.pop() self.push(self.resolve_name(name.value)) - + def ps_put(self): obj1 = self.pop() obj2 = self.pop() @@ -422,7 +422,7 @@ class PSOperators: elif tp == 'stringtype': index = obj2.value obj3.value = obj3.value[:index] + chr(obj1.value) + obj3.value[index+1:] - + def ps_get(self): obj1 = self.pop() if obj1.value == "Encoding": @@ -437,7 +437,7 @@ class PSOperators: self.push(ps_integer(ord(obj2.value[obj1.value]))) else: assert False, "shouldn't get here" - + def ps_getinterval(self): obj1 = self.pop('integertype') obj2 = self.pop('integertype') @@ -447,7 +447,7 @@ class PSOperators: self.push(ps_array(obj3.value[obj2.value:obj2.value + obj1.value])) elif tp == 'stringtype': self.push(ps_string(obj3.value[obj2.value:obj2.value + obj1.value])) - + def ps_putinterval(self): obj1 = self.pop('arraytype', 'stringtype') obj2 = self.pop('integertype') @@ -460,16 +460,16 @@ class PSOperators: newstr = newstr + obj1.value newstr = newstr + obj3.value[obj2.value + len(obj1.value):] obj3.value = newstr - + def ps_cvn(self): self.push(ps_name(self.pop('stringtype').value)) - + def ps_index(self): n = self.pop('integertype').value if n < 0: raise RuntimeError('index may not be negative') self.push(self.stack[-1-n]) - + def ps_for(self): proc = self.pop('proceduretype') limit = self.pop('integertype', 'realtype').value @@ -488,7 +488,7 @@ class PSOperators: self.push(ps_integer(i)) self.call_procedure(proc) i = i + increment - + def ps_forall(self): proc = self.pop('proceduretype') obj = self.pop('arraytype', 'stringtype', 'dicttype') @@ -505,37 +505,36 @@ class PSOperators: for key, value in obj.value.items(): self.push(ps_name(key)) self.push(value) - self.call_procedure(proc) - + self.call_procedure(proc) + def ps_definefont(self): font = self.pop('dicttype') name = self.pop() font = ps_font(font.value) self.dictstack[0]['FontDirectory'].value[name.value] = font self.push(font) - + def ps_findfont(self): name = self.pop() font = self.dictstack[0]['FontDirectory'].value[name.value] self.push(font) - + def ps_pop(self): self.pop() - + def ps_dict(self): self.pop('integertype') self.push(ps_dict({})) - + def ps_begin(self): self.dictstack.append(self.pop('dicttype').value) - + def ps_end(self): if len(self.dictstack) > 2: del self.dictstack[-1] else: raise RuntimeError('dictstack underflow') - + notdef = '.notdef' from fontTools.encodings.StandardEncoding import StandardEncoding ps_StandardEncoding = list(map(ps_name, StandardEncoding)) - diff --git a/Lib/fontTools/misc/py23.py b/Lib/fontTools/misc/py23.py index 90217a3..0a13b6f 100644 --- a/Lib/fontTools/misc/py23.py +++ b/Lib/fontTools/misc/py23.py @@ -1,35 +1,137 @@ """Python 2/3 compat layer.""" from __future__ import print_function, division, absolute_import +import sys + + +__all__ = ['basestring', 'unicode', 'unichr', 'byteord', 'bytechr', 'BytesIO', + 'StringIO', 'UnicodeIO', 'strjoin', 'bytesjoin', 'tobytes', 'tostr', + 'tounicode', 'Tag', 'open', 'range', 'xrange', 'round', 'Py23Error', + 'SimpleNamespace', 'zip'] + + +class Py23Error(NotImplementedError): + pass + + +PY3 = sys.version_info[0] == 3 +PY2 = sys.version_info[0] == 2 + try: - basestring + basestring = basestring except NameError: basestring = str try: - unicode + unicode = unicode except NameError: unicode = str try: - unichr + unichr = unichr + + if sys.maxunicode < 0x10FFFF: + # workarounds for Python 2 "narrow" builds with UCS2-only support. + + _narrow_unichr = unichr + + def unichr(i): + """ + Return the unicode character whose Unicode code is the integer 'i'. + The valid range is 0 to 0x10FFFF inclusive. + + >>> _narrow_unichr(0xFFFF + 1) + Traceback (most recent call last): + File "", line 1, in ? + ValueError: unichr() arg not in range(0x10000) (narrow Python build) + >>> unichr(0xFFFF + 1) == u'\U00010000' + True + >>> unichr(1114111) == u'\U0010FFFF' + True + >>> unichr(0x10FFFF + 1) + Traceback (most recent call last): + File "", line 1, in ? + ValueError: unichr() arg not in range(0x110000) + """ + try: + return _narrow_unichr(i) + except ValueError: + try: + padded_hex_str = hex(i)[2:].zfill(8) + escape_str = "\\U" + padded_hex_str + return escape_str.decode("unicode-escape") + except UnicodeDecodeError: + raise ValueError('unichr() arg not in range(0x110000)') + + import re + _unicode_escape_RE = re.compile(r'\\U[A-Fa-f0-9]{8}') + + def byteord(c): + """ + Given a 8-bit or unicode character, return an integer representing the + Unicode code point of the character. If a unicode argument is given, the + character's code point must be in the range 0 to 0x10FFFF inclusive. + + >>> ord(u'\U00010000') + Traceback (most recent call last): + File "", line 1, in ? + TypeError: ord() expected a character, but string of length 2 found + >>> byteord(u'\U00010000') == 0xFFFF + 1 + True + >>> byteord(u'\U0010FFFF') == 1114111 + True + """ + try: + return ord(c) + except TypeError as e: + try: + escape_str = c.encode('unicode-escape') + if not _unicode_escape_RE.match(escape_str): + raise + hex_str = escape_str[3:] + return int(hex_str, 16) + except: + raise TypeError(e) + + else: + byteord = ord bytechr = chr - byteord = ord -except: + +except NameError: unichr = chr def bytechr(n): return bytes([n]) def byteord(c): return c if isinstance(c, int) else ord(c) + +# the 'io' module provides the same I/O interface on both 2 and 3. +# here we define an alias of io.StringIO to disambiguate it eternally... +from io import BytesIO +from io import StringIO as UnicodeIO try: + # in python 2, by 'StringIO' we still mean a stream of *byte* strings from StringIO import StringIO except ImportError: - from io import BytesIO as StringIO + # in Python 3, we mean instead a stream of *unicode* strings + StringIO = UnicodeIO + + +def strjoin(iterable, joiner=''): + return tostr(joiner).join(iterable) + +def tobytes(s, encoding='ascii', errors='strict'): + if not isinstance(s, bytes): + return s.encode(encoding, errors) + else: + return s +def tounicode(s, encoding='ascii', errors='strict'): + if not isinstance(s, unicode): + return s.decode(encoding, errors) + else: + return s -def strjoin(iterable): - return ''.join(iterable) if str == bytes: class Tag(str): def tobytes(self): @@ -38,12 +140,7 @@ if str == bytes: else: return self.encode('latin1') - def tostr(s, encoding='ascii'): - if not isinstance(s, str): - return s.encode(encoding) - else: - return s - tobytes = tostr + tostr = tobytes bytesjoin = strjoin else: @@ -51,7 +148,7 @@ else: @staticmethod def transcode(blob): - if not isinstance(blob, str): + if isinstance(blob, bytes): blob = blob.decode('latin-1') return blob @@ -68,16 +165,326 @@ else: def tobytes(self): return self.encode('latin-1') - def tostr(s, encoding='ascii'): - if not isinstance(s, str): - return s.decode(encoding) + tostr = tounicode + + def bytesjoin(iterable, joiner=b''): + return tobytes(joiner).join(tobytes(item) for item in iterable) + + +import os +import io as _io + +try: + from msvcrt import setmode as _setmode +except ImportError: + _setmode = None # only available on the Windows platform + + +def open(file, mode='r', buffering=-1, encoding=None, errors=None, + newline=None, closefd=True, opener=None): + """ Wrapper around `io.open` that bridges the differences between Python 2 + and Python 3's built-in `open` functions. In Python 2, `io.open` is a + backport of Python 3's `open`, whereas in Python 3, it is an alias of the + built-in `open` function. + + One difference is that the 'opener' keyword argument is only supported in + Python 3. Here we pass the value of 'opener' only when it is not None. + This causes Python 2 to raise TypeError, complaining about the number of + expected arguments, so it must be avoided in py2 or py2-3 contexts. + + Another difference between 2 and 3, this time on Windows, has to do with + opening files by name or by file descriptor. + + On the Windows C runtime, the 'O_BINARY' flag is defined which disables + the newlines translation ('\r\n' <=> '\n') when reading/writing files. + On both Python 2 and 3 this flag is always set when opening files by name. + This way, the newlines translation at the MSVCRT level doesn't interfere + with the Python io module's own newlines translation. + + However, when opening files via fd, on Python 2 the fd is simply copied, + regardless of whether it has the 'O_BINARY' flag set or not. + This becomes a problem in the case of stdout, stdin, and stderr, because on + Windows these are opened in text mode by default (ie. don't have the + O_BINARY flag set). + + On Python 3, this issue has been fixed, and all fds are now opened in + binary mode on Windows, including standard streams. Similarly here, I use + the `_setmode` function to ensure that integer file descriptors are + O_BINARY'ed before I pass them on to io.open. + + For more info, see: https://bugs.python.org/issue10841 + """ + if isinstance(file, int): + # the 'file' argument is an integer file descriptor + fd = file + if fd < 0: + raise ValueError('negative file descriptor') + if _setmode: + # `_setmode` function sets the line-end translation and returns the + # value of the previous mode. AFAIK there's no `_getmode`, so to + # check if the previous mode already had the bit set, I fist need + # to duplicate the file descriptor, set the binary flag on the copy + # and check the returned value. + fdcopy = os.dup(fd) + current_mode = _setmode(fdcopy, os.O_BINARY) + if not (current_mode & os.O_BINARY): + # the binary mode was not set: use the file descriptor's copy + file = fdcopy + if closefd: + # close the original file descriptor + os.close(fd) + else: + # ensure the copy is closed when the file object is closed + closefd = True + else: + # original file descriptor already had binary flag, close copy + os.close(fdcopy) + + if opener is not None: + # "opener" is not supported on Python 2, use it at your own risk! + return _io.open( + file, mode, buffering, encoding, errors, newline, closefd, + opener=opener) + else: + return _io.open( + file, mode, buffering, encoding, errors, newline, closefd) + + +# always use iterator for 'range' and 'zip' on both py 2 and 3 +try: + range = xrange +except NameError: + range = range + +def xrange(*args, **kwargs): + raise Py23Error("'xrange' is not defined. Use 'range' instead.") + +try: + from itertools import izip as zip +except ImportError: + zip = zip + + +import math as _math + +try: + isclose = _math.isclose +except AttributeError: + # math.isclose() was only added in Python 3.5 + + _isinf = _math.isinf + _fabs = _math.fabs + + def isclose(a, b, rel_tol=1e-09, abs_tol=0): + """ + Python 2 implementation of Python 3.5 math.isclose() + https://hg.python.org/cpython/file/v3.5.2/Modules/mathmodule.c#l1993 + """ + # sanity check on the inputs + if rel_tol < 0 or abs_tol < 0: + raise ValueError("tolerances must be non-negative") + # short circuit exact equality -- needed to catch two infinities of + # the same sign. And perhaps speeds things up a bit sometimes. + if a == b: + return True + # This catches the case of two infinities of opposite sign, or + # one infinity and one finite number. Two infinities of opposite + # sign would otherwise have an infinite relative tolerance. + # Two infinities of the same sign are caught by the equality check + # above. + if _isinf(a) or _isinf(b): + return False + # Cast to float to allow decimal.Decimal arguments + if not isinstance(a, float): + a = float(a) + if not isinstance(b, float): + b = float(b) + # now do the regular computation + # this is essentially the "weak" test from the Boost library + diff = _fabs(b - a) + result = ((diff <= _fabs(rel_tol * a)) or + (diff <= _fabs(rel_tol * b)) or + (diff <= abs_tol)) + return result + + +import decimal as _decimal + +if PY3: + def round2(number, ndigits=None): + """ + Implementation of Python 2 built-in round() function. + + Rounds a number to a given precision in decimal digits (default + 0 digits). The result is a floating point number. Values are rounded + to the closest multiple of 10 to the power minus ndigits; if two + multiples are equally close, rounding is done away from 0. + + ndigits may be negative. + + See Python 2 documentation: + https://docs.python.org/2/library/functions.html?highlight=round#round + """ + if ndigits is None: + ndigits = 0 + + if ndigits < 0: + exponent = 10 ** (-ndigits) + quotient, remainder = divmod(number, exponent) + if remainder >= exponent//2 and number >= 0: + quotient += 1 + return float(quotient * exponent) else: - return s - def tobytes(s, encoding='ascii'): - if not isinstance(s, bytes): - return s.encode(encoding) + exponent = _decimal.Decimal('10') ** (-ndigits) + + d = _decimal.Decimal.from_float(number).quantize( + exponent, rounding=_decimal.ROUND_HALF_UP) + + return float(d) + + if sys.version_info[:2] >= (3, 6): + # in Python 3.6, 'round3' is an alias to the built-in 'round' + round = round3 = round + else: + # in Python3 < 3.6 we need work around the inconsistent behavior of + # built-in round(), whereby floats accept a second None argument, + # while integers raise TypeError. See https://bugs.python.org/issue27936 + _round = round + + def round3(number, ndigits=None): + return _round(number) if ndigits is None else _round(number, ndigits) + + round = round3 + +else: + # in Python 2, 'round2' is an alias to the built-in 'round' and + # 'round' is shadowed by 'round3' + round2 = round + + def round3(number, ndigits=None): + """ + Implementation of Python 3 built-in round() function. + + Rounds a number to a given precision in decimal digits (default + 0 digits). This returns an int when ndigits is omitted or is None, + otherwise the same type as the number. + + Values are rounded to the closest multiple of 10 to the power minus + ndigits; if two multiples are equally close, rounding is done toward + the even choice (aka "Banker's Rounding"). For example, both round(0.5) + and round(-0.5) are 0, and round(1.5) is 2. + + ndigits may be negative. + + See Python 3 documentation: + https://docs.python.org/3/library/functions.html?highlight=round#round + + Derived from python-future: + https://github.com/PythonCharmers/python-future/blob/master/src/future/builtins/newround.py + """ + if ndigits is None: + ndigits = 0 + # return an int when called with one argument + totype = int + # shortcut if already an integer, or a float with no decimal digits + inumber = totype(number) + if inumber == number: + return inumber + else: + # return the same type as the number, when called with two arguments + totype = type(number) + + m = number * (10 ** ndigits) + # if number is half-way between two multiples, and the mutliple that is + # closer to zero is even, we use the (slow) pure-Python implementation + if isclose(m % 1, .5) and int(m) % 2 == 0: + if ndigits < 0: + exponent = 10 ** (-ndigits) + quotient, remainder = divmod(number, exponent) + half = exponent//2 + if remainder > half or (remainder == half and quotient % 2 != 0): + quotient += 1 + d = quotient * exponent + else: + exponent = _decimal.Decimal('10') ** (-ndigits) if ndigits != 0 else 1 + + d = _decimal.Decimal.from_float(number).quantize( + exponent, rounding=_decimal.ROUND_HALF_EVEN) else: - return s + # else we use the built-in round() as it produces the same results + d = round2(number, ndigits) + + return totype(d) + + round = round3 + + +try: + from types import SimpleNamespace +except ImportError: + class SimpleNamespace(object): + """ + A backport of Python 3.3's ``types.SimpleNamespace``. + """ + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + def __repr__(self): + keys = sorted(self.__dict__) + items = ("{0}={1!r}".format(k, self.__dict__[k]) for k in keys) + return "{0}({1})".format(type(self).__name__, ", ".join(items)) + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + +if sys.version_info[:2] > (3, 4): + from contextlib import redirect_stdout, redirect_stderr +else: + # `redirect_stdout` was added with python3.4, while `redirect_stderr` + # with python3.5. For simplicity, I redefine both for any versions + # less than or equal to 3.4. + # The code below is copied from: + # https://github.com/python/cpython/blob/57161aa/Lib/contextlib.py + + class _RedirectStream(object): + + _stream = None + + def __init__(self, new_target): + self._new_target = new_target + # We use a list of old targets to make this CM re-entrant + self._old_targets = [] + + def __enter__(self): + self._old_targets.append(getattr(sys, self._stream)) + setattr(sys, self._stream, self._new_target) + return self._new_target + + def __exit__(self, exctype, excinst, exctb): + setattr(sys, self._stream, self._old_targets.pop()) + + + class redirect_stdout(_RedirectStream): + """Context manager for temporarily redirecting stdout to another file. + # How to send help() to stderr + with redirect_stdout(sys.stderr): + help(dir) + # How to write help() to a file + with open('help.txt', 'w') as f: + with redirect_stdout(f): + help(pow) + """ + + _stream = "stdout" + + + class redirect_stderr(_RedirectStream): + """Context manager for temporarily redirecting stderr to another file.""" + + _stream = "stderr" + - def bytesjoin(iterable): - return b''.join(tobytes(item) for item in iterable) +if __name__ == "__main__": + import doctest, sys + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/misc/sstruct.py b/Lib/fontTools/misc/sstruct.py index 8a2b073..528b5e0 100644 --- a/Lib/fontTools/misc/sstruct.py +++ b/Lib/fontTools/misc/sstruct.py @@ -1,44 +1,44 @@ """sstruct.py -- SuperStruct -Higher level layer on top of the struct module, enabling to -bind names to struct elements. The interface is similar to -struct, except the objects passed and returned are not tuples -(or argument lists), but dictionaries or instances. - -Just like struct, we use fmt strings to describe a data -structure, except we use one line per element. Lines are -separated by newlines or semi-colons. Each line contains -either one of the special struct characters ('@', '=', '<', -'>' or '!') or a 'name:formatchar' combo (eg. 'myFloat:f'). -Repetitions, like the struct module offers them are not useful -in this context, except for fixed length strings (eg. 'myInt:5h' -is not allowed but 'myString:5s' is). The 'x' fmt character -(pad byte) is treated as 'special', since it is by definition +Higher level layer on top of the struct module, enabling to +bind names to struct elements. The interface is similar to +struct, except the objects passed and returned are not tuples +(or argument lists), but dictionaries or instances. + +Just like struct, we use fmt strings to describe a data +structure, except we use one line per element. Lines are +separated by newlines or semi-colons. Each line contains +either one of the special struct characters ('@', '=', '<', +'>' or '!') or a 'name:formatchar' combo (eg. 'myFloat:f'). +Repetitions, like the struct module offers them are not useful +in this context, except for fixed length strings (eg. 'myInt:5h' +is not allowed but 'myString:5s' is). The 'x' fmt character +(pad byte) is treated as 'special', since it is by definition anonymous. Extra whitespace is allowed everywhere. The sstruct module offers one feature that the "normal" struct module doesn't: support for fixed point numbers. These are spelled as "n.mF", where n is the number of bits before the point, and m -the number of bits after the point. Fixed point numbers get +the number of bits after the point. Fixed point numbers get converted to floats. pack(fmt, object): 'object' is either a dictionary or an instance (or actually - anything that has a __dict__ attribute). If it is a dictionary, - its keys are used for names. If it is an instance, it's + anything that has a __dict__ attribute). If it is a dictionary, + its keys are used for names. If it is an instance, it's attributes are used to grab struct elements from. Returns a string containing the data. unpack(fmt, data, object=None) - If 'object' is omitted (or None), a new dictionary will be - returned. If 'object' is a dictionary, it will be used to add + If 'object' is omitted (or None), a new dictionary will be + returned. If 'object' is a dictionary, it will be used to add struct elements to. If it is an instance (or in fact anything - that has a __dict__ attribute), an attribute will be added for - each struct element. In the latter two cases, 'object' itself + that has a __dict__ attribute), an attribute will be added for + each struct element. In the latter two cases, 'object' itself is returned. unpack2(fmt, data, object=None) - Convenience function. Same as unpack, except data may be longer + Convenience function. Same as unpack, except data may be longer than needed. The returned value is a tuple: (object, leftoverdata). calcsize(fmt) @@ -133,6 +133,7 @@ _fixedpointmappings = { _formatcache = {} def getformat(fmt): + fmt = tostr(fmt, encoding="ascii") try: formatstring, names, fixes = _formatcache[fmt] except KeyError: @@ -174,7 +175,7 @@ def _test(): # comments are allowed > # big endian (see documentation for struct) # empty lines are allowed: - + ashort: h along: l abyte: b # a byte @@ -183,14 +184,14 @@ def _test(): afloat: f; adouble: d # multiple "statements" are allowed afixed: 16.16F """ - + print('size:', calcsize(fmt)) - + class foo(object): pass - + i = foo() - + i.ashort = 0x7fff i.along = 0x7fffffff i.abyte = 0x7f @@ -199,7 +200,7 @@ def _test(): i.afloat = 0.5 i.adouble = 0.5 i.afixed = 1.5 - + data = pack(fmt, i) print('data:', repr(data)) print(unpack(fmt, data)) diff --git a/Lib/fontTools/misc/symfont.py b/Lib/fontTools/misc/symfont.py new file mode 100644 index 0000000..d09efac --- /dev/null +++ b/Lib/fontTools/misc/symfont.py @@ -0,0 +1,196 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.pens.basePen import BasePen +from functools import partial +from itertools import count +import sympy as sp +import sys + +n = 3 # Max Bezier degree; 3 for cubic, 2 for quadratic + +t, x, y = sp.symbols('t x y', real=True) +c = sp.symbols('c', real=False) # Complex representation instead of x/y + +X = tuple(sp.symbols('x:%d'%(n+1), real=True)) +Y = tuple(sp.symbols('y:%d'%(n+1), real=True)) +P = tuple(zip(*(sp.symbols('p:%d[%s]'%(n+1,w), real=True) for w in '01'))) +C = tuple(sp.symbols('c:%d'%(n+1), real=False)) + +# Cubic Bernstein basis functions +BinomialCoefficient = [(1, 0)] +for i in range(1, n+1): + last = BinomialCoefficient[-1] + this = tuple(last[j-1]+last[j] for j in range(len(last)))+(0,) + BinomialCoefficient.append(this) +BinomialCoefficient = tuple(tuple(item[:-1]) for item in BinomialCoefficient) +del last, this + +BernsteinPolynomial = tuple( + tuple(c * t**i * (1-t)**(n-i) for i,c in enumerate(coeffs)) + for n,coeffs in enumerate(BinomialCoefficient)) + +BezierCurve = tuple( + tuple(sum(P[i][j]*bernstein for i,bernstein in enumerate(bernsteins)) + for j in range(2)) + for n,bernsteins in enumerate(BernsteinPolynomial)) +BezierCurveC = tuple( + sum(C[i]*bernstein for i,bernstein in enumerate(bernsteins)) + for n,bernsteins in enumerate(BernsteinPolynomial)) + + +def green(f, curveXY): + f = -sp.integrate(sp.sympify(f), y) + f = f.subs({x:curveXY[0], y:curveXY[1]}) + f = sp.integrate(f * sp.diff(curveXY[0], t), (t, 0, 1)) + return f + + +class _BezierFuncsLazy(dict): + + def __init__(self, symfunc): + self._symfunc = symfunc + self._bezfuncs = {} + + def __missing__(self, i): + args = ['p%d'%d for d in range(i+1)] + f = green(self._symfunc, BezierCurve[i]) + f = sp.gcd_terms(f.collect(sum(P,()))) # Optimize + return sp.lambdify(args, f) + +class GreenPen(BasePen): + + _BezierFuncs = {} + + @classmethod + def _getGreenBezierFuncs(celf, func): + funcstr = str(func) + if not funcstr in celf._BezierFuncs: + celf._BezierFuncs[funcstr] = _BezierFuncsLazy(func) + return celf._BezierFuncs[funcstr] + + def __init__(self, func, glyphset=None): + BasePen.__init__(self, glyphset) + self._funcs = self._getGreenBezierFuncs(func) + self.value = 0 + + def _moveTo(self, p0): + self.__startPoint = p0 + + def _closePath(self): + p0 = self._getCurrentPoint() + if p0 != self.__startPoint: + self._lineTo(self.__startPoint) + + def _endPath(self): + p0 = self._getCurrentPoint() + if p0 != self.__startPoint: + # Green theorem is not defined on open contours. + raise NotImplementedError + + def _lineTo(self, p1): + p0 = self._getCurrentPoint() + self.value += self._funcs[1](p0, p1) + + def _qCurveToOne(self, p1, p2): + p0 = self._getCurrentPoint() + self.value += self._funcs[2](p0, p1, p2) + + def _curveToOne(self, p1, p2, p3): + p0 = self._getCurrentPoint() + self.value += self._funcs[3](p0, p1, p2, p3) + +# Sample pens. +# Do not use this in real code. +# Use fontTools.pens.momentsPen.MomentsPen instead. +AreaPen = partial(GreenPen, func=1) +MomentXPen = partial(GreenPen, func=x) +MomentYPen = partial(GreenPen, func=y) +MomentXXPen = partial(GreenPen, func=x*x) +MomentYYPen = partial(GreenPen, func=y*y) +MomentXYPen = partial(GreenPen, func=x*y) + + +def printGreenPen(penName, funcs, file=sys.stdout): + + print( +'''from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.pens.basePen import BasePen + +class %s(BasePen): + + def __init__(self, glyphset=None): + BasePen.__init__(self, glyphset) +'''%penName, file=file) + for name,f in funcs: + print(' self.%s = 0' % name, file=file) + print(''' + def _moveTo(self, p0): + self.__startPoint = p0 + + def _closePath(self): + p0 = self._getCurrentPoint() + if p0 != self.__startPoint: + self._lineTo(self.__startPoint) + + def _endPath(self): + p0 = self._getCurrentPoint() + if p0 != self.__startPoint: + # Green theorem is not defined on open contours. + raise NotImplementedError +''', end='', file=file) + + for n in (1, 2, 3): + + if n == 1: + print(''' + def _lineTo(self, p1): + x0,y0 = self._getCurrentPoint() + x1,y1 = p1 +''', file=file) + elif n == 2: + print(''' + def _qCurveToOne(self, p1, p2): + x0,y0 = self._getCurrentPoint() + x1,y1 = p1 + x2,y2 = p2 +''', file=file) + elif n == 3: + print(''' + def _curveToOne(self, p1, p2, p3): + x0,y0 = self._getCurrentPoint() + x1,y1 = p1 + x2,y2 = p2 + x3,y3 = p3 +''', file=file) + subs = {P[i][j]: [X, Y][j][i] for i in range(n+1) for j in range(2)} + greens = [green(f, BezierCurve[n]) for name,f in funcs] + greens = [sp.gcd_terms(f.collect(sum(P,()))) for f in greens] # Optimize + greens = [f.subs(subs) for f in greens] # Convert to p to x/y + defs, exprs = sp.cse(greens, + optimizations='basic', + symbols=(sp.Symbol('r%d'%i) for i in count())) + for name,value in defs: + print(' %s = %s' % (name, value), file=file) + print(file=file) + for name,value in zip([f[0] for f in funcs], exprs): + print(' self.%s += %s' % (name, value), file=file) + + print(''' +if __name__ == '__main__': + from fontTools.misc.symfont import x, y, printGreenPen + printGreenPen('%s', ['''%penName, file=file) + for name,f in funcs: + print(" ('%s', %s)," % (name, str(f)), file=file) + print(' ])', file=file) + + +if __name__ == '__main__': + pen = AreaPen() + pen.moveTo((100,100)) + pen.lineTo((100,200)) + pen.lineTo((200,200)) + pen.curveTo((200,250),(300,300),(250,350)) + pen.lineTo((200,100)) + pen.closePath() + print(pen.value) diff --git a/Lib/fontTools/misc/testTools.py b/Lib/fontTools/misc/testTools.py new file mode 100644 index 0000000..8a37b7e --- /dev/null +++ b/Lib/fontTools/misc/testTools.py @@ -0,0 +1,183 @@ +"""Helpers for writing unit tests.""" + +from __future__ import (print_function, division, absolute_import, + unicode_literals) +import collections +import os +import shutil +import sys +import tempfile +from unittest import TestCase as _TestCase +from fontTools.misc.py23 import * +from fontTools.misc.xmlWriter import XMLWriter + + +def parseXML(xmlSnippet): + """Parses a snippet of XML. + + Input can be either a single string (unicode or UTF-8 bytes), or a + a sequence of strings. + + The result is in the same format that would be returned by + XMLReader, but the parser imposes no constraints on the root + element so it can be called on small snippets of TTX files. + """ + # To support snippets with multiple elements, we add a fake root. + reader = TestXMLReader_() + xml = b"" + if isinstance(xmlSnippet, bytes): + xml += xmlSnippet + elif isinstance(xmlSnippet, unicode): + xml += tobytes(xmlSnippet, 'utf-8') + elif isinstance(xmlSnippet, collections.Iterable): + xml += b"".join(tobytes(s, 'utf-8') for s in xmlSnippet) + else: + raise TypeError("expected string or sequence of strings; found %r" + % type(xmlSnippet).__name__) + xml += b"" + reader.parser.Parse(xml, 0) + return reader.root[2] + + +class FakeFont: + def __init__(self, glyphs): + self.glyphOrder_ = glyphs + self.reverseGlyphOrderDict_ = {g: i for i, g in enumerate(glyphs)} + self.lazy = False + self.tables = {} + + def __getitem__(self, tag): + return self.tables[tag] + + def __setitem__(self, tag, table): + self.tables[tag] = table + + def get(self, tag, default=None): + return self.tables.get(tag, default) + + def getGlyphID(self, name): + return self.reverseGlyphOrderDict_[name] + + def getGlyphName(self, glyphID): + if glyphID < len(self.glyphOrder_): + return self.glyphOrder_[glyphID] + else: + return "glyph%.5d" % glyphID + + def getGlyphOrder(self): + return self.glyphOrder_ + + def getReverseGlyphMap(self): + return self.reverseGlyphOrderDict_ + + +class TestXMLReader_(object): + def __init__(self): + from xml.parsers.expat import ParserCreate + self.parser = ParserCreate() + self.parser.StartElementHandler = self.startElement_ + self.parser.EndElementHandler = self.endElement_ + self.parser.CharacterDataHandler = self.addCharacterData_ + self.root = None + self.stack = [] + + def startElement_(self, name, attrs): + element = (name, attrs, []) + if self.stack: + self.stack[-1][2].append(element) + else: + self.root = element + self.stack.append(element) + + def endElement_(self, name): + self.stack.pop() + + def addCharacterData_(self, data): + self.stack[-1][2].append(data) + + +def makeXMLWriter(newlinestr='\n'): + # don't write OS-specific new lines + writer = XMLWriter(BytesIO(), newlinestr=newlinestr) + # erase XML declaration + writer.file.seek(0) + writer.file.truncate() + return writer + + +def getXML(func, ttFont=None): + """Call the passed toXML function and return the written content as a + list of lines (unicode strings). + Result is stripped of XML declaration and OS-specific newline characters. + """ + writer = makeXMLWriter() + func(writer, ttFont) + xml = writer.file.getvalue().decode("utf-8") + # toXML methods must always end with a writer.newline() + assert xml.endswith("\n") + return xml.splitlines() + + +class MockFont(object): + """A font-like object that automatically adds any looked up glyphname + to its glyphOrder.""" + + def __init__(self): + self._glyphOrder = ['.notdef'] + + class AllocatingDict(dict): + def __missing__(reverseDict, key): + self._glyphOrder.append(key) + gid = len(reverseDict) + reverseDict[key] = gid + return gid + self._reverseGlyphOrder = AllocatingDict({'.notdef': 0}) + self.lazy = False + + def getGlyphID(self, glyph, requireReal=None): + gid = self._reverseGlyphOrder[glyph] + return gid + + def getReverseGlyphMap(self): + return self._reverseGlyphOrder + + def getGlyphName(self, gid): + return self._glyphOrder[gid] + + def getGlyphOrder(self): + return self._glyphOrder + + +class TestCase(_TestCase): + + def __init__(self, methodName): + _TestCase.__init__(self, methodName) + # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, + # and fires deprecation warnings if a program uses the old name. + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + + +class DataFilesHandler(TestCase): + + def setUp(self): + self.tempdir = None + self.num_tempfiles = 0 + + def tearDown(self): + if self.tempdir: + shutil.rmtree(self.tempdir) + + def getpath(self, testfile): + folder = os.path.dirname(sys.modules[self.__module__].__file__) + return os.path.join(folder, "data", testfile) + + def temp_dir(self): + if not self.tempdir: + self.tempdir = tempfile.mkdtemp() + + def temp_font(self, font_path, file_name): + self.temp_dir() + temppath = os.path.join(self.tempdir, file_name) + shutil.copy2(font_path, temppath) + return temppath diff --git a/Lib/fontTools/misc/textTools.py b/Lib/fontTools/misc/textTools.py index d6e6a2f..08c5990 100644 --- a/Lib/fontTools/misc/textTools.py +++ b/Lib/fontTools/misc/textTools.py @@ -7,12 +7,15 @@ import ast import string +# alias kept for backward compatibility safeEval = ast.literal_eval + def readHex(content): """Convert a list of hex strings to binary data.""" return deHexStr(strjoin(chunk for chunk in content if isinstance(chunk, basestring))) + def deHexStr(hexdata): """Convert a hex string to binary data.""" hexdata = strjoin(hexdata.split()) @@ -64,12 +67,37 @@ def binary2num(bin): def caselessSort(alist): - """Return a sorted copy of a list. If there are only strings + """Return a sorted copy of a list. If there are only strings in the list, it will not consider case. """ - + try: return sorted(alist, key=lambda a: (a.lower(), a)) except TypeError: return sorted(alist) + +def pad(data, size): + r""" Pad byte string 'data' with null bytes until its length is a + multiple of 'size'. + + >>> len(pad(b'abcd', 4)) + 4 + >>> len(pad(b'abcde', 2)) + 6 + >>> len(pad(b'abcde', 4)) + 8 + >>> pad(b'abcdef', 4) == b'abcdef\x00\x00' + True + """ + data = tobytes(data) + if size > 1: + remainder = len(data) % size + if remainder: + data += b"\0" * (size - remainder) + return data + + +if __name__ == "__main__": + import doctest, sys + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/misc/timeTools.py b/Lib/fontTools/misc/timeTools.py new file mode 100644 index 0000000..c30a377 --- /dev/null +++ b/Lib/fontTools/misc/timeTools.py @@ -0,0 +1,64 @@ +"""fontTools.misc.timeTools.py -- tools for working with OpenType timestamps. +""" + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +import os +import time +import calendar + + +epoch_diff = calendar.timegm((1904, 1, 1, 0, 0, 0, 0, 0, 0)) + +DAYNAMES = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] +MONTHNAMES = [None, "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + + +def asctime(t=None): + """ + Convert a tuple or struct_time representing a time as returned by gmtime() + or localtime() to a 24-character string of the following form: + + >>> asctime(time.gmtime(0)) + 'Thu Jan 1 00:00:00 1970' + + If t is not provided, the current time as returned by localtime() is used. + Locale information is not used by asctime(). + + This is meant to normalise the output of the built-in time.asctime() across + different platforms and Python versions. + In Python 3.x, the day of the month is right-justified, whereas on Windows + Python 2.7 it is padded with zeros. + + See https://github.com/behdad/fonttools/issues/455 + """ + if t is None: + t = time.localtime() + s = "%s %s %2s %s" % ( + DAYNAMES[t.tm_wday], MONTHNAMES[t.tm_mon], t.tm_mday, + time.strftime("%H:%M:%S %Y", t)) + return s + + +def timestampToString(value): + return asctime(time.gmtime(max(0, value + epoch_diff))) + +def timestampFromString(value): + return calendar.timegm(time.strptime(value)) - epoch_diff + +def timestampNow(): + # https://reproducible-builds.org/specs/source-date-epoch/ + source_date_epoch = os.environ.get("SOURCE_DATE_EPOCH") + if source_date_epoch is not None: + return int(source_date_epoch) - epoch_diff + return int(time.time() - epoch_diff) + +def timestampSinceEpoch(value): + return int(value - epoch_diff) + + +if __name__ == "__main__": + import sys + import doctest + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/misc/transform.py b/Lib/fontTools/misc/transform.py index be7d21a..1296571 100644 --- a/Lib/fontTools/misc/transform.py +++ b/Lib/fontTools/misc/transform.py @@ -79,7 +79,7 @@ class Transform(object): >>> t.scale(2) >>> t.scale(2.5, 5.5) - + >>> >>> t.scale(2, 3).transformPoint((100, 100)) (200, 300) @@ -172,7 +172,7 @@ class Transform(object): >>> import math >>> t = Transform() >>> t.skew(math.pi / 4) - + >>> """ import math @@ -282,9 +282,9 @@ class Transform(object): >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6) >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3) >>> t1 - + >>> t2 - + >>> t1 == t2 0 >>> @@ -306,23 +306,44 @@ class Transform(object): >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6) >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3) >>> t1 - + >>> t2 - + >>> d = {t1: None} >>> d - {: None} + {: None} >>> d[t2] Traceback (most recent call last): File "", line 1, in ? - KeyError: + KeyError: >>> """ return hash(self.__affine) + def __bool__(self): + """Returns True if transform is not identity, False otherwise. + >>> bool(Identity) + False + >>> bool(Transform()) + False + >>> bool(Scale(1.)) + False + >>> bool(Scale(2)) + True + >>> bool(Offset()) + False + >>> bool(Offset(0)) + False + >>> bool(Offset(2)) + True + """ + return self.__affine != Identity.__affine + + __nonzero__ = __bool__ + def __repr__(self): - return "<%s [%s %s %s %s %s %s]>" % ((self.__class__.__name__,) \ - + tuple(map(str, self.__affine))) + return "<%s [%g %g %g %g %g %g]>" % ((self.__class__.__name__,) \ + + self.__affine) Identity = Transform() @@ -352,5 +373,6 @@ def Scale(x, y=None): if __name__ == "__main__": + import sys import doctest - doctest.testmod() + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/misc/xmlReader.py b/Lib/fontTools/misc/xmlReader.py index 85dd441..e9b5cfd 100644 --- a/Lib/fontTools/misc/xmlReader.py +++ b/Lib/fontTools/misc/xmlReader.py @@ -3,40 +3,65 @@ from fontTools.misc.py23 import * from fontTools import ttLib from fontTools.misc.textTools import safeEval from fontTools.ttLib.tables.DefaultTable import DefaultTable +import sys import os +import logging +log = logging.getLogger(__name__) + class TTXParseError(Exception): pass BUFSIZE = 0x4000 class XMLReader(object): - - def __init__(self, fileName, ttFont, progress=None, quiet=False): + + def __init__(self, fileOrPath, ttFont, progress=None, quiet=None, contentOnly=False): + if fileOrPath == '-': + fileOrPath = sys.stdin + if not hasattr(fileOrPath, "read"): + self.file = open(fileOrPath, "rb") + self._closeStream = True + else: + # assume readable file object + self.file = fileOrPath + self._closeStream = False self.ttFont = ttFont - self.fileName = fileName self.progress = progress - self.quiet = quiet + if quiet is not None: + from fontTools.misc.loggingTools import deprecateArgument + deprecateArgument("quiet", "configure logging instead") + self.quiet = quiet self.root = None self.contentStack = [] + self.contentOnly = contentOnly self.stackSize = 0 - - def read(self): + + def read(self, rootless=False): + if rootless: + self.stackSize += 1 if self.progress: - import stat - self.progress.set(0, os.stat(self.fileName)[stat.ST_SIZE] // 100 or 1) - file = open(self.fileName) - self._parseFile(file) - file.close() - + self.file.seek(0, 2) + fileSize = self.file.tell() + self.progress.set(0, fileSize // 100 or 1) + self.file.seek(0) + self._parseFile(self.file) + if self._closeStream: + self.close() + if rootless: + self.stackSize -= 1 + + def close(self): + self.file.close() + def _parseFile(self, file): from xml.parsers.expat import ParserCreate parser = ParserCreate() parser.StartElementHandler = self._startElementHandler parser.EndElementHandler = self._endElementHandler parser.CharacterDataHandler = self._characterDataHandler - + pos = 0 while True: chunk = file.read(BUFSIZE) @@ -47,10 +72,26 @@ class XMLReader(object): if self.progress: self.progress.set(pos // 100) parser.Parse(chunk, 0) - + def _startElementHandler(self, name, attrs): + if self.stackSize == 1 and self.contentOnly: + # We already know the table we're parsing, skip + # parsing the table tag and continue to + # stack '2' which begins parsing content + self.contentStack.append([]) + self.stackSize = 2 + return stackSize = self.stackSize self.stackSize = stackSize + 1 + subFile = attrs.get("src") + if subFile is not None: + if hasattr(self.file, 'name'): + # if file has a name, get its parent directory + dirname = os.path.dirname(self.file.name) + else: + # else fall back to using the current working directory + dirname = os.getcwd() + subFile = os.path.join(dirname, subFile) if not stackSize: if name != "ttFont": raise TTXParseError("illegal root tag: %s" % name) @@ -61,22 +102,16 @@ class XMLReader(object): self.ttFont.sfntVersion = sfntVersion self.contentStack.append([]) elif stackSize == 1: - subFile = attrs.get("src") if subFile is not None: - subFile = os.path.join(os.path.dirname(self.fileName), subFile) - subReader = XMLReader(subFile, self.ttFont, self.progress, self.quiet) + subReader = XMLReader(subFile, self.ttFont, self.progress) subReader.read() self.contentStack.append([]) return tag = ttLib.xmlToTag(name) msg = "Parsing '%s' table..." % tag if self.progress: - self.progress.setlabel(msg) - elif self.ttFont.verbose: - ttLib.debugmsg(msg) - else: - if not self.quiet: - print(msg) + self.progress.setLabel(msg) + log.info(msg) if tag == "GlyphOrder": tableClass = ttLib.GlyphOrder elif "ERROR" in attrs or ('raw' in attrs and safeEval(attrs['raw'])): @@ -93,6 +128,11 @@ class XMLReader(object): self.currentTable = tableClass(tag) self.ttFont[tag] = self.currentTable self.contentStack.append([]) + elif stackSize == 2 and subFile is not None: + subReader = XMLReader(subFile, self.ttFont, self.progress, contentOnly=True) + subReader.read() + self.contentStack.append([]) + self.root = subReader.root elif stackSize == 2: self.contentStack.append([]) self.root = (name, attrs, self.contentStack[-1]) @@ -100,33 +140,33 @@ class XMLReader(object): l = [] self.contentStack[-1].append((name, attrs, l)) self.contentStack.append(l) - + def _characterDataHandler(self, data): if self.stackSize > 1: self.contentStack[-1].append(data) - + def _endElementHandler(self, name): self.stackSize = self.stackSize - 1 del self.contentStack[-1] - if self.stackSize == 1: - self.root = None - elif self.stackSize == 2: - name, attrs, content = self.root - self.currentTable.fromXML(name, attrs, content, self.ttFont) - self.root = None + if not self.contentOnly: + if self.stackSize == 1: + self.root = None + elif self.stackSize == 2: + name, attrs, content = self.root + self.currentTable.fromXML(name, attrs, content, self.ttFont) + self.root = None class ProgressPrinter(object): - + def __init__(self, title, maxval=100): print(title) - + def set(self, val, maxval=None): pass - + def increment(self, val=1): pass - + def setLabel(self, text): print(text) - diff --git a/Lib/fontTools/misc/xmlWriter.py b/Lib/fontTools/misc/xmlWriter.py index b067c2d..3f30ab5 100644 --- a/Lib/fontTools/misc/xmlWriter.py +++ b/Lib/fontTools/misc/xmlWriter.py @@ -3,35 +3,57 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * import sys +import os import string INDENT = " " class XMLWriter(object): - - def __init__(self, fileOrPath, indentwhite=INDENT, idlefunc=None): + + def __init__(self, fileOrPath, indentwhite=INDENT, idlefunc=None, encoding="utf_8", + newlinestr=None): + if encoding.lower().replace('-','').replace('_','') != 'utf8': + raise Exception('Only UTF-8 encoding is supported.') + if fileOrPath == '-': + fileOrPath = sys.stdout if not hasattr(fileOrPath, "write"): - try: - # Python3 has encoding support. - self.file = open(fileOrPath, "w", encoding="utf-8") - except TypeError: - self.file = open(fileOrPath, "w") + self.filename = fileOrPath + self.file = open(fileOrPath, "wb") + self._closeStream = True else: + self.filename = None # assume writable file object self.file = fileOrPath - self.indentwhite = indentwhite + self._closeStream = False + + # Figure out if writer expects bytes or unicodes + try: + # The bytes check should be first. See: + # https://github.com/behdad/fonttools/pull/233 + self.file.write(b'') + self.totype = tobytes + except TypeError: + # This better not fail. + self.file.write(tounicode('')) + self.totype = tounicode + self.indentwhite = self.totype(indentwhite) + if newlinestr is None: + self.newlinestr = self.totype(os.linesep) + else: + self.newlinestr = self.totype(newlinestr) self.indentlevel = 0 self.stack = [] self.needindent = 1 self.idlefunc = idlefunc self.idlecounter = 0 - self._writeraw('') + self._writeraw('') self.newline() - + def close(self): - self.file.close() - + if self._closeStream: + self.file.close() + def write(self, string, indent=True): """Writes text.""" self._writeraw(escape(string), indent=indent) @@ -47,31 +69,28 @@ class XMLWriter(object): 'latin-1'.""" self._writeraw(escape8bit(data), strip=strip) - def write16bit(self, data, strip=False): - self._writeraw(escape16bit(data), strip=strip) - def write_noindent(self, string): """Writes text without indentation.""" self._writeraw(escape(string), indent=False) - + def _writeraw(self, data, indent=True, strip=False): """Writes bytes, possibly indented.""" if indent and self.needindent: self.file.write(self.indentlevel * self.indentwhite) self.needindent = 0 - s = tostr(data, encoding="utf-8") + s = self.totype(data, encoding="utf_8") if (strip): s = s.strip() self.file.write(s) - + def newline(self): - self.file.write("\n") + self.file.write(self.newlinestr) self.needindent = 1 idlecounter = self.idlecounter if not idlecounter % 100 and self.idlefunc is not None: self.idlefunc() self.idlecounter = idlecounter + 1 - + def comment(self, data): data = escape(data) lines = data.split("\n") @@ -80,26 +99,26 @@ class XMLWriter(object): self.newline() self._writeraw(" " + line) self._writeraw(" -->") - + def simpletag(self, _TAG_, *args, **kwargs): attrdata = self.stringifyattrs(*args, **kwargs) data = "<%s%s/>" % (_TAG_, attrdata) self._writeraw(data) - + def begintag(self, _TAG_, *args, **kwargs): attrdata = self.stringifyattrs(*args, **kwargs) data = "<%s%s>" % (_TAG_, attrdata) self._writeraw(data) self.stack.append(_TAG_) self.indent() - + def endtag(self, _TAG_): assert self.stack and self.stack[-1] == _TAG_, "nonmatching endtag" del self.stack[-1] self.dedent() data = "" % _TAG_ self._writeraw(data) - + def dumphex(self, data): linelength = 16 hexlinelength = linelength * 2 @@ -113,14 +132,14 @@ class XMLWriter(object): white = " " self._writeraw(line) self.newline() - + def indent(self): self.indentlevel = self.indentlevel + 1 - + def dedent(self): assert self.indentlevel > 0 self.indentlevel = self.indentlevel - 1 - + def stringifyattrs(self, *args, **kwargs): if kwargs: assert not args @@ -132,15 +151,18 @@ class XMLWriter(object): return "" data = "" for attr, value in attributes: - data = data + ' %s="%s"' % (attr, escapeattr(str(value))) + if not isinstance(value, (bytes, unicode)): + value = str(value) + data = data + ' %s="%s"' % (attr, escapeattr(value)) return data - + def escape(data): - data = tostr(data, 'utf-8') + data = tostr(data, 'utf_8') data = data.replace("&", "&") data = data.replace("<", "<") data = data.replace(">", ">") + data = data.replace("\r", " ") return data def escapeattr(data): @@ -158,24 +180,6 @@ def escape8bit(data): return "&#" + repr(n) + ";" return strjoin(map(escapechar, data.decode('latin-1'))) -def escape16bit(data): - import array - a = array.array("H") - a.fromstring(data) - if sys.byteorder != "big": - a.byteswap() - def escapenum(n, amp=byteord("&"), lt=byteord("<")): - if n == amp: - return "&" - elif n == lt: - return "<" - elif 32 <= n <= 127: - return chr(n) - else: - return "&#" + repr(n) + ";" - return strjoin(map(escapenum, a)) - - def hexStr(s): h = string.hexdigits r = '' diff --git a/Lib/fontTools/mtiLib/__init__.py b/Lib/fontTools/mtiLib/__init__.py new file mode 100644 index 0000000..d4f4880 --- /dev/null +++ b/Lib/fontTools/mtiLib/__init__.py @@ -0,0 +1,1196 @@ +#!/usr/bin/python + +# FontDame-to-FontTools for OpenType Layout tables +# +# Source language spec is available at: +# http://monotype.github.io/OpenType_Table_Source/otl_source.html +# https://github.com/Monotype/OpenType_Table_Source/ + +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.misc.py23 import * +from fontTools import ttLib +from fontTools.ttLib.tables._c_m_a_p import cmap_classes +from fontTools.ttLib.tables import otTables as ot +from fontTools.ttLib.tables.otBase import ValueRecord, valueRecordFormatDict +from fontTools.otlLib import builder as otl +from contextlib import contextmanager +from operator import setitem +import logging + +class MtiLibError(Exception): pass +class ReferenceNotFoundError(MtiLibError): pass +class FeatureNotFoundError(ReferenceNotFoundError): pass +class LookupNotFoundError(ReferenceNotFoundError): pass + + +log = logging.getLogger("fontTools.mtiLib") + + +def makeGlyph(s): + if s[:2] in ['U ', 'u ']: + return ttLib.TTFont._makeGlyphName(int(s[2:], 16)) + elif s[:2] == '# ': + return "glyph%.5d" % int(s[2:]) + assert s.find(' ') < 0, "Space found in glyph name: %s" % s + assert s, "Glyph name is empty" + return s + +def makeGlyphs(l): + return [makeGlyph(g) for g in l] + +def mapLookup(sym, mapping): + # Lookups are addressed by name. So resolved them using a map if available. + # Fallback to parsing as lookup index if a map isn't provided. + if mapping is not None: + try: + idx = mapping[sym] + except KeyError: + raise LookupNotFoundError(sym) + else: + idx = int(sym) + return idx + +def mapFeature(sym, mapping): + # Features are referenced by index according the spec. So, if symbol is an + # integer, use it directly. Otherwise look up in the map if provided. + try: + idx = int(sym) + except ValueError: + try: + idx = mapping[sym] + except KeyError: + raise FeatureNotFoundError(sym) + return idx + +def setReference(mapper, mapping, sym, setter, collection, key): + try: + mapped = mapper(sym, mapping) + except ReferenceNotFoundError as e: + try: + if mapping is not None: + mapping.addDeferredMapping(lambda ref: setter(collection, key, ref), sym, e) + return + except AttributeError: + pass + raise + setter(collection, key, mapped) + +class DeferredMapping(dict): + + def __init__(self): + self._deferredMappings = [] + + def addDeferredMapping(self, setter, sym, e): + log.debug("Adding deferred mapping for symbol '%s' %s", sym, type(e).__name__) + self._deferredMappings.append((setter,sym, e)) + + def applyDeferredMappings(self): + for setter,sym,e in self._deferredMappings: + log.debug("Applying deferred mapping for symbol '%s' %s", sym, type(e).__name__) + try: + mapped = self[sym] + except KeyError: + raise e + setter(mapped) + log.debug("Set to %s", mapped) + self._deferredMappings = [] + + +def parseScriptList(lines, featureMap=None): + self = ot.ScriptList() + records = [] + with lines.between('script table'): + for line in lines: + while len(line) < 4: + line.append('') + scriptTag, langSysTag, defaultFeature, features = line + log.debug("Adding script %s language-system %s", scriptTag, langSysTag) + + langSys = ot.LangSys() + langSys.LookupOrder = None + if defaultFeature: + setReference(mapFeature, featureMap, defaultFeature, setattr, langSys, 'ReqFeatureIndex') + else: + langSys.ReqFeatureIndex = 0xFFFF + syms = stripSplitComma(features) + langSys.FeatureIndex = theList = [3] * len(syms) + for i,sym in enumerate(syms): + setReference(mapFeature, featureMap, sym, setitem, theList, i) + langSys.FeatureCount = len(langSys.FeatureIndex) + + script = [s for s in records if s.ScriptTag == scriptTag] + if script: + script = script[0].Script + else: + scriptRec = ot.ScriptRecord() + scriptRec.ScriptTag = scriptTag + scriptRec.Script = ot.Script() + records.append(scriptRec) + script = scriptRec.Script + script.DefaultLangSys = None + script.LangSysRecord = [] + script.LangSysCount = 0 + + if langSysTag == 'default': + script.DefaultLangSys = langSys + else: + langSysRec = ot.LangSysRecord() + langSysRec.LangSysTag = langSysTag + ' '*(4 - len(langSysTag)) + langSysRec.LangSys = langSys + script.LangSysRecord.append(langSysRec) + script.LangSysCount = len(script.LangSysRecord) + + for script in records: + script.Script.LangSysRecord = sorted(script.Script.LangSysRecord, key=lambda rec: rec.LangSysTag) + self.ScriptRecord = sorted(records, key=lambda rec: rec.ScriptTag) + self.ScriptCount = len(self.ScriptRecord) + return self + +def parseFeatureList(lines, lookupMap=None, featureMap=None): + self = ot.FeatureList() + self.FeatureRecord = [] + with lines.between('feature table'): + for line in lines: + name, featureTag, lookups = line + if featureMap is not None: + assert name not in featureMap, "Duplicate feature name: %s" % name + featureMap[name] = len(self.FeatureRecord) + # If feature name is integer, make sure it matches its index. + try: + assert int(name) == len(self.FeatureRecord), "%d %d" % (name, len(self.FeatureRecord)) + except ValueError: + pass + featureRec = ot.FeatureRecord() + featureRec.FeatureTag = featureTag + featureRec.Feature = ot.Feature() + self.FeatureRecord.append(featureRec) + feature = featureRec.Feature + feature.FeatureParams = None + syms = stripSplitComma(lookups) + feature.LookupListIndex = theList = [None] * len(syms) + for i,sym in enumerate(syms): + setReference(mapLookup, lookupMap, sym, setitem, theList, i) + feature.LookupCount = len(feature.LookupListIndex) + + self.FeatureCount = len(self.FeatureRecord) + return self + +def parseLookupFlags(lines): + flags = 0 + filterset = None + allFlags = [ + 'righttoleft', + 'ignorebaseglyphs', + 'ignoreligatures', + 'ignoremarks', + 'markattachmenttype', + 'markfiltertype', + ] + while lines.peeks()[0].lower() in allFlags: + line = next(lines) + flag = { + 'righttoleft': 0x0001, + 'ignorebaseglyphs': 0x0002, + 'ignoreligatures': 0x0004, + 'ignoremarks': 0x0008, + }.get(line[0].lower()) + if flag: + assert line[1].lower() in ['yes', 'no'], line[1] + if line[1].lower() == 'yes': + flags |= flag + continue + if line[0].lower() == 'markattachmenttype': + flags |= int(line[1]) << 8 + continue + if line[0].lower() == 'markfiltertype': + flags |= 0x10 + filterset = int(line[1]) + return flags, filterset + +def parseSingleSubst(lines, font, _lookupMap=None): + mapping = {} + for line in lines: + assert len(line) == 2, line + line = makeGlyphs(line) + mapping[line[0]] = line[1] + return otl.buildSingleSubstSubtable(mapping) + +def parseMultiple(lines, font, _lookupMap=None): + mapping = {} + for line in lines: + line = makeGlyphs(line) + mapping[line[0]] = line[1:] + return otl.buildMultipleSubstSubtable(mapping) + +def parseAlternate(lines, font, _lookupMap=None): + mapping = {} + for line in lines: + line = makeGlyphs(line) + mapping[line[0]] = line[1:] + return otl.buildAlternateSubstSubtable(mapping) + +def parseLigature(lines, font, _lookupMap=None): + mapping = {} + for line in lines: + assert len(line) >= 2, line + line = makeGlyphs(line) + mapping[tuple(line[1:])] = line[0] + return otl.buildLigatureSubstSubtable(mapping) + +def parseSinglePos(lines, font, _lookupMap=None): + values = {} + for line in lines: + assert len(line) == 3, line + w = line[0].title().replace(' ', '') + assert w in valueRecordFormatDict + g = makeGlyph(line[1]) + v = int(line[2]) + if g not in values: + values[g] = ValueRecord() + assert not hasattr(values[g], w), (g, w) + setattr(values[g], w, v) + return otl.buildSinglePosSubtable(values, font.getReverseGlyphMap()) + +def parsePair(lines, font, _lookupMap=None): + self = ot.PairPos() + self.ValueFormat1 = self.ValueFormat2 = 0 + typ = lines.peeks()[0].split()[0].lower() + if typ in ('left', 'right'): + self.Format = 1 + values = {} + for line in lines: + assert len(line) == 4, line + side = line[0].split()[0].lower() + assert side in ('left', 'right'), side + what = line[0][len(side):].title().replace(' ', '') + mask = valueRecordFormatDict[what][0] + glyph1, glyph2 = makeGlyphs(line[1:3]) + value = int(line[3]) + if not glyph1 in values: values[glyph1] = {} + if not glyph2 in values[glyph1]: values[glyph1][glyph2] = (ValueRecord(),ValueRecord()) + rec2 = values[glyph1][glyph2] + if side == 'left': + self.ValueFormat1 |= mask + vr = rec2[0] + else: + self.ValueFormat2 |= mask + vr = rec2[1] + assert not hasattr(vr, what), (vr, what) + setattr(vr, what, value) + self.Coverage = makeCoverage(set(values.keys()), font) + self.PairSet = [] + for glyph1 in self.Coverage.glyphs: + values1 = values[glyph1] + pairset = ot.PairSet() + records = pairset.PairValueRecord = [] + for glyph2 in sorted(values1.keys(), key=font.getGlyphID): + values2 = values1[glyph2] + pair = ot.PairValueRecord() + pair.SecondGlyph = glyph2 + pair.Value1 = values2[0] + pair.Value2 = values2[1] if self.ValueFormat2 else None + records.append(pair) + pairset.PairValueCount = len(pairset.PairValueRecord) + self.PairSet.append(pairset) + self.PairSetCount = len(self.PairSet) + elif typ.endswith('class'): + self.Format = 2 + classDefs = [None, None] + while lines.peeks()[0].endswith("class definition begin"): + typ = lines.peek()[0][:-len("class definition begin")].lower() + idx,klass = { + 'first': (0,ot.ClassDef1), + 'second': (1,ot.ClassDef2), + }[typ] + assert classDefs[idx] is None + classDefs[idx] = parseClassDef(lines, font, klass=klass) + self.ClassDef1, self.ClassDef2 = classDefs + self.Class1Count, self.Class2Count = (1+max(c.classDefs.values()) for c in classDefs) + self.Class1Record = [ot.Class1Record() for i in range(self.Class1Count)] + for rec1 in self.Class1Record: + rec1.Class2Record = [ot.Class2Record() for j in range(self.Class2Count)] + for rec2 in rec1.Class2Record: + rec2.Value1 = ValueRecord() + rec2.Value2 = ValueRecord() + for line in lines: + assert len(line) == 4, line + side = line[0].split()[0].lower() + assert side in ('left', 'right'), side + what = line[0][len(side):].title().replace(' ', '') + mask = valueRecordFormatDict[what][0] + class1, class2, value = (int(x) for x in line[1:4]) + rec2 = self.Class1Record[class1].Class2Record[class2] + if side == 'left': + self.ValueFormat1 |= mask + vr = rec2.Value1 + else: + self.ValueFormat2 |= mask + vr = rec2.Value2 + assert not hasattr(vr, what), (vr, what) + setattr(vr, what, value) + for rec1 in self.Class1Record: + for rec2 in rec1.Class2Record: + rec2.Value1 = ValueRecord(self.ValueFormat1, rec2.Value1) + rec2.Value2 = ValueRecord(self.ValueFormat2, rec2.Value2) \ + if self.ValueFormat2 else None + + self.Coverage = makeCoverage(set(self.ClassDef1.classDefs.keys()), font) + else: + assert 0, typ + return self + +def parseKernset(lines, font, _lookupMap=None): + typ = lines.peeks()[0].split()[0].lower() + if typ in ('left', 'right'): + with lines.until(("firstclass definition begin", "secondclass definition begin")): + return parsePair(lines, font) + return parsePair(lines, font) + +def makeAnchor(data, klass=ot.Anchor): + assert len(data) <= 2 + anchor = klass() + anchor.Format = 1 + anchor.XCoordinate,anchor.YCoordinate = intSplitComma(data[0]) + if len(data) > 1 and data[1] != '': + anchor.Format = 2 + anchor.AnchorPoint = int(data[1]) + return anchor + +def parseCursive(lines, font, _lookupMap=None): + records = {} + for line in lines: + assert len(line) in [3,4], line + idx,klass = { + 'entry': (0,ot.EntryAnchor), + 'exit': (1,ot.ExitAnchor), + }[line[0]] + glyph = makeGlyph(line[1]) + if glyph not in records: + records[glyph] = [None,None] + assert records[glyph][idx] is None, (glyph, idx) + records[glyph][idx] = makeAnchor(line[2:], klass) + return otl.buildCursivePosSubtable(records, font.getReverseGlyphMap()) + +def makeMarkRecords(data, coverage, c): + records = [] + for glyph in coverage.glyphs: + klass, anchor = data[glyph] + record = c.MarkRecordClass() + record.Class = klass + setattr(record, c.MarkAnchor, anchor) + records.append(record) + return records + +def makeBaseRecords(data, coverage, c, classCount): + records = [] + idx = {} + for glyph in coverage.glyphs: + idx[glyph] = len(records) + record = c.BaseRecordClass() + anchors = [None] * classCount + setattr(record, c.BaseAnchor, anchors) + records.append(record) + for (glyph,klass),anchor in data.items(): + record = records[idx[glyph]] + anchors = getattr(record, c.BaseAnchor) + assert anchors[klass] is None, (glyph, klass) + anchors[klass] = anchor + return records + +def makeLigatureRecords(data, coverage, c, classCount): + records = [None] * len(coverage.glyphs) + idx = {g:i for i,g in enumerate(coverage.glyphs)} + + for (glyph,klass,compIdx,compCount),anchor in data.items(): + record = records[idx[glyph]] + if record is None: + record = records[idx[glyph]] = ot.LigatureAttach() + record.ComponentCount = compCount + record.ComponentRecord = [ot.ComponentRecord() for i in range(compCount)] + for compRec in record.ComponentRecord: + compRec.LigatureAnchor = [None] * classCount + assert record.ComponentCount == compCount, (glyph, record.ComponentCount, compCount) + + anchors = record.ComponentRecord[compIdx - 1].LigatureAnchor + assert anchors[klass] is None, (glyph, compIdx, klass) + anchors[klass] = anchor + return records + +def parseMarkToSomething(lines, font, c): + self = c.Type() + self.Format = 1 + markData = {} + baseData = {} + Data = { + 'mark': (markData, c.MarkAnchorClass), + 'base': (baseData, c.BaseAnchorClass), + 'ligature': (baseData, c.BaseAnchorClass), + } + maxKlass = 0 + for line in lines: + typ = line[0] + assert typ in ('mark', 'base', 'ligature') + glyph = makeGlyph(line[1]) + data, anchorClass = Data[typ] + extraItems = 2 if typ == 'ligature' else 0 + extras = tuple(int(i) for i in line[2:2+extraItems]) + klass = int(line[2+extraItems]) + anchor = makeAnchor(line[3+extraItems:], anchorClass) + if typ == 'mark': + key,value = glyph,(klass,anchor) + else: + key,value = ((glyph,klass)+extras),anchor + assert key not in data, key + data[key] = value + maxKlass = max(maxKlass, klass) + + # Mark + markCoverage = makeCoverage(set(markData.keys()), font, c.MarkCoverageClass) + markArray = c.MarkArrayClass() + markRecords = makeMarkRecords(markData, markCoverage, c) + setattr(markArray, c.MarkRecord, markRecords) + setattr(markArray, c.MarkCount, len(markRecords)) + setattr(self, c.MarkCoverage, markCoverage) + setattr(self, c.MarkArray, markArray) + self.ClassCount = maxKlass + 1 + + # Base + self.classCount = 0 if not baseData else 1+max(k[1] for k,v in baseData.items()) + baseCoverage = makeCoverage(set([k[0] for k in baseData.keys()]), font, c.BaseCoverageClass) + baseArray = c.BaseArrayClass() + if c.Base == 'Ligature': + baseRecords = makeLigatureRecords(baseData, baseCoverage, c, self.classCount) + else: + baseRecords = makeBaseRecords(baseData, baseCoverage, c, self.classCount) + setattr(baseArray, c.BaseRecord, baseRecords) + setattr(baseArray, c.BaseCount, len(baseRecords)) + setattr(self, c.BaseCoverage, baseCoverage) + setattr(self, c.BaseArray, baseArray) + + return self + +class MarkHelper(object): + def __init__(self): + for Which in ('Mark', 'Base'): + for What in ('Coverage', 'Array', 'Count', 'Record', 'Anchor'): + key = Which + What + if Which == 'Mark' and What in ('Count', 'Record', 'Anchor'): + value = key + else: + value = getattr(self, Which) + What + if value == 'LigatureRecord': + value = 'LigatureAttach' + setattr(self, key, value) + if What != 'Count': + klass = getattr(ot, value) + setattr(self, key+'Class', klass) + +class MarkToBaseHelper(MarkHelper): + Mark = 'Mark' + Base = 'Base' + Type = ot.MarkBasePos +class MarkToMarkHelper(MarkHelper): + Mark = 'Mark1' + Base = 'Mark2' + Type = ot.MarkMarkPos +class MarkToLigatureHelper(MarkHelper): + Mark = 'Mark' + Base = 'Ligature' + Type = ot.MarkLigPos + +def parseMarkToBase(lines, font, _lookupMap=None): + return parseMarkToSomething(lines, font, MarkToBaseHelper()) +def parseMarkToMark(lines, font, _lookupMap=None): + return parseMarkToSomething(lines, font, MarkToMarkHelper()) +def parseMarkToLigature(lines, font, _lookupMap=None): + return parseMarkToSomething(lines, font, MarkToLigatureHelper()) + +def stripSplitComma(line): + return [s.strip() for s in line.split(',')] if line else [] + +def intSplitComma(line): + return [int(i) for i in line.split(',')] if line else [] + +# Copied from fontTools.subset +class ContextHelper(object): + def __init__(self, klassName, Format): + if klassName.endswith('Subst'): + Typ = 'Sub' + Type = 'Subst' + else: + Typ = 'Pos' + Type = 'Pos' + if klassName.startswith('Chain'): + Chain = 'Chain' + InputIdx = 1 + DataLen = 3 + else: + Chain = '' + InputIdx = 0 + DataLen = 1 + ChainTyp = Chain+Typ + + self.Typ = Typ + self.Type = Type + self.Chain = Chain + self.ChainTyp = ChainTyp + self.InputIdx = InputIdx + self.DataLen = DataLen + + self.LookupRecord = Type+'LookupRecord' + + if Format == 1: + Coverage = lambda r: r.Coverage + ChainCoverage = lambda r: r.Coverage + ContextData = lambda r:(None,) + ChainContextData = lambda r:(None, None, None) + SetContextData = None + SetChainContextData = None + RuleData = lambda r:(r.Input,) + ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead) + def SetRuleData(r, d): + (r.Input,) = d + (r.GlyphCount,) = (len(x)+1 for x in d) + def ChainSetRuleData(r, d): + (r.Backtrack, r.Input, r.LookAhead) = d + (r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(d[0]),len(d[1])+1,len(d[2])) + elif Format == 2: + Coverage = lambda r: r.Coverage + ChainCoverage = lambda r: r.Coverage + ContextData = lambda r:(r.ClassDef,) + ChainContextData = lambda r:(r.BacktrackClassDef, + r.InputClassDef, + r.LookAheadClassDef) + def SetContextData(r, d): + (r.ClassDef,) = d + def SetChainContextData(r, d): + (r.BacktrackClassDef, + r.InputClassDef, + r.LookAheadClassDef) = d + RuleData = lambda r:(r.Class,) + ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead) + def SetRuleData(r, d): + (r.Class,) = d + (r.GlyphCount,) = (len(x)+1 for x in d) + def ChainSetRuleData(r, d): + (r.Backtrack, r.Input, r.LookAhead) = d + (r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(d[0]),len(d[1])+1,len(d[2])) + elif Format == 3: + Coverage = lambda r: r.Coverage[0] + ChainCoverage = lambda r: r.InputCoverage[0] + ContextData = None + ChainContextData = None + SetContextData = None + SetChainContextData = None + RuleData = lambda r: r.Coverage + ChainRuleData = lambda r:(r.BacktrackCoverage + + r.InputCoverage + + r.LookAheadCoverage) + def SetRuleData(r, d): + (r.Coverage,) = d + (r.GlyphCount,) = (len(x) for x in d) + def ChainSetRuleData(r, d): + (r.BacktrackCoverage, r.InputCoverage, r.LookAheadCoverage) = d + (r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(x) for x in d) + else: + assert 0, "unknown format: %s" % Format + + if Chain: + self.Coverage = ChainCoverage + self.ContextData = ChainContextData + self.SetContextData = SetChainContextData + self.RuleData = ChainRuleData + self.SetRuleData = ChainSetRuleData + else: + self.Coverage = Coverage + self.ContextData = ContextData + self.SetContextData = SetContextData + self.RuleData = RuleData + self.SetRuleData = SetRuleData + + if Format == 1: + self.Rule = ChainTyp+'Rule' + self.RuleCount = ChainTyp+'RuleCount' + self.RuleSet = ChainTyp+'RuleSet' + self.RuleSetCount = ChainTyp+'RuleSetCount' + self.Intersect = lambda glyphs, c, r: [r] if r in glyphs else [] + elif Format == 2: + self.Rule = ChainTyp+'ClassRule' + self.RuleCount = ChainTyp+'ClassRuleCount' + self.RuleSet = ChainTyp+'ClassSet' + self.RuleSetCount = ChainTyp+'ClassSetCount' + self.Intersect = lambda glyphs, c, r: (c.intersect_class(glyphs, r) if c + else (set(glyphs) if r == 0 else set())) + + self.ClassDef = 'InputClassDef' if Chain else 'ClassDef' + self.ClassDefIndex = 1 if Chain else 0 + self.Input = 'Input' if Chain else 'Class' + +def parseLookupRecords(items, klassName, lookupMap=None): + klass = getattr(ot, klassName) + lst = [] + for item in items: + rec = klass() + item = stripSplitComma(item) + assert len(item) == 2, item + idx = int(item[0]) + assert idx > 0, idx + rec.SequenceIndex = idx - 1 + setReference(mapLookup, lookupMap, item[1], setattr, rec, 'LookupListIndex') + lst.append(rec) + return lst + +def makeClassDef(classDefs, font, klass=ot.Coverage): + if not classDefs: return None + self = klass() + self.classDefs = dict(classDefs) + return self + +def parseClassDef(lines, font, klass=ot.ClassDef): + classDefs = {} + with lines.between('class definition'): + for line in lines: + glyph = makeGlyph(line[0]) + assert glyph not in classDefs, glyph + classDefs[glyph] = int(line[1]) + return makeClassDef(classDefs, font, klass) + +def makeCoverage(glyphs, font, klass=ot.Coverage): + if not glyphs: return None + if isinstance(glyphs, set): + glyphs = sorted(glyphs) + coverage = klass() + coverage.glyphs = sorted(set(glyphs), key=font.getGlyphID) + return coverage + +def parseCoverage(lines, font, klass=ot.Coverage): + glyphs = [] + with lines.between('coverage definition'): + for line in lines: + glyphs.append(makeGlyph(line[0])) + return makeCoverage(glyphs, font, klass) + +def bucketizeRules(self, c, rules, bucketKeys): + buckets = {} + for seq,recs in rules: + buckets.setdefault(seq[c.InputIdx][0], []).append((tuple(s[1 if i==c.InputIdx else 0:] for i,s in enumerate(seq)), recs)) + + rulesets = [] + for firstGlyph in bucketKeys: + if firstGlyph not in buckets: + rulesets.append(None) + continue + thisRules = [] + for seq,recs in buckets[firstGlyph]: + rule = getattr(ot, c.Rule)() + c.SetRuleData(rule, seq) + setattr(rule, c.Type+'Count', len(recs)) + setattr(rule, c.LookupRecord, recs) + thisRules.append(rule) + + ruleset = getattr(ot, c.RuleSet)() + setattr(ruleset, c.Rule, thisRules) + setattr(ruleset, c.RuleCount, len(thisRules)) + rulesets.append(ruleset) + + setattr(self, c.RuleSet, rulesets) + setattr(self, c.RuleSetCount, len(rulesets)) + +def parseContext(lines, font, Type, lookupMap=None): + self = getattr(ot, Type)() + typ = lines.peeks()[0].split()[0].lower() + if typ == 'glyph': + self.Format = 1 + log.debug("Parsing %s format %s", Type, self.Format) + c = ContextHelper(Type, self.Format) + rules = [] + for line in lines: + assert line[0].lower() == 'glyph', line[0] + while len(line) < 1+c.DataLen: line.append('') + seq = tuple(makeGlyphs(stripSplitComma(i)) for i in line[1:1+c.DataLen]) + recs = parseLookupRecords(line[1+c.DataLen:], c.LookupRecord, lookupMap) + rules.append((seq, recs)) + + firstGlyphs = set(seq[c.InputIdx][0] for seq,recs in rules) + self.Coverage = makeCoverage(firstGlyphs, font) + bucketizeRules(self, c, rules, self.Coverage.glyphs) + elif typ.endswith('class'): + self.Format = 2 + log.debug("Parsing %s format %s", Type, self.Format) + c = ContextHelper(Type, self.Format) + classDefs = [None] * c.DataLen + while lines.peeks()[0].endswith("class definition begin"): + typ = lines.peek()[0][:-len("class definition begin")].lower() + idx,klass = { + 1: { + '': (0,ot.ClassDef), + }, + 3: { + 'backtrack': (0,ot.BacktrackClassDef), + '': (1,ot.InputClassDef), + 'lookahead': (2,ot.LookAheadClassDef), + }, + }[c.DataLen][typ] + assert classDefs[idx] is None, idx + classDefs[idx] = parseClassDef(lines, font, klass=klass) + c.SetContextData(self, classDefs) + rules = [] + for line in lines: + assert line[0].lower().startswith('class'), line[0] + while len(line) < 1+c.DataLen: line.append('') + seq = tuple(intSplitComma(i) for i in line[1:1+c.DataLen]) + recs = parseLookupRecords(line[1+c.DataLen:], c.LookupRecord, lookupMap) + rules.append((seq, recs)) + firstClasses = set(seq[c.InputIdx][0] for seq,recs in rules) + firstGlyphs = set(g for g,c in classDefs[c.InputIdx].classDefs.items() if c in firstClasses) + self.Coverage = makeCoverage(firstGlyphs, font) + bucketizeRules(self, c, rules, range(max(firstClasses) + 1)) + elif typ.endswith('coverage'): + self.Format = 3 + log.debug("Parsing %s format %s", Type, self.Format) + c = ContextHelper(Type, self.Format) + coverages = tuple([] for i in range(c.DataLen)) + while lines.peeks()[0].endswith("coverage definition begin"): + typ = lines.peek()[0][:-len("coverage definition begin")].lower() + idx,klass = { + 1: { + '': (0,ot.Coverage), + }, + 3: { + 'backtrack': (0,ot.BacktrackCoverage), + 'input': (1,ot.InputCoverage), + 'lookahead': (2,ot.LookAheadCoverage), + }, + }[c.DataLen][typ] + coverages[idx].append(parseCoverage(lines, font, klass=klass)) + c.SetRuleData(self, coverages) + lines = list(lines) + assert len(lines) == 1 + line = lines[0] + assert line[0].lower() == 'coverage', line[0] + recs = parseLookupRecords(line[1:], c.LookupRecord, lookupMap) + setattr(self, c.Type+'Count', len(recs)) + setattr(self, c.LookupRecord, recs) + else: + assert 0, typ + return self + +def parseContextSubst(lines, font, lookupMap=None): + return parseContext(lines, font, "ContextSubst", lookupMap=lookupMap) +def parseContextPos(lines, font, lookupMap=None): + return parseContext(lines, font, "ContextPos", lookupMap=lookupMap) +def parseChainedSubst(lines, font, lookupMap=None): + return parseContext(lines, font, "ChainContextSubst", lookupMap=lookupMap) +def parseChainedPos(lines, font, lookupMap=None): + return parseContext(lines, font, "ChainContextPos", lookupMap=lookupMap) + +def parseReverseChainedSubst(lines, font, _lookupMap=None): + self = ot.ReverseChainSingleSubst() + self.Format = 1 + coverages = ([], []) + while lines.peeks()[0].endswith("coverage definition begin"): + typ = lines.peek()[0][:-len("coverage definition begin")].lower() + idx,klass = { + 'backtrack': (0,ot.BacktrackCoverage), + 'lookahead': (1,ot.LookAheadCoverage), + }[typ] + coverages[idx].append(parseCoverage(lines, font, klass=klass)) + self.BacktrackCoverage = coverages[0] + self.BacktrackGlyphCount = len(self.BacktrackCoverage) + self.LookAheadCoverage = coverages[1] + self.LookAheadGlyphCount = len(self.LookAheadCoverage) + mapping = {} + for line in lines: + assert len(line) == 2, line + line = makeGlyphs(line) + mapping[line[0]] = line[1] + self.Coverage = makeCoverage(set(mapping.keys()), font) + self.Substitute = [mapping[k] for k in self.Coverage.glyphs] + self.GlyphCount = len(self.Substitute) + return self + +def parseLookup(lines, tableTag, font, lookupMap=None): + line = lines.expect('lookup') + _, name, typ = line + log.debug("Parsing lookup type %s %s", typ, name) + lookup = ot.Lookup() + lookup.LookupFlag,filterset = parseLookupFlags(lines) + if filterset is not None: + lookup.MarkFilteringSet = filterset + lookup.LookupType, parseLookupSubTable = { + 'GSUB': { + 'single': (1, parseSingleSubst), + 'multiple': (2, parseMultiple), + 'alternate': (3, parseAlternate), + 'ligature': (4, parseLigature), + 'context': (5, parseContextSubst), + 'chained': (6, parseChainedSubst), + 'reversechained':(8, parseReverseChainedSubst), + }, + 'GPOS': { + 'single': (1, parseSinglePos), + 'pair': (2, parsePair), + 'kernset': (2, parseKernset), + 'cursive': (3, parseCursive), + 'mark to base': (4, parseMarkToBase), + 'mark to ligature':(5, parseMarkToLigature), + 'mark to mark': (6, parseMarkToMark), + 'context': (7, parseContextPos), + 'chained': (8, parseChainedPos), + }, + }[tableTag][typ] + + with lines.until('lookup end'): + subtables = [] + + while lines.peek(): + with lines.until(('% subtable', 'subtable end')): + while lines.peek(): + subtable = parseLookupSubTable(lines, font, lookupMap) + assert lookup.LookupType == subtable.LookupType + subtables.append(subtable) + if lines.peeks()[0] in ('% subtable', 'subtable end'): + next(lines) + lines.expect('lookup end') + + lookup.SubTable = subtables + lookup.SubTableCount = len(lookup.SubTable) + if lookup.SubTableCount is 0: + # Remove this return when following is fixed: + # https://github.com/fonttools/fonttools/issues/789 + return None + return lookup + +def parseGSUBGPOS(lines, font, tableTag): + container = ttLib.getTableClass(tableTag)() + lookupMap = DeferredMapping() + featureMap = DeferredMapping() + assert tableTag in ('GSUB', 'GPOS') + log.debug("Parsing %s", tableTag) + self = getattr(ot, tableTag)() + self.Version = 0x00010000 + fields = { + 'script table begin': + ('ScriptList', + lambda lines: parseScriptList (lines, featureMap)), + 'feature table begin': + ('FeatureList', + lambda lines: parseFeatureList (lines, lookupMap, featureMap)), + 'lookup': + ('LookupList', + None), + } + for attr,parser in fields.values(): + setattr(self, attr, None) + while lines.peek() is not None: + typ = lines.peek()[0].lower() + if typ not in fields: + log.debug('Skipping %s', lines.peek()) + next(lines) + continue + attr,parser = fields[typ] + if typ == 'lookup': + if self.LookupList is None: + self.LookupList = ot.LookupList() + self.LookupList.Lookup = [] + _, name, _ = lines.peek() + lookup = parseLookup(lines, tableTag, font, lookupMap) + if lookupMap is not None: + assert name not in lookupMap, "Duplicate lookup name: %s" % name + lookupMap[name] = len(self.LookupList.Lookup) + else: + assert int(name) == len(self.LookupList.Lookup), "%d %d" % (name, len(self.Lookup)) + self.LookupList.Lookup.append(lookup) + else: + assert getattr(self, attr) is None, attr + setattr(self, attr, parser(lines)) + if self.LookupList: + self.LookupList.LookupCount = len(self.LookupList.Lookup) + if lookupMap is not None: + lookupMap.applyDeferredMappings() + if featureMap is not None: + featureMap.applyDeferredMappings() + container.table = self + return container + +def parseGSUB(lines, font): + return parseGSUBGPOS(lines, font, 'GSUB') +def parseGPOS(lines, font): + return parseGSUBGPOS(lines, font, 'GPOS') + +def parseAttachList(lines, font): + points = {} + with lines.between('attachment list'): + for line in lines: + glyph = makeGlyph(line[0]) + assert glyph not in points, glyph + points[glyph] = [int(i) for i in line[1:]] + return otl.buildAttachList(points, font.getReverseGlyphMap()) + +def parseCaretList(lines, font): + carets = {} + with lines.between('carets'): + for line in lines: + glyph = makeGlyph(line[0]) + assert glyph not in carets, glyph + num = int(line[1]) + thisCarets = [int(i) for i in line[2:]] + assert num == len(thisCarets), line + carets[glyph] = thisCarets + return otl.buildLigCaretList(carets, {}, font.getReverseGlyphMap()) + +def makeMarkFilteringSets(sets, font): + self = ot.MarkGlyphSetsDef() + self.MarkSetTableFormat = 1 + self.MarkSetCount = 1 + max(sets.keys()) + self.Coverage = [None] * self.MarkSetCount + for k,v in sorted(sets.items()): + self.Coverage[k] = makeCoverage(set(v), font) + return self + +def parseMarkFilteringSets(lines, font): + sets = {} + with lines.between('set definition'): + for line in lines: + assert len(line) == 2, line + glyph = makeGlyph(line[0]) + # TODO accept set names + st = int(line[1]) + if st not in sets: + sets[st] = [] + sets[st].append(glyph) + return makeMarkFilteringSets(sets, font) + +def parseGDEF(lines, font): + container = ttLib.getTableClass('GDEF')() + log.debug("Parsing GDEF") + self = ot.GDEF() + fields = { + 'class definition begin': + ('GlyphClassDef', + lambda lines, font: parseClassDef(lines, font, klass=ot.GlyphClassDef)), + 'attachment list begin': + ('AttachList', parseAttachList), + 'carets begin': + ('LigCaretList', parseCaretList), + 'mark attachment class definition begin': + ('MarkAttachClassDef', + lambda lines, font: parseClassDef(lines, font, klass=ot.MarkAttachClassDef)), + 'markfilter set definition begin': + ('MarkGlyphSetsDef', parseMarkFilteringSets), + } + for attr,parser in fields.values(): + setattr(self, attr, None) + while lines.peek() is not None: + typ = lines.peek()[0].lower() + if typ not in fields: + log.debug('Skipping %s', typ) + next(lines) + continue + attr,parser = fields[typ] + assert getattr(self, attr) is None, attr + setattr(self, attr, parser(lines, font)) + self.Version = 0x00010000 if self.MarkGlyphSetsDef is None else 0x00010002 + container.table = self + return container + +def parseCmap(lines, font): + container = ttLib.getTableClass('cmap')() + log.debug("Parsing cmap") + tables = [] + while lines.peek() is not None: + lines.expect('cmap subtable %d' % len(tables)) + platId, encId, fmt, lang = [ + parseCmapId(lines, field) + for field in ('platformID', 'encodingID', 'format', 'language')] + table = cmap_classes[fmt](fmt) + table.platformID = platId + table.platEncID = encId + table.language = lang + table.cmap = {} + line = next(lines) + while line[0] != 'end subtable': + table.cmap[int(line[0], 16)] = line[1] + line = next(lines) + tables.append(table) + container.tableVersion = 0 + container.tables = tables + return container + +def parseCmapId(lines, field): + line = next(lines) + assert field == line[0] + return int(line[1]) + +def parseTable(lines, font, tableTag=None): + log.debug("Parsing table") + line = lines.peeks() + tag = None + if line[0].split()[0] == 'FontDame': + tag = line[0].split()[1] + elif ' '.join(line[0].split()[:3]) == 'Font Chef Table': + tag = line[0].split()[3] + if tag is not None: + next(lines) + tag = tag.ljust(4) + if tableTag is None: + tableTag = tag + else: + assert tableTag == tag, (tableTag, tag) + + assert tableTag is not None, "Don't know what table to parse and data doesn't specify" + + return { + 'GSUB': parseGSUB, + 'GPOS': parseGPOS, + 'GDEF': parseGDEF, + 'cmap': parseCmap, + }[tableTag](lines, font) + +class Tokenizer(object): + + def __init__(self, f): + # TODO BytesIO / StringIO as needed? also, figure out whether we work on bytes or unicode + lines = iter(f) + try: + self.filename = f.name + except: + self.filename = None + self.lines = iter(lines) + self.line = '' + self.lineno = 0 + self.stoppers = [] + self.buffer = None + + def __iter__(self): + return self + + def _next_line(self): + self.lineno += 1 + line = self.line = next(self.lines) + line = [s.strip() for s in line.split('\t')] + if len(line) == 1 and not line[0]: + del line[0] + if line and not line[-1]: + log.warning('trailing tab found on line %d: %s' % (self.lineno, self.line)) + while line and not line[-1]: + del line[-1] + return line + + def _next_nonempty(self): + while True: + line = self._next_line() + # Skip comments and empty lines + if line and line[0] and (line[0][0] != '%' or line[0] == '% subtable'): + return line + + def _next_buffered(self): + if self.buffer: + ret = self.buffer + self.buffer = None + return ret + else: + return self._next_nonempty() + + def __next__(self): + line = self._next_buffered() + if line[0].lower() in self.stoppers: + self.buffer = line + raise StopIteration + return line + + def next(self): + return self.__next__() + + def peek(self): + if not self.buffer: + try: + self.buffer = self._next_nonempty() + except StopIteration: + return None + if self.buffer[0].lower() in self.stoppers: + return None + return self.buffer + + def peeks(self): + ret = self.peek() + return ret if ret is not None else ('',) + + @contextmanager + def between(self, tag): + start = tag + ' begin' + end = tag + ' end' + self.expectendswith(start) + self.stoppers.append(end) + yield + del self.stoppers[-1] + self.expect(tag + ' end') + + @contextmanager + def until(self, tags): + if type(tags) is not tuple: + tags = (tags,) + self.stoppers.extend(tags) + yield + del self.stoppers[-len(tags):] + + def expect(self, s): + line = next(self) + tag = line[0].lower() + assert tag == s, "Expected '%s', got '%s'" % (s, tag) + return line + + def expectendswith(self, s): + line = next(self) + tag = line[0].lower() + assert tag.endswith(s), "Expected '*%s', got '%s'" % (s, tag) + return line + +def build(f, font, tableTag=None): + lines = Tokenizer(f) + return parseTable(lines, font, tableTag=tableTag) + + +def main(args=None, font=None): + import sys + from fontTools import configLogger + from fontTools.misc.testTools import MockFont + + if args is None: + args = sys.argv[1:] + + # configure the library logger (for >= WARNING) + configLogger() + # comment this out to enable debug messages from mtiLib's logger + # log.setLevel(logging.DEBUG) + + if font is None: + font = MockFont() + + tableTag = None + if args[0].startswith('-t'): + tableTag = args[0][2:] + del args[0] + for f in args: + log.debug("Processing %s", f) + table = build(open(f, 'rt', encoding="utf-8"), font, tableTag=tableTag) + blob = table.compile(font) # Make sure it compiles + decompiled = table.__class__() + decompiled.decompile(blob, font) # Make sure it decompiles! + + #continue + from fontTools.misc import xmlWriter + tag = table.tableTag + writer = xmlWriter.XMLWriter(sys.stdout) + writer.begintag(tag) + writer.newline() + #table.toXML(writer, font) + decompiled.toXML(writer, font) + writer.endtag(tag) + writer.newline() + + +if __name__ == '__main__': + import sys + sys.exit(main()) diff --git a/Lib/fontTools/mtiLib/__main__.py b/Lib/fontTools/mtiLib/__main__.py new file mode 100644 index 0000000..0741237 --- /dev/null +++ b/Lib/fontTools/mtiLib/__main__.py @@ -0,0 +1,7 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +import sys +from fontTools.mtiLib import main + +if __name__ == '__main__': + sys.exit(main()) diff --git a/Lib/fontTools/otlLib/__init__.py b/Lib/fontTools/otlLib/__init__.py new file mode 100644 index 0000000..12e414f --- /dev/null +++ b/Lib/fontTools/otlLib/__init__.py @@ -0,0 +1 @@ +"""OpenType Layout-related functionality.""" diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py new file mode 100644 index 0000000..efe66d5 --- /dev/null +++ b/Lib/fontTools/otlLib/builder.py @@ -0,0 +1,640 @@ +from __future__ import print_function, division, absolute_import +from fontTools import ttLib +from fontTools.ttLib.tables import otTables as ot +from fontTools.ttLib.tables.otBase import ValueRecord, valueRecordFormatDict + + +def buildCoverage(glyphs, glyphMap): + if not glyphs: + return None + self = ot.Coverage() + self.glyphs = sorted(glyphs, key=glyphMap.__getitem__) + return self + + +LOOKUP_FLAG_RIGHT_TO_LEFT = 0x0001 +LOOKUP_FLAG_IGNORE_BASE_GLYPHS = 0x0002 +LOOKUP_FLAG_IGNORE_LIGATURES = 0x0004 +LOOKUP_FLAG_IGNORE_MARKS = 0x0008 +LOOKUP_FLAG_USE_MARK_FILTERING_SET = 0x0010 + + +def buildLookup(subtables, flags=0, markFilterSet=None): + if subtables is None: + return None + subtables = [st for st in subtables if st is not None] + if not subtables: + return None + assert all(t.LookupType == subtables[0].LookupType for t in subtables), \ + ("all subtables must have the same LookupType; got %s" % + repr([t.LookupType for t in subtables])) + self = ot.Lookup() + self.LookupType = subtables[0].LookupType + self.LookupFlag = flags + self.SubTable = subtables + self.SubTableCount = len(self.SubTable) + if markFilterSet is not None: + assert self.LookupFlag & LOOKUP_FLAG_USE_MARK_FILTERING_SET, \ + ("if markFilterSet is not None, flags must set " + "LOOKUP_FLAG_USE_MARK_FILTERING_SET; flags=0x%04x" % flags) + assert isinstance(markFilterSet, int), markFilterSet + self.MarkFilteringSet = markFilterSet + else: + assert (self.LookupFlag & LOOKUP_FLAG_USE_MARK_FILTERING_SET) == 0, \ + ("if markFilterSet is None, flags must not set " + "LOOKUP_FLAG_USE_MARK_FILTERING_SET; flags=0x%04x" % flags) + return self + + +# GSUB + + +def buildSingleSubstSubtable(mapping): + if not mapping: + return None + self = ot.SingleSubst() + self.mapping = dict(mapping) + return self + + +def buildMultipleSubstSubtable(mapping): + if not mapping: + return None + self = ot.MultipleSubst() + self.mapping = dict(mapping) + return self + + +def buildAlternateSubstSubtable(mapping): + if not mapping: + return None + self = ot.AlternateSubst() + self.alternates = dict(mapping) + return self + + +def _getLigatureKey(components): + """Computes a key for ordering ligatures in a GSUB Type-4 lookup. + + When building the OpenType lookup, we need to make sure that + the longest sequence of components is listed first, so we + use the negative length as the primary key for sorting. + To make buildLigatureSubstSubtable() deterministic, we use the + component sequence as the secondary key. + + For example, this will sort (f,f,f) < (f,f,i) < (f,f) < (f,i) < (f,l). + """ + return (-len(components), components) + + +def buildLigatureSubstSubtable(mapping): + if not mapping: + return None + self = ot.LigatureSubst() + # The following single line can replace the rest of this function + # with fontTools >= 3.1: + # self.ligatures = dict(mapping) + self.ligatures = {} + for components in sorted(mapping.keys(), key=_getLigatureKey): + ligature = ot.Ligature() + ligature.Component = components[1:] + ligature.CompCount = len(ligature.Component) + 1 + ligature.LigGlyph = mapping[components] + firstGlyph = components[0] + self.ligatures.setdefault(firstGlyph, []).append(ligature) + return self + + +# GPOS + + +def buildAnchor(x, y, point=None, deviceX=None, deviceY=None): + self = ot.Anchor() + self.XCoordinate, self.YCoordinate = x, y + self.Format = 1 + if point is not None: + self.AnchorPoint = point + self.Format = 2 + if deviceX is not None or deviceY is not None: + assert self.Format == 1, \ + "Either point, or both of deviceX/deviceY, must be None." + self.XDeviceTable = deviceX + self.YDeviceTable = deviceY + self.Format = 3 + return self + + +def buildBaseArray(bases, numMarkClasses, glyphMap): + self = ot.BaseArray() + self.BaseRecord = [] + for base in sorted(bases, key=glyphMap.__getitem__): + b = bases[base] + anchors = [b.get(markClass) for markClass in range(numMarkClasses)] + self.BaseRecord.append(buildBaseRecord(anchors)) + self.BaseCount = len(self.BaseRecord) + return self + + +def buildBaseRecord(anchors): + """[otTables.Anchor, otTables.Anchor, ...] --> otTables.BaseRecord""" + self = ot.BaseRecord() + self.BaseAnchor = anchors + return self + + +def buildComponentRecord(anchors): + """[otTables.Anchor, otTables.Anchor, ...] --> otTables.ComponentRecord""" + if not anchors: + return None + self = ot.ComponentRecord() + self.LigatureAnchor = anchors + return self + + +def buildCursivePosSubtable(attach, glyphMap): + """{"alef": (entry, exit)} --> otTables.CursivePos""" + if not attach: + return None + self = ot.CursivePos() + self.Format = 1 + self.Coverage = buildCoverage(attach.keys(), glyphMap) + self.EntryExitRecord = [] + for glyph in self.Coverage.glyphs: + entryAnchor, exitAnchor = attach[glyph] + rec = ot.EntryExitRecord() + rec.EntryAnchor = entryAnchor + rec.ExitAnchor = exitAnchor + self.EntryExitRecord.append(rec) + self.EntryExitCount = len(self.EntryExitRecord) + return self + + +def buildDevice(deltas): + """{8:+1, 10:-3, ...} --> otTables.Device""" + if not deltas: + return None + self = ot.Device() + keys = deltas.keys() + self.StartSize = startSize = min(keys) + self.EndSize = endSize = max(keys) + assert 0 <= startSize <= endSize + self.DeltaValue = deltaValues = [ + deltas.get(size, 0) + for size in range(startSize, endSize + 1)] + maxDelta = max(deltaValues) + minDelta = min(deltaValues) + assert minDelta > -129 and maxDelta < 128 + if minDelta > -3 and maxDelta < 2: + self.DeltaFormat = 1 + elif minDelta > -9 and maxDelta < 8: + self.DeltaFormat = 2 + else: + self.DeltaFormat = 3 + return self + + +def buildLigatureArray(ligs, numMarkClasses, glyphMap): + self = ot.LigatureArray() + self.LigatureAttach = [] + for lig in sorted(ligs, key=glyphMap.__getitem__): + anchors = [] + for component in ligs[lig]: + anchors.append([component.get(mc) for mc in range(numMarkClasses)]) + self.LigatureAttach.append(buildLigatureAttach(anchors)) + self.LigatureCount = len(self.LigatureAttach) + return self + + +def buildLigatureAttach(components): + """[[Anchor, Anchor], [Anchor, Anchor, Anchor]] --> LigatureAttach""" + self = ot.LigatureAttach() + self.ComponentRecord = [buildComponentRecord(c) for c in components] + self.ComponentCount = len(self.ComponentRecord) + return self + + +def buildMarkArray(marks, glyphMap): + """{"acute": (markClass, otTables.Anchor)} --> otTables.MarkArray""" + self = ot.MarkArray() + self.MarkRecord = [] + for mark in sorted(marks.keys(), key=glyphMap.__getitem__): + markClass, anchor = marks[mark] + markrec = buildMarkRecord(markClass, anchor) + self.MarkRecord.append(markrec) + self.MarkCount = len(self.MarkRecord) + return self + + +def buildMarkBasePos(marks, bases, glyphMap): + """Build a list of MarkBasePos subtables. + + a1, a2, a3, a4, a5 = buildAnchor(500, 100), ... + marks = {"acute": (0, a1), "grave": (0, a1), "cedilla": (1, a2)} + bases = {"a": {0: a3, 1: a5}, "b": {0: a4, 1: a5}} + """ + # TODO: Consider emitting multiple subtables to save space. + # Partition the marks and bases into disjoint subsets, so that + # MarkBasePos rules would only access glyphs from a single + # subset. This would likely lead to smaller mark/base + # matrices, so we might be able to omit many of the empty + # anchor tables that we currently produce. Of course, this + # would only work if the MarkBasePos rules of real-world fonts + # allow partitioning into multiple subsets. We should find out + # whether this is the case; if so, implement the optimization. + # On the other hand, a very large number of subtables could + # slow down layout engines; so this would need profiling. + return [buildMarkBasePosSubtable(marks, bases, glyphMap)] + + +def buildMarkBasePosSubtable(marks, bases, glyphMap): + """Build a single MarkBasePos subtable. + + a1, a2, a3, a4, a5 = buildAnchor(500, 100), ... + marks = {"acute": (0, a1), "grave": (0, a1), "cedilla": (1, a2)} + bases = {"a": {0: a3, 1: a5}, "b": {0: a4, 1: a5}} + """ + self = ot.MarkBasePos() + self.Format = 1 + self.MarkCoverage = buildCoverage(marks, glyphMap) + self.MarkArray = buildMarkArray(marks, glyphMap) + self.ClassCount = max([mc for mc, _ in marks.values()]) + 1 + self.BaseCoverage = buildCoverage(bases, glyphMap) + self.BaseArray = buildBaseArray(bases, self.ClassCount, glyphMap) + return self + + +def buildMarkLigPos(marks, ligs, glyphMap): + """Build a list of MarkLigPos subtables. + + a1, a2, a3, a4, a5 = buildAnchor(500, 100), ... + marks = {"acute": (0, a1), "grave": (0, a1), "cedilla": (1, a2)} + ligs = {"f_i": [{0: a3, 1: a5}, {0: a4, 1: a5}], "c_t": [{...}, {...}]} + """ + # TODO: Consider splitting into multiple subtables to save space, + # as with MarkBasePos, this would be a trade-off that would need + # profiling. And, depending on how typical fonts are structured, + # it might not be worth doing at all. + return [buildMarkLigPosSubtable(marks, ligs, glyphMap)] + + +def buildMarkLigPosSubtable(marks, ligs, glyphMap): + """Build a single MarkLigPos subtable. + + a1, a2, a3, a4, a5 = buildAnchor(500, 100), ... + marks = {"acute": (0, a1), "grave": (0, a1), "cedilla": (1, a2)} + ligs = {"f_i": [{0: a3, 1: a5}, {0: a4, 1: a5}], "c_t": [{...}, {...}]} + """ + self = ot.MarkLigPos() + self.Format = 1 + self.MarkCoverage = buildCoverage(marks, glyphMap) + self.MarkArray = buildMarkArray(marks, glyphMap) + self.ClassCount = max([mc for mc, _ in marks.values()]) + 1 + self.LigatureCoverage = buildCoverage(ligs, glyphMap) + self.LigatureArray = buildLigatureArray(ligs, self.ClassCount, glyphMap) + return self + + +def buildMarkRecord(classID, anchor): + assert isinstance(classID, int) + assert isinstance(anchor, ot.Anchor) + self = ot.MarkRecord() + self.Class = classID + self.MarkAnchor = anchor + return self + + +def buildMark2Record(anchors): + """[otTables.Anchor, otTables.Anchor, ...] --> otTables.Mark2Record""" + self = ot.Mark2Record() + self.Mark2Anchor = anchors + return self + + +def _getValueFormat(f, values, i): + """Helper for buildPairPos{Glyphs|Classes}Subtable.""" + if f is not None: + return f + mask = 0 + for value in values: + if value is not None and value[i] is not None: + mask |= value[i].getFormat() + return mask + + +def buildPairPosClassesSubtable(pairs, glyphMap, + valueFormat1=None, valueFormat2=None): + coverage = set() + classDef1 = ClassDefBuilder(useClass0=True) + classDef2 = ClassDefBuilder(useClass0=False) + for gc1, gc2 in sorted(pairs): + coverage.update(gc1) + classDef1.add(gc1) + classDef2.add(gc2) + self = ot.PairPos() + self.Format = 2 + self.ValueFormat1 = _getValueFormat(valueFormat1, pairs.values(), 0) + self.ValueFormat2 = _getValueFormat(valueFormat2, pairs.values(), 1) + self.Coverage = buildCoverage(coverage, glyphMap) + self.ClassDef1 = classDef1.build() + self.ClassDef2 = classDef2.build() + classes1 = classDef1.classes() + classes2 = classDef2.classes() + self.Class1Record = [] + for c1 in classes1: + rec1 = ot.Class1Record() + rec1.Class2Record = [] + self.Class1Record.append(rec1) + for c2 in classes2: + rec2 = ot.Class2Record() + rec2.Value1, rec2.Value2 = pairs.get((c1, c2), (None, None)) + rec1.Class2Record.append(rec2) + self.Class1Count = len(self.Class1Record) + self.Class2Count = len(classes2) + return self + + +def buildPairPosGlyphs(pairs, glyphMap): + p = {} # (formatA, formatB) --> {(glyphA, glyphB): (valA, valB)} + for (glyphA, glyphB), (valA, valB) in pairs.items(): + formatA = valA.getFormat() if valA is not None else 0 + formatB = valB.getFormat() if valB is not None else 0 + pos = p.setdefault((formatA, formatB), {}) + pos[(glyphA, glyphB)] = (valA, valB) + return [ + buildPairPosGlyphsSubtable(pos, glyphMap, formatA, formatB) + for ((formatA, formatB), pos) in sorted(p.items())] + + +def buildPairPosGlyphsSubtable(pairs, glyphMap, + valueFormat1=None, valueFormat2=None): + self = ot.PairPos() + self.Format = 1 + self.ValueFormat1 = _getValueFormat(valueFormat1, pairs.values(), 0) + self.ValueFormat2 = _getValueFormat(valueFormat2, pairs.values(), 1) + p = {} + for (glyphA, glyphB), (valA, valB) in pairs.items(): + p.setdefault(glyphA, []).append((glyphB, valA, valB)) + self.Coverage = buildCoverage({g for g, _ in pairs.keys()}, glyphMap) + self.PairSet = [] + for glyph in self.Coverage.glyphs: + ps = ot.PairSet() + ps.PairValueRecord = [] + self.PairSet.append(ps) + for glyph2, val1, val2 in \ + sorted(p[glyph], key=lambda x: glyphMap[x[0]]): + pvr = ot.PairValueRecord() + pvr.SecondGlyph = glyph2 + pvr.Value1 = val1 if val1 and val1.getFormat() != 0 else None + pvr.Value2 = val2 if val2 and val2.getFormat() != 0 else None + ps.PairValueRecord.append(pvr) + ps.PairValueCount = len(ps.PairValueRecord) + self.PairSetCount = len(self.PairSet) + return self + + +def buildSinglePos(mapping, glyphMap): + """{"glyph": ValueRecord} --> [otTables.SinglePos*]""" + result, handled = [], set() + # In SinglePos format 1, the covered glyphs all share the same ValueRecord. + # In format 2, each glyph has its own ValueRecord, but these records + # all have the same properties (eg., all have an X but no Y placement). + coverages, masks, values = {}, {}, {} + for glyph, value in mapping.items(): + key = _getSinglePosValueKey(value) + coverages.setdefault(key, []).append(glyph) + masks.setdefault(key[0], []).append(key) + values[key] = value + + # If a ValueRecord is shared between multiple glyphs, we generate + # a SinglePos format 1 subtable; that is the most compact form. + for key, glyphs in coverages.items(): + if len(glyphs) > 1: + format1Mapping = {g: values[key] for g in glyphs} + result.append(buildSinglePosSubtable(format1Mapping, glyphMap)) + handled.add(key) + + # In the remaining ValueRecords, look for those whose valueFormat + # (the set of used properties) is shared between multiple records. + # These will get encoded in format 2. + for valueFormat, keys in masks.items(): + f2 = [k for k in keys if k not in handled] + if len(f2) > 1: + format2Mapping = {coverages[k][0]: values[k] for k in f2} + result.append(buildSinglePosSubtable(format2Mapping, glyphMap)) + handled.update(f2) + + # The remaining ValueRecords are singletons in the sense that + # they are only used by a single glyph, and their valueFormat + # is unique as well. We encode these in format 1 again. + for key, glyphs in coverages.items(): + if key not in handled: + assert len(glyphs) == 1, glyphs + st = buildSinglePosSubtable({glyphs[0]: values[key]}, glyphMap) + result.append(st) + + # When the OpenType layout engine traverses the subtables, it will + # stop after the first matching subtable. Therefore, we sort the + # resulting subtables by decreasing coverage size; this increases + # the chance that the layout engine can do an early exit. (Of course, + # this would only be true if all glyphs were equally frequent, which + # is not really the case; but we do not know their distribution). + # If two subtables cover the same number of glyphs, we sort them + # by glyph ID so that our output is deterministic. + result.sort(key=lambda t: _getSinglePosTableKey(t, glyphMap)) + return result + + +def buildSinglePosSubtable(values, glyphMap): + """{glyphName: otBase.ValueRecord} --> otTables.SinglePos""" + self = ot.SinglePos() + self.Coverage = buildCoverage(values.keys(), glyphMap) + valueRecords = [values[g] for g in self.Coverage.glyphs] + self.ValueFormat = 0 + for v in valueRecords: + self.ValueFormat |= v.getFormat() + if all(v == valueRecords[0] for v in valueRecords): + self.Format = 1 + if self.ValueFormat != 0: + self.Value = valueRecords[0] + else: + self.Value = None + else: + self.Format = 2 + self.Value = valueRecords + self.ValueCount = len(self.Value) + return self + + +def _getSinglePosTableKey(subtable, glyphMap): + assert isinstance(subtable, ot.SinglePos), subtable + glyphs = subtable.Coverage.glyphs + return (-len(glyphs), glyphMap[glyphs[0]]) + + +def _getSinglePosValueKey(valueRecord): + """otBase.ValueRecord --> (2, ("YPlacement": 12))""" + assert isinstance(valueRecord, ValueRecord), valueRecord + valueFormat, result = 0, [] + for name, value in valueRecord.__dict__.items(): + if isinstance(value, ot.Device): + result.append((name, _makeDeviceTuple(value))) + else: + result.append((name, value)) + valueFormat |= valueRecordFormatDict[name][0] + result.sort() + result.insert(0, valueFormat) + return tuple(result) + + +def _makeDeviceTuple(device): + """otTables.Device --> tuple, for making device tables unique""" + return (device.DeltaFormat, device.StartSize, device.EndSize, + tuple(device.DeltaValue)) + + +def buildValue(value): + self = ValueRecord() + for k, v in value.items(): + setattr(self, k, v) + return self + + +# GDEF + +def buildAttachList(attachPoints, glyphMap): + """{"glyphName": [4, 23]} --> otTables.AttachList, or None""" + if not attachPoints: + return None + self = ot.AttachList() + self.Coverage = buildCoverage(attachPoints.keys(), glyphMap) + self.AttachPoint = [buildAttachPoint(attachPoints[g]) + for g in self.Coverage.glyphs] + self.GlyphCount = len(self.AttachPoint) + return self + + +def buildAttachPoint(points): + """[4, 23, 41] --> otTables.AttachPoint""" + if not points: + return None + self = ot.AttachPoint() + self.PointIndex = sorted(set(points)) + self.PointCount = len(self.PointIndex) + return self + + +def buildCaretValueForCoord(coord): + """500 --> otTables.CaretValue, format 1""" + self = ot.CaretValue() + self.Format = 1 + self.Coordinate = coord + return self + + +def buildCaretValueForPoint(point): + """4 --> otTables.CaretValue, format 2""" + self = ot.CaretValue() + self.Format = 2 + self.CaretValuePoint = point + return self + + +def buildLigCaretList(coords, points, glyphMap): + """{"f_f_i":[300,600]}, {"c_t":[28]} --> otTables.LigCaretList, or None""" + glyphs = set(coords.keys()) if coords else set() + if points: + glyphs.update(points.keys()) + carets = {g: buildLigGlyph(coords.get(g), points.get(g)) for g in glyphs} + carets = {g: c for g, c in carets.items() if c is not None} + if not carets: + return None + self = ot.LigCaretList() + self.Coverage = buildCoverage(carets.keys(), glyphMap) + self.LigGlyph = [carets[g] for g in self.Coverage.glyphs] + self.LigGlyphCount = len(self.LigGlyph) + return self + + +def buildLigGlyph(coords, points): + """([500], [4]) --> otTables.LigGlyph; None for empty coords/points""" + carets = [] + if coords: + carets.extend([buildCaretValueForCoord(c) for c in sorted(coords)]) + if points: + carets.extend([buildCaretValueForPoint(p) for p in sorted(points)]) + if not carets: + return None + self = ot.LigGlyph() + self.CaretValue = carets + self.CaretCount = len(self.CaretValue) + return self + + +def buildMarkGlyphSetsDef(markSets, glyphMap): + """[{"acute","grave"}, {"caron","grave"}] --> otTables.MarkGlyphSetsDef""" + if not markSets: + return None + self = ot.MarkGlyphSetsDef() + self.MarkSetTableFormat = 1 + self.Coverage = [buildCoverage(m, glyphMap) for m in markSets] + self.MarkSetCount = len(self.Coverage) + return self + + +class ClassDefBuilder(object): + """Helper for building ClassDef tables.""" + def __init__(self, useClass0): + self.classes_ = set() + self.glyphs_ = {} + self.useClass0_ = useClass0 + + def canAdd(self, glyphs): + if isinstance(glyphs, (set, frozenset)): + glyphs = sorted(glyphs) + glyphs = tuple(glyphs) + if glyphs in self.classes_: + return True + for glyph in glyphs: + if glyph in self.glyphs_: + return False + return True + + def add(self, glyphs): + if isinstance(glyphs, (set, frozenset)): + glyphs = sorted(glyphs) + glyphs = tuple(glyphs) + if glyphs in self.classes_: + return + self.classes_.add(glyphs) + for glyph in glyphs: + assert glyph not in self.glyphs_ + self.glyphs_[glyph] = glyphs + + def classes(self): + # In ClassDef1 tables, class id #0 does not need to be encoded + # because zero is the default. Therefore, we use id #0 for the + # glyph class that has the largest number of members. However, + # in other tables than ClassDef1, 0 means "every other glyph" + # so we should not use that ID for any real glyph classes; + # we implement this by inserting an empty set at position 0. + # + # TODO: Instead of counting the number of glyphs in each class, + # we should determine the encoded size. If the glyphs in a large + # class form a contiguous range, the encoding is actually quite + # compact, whereas a non-contiguous set might need a lot of bytes + # in the output file. We don't get this right with the key below. + result = sorted(self.classes_, key=lambda s: (len(s), s), reverse=True) + if not self.useClass0_: + result.insert(0, frozenset()) + return result + + def build(self): + glyphClasses = {} + for classID, glyphs in enumerate(self.classes()): + if classID == 0: + continue + for glyph in glyphs: + glyphClasses[glyph] = classID + classDef = ot.ClassDef() + classDef.classDefs = glyphClasses + return classDef diff --git a/Lib/fontTools/pens/__init__.py b/Lib/fontTools/pens/__init__.py index e001bb2..3f9abc9 100644 --- a/Lib/fontTools/pens/__init__.py +++ b/Lib/fontTools/pens/__init__.py @@ -1,3 +1,4 @@ -"""Empty __init__.py file to signal Python this directory is a package. -(It can't be completely empty since WinZip seems to skip empty files.) -""" +"""Empty __init__.py file to signal Python this directory is a package.""" + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * diff --git a/Lib/fontTools/pens/areaPen.py b/Lib/fontTools/pens/areaPen.py new file mode 100644 index 0000000..564caa4 --- /dev/null +++ b/Lib/fontTools/pens/areaPen.py @@ -0,0 +1,59 @@ +"""Calculate the area of a glyph.""" + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.pens.basePen import BasePen + + +__all__ = ["AreaPen"] + + +class AreaPen(BasePen): + + def __init__(self, glyphset=None): + BasePen.__init__(self, glyphset) + self.value = 0 + + def _moveTo(self, p0): + self._p0 = self._startPoint = p0 + + def _lineTo(self, p1): + x0, y0 = self._p0 + x1, y1 = p1 + self.value -= (x1 - x0) * (y1 + y0) * .5 + self._p0 = p1 + + def _qCurveToOne(self, p1, p2): + # https://github.com/Pomax/bezierinfo/issues/44 + p0 = self._p0 + x0, y0 = p0[0], p0[1] + x1, y1 = p1[0] - x0, p1[1] - y0 + x2, y2 = p2[0] - x0, p2[1] - y0 + self.value -= (x2 * y1 - x1 * y2) / 3 + self._lineTo(p2) + self._p0 = p2 + + def _curveToOne(self, p1, p2, p3): + # https://github.com/Pomax/bezierinfo/issues/44 + p0 = self._p0 + x0, y0 = p0[0], p0[1] + x1, y1 = p1[0] - x0, p1[1] - y0 + x2, y2 = p2[0] - x0, p2[1] - y0 + x3, y3 = p3[0] - x0, p3[1] - y0 + self.value -= ( + x1 * ( - y2 - y3) + + x2 * (y1 - 2*y3) + + x3 * (y1 + 2*y2 ) + ) * 0.15 + self._lineTo(p3) + self._p0 = p3 + + def _closePath(self): + self._lineTo(self._startPoint) + del self._p0, self._startPoint + + def _endPath(self): + if self._p0 != self._startPoint: + # Area is not defined for open contours. + raise NotImplementedError + del self._p0, self._startPoint diff --git a/Lib/fontTools/pens/basePen.py b/Lib/fontTools/pens/basePen.py index eee4269..e0d3ae0 100644 --- a/Lib/fontTools/pens/basePen.py +++ b/Lib/fontTools/pens/basePen.py @@ -10,7 +10,7 @@ that drawings don't need to know the details of how outlines are stored. The most basic pattern is this: - outline.draw(pen) # 'outline' draws itself onto 'pen' + outline.draw(pen) # 'outline' draws itself onto 'pen' Pens can be used to render outlines to the screen, but also to construct new outlines. Eg. an outline object can be both a drawable object (it has a @@ -38,9 +38,10 @@ sequence of length 2 will do. from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * +from fontTools.misc.loggingTools import LogMixin -__all__ = ["AbstractPen", "NullPen", "BasePen", - "decomposeSuperBezierSegment", "decomposeQuadraticSegment"] +__all__ = ["AbstractPen", "NullPen", "BasePen", + "decomposeSuperBezierSegment", "decomposeQuadraticSegment"] class AbstractPen(object): @@ -141,7 +142,50 @@ class NullPen(object): pass -class BasePen(AbstractPen): +class LoggingPen(LogMixin, AbstractPen): + """A pen with a `log` property (see fontTools.misc.loggingTools.LogMixin) + """ + pass + + +class DecomposingPen(LoggingPen): + + """ Implements a 'addComponent' method that decomposes components + (i.e. draws them onto self as simple contours). + It can also be used as a mixin class (e.g. see ContourRecordingPen). + + You must override moveTo, lineTo, curveTo and qCurveTo. You may + additionally override closePath, endPath and addComponent. + """ + + # By default a warning message is logged when a base glyph is missing; + # set this to False if you want to raise a 'KeyError' exception + skipMissingComponents = True + + def __init__(self, glyphSet): + """ Takes a single 'glyphSet' argument (dict), in which the glyphs + that are referenced as components are looked up by their name. + """ + super(DecomposingPen, self).__init__() + self.glyphSet = glyphSet + + def addComponent(self, glyphName, transformation): + """ Transform the points of the base glyph and draw it onto self. + """ + from fontTools.pens.transformPen import TransformPen + try: + glyph = self.glyphSet[glyphName] + except KeyError: + if not self.skipMissingComponents: + raise + self.log.warning( + "glyph '%s' is missing from glyphSet; skipped" % glyphName) + else: + tPen = TransformPen(self, transformation) + glyph.draw(tPen) + + +class BasePen(DecomposingPen): """Base class for drawing pens. You must override _moveTo, _lineTo and _curveToOne. You may additionally override _closePath, _endPath, @@ -149,8 +193,8 @@ class BasePen(AbstractPen): methods. """ - def __init__(self, glyphSet): - self.glyphSet = glyphSet + def __init__(self, glyphSet=None): + super(BasePen, self).__init__(glyphSet) self.__currentPoint = None # must override @@ -186,19 +230,6 @@ class BasePen(AbstractPen): mid2y = pt2y + 0.66666666666666667 * (pt1y - pt2y) self._curveToOne((mid1x, mid1y), (mid2x, mid2y), pt2) - def addComponent(self, glyphName, transformation): - """This default implementation simply transforms the points - of the base glyph and draws it onto self. - """ - from fontTools.pens.transformPen import TransformPen - try: - glyph = self.glyphSet[glyphName] - except KeyError: - pass - else: - tPen = TransformPen(self, transformation) - glyph.draw(tPen) - # don't override def _getCurrentPoint(self): @@ -307,8 +338,8 @@ def decomposeSuperBezierSegment(points): if pt2 is None: pt2 = temp else: - pt3 = (0.5 * (pt2[0] + temp[0]), - 0.5 * (pt2[1] + temp[1])) + pt3 = (0.5 * (pt2[0] + temp[0]), + 0.5 * (pt2[1] + temp[1])) bezierSegments.append((pt1, pt2, pt3)) pt1, pt2, pt3 = temp, None, None bezierSegments.append((pt1, points[-2], points[-1])) diff --git a/Lib/fontTools/pens/boundsPen.py b/Lib/fontTools/pens/boundsPen.py index 4d14a0a..3a103e3 100644 --- a/Lib/fontTools/pens/boundsPen.py +++ b/Lib/fontTools/pens/boundsPen.py @@ -17,25 +17,42 @@ class ControlBoundsPen(BasePen): When the shape has been drawn, the bounds are available as the 'bounds' attribute of the pen object. It's a 4-tuple: - (xMin, yMin, xMax, yMax) + (xMin, yMin, xMax, yMax). + + If 'ignoreSinglePoints' is True, single points are ignored. """ - def __init__(self, glyphSet): + def __init__(self, glyphSet, ignoreSinglePoints=False): BasePen.__init__(self, glyphSet) - self.bounds = None + self.ignoreSinglePoints = ignoreSinglePoints + self.init() + + def init(self): + self.bounds = None + self._start = None def _moveTo(self, pt): + self._start = pt + if not self.ignoreSinglePoints: + self._addMoveTo() + + def _addMoveTo(self): + if self._start is None: + return bounds = self.bounds if bounds: - self.bounds = updateBounds(bounds, pt) + self.bounds = updateBounds(bounds, self._start) else: - x, y = pt + x, y = self._start self.bounds = (x, y, x, y) + self._start = None def _lineTo(self, pt): + self._addMoveTo() self.bounds = updateBounds(self.bounds, pt) def _curveToOne(self, bcp1, bcp2, pt): + self._addMoveTo() bounds = self.bounds bounds = updateBounds(bounds, bcp1) bounds = updateBounds(bounds, bcp2) @@ -43,6 +60,7 @@ class ControlBoundsPen(BasePen): self.bounds = bounds def _qCurveToOne(self, bcp, pt): + self._addMoveTo() bounds = self.bounds bounds = updateBounds(bounds, bcp) bounds = updateBounds(bounds, pt) @@ -62,6 +80,7 @@ class BoundsPen(ControlBoundsPen): """ def _curveToOne(self, bcp1, bcp2, pt): + self._addMoveTo() bounds = self.bounds bounds = updateBounds(bounds, pt) if not pointInRect(bcp1, bounds) or not pointInRect(bcp2, bounds): @@ -70,26 +89,10 @@ class BoundsPen(ControlBoundsPen): self.bounds = bounds def _qCurveToOne(self, bcp, pt): + self._addMoveTo() bounds = self.bounds bounds = updateBounds(bounds, pt) if not pointInRect(bcp, bounds): bounds = unionRect(bounds, calcQuadraticBounds( self._getCurrentPoint(), bcp, pt)) self.bounds = bounds - - -if __name__ == "__main__": - def draw(pen): - pen.moveTo((0, 0)) - pen.lineTo((0, 100)) - pen.qCurveTo((50, 75), (60, 50), (50, 25), (0, 0)) - pen.curveTo((-50, 25), (-60, 50), (-50, 75), (0, 100)) - pen.closePath() - - pen = ControlBoundsPen(None) - draw(pen) - print(pen.bounds) - - pen = BoundsPen(None) - draw(pen) - print(pen.bounds) diff --git a/Lib/fontTools/pens/filterPen.py b/Lib/fontTools/pens/filterPen.py new file mode 100644 index 0000000..2f94550 --- /dev/null +++ b/Lib/fontTools/pens/filterPen.py @@ -0,0 +1,121 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.pens.basePen import AbstractPen +from fontTools.pens.recordingPen import RecordingPen + + +class _PassThruComponentsMixin(object): + + def addComponent(self, glyphName, transformation): + self._outPen.addComponent(glyphName, transformation) + + +class FilterPen(_PassThruComponentsMixin, AbstractPen): + + """ Base class for pens that apply some transformation to the coordinates + they receive and pass them to another pen. + + You can override any of its methods. The default implementation does + nothing, but passes the commands unmodified to the other pen. + + >>> from fontTools.pens.recordingPen import RecordingPen + >>> rec = RecordingPen() + >>> pen = FilterPen(rec) + >>> v = iter(rec.value) + + >>> pen.moveTo((0, 0)) + >>> next(v) + ('moveTo', ((0, 0),)) + + >>> pen.lineTo((1, 1)) + >>> next(v) + ('lineTo', ((1, 1),)) + + >>> pen.curveTo((2, 2), (3, 3), (4, 4)) + >>> next(v) + ('curveTo', ((2, 2), (3, 3), (4, 4))) + + >>> pen.qCurveTo((5, 5), (6, 6), (7, 7), (8, 8)) + >>> next(v) + ('qCurveTo', ((5, 5), (6, 6), (7, 7), (8, 8))) + + >>> pen.closePath() + >>> next(v) + ('closePath', ()) + + >>> pen.moveTo((9, 9)) + >>> next(v) + ('moveTo', ((9, 9),)) + + >>> pen.endPath() + >>> next(v) + ('endPath', ()) + + >>> pen.addComponent('foo', (1, 0, 0, 1, 0, 0)) + >>> next(v) + ('addComponent', ('foo', (1, 0, 0, 1, 0, 0))) + """ + + def __init__(self, outPen): + self._outPen = outPen + + def moveTo(self, pt): + self._outPen.moveTo(pt) + + def lineTo(self, pt): + self._outPen.lineTo(pt) + + def curveTo(self, *points): + self._outPen.curveTo(*points) + + def qCurveTo(self, *points): + self._outPen.qCurveTo(*points) + + def closePath(self): + self._outPen.closePath() + + def endPath(self): + self._outPen.endPath() + + +class ContourFilterPen(_PassThruComponentsMixin, RecordingPen): + """A "buffered" filter pen that accumulates contour data, passes + it through a ``filterContour`` method when the contour is closed or ended, + and finally draws the result with the output pen. + + Components are passed through unchanged. + """ + + def __init__(self, outPen): + super(ContourFilterPen, self).__init__() + self._outPen = outPen + + def closePath(self): + super(ContourFilterPen, self).closePath() + self._flushContour() + + def endPath(self): + super(ContourFilterPen, self).endPath() + self._flushContour() + + def _flushContour(self): + result = self.filterContour(self.value) + if result is not None: + self.value = result + self.replay(self._outPen) + self.value = [] + + def filterContour(self, contour): + """Subclasses must override this to perform the filtering. + + The contour is a list of pen (operator, operands) tuples. + Operators are strings corresponding to the AbstractPen methods: + "moveTo", "lineTo", "curveTo", "qCurveTo", "closePath" and + "endPath". The operands are the positional arguments that are + passed to each method. + + If the method doesn't return a value (i.e. returns None), it's + assumed that the argument was modified in-place. + Otherwise, the return value is drawn with the output pen. + """ + return # or return contour diff --git a/Lib/fontTools/pens/momentsPen.py b/Lib/fontTools/pens/momentsPen.py new file mode 100644 index 0000000..b83102c --- /dev/null +++ b/Lib/fontTools/pens/momentsPen.py @@ -0,0 +1,294 @@ +"""Pen calculating 0th, 1st, and 2nd moments of area of glyph shapes. +This is low-level, autogenerated pen. Use statisticsPen instead.""" +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.pens.basePen import BasePen + + +__all__ = ["MomentsPen"] + + +class MomentsPen(BasePen): + + def __init__(self, glyphset=None): + BasePen.__init__(self, glyphset) + + self.area = 0 + self.momentX = 0 + self.momentY = 0 + self.momentXX = 0 + self.momentXY = 0 + self.momentYY = 0 + + def _moveTo(self, p0): + self.__startPoint = p0 + + def _closePath(self): + p0 = self._getCurrentPoint() + if p0 != self.__startPoint: + self._lineTo(self.__startPoint) + + def _endPath(self): + p0 = self._getCurrentPoint() + if p0 != self.__startPoint: + # Green theorem is not defined on open contours. + raise NotImplementedError + + def _lineTo(self, p1): + x0,y0 = self._getCurrentPoint() + x1,y1 = p1 + + r0 = x1*y0 + r1 = x1*y1 + r2 = x1**2 + r3 = x0**2 + r4 = 2*y0 + r5 = y0 - y1 + r6 = r5*x0 + r7 = y0**2 + r8 = y1**2 + r9 = x1**3 + r10 = r4*y1 + r11 = y0**3 + r12 = y1**3 + + self.area += -r0/2 - r1/2 + x0*(y0 + y1)/2 + self.momentX += -r2*y0/6 - r2*y1/3 + r3*(r4 + y1)/6 - r6*x1/6 + self.momentY += -r0*y1/6 - r7*x1/6 - r8*x1/6 + x0*(r7 + r8 + y0*y1)/6 + self.momentXX += -r2*r6/12 - r3*r5*x1/12 - r9*y0/12 - r9*y1/4 + x0**3*(3*y0 + y1)/12 + self.momentXY += -r10*r2/24 - r2*r7/24 - r2*r8/8 + r3*(r10 + 3*r7 + r8)/24 - x0*x1*(r7 - r8)/12 + self.momentYY += -r0*r8/12 - r1*r7/12 - r11*x1/12 - r12*x1/12 + x0*(r11 + r12 + r7*y1 + r8*y0)/12 + + def _qCurveToOne(self, p1, p2): + x0,y0 = self._getCurrentPoint() + x1,y1 = p1 + x2,y2 = p2 + + r0 = 2*x1 + r1 = r0*y2 + r2 = 2*y1 + r3 = r2*x2 + r4 = 3*y2 + r5 = r4*x2 + r6 = 3*y0 + r7 = x1**2 + r8 = 2*y2 + r9 = x2**2 + r10 = 4*y1 + r11 = 10*y2 + r12 = r0*x2 + r13 = x0**2 + r14 = 10*y0 + r15 = x2*y2 + r16 = r0*y1 + r15 + r17 = 4*x1 + r18 = x2*y0 + r19 = r10*r15 + r20 = y1**2 + r21 = 2*r20 + r22 = y2**2 + r23 = r22*x2 + r24 = 5*r23 + r25 = y0**2 + r26 = y0*y2 + r27 = 5*r25 + r28 = 8*x1**3 + r29 = x2**3 + r30 = 30*y1 + r31 = 6*y1 + r32 = 10*r9*x1 + r33 = 4*r7 + r34 = 5*y2 + r35 = 12*r7 + r36 = r5 + 20*x1*y1 + r37 = 30*x1 + r38 = 12*x1 + r39 = 20*r7 + r40 = 8*r7*y1 + r41 = r34*r9 + r42 = 60*y1 + r43 = 20*r20 + r44 = 4*r20 + r45 = 15*r22 + r46 = r38*x2 + r47 = y1*y2 + r48 = 8*r20*x1 + r24 + r49 = 6*x1 + r50 = 8*y1**3 + r51 = y2**3 + r52 = y0**3 + r53 = 10*y1 + r54 = 12*y1 + r55 = 12*r20 + + self.area += r1/6 - r3/6 - r5/6 + x0*(r2 + r6 + y2)/6 - y0*(r0 + x2)/6 + self.momentX += -r10*r9/30 - r11*r9/30 - r12*(-r8 + y1)/30 + r13*(r10 + r14 + y2)/30 + r7*r8/30 + x0*(r1 + r16 - r17*y0 - r18)/30 - y0*(r12 + 2*r7 + r9)/30 + self.momentY += r1*(r8 + y1)/30 - r19/30 - r21*x2/30 - r24/30 - r25*(r17 + x2)/30 + x0*(r10*y0 + r2*y2 + r21 + r22 + r26 + r27)/30 - y0*(r16 + r3)/30 + self.momentXX += r13*(r11*x1 - 5*r18 + r3 + r36 - r37*y0)/420 + r28*y2/420 - r29*r30/420 - r29*y2/4 - r32*(r2 - r4)/420 - r33*x2*(r2 - r34)/420 + x0**3*(r31 + 21*y0 + y2)/84 - x0*(-r15*r38 + r18*r38 + r2*r9 - r35*y2 + r39*y0 - r40 - r41 + r6*r9)/420 - y0*(r28 + 5*r29 + r32 + r35*x2)/420 + self.momentXY += r13*(r14*y2 + 3*r22 + 105*r25 + r42*y0 + r43 + 12*r47)/840 - r17*x2*(r44 - r45)/840 - r22*r9/8 - r25*(r39 + r46 + 3*r9)/840 + r33*y2*(r10 + r34)/840 - r42*r9*y2/840 - r43*r9/840 + x0*(-r10*r18 + r17*r26 + r19 + r22*r49 - r25*r37 - r27*x2 + r38*r47 + r48)/420 - y0*(r15*r17 + r31*r9 + r40 + r41 + r46*y1)/420 + self.momentYY += r1*(r11*y1 + r44 + r45)/420 - r15*r43/420 - r23*r30/420 - r25*(r1 + r36 + r53*x2)/420 - r50*x2/420 - r51*x2/12 - r52*(r49 + x2)/84 + x0*(r22*r53 + r22*r6 + r25*r30 + r25*r34 + r26*r54 + r43*y0 + r50 + 5*r51 + 35*r52 + r55*y2)/420 - y0*(-r0*r22 + r15*r54 + r48 + r55*x2)/420 + + def _curveToOne(self, p1, p2, p3): + x0,y0 = self._getCurrentPoint() + x1,y1 = p1 + x2,y2 = p2 + x3,y3 = p3 + + r0 = 6*x2 + r1 = r0*y3 + r2 = 6*y2 + r3 = 10*y3 + r4 = r3*x3 + r5 = 3*x1 + r6 = 3*y1 + r7 = 6*x1 + r8 = 3*x2 + r9 = 6*y1 + r10 = 3*y2 + r11 = x2**2 + r12 = r11*y3 + r13 = 45*r12 + r14 = x3**2 + r15 = r14*y2 + r16 = r14*y3 + r17 = x2*x3 + r18 = 15*r17 + r19 = 7*y3 + r20 = x1**2 + r21 = 9*r20 + r22 = x0**2 + r23 = 21*y1 + r24 = 9*r11 + r25 = 9*x2 + r26 = x2*y3 + r27 = 15*r26 + r28 = -r25*y1 + r27 + r29 = r25*y2 + r30 = r9*x3 + r31 = 45*x1 + r32 = x1*x3 + r33 = 45*r20 + r34 = 5*r14 + r35 = x2*y2 + r36 = 18*r35 + r37 = 5*x3 + r38 = r37*y3 + r39 = r31*y1 + r36 + r38 + r40 = x1*y0 + r41 = x1*y3 + r42 = x2*y0 + r43 = x3*y1 + r44 = r10*x3 + r45 = x3*y2*y3 + r46 = y2**2 + r47 = 45*r46 + r48 = r47*x3 + r49 = y3**2 + r50 = r49*x3 + r51 = y1**2 + r52 = 9*r51 + r53 = y0**2 + r54 = 21*x1 + r55 = x3*y2 + r56 = 15*r55 + r57 = 9*y2 + r58 = y2*y3 + r59 = 15*r58 + r60 = 9*r46 + r61 = 3*y3 + r62 = 45*y1 + r63 = r8*y3 + r64 = y0*y1 + r65 = y0*y2 + r66 = 30*r65 + r67 = 5*y3 + r68 = y1*y3 + r69 = 45*r51 + r70 = 5*r49 + r71 = x2**3 + r72 = x3**3 + r73 = 126*x3 + r74 = x1**3 + r75 = r14*x2 + r76 = 63*r11 + r77 = r76*x3 + r78 = 15*r35 + r79 = r19*x3 + r80 = x1*y1 + r81 = 63*r35 + r82 = r38 + 378*r80 + r81 + r83 = x1*y2 + r84 = x2*y1 + r85 = x3*y0 + r86 = x2*x3*y1 + r87 = x2*x3*y3 + r88 = r11*y2 + r89 = 27*r88 + r90 = 42*y3 + r91 = r14*r90 + r92 = 90*x1*x2 + r93 = 189*x2 + r94 = 30*x1*x3 + r95 = 14*r16 + 126*r20*y1 + 45*r88 + r94*y2 + r96 = x1*x2 + r97 = 252*r96 + r98 = x1*x2*y2 + r99 = 42*r32 + r100 = x1*x3*y1 + r101 = 30*r17 + r102 = 18*r17 + r103 = 378*r20 + r104 = 189*y2 + r105 = r20*y3 + r106 = r11*y1 + r107 = r14*y1 + r108 = 378*r46 + r109 = 252*y2 + r110 = y1*y2 + r111 = x2*x3*y2 + r112 = y0*y3 + r113 = 378*r51 + r114 = 63*r46 + r115 = 27*x2 + r116 = r115*r46 + 42*r50 + r117 = x2*y1*y3 + r118 = x3*y1*y2 + r119 = r49*x2 + r120 = r51*x3 + r121 = x3*y3 + r122 = 14*x3 + r123 = 30*r117 + r122*r49 + r47*x2 + 126*r51*x1 + r124 = x1*y1*y3 + r125 = x1*y2*y3 + r126 = x2*y1*y2 + r127 = 54*y3 + r128 = 21*r55 + r129 = 630*r53 + r130 = r46*x1 + r131 = r49*x1 + r132 = 126*r53 + r133 = y2**3 + r134 = y3**3 + r135 = 630*r49 + r136 = y1**3 + r137 = y0**3 + r138 = r114*y3 + r23*r49 + r139 = r49*y2 + + self.area += r1/20 - r2*x3/20 - r4/20 + r5*(y2 + y3)/20 - r6*(x2 + x3)/20 + x0*(r10 + r9 + 10*y0 + y3)/20 - y0*(r7 + r8 + x3)/20 + self.momentX += r13/840 - r15/8 - r16/3 - r18*(r10 - r19)/840 + r21*(r10 + 2*y3)/840 + r22*(r2 + r23 + 56*y0 + y3)/168 + r5*(r28 + r29 - r30 + r4)/840 - r6*(10*r14 + r18 + r24)/840 + x0*(12*r26 + r31*y2 - r37*y0 + r39 - 105*r40 + 15*r41 - 30*r42 - 3*r43 + r44)/840 - y0*(18*r11 + r18 + r31*x2 + 12*r32 + r33 + r34)/840 + self.momentY += r27*(r10 + r19)/840 - r45/8 - r48/840 + r5*(10*r49 + r57*y1 + r59 + r60 + r9*y3)/840 - r50/6 - r52*(r8 + 2*x3)/840 - r53*(r0 + r54 + x3)/168 - r6*(r29 + r4 + r56)/840 + x0*(18*r46 + 140*r53 + r59 + r62*y2 + 105*r64 + r66 + r67*y0 + 12*r68 + r69 + r70)/840 - y0*(r39 + 15*r43 + 12*r55 - r61*x1 + r62*x2 + r63)/840 + self.momentXX += -r11*r73*(-r61 + y2)/9240 + r21*(r28 - r37*y1 + r44 + r78 + r79)/9240 + r22*(21*r26 - 630*r40 + 42*r41 - 126*r42 + r57*x3 + r82 + 210*r83 + 42*r84 - 14*r85)/9240 - r5*(r11*r62 + r14*r23 + 14*r15 - r76*y3 + 54*r86 - 84*r87 - r89 - r91)/9240 - r6*(27*r71 + 42*r72 + 70*r75 + r77)/9240 + 3*r71*y3/220 - 3*r72*y2/44 - r72*y3/4 + 3*r74*(r57 + r67)/3080 - r75*(378*y2 - 630*y3)/9240 + x0**3*(r57 + r62 + 165*y0 + y3)/660 + x0*(-18*r100 - r101*y0 - r101*y1 + r102*y2 - r103*y0 + r104*r20 + 63*r105 - 27*r106 - 9*r107 + r13 - r34*y0 - r76*y0 + 42*r87 + r92*y3 + r94*y3 + r95 - r97*y0 + 162*r98 - r99*y0)/9240 - y0*(135*r11*x1 + r14*r54 + r20*r93 + r33*x3 + 45*r71 + 14*r72 + 126*r74 + 42*r75 + r77 + r92*x3)/9240 + self.momentXY += -r108*r14/18480 + r12*(r109 + 378*y3)/18480 - r14*r49/8 - 3*r14*r58/44 - r17*(252*r46 - 1260*r49)/18480 + r21*(18*r110 + r3*y1 + 15*r46 + 7*r49 + 18*r58)/18480 + r22*(252*r110 + 28*r112 + r113 + r114 + 2310*r53 + 30*r58 + 1260*r64 + 252*r65 + 42*r68 + r70)/18480 - r52*(r102 + 15*r11 + 7*r14)/18480 - r53*(r101 + r103 + r34 + r76 + r97 + r99)/18480 + r7*(-r115*r51 + r116 + 18*r117 - 18*r118 + 42*r119 - 15*r120 + 28*r45 + r81*y3)/18480 - r9*(63*r111 + 42*r15 + 28*r87 + r89 + r91)/18480 + x0*(r1*y0 + r104*r80 + r112*r54 + 21*r119 - 9*r120 - r122*r53 + r123 + 54*r124 + 60*r125 + 54*r126 + r127*r35 + r128*y3 - r129*x1 + 81*r130 + 15*r131 - r132*x2 - r2*r85 - r23*r85 + r30*y3 + 84*r40*y2 - 84*r42*y1 + r60*x3)/9240 - y0*(54*r100 - 9*r105 + 81*r106 + 15*r107 + 54*r111 + r121*r7 + 21*r15 + r24*y3 + 60*r86 + 21*r87 + r95 + 189*r96*y1 + 54*r98)/9240 + self.momentYY += -r108*r121/9240 - r133*r73/9240 - r134*x3/12 - r135*r55/9240 - 3*r136*(r25 + r37)/3080 - r137*(r25 + r31 + x3)/660 + r26*(r135 + 126*r46 + 378*y2*y3)/9240 + r5*(r110*r127 + 27*r133 + 42*r134 + r138 + 70*r139 + r46*r62 + 27*r51*y2 + 15*r51*y3)/9240 - r52*(r56 + r63 + r78 + r79)/9240 - r53*(r128 + r25*y3 + 42*r43 + r82 + 42*r83 + 210*r84)/9240 - r6*(r114*x3 + r116 - 14*r119 + 84*r45)/9240 + x0*(r104*r51 + r109*r64 + 90*r110*y3 + r113*y0 + r114*y0 + r129*y1 + r132*y2 + 45*r133 + 14*r134 + 126*r136 + 770*r137 + r138 + 42*r139 + 135*r46*y1 + 14*r53*y3 + r64*r90 + r66*y3 + r69*y3 + r70*y0)/9240 - y0*(90*r118 + 63*r120 + r123 - 18*r124 - 30*r125 + 162*r126 - 27*r130 - 9*r131 + r36*y3 + 30*r43*y3 + 42*r45 + r48 + r51*r93)/9240 + +if __name__ == '__main__': + from fontTools.misc.symfont import x, y, printGreenPen + printGreenPen('MomentsPen', [ + ('area', 1), + ('momentX', x), + ('momentY', y), + ('momentXX', x**2), + ('momentXY', x*y), + ('momentYY', y**2), + ]) diff --git a/Lib/fontTools/pens/perimeterPen.py b/Lib/fontTools/pens/perimeterPen.py new file mode 100644 index 0000000..12e7e47 --- /dev/null +++ b/Lib/fontTools/pens/perimeterPen.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +"""Calculate the perimeter of a glyph.""" + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.pens.basePen import BasePen +from fontTools.misc.bezierTools import approximateQuadraticArcLengthC, calcQuadraticArcLengthC, approximateCubicArcLengthC, calcCubicArcLengthC +import math + + +__all__ = ["PerimeterPen"] + + +def _distance(p0, p1): + return math.hypot(p0[0] - p1[0], p0[1] - p1[1]) + +class PerimeterPen(BasePen): + + def __init__(self, glyphset=None, tolerance=0.005): + BasePen.__init__(self, glyphset) + self.value = 0 + self.tolerance = tolerance + + # Choose which algorithm to use for quadratic and for cubic. + # Quadrature is faster but has fixed error characteristic with no strong + # error bound. The cutoff points are derived empirically. + self._addCubic = self._addCubicQuadrature if tolerance >= 0.0015 else self._addCubicRecursive + self._addQuadratic = self._addQuadraticQuadrature if tolerance >= 0.00075 else self._addQuadraticExact + + def _moveTo(self, p0): + self.__startPoint = p0 + + def _closePath(self): + p0 = self._getCurrentPoint() + if p0 != self.__startPoint: + self._lineTo(self.__startPoint) + + def _lineTo(self, p1): + p0 = self._getCurrentPoint() + self.value += _distance(p0, p1) + + def _addQuadraticExact(self, c0, c1, c2): + self.value += calcQuadraticArcLengthC(c0, c1, c2) + + def _addQuadraticQuadrature(self, c0, c1, c2): + self.value += approximateQuadraticArcLengthC(c0, c1, c2) + + def _qCurveToOne(self, p1, p2): + p0 = self._getCurrentPoint() + self._addQuadratic(complex(*p0), complex(*p1), complex(*p2)) + + def _addCubicRecursive(self, c0, c1, c2, c3): + self.value += calcCubicArcLengthC(c0, c1, c2, c3, self.tolerance) + + def _addCubicQuadrature(self, c0, c1, c2, c3): + self.value += approximateCubicArcLengthC(c0, c1, c2, c3) + + def _curveToOne(self, p1, p2, p3): + p0 = self._getCurrentPoint() + self._addCubic(complex(*p0), complex(*p1), complex(*p2), complex(*p3)) diff --git a/Lib/fontTools/pens/pointInsidePen.py b/Lib/fontTools/pens/pointInsidePen.py index 0b3373f..3311841 100644 --- a/Lib/fontTools/pens/pointInsidePen.py +++ b/Lib/fontTools/pens/pointInsidePen.py @@ -11,12 +11,6 @@ from fontTools.misc.bezierTools import solveQuadratic, solveCubic __all__ = ["PointInsidePen"] -# working around floating point errors -EPSILON = 1e-10 -ONE_PLUS_EPSILON = 1 + EPSILON -ZERO_MINUS_EPSILON = 0 - EPSILON - - class PointInsidePen(BasePen): """This pen implements "point inside" testing: to test whether @@ -46,29 +40,33 @@ class PointInsidePen(BasePen): # http://graphics.cs.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html # I extended the principles outlined on that page to curves. - def __init__(self, glyphSet, testPoint, evenOdd=0): + def __init__(self, glyphSet, testPoint, evenOdd=False): BasePen.__init__(self, glyphSet) self.setTestPoint(testPoint, evenOdd) - def setTestPoint(self, testPoint, evenOdd=0): + def setTestPoint(self, testPoint, evenOdd=False): """Set the point to test. Call this _before_ the outline gets drawn.""" self.testPoint = testPoint self.evenOdd = evenOdd self.firstPoint = None self.intersectionCount = 0 - def getResult(self): - """After the shape has been drawn, getResult() returns True if the test - point lies within the (black) shape, and False if it doesn't. - """ + def getWinding(self): if self.firstPoint is not None: # always make sure the sub paths are closed; the algorithm only works # for closed paths. self.closePath() + return self.intersectionCount + + def getResult(self): + """After the shape has been drawn, getResult() returns True if the test + point lies within the (black) shape, and False if it doesn't. + """ + winding = self.getWinding() if self.evenOdd: - result = self.intersectionCount % 2 - else: - result = self.intersectionCount + result = winding % 2 + else: # non-zero + result = self.intersectionCount != 0 return not not result def _addIntersection(self, goingUp): @@ -123,7 +121,7 @@ class PointInsidePen(BasePen): by = (y3 - y2) * 3.0 - cy ay = y4 - dy - cy - by solutions = sorted(solveCubic(ay, by, cy, dy - y)) - solutions = [t for t in solutions if ZERO_MINUS_EPSILON <= t <= ONE_PLUS_EPSILON] + solutions = [t for t in solutions if -0. <= t <= 1.] if not solutions: return @@ -142,29 +140,30 @@ class PointInsidePen(BasePen): t3 = t2 * t direction = 3*ay*t2 + 2*by*t + cy + incomingGoingUp = outgoingGoingUp = direction > 0.0 if direction == 0.0: direction = 6*ay*t + 2*by + outgoingGoingUp = direction > 0.0 + incomingGoingUp = not outgoingGoingUp if direction == 0.0: direction = ay - goingUp = direction > 0.0 + incomingGoingUp = outgoingGoingUp = direction > 0.0 xt = ax*t3 + bx*t2 + cx*t + dx if xt < x: - above = goingUp continue - if t == 0.0: - if not goingUp: - self._addIntersection(goingUp) + if t in (0.0, -0.0): + if not outgoingGoingUp: + self._addIntersection(outgoingGoingUp) elif t == 1.0: - if not above: - self._addIntersection(goingUp) + if incomingGoingUp: + self._addIntersection(incomingGoingUp) else: - if above != goingUp: - self._addIntersection(goingUp) + if incomingGoingUp == outgoingGoingUp: + self._addIntersection(outgoingGoingUp) #else: - # we're not really intersecting, merely touching the 'top' - above = goingUp + # we're not really intersecting, merely touching def _qCurveToOne_unfinished(self, bcp, point): # XXX need to finish this, for now doing it through a cubic @@ -181,11 +180,13 @@ class PointInsidePen(BasePen): solutions = [t for t in solutions if ZERO_MINUS_EPSILON <= t <= ONE_PLUS_EPSILON] if not solutions: return - XXX + # XXX def _closePath(self): if self._getCurrentPoint() != self.firstPoint: self.lineTo(self.firstPoint) self.firstPoint = None - _endPath = _closePath + def _endPath(self): + """Insideness is not defined for open contours.""" + raise NotImplementedError diff --git a/Lib/fontTools/pens/qtPen.py b/Lib/fontTools/pens/qtPen.py new file mode 100644 index 0000000..01cb37a --- /dev/null +++ b/Lib/fontTools/pens/qtPen.py @@ -0,0 +1,31 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.pens.basePen import BasePen + + +__all__ = ["QtPen"] + + +class QtPen(BasePen): + + def __init__(self, glyphSet, path=None): + BasePen.__init__(self, glyphSet) + if path is None: + from PyQt5.QtGui import QPainterPath + path = QPainterPath() + self.path = path + + def _moveTo(self, p): + self.path.moveTo(*p) + + def _lineTo(self, p): + self.path.lineTo(*p) + + def _curveToOne(self, p1, p2, p3): + self.path.cubicTo(*p1+p2+p3) + + def _qCurveToOne(self, p1, p2): + self.path.quadTo(*p1+p2) + + def _closePath(self): + self.path.closeSubpath() diff --git a/Lib/fontTools/pens/recordingPen.py b/Lib/fontTools/pens/recordingPen.py new file mode 100644 index 0000000..e583c8f --- /dev/null +++ b/Lib/fontTools/pens/recordingPen.py @@ -0,0 +1,101 @@ +"""Pen recording operations that can be accessed or replayed.""" +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.pens.basePen import AbstractPen, DecomposingPen + + +__all__ = ["replayRecording", "RecordingPen", "DecomposingRecordingPen"] + + +def replayRecording(recording, pen): + """Replay a recording, as produced by RecordingPen or DecomposingRecordingPen, + to a pen. + + Note that recording does not have to be produced by those pens. + It can be any iterable of tuples of method name and tuple-of-arguments. + Likewise, pen can be any objects receiving those method calls. + """ + for operator,operands in recording: + getattr(pen, operator)(*operands) + + +class RecordingPen(AbstractPen): + """Pen recording operations that can be accessed or replayed. + + The recording can be accessed as pen.value; or replayed using + pen.replay(otherPen). + + Usage example: + ============== + from fontTools.ttLib import TTFont + from fontTools.pens.recordingPen import RecordingPen + + glyph_name = 'dollar' + font_path = 'MyFont.otf' + + font = TTFont(font_path) + glyphset = font.getGlyphSet() + glyph = glyphset[glyph_name] + + pen = RecordingPen() + glyph.draw(pen) + print(pen.value) + """ + + def __init__(self): + self.value = [] + def moveTo(self, p0): + self.value.append(('moveTo', (p0,))) + def lineTo(self, p1): + self.value.append(('lineTo', (p1,))) + def qCurveTo(self, *points): + self.value.append(('qCurveTo', points)) + def curveTo(self, *points): + self.value.append(('curveTo', points)) + def closePath(self): + self.value.append(('closePath', ())) + def endPath(self): + self.value.append(('endPath', ())) + def addComponent(self, glyphName, transformation): + self.value.append(('addComponent', (glyphName, transformation))) + def replay(self, pen): + replayRecording(self.value, pen) + + +class DecomposingRecordingPen(DecomposingPen, RecordingPen): + """ Same as RecordingPen, except that it doesn't keep components + as references, but draws them decomposed as regular contours. + + The constructor takes a single 'glyphSet' positional argument, + a dictionary of glyph objects (i.e. with a 'draw' method) keyed + by thir name. + + >>> class SimpleGlyph(object): + ... def draw(self, pen): + ... pen.moveTo((0, 0)) + ... pen.curveTo((1, 1), (2, 2), (3, 3)) + ... pen.closePath() + >>> class CompositeGlyph(object): + ... def draw(self, pen): + ... pen.addComponent('a', (1, 0, 0, 1, -1, 1)) + >>> glyphSet = {'a': SimpleGlyph(), 'b': CompositeGlyph()} + >>> for name, glyph in sorted(glyphSet.items()): + ... pen = DecomposingRecordingPen(glyphSet) + ... glyph.draw(pen) + ... print("{}: {}".format(name, pen.value)) + a: [('moveTo', ((0, 0),)), ('curveTo', ((1, 1), (2, 2), (3, 3))), ('closePath', ())] + b: [('moveTo', ((-1, 1),)), ('curveTo', ((0, 2), (1, 3), (2, 4))), ('closePath', ())] + """ + # raises KeyError if base glyph is not found in glyphSet + skipMissingComponents = False + + +if __name__ == "__main__": + from fontTools.pens.basePen import _TestPen + pen = RecordingPen() + pen.moveTo((0, 0)) + pen.lineTo((0, 100)) + pen.curveTo((50, 75), (60, 50), (50, 25)) + pen.closePath() + from pprint import pprint + pprint(pen.value) diff --git a/Lib/fontTools/pens/reportLabPen.py b/Lib/fontTools/pens/reportLabPen.py index 60792f7..2068b25 100644 --- a/Lib/fontTools/pens/reportLabPen.py +++ b/Lib/fontTools/pens/reportLabPen.py @@ -4,6 +4,9 @@ from fontTools.pens.basePen import BasePen from reportlab.graphics.shapes import Path +__all__ = ["ReportLabPen"] + + class ReportLabPen(BasePen): """A pen for drawing onto a reportlab.graphics.shapes.Path object.""" diff --git a/Lib/fontTools/pens/reverseContourPen.py b/Lib/fontTools/pens/reverseContourPen.py new file mode 100644 index 0000000..27e54d7 --- /dev/null +++ b/Lib/fontTools/pens/reverseContourPen.py @@ -0,0 +1,97 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.arrayTools import pairwise +from fontTools.pens.filterPen import ContourFilterPen + + +__all__ = ["reversedContour", "ReverseContourPen"] + + +class ReverseContourPen(ContourFilterPen): + """Filter pen that passes outline data to another pen, but reversing + the winding direction of all contours. Components are simply passed + through unchanged. + + Closed contours are reversed in such a way that the first point remains + the first point. + """ + + def filterContour(self, contour): + return reversedContour(contour) + + +def reversedContour(contour): + """ Generator that takes a list of pen's (operator, operands) tuples, + and yields them with the winding direction reversed. + """ + if not contour: + return # nothing to do, stop iteration + + # valid contours must have at least a starting and ending command, + # can't have one without the other + assert len(contour) > 1, "invalid contour" + + # the type of the last command determines if the contour is closed + contourType = contour.pop()[0] + assert contourType in ("endPath", "closePath") + closed = contourType == "closePath" + + firstType, firstPts = contour.pop(0) + assert firstType in ("moveTo", "qCurveTo"), ( + "invalid initial segment type: %r" % firstType) + firstOnCurve = firstPts[-1] + if firstType == "qCurveTo": + # special case for TrueType paths contaning only off-curve points + assert firstOnCurve is None, ( + "off-curve only paths must end with 'None'") + assert not contour, ( + "only one qCurveTo allowed per off-curve path") + firstPts = ((firstPts[0],) + tuple(reversed(firstPts[1:-1])) + + (None,)) + + if not contour: + # contour contains only one segment, nothing to reverse + if firstType == "moveTo": + closed = False # single-point paths can't be closed + else: + closed = True # off-curve paths are closed by definition + yield firstType, firstPts + else: + lastType, lastPts = contour[-1] + lastOnCurve = lastPts[-1] + if closed: + # for closed paths, we keep the starting point + yield firstType, firstPts + if firstOnCurve != lastOnCurve: + # emit an implied line between the last and first points + yield "lineTo", (lastOnCurve,) + contour[-1] = (lastType, + tuple(lastPts[:-1]) + (firstOnCurve,)) + + if len(contour) > 1: + secondType, secondPts = contour[0] + else: + # contour has only two points, the second and last are the same + secondType, secondPts = lastType, lastPts + # if a lineTo follows the initial moveTo, after reversing it + # will be implied by the closePath, so we don't emit one; + # unless the lineTo and moveTo overlap, in which case we keep the + # duplicate points + if secondType == "lineTo" and firstPts != secondPts: + del contour[0] + if contour: + contour[-1] = (lastType, + tuple(lastPts[:-1]) + secondPts) + else: + # for open paths, the last point will become the first + yield firstType, (lastOnCurve,) + contour[-1] = (lastType, tuple(lastPts[:-1]) + (firstOnCurve,)) + + # we iterate over all segment pairs in reverse order, and yield + # each one with the off-curve points reversed (if any), and + # with the on-curve point of the following segment + for (curType, curPts), (_, nextPts) in pairwise( + contour, reverse=True): + yield curType, tuple(reversed(curPts[:-1])) + (nextPts[-1],) + + yield "closePath" if closed else "endPath", () diff --git a/Lib/fontTools/pens/statisticsPen.py b/Lib/fontTools/pens/statisticsPen.py new file mode 100644 index 0000000..7c2e85c --- /dev/null +++ b/Lib/fontTools/pens/statisticsPen.py @@ -0,0 +1,102 @@ +"""Pen calculating area, center of mass, variance and standard-deviation, +covariance and correlation, and slant, of glyph shapes.""" +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +import math +from fontTools.pens.momentsPen import MomentsPen + +__all__ = ["StatisticsPen"] + + +class StatisticsPen(MomentsPen): + + """Pen calculating area, center of mass, variance and + standard-deviation, covariance and correlation, and slant, + of glyph shapes. + + Note that all the calculated values are 'signed'. Ie. if the + glyph shape is self-intersecting, the values are not correct + (but well-defined). As such, area will be negative if contour + directions are clockwise. Moreover, variance might be negative + if the shapes are self-intersecting in certain ways.""" + + def __init__(self, glyphset=None): + MomentsPen.__init__(self, glyphset=glyphset) + self.__zero() + + def _closePath(self): + MomentsPen._closePath(self) + self.__update() + + def __zero(self): + self.meanX = 0 + self.meanY = 0 + self.varianceX = 0 + self.varianceY = 0 + self.stddevX = 0 + self.stddevY = 0 + self.covariance = 0 + self.correlation = 0 + self.slant = 0 + + def __update(self): + + area = self.area + if not area: + self.__zero() + return + + # Center of mass + # https://en.wikipedia.org/wiki/Center_of_mass#A_continuous_volume + self.meanX = meanX = self.momentX / area + self.meanY = meanY = self.momentY / area + + # Var(X) = E[X^2] - E[X]^2 + self.varianceX = varianceX = self.momentXX / area - meanX**2 + self.varianceY = varianceY = self.momentYY / area - meanY**2 + + self.stddevX = stddevX = math.copysign(abs(varianceX)**.5, varianceX) + self.stddevY = stddevY = math.copysign(abs(varianceY)**.5, varianceY) + + # Covariance(X,Y) = ( E[X.Y] - E[X]E[Y] ) + self.covariance = covariance = self.momentXY / area - meanX*meanY + + # Correlation(X,Y) = Covariance(X,Y) / ( stddev(X) * stddev(Y) ) + # https://en.wikipedia.org/wiki/Pearson_product-moment_correlation_coefficient + correlation = covariance / (stddevX * stddevY) + self.correlation = correlation if abs(correlation) > 1e-3 else 0 + + slant = covariance / varianceY + self.slant = slant if abs(slant) > 1e-3 else 0 + + +def _test(glyphset, upem, glyphs): + from fontTools.pens.transformPen import TransformPen + from fontTools.misc.transform import Scale + + print('upem', upem) + + for glyph_name in glyphs: + print() + print("glyph:", glyph_name) + glyph = glyphset[glyph_name] + pen = StatisticsPen(glyphset=glyphset) + transformer = TransformPen(pen, Scale(1./upem)) + glyph.draw(transformer) + for item in ['area', 'momentX', 'momentY', 'momentXX', 'momentYY', 'momentXY', 'meanX', 'meanY', 'varianceX', 'varianceY', 'stddevX', 'stddevY', 'covariance', 'correlation', 'slant']: + if item[0] == '_': continue + print ("%s: %g" % (item, getattr(pen, item))) + +def main(args): + if not args: + return + filename, glyphs = args[0], args[1:] + if not glyphs: + glyphs = ['e', 'o', 'I', 'slash', 'E', 'zero', 'eight', 'minus', 'equal'] + from fontTools.ttLib import TTFont + font = TTFont(filename) + _test(font.getGlyphSet(), font['head'].unitsPerEm, glyphs) + +if __name__ == '__main__': + import sys + main(sys.argv[1:]) diff --git a/Lib/fontTools/pens/svgPathPen.py b/Lib/fontTools/pens/svgPathPen.py new file mode 100644 index 0000000..17e4ccc --- /dev/null +++ b/Lib/fontTools/pens/svgPathPen.py @@ -0,0 +1,178 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.pens.basePen import BasePen + + +def pointToString(pt): + return " ".join([str(i) for i in pt]) + + +class SVGPathPen(BasePen): + + def __init__(self, glyphSet): + BasePen.__init__(self, glyphSet) + self._commands = [] + self._lastCommand = None + self._lastX = None + self._lastY = None + + def _handleAnchor(self): + """ + >>> pen = SVGPathPen(None) + >>> pen.moveTo((0, 0)) + >>> pen.moveTo((10, 10)) + >>> pen._commands + ['M10 10'] + """ + if self._lastCommand == "M": + self._commands.pop(-1) + + def _moveTo(self, pt): + """ + >>> pen = SVGPathPen(None) + >>> pen.moveTo((0, 0)) + >>> pen._commands + ['M0 0'] + + >>> pen = SVGPathPen(None) + >>> pen.moveTo((10, 0)) + >>> pen._commands + ['M10 0'] + + >>> pen = SVGPathPen(None) + >>> pen.moveTo((0, 10)) + >>> pen._commands + ['M0 10'] + """ + self._handleAnchor() + t = "M%s" % (pointToString(pt)) + self._commands.append(t) + self._lastCommand = "M" + self._lastX, self._lastY = pt + + def _lineTo(self, pt): + """ + # duplicate point + >>> pen = SVGPathPen(None) + >>> pen.moveTo((10, 10)) + >>> pen.lineTo((10, 10)) + >>> pen._commands + ['M10 10'] + + # vertical line + >>> pen = SVGPathPen(None) + >>> pen.moveTo((10, 10)) + >>> pen.lineTo((10, 0)) + >>> pen._commands + ['M10 10', 'V0'] + + # horizontal line + >>> pen = SVGPathPen(None) + >>> pen.moveTo((10, 10)) + >>> pen.lineTo((0, 10)) + >>> pen._commands + ['M10 10', 'H0'] + + # basic + >>> pen = SVGPathPen(None) + >>> pen.lineTo((70, 80)) + >>> pen._commands + ['L70 80'] + + # basic following a moveto + >>> pen = SVGPathPen(None) + >>> pen.moveTo((0, 0)) + >>> pen.lineTo((10, 10)) + >>> pen._commands + ['M0 0', ' 10 10'] + """ + x, y = pt + # duplicate point + if x == self._lastX and y == self._lastY: + return + # vertical line + elif x == self._lastX: + cmd = "V" + pts = str(y) + # horizontal line + elif y == self._lastY: + cmd = "H" + pts = str(x) + # previous was a moveto + elif self._lastCommand == "M": + cmd = None + pts = " " + pointToString(pt) + # basic + else: + cmd = "L" + pts = pointToString(pt) + # write the string + t = "" + if cmd: + t += cmd + self._lastCommand = cmd + t += pts + self._commands.append(t) + # store for future reference + self._lastX, self._lastY = pt + + def _curveToOne(self, pt1, pt2, pt3): + """ + >>> pen = SVGPathPen(None) + >>> pen.curveTo((10, 20), (30, 40), (50, 60)) + >>> pen._commands + ['C10 20 30 40 50 60'] + """ + t = "C" + t += pointToString(pt1) + " " + t += pointToString(pt2) + " " + t += pointToString(pt3) + self._commands.append(t) + self._lastCommand = "C" + self._lastX, self._lastY = pt3 + + def _qCurveToOne(self, pt1, pt2): + """ + >>> pen = SVGPathPen(None) + >>> pen.qCurveTo((10, 20), (30, 40)) + >>> pen._commands + ['Q10 20 30 40'] + """ + assert pt2 is not None + t = "Q" + t += pointToString(pt1) + " " + t += pointToString(pt2) + self._commands.append(t) + self._lastCommand = "Q" + self._lastX, self._lastY = pt2 + + def _closePath(self): + """ + >>> pen = SVGPathPen(None) + >>> pen.closePath() + >>> pen._commands + ['Z'] + """ + self._commands.append("Z") + self._lastCommand = "Z" + self._lastX = self._lastY = None + + def _endPath(self): + """ + >>> pen = SVGPathPen(None) + >>> pen.endPath() + >>> pen._commands + ['Z'] + """ + self._closePath() + self._lastCommand = None + self._lastX = self._lastY = None + + def getCommands(self): + return "".join(self._commands) + + +if __name__ == "__main__": + import sys + import doctest + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/pens/t2CharStringPen.py b/Lib/fontTools/pens/t2CharStringPen.py new file mode 100644 index 0000000..8cc5e08 --- /dev/null +++ b/Lib/fontTools/pens/t2CharStringPen.py @@ -0,0 +1,91 @@ +# Copyright (c) 2009 Type Supply LLC +# Author: Tal Leming + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import otRound +from fontTools.misc.psCharStrings import T2CharString +from fontTools.pens.basePen import BasePen +from fontTools.cffLib.specializer import specializeCommands, commandsToProgram + + +def makeRoundFunc(tolerance): + if tolerance < 0: + raise ValueError("Rounding tolerance must be positive") + + def _round(number): + if tolerance == 0: + return number # no-op + rounded = otRound(number) + # return rounded integer if the tolerance >= 0.5, or if the absolute + # difference between the original float and the rounded integer is + # within the tolerance + if tolerance >= .5 or abs(rounded - number) <= tolerance: + return rounded + else: + # else return the value un-rounded + return number + + def roundPoint(point): + x, y = point + return _round(x), _round(y) + + return roundPoint + + +class T2CharStringPen(BasePen): + """Pen to draw Type 2 CharStrings. + + The 'roundTolerance' argument controls the rounding of point coordinates. + It is defined as the maximum absolute difference between the original + float and the rounded integer value. + The default tolerance of 0.5 means that all floats are rounded to integer; + a value of 0 disables rounding; values in between will only round floats + which are close to their integral part within the tolerated range. + """ + + def __init__(self, width, glyphSet, roundTolerance=0.5, CFF2=False): + super(T2CharStringPen, self).__init__(glyphSet) + self.roundPoint = makeRoundFunc(roundTolerance) + self._CFF2 = CFF2 + self._width = width + self._commands = [] + self._p0 = (0,0) + + def _p(self, pt): + p0 = self._p0 + pt = self._p0 = self.roundPoint(pt) + return [pt[0]-p0[0], pt[1]-p0[1]] + + def _moveTo(self, pt): + self._commands.append(('rmoveto', self._p(pt))) + + def _lineTo(self, pt): + self._commands.append(('rlineto', self._p(pt))) + + def _curveToOne(self, pt1, pt2, pt3): + _p = self._p + self._commands.append(('rrcurveto', _p(pt1)+_p(pt2)+_p(pt3))) + + def _closePath(self): + pass + + def _endPath(self): + pass + + def getCharString(self, private=None, globalSubrs=None, optimize=True): + commands = self._commands + if optimize: + maxstack = 48 if not self._CFF2 else 513 + commands = specializeCommands(commands, + generalizeFirst=False, + maxstack=maxstack) + program = commandsToProgram(commands) + if self._width is not None: + assert not self._CFF2, "CFF2 does not allow encoding glyph width in CharString." + program.insert(0, otRound(self._width)) + if not self._CFF2: + program.append('endchar') + charString = T2CharString( + program=program, private=private, globalSubrs=globalSubrs) + return charString diff --git a/Lib/fontTools/pens/teePen.py b/Lib/fontTools/pens/teePen.py new file mode 100644 index 0000000..ce22c85 --- /dev/null +++ b/Lib/fontTools/pens/teePen.py @@ -0,0 +1,48 @@ +"""Pen multiplexing drawing to one or more pens.""" +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.pens.basePen import AbstractPen + + +__all__ = ["TeePen"] + + +class TeePen(AbstractPen): + """Pen multiplexing drawing to one or more pens. + + Use either as TeePen(pen1, pen2, ...) or TeePen(iterableOfPens).""" + + def __init__(self, *pens): + if len(pens) == 1: + pens = pens[0] + self.pens = pens + def moveTo(self, p0): + for pen in self.pens: + pen.moveTo(p0) + def lineTo(self, p1): + for pen in self.pens: + pen.lineTo(p1) + def qCurveTo(self, *points): + for pen in self.pens: + pen.qCurveTo(*points) + def curveTo(self, *points): + for pen in self.pens: + pen.curveTo(*points) + def closePath(self): + for pen in self.pens: + pen.closePath() + def endPath(self): + for pen in self.pens: + pen.endPath() + def addComponent(self, glyphName, transformation): + for pen in self.pens: + pen.addComponent(glyphName, transformation) + + +if __name__ == "__main__": + from fontTools.pens.basePen import _TestPen + pen = TeePen(_TestPen(), _TestPen()) + pen.moveTo((0, 0)) + pen.lineTo((0, 100)) + pen.curveTo((50, 75), (60, 50), (50, 25)) + pen.closePath() diff --git a/Lib/fontTools/pens/transformPen.py b/Lib/fontTools/pens/transformPen.py index 9fca009..8f4fcd0 100644 --- a/Lib/fontTools/pens/transformPen.py +++ b/Lib/fontTools/pens/transformPen.py @@ -1,12 +1,12 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * -from fontTools.pens.basePen import AbstractPen +from fontTools.pens.filterPen import FilterPen __all__ = ["TransformPen"] -class TransformPen(AbstractPen): +class TransformPen(FilterPen): """Pen that transforms all coordinates using a Affine transformation, and passes them to another pen. @@ -17,12 +17,12 @@ class TransformPen(AbstractPen): transformed coordinates. The 'transformation' argument can either be a six-tuple, or a fontTools.misc.transform.Transform object. """ + super(TransformPen, self).__init__(outPen) if not hasattr(transformation, "transformPoint"): from fontTools.misc.transform import Transform transformation = Transform(*transformation) self._transformation = transformation self._transformPoint = transformation.transformPoint - self._outPen = outPen self._stack = [] def moveTo(self, pt): @@ -42,15 +42,15 @@ class TransformPen(AbstractPen): self._outPen.qCurveTo(*points) def _transformPoints(self, points): - new = [] transformPoint = self._transformPoint - for pt in points: - new.append(transformPoint(pt)) - return new + return [transformPoint(pt) for pt in points] def closePath(self): self._outPen.closePath() + def endPath(self): + self._outPen.endPath() + def addComponent(self, glyphName, transformation): transformation = self._transformation.transform(transformation) self._outPen.addComponent(glyphName, transformation) diff --git a/Lib/fontTools/pens/ttGlyphPen.py b/Lib/fontTools/pens/ttGlyphPen.py new file mode 100644 index 0000000..6e2f0d3 --- /dev/null +++ b/Lib/fontTools/pens/ttGlyphPen.py @@ -0,0 +1,158 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from array import array +from fontTools.pens.basePen import LoggingPen +from fontTools.pens.transformPen import TransformPen +from fontTools.ttLib.tables import ttProgram +from fontTools.ttLib.tables._g_l_y_f import Glyph +from fontTools.ttLib.tables._g_l_y_f import GlyphComponent +from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates + + +__all__ = ["TTGlyphPen"] + + +# the max value that can still fit in an F2Dot14: +# 1.99993896484375 +MAX_F2DOT14 = 0x7FFF / (1 << 14) + + +class TTGlyphPen(LoggingPen): + """Pen used for drawing to a TrueType glyph. + + If `handleOverflowingTransforms` is True, the components' transform values + are checked that they don't overflow the limits of a F2Dot14 number: + -2.0 <= v < +2.0. If any transform value exceeds these, the composite + glyph is decomposed. + An exception to this rule is done for values that are very close to +2.0 + (both for consistency with the -2.0 case, and for the relative frequency + these occur in real fonts). When almost +2.0 values occur (and all other + values are within the range -2.0 <= x <= +2.0), they are clamped to the + maximum positive value that can still be encoded as an F2Dot14: i.e. + 1.99993896484375. + If False, no check is done and all components are translated unmodified + into the glyf table, followed by an inevitable `struct.error` once an + attempt is made to compile them. + """ + + def __init__(self, glyphSet, handleOverflowingTransforms=True): + self.glyphSet = glyphSet + self.handleOverflowingTransforms = handleOverflowingTransforms + self.init() + + def init(self): + self.points = [] + self.endPts = [] + self.types = [] + self.components = [] + + def _addPoint(self, pt, onCurve): + self.points.append(pt) + self.types.append(onCurve) + + def _popPoint(self): + self.points.pop() + self.types.pop() + + def _isClosed(self): + return ( + (not self.points) or + (self.endPts and self.endPts[-1] == len(self.points) - 1)) + + def lineTo(self, pt): + self._addPoint(pt, 1) + + def moveTo(self, pt): + assert self._isClosed(), '"move"-type point must begin a new contour.' + self._addPoint(pt, 1) + + def qCurveTo(self, *points): + assert len(points) >= 1 + for pt in points[:-1]: + self._addPoint(pt, 0) + + # last point is None if there are no on-curve points + if points[-1] is not None: + self._addPoint(points[-1], 1) + + def closePath(self): + endPt = len(self.points) - 1 + + # ignore anchors (one-point paths) + if endPt == 0 or (self.endPts and endPt == self.endPts[-1] + 1): + self._popPoint() + return + + # if first and last point on this path are the same, remove last + startPt = 0 + if self.endPts: + startPt = self.endPts[-1] + 1 + if self.points[startPt] == self.points[endPt]: + self._popPoint() + endPt -= 1 + + self.endPts.append(endPt) + + def endPath(self): + # TrueType contours are always "closed" + self.closePath() + + def addComponent(self, glyphName, transformation): + self.components.append((glyphName, transformation)) + + def _buildComponents(self, componentFlags): + if self.handleOverflowingTransforms: + # we can't encode transform values > 2 or < -2 in F2Dot14, + # so we must decompose the glyph if any transform exceeds these + overflowing = any(s > 2 or s < -2 + for (glyphName, transformation) in self.components + for s in transformation[:4]) + components = [] + for glyphName, transformation in self.components: + if glyphName not in self.glyphSet: + self.log.warning( + "skipped non-existing component '%s'", glyphName + ) + continue + if (self.points or + (self.handleOverflowingTransforms and overflowing)): + # can't have both coordinates and components, so decompose + tpen = TransformPen(self, transformation) + self.glyphSet[glyphName].draw(tpen) + continue + + component = GlyphComponent() + component.glyphName = glyphName + component.x, component.y = transformation[4:] + transformation = transformation[:4] + if transformation != (1, 0, 0, 1): + if (self.handleOverflowingTransforms and + any(MAX_F2DOT14 < s <= 2 for s in transformation)): + # clamp values ~= +2.0 so we can keep the component + transformation = tuple(MAX_F2DOT14 if MAX_F2DOT14 < s <= 2 + else s for s in transformation) + component.transform = (transformation[:2], transformation[2:]) + component.flags = componentFlags + components.append(component) + return components + + def glyph(self, componentFlags=0x4): + assert self._isClosed(), "Didn't close last contour." + + components = self._buildComponents(componentFlags) + + glyph = Glyph() + glyph.coordinates = GlyphCoordinates(self.points) + glyph.endPtsOfContours = self.endPts + glyph.flags = array("B", self.types) + self.init() + + if components: + glyph.components = components + glyph.numberOfContours = -1 + else: + glyph.numberOfContours = len(glyph.endPtsOfContours) + glyph.program = ttProgram.Program() + glyph.program.fromBytecode(b"") + + return glyph diff --git a/Lib/fontTools/pens/wxPen.py b/Lib/fontTools/pens/wxPen.py new file mode 100644 index 0000000..8e0e463 --- /dev/null +++ b/Lib/fontTools/pens/wxPen.py @@ -0,0 +1,31 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.pens.basePen import BasePen + + +__all__ = ["WxPen"] + + +class WxPen(BasePen): + + def __init__(self, glyphSet, path=None): + BasePen.__init__(self, glyphSet) + if path is None: + import wx + path = wx.GraphicsRenderer.GetDefaultRenderer().CreatePath() + self.path = path + + def _moveTo(self, p): + self.path.MoveToPoint(*p) + + def _lineTo(self, p): + self.path.AddLineToPoint(*p) + + def _curveToOne(self, p1, p2, p3): + self.path.AddCurveToPoint(*p1+p2+p3) + + def _qCurveToOne(self, p1, p2): + self.path.AddQuadCurveToPoint(*p1+p2) + + def _closePath(self): + self.path.CloseSubpath() diff --git a/Lib/fontTools/subset.py b/Lib/fontTools/subset.py deleted file mode 100644 index 45bd457..0000000 --- a/Lib/fontTools/subset.py +++ /dev/null @@ -1,2251 +0,0 @@ -# Copyright 2013 Google, Inc. All Rights Reserved. -# -# Google Author(s): Behdad Esfahbod - -"""Python OpenType Layout Subsetter. - -Later grown into full OpenType subsetter, supporting all standard tables. -""" - -from __future__ import print_function, division, absolute_import -from fontTools.misc.py23 import * -from fontTools import ttLib -from fontTools.ttLib.tables import otTables -from fontTools.misc import psCharStrings -from fontTools.pens import basePen -import sys -import struct -import time -import array - - -def _add_method(*clazzes): - """Returns a decorator function that adds a new method to one or - more classes.""" - def wrapper(method): - for clazz in clazzes: - assert clazz.__name__ != 'DefaultTable', 'Oops, table class not found.' - assert not hasattr(clazz, method.__name__), \ - "Oops, class '%s' has method '%s'." % (clazz.__name__, - method.__name__) - setattr(clazz, method.__name__, method) - return None - return wrapper - -def _uniq_sort(l): - return sorted(set(l)) - -def _set_update(s, *others): - # Jython's set.update only takes one other argument. - # Emulate real set.update... - for other in others: - s.update(other) - - -@_add_method(otTables.Coverage) -def intersect(self, glyphs): - "Returns ascending list of matching coverage values." - return [i for i,g in enumerate(self.glyphs) if g in glyphs] - -@_add_method(otTables.Coverage) -def intersect_glyphs(self, glyphs): - "Returns set of intersecting glyphs." - return set(g for g in self.glyphs if g in glyphs) - -@_add_method(otTables.Coverage) -def subset(self, glyphs): - "Returns ascending list of remaining coverage values." - indices = self.intersect(glyphs) - self.glyphs = [g for g in self.glyphs if g in glyphs] - return indices - -@_add_method(otTables.Coverage) -def remap(self, coverage_map): - "Remaps coverage." - self.glyphs = [self.glyphs[i] for i in coverage_map] - -@_add_method(otTables.ClassDef) -def intersect(self, glyphs): - "Returns ascending list of matching class values." - return _uniq_sort( - ([0] if any(g not in self.classDefs for g in glyphs) else []) + - [v for g,v in self.classDefs.items() if g in glyphs]) - -@_add_method(otTables.ClassDef) -def intersect_class(self, glyphs, klass): - "Returns set of glyphs matching class." - if klass == 0: - return set(g for g in glyphs if g not in self.classDefs) - return set(g for g,v in self.classDefs.items() - if v == klass and g in glyphs) - -@_add_method(otTables.ClassDef) -def subset(self, glyphs, remap=False): - "Returns ascending list of remaining classes." - self.classDefs = dict((g,v) for g,v in self.classDefs.items() if g in glyphs) - # Note: while class 0 has the special meaning of "not matched", - # if no glyph will ever /not match/, we can optimize class 0 out too. - indices = _uniq_sort( - ([0] if any(g not in self.classDefs for g in glyphs) else []) + - list(self.classDefs.values())) - if remap: - self.remap(indices) - return indices - -@_add_method(otTables.ClassDef) -def remap(self, class_map): - "Remaps classes." - self.classDefs = dict((g,class_map.index(v)) - for g,v in self.classDefs.items()) - -@_add_method(otTables.SingleSubst) -def closure_glyphs(self, s, cur_glyphs=None): - if cur_glyphs is None: cur_glyphs = s.glyphs - s.glyphs.update(v for g,v in self.mapping.items() if g in cur_glyphs) - -@_add_method(otTables.SingleSubst) -def subset_glyphs(self, s): - self.mapping = dict((g,v) for g,v in self.mapping.items() - if g in s.glyphs and v in s.glyphs) - return bool(self.mapping) - -@_add_method(otTables.MultipleSubst) -def closure_glyphs(self, s, cur_glyphs=None): - if cur_glyphs is None: cur_glyphs = s.glyphs - indices = self.Coverage.intersect(cur_glyphs) - _set_update(s.glyphs, *(self.Sequence[i].Substitute for i in indices)) - -@_add_method(otTables.MultipleSubst) -def subset_glyphs(self, s): - indices = self.Coverage.subset(s.glyphs) - self.Sequence = [self.Sequence[i] for i in indices] - # Now drop rules generating glyphs we don't want - indices = [i for i,seq in enumerate(self.Sequence) - if all(sub in s.glyphs for sub in seq.Substitute)] - self.Sequence = [self.Sequence[i] for i in indices] - self.Coverage.remap(indices) - self.SequenceCount = len(self.Sequence) - return bool(self.SequenceCount) - -@_add_method(otTables.AlternateSubst) -def closure_glyphs(self, s, cur_glyphs=None): - if cur_glyphs is None: cur_glyphs = s.glyphs - _set_update(s.glyphs, *(vlist for g,vlist in self.alternates.items() - if g in cur_glyphs)) - -@_add_method(otTables.AlternateSubst) -def subset_glyphs(self, s): - self.alternates = dict((g,vlist) - for g,vlist in self.alternates.items() - if g in s.glyphs and - all(v in s.glyphs for v in vlist)) - return bool(self.alternates) - -@_add_method(otTables.LigatureSubst) -def closure_glyphs(self, s, cur_glyphs=None): - if cur_glyphs is None: cur_glyphs = s.glyphs - _set_update(s.glyphs, *([seq.LigGlyph for seq in seqs - if all(c in s.glyphs for c in seq.Component)] - for g,seqs in self.ligatures.items() - if g in cur_glyphs)) - -@_add_method(otTables.LigatureSubst) -def subset_glyphs(self, s): - self.ligatures = dict((g,v) for g,v in self.ligatures.items() - if g in s.glyphs) - self.ligatures = dict((g,[seq for seq in seqs - if seq.LigGlyph in s.glyphs and - all(c in s.glyphs for c in seq.Component)]) - for g,seqs in self.ligatures.items()) - self.ligatures = dict((g,v) for g,v in self.ligatures.items() if v) - return bool(self.ligatures) - -@_add_method(otTables.ReverseChainSingleSubst) -def closure_glyphs(self, s, cur_glyphs=None): - if cur_glyphs is None: cur_glyphs = s.glyphs - if self.Format == 1: - indices = self.Coverage.intersect(cur_glyphs) - if(not indices or - not all(c.intersect(s.glyphs) - for c in self.LookAheadCoverage + self.BacktrackCoverage)): - return - s.glyphs.update(self.Substitute[i] for i in indices) - else: - assert 0, "unknown format: %s" % self.Format - -@_add_method(otTables.ReverseChainSingleSubst) -def subset_glyphs(self, s): - if self.Format == 1: - indices = self.Coverage.subset(s.glyphs) - self.Substitute = [self.Substitute[i] for i in indices] - # Now drop rules generating glyphs we don't want - indices = [i for i,sub in enumerate(self.Substitute) - if sub in s.glyphs] - self.Substitute = [self.Substitute[i] for i in indices] - self.Coverage.remap(indices) - self.GlyphCount = len(self.Substitute) - return bool(self.GlyphCount and - all(c.subset(s.glyphs) - for c in self.LookAheadCoverage+self.BacktrackCoverage)) - else: - assert 0, "unknown format: %s" % self.Format - -@_add_method(otTables.SinglePos) -def subset_glyphs(self, s): - if self.Format == 1: - return len(self.Coverage.subset(s.glyphs)) - elif self.Format == 2: - indices = self.Coverage.subset(s.glyphs) - self.Value = [self.Value[i] for i in indices] - self.ValueCount = len(self.Value) - return bool(self.ValueCount) - else: - assert 0, "unknown format: %s" % self.Format - -@_add_method(otTables.SinglePos) -def prune_post_subset(self, options): - if not options.hinting: - # Drop device tables - self.ValueFormat &= ~0x00F0 - return True - -@_add_method(otTables.PairPos) -def subset_glyphs(self, s): - if self.Format == 1: - indices = self.Coverage.subset(s.glyphs) - self.PairSet = [self.PairSet[i] for i in indices] - for p in self.PairSet: - p.PairValueRecord = [r for r in p.PairValueRecord - if r.SecondGlyph in s.glyphs] - p.PairValueCount = len(p.PairValueRecord) - # Remove empty pairsets - indices = [i for i,p in enumerate(self.PairSet) if p.PairValueCount] - self.Coverage.remap(indices) - self.PairSet = [self.PairSet[i] for i in indices] - self.PairSetCount = len(self.PairSet) - return bool(self.PairSetCount) - elif self.Format == 2: - class1_map = self.ClassDef1.subset(s.glyphs, remap=True) - class2_map = self.ClassDef2.subset(s.glyphs, remap=True) - self.Class1Record = [self.Class1Record[i] for i in class1_map] - for c in self.Class1Record: - c.Class2Record = [c.Class2Record[i] for i in class2_map] - self.Class1Count = len(class1_map) - self.Class2Count = len(class2_map) - return bool(self.Class1Count and - self.Class2Count and - self.Coverage.subset(s.glyphs)) - else: - assert 0, "unknown format: %s" % self.Format - -@_add_method(otTables.PairPos) -def prune_post_subset(self, options): - if not options.hinting: - # Drop device tables - self.ValueFormat1 &= ~0x00F0 - self.ValueFormat2 &= ~0x00F0 - return True - -@_add_method(otTables.CursivePos) -def subset_glyphs(self, s): - if self.Format == 1: - indices = self.Coverage.subset(s.glyphs) - self.EntryExitRecord = [self.EntryExitRecord[i] for i in indices] - self.EntryExitCount = len(self.EntryExitRecord) - return bool(self.EntryExitCount) - else: - assert 0, "unknown format: %s" % self.Format - -@_add_method(otTables.Anchor) -def prune_hints(self): - # Drop device tables / contour anchor point - self.ensureDecompiled() - self.Format = 1 - -@_add_method(otTables.CursivePos) -def prune_post_subset(self, options): - if not options.hinting: - for rec in self.EntryExitRecord: - if rec.EntryAnchor: rec.EntryAnchor.prune_hints() - if rec.ExitAnchor: rec.ExitAnchor.prune_hints() - return True - -@_add_method(otTables.MarkBasePos) -def subset_glyphs(self, s): - if self.Format == 1: - mark_indices = self.MarkCoverage.subset(s.glyphs) - self.MarkArray.MarkRecord = [self.MarkArray.MarkRecord[i] - for i in mark_indices] - self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord) - base_indices = self.BaseCoverage.subset(s.glyphs) - self.BaseArray.BaseRecord = [self.BaseArray.BaseRecord[i] - for i in base_indices] - self.BaseArray.BaseCount = len(self.BaseArray.BaseRecord) - # Prune empty classes - class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord) - self.ClassCount = len(class_indices) - for m in self.MarkArray.MarkRecord: - m.Class = class_indices.index(m.Class) - for b in self.BaseArray.BaseRecord: - b.BaseAnchor = [b.BaseAnchor[i] for i in class_indices] - return bool(self.ClassCount and - self.MarkArray.MarkCount and - self.BaseArray.BaseCount) - else: - assert 0, "unknown format: %s" % self.Format - -@_add_method(otTables.MarkBasePos) -def prune_post_subset(self, options): - if not options.hinting: - for m in self.MarkArray.MarkRecord: - if m.MarkAnchor: - m.MarkAnchor.prune_hints() - for b in self.BaseArray.BaseRecord: - for a in b.BaseAnchor: - if a: - a.prune_hints() - return True - -@_add_method(otTables.MarkLigPos) -def subset_glyphs(self, s): - if self.Format == 1: - mark_indices = self.MarkCoverage.subset(s.glyphs) - self.MarkArray.MarkRecord = [self.MarkArray.MarkRecord[i] - for i in mark_indices] - self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord) - ligature_indices = self.LigatureCoverage.subset(s.glyphs) - self.LigatureArray.LigatureAttach = [self.LigatureArray.LigatureAttach[i] - for i in ligature_indices] - self.LigatureArray.LigatureCount = len(self.LigatureArray.LigatureAttach) - # Prune empty classes - class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord) - self.ClassCount = len(class_indices) - for m in self.MarkArray.MarkRecord: - m.Class = class_indices.index(m.Class) - for l in self.LigatureArray.LigatureAttach: - for c in l.ComponentRecord: - c.LigatureAnchor = [c.LigatureAnchor[i] for i in class_indices] - return bool(self.ClassCount and - self.MarkArray.MarkCount and - self.LigatureArray.LigatureCount) - else: - assert 0, "unknown format: %s" % self.Format - -@_add_method(otTables.MarkLigPos) -def prune_post_subset(self, options): - if not options.hinting: - for m in self.MarkArray.MarkRecord: - if m.MarkAnchor: - m.MarkAnchor.prune_hints() - for l in self.LigatureArray.LigatureAttach: - for c in l.ComponentRecord: - for a in c.LigatureAnchor: - if a: - a.prune_hints() - return True - -@_add_method(otTables.MarkMarkPos) -def subset_glyphs(self, s): - if self.Format == 1: - mark1_indices = self.Mark1Coverage.subset(s.glyphs) - self.Mark1Array.MarkRecord = [self.Mark1Array.MarkRecord[i] - for i in mark1_indices] - self.Mark1Array.MarkCount = len(self.Mark1Array.MarkRecord) - mark2_indices = self.Mark2Coverage.subset(s.glyphs) - self.Mark2Array.Mark2Record = [self.Mark2Array.Mark2Record[i] - for i in mark2_indices] - self.Mark2Array.MarkCount = len(self.Mark2Array.Mark2Record) - # Prune empty classes - class_indices = _uniq_sort(v.Class for v in self.Mark1Array.MarkRecord) - self.ClassCount = len(class_indices) - for m in self.Mark1Array.MarkRecord: - m.Class = class_indices.index(m.Class) - for b in self.Mark2Array.Mark2Record: - b.Mark2Anchor = [b.Mark2Anchor[i] for i in class_indices] - return bool(self.ClassCount and - self.Mark1Array.MarkCount and - self.Mark2Array.MarkCount) - else: - assert 0, "unknown format: %s" % self.Format - -@_add_method(otTables.MarkMarkPos) -def prune_post_subset(self, options): - if not options.hinting: - # Drop device tables or contour anchor point - for m in self.Mark1Array.MarkRecord: - if m.MarkAnchor: - m.MarkAnchor.prune_hints() - for b in self.Mark2Array.Mark2Record: - for m in b.Mark2Anchor: - if m: - m.prune_hints() - return True - -@_add_method(otTables.SingleSubst, - otTables.MultipleSubst, - otTables.AlternateSubst, - otTables.LigatureSubst, - otTables.ReverseChainSingleSubst, - otTables.SinglePos, - otTables.PairPos, - otTables.CursivePos, - otTables.MarkBasePos, - otTables.MarkLigPos, - otTables.MarkMarkPos) -def subset_lookups(self, lookup_indices): - pass - -@_add_method(otTables.SingleSubst, - otTables.MultipleSubst, - otTables.AlternateSubst, - otTables.LigatureSubst, - otTables.ReverseChainSingleSubst, - otTables.SinglePos, - otTables.PairPos, - otTables.CursivePos, - otTables.MarkBasePos, - otTables.MarkLigPos, - otTables.MarkMarkPos) -def collect_lookups(self): - return [] - -@_add_method(otTables.SingleSubst, - otTables.MultipleSubst, - otTables.AlternateSubst, - otTables.LigatureSubst, - otTables.ContextSubst, - otTables.ChainContextSubst, - otTables.ReverseChainSingleSubst, - otTables.SinglePos, - otTables.PairPos, - otTables.CursivePos, - otTables.MarkBasePos, - otTables.MarkLigPos, - otTables.MarkMarkPos, - otTables.ContextPos, - otTables.ChainContextPos) -def prune_pre_subset(self, options): - return True - -@_add_method(otTables.SingleSubst, - otTables.MultipleSubst, - otTables.AlternateSubst, - otTables.LigatureSubst, - otTables.ReverseChainSingleSubst, - otTables.ContextSubst, - otTables.ChainContextSubst, - otTables.ContextPos, - otTables.ChainContextPos) -def prune_post_subset(self, options): - return True - -@_add_method(otTables.SingleSubst, - otTables.AlternateSubst, - otTables.ReverseChainSingleSubst) -def may_have_non_1to1(self): - return False - -@_add_method(otTables.MultipleSubst, - otTables.LigatureSubst, - otTables.ContextSubst, - otTables.ChainContextSubst) -def may_have_non_1to1(self): - return True - -@_add_method(otTables.ContextSubst, - otTables.ChainContextSubst, - otTables.ContextPos, - otTables.ChainContextPos) -def __classify_context(self): - - class ContextHelper(object): - def __init__(self, klass, Format): - if klass.__name__.endswith('Subst'): - Typ = 'Sub' - Type = 'Subst' - else: - Typ = 'Pos' - Type = 'Pos' - if klass.__name__.startswith('Chain'): - Chain = 'Chain' - else: - Chain = '' - ChainTyp = Chain+Typ - - self.Typ = Typ - self.Type = Type - self.Chain = Chain - self.ChainTyp = ChainTyp - - self.LookupRecord = Type+'LookupRecord' - - if Format == 1: - Coverage = lambda r: r.Coverage - ChainCoverage = lambda r: r.Coverage - ContextData = lambda r:(None,) - ChainContextData = lambda r:(None, None, None) - RuleData = lambda r:(r.Input,) - ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead) - SetRuleData = None - ChainSetRuleData = None - elif Format == 2: - Coverage = lambda r: r.Coverage - ChainCoverage = lambda r: r.Coverage - ContextData = lambda r:(r.ClassDef,) - ChainContextData = lambda r:(r.LookAheadClassDef, - r.InputClassDef, - r.BacktrackClassDef) - RuleData = lambda r:(r.Class,) - ChainRuleData = lambda r:(r.LookAhead, r.Input, r.Backtrack) - def SetRuleData(r, d):(r.Class,) = d - def ChainSetRuleData(r, d):(r.LookAhead, r.Input, r.Backtrack) = d - elif Format == 3: - Coverage = lambda r: r.Coverage[0] - ChainCoverage = lambda r: r.InputCoverage[0] - ContextData = None - ChainContextData = None - RuleData = lambda r: r.Coverage - ChainRuleData = lambda r:(r.LookAheadCoverage + - r.InputCoverage + - r.BacktrackCoverage) - SetRuleData = None - ChainSetRuleData = None - else: - assert 0, "unknown format: %s" % Format - - if Chain: - self.Coverage = ChainCoverage - self.ContextData = ChainContextData - self.RuleData = ChainRuleData - self.SetRuleData = ChainSetRuleData - else: - self.Coverage = Coverage - self.ContextData = ContextData - self.RuleData = RuleData - self.SetRuleData = SetRuleData - - if Format == 1: - self.Rule = ChainTyp+'Rule' - self.RuleCount = ChainTyp+'RuleCount' - self.RuleSet = ChainTyp+'RuleSet' - self.RuleSetCount = ChainTyp+'RuleSetCount' - self.Intersect = lambda glyphs, c, r: [r] if r in glyphs else [] - elif Format == 2: - self.Rule = ChainTyp+'ClassRule' - self.RuleCount = ChainTyp+'ClassRuleCount' - self.RuleSet = ChainTyp+'ClassSet' - self.RuleSetCount = ChainTyp+'ClassSetCount' - self.Intersect = lambda glyphs, c, r: c.intersect_class(glyphs, r) - - self.ClassDef = 'InputClassDef' if Chain else 'ClassDef' - self.ClassDefIndex = 1 if Chain else 0 - self.Input = 'Input' if Chain else 'Class' - - if self.Format not in [1, 2, 3]: - return None # Don't shoot the messenger; let it go - if not hasattr(self.__class__, "__ContextHelpers"): - self.__class__.__ContextHelpers = {} - if self.Format not in self.__class__.__ContextHelpers: - helper = ContextHelper(self.__class__, self.Format) - self.__class__.__ContextHelpers[self.Format] = helper - return self.__class__.__ContextHelpers[self.Format] - -@_add_method(otTables.ContextSubst, - otTables.ChainContextSubst) -def closure_glyphs(self, s, cur_glyphs=None): - if cur_glyphs is None: cur_glyphs = s.glyphs - c = self.__classify_context() - - indices = c.Coverage(self).intersect(s.glyphs) - if not indices: - return [] - cur_glyphs = c.Coverage(self).intersect_glyphs(s.glyphs); - - if self.Format == 1: - ContextData = c.ContextData(self) - rss = getattr(self, c.RuleSet) - rssCount = getattr(self, c.RuleSetCount) - for i in indices: - if i >= rssCount or not rss[i]: continue - for r in getattr(rss[i], c.Rule): - if not r: continue - if all(all(c.Intersect(s.glyphs, cd, k) for k in klist) - for cd,klist in zip(ContextData, c.RuleData(r))): - chaos = False - for ll in getattr(r, c.LookupRecord): - if not ll: continue - seqi = ll.SequenceIndex - if chaos: - pos_glyphs = s.glyphs - else: - if seqi == 0: - pos_glyphs = set([c.Coverage(self).glyphs[i]]) - else: - pos_glyphs = set([r.Input[seqi - 1]]) - lookup = s.table.LookupList.Lookup[ll.LookupListIndex] - chaos = chaos or lookup.may_have_non_1to1() - lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) - elif self.Format == 2: - ClassDef = getattr(self, c.ClassDef) - indices = ClassDef.intersect(cur_glyphs) - ContextData = c.ContextData(self) - rss = getattr(self, c.RuleSet) - rssCount = getattr(self, c.RuleSetCount) - for i in indices: - if i >= rssCount or not rss[i]: continue - for r in getattr(rss[i], c.Rule): - if not r: continue - if all(all(c.Intersect(s.glyphs, cd, k) for k in klist) - for cd,klist in zip(ContextData, c.RuleData(r))): - chaos = False - for ll in getattr(r, c.LookupRecord): - if not ll: continue - seqi = ll.SequenceIndex - if chaos: - pos_glyphs = s.glyphs - else: - if seqi == 0: - pos_glyphs = ClassDef.intersect_class(cur_glyphs, i) - else: - pos_glyphs = ClassDef.intersect_class(s.glyphs, - getattr(r, c.Input)[seqi - 1]) - lookup = s.table.LookupList.Lookup[ll.LookupListIndex] - chaos = chaos or lookup.may_have_non_1to1() - lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) - elif self.Format == 3: - if not all(x.intersect(s.glyphs) for x in c.RuleData(self)): - return [] - r = self - chaos = False - for ll in getattr(r, c.LookupRecord): - if not ll: continue - seqi = ll.SequenceIndex - if chaos: - pos_glyphs = s.glyphs - else: - if seqi == 0: - pos_glyphs = cur_glyphs - else: - pos_glyphs = r.InputCoverage[seqi].intersect_glyphs(s.glyphs) - lookup = s.table.LookupList.Lookup[ll.LookupListIndex] - chaos = chaos or lookup.may_have_non_1to1() - lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) - else: - assert 0, "unknown format: %s" % self.Format - -@_add_method(otTables.ContextSubst, - otTables.ContextPos, - otTables.ChainContextSubst, - otTables.ChainContextPos) -def subset_glyphs(self, s): - c = self.__classify_context() - - if self.Format == 1: - indices = self.Coverage.subset(s.glyphs) - rss = getattr(self, c.RuleSet) - rss = [rss[i] for i in indices] - for rs in rss: - if not rs: continue - ss = getattr(rs, c.Rule) - ss = [r for r in ss - if r and all(all(g in s.glyphs for g in glist) - for glist in c.RuleData(r))] - setattr(rs, c.Rule, ss) - setattr(rs, c.RuleCount, len(ss)) - # Prune empty subrulesets - rss = [rs for rs in rss if rs and getattr(rs, c.Rule)] - setattr(self, c.RuleSet, rss) - setattr(self, c.RuleSetCount, len(rss)) - return bool(rss) - elif self.Format == 2: - if not self.Coverage.subset(s.glyphs): - return False - ContextData = c.ContextData(self) - klass_maps = [x.subset(s.glyphs, remap=True) for x in ContextData] - - # Keep rulesets for class numbers that survived. - indices = klass_maps[c.ClassDefIndex] - rss = getattr(self, c.RuleSet) - rssCount = getattr(self, c.RuleSetCount) - rss = [rss[i] for i in indices if i < rssCount] - del rssCount - # Delete, but not renumber, unreachable rulesets. - indices = getattr(self, c.ClassDef).intersect(self.Coverage.glyphs) - rss = [rss if i in indices else None for i,rss in enumerate(rss)] - while rss and rss[-1] is None: - del rss[-1] - - for rs in rss: - if not rs: continue - ss = getattr(rs, c.Rule) - ss = [r for r in ss - if r and all(all(k in klass_map for k in klist) - for klass_map,klist in zip(klass_maps, c.RuleData(r)))] - setattr(rs, c.Rule, ss) - setattr(rs, c.RuleCount, len(ss)) - - # Remap rule classes - for r in ss: - c.SetRuleData(r, [[klass_map.index(k) for k in klist] - for klass_map,klist in zip(klass_maps, c.RuleData(r))]) - return bool(rss) - elif self.Format == 3: - return all(x.subset(s.glyphs) for x in c.RuleData(self)) - else: - assert 0, "unknown format: %s" % self.Format - -@_add_method(otTables.ContextSubst, - otTables.ChainContextSubst, - otTables.ContextPos, - otTables.ChainContextPos) -def subset_lookups(self, lookup_indices): - c = self.__classify_context() - - if self.Format in [1, 2]: - for rs in getattr(self, c.RuleSet): - if not rs: continue - for r in getattr(rs, c.Rule): - if not r: continue - setattr(r, c.LookupRecord, - [ll for ll in getattr(r, c.LookupRecord) - if ll and ll.LookupListIndex in lookup_indices]) - for ll in getattr(r, c.LookupRecord): - if not ll: continue - ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex) - elif self.Format == 3: - setattr(self, c.LookupRecord, - [ll for ll in getattr(self, c.LookupRecord) - if ll and ll.LookupListIndex in lookup_indices]) - for ll in getattr(self, c.LookupRecord): - if not ll: continue - ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex) - else: - assert 0, "unknown format: %s" % self.Format - -@_add_method(otTables.ContextSubst, - otTables.ChainContextSubst, - otTables.ContextPos, - otTables.ChainContextPos) -def collect_lookups(self): - c = self.__classify_context() - - if self.Format in [1, 2]: - return [ll.LookupListIndex - for rs in getattr(self, c.RuleSet) if rs - for r in getattr(rs, c.Rule) if r - for ll in getattr(r, c.LookupRecord) if ll] - elif self.Format == 3: - return [ll.LookupListIndex - for ll in getattr(self, c.LookupRecord) if ll] - else: - assert 0, "unknown format: %s" % self.Format - -@_add_method(otTables.ExtensionSubst) -def closure_glyphs(self, s, cur_glyphs=None): - if self.Format == 1: - self.ExtSubTable.closure_glyphs(s, cur_glyphs) - else: - assert 0, "unknown format: %s" % self.Format - -@_add_method(otTables.ExtensionSubst) -def may_have_non_1to1(self): - if self.Format == 1: - return self.ExtSubTable.may_have_non_1to1() - else: - assert 0, "unknown format: %s" % self.Format - -@_add_method(otTables.ExtensionSubst, - otTables.ExtensionPos) -def prune_pre_subset(self, options): - if self.Format == 1: - return self.ExtSubTable.prune_pre_subset(options) - else: - assert 0, "unknown format: %s" % self.Format - -@_add_method(otTables.ExtensionSubst, - otTables.ExtensionPos) -def subset_glyphs(self, s): - if self.Format == 1: - return self.ExtSubTable.subset_glyphs(s) - else: - assert 0, "unknown format: %s" % self.Format - -@_add_method(otTables.ExtensionSubst, - otTables.ExtensionPos) -def prune_post_subset(self, options): - if self.Format == 1: - return self.ExtSubTable.prune_post_subset(options) - else: - assert 0, "unknown format: %s" % self.Format - -@_add_method(otTables.ExtensionSubst, - otTables.ExtensionPos) -def subset_lookups(self, lookup_indices): - if self.Format == 1: - return self.ExtSubTable.subset_lookups(lookup_indices) - else: - assert 0, "unknown format: %s" % self.Format - -@_add_method(otTables.ExtensionSubst, - otTables.ExtensionPos) -def collect_lookups(self): - if self.Format == 1: - return self.ExtSubTable.collect_lookups() - else: - assert 0, "unknown format: %s" % self.Format - -@_add_method(otTables.Lookup) -def closure_glyphs(self, s, cur_glyphs=None): - for st in self.SubTable: - if not st: continue - st.closure_glyphs(s, cur_glyphs) - -@_add_method(otTables.Lookup) -def prune_pre_subset(self, options): - ret = False - for st in self.SubTable: - if not st: continue - if st.prune_pre_subset(options): ret = True - return ret - -@_add_method(otTables.Lookup) -def subset_glyphs(self, s): - self.SubTable = [st for st in self.SubTable if st and st.subset_glyphs(s)] - self.SubTableCount = len(self.SubTable) - return bool(self.SubTableCount) - -@_add_method(otTables.Lookup) -def prune_post_subset(self, options): - ret = False - for st in self.SubTable: - if not st: continue - if st.prune_post_subset(options): ret = True - return ret - -@_add_method(otTables.Lookup) -def subset_lookups(self, lookup_indices): - for s in self.SubTable: - s.subset_lookups(lookup_indices) - -@_add_method(otTables.Lookup) -def collect_lookups(self): - return _uniq_sort(sum((st.collect_lookups() for st in self.SubTable - if st), [])) - -@_add_method(otTables.Lookup) -def may_have_non_1to1(self): - return any(st.may_have_non_1to1() for st in self.SubTable if st) - -@_add_method(otTables.LookupList) -def prune_pre_subset(self, options): - ret = False - for l in self.Lookup: - if not l: continue - if l.prune_pre_subset(options): ret = True - return ret - -@_add_method(otTables.LookupList) -def subset_glyphs(self, s): - "Returns the indices of nonempty lookups." - return [i for i,l in enumerate(self.Lookup) if l and l.subset_glyphs(s)] - -@_add_method(otTables.LookupList) -def prune_post_subset(self, options): - ret = False - for l in self.Lookup: - if not l: continue - if l.prune_post_subset(options): ret = True - return ret - -@_add_method(otTables.LookupList) -def subset_lookups(self, lookup_indices): - self.ensureDecompiled() - self.Lookup = [self.Lookup[i] for i in lookup_indices - if i < self.LookupCount] - self.LookupCount = len(self.Lookup) - for l in self.Lookup: - l.subset_lookups(lookup_indices) - -@_add_method(otTables.LookupList) -def closure_lookups(self, lookup_indices): - lookup_indices = _uniq_sort(lookup_indices) - recurse = lookup_indices - while True: - recurse_lookups = sum((self.Lookup[i].collect_lookups() - for i in recurse if i < self.LookupCount), []) - recurse_lookups = [l for l in recurse_lookups - if l not in lookup_indices and l < self.LookupCount] - if not recurse_lookups: - return _uniq_sort(lookup_indices) - recurse_lookups = _uniq_sort(recurse_lookups) - lookup_indices.extend(recurse_lookups) - recurse = recurse_lookups - -@_add_method(otTables.Feature) -def subset_lookups(self, lookup_indices): - self.LookupListIndex = [l for l in self.LookupListIndex - if l in lookup_indices] - # Now map them. - self.LookupListIndex = [lookup_indices.index(l) - for l in self.LookupListIndex] - self.LookupCount = len(self.LookupListIndex) - return self.LookupCount or self.FeatureParams - -@_add_method(otTables.Feature) -def collect_lookups(self): - return self.LookupListIndex[:] - -@_add_method(otTables.FeatureList) -def subset_lookups(self, lookup_indices): - "Returns the indices of nonempty features." - # Note: Never ever drop feature 'pref', even if it's empty. - # HarfBuzz chooses shaper for Khmer based on presence of this - # feature. See thread at: - # http://lists.freedesktop.org/archives/harfbuzz/2012-November/002660.html - feature_indices = [i for i,f in enumerate(self.FeatureRecord) - if (f.Feature.subset_lookups(lookup_indices) or - f.FeatureTag == 'pref')] - self.subset_features(feature_indices) - return feature_indices - -@_add_method(otTables.FeatureList) -def collect_lookups(self, feature_indices): - return _uniq_sort(sum((self.FeatureRecord[i].Feature.collect_lookups() - for i in feature_indices - if i < self.FeatureCount), [])) - -@_add_method(otTables.FeatureList) -def subset_features(self, feature_indices): - self.ensureDecompiled() - self.FeatureRecord = [self.FeatureRecord[i] for i in feature_indices] - self.FeatureCount = len(self.FeatureRecord) - return bool(self.FeatureCount) - -@_add_method(otTables.DefaultLangSys, - otTables.LangSys) -def subset_features(self, feature_indices): - if self.ReqFeatureIndex in feature_indices: - self.ReqFeatureIndex = feature_indices.index(self.ReqFeatureIndex) - else: - self.ReqFeatureIndex = 65535 - self.FeatureIndex = [f for f in self.FeatureIndex if f in feature_indices] - # Now map them. - self.FeatureIndex = [feature_indices.index(f) for f in self.FeatureIndex - if f in feature_indices] - self.FeatureCount = len(self.FeatureIndex) - return bool(self.FeatureCount or self.ReqFeatureIndex != 65535) - -@_add_method(otTables.DefaultLangSys, - otTables.LangSys) -def collect_features(self): - feature_indices = self.FeatureIndex[:] - if self.ReqFeatureIndex != 65535: - feature_indices.append(self.ReqFeatureIndex) - return _uniq_sort(feature_indices) - -@_add_method(otTables.Script) -def subset_features(self, feature_indices): - if(self.DefaultLangSys and - not self.DefaultLangSys.subset_features(feature_indices)): - self.DefaultLangSys = None - self.LangSysRecord = [l for l in self.LangSysRecord - if l.LangSys.subset_features(feature_indices)] - self.LangSysCount = len(self.LangSysRecord) - return bool(self.LangSysCount or self.DefaultLangSys) - -@_add_method(otTables.Script) -def collect_features(self): - feature_indices = [l.LangSys.collect_features() for l in self.LangSysRecord] - if self.DefaultLangSys: - feature_indices.append(self.DefaultLangSys.collect_features()) - return _uniq_sort(sum(feature_indices, [])) - -@_add_method(otTables.ScriptList) -def subset_features(self, feature_indices): - self.ScriptRecord = [s for s in self.ScriptRecord - if s.Script.subset_features(feature_indices)] - self.ScriptCount = len(self.ScriptRecord) - return bool(self.ScriptCount) - -@_add_method(otTables.ScriptList) -def collect_features(self): - return _uniq_sort(sum((s.Script.collect_features() - for s in self.ScriptRecord), [])) - -@_add_method(ttLib.getTableClass('GSUB')) -def closure_glyphs(self, s): - s.table = self.table - if self.table.ScriptList: - feature_indices = self.table.ScriptList.collect_features() - else: - feature_indices = [] - if self.table.FeatureList: - lookup_indices = self.table.FeatureList.collect_lookups(feature_indices) - else: - lookup_indices = [] - if self.table.LookupList: - while True: - orig_glyphs = s.glyphs.copy() - for i in lookup_indices: - if i >= self.table.LookupList.LookupCount: continue - if not self.table.LookupList.Lookup[i]: continue - self.table.LookupList.Lookup[i].closure_glyphs(s) - if orig_glyphs == s.glyphs: - break - del s.table - -@_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) -def subset_glyphs(self, s): - s.glyphs = s.glyphs_gsubed - if self.table.LookupList: - lookup_indices = self.table.LookupList.subset_glyphs(s) - else: - lookup_indices = [] - self.subset_lookups(lookup_indices) - self.prune_lookups() - return True - -@_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) -def subset_lookups(self, lookup_indices): - """Retains specified lookups, then removes empty features, language - systems, and scripts.""" - if self.table.LookupList: - self.table.LookupList.subset_lookups(lookup_indices) - if self.table.FeatureList: - feature_indices = self.table.FeatureList.subset_lookups(lookup_indices) - else: - feature_indices = [] - if self.table.ScriptList: - self.table.ScriptList.subset_features(feature_indices) - -@_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) -def prune_lookups(self): - "Remove unreferenced lookups" - if self.table.ScriptList: - feature_indices = self.table.ScriptList.collect_features() - else: - feature_indices = [] - if self.table.FeatureList: - lookup_indices = self.table.FeatureList.collect_lookups(feature_indices) - else: - lookup_indices = [] - if self.table.LookupList: - lookup_indices = self.table.LookupList.closure_lookups(lookup_indices) - else: - lookup_indices = [] - self.subset_lookups(lookup_indices) - -@_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) -def subset_feature_tags(self, feature_tags): - if self.table.FeatureList: - feature_indices = [i for i,f in - enumerate(self.table.FeatureList.FeatureRecord) - if f.FeatureTag in feature_tags] - self.table.FeatureList.subset_features(feature_indices) - else: - feature_indices = [] - if self.table.ScriptList: - self.table.ScriptList.subset_features(feature_indices) - -@_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) -def prune_features(self): - "Remove unreferenced featurs" - if self.table.ScriptList: - feature_indices = self.table.ScriptList.collect_features() - else: - feature_indices = [] - if self.table.FeatureList: - self.table.FeatureList.subset_features(feature_indices) - if self.table.ScriptList: - self.table.ScriptList.subset_features(feature_indices) - -@_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) -def prune_pre_subset(self, options): - # Drop undesired features - if '*' not in options.layout_features: - self.subset_feature_tags(options.layout_features) - # Drop unreferenced lookups - self.prune_lookups() - # Prune lookups themselves - if self.table.LookupList: - self.table.LookupList.prune_pre_subset(options); - return True - -@_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) -def remove_redundant_langsys(self): - table = self.table - if not table.ScriptList or not table.FeatureList: - return - - features = table.FeatureList.FeatureRecord - - for s in table.ScriptList.ScriptRecord: - d = s.Script.DefaultLangSys - if not d: - continue - for lr in s.Script.LangSysRecord[:]: - l = lr.LangSys - # Compare d and l - if len(d.FeatureIndex) != len(l.FeatureIndex): - continue - if (d.ReqFeatureIndex == 65535) != (l.ReqFeatureIndex == 65535): - continue - - if d.ReqFeatureIndex != 65535: - if features[d.ReqFeatureIndex] != features[l.ReqFeatureIndex]: - continue - - for i in range(len(d.FeatureIndex)): - if features[d.FeatureIndex[i]] != features[l.FeatureIndex[i]]: - break - else: - # LangSys and default are equal; delete LangSys - s.Script.LangSysRecord.remove(lr) - -@_add_method(ttLib.getTableClass('GSUB'), - ttLib.getTableClass('GPOS')) -def prune_post_subset(self, options): - table = self.table - - # LookupList looks good. Just prune lookups themselves - if table.LookupList: - table.LookupList.prune_post_subset(options); - # XXX Next two lines disabled because OTS is stupid and - # doesn't like NULL offsetse here. - #if not table.LookupList.Lookup: - # table.LookupList = None - - if not table.LookupList: - table.FeatureList = None - - if table.FeatureList: - self.remove_redundant_langsys() - # Remove unreferenced features - self.prune_features() - - # XXX Next two lines disabled because OTS is stupid and - # doesn't like NULL offsetse here. - #if table.FeatureList and not table.FeatureList.FeatureRecord: - # table.FeatureList = None - - # Never drop scripts themselves as them just being available - # holds semantic significance. - # XXX Next two lines disabled because OTS is stupid and - # doesn't like NULL offsetse here. - #if table.ScriptList and not table.ScriptList.ScriptRecord: - # table.ScriptList = None - - return True - -@_add_method(ttLib.getTableClass('GDEF')) -def subset_glyphs(self, s): - glyphs = s.glyphs_gsubed - table = self.table - if table.LigCaretList: - indices = table.LigCaretList.Coverage.subset(glyphs) - table.LigCaretList.LigGlyph = [table.LigCaretList.LigGlyph[i] - for i in indices] - table.LigCaretList.LigGlyphCount = len(table.LigCaretList.LigGlyph) - if table.MarkAttachClassDef: - table.MarkAttachClassDef.classDefs = dict((g,v) for g,v in - table.MarkAttachClassDef. - classDefs.items() - if g in glyphs) - if table.GlyphClassDef: - table.GlyphClassDef.classDefs = dict((g,v) for g,v in - table.GlyphClassDef. - classDefs.items() - if g in glyphs) - if table.AttachList: - indices = table.AttachList.Coverage.subset(glyphs) - GlyphCount = table.AttachList.GlyphCount - table.AttachList.AttachPoint = [table.AttachList.AttachPoint[i] - for i in indices - if i < GlyphCount] - table.AttachList.GlyphCount = len(table.AttachList.AttachPoint) - if hasattr(table, "MarkGlyphSetsDef") and table.MarkGlyphSetsDef: - for coverage in table.MarkGlyphSetsDef.Coverage: - coverage.subset(glyphs) - # TODO: The following is disabled. If enabling, we need to go fixup all - # lookups that use MarkFilteringSet and map their set. - #indices = table.MarkGlyphSetsDef.Coverage = [c for c in table.MarkGlyphSetsDef.Coverage if c.glyphs] - return True - -@_add_method(ttLib.getTableClass('GDEF')) -def prune_post_subset(self, options): - table = self.table - # XXX check these against OTS - if table.LigCaretList and not table.LigCaretList.LigGlyphCount: - table.LigCaretList = None - if table.MarkAttachClassDef and not table.MarkAttachClassDef.classDefs: - table.MarkAttachClassDef = None - if table.GlyphClassDef and not table.GlyphClassDef.classDefs: - table.GlyphClassDef = None - if table.AttachList and not table.AttachList.GlyphCount: - table.AttachList = None - if hasattr(table, "MarkGlyphSetsDef") and table.MarkGlyphSetsDef and not table.MarkGlyphSetsDef.Coverage: - table.MarkGlyphSetsDef = None - if table.Version == 0x00010002/0x10000: - table.Version = 1.0 - return bool(table.LigCaretList or - table.MarkAttachClassDef or - table.GlyphClassDef or - table.AttachList or - (table.Version >= 0x00010002/0x10000 and table.MarkGlyphSetsDef)) - -@_add_method(ttLib.getTableClass('kern')) -def prune_pre_subset(self, options): - # Prune unknown kern table types - self.kernTables = [t for t in self.kernTables if hasattr(t, 'kernTable')] - return bool(self.kernTables) - -@_add_method(ttLib.getTableClass('kern')) -def subset_glyphs(self, s): - glyphs = s.glyphs_gsubed - for t in self.kernTables: - t.kernTable = dict(((a,b),v) for (a,b),v in t.kernTable.items() - if a in glyphs and b in glyphs) - self.kernTables = [t for t in self.kernTables if t.kernTable] - return bool(self.kernTables) - -@_add_method(ttLib.getTableClass('vmtx')) -def subset_glyphs(self, s): - self.metrics = dict((g,v) for g,v in self.metrics.items() if g in s.glyphs) - return bool(self.metrics) - -@_add_method(ttLib.getTableClass('hmtx')) -def subset_glyphs(self, s): - self.metrics = dict((g,v) for g,v in self.metrics.items() if g in s.glyphs) - return True # Required table - -@_add_method(ttLib.getTableClass('hdmx')) -def subset_glyphs(self, s): - self.hdmx = dict((sz,dict((g,v) for g,v in l.items() if g in s.glyphs)) - for sz,l in self.hdmx.items()) - return bool(self.hdmx) - -@_add_method(ttLib.getTableClass('VORG')) -def subset_glyphs(self, s): - self.VOriginRecords = dict((g,v) for g,v in self.VOriginRecords.items() - if g in s.glyphs) - self.numVertOriginYMetrics = len(self.VOriginRecords) - return True # Never drop; has default metrics - -@_add_method(ttLib.getTableClass('post')) -def prune_pre_subset(self, options): - if not options.glyph_names: - self.formatType = 3.0 - return True # Required table - -@_add_method(ttLib.getTableClass('post')) -def subset_glyphs(self, s): - self.extraNames = [] # This seems to do it - return True # Required table - -@_add_method(ttLib.getTableModule('glyf').Glyph) -def remapComponentsFast(self, indices): - if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0: - return # Not composite - data = array.array("B", self.data) - i = 10 - more = 1 - while more: - flags =(data[i] << 8) | data[i+1] - glyphID =(data[i+2] << 8) | data[i+3] - # Remap - glyphID = indices.index(glyphID) - data[i+2] = glyphID >> 8 - data[i+3] = glyphID & 0xFF - i += 4 - flags = int(flags) - - if flags & 0x0001: i += 4 # ARG_1_AND_2_ARE_WORDS - else: i += 2 - if flags & 0x0008: i += 2 # WE_HAVE_A_SCALE - elif flags & 0x0040: i += 4 # WE_HAVE_AN_X_AND_Y_SCALE - elif flags & 0x0080: i += 8 # WE_HAVE_A_TWO_BY_TWO - more = flags & 0x0020 # MORE_COMPONENTS - - self.data = data.tostring() - -@_add_method(ttLib.getTableClass('glyf')) -def closure_glyphs(self, s): - decompose = s.glyphs - while True: - components = set() - for g in decompose: - if g not in self.glyphs: - continue - gl = self.glyphs[g] - for c in gl.getComponentNames(self): - if c not in s.glyphs: - components.add(c) - components = set(c for c in components if c not in s.glyphs) - if not components: - break - decompose = components - s.glyphs.update(components) - -@_add_method(ttLib.getTableClass('glyf')) -def prune_pre_subset(self, options): - if options.notdef_glyph and not options.notdef_outline: - g = self[self.glyphOrder[0]] - # Yay, easy! - g.__dict__.clear() - g.data = "" - return True - -@_add_method(ttLib.getTableClass('glyf')) -def subset_glyphs(self, s): - self.glyphs = dict((g,v) for g,v in self.glyphs.items() if g in s.glyphs) - indices = [i for i,g in enumerate(self.glyphOrder) if g in s.glyphs] - for v in self.glyphs.values(): - if hasattr(v, "data"): - v.remapComponentsFast(indices) - else: - pass # No need - self.glyphOrder = [g for g in self.glyphOrder if g in s.glyphs] - # Don't drop empty 'glyf' tables, otherwise 'loca' doesn't get subset. - return True - -@_add_method(ttLib.getTableClass('glyf')) -def prune_post_subset(self, options): - if not options.hinting: - for v in self.glyphs.values(): - v.removeHinting() - return True - -@_add_method(ttLib.getTableClass('CFF ')) -def prune_pre_subset(self, options): - cff = self.cff - # CFF table must have one font only - cff.fontNames = cff.fontNames[:1] - - if options.notdef_glyph and not options.notdef_outline: - for fontname in cff.keys(): - font = cff[fontname] - c,_ = font.CharStrings.getItemAndSelector('.notdef') - # XXX we should preserve the glyph width - c.bytecode = '\x0e' # endchar - c.program = None - - return True # bool(cff.fontNames) - -@_add_method(ttLib.getTableClass('CFF ')) -def subset_glyphs(self, s): - cff = self.cff - for fontname in cff.keys(): - font = cff[fontname] - cs = font.CharStrings - - # Load all glyphs - for g in font.charset: - if g not in s.glyphs: continue - c,sel = cs.getItemAndSelector(g) - - if cs.charStringsAreIndexed: - indices = [i for i,g in enumerate(font.charset) if g in s.glyphs] - csi = cs.charStringsIndex - csi.items = [csi.items[i] for i in indices] - csi.count = len(csi.items) - del csi.file, csi.offsets - if hasattr(font, "FDSelect"): - sel = font.FDSelect - sel.format = None - sel.gidArray = [sel.gidArray[i] for i in indices] - cs.charStrings = dict((g,indices.index(v)) - for g,v in cs.charStrings.items() - if g in s.glyphs) - else: - cs.charStrings = dict((g,v) - for g,v in cs.charStrings.items() - if g in s.glyphs) - font.charset = [g for g in font.charset if g in s.glyphs] - font.numGlyphs = len(font.charset) - - return True # any(cff[fontname].numGlyphs for fontname in cff.keys()) - -@_add_method(psCharStrings.T2CharString) -def subset_subroutines(self, subrs, gsubrs): - p = self.program - assert len(p) - for i in range(1, len(p)): - if p[i] == 'callsubr': - assert isinstance(p[i-1], int) - p[i-1] = subrs._used.index(p[i-1] + subrs._old_bias) - subrs._new_bias - elif p[i] == 'callgsubr': - assert isinstance(p[i-1], int) - p[i-1] = gsubrs._used.index(p[i-1] + gsubrs._old_bias) - gsubrs._new_bias - -@_add_method(psCharStrings.T2CharString) -def drop_hints(self): - hints = self._hints - - if hints.has_hint: - self.program = self.program[hints.last_hint:] - if hasattr(self, 'width'): - # Insert width back if needed - if self.width != self.private.defaultWidthX: - self.program.insert(0, self.width - self.private.nominalWidthX) - - if hints.has_hintmask: - i = 0 - p = self.program - while i < len(p): - if p[i] in ['hintmask', 'cntrmask']: - assert i + 1 <= len(p) - del p[i:i+2] - continue - i += 1 - - # TODO: we currently don't drop calls to "empty" subroutines. - - assert len(self.program) - - del self._hints - -class _MarkingT2Decompiler(psCharStrings.SimpleT2Decompiler): - - def __init__(self, localSubrs, globalSubrs): - psCharStrings.SimpleT2Decompiler.__init__(self, - localSubrs, - globalSubrs) - for subrs in [localSubrs, globalSubrs]: - if subrs and not hasattr(subrs, "_used"): - subrs._used = set() - - def op_callsubr(self, index): - self.localSubrs._used.add(self.operandStack[-1]+self.localBias) - psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) - - def op_callgsubr(self, index): - self.globalSubrs._used.add(self.operandStack[-1]+self.globalBias) - psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) - -class _DehintingT2Decompiler(psCharStrings.SimpleT2Decompiler): - - class Hints(object): - def __init__(self): - # Whether calling this charstring produces any hint stems - self.has_hint = False - # Index to start at to drop all hints - self.last_hint = 0 - # Index up to which we know more hints are possible. Only - # relevant if status is 0 or 1. - self.last_checked = 0 - # The status means: - # 0: after dropping hints, this charstring is empty - # 1: after dropping hints, there may be more hints continuing after this - # 2: no more hints possible after this charstring - self.status = 0 - # Has hintmask instructions; not recursive - self.has_hintmask = False - pass - - def __init__(self, css, localSubrs, globalSubrs): - self._css = css - psCharStrings.SimpleT2Decompiler.__init__(self, - localSubrs, - globalSubrs) - - def execute(self, charString): - old_hints = charString._hints if hasattr(charString, '_hints') else None - charString._hints = self.Hints() - - psCharStrings.SimpleT2Decompiler.execute(self, charString) - - hints = charString._hints - - if hints.has_hint or hints.has_hintmask: - self._css.add(charString) - - if hints.status != 2: - # Check from last_check, make sure we didn't have any operators. - for i in range(hints.last_checked, len(charString.program) - 1): - if isinstance(charString.program[i], str): - hints.status = 2 - break; - else: - hints.status = 1 # There's *something* here - hints.last_checked = len(charString.program) - - if old_hints: - assert hints.__dict__ == old_hints.__dict__ - - def op_callsubr(self, index): - subr = self.localSubrs[self.operandStack[-1]+self.localBias] - psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) - self.processSubr(index, subr) - - def op_callgsubr(self, index): - subr = self.globalSubrs[self.operandStack[-1]+self.globalBias] - psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) - self.processSubr(index, subr) - - def op_hstem(self, index): - psCharStrings.SimpleT2Decompiler.op_hstem(self, index) - self.processHint(index) - def op_vstem(self, index): - psCharStrings.SimpleT2Decompiler.op_vstem(self, index) - self.processHint(index) - def op_hstemhm(self, index): - psCharStrings.SimpleT2Decompiler.op_hstemhm(self, index) - self.processHint(index) - def op_vstemhm(self, index): - psCharStrings.SimpleT2Decompiler.op_vstemhm(self, index) - self.processHint(index) - def op_hintmask(self, index): - psCharStrings.SimpleT2Decompiler.op_hintmask(self, index) - self.processHintmask(index) - def op_cntrmask(self, index): - psCharStrings.SimpleT2Decompiler.op_cntrmask(self, index) - self.processHintmask(index) - - def processHintmask(self, index): - cs = self.callingStack[-1] - hints = cs._hints - hints.has_hintmask = True - if hints.status != 2 and hints.has_hint: - # Check from last_check, see if we may be an implicit vstem - for i in range(hints.last_checked, index - 1): - if isinstance(cs.program[i], str): - hints.status = 2 - break; - if hints.status != 2: - # We are an implicit vstem - hints.last_hint = index + 1 - hints.status = 0 - hints.last_checked = index + 1 - - def processHint(self, index): - cs = self.callingStack[-1] - hints = cs._hints - hints.has_hint = True - hints.last_hint = index - hints.last_checked = index - - def processSubr(self, index, subr): - cs = self.callingStack[-1] - hints = cs._hints - subr_hints = subr._hints - - if subr_hints.has_hint: - if hints.status != 2: - hints.has_hint = True - hints.last_checked = index - hints.status = subr_hints.status - # Decide where to chop off from - if subr_hints.status == 0: - hints.last_hint = index - else: - hints.last_hint = index - 2 # Leave the subr call in - else: - # In my understanding, this is a font bug. Ie. it has hint stems - # *after* path construction. I've seen this in widespread fonts. - # Best to ignore the hints I suppose... - pass - #assert 0 - else: - hints.status = max(hints.status, subr_hints.status) - if hints.status != 2: - # Check from last_check, make sure we didn't have - # any operators. - for i in range(hints.last_checked, index - 1): - if isinstance(cs.program[i], str): - hints.status = 2 - break; - hints.last_checked = index - if hints.status != 2: - # Decide where to chop off from - if subr_hints.status == 0: - hints.last_hint = index - else: - hints.last_hint = index - 2 # Leave the subr call in - -@_add_method(ttLib.getTableClass('CFF ')) -def prune_post_subset(self, options): - cff = self.cff - for fontname in cff.keys(): - font = cff[fontname] - cs = font.CharStrings - - - # - # Drop unused FontDictionaries - # - if hasattr(font, "FDSelect"): - sel = font.FDSelect - indices = _uniq_sort(sel.gidArray) - sel.gidArray = [indices.index (ss) for ss in sel.gidArray] - arr = font.FDArray - arr.items = [arr[i] for i in indices] - arr.count = len(arr.items) - del arr.file, arr.offsets - - - # - # Drop hints if not needed - # - if not options.hinting: - - # - # This can be tricky, but doesn't have to. What we do is: - # - # - Run all used glyph charstrings and recurse into subroutines, - # - For each charstring (including subroutines), if it has any - # of the hint stem operators, we mark it as such. Upon returning, - # for each charstring we note all the subroutine calls it makes - # that (recursively) contain a stem, - # - Dropping hinting then consists of the following two ops: - # * Drop the piece of the program in each charstring before the - # last call to a stem op or a stem-calling subroutine, - # * Drop all hintmask operations. - # - It's trickier... A hintmask right after hints and a few numbers - # will act as an implicit vstemhm. As such, we track whether - # we have seen any non-hint operators so far and do the right - # thing, recursively... Good luck understanding that :( - # - css = set() - for g in font.charset: - c,sel = cs.getItemAndSelector(g) - # Make sure it's decompiled. We want our "decompiler" to walk - # the program, not the bytecode. - c.draw(basePen.NullPen()) - subrs = getattr(c.private, "Subrs", []) - decompiler = _DehintingT2Decompiler(css, subrs, c.globalSubrs) - decompiler.execute(c) - for charstring in css: - charstring.drop_hints() - - # Drop font-wide hinting values - all_privs = [] - if hasattr(font, 'FDSelect'): - all_privs.extend(fd.Private for fd in font.FDArray) - else: - all_privs.append(font.Private) - for priv in all_privs: - for k in ['BlueValues', 'OtherBlues', 'FamilyBlues', 'FamilyOtherBlues', - 'BlueScale', 'BlueShift', 'BlueFuzz', - 'StemSnapH', 'StemSnapV', 'StdHW', 'StdVW']: - if hasattr(priv, k): - setattr(priv, k, None) - - - # - # Renumber subroutines to remove unused ones - # - - # Mark all used subroutines - for g in font.charset: - c,sel = cs.getItemAndSelector(g) - subrs = getattr(c.private, "Subrs", []) - decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs) - decompiler.execute(c) - - all_subrs = [font.GlobalSubrs] - if hasattr(font, 'FDSelect'): - all_subrs.extend(fd.Private.Subrs for fd in font.FDArray if hasattr(fd.Private, 'Subrs') and fd.Private.Subrs) - elif hasattr(font.Private, 'Subrs') and font.Private.Subrs: - all_subrs.append(font.Private.Subrs) - - subrs = set(subrs) # Remove duplicates - - # Prepare - for subrs in all_subrs: - if not hasattr(subrs, '_used'): - subrs._used = set() - subrs._used = _uniq_sort(subrs._used) - subrs._old_bias = psCharStrings.calcSubrBias(subrs) - subrs._new_bias = psCharStrings.calcSubrBias(subrs._used) - - # Renumber glyph charstrings - for g in font.charset: - c,sel = cs.getItemAndSelector(g) - subrs = getattr(c.private, "Subrs", []) - c.subset_subroutines (subrs, font.GlobalSubrs) - - # Renumber subroutines themselves - for subrs in all_subrs: - - if subrs == font.GlobalSubrs: - if not hasattr(font, 'FDSelect') and hasattr(font.Private, 'Subrs'): - local_subrs = font.Private.Subrs - else: - local_subrs = [] - else: - local_subrs = subrs - - subrs.items = [subrs.items[i] for i in subrs._used] - subrs.count = len(subrs.items) - del subrs.file - if hasattr(subrs, 'offsets'): - del subrs.offsets - - for i in range (subrs.count): - subrs[i].subset_subroutines (local_subrs, font.GlobalSubrs) - - # Cleanup - for subrs in all_subrs: - del subrs._used, subrs._old_bias, subrs._new_bias - - return True - -@_add_method(ttLib.getTableClass('cmap')) -def closure_glyphs(self, s): - tables = [t for t in self.tables if t.isUnicode()] - for u in s.unicodes_requested: - found = False - for table in tables: - if table.format == 14: - for l in table.uvsDict.values(): - # TODO(behdad) Speed this up! - gids = [g for uc,g in l if u == uc and g is not None] - s.glyphs.update(gids) - # Intentionally not setting found=True here. - else: - if u in table.cmap: - s.glyphs.add(table.cmap[u]) - found = True - if not found: - s.log("No default glyph for Unicode %04X found." % u) - -@_add_method(ttLib.getTableClass('cmap')) -def prune_pre_subset(self, options): - if not options.legacy_cmap: - # Drop non-Unicode / non-Symbol cmaps - self.tables = [t for t in self.tables if t.isUnicode() or t.isSymbol()] - if not options.symbol_cmap: - self.tables = [t for t in self.tables if not t.isSymbol()] - # TODO(behdad) Only keep one subtable? - # For now, drop format=0 which can't be subset_glyphs easily? - self.tables = [t for t in self.tables if t.format != 0] - self.numSubTables = len(self.tables) - return True # Required table - -@_add_method(ttLib.getTableClass('cmap')) -def subset_glyphs(self, s): - s.glyphs = s.glyphs_cmaped - for t in self.tables: - # For reasons I don't understand I need this here - # to force decompilation of the cmap format 14. - try: - getattr(t, "asdf") - except AttributeError: - pass - if t.format == 14: - # TODO(behdad) We drop all the default-UVS mappings for glyphs_requested. - # I don't think we care about that... - t.uvsDict = dict((v,[(u,g) for u,g in l - if g in s.glyphs or u in s.unicodes_requested]) - for v,l in t.uvsDict.items()) - t.uvsDict = dict((v,l) for v,l in t.uvsDict.items() if l) - elif t.isUnicode(): - t.cmap = dict((u,g) for u,g in t.cmap.items() - if g in s.glyphs_requested or u in s.unicodes_requested) - else: - t.cmap = dict((u,g) for u,g in t.cmap.items() - if g in s.glyphs_requested) - self.tables = [t for t in self.tables - if (t.cmap if t.format != 14 else t.uvsDict)] - self.numSubTables = len(self.tables) - # TODO(behdad) Convert formats when needed. - # In particular, if we have a format=12 without non-BMP - # characters, either drop format=12 one or convert it - # to format=4 if there's not one. - return True # Required table - -@_add_method(ttLib.getTableClass('name')) -def prune_pre_subset(self, options): - if '*' not in options.name_IDs: - self.names = [n for n in self.names if n.nameID in options.name_IDs] - if not options.name_legacy: - self.names = [n for n in self.names if n.isUnicode()] - # TODO(behdad) Option to keep only one platform's - if '*' not in options.name_languages: - # TODO(behdad) This is Windows-platform specific! - self.names = [n for n in self.names if n.langID in options.name_languages] - return True # Required table - - -# TODO(behdad) OS/2 ulUnicodeRange / ulCodePageRange? -# TODO(behdad) Drop AAT tables. -# TODO(behdad) Drop unneeded GSUB/GPOS Script/LangSys entries. -# TODO(behdad) Drop empty GSUB/GPOS, and GDEF if no GSUB/GPOS left -# TODO(behdad) Drop GDEF subitems if unused by lookups -# TODO(behdad) Avoid recursing too much (in GSUB/GPOS and in CFF) -# TODO(behdad) Text direction considerations. -# TODO(behdad) Text script / language considerations. -# TODO(behdad) Optionally drop 'kern' table if GPOS available -# TODO(behdad) Implement --unicode='*' to choose all cmap'ed -# TODO(behdad) Drop old-spec Indic scripts - - -class Options(object): - - class UnknownOptionError(Exception): - pass - - _drop_tables_default = ['BASE', 'JSTF', 'DSIG', 'EBDT', 'EBLC', 'EBSC', 'SVG ', - 'PCLT', 'LTSH'] - _drop_tables_default += ['Feat', 'Glat', 'Gloc', 'Silf', 'Sill'] # Graphite - _drop_tables_default += ['CBLC', 'CBDT', 'sbix', 'COLR', 'CPAL'] # Color - _no_subset_tables_default = ['gasp', 'head', 'hhea', 'maxp', 'vhea', 'OS/2', - 'loca', 'name', 'cvt ', 'fpgm', 'prep'] - _hinting_tables_default = ['cvt ', 'fpgm', 'prep', 'hdmx', 'VDMX'] - - # Based on HarfBuzz shapers - _layout_features_groups = { - # Default shaper - 'common': ['ccmp', 'liga', 'locl', 'mark', 'mkmk', 'rlig'], - 'horizontal': ['calt', 'clig', 'curs', 'kern', 'rclt'], - 'vertical': ['valt', 'vert', 'vkrn', 'vpal', 'vrt2'], - 'ltr': ['ltra', 'ltrm'], - 'rtl': ['rtla', 'rtlm'], - # Complex shapers - 'arabic': ['init', 'medi', 'fina', 'isol', 'med2', 'fin2', 'fin3', - 'cswh', 'mset'], - 'hangul': ['ljmo', 'vjmo', 'tjmo'], - 'tibetan': ['abvs', 'blws', 'abvm', 'blwm'], - 'indic': ['nukt', 'akhn', 'rphf', 'rkrf', 'pref', 'blwf', 'half', - 'abvf', 'pstf', 'cfar', 'vatu', 'cjct', 'init', 'pres', - 'abvs', 'blws', 'psts', 'haln', 'dist', 'abvm', 'blwm'], - } - _layout_features_default = _uniq_sort(sum( - iter(_layout_features_groups.values()), [])) - - drop_tables = _drop_tables_default - no_subset_tables = _no_subset_tables_default - hinting_tables = _hinting_tables_default - layout_features = _layout_features_default - hinting = True - glyph_names = False - legacy_cmap = False - symbol_cmap = False - name_IDs = [1, 2] # Family and Style - name_legacy = False - name_languages = [0x0409] # English - notdef_glyph = True # gid0 for TrueType / .notdef for CFF - notdef_outline = False # No need for notdef to have an outline really - recommended_glyphs = False # gid1, gid2, gid3 for TrueType - recalc_bounds = False # Recalculate font bounding boxes - recalc_timestamp = False # Recalculate font modified timestamp - canonical_order = False # Order tables as recommended - flavor = None # May be 'woff' - - def __init__(self, **kwargs): - - self.set(**kwargs) - - def set(self, **kwargs): - for k,v in kwargs.items(): - if not hasattr(self, k): - raise self.UnknownOptionError("Unknown option '%s'" % k) - setattr(self, k, v) - - def parse_opts(self, argv, ignore_unknown=False): - ret = [] - opts = {} - for a in argv: - orig_a = a - if not a.startswith('--'): - ret.append(a) - continue - a = a[2:] - i = a.find('=') - op = '=' - if i == -1: - if a.startswith("no-"): - k = a[3:] - v = False - else: - k = a - v = True - else: - k = a[:i] - if k[-1] in "-+": - op = k[-1]+'=' # Ops is '-=' or '+=' now. - k = k[:-1] - v = a[i+1:] - k = k.replace('-', '_') - if not hasattr(self, k): - if ignore_unknown is True or k in ignore_unknown: - ret.append(orig_a) - continue - else: - raise self.UnknownOptionError("Unknown option '%s'" % a) - - ov = getattr(self, k) - if isinstance(ov, bool): - v = bool(v) - elif isinstance(ov, int): - v = int(v) - elif isinstance(ov, list): - vv = v.split(',') - if vv == ['']: - vv = [] - vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv] - if op == '=': - v = vv - elif op == '+=': - v = ov - v.extend(vv) - elif op == '-=': - v = ov - for x in vv: - if x in v: - v.remove(x) - else: - assert False - - opts[k] = v - self.set(**opts) - - return ret - - -class Subsetter(object): - - def __init__(self, options=None, log=None): - - if not log: - log = Logger() - if not options: - options = Options() - - self.options = options - self.log = log - self.unicodes_requested = set() - self.glyphs_requested = set() - self.glyphs = set() - - def populate(self, glyphs=[], unicodes=[], text=""): - self.unicodes_requested.update(unicodes) - if isinstance(text, bytes): - text = text.decode("utf8") - for u in text: - self.unicodes_requested.add(ord(u)) - self.glyphs_requested.update(glyphs) - self.glyphs.update(glyphs) - - def _prune_pre_subset(self, font): - - for tag in font.keys(): - if tag == 'GlyphOrder': continue - - if(tag in self.options.drop_tables or - (tag in self.options.hinting_tables and not self.options.hinting)): - self.log(tag, "dropped") - del font[tag] - continue - - clazz = ttLib.getTableClass(tag) - - if hasattr(clazz, 'prune_pre_subset'): - table = font[tag] - self.log.lapse("load '%s'" % tag) - retain = table.prune_pre_subset(self.options) - self.log.lapse("prune '%s'" % tag) - if not retain: - self.log(tag, "pruned to empty; dropped") - del font[tag] - continue - else: - self.log(tag, "pruned") - - def _closure_glyphs(self, font): - - realGlyphs = set(font.getGlyphOrder()) - - self.glyphs = self.glyphs_requested.copy() - - if 'cmap' in font: - font['cmap'].closure_glyphs(self) - self.glyphs.intersection_update(realGlyphs) - self.glyphs_cmaped = self.glyphs - - if self.options.notdef_glyph: - if 'glyf' in font: - self.glyphs.add(font.getGlyphName(0)) - self.log("Added gid0 to subset") - else: - self.glyphs.add('.notdef') - self.log("Added .notdef to subset") - if self.options.recommended_glyphs: - if 'glyf' in font: - for i in range(min(4, len(font.getGlyphOrder()))): - self.glyphs.add(font.getGlyphName(i)) - self.log("Added first four glyphs to subset") - - if 'GSUB' in font: - self.log("Closing glyph list over 'GSUB': %d glyphs before" % - len(self.glyphs)) - self.log.glyphs(self.glyphs, font=font) - font['GSUB'].closure_glyphs(self) - self.glyphs.intersection_update(realGlyphs) - self.log("Closed glyph list over 'GSUB': %d glyphs after" % - len(self.glyphs)) - self.log.glyphs(self.glyphs, font=font) - self.log.lapse("close glyph list over 'GSUB'") - self.glyphs_gsubed = self.glyphs.copy() - - if 'glyf' in font: - self.log("Closing glyph list over 'glyf': %d glyphs before" % - len(self.glyphs)) - self.log.glyphs(self.glyphs, font=font) - font['glyf'].closure_glyphs(self) - self.glyphs.intersection_update(realGlyphs) - self.log("Closed glyph list over 'glyf': %d glyphs after" % - len(self.glyphs)) - self.log.glyphs(self.glyphs, font=font) - self.log.lapse("close glyph list over 'glyf'") - self.glyphs_glyfed = self.glyphs.copy() - - self.glyphs_all = self.glyphs.copy() - - self.log("Retaining %d glyphs: " % len(self.glyphs_all)) - - del self.glyphs - - - def _subset_glyphs(self, font): - for tag in font.keys(): - if tag == 'GlyphOrder': continue - clazz = ttLib.getTableClass(tag) - - if tag in self.options.no_subset_tables: - self.log(tag, "subsetting not needed") - elif hasattr(clazz, 'subset_glyphs'): - table = font[tag] - self.glyphs = self.glyphs_all - retain = table.subset_glyphs(self) - del self.glyphs - self.log.lapse("subset '%s'" % tag) - if not retain: - self.log(tag, "subsetted to empty; dropped") - del font[tag] - else: - self.log(tag, "subsetted") - else: - self.log(tag, "NOT subset; don't know how to subset; dropped") - del font[tag] - - glyphOrder = font.getGlyphOrder() - glyphOrder = [g for g in glyphOrder if g in self.glyphs_all] - font.setGlyphOrder(glyphOrder) - font._buildReverseGlyphOrderDict() - self.log.lapse("subset GlyphOrder") - - def _prune_post_subset(self, font): - for tag in font.keys(): - if tag == 'GlyphOrder': continue - clazz = ttLib.getTableClass(tag) - if hasattr(clazz, 'prune_post_subset'): - table = font[tag] - retain = table.prune_post_subset(self.options) - self.log.lapse("prune '%s'" % tag) - if not retain: - self.log(tag, "pruned to empty; dropped") - del font[tag] - else: - self.log(tag, "pruned") - - def subset(self, font): - - self._prune_pre_subset(font) - self._closure_glyphs(font) - self._subset_glyphs(font) - self._prune_post_subset(font) - - -class Logger(object): - - def __init__(self, verbose=False, xml=False, timing=False): - self.verbose = verbose - self.xml = xml - self.timing = timing - self.last_time = self.start_time = time.time() - - def parse_opts(self, argv): - argv = argv[:] - for v in ['verbose', 'xml', 'timing']: - if "--"+v in argv: - setattr(self, v, True) - argv.remove("--"+v) - return argv - - def __call__(self, *things): - if not self.verbose: - return - print(' '.join(str(x) for x in things)) - - def lapse(self, *things): - if not self.timing: - return - new_time = time.time() - print("Took %0.3fs to %s" %(new_time - self.last_time, - ' '.join(str(x) for x in things))) - self.last_time = new_time - - def glyphs(self, glyphs, font=None): - if not self.verbose: - return - self("Names: ", sorted(glyphs)) - if font: - reverseGlyphMap = font.getReverseGlyphMap() - self("Gids : ", sorted(reverseGlyphMap[g] for g in glyphs)) - - def font(self, font, file=sys.stdout): - if not self.xml: - return - from fontTools.misc import xmlWriter - writer = xmlWriter.XMLWriter(file) - for tag in font.keys(): - writer.begintag(tag) - writer.newline() - font[tag].toXML(writer, font) - writer.endtag(tag) - writer.newline() - - -def load_font(fontFile, - options, - allowVID=False, - checkChecksums=False, - dontLoadGlyphNames=False, - lazy=True): - - font = ttLib.TTFont(fontFile, - allowVID=allowVID, - checkChecksums=checkChecksums, - recalcBBoxes=options.recalc_bounds, - recalcTimestamp=options.recalc_timestamp, - lazy=lazy) - - # Hack: - # - # If we don't need glyph names, change 'post' class to not try to - # load them. It avoid lots of headache with broken fonts as well - # as loading time. - # - # Ideally ttLib should provide a way to ask it to skip loading - # glyph names. But it currently doesn't provide such a thing. - # - if dontLoadGlyphNames: - post = ttLib.getTableClass('post') - saved = post.decode_format_2_0 - post.decode_format_2_0 = post.decode_format_3_0 - f = font['post'] - if f.formatType == 2.0: - f.formatType = 3.0 - post.decode_format_2_0 = saved - - return font - -def save_font(font, outfile, options): - if options.flavor and not hasattr(font, 'flavor'): - raise Exception("fonttools version does not support flavors.") - font.flavor = options.flavor - font.save(outfile, reorderTables=options.canonical_order) - -def main(args): - - log = Logger() - args = log.parse_opts(args) - - options = Options() - args = options.parse_opts(args, ignore_unknown=['text']) - - if len(args) < 2: - print("usage: pyftsubset font-file glyph... [--text=ABC]... [--option=value]...", file=sys.stderr) - sys.exit(1) - - fontfile = args[0] - args = args[1:] - - dontLoadGlyphNames =(not options.glyph_names and - all(any(g.startswith(p) - for p in ['gid', 'glyph', 'uni', 'U+']) - for g in args)) - - font = load_font(fontfile, options, dontLoadGlyphNames=dontLoadGlyphNames) - log.lapse("load font") - subsetter = Subsetter(options=options, log=log) - - names = font.getGlyphNames() - log.lapse("loading glyph names") - - glyphs = [] - unicodes = [] - text = "" - for g in args: - if g == '*': - glyphs.extend(font.getGlyphOrder()) - continue - if g in names: - glyphs.append(g) - continue - if g.startswith('--text='): - text += g[7:] - continue - if g.startswith('uni') or g.startswith('U+'): - if g.startswith('uni') and len(g) > 3: - g = g[3:] - elif g.startswith('U+') and len(g) > 2: - g = g[2:] - u = int(g, 16) - unicodes.append(u) - continue - if g.startswith('gid') or g.startswith('glyph'): - if g.startswith('gid') and len(g) > 3: - g = g[3:] - elif g.startswith('glyph') and len(g) > 5: - g = g[5:] - try: - glyphs.append(font.getGlyphName(int(g), requireReal=True)) - except ValueError: - raise Exception("Invalid glyph identifier: %s" % g) - continue - raise Exception("Invalid glyph identifier: %s" % g) - log.lapse("compile glyph list") - log("Unicodes:", unicodes) - log("Glyphs:", glyphs) - - subsetter.populate(glyphs=glyphs, unicodes=unicodes, text=text) - subsetter.subset(font) - - outfile = fontfile + '.subset' - - save_font (font, outfile, options) - log.lapse("compile and save font") - - log.last_time = log.start_time - log.lapse("make one with everything(TOTAL TIME)") - - if log.verbose: - import os - log("Input font: %d bytes" % os.path.getsize(fontfile)) - log("Subset font: %d bytes" % os.path.getsize(outfile)) - - log.font(font) - - font.close() - - -__all__ = [ - 'Options', - 'Subsetter', - 'Logger', - 'load_font', - 'save_font', - 'main' -] - -if __name__ == '__main__': - main(sys.argv[1:]) diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py new file mode 100644 index 0000000..8468b7a --- /dev/null +++ b/Lib/fontTools/subset/__init__.py @@ -0,0 +1,3313 @@ +# Copyright 2013 Google, Inc. All Rights Reserved. +# +# Google Author(s): Behdad Esfahbod + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import otRound +from fontTools import ttLib +from fontTools.ttLib.tables import otTables +from fontTools.misc import psCharStrings +from fontTools.pens.basePen import NullPen +from fontTools.misc.loggingTools import Timer +from fontTools.varLib import varStore +import sys +import struct +import array +import logging +from collections import Counter +from types import MethodType + +__usage__ = "pyftsubset font-file [glyph...] [--option=value]..." + +__doc__="""\ +pyftsubset -- OpenType font subsetter and optimizer + + pyftsubset is an OpenType font subsetter and optimizer, based on fontTools. + It accepts any TT- or CFF-flavored OpenType (.otf or .ttf) or WOFF (.woff) + font file. The subsetted glyph set is based on the specified glyphs + or characters, and specified OpenType layout features. + + The tool also performs some size-reducing optimizations, aimed for using + subset fonts as webfonts. Individual optimizations can be enabled or + disabled, and are enabled by default when they are safe. + +Usage: + """+__usage__+""" + + At least one glyph or one of --gids, --gids-file, --glyphs, --glyphs-file, + --text, --text-file, --unicodes, or --unicodes-file, must be specified. + +Arguments: + font-file + The input font file. + glyph + Specify one or more glyph identifiers to include in the subset. Must be + PS glyph names, or the special string '*' to keep the entire glyph set. + +Initial glyph set specification: + These options populate the initial glyph set. Same option can appear + multiple times, and the results are accummulated. + --gids=[,...] + Specify comma/whitespace-separated list of glyph IDs or ranges as + decimal numbers. For example, --gids=10-12,14 adds glyphs with + numbers 10, 11, 12, and 14. + --gids-file= + Like --gids but reads from a file. Anything after a '#' on any line + is ignored as comments. + --glyphs=[,...] + Specify comma/whitespace-separated PS glyph names to add to the subset. + Note that only PS glyph names are accepted, not gidNNN, U+XXXX, etc + that are accepted on the command line. The special string '*' will keep + the entire glyph set. + --glyphs-file= + Like --glyphs but reads from a file. Anything after a '#' on any line + is ignored as comments. + --text= + Specify characters to include in the subset, as UTF-8 string. + --text-file= + Like --text but reads from a file. Newline character are not added to + the subset. + --unicodes=[,...] + Specify comma/whitespace-separated list of Unicode codepoints or + ranges as hex numbers, optionally prefixed with 'U+', 'u', etc. + For example, --unicodes=41-5a,61-7a adds ASCII letters, so does + the more verbose --unicodes=U+0041-005A,U+0061-007A. + The special strings '*' will choose all Unicode characters mapped + by the font. + --unicodes-file= + Like --unicodes, but reads from a file. Anything after a '#' on any + line in the file is ignored as comments. + --ignore-missing-glyphs + Do not fail if some requested glyphs or gids are not available in + the font. + --no-ignore-missing-glyphs + Stop and fail if some requested glyphs or gids are not available + in the font. [default] + --ignore-missing-unicodes [default] + Do not fail if some requested Unicode characters (including those + indirectly specified using --text or --text-file) are not available + in the font. + --no-ignore-missing-unicodes + Stop and fail if some requested Unicode characters are not available + in the font. + Note the default discrepancy between ignoring missing glyphs versus + unicodes. This is for historical reasons and in the future + --no-ignore-missing-unicodes might become default. + +Other options: + For the other options listed below, to see the current value of the option, + pass a value of '?' to it, with or without a '='. + Examples: + $ pyftsubset --glyph-names? + Current setting for 'glyph-names' is: False + $ ./pyftsubset --name-IDs=? + Current setting for 'name-IDs' is: [0, 1, 2, 3, 4, 5, 6] + $ ./pyftsubset --hinting? --no-hinting --hinting? + Current setting for 'hinting' is: True + Current setting for 'hinting' is: False + +Output options: + --output-file= + The output font file. If not specified, the subsetted font + will be saved in as font-file.subset. + --flavor= + Specify flavor of output font file. May be 'woff' or 'woff2'. + Note that WOFF2 requires the Brotli Python extension, available + at https://github.com/google/brotli + --with-zopfli + Use the Google Zopfli algorithm to compress WOFF. The output is 3-8 % + smaller than pure zlib, but the compression speed is much slower. + The Zopfli Python bindings are available at: + https://pypi.python.org/pypi/zopfli + +Glyph set expansion: + These options control how additional glyphs are added to the subset. + --notdef-glyph + Add the '.notdef' glyph to the subset (ie, keep it). [default] + --no-notdef-glyph + Drop the '.notdef' glyph unless specified in the glyph set. This + saves a few bytes, but is not possible for Postscript-flavored + fonts, as those require '.notdef'. For TrueType-flavored fonts, + this works fine as long as no unsupported glyphs are requested + from the font. + --notdef-outline + Keep the outline of '.notdef' glyph. The '.notdef' glyph outline is + used when glyphs not supported by the font are to be shown. It is not + needed otherwise. + --no-notdef-outline + When including a '.notdef' glyph, remove its outline. This saves + a few bytes. [default] + --recommended-glyphs + Add glyphs 0, 1, 2, and 3 to the subset, as recommended for + TrueType-flavored fonts: '.notdef', 'NULL' or '.null', 'CR', 'space'. + Some legacy software might require this, but no modern system does. + --no-recommended-glyphs + Do not add glyphs 0, 1, 2, and 3 to the subset, unless specified in + glyph set. [default] + --layout-features[+|-]=[,...] + Specify (=), add to (+=) or exclude from (-=) the comma-separated + set of OpenType layout feature tags that will be preserved. + Glyph variants used by the preserved features are added to the + specified subset glyph set. By default, 'calt', 'ccmp', 'clig', 'curs', + 'dnom', 'frac', 'kern', 'liga', 'locl', 'mark', 'mkmk', 'numr', 'rclt', + 'rlig', 'rvrn', and all features required for script shaping are + preserved. To see the full list, try '--layout-features=?'. + Use '*' to keep all features. + Multiple --layout-features options can be provided if necessary. + Examples: + --layout-features+=onum,pnum,ss01 + * Keep the default set of features and 'onum', 'pnum', 'ss01'. + --layout-features-='mark','mkmk' + * Keep the default set of features but drop 'mark' and 'mkmk'. + --layout-features='kern' + * Only keep the 'kern' feature, drop all others. + --layout-features='' + * Drop all features. + --layout-features='*' + * Keep all features. + --layout-features+=aalt --layout-features-=vrt2 + * Keep default set of features plus 'aalt', but drop 'vrt2'. + +Hinting options: + --hinting + Keep hinting [default] + --no-hinting + Drop glyph-specific hinting and font-wide hinting tables, as well + as remove hinting-related bits and pieces from other tables (eg. GPOS). + See --hinting-tables for list of tables that are dropped by default. + Instructions and hints are stripped from 'glyf' and 'CFF ' tables + respectively. This produces (sometimes up to 30%) smaller fonts that + are suitable for extremely high-resolution systems, like high-end + mobile devices and retina displays. + +Optimization options: + --desubroutinize + Remove CFF use of subroutinizes. Subroutinization is a way to make CFF + fonts smaller. For small subsets however, desubroutinizing might make + the font smaller. It has even been reported that desubroutinized CFF + fonts compress better (produce smaller output) WOFF and WOFF2 fonts. + Also see note under --no-hinting. + --no-desubroutinize [default] + Leave CFF subroutinizes as is, only throw away unused subroutinizes. + +Font table options: + --drop-tables[+|-]=[,
...] + Specify (=), add to (+=) or exclude from (-=) the comma-separated + set of tables that will be be dropped. + By default, the following tables are dropped: + 'BASE', 'JSTF', 'DSIG', 'EBDT', 'EBLC', 'EBSC', 'SVG ', 'PCLT', 'LTSH' + and Graphite tables: 'Feat', 'Glat', 'Gloc', 'Silf', 'Sill' + and color tables: 'CBLC', 'CBDT', 'sbix'. + The tool will attempt to subset the remaining tables. + Examples: + --drop-tables-='SVG ' + * Drop the default set of tables but keep 'SVG '. + --drop-tables+=GSUB + * Drop the default set of tables and 'GSUB'. + --drop-tables=DSIG + * Only drop the 'DSIG' table, keep all others. + --drop-tables= + * Keep all tables. + --no-subset-tables+=
[,
...] + Add to the set of tables that will not be subsetted. + By default, the following tables are included in this list, as + they do not need subsetting (ignore the fact that 'loca' is listed + here): 'gasp', 'head', 'hhea', 'maxp', 'vhea', 'OS/2', 'loca', 'name', + 'cvt ', 'fpgm', 'prep', 'VMDX', 'DSIG', 'CPAL', 'MVAR', 'cvar', 'STAT'. + By default, tables that the tool does not know how to subset and are not + specified here will be dropped from the font, unless --passthrough-tables + option is passed. + Example: + --no-subset-tables+=FFTM + * Keep 'FFTM' table in the font by preventing subsetting. + --passthrough-tables + Do not drop tables that the tool does not know how to subset. + --no-passthrough-tables + Tables that the tool does not know how to subset and are not specified + in --no-subset-tables will be dropped from the font. [default] + --hinting-tables[-]=
[,
...] + Specify (=), add to (+=) or exclude from (-=) the list of font-wide + hinting tables that will be dropped if --no-hinting is specified, + Examples: + --hinting-tables-='VDMX' + * Drop font-wide hinting tables except 'VDMX'. + --hinting-tables='' + * Keep all font-wide hinting tables (but strip hints from glyphs). + --legacy-kern + Keep TrueType 'kern' table even when OpenType 'GPOS' is available. + --no-legacy-kern + Drop TrueType 'kern' table if OpenType 'GPOS' is available. [default] + +Font naming options: + These options control what is retained in the 'name' table. For numerical + codes, see: http://www.microsoft.com/typography/otspec/name.htm + --name-IDs[+|-]=[,...] + Specify (=), add to (+=) or exclude from (-=) the set of 'name' table + entry nameIDs that will be preserved. By default, only nameIDs between 0 + and 6 are preserved, the rest are dropped. Use '*' to keep all entries. + Examples: + --name-IDs+=7,8,9 + * Also keep Trademark, Manufacturer and Designer name entries. + --name-IDs='' + * Drop all 'name' table entries. + --name-IDs='*' + * keep all 'name' table entries + --name-legacy + Keep legacy (non-Unicode) 'name' table entries (0.x, 1.x etc.). + XXX Note: This might be needed for some fonts that have no Unicode name + entires for English. See: https://github.com/behdad/fonttools/issues/146 + --no-name-legacy + Drop legacy (non-Unicode) 'name' table entries [default] + --name-languages[+|-]=[,] + Specify (=), add to (+=) or exclude from (-=) the set of 'name' table + langIDs that will be preserved. By default only records with langID + 0x0409 (English) are preserved. Use '*' to keep all langIDs. + --obfuscate-names + Make the font unusable as a system font by replacing name IDs 1, 2, 3, 4, + and 6 with dummy strings (it is still fully functional as webfont). + +Glyph naming and encoding options: + --glyph-names + Keep PS glyph names in TT-flavored fonts. In general glyph names are + not needed for correct use of the font. However, some PDF generators + and PDF viewers might rely on glyph names to extract Unicode text + from PDF documents. + --no-glyph-names + Drop PS glyph names in TT-flavored fonts, by using 'post' table + version 3.0. [default] + --legacy-cmap + Keep the legacy 'cmap' subtables (0.x, 1.x, 4.x etc.). + --no-legacy-cmap + Drop the legacy 'cmap' subtables. [default] + --symbol-cmap + Keep the 3.0 symbol 'cmap'. + --no-symbol-cmap + Drop the 3.0 symbol 'cmap'. [default] + +Other font-specific options: + --recalc-bounds + Recalculate font bounding boxes. + --no-recalc-bounds + Keep original font bounding boxes. This is faster and still safe + for all practical purposes. [default] + --recalc-timestamp + Set font 'modified' timestamp to current time. + --no-recalc-timestamp + Do not modify font 'modified' timestamp. [default] + --canonical-order + Order tables as recommended in the OpenType standard. This is not + required by the standard, nor by any known implementation. + --no-canonical-order + Keep original order of font tables. This is faster. [default] + --prune-unicode-ranges + Update the 'OS/2 ulUnicodeRange*' bits after subsetting. The Unicode + ranges defined in the OpenType specification v1.7 are intersected with + the Unicode codepoints specified in the font's Unicode 'cmap' subtables: + when no overlap is found, the bit will be switched off. However, it will + *not* be switched on if an intersection is found. [default] + --no-prune-unicode-ranges + Don't change the 'OS/2 ulUnicodeRange*' bits. + --recalc-average-width + Update the 'OS/2 xAvgCharWidth' field after subsetting. + --no-recalc-average-width + Don't change the 'OS/2 xAvgCharWidth' field. [default] + --font-number= + Select font number for TrueType Collection (.ttc/.otc), starting from 0. + +Application options: + --verbose + Display verbose information of the subsetting process. + --timing + Display detailed timing information of the subsetting process. + --xml + Display the TTX XML representation of subsetted font. + +Example: + Produce a subset containing the characters ' !"#$%' without performing + size-reducing optimizations: + + $ pyftsubset font.ttf --unicodes="U+0020-0025" \\ + --layout-features='*' --glyph-names --symbol-cmap --legacy-cmap \\ + --notdef-glyph --notdef-outline --recommended-glyphs \\ + --name-IDs='*' --name-legacy --name-languages='*' +""" + + +log = logging.getLogger("fontTools.subset") + +def _log_glyphs(self, glyphs, font=None): + self.info("Glyph names: %s", sorted(glyphs)) + if font: + reverseGlyphMap = font.getReverseGlyphMap() + self.info("Glyph IDs: %s", sorted(reverseGlyphMap[g] for g in glyphs)) + +# bind "glyphs" function to 'log' object +log.glyphs = MethodType(_log_glyphs, log) + +# I use a different timing channel so I can configure it separately from the +# main module's logger +timer = Timer(logger=logging.getLogger("fontTools.subset.timer")) + + +def _add_method(*clazzes): + """Returns a decorator function that adds a new method to one or + more classes.""" + def wrapper(method): + done = [] + for clazz in clazzes: + if clazz in done: continue # Support multiple names of a clazz + done.append(clazz) + assert clazz.__name__ != 'DefaultTable', \ + 'Oops, table class not found.' + assert not hasattr(clazz, method.__name__), \ + "Oops, class '%s' has method '%s'." % (clazz.__name__, + method.__name__) + setattr(clazz, method.__name__, method) + return None + return wrapper + +def _uniq_sort(l): + return sorted(set(l)) + +def _set_update(s, *others): + # Jython's set.update only takes one other argument. + # Emulate real set.update... + for other in others: + s.update(other) + +def _dict_subset(d, glyphs): + return {g:d[g] for g in glyphs} + + +@_add_method(otTables.Coverage) +def intersect(self, glyphs): + """Returns ascending list of matching coverage values.""" + return [i for i,g in enumerate(self.glyphs) if g in glyphs] + +@_add_method(otTables.Coverage) +def intersect_glyphs(self, glyphs): + """Returns set of intersecting glyphs.""" + return set(g for g in self.glyphs if g in glyphs) + +@_add_method(otTables.Coverage) +def subset(self, glyphs): + """Returns ascending list of remaining coverage values.""" + indices = self.intersect(glyphs) + self.glyphs = [g for g in self.glyphs if g in glyphs] + return indices + +@_add_method(otTables.Coverage) +def remap(self, coverage_map): + """Remaps coverage.""" + self.glyphs = [self.glyphs[i] for i in coverage_map] + +@_add_method(otTables.ClassDef) +def intersect(self, glyphs): + """Returns ascending list of matching class values.""" + return _uniq_sort( + ([0] if any(g not in self.classDefs for g in glyphs) else []) + + [v for g,v in self.classDefs.items() if g in glyphs]) + +@_add_method(otTables.ClassDef) +def intersect_class(self, glyphs, klass): + """Returns set of glyphs matching class.""" + if klass == 0: + return set(g for g in glyphs if g not in self.classDefs) + return set(g for g,v in self.classDefs.items() + if v == klass and g in glyphs) + +@_add_method(otTables.ClassDef) +def subset(self, glyphs, remap=False): + """Returns ascending list of remaining classes.""" + self.classDefs = {g:v for g,v in self.classDefs.items() if g in glyphs} + # Note: while class 0 has the special meaning of "not matched", + # if no glyph will ever /not match/, we can optimize class 0 out too. + indices = _uniq_sort( + ([0] if any(g not in self.classDefs for g in glyphs) else []) + + list(self.classDefs.values())) + if remap: + self.remap(indices) + return indices + +@_add_method(otTables.ClassDef) +def remap(self, class_map): + """Remaps classes.""" + self.classDefs = {g:class_map.index(v) for g,v in self.classDefs.items()} + +@_add_method(otTables.SingleSubst) +def closure_glyphs(self, s, cur_glyphs): + s.glyphs.update(v for g,v in self.mapping.items() if g in cur_glyphs) + +@_add_method(otTables.SingleSubst) +def subset_glyphs(self, s): + self.mapping = {g:v for g,v in self.mapping.items() + if g in s.glyphs and v in s.glyphs} + return bool(self.mapping) + +@_add_method(otTables.MultipleSubst) +def closure_glyphs(self, s, cur_glyphs): + for glyph, subst in self.mapping.items(): + if glyph in cur_glyphs: + _set_update(s.glyphs, subst) + +@_add_method(otTables.MultipleSubst) +def subset_glyphs(self, s): + self.mapping = {g:v for g,v in self.mapping.items() + if g in s.glyphs and all(sub in s.glyphs for sub in v)} + return bool(self.mapping) + +@_add_method(otTables.AlternateSubst) +def closure_glyphs(self, s, cur_glyphs): + _set_update(s.glyphs, *(vlist for g,vlist in self.alternates.items() + if g in cur_glyphs)) + +@_add_method(otTables.AlternateSubst) +def subset_glyphs(self, s): + self.alternates = {g:vlist + for g,vlist in self.alternates.items() + if g in s.glyphs and + all(v in s.glyphs for v in vlist)} + return bool(self.alternates) + +@_add_method(otTables.LigatureSubst) +def closure_glyphs(self, s, cur_glyphs): + _set_update(s.glyphs, *([seq.LigGlyph for seq in seqs + if all(c in s.glyphs for c in seq.Component)] + for g,seqs in self.ligatures.items() + if g in cur_glyphs)) + +@_add_method(otTables.LigatureSubst) +def subset_glyphs(self, s): + self.ligatures = {g:v for g,v in self.ligatures.items() + if g in s.glyphs} + self.ligatures = {g:[seq for seq in seqs + if seq.LigGlyph in s.glyphs and + all(c in s.glyphs for c in seq.Component)] + for g,seqs in self.ligatures.items()} + self.ligatures = {g:v for g,v in self.ligatures.items() if v} + return bool(self.ligatures) + +@_add_method(otTables.ReverseChainSingleSubst) +def closure_glyphs(self, s, cur_glyphs): + if self.Format == 1: + indices = self.Coverage.intersect(cur_glyphs) + if(not indices or + not all(c.intersect(s.glyphs) + for c in self.LookAheadCoverage + self.BacktrackCoverage)): + return + s.glyphs.update(self.Substitute[i] for i in indices) + else: + assert 0, "unknown format: %s" % self.Format + +@_add_method(otTables.ReverseChainSingleSubst) +def subset_glyphs(self, s): + if self.Format == 1: + indices = self.Coverage.subset(s.glyphs) + self.Substitute = [self.Substitute[i] for i in indices] + # Now drop rules generating glyphs we don't want + indices = [i for i,sub in enumerate(self.Substitute) + if sub in s.glyphs] + self.Substitute = [self.Substitute[i] for i in indices] + self.Coverage.remap(indices) + self.GlyphCount = len(self.Substitute) + return bool(self.GlyphCount and + all(c.subset(s.glyphs) + for c in self.LookAheadCoverage+self.BacktrackCoverage)) + else: + assert 0, "unknown format: %s" % self.Format + +@_add_method(otTables.SinglePos) +def subset_glyphs(self, s): + if self.Format == 1: + return len(self.Coverage.subset(s.glyphs)) + elif self.Format == 2: + indices = self.Coverage.subset(s.glyphs) + values = self.Value + count = len(values) + self.Value = [values[i] for i in indices if i < count] + self.ValueCount = len(self.Value) + return bool(self.ValueCount) + else: + assert 0, "unknown format: %s" % self.Format + +@_add_method(otTables.SinglePos) +def prune_post_subset(self, font, options): + if not options.hinting: + # Drop device tables + self.ValueFormat &= ~0x00F0 + return True + +@_add_method(otTables.PairPos) +def subset_glyphs(self, s): + if self.Format == 1: + indices = self.Coverage.subset(s.glyphs) + pairs = self.PairSet + count = len(pairs) + self.PairSet = [pairs[i] for i in indices if i < count] + for p in self.PairSet: + p.PairValueRecord = [r for r in p.PairValueRecord if r.SecondGlyph in s.glyphs] + p.PairValueCount = len(p.PairValueRecord) + # Remove empty pairsets + indices = [i for i,p in enumerate(self.PairSet) if p.PairValueCount] + self.Coverage.remap(indices) + self.PairSet = [self.PairSet[i] for i in indices] + self.PairSetCount = len(self.PairSet) + return bool(self.PairSetCount) + elif self.Format == 2: + class1_map = [c for c in self.ClassDef1.subset(s.glyphs, remap=True) if c < self.Class1Count] + class2_map = [c for c in self.ClassDef2.subset(s.glyphs, remap=True) if c < self.Class2Count] + self.Class1Record = [self.Class1Record[i] for i in class1_map] + for c in self.Class1Record: + c.Class2Record = [c.Class2Record[i] for i in class2_map] + self.Class1Count = len(class1_map) + self.Class2Count = len(class2_map) + return bool(self.Class1Count and + self.Class2Count and + self.Coverage.subset(s.glyphs)) + else: + assert 0, "unknown format: %s" % self.Format + +@_add_method(otTables.PairPos) +def prune_post_subset(self, font, options): + if not options.hinting: + # Drop device tables + self.ValueFormat1 &= ~0x00F0 + self.ValueFormat2 &= ~0x00F0 + return True + +@_add_method(otTables.CursivePos) +def subset_glyphs(self, s): + if self.Format == 1: + indices = self.Coverage.subset(s.glyphs) + records = self.EntryExitRecord + count = len(records) + self.EntryExitRecord = [records[i] for i in indices if i < count] + self.EntryExitCount = len(self.EntryExitRecord) + return bool(self.EntryExitCount) + else: + assert 0, "unknown format: %s" % self.Format + +@_add_method(otTables.Anchor) +def prune_hints(self): + # Drop device tables / contour anchor point + self.ensureDecompiled() + self.Format = 1 + +@_add_method(otTables.CursivePos) +def prune_post_subset(self, font, options): + if not options.hinting: + for rec in self.EntryExitRecord: + if rec.EntryAnchor: rec.EntryAnchor.prune_hints() + if rec.ExitAnchor: rec.ExitAnchor.prune_hints() + return True + +@_add_method(otTables.MarkBasePos) +def subset_glyphs(self, s): + if self.Format == 1: + mark_indices = self.MarkCoverage.subset(s.glyphs) + self.MarkArray.MarkRecord = [self.MarkArray.MarkRecord[i] for i in mark_indices] + self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord) + base_indices = self.BaseCoverage.subset(s.glyphs) + self.BaseArray.BaseRecord = [self.BaseArray.BaseRecord[i] for i in base_indices] + self.BaseArray.BaseCount = len(self.BaseArray.BaseRecord) + # Prune empty classes + class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord) + self.ClassCount = len(class_indices) + for m in self.MarkArray.MarkRecord: + m.Class = class_indices.index(m.Class) + for b in self.BaseArray.BaseRecord: + b.BaseAnchor = [b.BaseAnchor[i] for i in class_indices] + return bool(self.ClassCount and + self.MarkArray.MarkCount and + self.BaseArray.BaseCount) + else: + assert 0, "unknown format: %s" % self.Format + +@_add_method(otTables.MarkBasePos) +def prune_post_subset(self, font, options): + if not options.hinting: + for m in self.MarkArray.MarkRecord: + if m.MarkAnchor: + m.MarkAnchor.prune_hints() + for b in self.BaseArray.BaseRecord: + for a in b.BaseAnchor: + if a: + a.prune_hints() + return True + +@_add_method(otTables.MarkLigPos) +def subset_glyphs(self, s): + if self.Format == 1: + mark_indices = self.MarkCoverage.subset(s.glyphs) + self.MarkArray.MarkRecord = [self.MarkArray.MarkRecord[i] for i in mark_indices] + self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord) + ligature_indices = self.LigatureCoverage.subset(s.glyphs) + self.LigatureArray.LigatureAttach = [self.LigatureArray.LigatureAttach[i] for i in ligature_indices] + self.LigatureArray.LigatureCount = len(self.LigatureArray.LigatureAttach) + # Prune empty classes + class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord) + self.ClassCount = len(class_indices) + for m in self.MarkArray.MarkRecord: + m.Class = class_indices.index(m.Class) + for l in self.LigatureArray.LigatureAttach: + for c in l.ComponentRecord: + c.LigatureAnchor = [c.LigatureAnchor[i] for i in class_indices] + return bool(self.ClassCount and + self.MarkArray.MarkCount and + self.LigatureArray.LigatureCount) + else: + assert 0, "unknown format: %s" % self.Format + +@_add_method(otTables.MarkLigPos) +def prune_post_subset(self, font, options): + if not options.hinting: + for m in self.MarkArray.MarkRecord: + if m.MarkAnchor: + m.MarkAnchor.prune_hints() + for l in self.LigatureArray.LigatureAttach: + for c in l.ComponentRecord: + for a in c.LigatureAnchor: + if a: + a.prune_hints() + return True + +@_add_method(otTables.MarkMarkPos) +def subset_glyphs(self, s): + if self.Format == 1: + mark1_indices = self.Mark1Coverage.subset(s.glyphs) + self.Mark1Array.MarkRecord = [self.Mark1Array.MarkRecord[i] for i in mark1_indices] + self.Mark1Array.MarkCount = len(self.Mark1Array.MarkRecord) + mark2_indices = self.Mark2Coverage.subset(s.glyphs) + self.Mark2Array.Mark2Record = [self.Mark2Array.Mark2Record[i] for i in mark2_indices] + self.Mark2Array.MarkCount = len(self.Mark2Array.Mark2Record) + # Prune empty classes + class_indices = _uniq_sort(v.Class for v in self.Mark1Array.MarkRecord) + self.ClassCount = len(class_indices) + for m in self.Mark1Array.MarkRecord: + m.Class = class_indices.index(m.Class) + for b in self.Mark2Array.Mark2Record: + b.Mark2Anchor = [b.Mark2Anchor[i] for i in class_indices] + return bool(self.ClassCount and + self.Mark1Array.MarkCount and + self.Mark2Array.MarkCount) + else: + assert 0, "unknown format: %s" % self.Format + +@_add_method(otTables.MarkMarkPos) +def prune_post_subset(self, font, options): + if not options.hinting: + # Drop device tables or contour anchor point + for m in self.Mark1Array.MarkRecord: + if m.MarkAnchor: + m.MarkAnchor.prune_hints() + for b in self.Mark2Array.Mark2Record: + for m in b.Mark2Anchor: + if m: + m.prune_hints() + return True + +@_add_method(otTables.SingleSubst, + otTables.MultipleSubst, + otTables.AlternateSubst, + otTables.LigatureSubst, + otTables.ReverseChainSingleSubst, + otTables.SinglePos, + otTables.PairPos, + otTables.CursivePos, + otTables.MarkBasePos, + otTables.MarkLigPos, + otTables.MarkMarkPos) +def subset_lookups(self, lookup_indices): + pass + +@_add_method(otTables.SingleSubst, + otTables.MultipleSubst, + otTables.AlternateSubst, + otTables.LigatureSubst, + otTables.ReverseChainSingleSubst, + otTables.SinglePos, + otTables.PairPos, + otTables.CursivePos, + otTables.MarkBasePos, + otTables.MarkLigPos, + otTables.MarkMarkPos) +def collect_lookups(self): + return [] + +@_add_method(otTables.SingleSubst, + otTables.MultipleSubst, + otTables.AlternateSubst, + otTables.LigatureSubst, + otTables.ReverseChainSingleSubst, + otTables.ContextSubst, + otTables.ChainContextSubst, + otTables.ContextPos, + otTables.ChainContextPos) +def prune_post_subset(self, font, options): + return True + +@_add_method(otTables.SingleSubst, + otTables.AlternateSubst, + otTables.ReverseChainSingleSubst) +def may_have_non_1to1(self): + return False + +@_add_method(otTables.MultipleSubst, + otTables.LigatureSubst, + otTables.ContextSubst, + otTables.ChainContextSubst) +def may_have_non_1to1(self): + return True + +@_add_method(otTables.ContextSubst, + otTables.ChainContextSubst, + otTables.ContextPos, + otTables.ChainContextPos) +def __subset_classify_context(self): + + class ContextHelper(object): + def __init__(self, klass, Format): + if klass.__name__.endswith('Subst'): + Typ = 'Sub' + Type = 'Subst' + else: + Typ = 'Pos' + Type = 'Pos' + if klass.__name__.startswith('Chain'): + Chain = 'Chain' + InputIdx = 1 + DataLen = 3 + else: + Chain = '' + InputIdx = 0 + DataLen = 1 + ChainTyp = Chain+Typ + + self.Typ = Typ + self.Type = Type + self.Chain = Chain + self.ChainTyp = ChainTyp + self.InputIdx = InputIdx + self.DataLen = DataLen + + self.LookupRecord = Type+'LookupRecord' + + if Format == 1: + Coverage = lambda r: r.Coverage + ChainCoverage = lambda r: r.Coverage + ContextData = lambda r:(None,) + ChainContextData = lambda r:(None, None, None) + SetContextData = None + SetChainContextData = None + RuleData = lambda r:(r.Input,) + ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead) + def SetRuleData(r, d): + (r.Input,) = d + (r.GlyphCount,) = (len(x)+1 for x in d) + def ChainSetRuleData(r, d): + (r.Backtrack, r.Input, r.LookAhead) = d + (r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(d[0]),len(d[1])+1,len(d[2])) + elif Format == 2: + Coverage = lambda r: r.Coverage + ChainCoverage = lambda r: r.Coverage + ContextData = lambda r:(r.ClassDef,) + ChainContextData = lambda r:(r.BacktrackClassDef, + r.InputClassDef, + r.LookAheadClassDef) + def SetContextData(r, d): + (r.ClassDef,) = d + def SetChainContextData(r, d): + (r.BacktrackClassDef, + r.InputClassDef, + r.LookAheadClassDef) = d + RuleData = lambda r:(r.Class,) + ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead) + def SetRuleData(r, d): + (r.Class,) = d + (r.GlyphCount,) = (len(x)+1 for x in d) + def ChainSetRuleData(r, d): + (r.Backtrack, r.Input, r.LookAhead) = d + (r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(d[0]),len(d[1])+1,len(d[2])) + elif Format == 3: + Coverage = lambda r: r.Coverage[0] + ChainCoverage = lambda r: r.InputCoverage[0] + ContextData = None + ChainContextData = None + SetContextData = None + SetChainContextData = None + RuleData = lambda r: r.Coverage + ChainRuleData = lambda r:(r.BacktrackCoverage + + r.InputCoverage + + r.LookAheadCoverage) + def SetRuleData(r, d): + (r.Coverage,) = d + (r.GlyphCount,) = (len(x) for x in d) + def ChainSetRuleData(r, d): + (r.BacktrackCoverage, r.InputCoverage, r.LookAheadCoverage) = d + (r.BacktrackGlyphCount,r.InputGlyphCount,r.LookAheadGlyphCount,) = (len(x) for x in d) + else: + assert 0, "unknown format: %s" % Format + + if Chain: + self.Coverage = ChainCoverage + self.ContextData = ChainContextData + self.SetContextData = SetChainContextData + self.RuleData = ChainRuleData + self.SetRuleData = ChainSetRuleData + else: + self.Coverage = Coverage + self.ContextData = ContextData + self.SetContextData = SetContextData + self.RuleData = RuleData + self.SetRuleData = SetRuleData + + if Format == 1: + self.Rule = ChainTyp+'Rule' + self.RuleCount = ChainTyp+'RuleCount' + self.RuleSet = ChainTyp+'RuleSet' + self.RuleSetCount = ChainTyp+'RuleSetCount' + self.Intersect = lambda glyphs, c, r: [r] if r in glyphs else [] + elif Format == 2: + self.Rule = ChainTyp+'ClassRule' + self.RuleCount = ChainTyp+'ClassRuleCount' + self.RuleSet = ChainTyp+'ClassSet' + self.RuleSetCount = ChainTyp+'ClassSetCount' + self.Intersect = lambda glyphs, c, r: (c.intersect_class(glyphs, r) if c + else (set(glyphs) if r == 0 else set())) + + self.ClassDef = 'InputClassDef' if Chain else 'ClassDef' + self.ClassDefIndex = 1 if Chain else 0 + self.Input = 'Input' if Chain else 'Class' + + if self.Format not in [1, 2, 3]: + return None # Don't shoot the messenger; let it go + if not hasattr(self.__class__, "__ContextHelpers"): + self.__class__.__ContextHelpers = {} + if self.Format not in self.__class__.__ContextHelpers: + helper = ContextHelper(self.__class__, self.Format) + self.__class__.__ContextHelpers[self.Format] = helper + return self.__class__.__ContextHelpers[self.Format] + +@_add_method(otTables.ContextSubst, + otTables.ChainContextSubst) +def closure_glyphs(self, s, cur_glyphs): + c = self.__subset_classify_context() + + indices = c.Coverage(self).intersect(cur_glyphs) + if not indices: + return [] + cur_glyphs = c.Coverage(self).intersect_glyphs(cur_glyphs) + + if self.Format == 1: + ContextData = c.ContextData(self) + rss = getattr(self, c.RuleSet) + rssCount = getattr(self, c.RuleSetCount) + for i in indices: + if i >= rssCount or not rss[i]: continue + for r in getattr(rss[i], c.Rule): + if not r: continue + if not all(all(c.Intersect(s.glyphs, cd, k) for k in klist) + for cd,klist in zip(ContextData, c.RuleData(r))): + continue + chaos = set() + for ll in getattr(r, c.LookupRecord): + if not ll: continue + seqi = ll.SequenceIndex + if seqi in chaos: + # TODO Can we improve this? + pos_glyphs = None + else: + if seqi == 0: + pos_glyphs = frozenset([c.Coverage(self).glyphs[i]]) + else: + pos_glyphs = frozenset([r.Input[seqi - 1]]) + lookup = s.table.LookupList.Lookup[ll.LookupListIndex] + chaos.add(seqi) + if lookup.may_have_non_1to1(): + chaos.update(range(seqi, len(r.Input)+2)) + lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) + elif self.Format == 2: + ClassDef = getattr(self, c.ClassDef) + indices = ClassDef.intersect(cur_glyphs) + ContextData = c.ContextData(self) + rss = getattr(self, c.RuleSet) + rssCount = getattr(self, c.RuleSetCount) + for i in indices: + if i >= rssCount or not rss[i]: continue + for r in getattr(rss[i], c.Rule): + if not r: continue + if not all(all(c.Intersect(s.glyphs, cd, k) for k in klist) + for cd,klist in zip(ContextData, c.RuleData(r))): + continue + chaos = set() + for ll in getattr(r, c.LookupRecord): + if not ll: continue + seqi = ll.SequenceIndex + if seqi in chaos: + # TODO Can we improve this? + pos_glyphs = None + else: + if seqi == 0: + pos_glyphs = frozenset(ClassDef.intersect_class(cur_glyphs, i)) + else: + pos_glyphs = frozenset(ClassDef.intersect_class(s.glyphs, getattr(r, c.Input)[seqi - 1])) + lookup = s.table.LookupList.Lookup[ll.LookupListIndex] + chaos.add(seqi) + if lookup.may_have_non_1to1(): + chaos.update(range(seqi, len(getattr(r, c.Input))+2)) + lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) + elif self.Format == 3: + if not all(x.intersect(s.glyphs) for x in c.RuleData(self)): + return [] + r = self + chaos = set() + for ll in getattr(r, c.LookupRecord): + if not ll: continue + seqi = ll.SequenceIndex + if seqi in chaos: + # TODO Can we improve this? + pos_glyphs = None + else: + if seqi == 0: + pos_glyphs = frozenset(cur_glyphs) + else: + pos_glyphs = frozenset(r.InputCoverage[seqi].intersect_glyphs(s.glyphs)) + lookup = s.table.LookupList.Lookup[ll.LookupListIndex] + chaos.add(seqi) + if lookup.may_have_non_1to1(): + chaos.update(range(seqi, len(r.InputCoverage)+1)) + lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) + else: + assert 0, "unknown format: %s" % self.Format + +@_add_method(otTables.ContextSubst, + otTables.ContextPos, + otTables.ChainContextSubst, + otTables.ChainContextPos) +def subset_glyphs(self, s): + c = self.__subset_classify_context() + + if self.Format == 1: + indices = self.Coverage.subset(s.glyphs) + rss = getattr(self, c.RuleSet) + rssCount = getattr(self, c.RuleSetCount) + rss = [rss[i] for i in indices if i < rssCount] + for rs in rss: + if not rs: continue + ss = getattr(rs, c.Rule) + ss = [r for r in ss + if r and all(all(g in s.glyphs for g in glist) + for glist in c.RuleData(r))] + setattr(rs, c.Rule, ss) + setattr(rs, c.RuleCount, len(ss)) + # Prune empty rulesets + indices = [i for i,rs in enumerate(rss) if rs and getattr(rs, c.Rule)] + self.Coverage.remap(indices) + rss = [rss[i] for i in indices] + setattr(self, c.RuleSet, rss) + setattr(self, c.RuleSetCount, len(rss)) + return bool(rss) + elif self.Format == 2: + if not self.Coverage.subset(s.glyphs): + return False + ContextData = c.ContextData(self) + klass_maps = [x.subset(s.glyphs, remap=True) if x else None for x in ContextData] + + # Keep rulesets for class numbers that survived. + indices = klass_maps[c.ClassDefIndex] + rss = getattr(self, c.RuleSet) + rssCount = getattr(self, c.RuleSetCount) + rss = [rss[i] for i in indices if i < rssCount] + del rssCount + # Delete, but not renumber, unreachable rulesets. + indices = getattr(self, c.ClassDef).intersect(self.Coverage.glyphs) + rss = [rss if i in indices else None for i,rss in enumerate(rss)] + + for rs in rss: + if not rs: continue + ss = getattr(rs, c.Rule) + ss = [r for r in ss + if r and all(all(k in klass_map for k in klist) + for klass_map,klist in zip(klass_maps, c.RuleData(r)))] + setattr(rs, c.Rule, ss) + setattr(rs, c.RuleCount, len(ss)) + + # Remap rule classes + for r in ss: + c.SetRuleData(r, [[klass_map.index(k) for k in klist] + for klass_map,klist in zip(klass_maps, c.RuleData(r))]) + + # Prune empty rulesets + rss = [rs if rs and getattr(rs, c.Rule) else None for rs in rss] + while rss and rss[-1] is None: + del rss[-1] + setattr(self, c.RuleSet, rss) + setattr(self, c.RuleSetCount, len(rss)) + + # TODO: We can do a second round of remapping class values based + # on classes that are actually used in at least one rule. Right + # now we subset classes to c.glyphs only. Or better, rewrite + # the above to do that. + + return bool(rss) + elif self.Format == 3: + return all(x.subset(s.glyphs) for x in c.RuleData(self)) + else: + assert 0, "unknown format: %s" % self.Format + +@_add_method(otTables.ContextSubst, + otTables.ChainContextSubst, + otTables.ContextPos, + otTables.ChainContextPos) +def subset_lookups(self, lookup_indices): + c = self.__subset_classify_context() + + if self.Format in [1, 2]: + for rs in getattr(self, c.RuleSet): + if not rs: continue + for r in getattr(rs, c.Rule): + if not r: continue + setattr(r, c.LookupRecord, + [ll for ll in getattr(r, c.LookupRecord) + if ll and ll.LookupListIndex in lookup_indices]) + for ll in getattr(r, c.LookupRecord): + if not ll: continue + ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex) + elif self.Format == 3: + setattr(self, c.LookupRecord, + [ll for ll in getattr(self, c.LookupRecord) + if ll and ll.LookupListIndex in lookup_indices]) + for ll in getattr(self, c.LookupRecord): + if not ll: continue + ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex) + else: + assert 0, "unknown format: %s" % self.Format + +@_add_method(otTables.ContextSubst, + otTables.ChainContextSubst, + otTables.ContextPos, + otTables.ChainContextPos) +def collect_lookups(self): + c = self.__subset_classify_context() + + if self.Format in [1, 2]: + return [ll.LookupListIndex + for rs in getattr(self, c.RuleSet) if rs + for r in getattr(rs, c.Rule) if r + for ll in getattr(r, c.LookupRecord) if ll] + elif self.Format == 3: + return [ll.LookupListIndex + for ll in getattr(self, c.LookupRecord) if ll] + else: + assert 0, "unknown format: %s" % self.Format + +@_add_method(otTables.ExtensionSubst) +def closure_glyphs(self, s, cur_glyphs): + if self.Format == 1: + self.ExtSubTable.closure_glyphs(s, cur_glyphs) + else: + assert 0, "unknown format: %s" % self.Format + +@_add_method(otTables.ExtensionSubst) +def may_have_non_1to1(self): + if self.Format == 1: + return self.ExtSubTable.may_have_non_1to1() + else: + assert 0, "unknown format: %s" % self.Format + +@_add_method(otTables.ExtensionSubst, + otTables.ExtensionPos) +def subset_glyphs(self, s): + if self.Format == 1: + return self.ExtSubTable.subset_glyphs(s) + else: + assert 0, "unknown format: %s" % self.Format + +@_add_method(otTables.ExtensionSubst, + otTables.ExtensionPos) +def prune_post_subset(self, font, options): + if self.Format == 1: + return self.ExtSubTable.prune_post_subset(font, options) + else: + assert 0, "unknown format: %s" % self.Format + +@_add_method(otTables.ExtensionSubst, + otTables.ExtensionPos) +def subset_lookups(self, lookup_indices): + if self.Format == 1: + return self.ExtSubTable.subset_lookups(lookup_indices) + else: + assert 0, "unknown format: %s" % self.Format + +@_add_method(otTables.ExtensionSubst, + otTables.ExtensionPos) +def collect_lookups(self): + if self.Format == 1: + return self.ExtSubTable.collect_lookups() + else: + assert 0, "unknown format: %s" % self.Format + +@_add_method(otTables.Lookup) +def closure_glyphs(self, s, cur_glyphs=None): + if cur_glyphs is None: + cur_glyphs = frozenset(s.glyphs) + + # Memoize + key = id(self) + doneLookups = s._doneLookups + count,covered = doneLookups.get(key, (0, None)) + if count != len(s.glyphs): + count,covered = doneLookups[key] = (len(s.glyphs), set()) + if cur_glyphs.issubset(covered): + return + covered.update(cur_glyphs) + + for st in self.SubTable: + if not st: continue + st.closure_glyphs(s, cur_glyphs) + +@_add_method(otTables.Lookup) +def subset_glyphs(self, s): + self.SubTable = [st for st in self.SubTable if st and st.subset_glyphs(s)] + self.SubTableCount = len(self.SubTable) + return bool(self.SubTableCount) + +@_add_method(otTables.Lookup) +def prune_post_subset(self, font, options): + ret = False + for st in self.SubTable: + if not st: continue + if st.prune_post_subset(font, options): ret = True + return ret + +@_add_method(otTables.Lookup) +def subset_lookups(self, lookup_indices): + for s in self.SubTable: + s.subset_lookups(lookup_indices) + +@_add_method(otTables.Lookup) +def collect_lookups(self): + return sum((st.collect_lookups() for st in self.SubTable if st), []) + +@_add_method(otTables.Lookup) +def may_have_non_1to1(self): + return any(st.may_have_non_1to1() for st in self.SubTable if st) + +@_add_method(otTables.LookupList) +def subset_glyphs(self, s): + """Returns the indices of nonempty lookups.""" + return [i for i,l in enumerate(self.Lookup) if l and l.subset_glyphs(s)] + +@_add_method(otTables.LookupList) +def prune_post_subset(self, font, options): + ret = False + for l in self.Lookup: + if not l: continue + if l.prune_post_subset(font, options): ret = True + return ret + +@_add_method(otTables.LookupList) +def subset_lookups(self, lookup_indices): + self.ensureDecompiled() + self.Lookup = [self.Lookup[i] for i in lookup_indices + if i < self.LookupCount] + self.LookupCount = len(self.Lookup) + for l in self.Lookup: + l.subset_lookups(lookup_indices) + +@_add_method(otTables.LookupList) +def neuter_lookups(self, lookup_indices): + """Sets lookups not in lookup_indices to None.""" + self.ensureDecompiled() + self.Lookup = [l if i in lookup_indices else None for i,l in enumerate(self.Lookup)] + +@_add_method(otTables.LookupList) +def closure_lookups(self, lookup_indices): + """Returns sorted index of all lookups reachable from lookup_indices.""" + lookup_indices = _uniq_sort(lookup_indices) + recurse = lookup_indices + while True: + recurse_lookups = sum((self.Lookup[i].collect_lookups() + for i in recurse if i < self.LookupCount), []) + recurse_lookups = [l for l in recurse_lookups + if l not in lookup_indices and l < self.LookupCount] + if not recurse_lookups: + return _uniq_sort(lookup_indices) + recurse_lookups = _uniq_sort(recurse_lookups) + lookup_indices.extend(recurse_lookups) + recurse = recurse_lookups + +@_add_method(otTables.Feature) +def subset_lookups(self, lookup_indices): + """"Returns True if feature is non-empty afterwards.""" + self.LookupListIndex = [l for l in self.LookupListIndex + if l in lookup_indices] + # Now map them. + self.LookupListIndex = [lookup_indices.index(l) + for l in self.LookupListIndex] + self.LookupCount = len(self.LookupListIndex) + return self.LookupCount or self.FeatureParams + +@_add_method(otTables.FeatureList) +def subset_lookups(self, lookup_indices): + """Returns the indices of nonempty features.""" + # Note: Never ever drop feature 'pref', even if it's empty. + # HarfBuzz chooses shaper for Khmer based on presence of this + # feature. See thread at: + # http://lists.freedesktop.org/archives/harfbuzz/2012-November/002660.html + return [i for i,f in enumerate(self.FeatureRecord) + if (f.Feature.subset_lookups(lookup_indices) or + f.FeatureTag == 'pref')] + +@_add_method(otTables.FeatureList) +def collect_lookups(self, feature_indices): + return sum((self.FeatureRecord[i].Feature.LookupListIndex + for i in feature_indices + if i < self.FeatureCount), []) + +@_add_method(otTables.FeatureList) +def subset_features(self, feature_indices): + self.ensureDecompiled() + self.FeatureRecord = [self.FeatureRecord[i] for i in feature_indices] + self.FeatureCount = len(self.FeatureRecord) + return bool(self.FeatureCount) + +@_add_method(otTables.FeatureTableSubstitution) +def subset_lookups(self, lookup_indices): + """Returns the indices of nonempty features.""" + return [r.FeatureIndex for r in self.SubstitutionRecord + if r.Feature.subset_lookups(lookup_indices)] + +@_add_method(otTables.FeatureVariations) +def subset_lookups(self, lookup_indices): + """Returns the indices of nonempty features.""" + return sum((f.FeatureTableSubstitution.subset_lookups(lookup_indices) + for f in self.FeatureVariationRecord), []) + +@_add_method(otTables.FeatureVariations) +def collect_lookups(self, feature_indices): + return sum((r.Feature.LookupListIndex + for vr in self.FeatureVariationRecord + for r in vr.FeatureTableSubstitution.SubstitutionRecord + if r.FeatureIndex in feature_indices), []) + +@_add_method(otTables.FeatureTableSubstitution) +def subset_features(self, feature_indices): + self.ensureDecompiled() + self.SubstitutionRecord = [r for r in self.SubstitutionRecord + if r.FeatureIndex in feature_indices] + self.SubstitutionCount = len(self.SubstitutionRecord) + return bool(self.SubstitutionCount) + +@_add_method(otTables.FeatureVariations) +def subset_features(self, feature_indices): + self.ensureDecompiled() + self.FeaturVariationRecord = [r for r in self.FeatureVariationRecord + if r.FeatureTableSubstitution.subset_features(feature_indices)] + self.FeatureVariationCount = len(self.FeatureVariationRecord) + return bool(self.FeatureVariationCount) + +@_add_method(otTables.DefaultLangSys, + otTables.LangSys) +def subset_features(self, feature_indices): + if self.ReqFeatureIndex in feature_indices: + self.ReqFeatureIndex = feature_indices.index(self.ReqFeatureIndex) + else: + self.ReqFeatureIndex = 65535 + self.FeatureIndex = [f for f in self.FeatureIndex if f in feature_indices] + # Now map them. + self.FeatureIndex = [feature_indices.index(f) for f in self.FeatureIndex + if f in feature_indices] + self.FeatureCount = len(self.FeatureIndex) + return bool(self.FeatureCount or self.ReqFeatureIndex != 65535) + +@_add_method(otTables.DefaultLangSys, + otTables.LangSys) +def collect_features(self): + feature_indices = self.FeatureIndex[:] + if self.ReqFeatureIndex != 65535: + feature_indices.append(self.ReqFeatureIndex) + return _uniq_sort(feature_indices) + +@_add_method(otTables.Script) +def subset_features(self, feature_indices, keepEmptyDefaultLangSys=False): + if(self.DefaultLangSys and + not self.DefaultLangSys.subset_features(feature_indices) and + not keepEmptyDefaultLangSys): + self.DefaultLangSys = None + self.LangSysRecord = [l for l in self.LangSysRecord + if l.LangSys.subset_features(feature_indices)] + self.LangSysCount = len(self.LangSysRecord) + return bool(self.LangSysCount or self.DefaultLangSys) + +@_add_method(otTables.Script) +def collect_features(self): + feature_indices = [l.LangSys.collect_features() for l in self.LangSysRecord] + if self.DefaultLangSys: + feature_indices.append(self.DefaultLangSys.collect_features()) + return _uniq_sort(sum(feature_indices, [])) + +@_add_method(otTables.ScriptList) +def subset_features(self, feature_indices, retain_empty): + # https://bugzilla.mozilla.org/show_bug.cgi?id=1331737#c32 + self.ScriptRecord = [s for s in self.ScriptRecord + if s.Script.subset_features(feature_indices, s.ScriptTag=='DFLT') or + retain_empty] + self.ScriptCount = len(self.ScriptRecord) + return bool(self.ScriptCount) + +@_add_method(otTables.ScriptList) +def collect_features(self): + return _uniq_sort(sum((s.Script.collect_features() + for s in self.ScriptRecord), [])) + +# CBLC will inherit it +@_add_method(ttLib.getTableClass('EBLC')) +def subset_glyphs(self, s): + for strike in self.strikes: + for indexSubTable in strike.indexSubTables: + indexSubTable.names = [n for n in indexSubTable.names if n in s.glyphs] + strike.indexSubTables = [i for i in strike.indexSubTables if i.names] + self.strikes = [s for s in self.strikes if s.indexSubTables] + + return True + +# CBDC will inherit it +@_add_method(ttLib.getTableClass('EBDT')) +def subset_glyphs(self, s): + self.strikeData = [{g: strike[g] for g in s.glyphs if g in strike} + for strike in self.strikeData] + return True + +@_add_method(ttLib.getTableClass('GSUB')) +def closure_glyphs(self, s): + s.table = self.table + if self.table.ScriptList: + feature_indices = self.table.ScriptList.collect_features() + else: + feature_indices = [] + if self.table.FeatureList: + lookup_indices = self.table.FeatureList.collect_lookups(feature_indices) + else: + lookup_indices = [] + if getattr(self.table, 'FeatureVariations', None): + lookup_indices += self.table.FeatureVariations.collect_lookups(feature_indices) + lookup_indices = _uniq_sort(lookup_indices) + if self.table.LookupList: + s._doneLookups = {} + while True: + orig_glyphs = frozenset(s.glyphs) + for i in lookup_indices: + if i >= self.table.LookupList.LookupCount: continue + if not self.table.LookupList.Lookup[i]: continue + self.table.LookupList.Lookup[i].closure_glyphs(s) + if orig_glyphs == s.glyphs: + break + del s._doneLookups + del s.table + +@_add_method(ttLib.getTableClass('GSUB'), + ttLib.getTableClass('GPOS')) +def subset_glyphs(self, s): + s.glyphs = s.glyphs_gsubed + if self.table.LookupList: + lookup_indices = self.table.LookupList.subset_glyphs(s) + else: + lookup_indices = [] + self.subset_lookups(lookup_indices) + return True + +@_add_method(ttLib.getTableClass('GSUB'), + ttLib.getTableClass('GPOS')) +def retain_empty_scripts(self): + # https://github.com/behdad/fonttools/issues/518 + # https://bugzilla.mozilla.org/show_bug.cgi?id=1080739#c15 + return self.__class__ == ttLib.getTableClass('GSUB') + +@_add_method(ttLib.getTableClass('GSUB'), + ttLib.getTableClass('GPOS')) +def subset_lookups(self, lookup_indices): + """Retains specified lookups, then removes empty features, language + systems, and scripts.""" + if self.table.LookupList: + self.table.LookupList.subset_lookups(lookup_indices) + if self.table.FeatureList: + feature_indices = self.table.FeatureList.subset_lookups(lookup_indices) + else: + feature_indices = [] + if getattr(self.table, 'FeatureVariations', None): + feature_indices += self.table.FeatureVariations.subset_lookups(lookup_indices) + feature_indices = _uniq_sort(feature_indices) + if self.table.FeatureList: + self.table.FeatureList.subset_features(feature_indices) + if getattr(self.table, 'FeatureVariations', None): + self.table.FeatureVariations.subset_features(feature_indices) + if self.table.ScriptList: + self.table.ScriptList.subset_features(feature_indices, self.retain_empty_scripts()) + +@_add_method(ttLib.getTableClass('GSUB'), + ttLib.getTableClass('GPOS')) +def neuter_lookups(self, lookup_indices): + """Sets lookups not in lookup_indices to None.""" + if self.table.LookupList: + self.table.LookupList.neuter_lookups(lookup_indices) + +@_add_method(ttLib.getTableClass('GSUB'), + ttLib.getTableClass('GPOS')) +def prune_lookups(self, remap=True): + """Remove (default) or neuter unreferenced lookups""" + if self.table.ScriptList: + feature_indices = self.table.ScriptList.collect_features() + else: + feature_indices = [] + if self.table.FeatureList: + lookup_indices = self.table.FeatureList.collect_lookups(feature_indices) + else: + lookup_indices = [] + if getattr(self.table, 'FeatureVariations', None): + lookup_indices += self.table.FeatureVariations.collect_lookups(feature_indices) + lookup_indices = _uniq_sort(lookup_indices) + if self.table.LookupList: + lookup_indices = self.table.LookupList.closure_lookups(lookup_indices) + else: + lookup_indices = [] + if remap: + self.subset_lookups(lookup_indices) + else: + self.neuter_lookups(lookup_indices) + +@_add_method(ttLib.getTableClass('GSUB'), + ttLib.getTableClass('GPOS')) +def subset_feature_tags(self, feature_tags): + if self.table.FeatureList: + feature_indices = \ + [i for i,f in enumerate(self.table.FeatureList.FeatureRecord) + if f.FeatureTag in feature_tags] + self.table.FeatureList.subset_features(feature_indices) + if getattr(self.table, 'FeatureVariations', None): + self.table.FeatureVariations.subset_features(feature_indices) + else: + feature_indices = [] + if self.table.ScriptList: + self.table.ScriptList.subset_features(feature_indices, self.retain_empty_scripts()) + +@_add_method(ttLib.getTableClass('GSUB'), + ttLib.getTableClass('GPOS')) +def prune_features(self): + """Remove unreferenced features""" + if self.table.ScriptList: + feature_indices = self.table.ScriptList.collect_features() + else: + feature_indices = [] + if self.table.FeatureList: + self.table.FeatureList.subset_features(feature_indices) + if getattr(self.table, 'FeatureVariations', None): + self.table.FeatureVariations.subset_features(feature_indices) + if self.table.ScriptList: + self.table.ScriptList.subset_features(feature_indices, self.retain_empty_scripts()) + +@_add_method(ttLib.getTableClass('GSUB'), + ttLib.getTableClass('GPOS')) +def prune_pre_subset(self, font, options): + # Drop undesired features + if '*' not in options.layout_features: + self.subset_feature_tags(options.layout_features) + # Neuter unreferenced lookups + self.prune_lookups(remap=False) + return True + +@_add_method(ttLib.getTableClass('GSUB'), + ttLib.getTableClass('GPOS')) +def remove_redundant_langsys(self): + table = self.table + if not table.ScriptList or not table.FeatureList: + return + + features = table.FeatureList.FeatureRecord + + for s in table.ScriptList.ScriptRecord: + d = s.Script.DefaultLangSys + if not d: + continue + for lr in s.Script.LangSysRecord[:]: + l = lr.LangSys + # Compare d and l + if len(d.FeatureIndex) != len(l.FeatureIndex): + continue + if (d.ReqFeatureIndex == 65535) != (l.ReqFeatureIndex == 65535): + continue + + if d.ReqFeatureIndex != 65535: + if features[d.ReqFeatureIndex] != features[l.ReqFeatureIndex]: + continue + + for i in range(len(d.FeatureIndex)): + if features[d.FeatureIndex[i]] != features[l.FeatureIndex[i]]: + break + else: + # LangSys and default are equal; delete LangSys + s.Script.LangSysRecord.remove(lr) + +@_add_method(ttLib.getTableClass('GSUB'), + ttLib.getTableClass('GPOS')) +def prune_post_subset(self, font, options): + table = self.table + + self.prune_lookups() # XXX Is this actually needed?! + + if table.LookupList: + table.LookupList.prune_post_subset(font, options) + # XXX Next two lines disabled because OTS is stupid and + # doesn't like NULL offsets here. + #if not table.LookupList.Lookup: + # table.LookupList = None + + if not table.LookupList: + table.FeatureList = None + + + if table.FeatureList: + self.remove_redundant_langsys() + # Remove unreferenced features + self.prune_features() + + # XXX Next two lines disabled because OTS is stupid and + # doesn't like NULL offsets here. + #if table.FeatureList and not table.FeatureList.FeatureRecord: + # table.FeatureList = None + + # Never drop scripts themselves as them just being available + # holds semantic significance. + # XXX Next two lines disabled because OTS is stupid and + # doesn't like NULL offsets here. + #if table.ScriptList and not table.ScriptList.ScriptRecord: + # table.ScriptList = None + + if not table.FeatureList and hasattr(table, 'FeatureVariations'): + table.FeatureVariations = None + + if hasattr(table, 'FeatureVariations') and not table.FeatureVariations: + if table.Version == 0x00010001: + table.Version = 0x00010000 + + return True + +@_add_method(ttLib.getTableClass('GDEF')) +def subset_glyphs(self, s): + glyphs = s.glyphs_gsubed + table = self.table + if table.LigCaretList: + indices = table.LigCaretList.Coverage.subset(glyphs) + table.LigCaretList.LigGlyph = [table.LigCaretList.LigGlyph[i] for i in indices] + table.LigCaretList.LigGlyphCount = len(table.LigCaretList.LigGlyph) + if table.MarkAttachClassDef: + table.MarkAttachClassDef.classDefs = \ + {g:v for g,v in table.MarkAttachClassDef.classDefs.items() + if g in glyphs} + if table.GlyphClassDef: + table.GlyphClassDef.classDefs = \ + {g:v for g,v in table.GlyphClassDef.classDefs.items() + if g in glyphs} + if table.AttachList: + indices = table.AttachList.Coverage.subset(glyphs) + GlyphCount = table.AttachList.GlyphCount + table.AttachList.AttachPoint = [table.AttachList.AttachPoint[i] + for i in indices if i < GlyphCount] + table.AttachList.GlyphCount = len(table.AttachList.AttachPoint) + if hasattr(table, "MarkGlyphSetsDef") and table.MarkGlyphSetsDef: + for coverage in table.MarkGlyphSetsDef.Coverage: + if coverage: + coverage.subset(glyphs) + + # TODO: The following is disabled. If enabling, we need to go fixup all + # lookups that use MarkFilteringSet and map their set. + # indices = table.MarkGlyphSetsDef.Coverage = \ + # [c for c in table.MarkGlyphSetsDef.Coverage if c.glyphs] + # TODO: The following is disabled, as ots doesn't like it. Phew... + # https://github.com/khaledhosny/ots/issues/172 + # table.MarkGlyphSetsDef.Coverage = [c if c.glyphs else None for c in table.MarkGlyphSetsDef.Coverage] + return True + + +def _pruneGDEF(font): + if 'GDEF' not in font: return + gdef = font['GDEF'] + table = gdef.table + if not hasattr(table, 'VarStore'): return + + store = table.VarStore + + usedVarIdxes = set() + + # Collect. + table.collect_device_varidxes(usedVarIdxes) + if 'GPOS' in font: + font['GPOS'].table.collect_device_varidxes(usedVarIdxes) + + # Subset. + varidx_map = store.subset_varidxes(usedVarIdxes) + + # Map. + table.remap_device_varidxes(varidx_map) + if 'GPOS' in font: + font['GPOS'].table.remap_device_varidxes(varidx_map) + +@_add_method(ttLib.getTableClass('GDEF')) +def prune_post_subset(self, font, options): + table = self.table + # XXX check these against OTS + if table.LigCaretList and not table.LigCaretList.LigGlyphCount: + table.LigCaretList = None + if table.MarkAttachClassDef and not table.MarkAttachClassDef.classDefs: + table.MarkAttachClassDef = None + if table.GlyphClassDef and not table.GlyphClassDef.classDefs: + table.GlyphClassDef = None + if table.AttachList and not table.AttachList.GlyphCount: + table.AttachList = None + if hasattr(table, "VarStore"): + _pruneGDEF(font) + if table.VarStore.VarDataCount == 0: + if table.Version == 0x00010003: + table.Version = 0x00010002 + if (not hasattr(table, "MarkGlyphSetsDef") or + not table.MarkGlyphSetsDef or + not table.MarkGlyphSetsDef.Coverage): + table.MarkGlyphSetsDef = None + if table.Version == 0x00010002: + table.Version = 0x00010000 + return bool(table.LigCaretList or + table.MarkAttachClassDef or + table.GlyphClassDef or + table.AttachList or + (table.Version >= 0x00010002 and table.MarkGlyphSetsDef) or + (table.Version >= 0x00010003 and table.VarStore)) + +@_add_method(ttLib.getTableClass('kern')) +def prune_pre_subset(self, font, options): + # Prune unknown kern table types + self.kernTables = [t for t in self.kernTables if hasattr(t, 'kernTable')] + return bool(self.kernTables) + +@_add_method(ttLib.getTableClass('kern')) +def subset_glyphs(self, s): + glyphs = s.glyphs_gsubed + for t in self.kernTables: + t.kernTable = {(a,b):v for (a,b),v in t.kernTable.items() + if a in glyphs and b in glyphs} + self.kernTables = [t for t in self.kernTables if t.kernTable] + return bool(self.kernTables) + +@_add_method(ttLib.getTableClass('vmtx')) +def subset_glyphs(self, s): + self.metrics = _dict_subset(self.metrics, s.glyphs) + return bool(self.metrics) + +@_add_method(ttLib.getTableClass('hmtx')) +def subset_glyphs(self, s): + self.metrics = _dict_subset(self.metrics, s.glyphs) + return True # Required table + +@_add_method(ttLib.getTableClass('hdmx')) +def subset_glyphs(self, s): + self.hdmx = {sz:_dict_subset(l, s.glyphs) for sz,l in self.hdmx.items()} + return bool(self.hdmx) + +@_add_method(ttLib.getTableClass('ankr')) +def subset_glyphs(self, s): + table = self.table.AnchorPoints + assert table.Format == 0, "unknown 'ankr' format %s" % table.Format + table.Anchors = {glyph: table.Anchors[glyph] for glyph in s.glyphs + if glyph in table.Anchors} + return len(table.Anchors) > 0 + +@_add_method(ttLib.getTableClass('bsln')) +def closure_glyphs(self, s): + table = self.table.Baseline + if table.Format in (2, 3): + s.glyphs.add(table.StandardGlyph) + +@_add_method(ttLib.getTableClass('bsln')) +def subset_glyphs(self, s): + table = self.table.Baseline + if table.Format in (1, 3): + baselines = {glyph: table.BaselineValues.get(glyph, table.DefaultBaseline) + for glyph in s.glyphs} + if len(baselines) > 0: + mostCommon, _cnt = Counter(baselines.values()).most_common(1)[0] + table.DefaultBaseline = mostCommon + baselines = {glyph: b for glyph, b in baselines.items() + if b != mostCommon} + if len(baselines) > 0: + table.BaselineValues = baselines + else: + table.Format = {1: 0, 3: 2}[table.Format] + del table.BaselineValues + return True + +@_add_method(ttLib.getTableClass('lcar')) +def subset_glyphs(self, s): + table = self.table.LigatureCarets + if table.Format in (0, 1): + table.Carets = {glyph: table.Carets[glyph] for glyph in s.glyphs + if glyph in table.Carets} + return len(table.Carets) > 0 + else: + assert False, "unknown 'lcar' format %s" % table.Format + +@_add_method(ttLib.getTableClass('gvar')) +def prune_pre_subset(self, font, options): + if options.notdef_glyph and not options.notdef_outline: + self.variations[font.glyphOrder[0]] = [] + return True + +@_add_method(ttLib.getTableClass('gvar')) +def subset_glyphs(self, s): + self.variations = _dict_subset(self.variations, s.glyphs) + self.glyphCount = len(self.variations) + return bool(self.variations) + +@_add_method(ttLib.getTableClass('HVAR')) +def subset_glyphs(self, s): + table = self.table + + used = set() + + if table.AdvWidthMap: + table.AdvWidthMap.mapping = _dict_subset(table.AdvWidthMap.mapping, s.glyphs) + used.update(table.AdvWidthMap.mapping.values()) + else: + assert table.LsbMap is None and table.RsbMap is None, "File a bug." + used.update(s.reverseOrigGlyphMap.values()) + + if table.LsbMap: + table.LsbMap.mapping = _dict_subset(table.LsbMap.mapping, s.glyphs) + used.update(table.LsbMap.mapping.values()) + if table.RsbMap: + table.RsbMap.mapping = _dict_subset(table.RsbMap.mapping, s.glyphs) + used.update(table.RsbMap.mapping.values()) + + varidx_map = varStore.VarStore_subset_varidxes(table.VarStore, used) + + if table.AdvWidthMap: + table.AdvWidthMap.mapping = {k:varidx_map[v] for k,v in table.AdvWidthMap.mapping.items()} + if table.LsbMap: + table.LsbMap.mapping = {k:varidx_map[v] for k,v in table.LsbMap.mapping.items()} + if table.RsbMap: + table.RsbMap.mapping = {k:varidx_map[v] for k,v in table.RsbMap.mapping.items()} + + # TODO Return emptiness... + return True + +@_add_method(ttLib.getTableClass('VVAR')) +def subset_glyphs(self, s): + table = self.table + + used = set() + + if table.AdvHeightMap: + table.AdvHeightMap.mapping = _dict_subset(table.AdvHeightMap.mapping, s.glyphs) + used.update(table.AdvHeightMap.mapping.values()) + else: + assert table.TsbMap is None and table.BsbMap is None and table.VOrgMap is None, "File a bug." + used.update(s.reverseOrigGlyphMap.values()) + if table.TsbMap: + table.TsbMap.mapping = _dict_subset(table.TsbMap.mapping, s.glyphs) + used.update(table.TsbMap.mapping.values()) + if table.BsbMap: + table.BsbMap.mapping = _dict_subset(table.BsbMap.mapping, s.glyphs) + used.update(table.BsbMap.mapping.values()) + if table.VOrgMap: + table.VOrgMap.mapping = _dict_subset(table.VOrgMap.mapping, s.glyphs) + used.update(table.VOrgMap.mapping.values()) + + varidx_map = varStore.VarStore_subset_varidxes(table.VarStore, used) + + if table.AdvHeightMap: + table.AdvHeightMap.mapping = {k:varidx_map[v] for k,v in table.AdvHeightMap.mapping.items()} + if table.TsbMap: + table.TsbMap.mapping = {k:varidx_map[v] for k,v in table.TsbMap.mapping.items()} + if table.BsbMap: + table.RsbMap.mapping = {k:varidx_map[v] for k,v in table.RsbMap.mapping.items()} + if table.VOrgMap: + table.RsbMap.mapping = {k:varidx_map[v] for k,v in table.RsbMap.mapping.items()} + + # TODO Return emptiness... + return True + +@_add_method(ttLib.getTableClass('VORG')) +def subset_glyphs(self, s): + self.VOriginRecords = {g:v for g,v in self.VOriginRecords.items() + if g in s.glyphs} + self.numVertOriginYMetrics = len(self.VOriginRecords) + return True # Never drop; has default metrics + +@_add_method(ttLib.getTableClass('opbd')) +def subset_glyphs(self, s): + table = self.table.OpticalBounds + if table.Format == 0: + table.OpticalBoundsDeltas = {glyph: table.OpticalBoundsDeltas[glyph] + for glyph in s.glyphs + if glyph in table.OpticalBoundsDeltas} + return len(table.OpticalBoundsDeltas) > 0 + elif table.Format == 1: + table.OpticalBoundsPoints = {glyph: table.OpticalBoundsPoints[glyph] + for glyph in s.glyphs + if glyph in table.OpticalBoundsPoints} + return len(table.OpticalBoundsPoints) > 0 + else: + assert False, "unknown 'opbd' format %s" % table.Format + +@_add_method(ttLib.getTableClass('post')) +def prune_pre_subset(self, font, options): + if not options.glyph_names: + self.formatType = 3.0 + return True # Required table + +@_add_method(ttLib.getTableClass('post')) +def subset_glyphs(self, s): + self.extraNames = [] # This seems to do it + return True # Required table + +@_add_method(ttLib.getTableClass('prop')) +def subset_glyphs(self, s): + prop = self.table.GlyphProperties + if prop.Format == 0: + return prop.DefaultProperties != 0 + elif prop.Format == 1: + prop.Properties = {g: prop.Properties.get(g, prop.DefaultProperties) + for g in s.glyphs} + mostCommon, _cnt = Counter(prop.Properties.values()).most_common(1)[0] + prop.DefaultProperties = mostCommon + prop.Properties = {g: prop for g, prop in prop.Properties.items() + if prop != mostCommon} + if len(prop.Properties) == 0: + del prop.Properties + prop.Format = 0 + return prop.DefaultProperties != 0 + return True + else: + assert False, "unknown 'prop' format %s" % prop.Format + +@_add_method(ttLib.getTableClass('COLR')) +def closure_glyphs(self, s): + decompose = s.glyphs + while decompose: + layers = set() + for g in decompose: + for l in self.ColorLayers.get(g, []): + layers.add(l.name) + layers -= s.glyphs + s.glyphs.update(layers) + decompose = layers + +@_add_method(ttLib.getTableClass('COLR')) +def subset_glyphs(self, s): + self.ColorLayers = {g: self.ColorLayers[g] for g in s.glyphs if g in self.ColorLayers} + return bool(self.ColorLayers) + +# TODO: prune unused palettes +@_add_method(ttLib.getTableClass('CPAL')) +def prune_post_subset(self, font, options): + return True + +@_add_method(otTables.MathGlyphConstruction) +def closure_glyphs(self, glyphs): + variants = set() + for v in self.MathGlyphVariantRecord: + variants.add(v.VariantGlyph) + if self.GlyphAssembly: + for p in self.GlyphAssembly.PartRecords: + variants.add(p.glyph) + return variants + +@_add_method(otTables.MathVariants) +def closure_glyphs(self, s): + glyphs = frozenset(s.glyphs) + variants = set() + + if self.VertGlyphCoverage: + indices = self.VertGlyphCoverage.intersect(glyphs) + for i in indices: + variants.update(self.VertGlyphConstruction[i].closure_glyphs(glyphs)) + + if self.HorizGlyphCoverage: + indices = self.HorizGlyphCoverage.intersect(glyphs) + for i in indices: + variants.update(self.HorizGlyphConstruction[i].closure_glyphs(glyphs)) + + s.glyphs.update(variants) + +@_add_method(ttLib.getTableClass('MATH')) +def closure_glyphs(self, s): + self.table.MathVariants.closure_glyphs(s) + +@_add_method(otTables.MathItalicsCorrectionInfo) +def subset_glyphs(self, s): + indices = self.Coverage.subset(s.glyphs) + self.ItalicsCorrection = [self.ItalicsCorrection[i] for i in indices] + self.ItalicsCorrectionCount = len(self.ItalicsCorrection) + return bool(self.ItalicsCorrectionCount) + +@_add_method(otTables.MathTopAccentAttachment) +def subset_glyphs(self, s): + indices = self.TopAccentCoverage.subset(s.glyphs) + self.TopAccentAttachment = [self.TopAccentAttachment[i] for i in indices] + self.TopAccentAttachmentCount = len(self.TopAccentAttachment) + return bool(self.TopAccentAttachmentCount) + +@_add_method(otTables.MathKernInfo) +def subset_glyphs(self, s): + indices = self.MathKernCoverage.subset(s.glyphs) + self.MathKernInfoRecords = [self.MathKernInfoRecords[i] for i in indices] + self.MathKernCount = len(self.MathKernInfoRecords) + return bool(self.MathKernCount) + +@_add_method(otTables.MathGlyphInfo) +def subset_glyphs(self, s): + if self.MathItalicsCorrectionInfo: + self.MathItalicsCorrectionInfo.subset_glyphs(s) + if self.MathTopAccentAttachment: + self.MathTopAccentAttachment.subset_glyphs(s) + if self.MathKernInfo: + self.MathKernInfo.subset_glyphs(s) + if self.ExtendedShapeCoverage: + self.ExtendedShapeCoverage.subset(s.glyphs) + return True + +@_add_method(otTables.MathVariants) +def subset_glyphs(self, s): + if self.VertGlyphCoverage: + indices = self.VertGlyphCoverage.subset(s.glyphs) + self.VertGlyphConstruction = [self.VertGlyphConstruction[i] for i in indices] + self.VertGlyphCount = len(self.VertGlyphConstruction) + + if self.HorizGlyphCoverage: + indices = self.HorizGlyphCoverage.subset(s.glyphs) + self.HorizGlyphConstruction = [self.HorizGlyphConstruction[i] for i in indices] + self.HorizGlyphCount = len(self.HorizGlyphConstruction) + + return True + +@_add_method(ttLib.getTableClass('MATH')) +def subset_glyphs(self, s): + s.glyphs = s.glyphs_mathed + self.table.MathGlyphInfo.subset_glyphs(s) + self.table.MathVariants.subset_glyphs(s) + return True + +@_add_method(ttLib.getTableModule('glyf').Glyph) +def remapComponentsFast(self, indices): + if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0: + return # Not composite + data = array.array("B", self.data) + i = 10 + more = 1 + while more: + flags =(data[i] << 8) | data[i+1] + glyphID =(data[i+2] << 8) | data[i+3] + # Remap + glyphID = indices.index(glyphID) + data[i+2] = glyphID >> 8 + data[i+3] = glyphID & 0xFF + i += 4 + flags = int(flags) + + if flags & 0x0001: i += 4 # ARG_1_AND_2_ARE_WORDS + else: i += 2 + if flags & 0x0008: i += 2 # WE_HAVE_A_SCALE + elif flags & 0x0040: i += 4 # WE_HAVE_AN_X_AND_Y_SCALE + elif flags & 0x0080: i += 8 # WE_HAVE_A_TWO_BY_TWO + more = flags & 0x0020 # MORE_COMPONENTS + + self.data = data.tostring() + +@_add_method(ttLib.getTableClass('glyf')) +def closure_glyphs(self, s): + glyphSet = self.glyphs + decompose = s.glyphs + while decompose: + components = set() + for g in decompose: + if g not in glyphSet: + continue + gl = glyphSet[g] + for c in gl.getComponentNames(self): + components.add(c) + components -= s.glyphs + s.glyphs.update(components) + decompose = components + +@_add_method(ttLib.getTableClass('glyf')) +def prune_pre_subset(self, font, options): + if options.notdef_glyph and not options.notdef_outline: + g = self[self.glyphOrder[0]] + # Yay, easy! + g.__dict__.clear() + g.data = "" + return True + +@_add_method(ttLib.getTableClass('glyf')) +def subset_glyphs(self, s): + self.glyphs = _dict_subset(self.glyphs, s.glyphs) + indices = [i for i,g in enumerate(self.glyphOrder) if g in s.glyphs] + for v in self.glyphs.values(): + if hasattr(v, "data"): + v.remapComponentsFast(indices) + else: + pass # No need + self.glyphOrder = [g for g in self.glyphOrder if g in s.glyphs] + # Don't drop empty 'glyf' tables, otherwise 'loca' doesn't get subset. + return True + +@_add_method(ttLib.getTableClass('glyf')) +def prune_post_subset(self, font, options): + remove_hinting = not options.hinting + for v in self.glyphs.values(): + v.trim(remove_hinting=remove_hinting) + return True + + +class _ClosureGlyphsT2Decompiler(psCharStrings.SimpleT2Decompiler): + + def __init__(self, components, localSubrs, globalSubrs): + psCharStrings.SimpleT2Decompiler.__init__(self, + localSubrs, + globalSubrs) + self.components = components + + def op_endchar(self, index): + args = self.popall() + if len(args) >= 4: + from fontTools.encodings.StandardEncoding import StandardEncoding + # endchar can do seac accent bulding; The T2 spec says it's deprecated, + # but recent software that shall remain nameless does output it. + adx, ady, bchar, achar = args[-4:] + baseGlyph = StandardEncoding[bchar] + accentGlyph = StandardEncoding[achar] + self.components.add(baseGlyph) + self.components.add(accentGlyph) + +@_add_method(ttLib.getTableClass('CFF ')) +def closure_glyphs(self, s): + cff = self.cff + assert len(cff) == 1 + font = cff[cff.keys()[0]] + glyphSet = font.CharStrings + + decompose = s.glyphs + while decompose: + components = set() + for g in decompose: + if g not in glyphSet: + continue + gl = glyphSet[g] + + subrs = getattr(gl.private, "Subrs", []) + decompiler = _ClosureGlyphsT2Decompiler(components, subrs, gl.globalSubrs) + decompiler.execute(gl) + components -= s.glyphs + s.glyphs.update(components) + decompose = components + +@_add_method(ttLib.getTableClass('CFF ')) +def prune_pre_subset(self, font, options): + cff = self.cff + # CFF table must have one font only + cff.fontNames = cff.fontNames[:1] + + if options.notdef_glyph and not options.notdef_outline: + for fontname in cff.keys(): + font = cff[fontname] + c, fdSelectIndex = font.CharStrings.getItemAndSelector('.notdef') + if hasattr(font, 'FDArray') and font.FDArray is not None: + private = font.FDArray[fdSelectIndex].Private + else: + private = font.Private + dfltWdX = private.defaultWidthX + nmnlWdX = private.nominalWidthX + pen = NullPen() + c.draw(pen) # this will set the charstring's width + if c.width != dfltWdX: + c.program = [c.width - nmnlWdX, 'endchar'] + else: + c.program = ['endchar'] + + # Clear useless Encoding + for fontname in cff.keys(): + font = cff[fontname] + # https://github.com/behdad/fonttools/issues/620 + font.Encoding = "StandardEncoding" + + return True # bool(cff.fontNames) + +@_add_method(ttLib.getTableClass('CFF ')) +def subset_glyphs(self, s): + cff = self.cff + for fontname in cff.keys(): + font = cff[fontname] + cs = font.CharStrings + + # Load all glyphs + for g in font.charset: + if g not in s.glyphs: continue + c, _ = cs.getItemAndSelector(g) + + if cs.charStringsAreIndexed: + indices = [i for i,g in enumerate(font.charset) if g in s.glyphs] + csi = cs.charStringsIndex + csi.items = [csi.items[i] for i in indices] + del csi.file, csi.offsets + if hasattr(font, "FDSelect"): + sel = font.FDSelect + # XXX We want to set sel.format to None, such that the + # most compact format is selected. However, OTS was + # broken and couldn't parse a FDSelect format 0 that + # happened before CharStrings. As such, always force + # format 3 until we fix cffLib to always generate + # FDSelect after CharStrings. + # https://github.com/khaledhosny/ots/pull/31 + #sel.format = None + sel.format = 3 + sel.gidArray = [sel.gidArray[i] for i in indices] + cs.charStrings = {g:indices.index(v) + for g,v in cs.charStrings.items() + if g in s.glyphs} + else: + cs.charStrings = {g:v + for g,v in cs.charStrings.items() + if g in s.glyphs} + font.charset = [g for g in font.charset if g in s.glyphs] + font.numGlyphs = len(font.charset) + + return True # any(cff[fontname].numGlyphs for fontname in cff.keys()) + +@_add_method(psCharStrings.T2CharString) +def subset_subroutines(self, subrs, gsubrs): + p = self.program + assert len(p) + for i in range(1, len(p)): + if p[i] == 'callsubr': + assert isinstance(p[i-1], int) + p[i-1] = subrs._used.index(p[i-1] + subrs._old_bias) - subrs._new_bias + elif p[i] == 'callgsubr': + assert isinstance(p[i-1], int) + p[i-1] = gsubrs._used.index(p[i-1] + gsubrs._old_bias) - gsubrs._new_bias + +@_add_method(psCharStrings.T2CharString) +def drop_hints(self): + hints = self._hints + + if hints.deletions: + p = self.program + for idx in reversed(hints.deletions): + del p[idx-2:idx] + + if hints.has_hint: + assert not hints.deletions or hints.last_hint <= hints.deletions[0] + self.program = self.program[hints.last_hint:] + if hasattr(self, 'width'): + # Insert width back if needed + if self.width != self.private.defaultWidthX: + self.program.insert(0, self.width - self.private.nominalWidthX) + + if hints.has_hintmask: + i = 0 + p = self.program + while i < len(p): + if p[i] in ['hintmask', 'cntrmask']: + assert i + 1 <= len(p) + del p[i:i+2] + continue + i += 1 + + assert len(self.program) + + del self._hints + +class _MarkingT2Decompiler(psCharStrings.SimpleT2Decompiler): + + def __init__(self, localSubrs, globalSubrs): + psCharStrings.SimpleT2Decompiler.__init__(self, + localSubrs, + globalSubrs) + for subrs in [localSubrs, globalSubrs]: + if subrs and not hasattr(subrs, "_used"): + subrs._used = set() + + def op_callsubr(self, index): + self.localSubrs._used.add(self.operandStack[-1]+self.localBias) + psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) + + def op_callgsubr(self, index): + self.globalSubrs._used.add(self.operandStack[-1]+self.globalBias) + psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) + +class _DehintingT2Decompiler(psCharStrings.T2WidthExtractor): + + class Hints(object): + def __init__(self): + # Whether calling this charstring produces any hint stems + # Note that if a charstring starts with hintmask, it will + # have has_hint set to True, because it *might* produce an + # implicit vstem if called under certain conditions. + self.has_hint = False + # Index to start at to drop all hints + self.last_hint = 0 + # Index up to which we know more hints are possible. + # Only relevant if status is 0 or 1. + self.last_checked = 0 + # The status means: + # 0: after dropping hints, this charstring is empty + # 1: after dropping hints, there may be more hints + # continuing after this + # 2: no more hints possible after this charstring + self.status = 0 + # Has hintmask instructions; not recursive + self.has_hintmask = False + # List of indices of calls to empty subroutines to remove. + self.deletions = [] + pass + + def __init__(self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX): + self._css = css + psCharStrings.T2WidthExtractor.__init__( + self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX) + + def execute(self, charString): + old_hints = charString._hints if hasattr(charString, '_hints') else None + charString._hints = self.Hints() + + psCharStrings.T2WidthExtractor.execute(self, charString) + + hints = charString._hints + + if hints.has_hint or hints.has_hintmask: + self._css.add(charString) + + if hints.status != 2: + # Check from last_check, make sure we didn't have any operators. + for i in range(hints.last_checked, len(charString.program) - 1): + if isinstance(charString.program[i], str): + hints.status = 2 + break + else: + hints.status = 1 # There's *something* here + hints.last_checked = len(charString.program) + + if old_hints: + assert hints.__dict__ == old_hints.__dict__ + + def op_callsubr(self, index): + subr = self.localSubrs[self.operandStack[-1]+self.localBias] + psCharStrings.T2WidthExtractor.op_callsubr(self, index) + self.processSubr(index, subr) + + def op_callgsubr(self, index): + subr = self.globalSubrs[self.operandStack[-1]+self.globalBias] + psCharStrings.T2WidthExtractor.op_callgsubr(self, index) + self.processSubr(index, subr) + + def op_hstem(self, index): + psCharStrings.T2WidthExtractor.op_hstem(self, index) + self.processHint(index) + def op_vstem(self, index): + psCharStrings.T2WidthExtractor.op_vstem(self, index) + self.processHint(index) + def op_hstemhm(self, index): + psCharStrings.T2WidthExtractor.op_hstemhm(self, index) + self.processHint(index) + def op_vstemhm(self, index): + psCharStrings.T2WidthExtractor.op_vstemhm(self, index) + self.processHint(index) + def op_hintmask(self, index): + rv = psCharStrings.T2WidthExtractor.op_hintmask(self, index) + self.processHintmask(index) + return rv + def op_cntrmask(self, index): + rv = psCharStrings.T2WidthExtractor.op_cntrmask(self, index) + self.processHintmask(index) + return rv + + def processHintmask(self, index): + cs = self.callingStack[-1] + hints = cs._hints + hints.has_hintmask = True + if hints.status != 2: + # Check from last_check, see if we may be an implicit vstem + for i in range(hints.last_checked, index - 1): + if isinstance(cs.program[i], str): + hints.status = 2 + break + else: + # We are an implicit vstem + hints.has_hint = True + hints.last_hint = index + 1 + hints.status = 0 + hints.last_checked = index + 1 + + def processHint(self, index): + cs = self.callingStack[-1] + hints = cs._hints + hints.has_hint = True + hints.last_hint = index + hints.last_checked = index + + def processSubr(self, index, subr): + cs = self.callingStack[-1] + hints = cs._hints + subr_hints = subr._hints + + # Check from last_check, make sure we didn't have + # any operators. + if hints.status != 2: + for i in range(hints.last_checked, index - 1): + if isinstance(cs.program[i], str): + hints.status = 2 + break + hints.last_checked = index + + if hints.status != 2: + if subr_hints.has_hint: + hints.has_hint = True + + # Decide where to chop off from + if subr_hints.status == 0: + hints.last_hint = index + else: + hints.last_hint = index - 2 # Leave the subr call in + elif subr_hints.status == 0: + hints.deletions.append(index) + + hints.status = max(hints.status, subr_hints.status) + +class _DesubroutinizingT2Decompiler(psCharStrings.SimpleT2Decompiler): + + def __init__(self, localSubrs, globalSubrs): + psCharStrings.SimpleT2Decompiler.__init__(self, + localSubrs, + globalSubrs) + + def execute(self, charString): + # Note: Currently we recompute _desubroutinized each time. + # This is more robust in some cases, but in other places we assume + # that each subroutine always expands to the same code, so + # maybe it doesn't matter. To speed up we can just not + # recompute _desubroutinized if it's there. For now I just + # double-check that it desubroutinized to the same thing. + old_desubroutinized = charString._desubroutinized if hasattr(charString, '_desubroutinized') else None + + charString._patches = [] + psCharStrings.SimpleT2Decompiler.execute(self, charString) + desubroutinized = charString.program[:] + for idx,expansion in reversed (charString._patches): + assert idx >= 2 + assert desubroutinized[idx - 1] in ['callsubr', 'callgsubr'], desubroutinized[idx - 1] + assert type(desubroutinized[idx - 2]) == int + if expansion[-1] == 'return': + expansion = expansion[:-1] + desubroutinized[idx-2:idx] = expansion + if 'endchar' in desubroutinized: + # Cut off after first endchar + desubroutinized = desubroutinized[:desubroutinized.index('endchar') + 1] + else: + if not len(desubroutinized) or desubroutinized[-1] != 'return': + desubroutinized.append('return') + + charString._desubroutinized = desubroutinized + del charString._patches + + if old_desubroutinized: + assert desubroutinized == old_desubroutinized + + def op_callsubr(self, index): + subr = self.localSubrs[self.operandStack[-1]+self.localBias] + psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) + self.processSubr(index, subr) + + def op_callgsubr(self, index): + subr = self.globalSubrs[self.operandStack[-1]+self.globalBias] + psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) + self.processSubr(index, subr) + + def processSubr(self, index, subr): + cs = self.callingStack[-1] + cs._patches.append((index, subr._desubroutinized)) + + +@_add_method(ttLib.getTableClass('CFF ')) +def prune_post_subset(self, font, options): + cff = self.cff + for fontname in cff.keys(): + font = cff[fontname] + cs = font.CharStrings + + # Drop unused FontDictionaries + if hasattr(font, "FDSelect"): + sel = font.FDSelect + indices = _uniq_sort(sel.gidArray) + sel.gidArray = [indices.index (ss) for ss in sel.gidArray] + arr = font.FDArray + arr.items = [arr[i] for i in indices] + del arr.file, arr.offsets + + # Desubroutinize if asked for + if options.desubroutinize: + for g in font.charset: + c, _ = cs.getItemAndSelector(g) + c.decompile() + subrs = getattr(c.private, "Subrs", []) + decompiler = _DesubroutinizingT2Decompiler(subrs, c.globalSubrs) + decompiler.execute(c) + c.program = c._desubroutinized + + # Drop hints if not needed + if not options.hinting: + + # This can be tricky, but doesn't have to. What we do is: + # + # - Run all used glyph charstrings and recurse into subroutines, + # - For each charstring (including subroutines), if it has any + # of the hint stem operators, we mark it as such. + # Upon returning, for each charstring we note all the + # subroutine calls it makes that (recursively) contain a stem, + # - Dropping hinting then consists of the following two ops: + # * Drop the piece of the program in each charstring before the + # last call to a stem op or a stem-calling subroutine, + # * Drop all hintmask operations. + # - It's trickier... A hintmask right after hints and a few numbers + # will act as an implicit vstemhm. As such, we track whether + # we have seen any non-hint operators so far and do the right + # thing, recursively... Good luck understanding that :( + css = set() + for g in font.charset: + c, _ = cs.getItemAndSelector(g) + c.decompile() + subrs = getattr(c.private, "Subrs", []) + decompiler = _DehintingT2Decompiler(css, subrs, c.globalSubrs, + c.private.nominalWidthX, + c.private.defaultWidthX) + decompiler.execute(c) + c.width = decompiler.width + for charstring in css: + charstring.drop_hints() + del css + + # Drop font-wide hinting values + all_privs = [] + if hasattr(font, 'FDSelect'): + all_privs.extend(fd.Private for fd in font.FDArray) + else: + all_privs.append(font.Private) + for priv in all_privs: + for k in ['BlueValues', 'OtherBlues', + 'FamilyBlues', 'FamilyOtherBlues', + 'BlueScale', 'BlueShift', 'BlueFuzz', + 'StemSnapH', 'StemSnapV', 'StdHW', 'StdVW']: + if hasattr(priv, k): + setattr(priv, k, None) + + # Renumber subroutines to remove unused ones + + # Mark all used subroutines + for g in font.charset: + c, _ = cs.getItemAndSelector(g) + subrs = getattr(c.private, "Subrs", []) + decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs) + decompiler.execute(c) + + all_subrs = [font.GlobalSubrs] + if hasattr(font, 'FDSelect'): + all_subrs.extend(fd.Private.Subrs for fd in font.FDArray if hasattr(fd.Private, 'Subrs') and fd.Private.Subrs) + elif hasattr(font.Private, 'Subrs') and font.Private.Subrs: + all_subrs.append(font.Private.Subrs) + + subrs = set(subrs) # Remove duplicates + + # Prepare + for subrs in all_subrs: + if not hasattr(subrs, '_used'): + subrs._used = set() + subrs._used = _uniq_sort(subrs._used) + subrs._old_bias = psCharStrings.calcSubrBias(subrs) + subrs._new_bias = psCharStrings.calcSubrBias(subrs._used) + + # Renumber glyph charstrings + for g in font.charset: + c, _ = cs.getItemAndSelector(g) + subrs = getattr(c.private, "Subrs", []) + c.subset_subroutines (subrs, font.GlobalSubrs) + + # Renumber subroutines themselves + for subrs in all_subrs: + if subrs == font.GlobalSubrs: + if not hasattr(font, 'FDSelect') and hasattr(font.Private, 'Subrs'): + local_subrs = font.Private.Subrs + else: + local_subrs = [] + else: + local_subrs = subrs + + subrs.items = [subrs.items[i] for i in subrs._used] + if hasattr(subrs, 'file'): + del subrs.file + if hasattr(subrs, 'offsets'): + del subrs.offsets + + for subr in subrs.items: + subr.subset_subroutines (local_subrs, font.GlobalSubrs) + + # Delete local SubrsIndex if empty + if hasattr(font, 'FDSelect'): + for fd in font.FDArray: + _delete_empty_subrs(fd.Private) + else: + _delete_empty_subrs(font.Private) + + # Cleanup + for subrs in all_subrs: + del subrs._used, subrs._old_bias, subrs._new_bias + + return True + + +def _delete_empty_subrs(private_dict): + if hasattr(private_dict, 'Subrs') and not private_dict.Subrs: + if 'Subrs' in private_dict.rawDict: + del private_dict.rawDict['Subrs'] + del private_dict.Subrs + + +@_add_method(ttLib.getTableClass('cmap')) +def closure_glyphs(self, s): + tables = [t for t in self.tables if t.isUnicode()] + + # Close glyphs + for table in tables: + if table.format == 14: + for cmap in table.uvsDict.values(): + glyphs = {g for u,g in cmap if u in s.unicodes_requested} + if None in glyphs: + glyphs.remove(None) + s.glyphs.update(glyphs) + else: + cmap = table.cmap + intersection = s.unicodes_requested.intersection(cmap.keys()) + s.glyphs.update(cmap[u] for u in intersection) + + # Calculate unicodes_missing + s.unicodes_missing = s.unicodes_requested.copy() + for table in tables: + s.unicodes_missing.difference_update(table.cmap) + +@_add_method(ttLib.getTableClass('cmap')) +def prune_pre_subset(self, font, options): + if not options.legacy_cmap: + # Drop non-Unicode / non-Symbol cmaps + self.tables = [t for t in self.tables if t.isUnicode() or t.isSymbol()] + if not options.symbol_cmap: + self.tables = [t for t in self.tables if not t.isSymbol()] + # TODO(behdad) Only keep one subtable? + # For now, drop format=0 which can't be subset_glyphs easily? + self.tables = [t for t in self.tables if t.format != 0] + self.numSubTables = len(self.tables) + return True # Required table + +@_add_method(ttLib.getTableClass('cmap')) +def subset_glyphs(self, s): + s.glyphs = None # We use s.glyphs_requested and s.unicodes_requested only + for t in self.tables: + if t.format == 14: + # TODO(behdad) We drop all the default-UVS mappings + # for glyphs_requested. So it's the caller's responsibility to make + # sure those are included. + t.uvsDict = {v:[(u,g) for u,g in l + if g in s.glyphs_requested or u in s.unicodes_requested] + for v,l in t.uvsDict.items()} + t.uvsDict = {v:l for v,l in t.uvsDict.items() if l} + elif t.isUnicode(): + t.cmap = {u:g for u,g in t.cmap.items() + if g in s.glyphs_requested or u in s.unicodes_requested} + else: + t.cmap = {u:g for u,g in t.cmap.items() + if g in s.glyphs_requested} + self.tables = [t for t in self.tables + if (t.cmap if t.format != 14 else t.uvsDict)] + self.numSubTables = len(self.tables) + # TODO(behdad) Convert formats when needed. + # In particular, if we have a format=12 without non-BMP + # characters, either drop format=12 one or convert it + # to format=4 if there's not one. + return True # Required table + +@_add_method(ttLib.getTableClass('DSIG')) +def prune_pre_subset(self, font, options): + # Drop all signatures since they will be invalid + self.usNumSigs = 0 + self.signatureRecords = [] + return True + +@_add_method(ttLib.getTableClass('maxp')) +def prune_pre_subset(self, font, options): + if not options.hinting: + if self.tableVersion == 0x00010000: + self.maxZones = 1 + self.maxTwilightPoints = 0 + self.maxStorage = 0 + self.maxFunctionDefs = 0 + self.maxInstructionDefs = 0 + self.maxStackElements = 0 + self.maxSizeOfInstructions = 0 + return True + +@_add_method(ttLib.getTableClass('name')) +def prune_pre_subset(self, font, options): + nameIDs = set(options.name_IDs) + fvar = font.get('fvar') + if fvar: + nameIDs.update([axis.axisNameID for axis in fvar.axes]) + nameIDs.update([inst.subfamilyNameID for inst in fvar.instances]) + nameIDs.update([inst.postscriptNameID for inst in fvar.instances + if inst.postscriptNameID != 0xFFFF]) + if '*' not in options.name_IDs: + self.names = [n for n in self.names if n.nameID in nameIDs] + if not options.name_legacy: + # TODO(behdad) Sometimes (eg Apple Color Emoji) there's only a macroman + # entry for Latin and no Unicode names. + self.names = [n for n in self.names if n.isUnicode()] + # TODO(behdad) Option to keep only one platform's + if '*' not in options.name_languages: + # TODO(behdad) This is Windows-platform specific! + self.names = [n for n in self.names + if n.langID in options.name_languages] + if options.obfuscate_names: + namerecs = [] + for n in self.names: + if n.nameID in [1, 4]: + n.string = ".\x7f".encode('utf_16_be') if n.isUnicode() else ".\x7f" + elif n.nameID in [2, 6]: + n.string = "\x7f".encode('utf_16_be') if n.isUnicode() else "\x7f" + elif n.nameID == 3: + n.string = "" + elif n.nameID in [16, 17, 18]: + continue + namerecs.append(n) + self.names = namerecs + return True # Required table + + +# TODO(behdad) OS/2 ulCodePageRange? +# TODO(behdad) Drop AAT tables. +# TODO(behdad) Drop unneeded GSUB/GPOS Script/LangSys entries. +# TODO(behdad) Drop empty GSUB/GPOS, and GDEF if no GSUB/GPOS left +# TODO(behdad) Drop GDEF subitems if unused by lookups +# TODO(behdad) Avoid recursing too much (in GSUB/GPOS and in CFF) +# TODO(behdad) Text direction considerations. +# TODO(behdad) Text script / language considerations. +# TODO(behdad) Optionally drop 'kern' table if GPOS available +# TODO(behdad) Implement --unicode='*' to choose all cmap'ed +# TODO(behdad) Drop old-spec Indic scripts + + +class Options(object): + + class OptionError(Exception): pass + class UnknownOptionError(OptionError): pass + + # spaces in tag names (e.g. "SVG ", "cvt ") are stripped by the argument parser + _drop_tables_default = ['BASE', 'JSTF', 'DSIG', 'EBDT', 'EBLC', + 'EBSC', 'SVG', 'PCLT', 'LTSH'] + _drop_tables_default += ['Feat', 'Glat', 'Gloc', 'Silf', 'Sill'] # Graphite + _drop_tables_default += ['sbix'] # Color + _no_subset_tables_default = ['avar', 'fvar', + 'gasp', 'head', 'hhea', 'maxp', + 'vhea', 'OS/2', 'loca', 'name', 'cvt', + 'fpgm', 'prep', 'VDMX', 'DSIG', 'CPAL', + 'MVAR', 'cvar', 'STAT'] + _hinting_tables_default = ['cvt', 'cvar', 'fpgm', 'prep', 'hdmx', 'VDMX'] + + # Based on HarfBuzz shapers + _layout_features_groups = { + # Default shaper + 'common': ['rvrn', 'ccmp', 'liga', 'locl', 'mark', 'mkmk', 'rlig'], + 'fractions': ['frac', 'numr', 'dnom'], + 'horizontal': ['calt', 'clig', 'curs', 'kern', 'rclt'], + 'vertical': ['valt', 'vert', 'vkrn', 'vpal', 'vrt2'], + 'ltr': ['ltra', 'ltrm'], + 'rtl': ['rtla', 'rtlm'], + # Complex shapers + 'arabic': ['init', 'medi', 'fina', 'isol', 'med2', 'fin2', 'fin3', + 'cswh', 'mset', 'stch'], + 'hangul': ['ljmo', 'vjmo', 'tjmo'], + 'tibetan': ['abvs', 'blws', 'abvm', 'blwm'], + 'indic': ['nukt', 'akhn', 'rphf', 'rkrf', 'pref', 'blwf', 'half', + 'abvf', 'pstf', 'cfar', 'vatu', 'cjct', 'init', 'pres', + 'abvs', 'blws', 'psts', 'haln', 'dist', 'abvm', 'blwm'], + } + _layout_features_default = _uniq_sort(sum( + iter(_layout_features_groups.values()), [])) + + def __init__(self, **kwargs): + + self.drop_tables = self._drop_tables_default[:] + self.no_subset_tables = self._no_subset_tables_default[:] + self.passthrough_tables = False # keep/drop tables we can't subset + self.hinting_tables = self._hinting_tables_default[:] + self.legacy_kern = False # drop 'kern' table if GPOS available + self.layout_features = self._layout_features_default[:] + self.ignore_missing_glyphs = False + self.ignore_missing_unicodes = True + self.hinting = True + self.glyph_names = False + self.legacy_cmap = False + self.symbol_cmap = False + self.name_IDs = [0, 1, 2, 3, 4, 5, 6] # https://github.com/fonttools/fonttools/issues/1170#issuecomment-364631225 + self.name_legacy = False + self.name_languages = [0x0409] # English + self.obfuscate_names = False # to make webfont unusable as a system font + self.notdef_glyph = True # gid0 for TrueType / .notdef for CFF + self.notdef_outline = False # No need for notdef to have an outline really + self.recommended_glyphs = False # gid1, gid2, gid3 for TrueType + self.recalc_bounds = False # Recalculate font bounding boxes + self.recalc_timestamp = False # Recalculate font modified timestamp + self.prune_unicode_ranges = True # Clear unused 'ulUnicodeRange' bits + self.recalc_average_width = False # update 'xAvgCharWidth' + self.canonical_order = None # Order tables as recommended + self.flavor = None # May be 'woff' or 'woff2' + self.with_zopfli = False # use zopfli instead of zlib for WOFF 1.0 + self.desubroutinize = False # Desubroutinize CFF CharStrings + self.verbose = False + self.timing = False + self.xml = False + self.font_number = -1 + + self.set(**kwargs) + + def set(self, **kwargs): + for k,v in kwargs.items(): + if not hasattr(self, k): + raise self.UnknownOptionError("Unknown option '%s'" % k) + setattr(self, k, v) + + def parse_opts(self, argv, ignore_unknown=[]): + posargs = [] + passthru_options = [] + for a in argv: + orig_a = a + if not a.startswith('--'): + posargs.append(a) + continue + a = a[2:] + i = a.find('=') + op = '=' + if i == -1: + if a.startswith("no-"): + k = a[3:] + if k == "canonical-order": + # reorderTables=None is faster than False (the latter + # still reorders to "keep" the original table order) + v = None + else: + v = False + else: + k = a + v = True + if k.endswith("?"): + k = k[:-1] + v = '?' + else: + k = a[:i] + if k[-1] in "-+": + op = k[-1]+'=' # Op is '-=' or '+=' now. + k = k[:-1] + v = a[i+1:] + ok = k + k = k.replace('-', '_') + if not hasattr(self, k): + if ignore_unknown is True or ok in ignore_unknown: + passthru_options.append(orig_a) + continue + else: + raise self.UnknownOptionError("Unknown option '%s'" % a) + + ov = getattr(self, k) + if v == '?': + print("Current setting for '%s' is: %s" % (ok, ov)) + continue + if isinstance(ov, bool): + v = bool(v) + elif isinstance(ov, int): + v = int(v) + elif isinstance(ov, str): + v = str(v) # redundant + elif isinstance(ov, list): + if isinstance(v, bool): + raise self.OptionError("Option '%s' requires values to be specified using '='" % a) + vv = v.replace(',', ' ').split() + if vv == ['']: + vv = [] + vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv] + if op == '=': + v = vv + elif op == '+=': + v = ov + v.extend(vv) + elif op == '-=': + v = ov + for x in vv: + if x in v: + v.remove(x) + else: + assert False + + setattr(self, k, v) + + return posargs + passthru_options + + +class Subsetter(object): + + class SubsettingError(Exception): pass + class MissingGlyphsSubsettingError(SubsettingError): pass + class MissingUnicodesSubsettingError(SubsettingError): pass + + def __init__(self, options=None): + + if not options: + options = Options() + + self.options = options + self.unicodes_requested = set() + self.glyph_names_requested = set() + self.glyph_ids_requested = set() + + def populate(self, glyphs=[], gids=[], unicodes=[], text=""): + self.unicodes_requested.update(unicodes) + if isinstance(text, bytes): + text = text.decode("utf_8") + text_utf32 = text.encode("utf-32-be") + nchars = len(text_utf32)//4 + for u in struct.unpack('>%dL' % nchars, text_utf32): + self.unicodes_requested.add(u) + self.glyph_names_requested.update(glyphs) + self.glyph_ids_requested.update(gids) + + def _prune_pre_subset(self, font): + for tag in self._sort_tables(font): + if (tag.strip() in self.options.drop_tables or + (tag.strip() in self.options.hinting_tables and not self.options.hinting) or + (tag == 'kern' and (not self.options.legacy_kern and 'GPOS' in font))): + log.info("%s dropped", tag) + del font[tag] + continue + + clazz = ttLib.getTableClass(tag) + + if hasattr(clazz, 'prune_pre_subset'): + with timer("load '%s'" % tag): + table = font[tag] + with timer("prune '%s'" % tag): + retain = table.prune_pre_subset(font, self.options) + if not retain: + log.info("%s pruned to empty; dropped", tag) + del font[tag] + continue + else: + log.info("%s pruned", tag) + + def _closure_glyphs(self, font): + + realGlyphs = set(font.getGlyphOrder()) + glyph_order = font.getGlyphOrder() + + self.glyphs_requested = set() + self.glyphs_requested.update(self.glyph_names_requested) + self.glyphs_requested.update(glyph_order[i] + for i in self.glyph_ids_requested + if i < len(glyph_order)) + + self.glyphs_missing = set() + self.glyphs_missing.update(self.glyphs_requested.difference(realGlyphs)) + self.glyphs_missing.update(i for i in self.glyph_ids_requested + if i >= len(glyph_order)) + if self.glyphs_missing: + log.info("Missing requested glyphs: %s", self.glyphs_missing) + if not self.options.ignore_missing_glyphs: + raise self.MissingGlyphsSubsettingError(self.glyphs_missing) + + self.glyphs = self.glyphs_requested.copy() + + self.unicodes_missing = set() + if 'cmap' in font: + with timer("close glyph list over 'cmap'"): + font['cmap'].closure_glyphs(self) + self.glyphs.intersection_update(realGlyphs) + self.glyphs_cmaped = frozenset(self.glyphs) + if self.unicodes_missing: + missing = ["U+%04X" % u for u in self.unicodes_missing] + log.info("Missing glyphs for requested Unicodes: %s", missing) + if not self.options.ignore_missing_unicodes: + raise self.MissingUnicodesSubsettingError(missing) + del missing + + if self.options.notdef_glyph: + if 'glyf' in font: + self.glyphs.add(font.getGlyphName(0)) + log.info("Added gid0 to subset") + else: + self.glyphs.add('.notdef') + log.info("Added .notdef to subset") + if self.options.recommended_glyphs: + if 'glyf' in font: + for i in range(min(4, len(font.getGlyphOrder()))): + self.glyphs.add(font.getGlyphName(i)) + log.info("Added first four glyphs to subset") + + if 'GSUB' in font: + with timer("close glyph list over 'GSUB'"): + log.info("Closing glyph list over 'GSUB': %d glyphs before", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + font['GSUB'].closure_glyphs(self) + self.glyphs.intersection_update(realGlyphs) + log.info("Closed glyph list over 'GSUB': %d glyphs after", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + self.glyphs_gsubed = frozenset(self.glyphs) + + if 'MATH' in font: + with timer("close glyph list over 'MATH'"): + log.info("Closing glyph list over 'MATH': %d glyphs before", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + font['MATH'].closure_glyphs(self) + self.glyphs.intersection_update(realGlyphs) + log.info("Closed glyph list over 'MATH': %d glyphs after", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + self.glyphs_mathed = frozenset(self.glyphs) + + for table in ('COLR', 'bsln'): + if table in font: + with timer("close glyph list over '%s'" % table): + log.info("Closing glyph list over '%s': %d glyphs before", + table, len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + font[table].closure_glyphs(self) + self.glyphs.intersection_update(realGlyphs) + log.info("Closed glyph list over '%s': %d glyphs after", + table, len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + + if 'glyf' in font: + with timer("close glyph list over 'glyf'"): + log.info("Closing glyph list over 'glyf': %d glyphs before", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + font['glyf'].closure_glyphs(self) + self.glyphs.intersection_update(realGlyphs) + log.info("Closed glyph list over 'glyf': %d glyphs after", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + self.glyphs_glyfed = frozenset(self.glyphs) + + if 'CFF ' in font: + with timer("close glyph list over 'CFF '"): + log.info("Closing glyph list over 'CFF ': %d glyphs before", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + font['CFF '].closure_glyphs(self) + self.glyphs.intersection_update(realGlyphs) + log.info("Closed glyph list over 'CFF ': %d glyphs after", + len(self.glyphs)) + log.glyphs(self.glyphs, font=font) + self.glyphs_cffed = frozenset(self.glyphs) + + self.glyphs_all = frozenset(self.glyphs) + + order = font.getReverseGlyphMap() + self.reverseOrigGlyphMap = {g:order[g] for g in self.glyphs_all} + + log.info("Retaining %d glyphs", len(self.glyphs_all)) + + del self.glyphs + + def _subset_glyphs(self, font): + for tag in self._sort_tables(font): + clazz = ttLib.getTableClass(tag) + + if tag.strip() in self.options.no_subset_tables: + log.info("%s subsetting not needed", tag) + elif hasattr(clazz, 'subset_glyphs'): + with timer("subset '%s'" % tag): + table = font[tag] + self.glyphs = self.glyphs_all + retain = table.subset_glyphs(self) + del self.glyphs + if not retain: + log.info("%s subsetted to empty; dropped", tag) + del font[tag] + else: + log.info("%s subsetted", tag) + elif self.options.passthrough_tables: + log.info("%s NOT subset; don't know how to subset", tag) + else: + log.warning("%s NOT subset; don't know how to subset; dropped", tag) + del font[tag] + + with timer("subset GlyphOrder"): + glyphOrder = font.getGlyphOrder() + glyphOrder = [g for g in glyphOrder if g in self.glyphs_all] + font.setGlyphOrder(glyphOrder) + font._buildReverseGlyphOrderDict() + + def _prune_post_subset(self, font): + for tag in font.keys(): + if tag == 'GlyphOrder': continue + if tag == 'OS/2' and self.options.prune_unicode_ranges: + old_uniranges = font[tag].getUnicodeRanges() + new_uniranges = font[tag].recalcUnicodeRanges(font, pruneOnly=True) + if old_uniranges != new_uniranges: + log.info("%s Unicode ranges pruned: %s", tag, sorted(new_uniranges)) + if self.options.recalc_average_width: + widths = [m[0] for m in font["hmtx"].metrics.values() if m[0] > 0] + avg_width = otRound(sum(widths) / len(widths)) + if avg_width != font[tag].xAvgCharWidth: + font[tag].xAvgCharWidth = avg_width + log.info("%s xAvgCharWidth updated: %d", tag, avg_width) + clazz = ttLib.getTableClass(tag) + if hasattr(clazz, 'prune_post_subset'): + with timer("prune '%s'" % tag): + table = font[tag] + retain = table.prune_post_subset(font, self.options) + if not retain: + log.info("%s pruned to empty; dropped", tag) + del font[tag] + else: + log.info("%s pruned", tag) + + def _sort_tables(self, font): + tagOrder = ['fvar', 'avar', 'gvar', 'name', 'glyf'] + tagOrder = {t: i + 1 for i, t in enumerate(tagOrder)} + tags = sorted(font.keys(), key=lambda tag: tagOrder.get(tag, 0)) + return [t for t in tags if t != 'GlyphOrder'] + + def subset(self, font): + self._prune_pre_subset(font) + self._closure_glyphs(font) + self._subset_glyphs(font) + self._prune_post_subset(font) + + +@timer("load font") +def load_font(fontFile, + options, + allowVID=False, + checkChecksums=False, + dontLoadGlyphNames=False, + lazy=True): + + font = ttLib.TTFont(fontFile, + allowVID=allowVID, + checkChecksums=checkChecksums, + recalcBBoxes=options.recalc_bounds, + recalcTimestamp=options.recalc_timestamp, + lazy=lazy, + fontNumber=options.font_number) + + # Hack: + # + # If we don't need glyph names, change 'post' class to not try to + # load them. It avoid lots of headache with broken fonts as well + # as loading time. + # + # Ideally ttLib should provide a way to ask it to skip loading + # glyph names. But it currently doesn't provide such a thing. + # + if dontLoadGlyphNames: + post = ttLib.getTableClass('post') + saved = post.decode_format_2_0 + post.decode_format_2_0 = post.decode_format_3_0 + f = font['post'] + if f.formatType == 2.0: + f.formatType = 3.0 + post.decode_format_2_0 = saved + + return font + +@timer("compile and save font") +def save_font(font, outfile, options): + if options.flavor and not hasattr(font, 'flavor'): + raise Exception("fonttools version does not support flavors.") + if options.with_zopfli and options.flavor == "woff": + from fontTools.ttLib import sfnt + sfnt.USE_ZOPFLI = True + font.flavor = options.flavor + font.save(outfile, reorderTables=options.canonical_order) + +def parse_unicodes(s): + import re + s = re.sub (r"0[xX]", " ", s) + s = re.sub (r"[<+>,;&#\\xXuU\n ]", " ", s) + l = [] + for item in s.split(): + fields = item.split('-') + if len(fields) == 1: + l.append(int(item, 16)) + else: + start,end = fields + l.extend(range(int(start, 16), int(end, 16)+1)) + return l + +def parse_gids(s): + l = [] + for item in s.replace(',', ' ').split(): + fields = item.split('-') + if len(fields) == 1: + l.append(int(fields[0])) + else: + l.extend(range(int(fields[0]), int(fields[1])+1)) + return l + +def parse_glyphs(s): + return s.replace(',', ' ').split() + +def usage(): + print("usage:", __usage__, file=sys.stderr) + print("Try pyftsubset --help for more information.\n", file=sys.stderr) + +@timer("make one with everything (TOTAL TIME)") +def main(args=None): + from os.path import splitext + from fontTools import configLogger + + if args is None: + args = sys.argv[1:] + + if '--help' in args: + print(__doc__) + return 0 + + options = Options() + try: + args = options.parse_opts(args, + ignore_unknown=['gids', 'gids-file', + 'glyphs', 'glyphs-file', + 'text', 'text-file', + 'unicodes', 'unicodes-file', + 'output-file']) + except options.OptionError as e: + usage() + print("ERROR:", e, file=sys.stderr) + return 2 + + if len(args) < 2: + usage() + return 1 + + configLogger(level=logging.INFO if options.verbose else logging.WARNING) + if options.timing: + timer.logger.setLevel(logging.DEBUG) + else: + timer.logger.disabled = True + + fontfile = args[0] + args = args[1:] + + subsetter = Subsetter(options=options) + basename, extension = splitext(fontfile) + outfile = basename + '.subset' + extension + glyphs = [] + gids = [] + unicodes = [] + wildcard_glyphs = False + wildcard_unicodes = False + text = "" + for g in args: + if g == '*': + wildcard_glyphs = True + continue + if g.startswith('--output-file='): + outfile = g[14:] + continue + if g.startswith('--text='): + text += g[7:] + continue + if g.startswith('--text-file='): + text += open(g[12:], encoding='utf-8').read().replace('\n', '') + continue + if g.startswith('--unicodes='): + if g[11:] == '*': + wildcard_unicodes = True + else: + unicodes.extend(parse_unicodes(g[11:])) + continue + if g.startswith('--unicodes-file='): + for line in open(g[16:]).readlines(): + unicodes.extend(parse_unicodes(line.split('#')[0])) + continue + if g.startswith('--gids='): + gids.extend(parse_gids(g[7:])) + continue + if g.startswith('--gids-file='): + for line in open(g[12:]).readlines(): + gids.extend(parse_gids(line.split('#')[0])) + continue + if g.startswith('--glyphs='): + if g[9:] == '*': + wildcard_glyphs = True + else: + glyphs.extend(parse_glyphs(g[9:])) + continue + if g.startswith('--glyphs-file='): + for line in open(g[14:]).readlines(): + glyphs.extend(parse_glyphs(line.split('#')[0])) + continue + glyphs.append(g) + + dontLoadGlyphNames = not options.glyph_names and not glyphs + font = load_font(fontfile, options, dontLoadGlyphNames=dontLoadGlyphNames) + + with timer("compile glyph list"): + if wildcard_glyphs: + glyphs.extend(font.getGlyphOrder()) + if wildcard_unicodes: + for t in font['cmap'].tables: + if t.isUnicode(): + unicodes.extend(t.cmap.keys()) + assert '' not in glyphs + + log.info("Text: '%s'" % text) + log.info("Unicodes: %s", unicodes) + log.info("Glyphs: %s", glyphs) + log.info("Gids: %s", gids) + + subsetter.populate(glyphs=glyphs, gids=gids, unicodes=unicodes, text=text) + subsetter.subset(font) + + save_font(font, outfile, options) + + if options.verbose: + import os + log.info("Input font:% 7d bytes: %s" % (os.path.getsize(fontfile), fontfile)) + log.info("Subset font:% 7d bytes: %s" % (os.path.getsize(outfile), outfile)) + + if options.xml: + font.saveXML(sys.stdout) + + font.close() + + +__all__ = [ + 'Options', + 'Subsetter', + 'load_font', + 'save_font', + 'parse_gids', + 'parse_glyphs', + 'parse_unicodes', + 'main' +] + +if __name__ == '__main__': + sys.exit(main()) diff --git a/Lib/fontTools/subset/__main__.py b/Lib/fontTools/subset/__main__.py new file mode 100644 index 0000000..10e470f --- /dev/null +++ b/Lib/fontTools/subset/__main__.py @@ -0,0 +1,7 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +import sys +from fontTools.subset import main + +if __name__ == '__main__': + sys.exit(main()) diff --git a/Lib/fontTools/svgLib/__init__.py b/Lib/fontTools/svgLib/__init__.py new file mode 100644 index 0000000..b301a3b --- /dev/null +++ b/Lib/fontTools/svgLib/__init__.py @@ -0,0 +1,6 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * + +from .path import SVGPath, parse_path + +__all__ = ["SVGPath", "parse_path"] diff --git a/Lib/fontTools/svgLib/path/__init__.py b/Lib/fontTools/svgLib/path/__init__.py new file mode 100644 index 0000000..4f17e76 --- /dev/null +++ b/Lib/fontTools/svgLib/path/__init__.py @@ -0,0 +1,58 @@ +from __future__ import ( + print_function, division, absolute_import, unicode_literals) +from fontTools.misc.py23 import * + +from fontTools.pens.transformPen import TransformPen +from .parser import parse_path + +try: + from xml.etree import cElementTree as ElementTree # python 2 +except ImportError: # pragma nocover + from xml.etree import ElementTree # python 3 + + +__all__ = [tostr(s) for s in ("SVGPath", "parse_path")] + + +class SVGPath(object): + """ Parse SVG ``path`` elements from a file or string, and draw them + onto a glyph object that supports the FontTools Pen protocol. + + For example, reading from an SVG file and drawing to a Defcon Glyph: + + import defcon + glyph = defcon.Glyph() + pen = glyph.getPen() + svg = SVGPath("path/to/a.svg") + svg.draw(pen) + + Or reading from a string containing SVG data, using the alternative + 'fromstring' (a class method): + + data = ' elements) + and call a 'pen' object's moveTo, lineTo, curveTo, qCurveTo and closePath + methods. + + If 'current_pos' (2-float tuple) is provided, the initial moveTo will + be relative to that instead being absolute. + + Arc segments (commands "A" or "a") are not currently supported, and raise + NotImplementedError. + """ + # In the SVG specs, initial movetos are absolute, even if + # specified as 'm'. This is the default behavior here as well. + # But if you pass in a current_pos variable, the initial moveto + # will be relative to that current_pos. This is useful. + current_pos = complex(*current_pos) + + elements = list(_tokenize_path(pathdef)) + # Reverse for easy use of .pop() + elements.reverse() + + start_pos = None + command = None + last_control = None + + while elements: + + if elements[-1] in COMMANDS: + # New command. + last_command = command # Used by S and T + command = elements.pop() + absolute = command in UPPERCASE + command = command.upper() + else: + # If this element starts with numbers, it is an implicit command + # and we don't change the command. Check that it's allowed: + if command is None: + raise ValueError("Unallowed implicit command in %s, position %s" % ( + pathdef, len(pathdef.split()) - len(elements))) + last_command = command # Used by S and T + + if command == 'M': + # Moveto command. + x = elements.pop() + y = elements.pop() + pos = float(x) + float(y) * 1j + if absolute: + current_pos = pos + else: + current_pos += pos + + # M is not preceded by Z; it's an open subpath + if start_pos is not None: + pen.endPath() + + pen.moveTo((current_pos.real, current_pos.imag)) + + # when M is called, reset start_pos + # This behavior of Z is defined in svg spec: + # http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand + start_pos = current_pos + + # Implicit moveto commands are treated as lineto commands. + # So we set command to lineto here, in case there are + # further implicit commands after this moveto. + command = 'L' + + elif command == 'Z': + # Close path + if current_pos != start_pos: + pen.lineTo((start_pos.real, start_pos.imag)) + pen.closePath() + current_pos = start_pos + start_pos = None + command = None # You can't have implicit commands after closing. + + elif command == 'L': + x = elements.pop() + y = elements.pop() + pos = float(x) + float(y) * 1j + if not absolute: + pos += current_pos + pen.lineTo((pos.real, pos.imag)) + current_pos = pos + + elif command == 'H': + x = elements.pop() + pos = float(x) + current_pos.imag * 1j + if not absolute: + pos += current_pos.real + pen.lineTo((pos.real, pos.imag)) + current_pos = pos + + elif command == 'V': + y = elements.pop() + pos = current_pos.real + float(y) * 1j + if not absolute: + pos += current_pos.imag * 1j + pen.lineTo((pos.real, pos.imag)) + current_pos = pos + + elif command == 'C': + control1 = float(elements.pop()) + float(elements.pop()) * 1j + control2 = float(elements.pop()) + float(elements.pop()) * 1j + end = float(elements.pop()) + float(elements.pop()) * 1j + + if not absolute: + control1 += current_pos + control2 += current_pos + end += current_pos + + pen.curveTo((control1.real, control1.imag), + (control2.real, control2.imag), + (end.real, end.imag)) + current_pos = end + last_control = control2 + + elif command == 'S': + # Smooth curve. First control point is the "reflection" of + # the second control point in the previous path. + + if last_command not in 'CS': + # If there is no previous command or if the previous command + # was not an C, c, S or s, assume the first control point is + # coincident with the current point. + control1 = current_pos + else: + # The first control point is assumed to be the reflection of + # the second control point on the previous command relative + # to the current point. + control1 = current_pos + current_pos - last_control + + control2 = float(elements.pop()) + float(elements.pop()) * 1j + end = float(elements.pop()) + float(elements.pop()) * 1j + + if not absolute: + control2 += current_pos + end += current_pos + + pen.curveTo((control1.real, control1.imag), + (control2.real, control2.imag), + (end.real, end.imag)) + current_pos = end + last_control = control2 + + elif command == 'Q': + control = float(elements.pop()) + float(elements.pop()) * 1j + end = float(elements.pop()) + float(elements.pop()) * 1j + + if not absolute: + control += current_pos + end += current_pos + + pen.qCurveTo((control.real, control.imag), (end.real, end.imag)) + current_pos = end + last_control = control + + elif command == 'T': + # Smooth curve. Control point is the "reflection" of + # the second control point in the previous path. + + if last_command not in 'QT': + # If there is no previous command or if the previous command + # was not an Q, q, T or t, assume the first control point is + # coincident with the current point. + control = current_pos + else: + # The control point is assumed to be the reflection of + # the control point on the previous command relative + # to the current point. + control = current_pos + current_pos - last_control + + end = float(elements.pop()) + float(elements.pop()) * 1j + + if not absolute: + end += current_pos + + pen.qCurveTo((control.real, control.imag), (end.real, end.imag)) + current_pos = end + last_control = control + + elif command == 'A': + raise NotImplementedError('arcs are not supported') + + # no final Z command, it's an open path + if start_pos is not None: + pen.endPath() diff --git a/Lib/fontTools/t1Lib.py b/Lib/fontTools/t1Lib/__init__.py similarity index 87% rename from Lib/fontTools/t1Lib.py rename to Lib/fontTools/t1Lib/__init__.py index 14cc904..5da9cea 100644 --- a/Lib/fontTools/t1Lib.py +++ b/Lib/fontTools/t1Lib/__init__.py @@ -1,19 +1,19 @@ -"""fontTools.t1Lib.py -- Tools for PostScript Type 1 fonts +"""fontTools.t1Lib.py -- Tools for PostScript Type 1 fonts (Python2 only) Functions for reading and writing raw Type 1 data: read(path) - reads any Type 1 font file, returns the raw data and a type indicator: - 'LWFN', 'PFB' or 'OTHER', depending on the format of the file pointed - to by 'path'. + reads any Type 1 font file, returns the raw data and a type indicator: + 'LWFN', 'PFB' or 'OTHER', depending on the format of the file pointed + to by 'path'. Raises an error when the file does not contain valid Type 1 data. write(path, data, kind='OTHER', dohex=False) - writes raw Type 1 data to the file pointed to by 'path'. + writes raw Type 1 data to the file pointed to by 'path'. 'kind' can be one of 'LWFN', 'PFB' or 'OTHER'; it defaults to 'OTHER'. 'dohex' is a flag which determines whether the eexec encrypted part should be written as hexadecimal or binary, but only if kind - is 'LWFN' or 'PFB'. + is 'OTHER'. """ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * @@ -36,54 +36,60 @@ except ImportError: haveMacSupport = 0 else: haveMacSupport = 1 - import MacOS - + class T1Error(Exception): pass class T1Font(object): - + """Type 1 font class. - + Uses a minimal interpeter that supports just about enough PS to parse Type 1 fonts. """ - - def __init__(self, path=None): - if path is not None: - self.data, type = read(path) + + def __init__(self, path, encoding="ascii", kind=None): + if kind is None: + self.data, _ = read(path) + elif kind == "LWFN": + self.data = readLWFN(path) + elif kind == "PFB": + self.data = readPFB(path) + elif kind == "OTHER": + self.data = readOther(path) else: - pass # XXX - - def saveAs(self, path, type): - write(path, self.getData(), type) - + raise ValueError(kind) + self.encoding = encoding + + def saveAs(self, path, type, dohex=False): + write(path, self.getData(), type, dohex) + def getData(self): # XXX Todo: if the data has been converted to Python object, # recreate the PS stream return self.data - + def getGlyphSet(self): """Return a generic GlyphSet, which is a dict-like object mapping glyph names to glyph objects. The returned glyph objects have a .draw() method that supports the Pen protocol, and will have an attribute named 'width', but only *after* the .draw() method has been called. - + In the case of Type 1, the GlyphSet is simply the CharStrings dict. """ return self["CharStrings"] - + def __getitem__(self, key): if not hasattr(self, "font"): self.parse() return self.font[key] - + def parse(self): from fontTools.misc import psLib from fontTools.misc import psCharStrings - self.font = psLib.suckfont(self.data) + self.font = psLib.suckfont(self.data, self.encoding) charStrings = self.font["CharStrings"] lenIV = self.font["Private"].get("lenIV", 4) assert lenIV >= 0 @@ -135,7 +141,7 @@ def write(path, data, kind='OTHER', dohex=False): pass -# -- internal -- +# -- internal -- LWFNCHUNKSIZE = 2000 HEXLINELENGTH = 80 @@ -143,13 +149,11 @@ HEXLINELENGTH = 80 def readLWFN(path, onlyHeader=False): """reads an LWFN font file, returns raw data""" - resRef = Res.FSOpenResFile(path, 1) # read-only + from fontTools.misc.macRes import ResourceReader + reader = ResourceReader(path) try: - Res.UseResFile(resRef) - n = Res.Count1Resources('POST') data = [] - for i in range(501, 501 + n): - res = Res.Get1Resource('POST', i) + for res in reader.get('POST', []): code = byteord(res.data[0]) if byteord(res.data[1]) != 0: raise T1Error('corrupt LWFN file') @@ -168,7 +172,7 @@ def readLWFN(path, onlyHeader=False): else: raise T1Error('bad chunk code: ' + repr(code)) finally: - Res.CloseResFile(resRef) + reader.close() data = bytesjoin(data) assertType1(data) return data @@ -203,7 +207,7 @@ def readOther(path): data = f.read() f.close() assertType1(data) - + chunks = findEncryptedChunks(data) data = [] for isEncrypted, chunk in chunks: @@ -216,6 +220,7 @@ def readOther(path): # file writing tools def writeLWFN(path, data): + # Res.FSpCreateResFile was deprecated in OS X 10.5 Res.FSpCreateResFile(path, "just", "LWFN", 0) resRef = Res.FSOpenResFile(path, 2) # write-only try: @@ -266,7 +271,7 @@ def writeOther(path, data, dohex=False): if code == 2 and dohex: while chunk: f.write(eexec.hexString(chunk[:hexlinelen])) - f.write('\r') + f.write(b'\r') chunk = chunk[hexlinelen:] else: f.write(chunk) @@ -276,13 +281,13 @@ def writeOther(path, data, dohex=False): # decryption tools -EEXECBEGIN = "currentfile eexec" -EEXECEND = '0' * 64 -EEXECINTERNALEND = "currentfile closefile" -EEXECBEGINMARKER = "%-- eexec start\r" -EEXECENDMARKER = "%-- eexec end\r" +EEXECBEGIN = b"currentfile eexec" +EEXECEND = b'0' * 64 +EEXECINTERNALEND = b"currentfile closefile" +EEXECBEGINMARKER = b"%-- eexec start\r" +EEXECENDMARKER = b"%-- eexec end\r" -_ishexRE = re.compile('[0-9A-Fa-f]*$') +_ishexRE = re.compile(b'[0-9A-Fa-f]*$') def isHex(text): return _ishexRE.match(text) is not None @@ -300,7 +305,7 @@ def decryptType1(data): if decrypted[-len(EEXECINTERNALEND)-1:-1] != EEXECINTERNALEND \ and decrypted[-len(EEXECINTERNALEND)-2:-2] != EEXECINTERNALEND: raise T1Error("invalid end of eexec part") - decrypted = decrypted[:-len(EEXECINTERNALEND)-2] + '\r' + decrypted = decrypted[:-len(EEXECINTERNALEND)-2] + b'\r' data.append(EEXECBEGINMARKER + decrypted + EEXECENDMARKER) else: if chunk[-len(EEXECBEGIN)-1:-1] == EEXECBEGIN: @@ -333,7 +338,7 @@ def findEncryptedChunks(data): return chunks def deHexString(hexstring): - return eexec.deHexString(strjoin(hexstring.split())) + return eexec.deHexString(bytesjoin(hexstring.split())) # Type 1 assertion @@ -357,7 +362,7 @@ def assertType1(data): # pfb helpers def longToString(long): - s = "" + s = b"" for i in range(4): s += bytechr((long & (0xff << (i * 8))) >> i * 8) return s @@ -369,4 +374,3 @@ def stringToLong(s): for i in range(4): l += byteord(s[i]) << (i * 8) return l - diff --git a/Lib/fontTools/ttLib/__init__.py b/Lib/fontTools/ttLib/__init__.py index 546b327..a3ab303 100644 --- a/Lib/fontTools/ttLib/__init__.py +++ b/Lib/fontTools/ttLib/__init__.py @@ -1,6 +1,6 @@ """fontTools.ttLib -- a package for dealing with TrueType fonts. -This package offers translators to convert TrueType fonts to Python +This package offers translators to convert TrueType fonts to Python objects and vice versa, and additionally from Python to TTX (an XML-based text format) and vice versa. @@ -8,15 +8,15 @@ Example interactive session: Python 1.5.2c1 (#43, Mar 9 1999, 13:06:43) [CW PPC w/GUSI w/MSL] Copyright 1991-1995 Stichting Mathematisch Centrum, Amsterdam ->>> from fontTools import ttLib ->>> tt = ttLib.TTFont("afont.ttf") ->>> tt['maxp'].numGlyphs +>> from fontTools import ttLib +>> tt = ttLib.TTFont("afont.ttf") +>> tt['maxp'].numGlyphs 242 ->>> tt['OS/2'].achVendID +>> tt['OS/2'].achVendID 'B&H\000' ->>> tt['head'].unitsPerEm +>> tt['head'].unitsPerEm 2048 ->>> tt.saveXML("afont.ttx") +>> tt.saveXML("afont.ttx") Dumping 'LTSH' table... Dumping 'OS/2' table... Dumping 'VDMX' table... @@ -33,958 +33,28 @@ Dumping 'maxp' table... Dumping 'name' table... Dumping 'post' table... Dumping 'prep' table... ->>> tt2 = ttLib.TTFont() ->>> tt2.importXML("afont.ttx") ->>> tt2['maxp'].numGlyphs +>> tt2 = ttLib.TTFont() +>> tt2.importXML("afont.ttx") +>> tt2['maxp'].numGlyphs 242 ->>> +>> """ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * -import os -import sys +from fontTools.misc.loggingTools import deprecateFunction +import logging -haveMacSupport = 0 -if sys.platform == "mac": - haveMacSupport = 1 -elif sys.platform == "darwin" and sys.version_info[:3] != (2, 2, 0): - # Python 2.2's Mac support is broken, so don't enable it there. - haveMacSupport = 1 +log = logging.getLogger(__name__) class TTLibError(Exception): pass - -class TTFont(object): - - """The main font object. It manages file input and output, and offers - a convenient way of accessing tables. - Tables will be only decompiled when necessary, ie. when they're actually - accessed. This means that simple operations can be extremely fast. - """ - - def __init__(self, file=None, res_name_or_index=None, - sfntVersion="\000\001\000\000", flavor=None, checkChecksums=False, - verbose=False, recalcBBoxes=True, allowVID=False, ignoreDecompileErrors=False, - recalcTimestamp=True, fontNumber=-1, lazy=False, quiet=False): - - """The constructor can be called with a few different arguments. - When reading a font from disk, 'file' should be either a pathname - pointing to a file, or a readable file object. - - It we're running on a Macintosh, 'res_name_or_index' maybe an sfnt - resource name or an sfnt resource index number or zero. The latter - case will cause TTLib to autodetect whether the file is a flat file - or a suitcase. (If it's a suitcase, only the first 'sfnt' resource - will be read!) - - The 'checkChecksums' argument is used to specify how sfnt - checksums are treated upon reading a file from disk: - 0: don't check (default) - 1: check, print warnings if a wrong checksum is found - 2: check, raise an exception if a wrong checksum is found. - - The TTFont constructor can also be called without a 'file' - argument: this is the way to create a new empty font. - In this case you can optionally supply the 'sfntVersion' argument, - and a 'flavor' which can be None, or 'woff'. - - If the recalcBBoxes argument is false, a number of things will *not* - be recalculated upon save/compile: - 1) glyph bounding boxes - 2) maxp font bounding box - 3) hhea min/max values - (1) is needed for certain kinds of CJK fonts (ask Werner Lemberg ;-). - Additionally, upon importing an TTX file, this option cause glyphs - to be compiled right away. This should reduce memory consumption - greatly, and therefore should have some impact on the time needed - to parse/compile large fonts. - - If the recalcTimestamp argument is false, the modified timestamp in the - 'head' table will *not* be recalculated upon save/compile. - - If the allowVID argument is set to true, then virtual GID's are - supported. Asking for a glyph ID with a glyph name or GID that is not in - the font will return a virtual GID. This is valid for GSUB and cmap - tables. For SING glyphlets, the cmap table is used to specify Unicode - values for virtual GI's used in GSUB/GPOS rules. If the gid N is requested - and does not exist in the font, or the glyphname has the form glyphN - and does not exist in the font, then N is used as the virtual GID. - Else, the first virtual GID is assigned as 0x1000 -1; for subsequent new - virtual GIDs, the next is one less than the previous. - - If ignoreDecompileErrors is set to True, exceptions raised in - individual tables during decompilation will be ignored, falling - back to the DefaultTable implementation, which simply keeps the - binary data. - - If lazy is set to True, many data structures are loaded lazily, upon - access only. - """ - - from fontTools.ttLib import sfnt - self.verbose = verbose - self.quiet = quiet - self.lazy = lazy - self.recalcBBoxes = recalcBBoxes - self.recalcTimestamp = recalcTimestamp - self.tables = {} - self.reader = None - - # Permit the user to reference glyphs that are not int the font. - self.last_vid = 0xFFFE # Can't make it be 0xFFFF, as the world is full unsigned short integer counters that get incremented after the last seen GID value. - self.reverseVIDDict = {} - self.VIDDict = {} - self.allowVID = allowVID - self.ignoreDecompileErrors = ignoreDecompileErrors - - if not file: - self.sfntVersion = sfntVersion - self.flavor = flavor - self.flavorData = None - return - if not hasattr(file, "read"): - # assume file is a string - if haveMacSupport and res_name_or_index is not None: - # on the mac, we deal with sfnt resources as well as flat files - from . import macUtils - if res_name_or_index == 0: - if macUtils.getSFNTResIndices(file): - # get the first available sfnt font. - file = macUtils.SFNTResourceReader(file, 1) - else: - file = open(file, "rb") - else: - file = macUtils.SFNTResourceReader(file, res_name_or_index) - else: - file = open(file, "rb") - else: - pass # assume "file" is a readable file object - self.reader = sfnt.SFNTReader(file, checkChecksums, fontNumber=fontNumber) - self.sfntVersion = self.reader.sfntVersion - self.flavor = self.reader.flavor - self.flavorData = self.reader.flavorData - - def close(self): - """If we still have a reader object, close it.""" - if self.reader is not None: - self.reader.close() - - def save(self, file, makeSuitcase=False, reorderTables=True): - """Save the font to disk. Similarly to the constructor, - the 'file' argument can be either a pathname or a writable - file object. - - On the Mac, if makeSuitcase is true, a suitcase (resource fork) - file will we made instead of a flat .ttf file. - """ - from fontTools.ttLib import sfnt - if not hasattr(file, "write"): - closeStream = 1 - if os.name == "mac" and makeSuitcase: - from . import macUtils - file = macUtils.SFNTResourceWriter(file, self) - else: - file = open(file, "wb") - if os.name == "mac": - from fontTools.misc.macCreator import setMacCreatorAndType - setMacCreatorAndType(file.name, 'mdos', 'BINA') - else: - # assume "file" is a writable file object - closeStream = 0 - - tags = list(self.keys()) - if "GlyphOrder" in tags: - tags.remove("GlyphOrder") - numTables = len(tags) - if reorderTables: - import tempfile - tmp = tempfile.TemporaryFile(prefix="ttx-fonttools") - else: - tmp = file - writer = sfnt.SFNTWriter(tmp, numTables, self.sfntVersion, self.flavor, self.flavorData) - - done = [] - for tag in tags: - self._writeTable(tag, writer, done) - - writer.close() - - if reorderTables: - tmp.flush() - tmp.seek(0) - reorderFontTables(tmp, file) - tmp.close() - - if closeStream: - file.close() - - def saveXML(self, fileOrPath, progress=None, quiet=False, - tables=None, skipTables=None, splitTables=False, disassembleInstructions=True, - bitmapGlyphDataFormat='raw'): - """Export the font as TTX (an XML-based text file), or as a series of text - files when splitTables is true. In the latter case, the 'fileOrPath' - argument should be a path to a directory. - The 'tables' argument must either be false (dump all tables) or a - list of tables to dump. The 'skipTables' argument may be a list of tables - to skip, but only when the 'tables' argument is false. - """ - from fontTools import version - from fontTools.misc import xmlWriter - - self.disassembleInstructions = disassembleInstructions - self.bitmapGlyphDataFormat = bitmapGlyphDataFormat - if not tables: - tables = list(self.keys()) - if "GlyphOrder" not in tables: - tables = ["GlyphOrder"] + tables - if skipTables: - for tag in skipTables: - if tag in tables: - tables.remove(tag) - numTables = len(tables) - if progress: - progress.set(0, numTables) - idlefunc = getattr(progress, "idle", None) - else: - idlefunc = None - - writer = xmlWriter.XMLWriter(fileOrPath, idlefunc=idlefunc) - writer.begintag("ttFont", sfntVersion=repr(self.sfntVersion)[1:-1], - ttLibVersion=version) - writer.newline() - - if not splitTables: - writer.newline() - else: - # 'fileOrPath' must now be a path - path, ext = os.path.splitext(fileOrPath) - fileNameTemplate = path + ".%s" + ext - - for i in range(numTables): - if progress: - progress.set(i) - tag = tables[i] - if splitTables: - tablePath = fileNameTemplate % tagToIdentifier(tag) - tableWriter = xmlWriter.XMLWriter(tablePath, idlefunc=idlefunc) - tableWriter.begintag("ttFont", ttLibVersion=version) - tableWriter.newline() - tableWriter.newline() - writer.simpletag(tagToXML(tag), src=os.path.basename(tablePath)) - writer.newline() - else: - tableWriter = writer - self._tableToXML(tableWriter, tag, progress, quiet) - if splitTables: - tableWriter.endtag("ttFont") - tableWriter.newline() - tableWriter.close() - if progress: - progress.set((i + 1)) - writer.endtag("ttFont") - writer.newline() - writer.close() - if self.verbose: - debugmsg("Done dumping TTX") - - def _tableToXML(self, writer, tag, progress, quiet): - if tag in self: - table = self[tag] - report = "Dumping '%s' table..." % tag - else: - report = "No '%s' table found." % tag - if progress: - progress.setLabel(report) - elif self.verbose: - debugmsg(report) - else: - if not quiet: - print(report) - if tag not in self: - return - xmlTag = tagToXML(tag) - if hasattr(table, "ERROR"): - writer.begintag(xmlTag, ERROR="decompilation error") - else: - writer.begintag(xmlTag) - writer.newline() - if tag in ("glyf", "CFF "): - table.toXML(writer, self, progress) - else: - table.toXML(writer, self) - writer.endtag(xmlTag) - writer.newline() - writer.newline() - - def importXML(self, file, progress=None, quiet=False): - """Import a TTX file (an XML-based text format), so as to recreate - a font object. - """ - if "maxp" in self and "post" in self: - # Make sure the glyph order is loaded, as it otherwise gets - # lost if the XML doesn't contain the glyph order, yet does - # contain the table which was originally used to extract the - # glyph names from (ie. 'post', 'cmap' or 'CFF '). - self.getGlyphOrder() - - from fontTools.misc import xmlReader - - reader = xmlReader.XMLReader(file, self, progress, quiet) - reader.read() - - def isLoaded(self, tag): - """Return true if the table identified by 'tag' has been - decompiled and loaded into memory.""" - return tag in self.tables - - def has_key(self, tag): - if self.isLoaded(tag): - return True - elif self.reader and tag in self.reader: - return True - elif tag == "GlyphOrder": - return True - else: - return False - - __contains__ = has_key - - def keys(self): - keys = list(self.tables.keys()) - if self.reader: - for key in list(self.reader.keys()): - if key not in keys: - keys.append(key) - - if "GlyphOrder" in keys: - keys.remove("GlyphOrder") - keys = sortedTagList(keys) - return ["GlyphOrder"] + keys - - def __len__(self): - return len(list(self.keys())) - - def __getitem__(self, tag): - tag = Tag(tag) - try: - return self.tables[tag] - except KeyError: - if tag == "GlyphOrder": - table = GlyphOrder(tag) - self.tables[tag] = table - return table - if self.reader is not None: - import traceback - if self.verbose: - debugmsg("Reading '%s' table from disk" % tag) - data = self.reader[tag] - tableClass = getTableClass(tag) - table = tableClass(tag) - self.tables[tag] = table - if self.verbose: - debugmsg("Decompiling '%s' table" % tag) - try: - table.decompile(data, self) - except: - if not self.ignoreDecompileErrors: - raise - # fall back to DefaultTable, retaining the binary table data - print("An exception occurred during the decompilation of the '%s' table" % tag) - from .tables.DefaultTable import DefaultTable - file = StringIO() - traceback.print_exc(file=file) - table = DefaultTable(tag) - table.ERROR = file.getvalue() - self.tables[tag] = table - table.decompile(data, self) - return table - else: - raise KeyError("'%s' table not found" % tag) - - def __setitem__(self, tag, table): - self.tables[Tag(tag)] = table - - def __delitem__(self, tag): - if tag not in self: - raise KeyError("'%s' table not found" % tag) - if tag in self.tables: - del self.tables[tag] - if self.reader and tag in self.reader: - del self.reader[tag] - - def get(self, tag, default=None): - try: - return self[tag] - except KeyError: - return default - - def setGlyphOrder(self, glyphOrder): - self.glyphOrder = glyphOrder - - def getGlyphOrder(self): - try: - return self.glyphOrder - except AttributeError: - pass - if 'CFF ' in self: - cff = self['CFF '] - self.glyphOrder = cff.getGlyphOrder() - elif 'post' in self: - # TrueType font - glyphOrder = self['post'].getGlyphOrder() - if glyphOrder is None: - # - # No names found in the 'post' table. - # Try to create glyph names from the unicode cmap (if available) - # in combination with the Adobe Glyph List (AGL). - # - self._getGlyphNamesFromCmap() - else: - self.glyphOrder = glyphOrder - else: - self._getGlyphNamesFromCmap() - return self.glyphOrder - - def _getGlyphNamesFromCmap(self): - # - # This is rather convoluted, but then again, it's an interesting problem: - # - we need to use the unicode values found in the cmap table to - # build glyph names (eg. because there is only a minimal post table, - # or none at all). - # - but the cmap parser also needs glyph names to work with... - # So here's what we do: - # - make up glyph names based on glyphID - # - load a temporary cmap table based on those names - # - extract the unicode values, build the "real" glyph names - # - unload the temporary cmap table - # - if self.isLoaded("cmap"): - # Bootstrapping: we're getting called by the cmap parser - # itself. This means self.tables['cmap'] contains a partially - # loaded cmap, making it impossible to get at a unicode - # subtable here. We remove the partially loaded cmap and - # restore it later. - # This only happens if the cmap table is loaded before any - # other table that does f.getGlyphOrder() or f.getGlyphName(). - cmapLoading = self.tables['cmap'] - del self.tables['cmap'] - else: - cmapLoading = None - # Make up glyph names based on glyphID, which will be used by the - # temporary cmap and by the real cmap in case we don't find a unicode - # cmap. - numGlyphs = int(self['maxp'].numGlyphs) - glyphOrder = [None] * numGlyphs - glyphOrder[0] = ".notdef" - for i in range(1, numGlyphs): - glyphOrder[i] = "glyph%.5d" % i - # Set the glyph order, so the cmap parser has something - # to work with (so we don't get called recursively). - self.glyphOrder = glyphOrder - # Get a (new) temporary cmap (based on the just invented names) - tempcmap = self['cmap'].getcmap(3, 1) - if tempcmap is not None: - # we have a unicode cmap - from fontTools import agl - cmap = tempcmap.cmap - # create a reverse cmap dict - reversecmap = {} - for unicode, name in list(cmap.items()): - reversecmap[name] = unicode - allNames = {} - for i in range(numGlyphs): - tempName = glyphOrder[i] - if tempName in reversecmap: - unicode = reversecmap[tempName] - if unicode in agl.UV2AGL: - # get name from the Adobe Glyph List - glyphName = agl.UV2AGL[unicode] - else: - # create uni name - glyphName = "uni%04X" % unicode - tempName = glyphName - n = 1 - while tempName in allNames: - tempName = glyphName + "#" + repr(n) - n = n + 1 - glyphOrder[i] = tempName - allNames[tempName] = 1 - # Delete the temporary cmap table from the cache, so it can - # be parsed again with the right names. - del self.tables['cmap'] - else: - pass # no unicode cmap available, stick with the invented names - self.glyphOrder = glyphOrder - if cmapLoading: - # restore partially loaded cmap, so it can continue loading - # using the proper names. - self.tables['cmap'] = cmapLoading - - def getGlyphNames(self): - """Get a list of glyph names, sorted alphabetically.""" - glyphNames = sorted(self.getGlyphOrder()[:]) - return glyphNames - - def getGlyphNames2(self): - """Get a list of glyph names, sorted alphabetically, - but not case sensitive. - """ - from fontTools.misc import textTools - return textTools.caselessSort(self.getGlyphOrder()) - - def getGlyphName(self, glyphID, requireReal=False): - try: - return self.getGlyphOrder()[glyphID] - except IndexError: - if requireReal or not self.allowVID: - # XXX The ??.W8.otf font that ships with OSX uses higher glyphIDs in - # the cmap table than there are glyphs. I don't think it's legal... - return "glyph%.5d" % glyphID - else: - # user intends virtual GID support - try: - glyphName = self.VIDDict[glyphID] - except KeyError: - glyphName ="glyph%.5d" % glyphID - self.last_vid = min(glyphID, self.last_vid ) - self.reverseVIDDict[glyphName] = glyphID - self.VIDDict[glyphID] = glyphName - return glyphName - - def getGlyphID(self, glyphName, requireReal=False): - if not hasattr(self, "_reverseGlyphOrderDict"): - self._buildReverseGlyphOrderDict() - glyphOrder = self.getGlyphOrder() - d = self._reverseGlyphOrderDict - if glyphName not in d: - if glyphName in glyphOrder: - self._buildReverseGlyphOrderDict() - return self.getGlyphID(glyphName) - else: - if requireReal: - raise KeyError(glyphName) - elif not self.allowVID: - # Handle glyphXXX only - if glyphName[:5] == "glyph": - try: - return int(glyphName[5:]) - except (NameError, ValueError): - raise KeyError(glyphName) - else: - # user intends virtual GID support - try: - glyphID = self.reverseVIDDict[glyphName] - except KeyError: - # if name is in glyphXXX format, use the specified name. - if glyphName[:5] == "glyph": - try: - glyphID = int(glyphName[5:]) - except (NameError, ValueError): - glyphID = None - if glyphID is None: - glyphID = self.last_vid -1 - self.last_vid = glyphID - self.reverseVIDDict[glyphName] = glyphID - self.VIDDict[glyphID] = glyphName - return glyphID - - glyphID = d[glyphName] - if glyphName != glyphOrder[glyphID]: - self._buildReverseGlyphOrderDict() - return self.getGlyphID(glyphName) - return glyphID - - def getReverseGlyphMap(self, rebuild=False): - if rebuild or not hasattr(self, "_reverseGlyphOrderDict"): - self._buildReverseGlyphOrderDict() - return self._reverseGlyphOrderDict - - def _buildReverseGlyphOrderDict(self): - self._reverseGlyphOrderDict = d = {} - glyphOrder = self.getGlyphOrder() - for glyphID in range(len(glyphOrder)): - d[glyphOrder[glyphID]] = glyphID - - def _writeTable(self, tag, writer, done): - """Internal helper function for self.save(). Keeps track of - inter-table dependencies. - """ - if tag in done: - return - tableClass = getTableClass(tag) - for masterTable in tableClass.dependencies: - if masterTable not in done: - if masterTable in self: - self._writeTable(masterTable, writer, done) - else: - done.append(masterTable) - tabledata = self.getTableData(tag) - if self.verbose: - debugmsg("writing '%s' table to disk" % tag) - writer[tag] = tabledata - done.append(tag) - - def getTableData(self, tag): - """Returns raw table data, whether compiled or directly read from disk. - """ - tag = Tag(tag) - if self.isLoaded(tag): - if self.verbose: - debugmsg("compiling '%s' table" % tag) - return self.tables[tag].compile(self) - elif self.reader and tag in self.reader: - if self.verbose: - debugmsg("Reading '%s' table from disk" % tag) - return self.reader[tag] - else: - raise KeyError(tag) - - def getGlyphSet(self, preferCFF=True): - """Return a generic GlyphSet, which is a dict-like object - mapping glyph names to glyph objects. The returned glyph objects - have a .draw() method that supports the Pen protocol, and will - have an attribute named 'width', but only *after* the .draw() method - has been called. - - If the font is CFF-based, the outlines will be taken from the 'CFF ' - table. Otherwise the outlines will be taken from the 'glyf' table. - If the font contains both a 'CFF ' and a 'glyf' table, you can use - the 'preferCFF' argument to specify which one should be taken. - """ - if preferCFF and "CFF " in self: - return list(self["CFF "].cff.values())[0].CharStrings - if "glyf" in self: - return _TTGlyphSet(self) - if "CFF " in self: - return list(self["CFF "].cff.values())[0].CharStrings - raise TTLibError("Font contains no outlines") - - -class _TTGlyphSet(object): - - """Generic dict-like GlyphSet class, meant as a TrueType counterpart - to CFF's CharString dict. See TTFont.getGlyphSet(). - """ - - # This class is distinct from the 'glyf' table itself because we need - # access to the 'hmtx' table, which could cause a dependency problem - # there when reading from XML. - - def __init__(self, ttFont): - self._ttFont = ttFont - - def keys(self): - return list(self._ttFont["glyf"].keys()) - - def has_key(self, glyphName): - return glyphName in self._ttFont["glyf"] - - __contains__ = has_key - - def __getitem__(self, glyphName): - return _TTGlyph(glyphName, self._ttFont) - - def get(self, glyphName, default=None): - try: - return self[glyphName] - except KeyError: - return default - - -class _TTGlyph(object): - - """Wrapper for a TrueType glyph that supports the Pen protocol, meaning - that it has a .draw() method that takes a pen object as its only - argument. Additionally there is a 'width' attribute. - """ - - def __init__(self, glyphName, ttFont): - self._glyphName = glyphName - self._ttFont = ttFont - self.width, self.lsb = self._ttFont['hmtx'][self._glyphName] - - def draw(self, pen): - """Draw the glyph onto Pen. See fontTools.pens.basePen for details - how that works. - """ - glyfTable = self._ttFont['glyf'] - glyph = glyfTable[self._glyphName] - if hasattr(glyph, "xMin"): - offset = self.lsb - glyph.xMin - else: - offset = 0 - if glyph.isComposite(): - for component in glyph: - glyphName, transform = component.getComponentInfo() - pen.addComponent(glyphName, transform) - else: - coordinates, endPts, flags = glyph.getCoordinates(glyfTable) - if offset: - coordinates = coordinates + (offset, 0) - start = 0 - for end in endPts: - end = end + 1 - contour = coordinates[start:end].tolist() - cFlags = flags[start:end].tolist() - start = end - if 1 not in cFlags: - # There is not a single on-curve point on the curve, - # use pen.qCurveTo's special case by specifying None - # as the on-curve point. - contour.append(None) - pen.qCurveTo(*contour) - else: - # Shuffle the points so that contour the is guaranteed - # to *end* in an on-curve point, which we'll use for - # the moveTo. - firstOnCurve = cFlags.index(1) + 1 - contour = contour[firstOnCurve:] + contour[:firstOnCurve] - cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve] - pen.moveTo(contour[-1]) - while contour: - nextOnCurve = cFlags.index(1) + 1 - if nextOnCurve == 1: - pen.lineTo(contour[0]) - else: - pen.qCurveTo(*contour[:nextOnCurve]) - contour = contour[nextOnCurve:] - cFlags = cFlags[nextOnCurve:] - pen.closePath() - - -class GlyphOrder(object): - - """A pseudo table. The glyph order isn't in the font as a separate - table, but it's nice to present it as such in the TTX format. - """ - - def __init__(self, tag=None): - pass - - def toXML(self, writer, ttFont): - glyphOrder = ttFont.getGlyphOrder() - writer.comment("The 'id' attribute is only for humans; " - "it is ignored when parsed.") - writer.newline() - for i in range(len(glyphOrder)): - glyphName = glyphOrder[i] - writer.simpletag("GlyphID", id=i, name=glyphName) - writer.newline() - - def fromXML(self, name, attrs, content, ttFont): - if not hasattr(self, "glyphOrder"): - self.glyphOrder = [] - ttFont.setGlyphOrder(self.glyphOrder) - if name == "GlyphID": - self.glyphOrder.append(attrs["name"]) - - -def getTableModule(tag): - """Fetch the packer/unpacker module for a table. - Return None when no module is found. - """ - from . import tables - pyTag = tagToIdentifier(tag) - try: - __import__("fontTools.ttLib.tables." + pyTag) - except ImportError as err: - # If pyTag is found in the ImportError message, - # means table is not implemented. If it's not - # there, then some other module is missing, don't - # suppress the error. - if str(err).find(pyTag) >= 0: - return None - else: - raise err - else: - return getattr(tables, pyTag) - - -def getTableClass(tag): - """Fetch the packer/unpacker class for a table. - Return None when no class is found. - """ - module = getTableModule(tag) - if module is None: - from .tables.DefaultTable import DefaultTable - return DefaultTable - pyTag = tagToIdentifier(tag) - tableClass = getattr(module, "table_" + pyTag) - return tableClass - - -def getClassTag(klass): - """Fetch the table tag for a class object.""" - name = klass.__name__ - assert name[:6] == 'table_' - name = name[6:] # Chop 'table_' - return identifierToTag(name) - - - -def newTable(tag): - """Return a new instance of a table.""" - tableClass = getTableClass(tag) - return tableClass(tag) - - -def _escapechar(c): - """Helper function for tagToIdentifier()""" - import re - if re.match("[a-z0-9]", c): - return "_" + c - elif re.match("[A-Z]", c): - return c + "_" - else: - return hex(byteord(c))[2:] - - -def tagToIdentifier(tag): - """Convert a table tag to a valid (but UGLY) python identifier, - as well as a filename that's guaranteed to be unique even on a - caseless file system. Each character is mapped to two characters. - Lowercase letters get an underscore before the letter, uppercase - letters get an underscore after the letter. Trailing spaces are - trimmed. Illegal characters are escaped as two hex bytes. If the - result starts with a number (as the result of a hex escape), an - extra underscore is prepended. Examples: - 'glyf' -> '_g_l_y_f' - 'cvt ' -> '_c_v_t' - 'OS/2' -> 'O_S_2f_2' - """ - import re - tag = Tag(tag) - if tag == "GlyphOrder": - return tag - assert len(tag) == 4, "tag should be 4 characters long" - while len(tag) > 1 and tag[-1] == ' ': - tag = tag[:-1] - ident = "" - for c in tag: - ident = ident + _escapechar(c) - if re.match("[0-9]", ident): - ident = "_" + ident - return ident - - -def identifierToTag(ident): - """the opposite of tagToIdentifier()""" - if ident == "GlyphOrder": - return ident - if len(ident) % 2 and ident[0] == "_": - ident = ident[1:] - assert not (len(ident) % 2) - tag = "" - for i in range(0, len(ident), 2): - if ident[i] == "_": - tag = tag + ident[i+1] - elif ident[i+1] == "_": - tag = tag + ident[i] - else: - # assume hex - tag = tag + chr(int(ident[i:i+2], 16)) - # append trailing spaces - tag = tag + (4 - len(tag)) * ' ' - return Tag(tag) - - -def tagToXML(tag): - """Similarly to tagToIdentifier(), this converts a TT tag - to a valid XML element name. Since XML element names are - case sensitive, this is a fairly simple/readable translation. - """ - import re - tag = Tag(tag) - if tag == "OS/2": - return "OS_2" - elif tag == "GlyphOrder": - return tag - if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): - return tag.strip() - else: - return tagToIdentifier(tag) - - -def xmlToTag(tag): - """The opposite of tagToXML()""" - if tag == "OS_2": - return Tag("OS/2") - if len(tag) == 8: - return identifierToTag(tag) - else: - return Tag(tag + " " * (4 - len(tag))) - - +@deprecateFunction("use logging instead", category=DeprecationWarning) def debugmsg(msg): import time print(msg + time.strftime(" (%H:%M:%S)", time.localtime(time.time()))) - -# Table order as recommended in the OpenType specification 1.4 -TTFTableOrder = ["head", "hhea", "maxp", "OS/2", "hmtx", "LTSH", "VDMX", - "hdmx", "cmap", "fpgm", "prep", "cvt ", "loca", "glyf", - "kern", "name", "post", "gasp", "PCLT"] - -OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", - "CFF "] - -def sortedTagList(tagList, tableOrder=None): - """Return a sorted copy of tagList, sorted according to the OpenType - specification, or according to a custom tableOrder. If given and not - None, tableOrder needs to be a list of tag names. - """ - tagList = sorted(tagList) - if tableOrder is None: - if "DSIG" in tagList: - # DSIG should be last (XXX spec reference?) - tagList.remove("DSIG") - tagList.append("DSIG") - if "CFF " in tagList: - tableOrder = OTFTableOrder - else: - tableOrder = TTFTableOrder - orderedTables = [] - for tag in tableOrder: - if tag in tagList: - orderedTables.append(tag) - tagList.remove(tag) - orderedTables.extend(tagList) - return orderedTables - - -def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=False): - """Rewrite a font file, ordering the tables as recommended by the - OpenType specification 1.4. - """ - from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter - reader = SFNTReader(inFile, checkChecksums=checkChecksums) - writer = SFNTWriter(outFile, len(reader.tables), reader.sfntVersion, reader.flavor, reader.flavorData) - tables = list(reader.keys()) - for tag in sortedTagList(tables, tableOrder): - writer[tag] = reader[tag] - writer.close() - - -def maxPowerOfTwo(x): - """Return the highest exponent of two, so that - (2 ** exponent) <= x. Return 0 if x is 0. - """ - exponent = 0 - while x: - x = x >> 1 - exponent = exponent + 1 - return max(exponent - 1, 0) - - -def getSearchRange(n, itemSize): - """Calculate searchRange, entrySelector, rangeShift. - """ - # This stuff needs to be stored in the file, because? - exponent = maxPowerOfTwo(n) - searchRange = (2 ** exponent) * itemSize - entrySelector = exponent - rangeShift = max(0, n * itemSize - searchRange) - return searchRange, entrySelector, rangeShift +from fontTools.ttLib.ttFont import * +from fontTools.ttLib.ttCollection import TTCollection diff --git a/Lib/fontTools/ttLib/macUtils.py b/Lib/fontTools/ttLib/macUtils.py index d565528..17dd5ef 100644 --- a/Lib/fontTools/ttLib/macUtils.py +++ b/Lib/fontTools/ttLib/macUtils.py @@ -1,42 +1,22 @@ """ttLib.macUtils.py -- Various Mac-specific stuff.""" - from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * -import sys -import os -if sys.platform not in ("mac", "darwin"): - raise ImportError("This module is Mac-only!") -try: - from Carbon import Res -except ImportError: - import Res - - - -def MyOpenResFile(path): - mode = 1 # read only - try: - resref = Res.FSOpenResFile(path, mode) - except Res.Error: - # try data fork - resref = Res.FSOpenResourceFile(path, unicode(), mode) - return resref +from fontTools.misc.macRes import ResourceReader, ResourceError def getSFNTResIndices(path): - """Determine whether a file has a resource fork or not.""" + """Determine whether a file has a 'sfnt' resource fork or not.""" try: - resref = MyOpenResFile(path) - except Res.Error: + reader = ResourceReader(path) + indices = reader.getIndices('sfnt') + reader.close() + return indices + except ResourceError: return [] - Res.UseResFile(resref) - numSFNTs = Res.Count1Resources('sfnt') - Res.CloseResFile(resref) - return list(range(1, numSFNTs + 1)) def openTTFonts(path): - """Given a pathname, return a list of TTFont objects. In the case + """Given a pathname, return a list of TTFont objects. In the case of a flat TTF/OTF file, the list will contain just one font object; but in the case of a Mac font suitcase it will contain as many font objects as there are sfnt resources in the file. @@ -54,149 +34,20 @@ def openTTFonts(path): return fonts -class SFNTResourceReader(object): - - """Simple (Mac-only) read-only file wrapper for 'sfnt' resources.""" - +class SFNTResourceReader(BytesIO): + + """Simple read-only file wrapper for 'sfnt' resources.""" + def __init__(self, path, res_name_or_index): - resref = MyOpenResFile(path) - Res.UseResFile(resref) + from fontTools import ttLib + reader = ResourceReader(path) if isinstance(res_name_or_index, basestring): - res = Res.Get1NamedResource('sfnt', res_name_or_index) + rsrc = reader.getNamedResource('sfnt', res_name_or_index) else: - res = Res.Get1IndResource('sfnt', res_name_or_index) - self.file = StringIO(res.data) - Res.CloseResFile(resref) + rsrc = reader.getIndResource('sfnt', res_name_or_index) + if rsrc is None: + raise ttLib.TTLibError("sfnt resource not found: %s" % res_name_or_index) + reader.close() + self.rsrc = rsrc + super(SFNTResourceReader, self).__init__(rsrc.data) self.name = path - - def __getattr__(self, attr): - # cheap inheritance - return getattr(self.file, attr) - - -class SFNTResourceWriter(object): - - """Simple (Mac-only) file wrapper for 'sfnt' resources.""" - - def __init__(self, path, ttFont, res_id=None): - self.file = StringIO() - self.name = path - self.closed = 0 - fullname = ttFont['name'].getName(4, 1, 0) # Full name, mac, default encoding - familyname = ttFont['name'].getName(1, 1, 0) # Fam. name, mac, default encoding - psname = ttFont['name'].getName(6, 1, 0) # PostScript name, etc. - if fullname is None or fullname is None or psname is None: - from fontTools import ttLib - raise ttLib.TTLibError("can't make 'sfnt' resource, no Macintosh 'name' table found") - self.fullname = fullname.string - self.familyname = familyname.string - self.psname = psname.string - if self.familyname != self.psname[:len(self.familyname)]: - # ugh. force fam name to be the same as first part of ps name, - # fondLib otherwise barfs. - for i in range(min(len(self.psname), len(self.familyname))): - if self.familyname[i] != self.psname[i]: - break - self.familyname = self.psname[:i] - - self.ttFont = ttFont - self.res_id = res_id - if os.path.exists(self.name): - os.remove(self.name) - # XXX datafork support - Res.FSpCreateResFile(self.name, 'DMOV', 'FFIL', 0) - self.resref = Res.FSOpenResFile(self.name, 3) # exclusive read/write permission - - def close(self): - if self.closed: - return - Res.UseResFile(self.resref) - try: - res = Res.Get1NamedResource('sfnt', self.fullname) - except Res.Error: - pass - else: - res.RemoveResource() - res = Res.Resource(self.file.getvalue()) - if self.res_id is None: - self.res_id = Res.Unique1ID('sfnt') - res.AddResource('sfnt', self.res_id, self.fullname) - res.ChangedResource() - - self.createFond() - del self.ttFont - Res.CloseResFile(self.resref) - self.file.close() - self.closed = 1 - - def createFond(self): - fond_res = Res.Resource("") - fond_res.AddResource('FOND', self.res_id, self.fullname) - - from fontTools import fondLib - fond = fondLib.FontFamily(fond_res, "w") - - fond.ffFirstChar = 0 - fond.ffLastChar = 255 - fond.fondClass = 0 - fond.fontAssoc = [(0, 0, self.res_id)] - fond.ffFlags = 20480 # XXX ??? - fond.ffIntl = (0, 0) - fond.ffLeading = 0 - fond.ffProperty = (0, 0, 0, 0, 0, 0, 0, 0, 0) - fond.ffVersion = 0 - fond.glyphEncoding = {} - if self.familyname == self.psname: - fond.styleIndices = (1,) * 48 # uh-oh, fondLib is too dumb. - else: - fond.styleIndices = (2,) * 48 - fond.styleStrings = [] - fond.boundingBoxes = None - fond.ffFamID = self.res_id - fond.changed = 1 - fond.glyphTableOffset = 0 - fond.styleMappingReserved = 0 - - # calc: - scale = 4096 / self.ttFont['head'].unitsPerEm - fond.ffAscent = scale * self.ttFont['hhea'].ascent - fond.ffDescent = scale * self.ttFont['hhea'].descent - fond.ffWidMax = scale * self.ttFont['hhea'].advanceWidthMax - - fond.ffFamilyName = self.familyname - fond.psNames = {0: self.psname} - - fond.widthTables = {} - fond.kernTables = {} - cmap = self.ttFont['cmap'].getcmap(1, 0) - if cmap: - names = {} - for code, name in cmap.cmap.items(): - names[name] = code - if 'kern' in self.ttFont: - kern = self.ttFont['kern'].getkern(0) - if kern: - fondkerning = [] - for (left, right), value in kern.kernTable.items(): - if left in names and right in names: - fondkerning.append((names[left], names[right], scale * value)) - fondkerning.sort() - fond.kernTables = {0: fondkerning} - if 'hmtx' in self.ttFont: - hmtx = self.ttFont['hmtx'] - fondwidths = [2048] * 256 + [0, 0] # default width, + plus two zeros. - for name, (width, lsb) in hmtx.metrics.items(): - if name in names: - fondwidths[names[name]] = scale * width - fond.widthTables = {0: fondwidths} - fond.save() - - def __del__(self): - if not self.closed: - self.close() - - def __getattr__(self, attr): - # cheap inheritance - return getattr(self.file, attr) - - diff --git a/Lib/fontTools/ttLib/sfnt.py b/Lib/fontTools/ttLib/sfnt.py index c65fd28..6dc48ba 100644 --- a/Lib/fontTools/ttLib/sfnt.py +++ b/Lib/fontTools/ttLib/sfnt.py @@ -4,10 +4,10 @@ Defines two public classes: SFNTReader SFNTWriter -(Normally you don't have to use these classes explicitly; they are +(Normally you don't have to use these classes explicitly; they are used automatically by ttLib.TTFont.) -The reading and writing of sfnt files is separated in two distinct +The reading and writing of sfnt files is separated in two distinct classes, since whenever to number of tables changes or whenever a table's length chages you need to rewrite the whole file anyway. """ @@ -15,12 +15,33 @@ a table's length chages you need to rewrite the whole file anyway. from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.misc import sstruct -from fontTools.ttLib import getSearchRange +from fontTools.ttLib import TTLibError import struct +from collections import OrderedDict +import logging + + +log = logging.getLogger(__name__) class SFNTReader(object): - + + def __new__(cls, *args, **kwargs): + """ Return an instance of the SFNTReader sub-class which is compatible + with the input file type. + """ + if args and cls is SFNTReader: + infile = args[0] + infile.seek(0) + sfntVersion = Tag(infile.read(4)) + infile.seek(0) + if sfntVersion == "wOF2": + # return new WOFF2Reader object + from fontTools.ttLib.woff2 import WOFF2Reader + return object.__new__(WOFF2Reader) + # return default object + return object.__new__(cls) + def __init__(self, file, checkChecksums=1, fontNumber=-1): self.file = file self.checkChecksums = checkChecksums @@ -28,35 +49,43 @@ class SFNTReader(object): self.flavor = None self.flavorData = None self.DirectoryEntry = SFNTDirectoryEntry + self.file.seek(0) self.sfntVersion = self.file.read(4) self.file.seek(0) if self.sfntVersion == b"ttcf": - sstruct.unpack(ttcHeaderFormat, self.file.read(ttcHeaderSize), self) - assert self.Version == 0x00010000 or self.Version == 0x00020000, "unrecognized TTC version 0x%08x" % self.Version - if not 0 <= fontNumber < self.numFonts: - from fontTools import ttLib - raise ttLib.TTLibError("specify a font number between 0 and %d (inclusive)" % (self.numFonts - 1)) - offsetTable = struct.unpack(">%dL" % self.numFonts, self.file.read(self.numFonts * 4)) - if self.Version == 0x00020000: - pass # ignoring version 2.0 signatures - self.file.seek(offsetTable[fontNumber]) - sstruct.unpack(sfntDirectoryFormat, self.file.read(sfntDirectorySize), self) + header = readTTCHeader(self.file) + numFonts = header.numFonts + if not 0 <= fontNumber < numFonts: + raise TTLibError("specify a font number between 0 and %d (inclusive)" % (numFonts - 1)) + self.numFonts = numFonts + self.file.seek(header.offsetTable[fontNumber]) + data = self.file.read(sfntDirectorySize) + if len(data) != sfntDirectorySize: + raise TTLibError("Not a Font Collection (not enough data)") + sstruct.unpack(sfntDirectoryFormat, data, self) elif self.sfntVersion == b"wOFF": self.flavor = "woff" self.DirectoryEntry = WOFFDirectoryEntry - sstruct.unpack(woffDirectoryFormat, self.file.read(woffDirectorySize), self) + data = self.file.read(woffDirectorySize) + if len(data) != woffDirectorySize: + raise TTLibError("Not a WOFF font (not enough data)") + sstruct.unpack(woffDirectoryFormat, data, self) else: - sstruct.unpack(sfntDirectoryFormat, self.file.read(sfntDirectorySize), self) + data = self.file.read(sfntDirectorySize) + if len(data) != sfntDirectorySize: + raise TTLibError("Not a TrueType or OpenType font (not enough data)") + sstruct.unpack(sfntDirectoryFormat, data, self) self.sfntVersion = Tag(self.sfntVersion) if self.sfntVersion not in ("\x00\x01\x00\x00", "OTTO", "true"): - from fontTools import ttLib - raise ttLib.TTLibError("Not a TrueType or OpenType font (bad sfntVersion)") - self.tables = {} + raise TTLibError("Not a TrueType or OpenType font (bad sfntVersion)") + tables = {} for i in range(self.numTables): entry = self.DirectoryEntry() entry.fromFile(self.file) - self.tables[Tag(entry.tag)] = entry + tag = Tag(entry.tag) + tables[tag] = entry + self.tables = OrderedDict(sorted(tables.items(), key=lambda i: i[1].offset)) # Load flavor data if any if self.flavor == "woff": @@ -66,10 +95,10 @@ class SFNTReader(object): return tag in self.tables __contains__ = has_key - + def keys(self): return self.tables.keys() - + def __getitem__(self, tag): """Fetch the raw table data.""" entry = self.tables[Tag(tag)] @@ -82,23 +111,80 @@ class SFNTReader(object): checksum = calcChecksum(data) if self.checkChecksums > 1: # Be obnoxious, and barf when it's wrong - assert checksum == entry.checksum, "bad checksum for '%s' table" % tag + assert checksum == entry.checkSum, "bad checksum for '%s' table" % tag elif checksum != entry.checkSum: - # Be friendly, and just print a warning. - print("bad checksum for '%s' table" % tag) + # Be friendly, and just log a warning. + log.warning("bad checksum for '%s' table", tag) return data - + def __delitem__(self, tag): del self.tables[Tag(tag)] - + def close(self): self.file.close() +# default compression level for WOFF 1.0 tables and metadata +ZLIB_COMPRESSION_LEVEL = 6 + +# if set to True, use zopfli instead of zlib for compressing WOFF 1.0. +# The Python bindings are available at https://pypi.python.org/pypi/zopfli +USE_ZOPFLI = False + +# mapping between zlib's compression levels and zopfli's 'numiterations'. +# Use lower values for files over several MB in size or it will be too slow +ZOPFLI_LEVELS = { + # 0: 0, # can't do 0 iterations... + 1: 1, + 2: 3, + 3: 5, + 4: 8, + 5: 10, + 6: 15, + 7: 25, + 8: 50, + 9: 100, +} + + +def compress(data, level=ZLIB_COMPRESSION_LEVEL): + """ Compress 'data' to Zlib format. If 'USE_ZOPFLI' variable is True, + zopfli is used instead of the zlib module. + The compression 'level' must be between 0 and 9. 1 gives best speed, + 9 gives best compression (0 gives no compression at all). + The default value is a compromise between speed and compression (6). + """ + if not (0 <= level <= 9): + raise ValueError('Bad compression level: %s' % level) + if not USE_ZOPFLI or level == 0: + from zlib import compress + return compress(data, level) + else: + from zopfli.zlib import compress + return compress(data, numiterations=ZOPFLI_LEVELS[level]) + + class SFNTWriter(object): - + + def __new__(cls, *args, **kwargs): + """ Return an instance of the SFNTWriter sub-class which is compatible + with the specified 'flavor'. + """ + flavor = None + if kwargs and 'flavor' in kwargs: + flavor = kwargs['flavor'] + elif args and len(args) > 3: + flavor = args[3] + if cls is SFNTWriter: + if flavor == "woff2": + # return new WOFF2Writer object + from fontTools.ttLib.woff2 import WOFF2Writer + return object.__new__(WOFF2Writer) + # return default object + return object.__new__(cls) + def __init__(self, file, numTables, sfntVersion="\000\001\000\000", - flavor=None, flavorData=None): + flavor=None, flavorData=None): self.file = file self.numTables = numTables self.sfntVersion = Tag(sfntVersion) @@ -111,67 +197,72 @@ class SFNTWriter(object): self.DirectoryEntry = WOFFDirectoryEntry self.signature = "wOFF" + + # to calculate WOFF checksum adjustment, we also need the original SFNT offsets + self.origNextTableOffset = sfntDirectorySize + numTables * sfntDirectoryEntrySize else: - assert not self.flavor, "Unknown flavor '%s'" % self.flavor + assert not self.flavor, "Unknown flavor '%s'" % self.flavor self.directoryFormat = sfntDirectoryFormat self.directorySize = sfntDirectorySize self.DirectoryEntry = SFNTDirectoryEntry + from fontTools.ttLib import getSearchRange self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(numTables, 16) - self.nextTableOffset = self.directorySize + numTables * self.DirectoryEntry.formatSize + self.directoryOffset = self.file.tell() + self.nextTableOffset = self.directoryOffset + self.directorySize + numTables * self.DirectoryEntry.formatSize # clear out directory area self.file.seek(self.nextTableOffset) # make sure we're actually where we want to be. (old cStringIO bug) self.file.write(b'\0' * (self.nextTableOffset - self.file.tell())) - self.tables = {} - + self.tables = OrderedDict() + + def setEntry(self, tag, entry): + if tag in self.tables: + raise TTLibError("cannot rewrite '%s' table" % tag) + + self.tables[tag] = entry + def __setitem__(self, tag, data): """Write raw table data to disk.""" - reuse = False if tag in self.tables: - # We've written this table to file before. If the length - # of the data is still the same, we allow overwriting it. - entry = self.tables[tag] - assert not hasattr(entry.__class__, 'encodeData') - if len(data) != entry.length: - from fontTools import ttLib - raise ttLib.TTLibError("cannot rewrite '%s' table: length does not match directory entry" % tag) - reuse = True - else: - entry = self.DirectoryEntry() - entry.tag = tag + raise TTLibError("cannot rewrite '%s' table" % tag) + entry = self.DirectoryEntry() + entry.tag = tag + entry.offset = self.nextTableOffset if tag == 'head': entry.checkSum = calcChecksum(data[:8] + b'\0\0\0\0' + data[12:]) self.headTable = data entry.uncompressed = True else: entry.checkSum = calcChecksum(data) + entry.saveData(self.file, data) - entry.offset = self.nextTableOffset - entry.saveData (self.file, data) - - if not reuse: - self.nextTableOffset = self.nextTableOffset + ((entry.length + 3) & ~3) + if self.flavor == "woff": + entry.origOffset = self.origNextTableOffset + self.origNextTableOffset += (entry.origLength + 3) & ~3 + self.nextTableOffset = self.nextTableOffset + ((entry.length + 3) & ~3) # Add NUL bytes to pad the table data to a 4-byte boundary. # Don't depend on f.seek() as we need to add the padding even if no # subsequent write follows (seek is lazy), ie. after the final table # in the font. self.file.write(b'\0' * (self.nextTableOffset - self.file.tell())) assert self.nextTableOffset == self.file.tell() - - self.tables[tag] = entry - + + self.setEntry(tag, entry) + + def __getitem__(self, tag): + return self.tables[tag] + def close(self): """All tables must have been written to disk. Now write the directory. """ tables = sorted(self.tables.items()) if len(tables) != self.numTables: - from fontTools import ttLib - raise ttLib.TTLibError("wrong number of tables; expected %d, found %d" % (self.numTables, len(tables))) + raise TTLibError("wrong number of tables; expected %d, found %d" % (self.numTables, len(tables))) if self.flavor == "woff": self.signature = b"wOFF" @@ -195,8 +286,7 @@ class SFNTWriter(object): self.metaOrigLength = len(data.metaData) self.file.seek(0,2) self.metaOffset = self.file.tell() - import zlib - compressedMetaData = zlib.compress(data.metaData) + compressedMetaData = compress(data.metaData) self.metaLength = len(compressedMetaData) self.file.write(compressedMetaData) else: @@ -216,12 +306,12 @@ class SFNTWriter(object): self.length = self.file.tell() else: - assert not self.flavor, "Unknown flavor '%s'" % self.flavor + assert not self.flavor, "Unknown flavor '%s'" % self.flavor pass - + directory = sstruct.pack(self.directoryFormat, self) - - self.file.seek(self.directorySize) + + self.file.seek(self.directoryOffset + self.directorySize) seenHead = 0 for tag, entry in tables: if tag == "head": @@ -229,7 +319,7 @@ class SFNTWriter(object): directory = directory + entry.toString() if seenHead: self.writeMasterChecksum(directory) - self.file.seek(0) + self.file.seek(self.directoryOffset) self.file.write(directory) def _calcMasterChecksum(self, directory): @@ -239,17 +329,18 @@ class SFNTWriter(object): for i in range(len(tags)): checksums.append(self.tables[tags[i]].checkSum) - # TODO(behdad) I'm fairly sure the checksum for woff is not working correctly. - # Haven't debugged. if self.DirectoryEntry != SFNTDirectoryEntry: # Create a SFNT directory for checksum calculation purposes + from fontTools.ttLib import getSearchRange self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(self.numTables, 16) directory = sstruct.pack(sfntDirectoryFormat, self) tables = sorted(self.tables.items()) for tag, entry in tables: sfntEntry = SFNTDirectoryEntry() - for item in ['tag', 'checkSum', 'offset', 'length']: - setattr(sfntEntry, item, getattr(entry, item)) + sfntEntry.tag = entry.tag + sfntEntry.checkSum = entry.checkSum + sfntEntry.offset = entry.origOffset + sfntEntry.length = entry.origLength directory = directory + sfntEntry.toString() directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize @@ -267,6 +358,9 @@ class SFNTWriter(object): self.file.seek(self.tables['head'].offset + 8) self.file.write(struct.pack(">L", checksumadjustment)) + def reordersTables(self): + return False + # -- sfnt directory helpers and cruft @@ -336,19 +430,19 @@ woffDirectoryEntrySize = sstruct.calcsize(woffDirectoryEntryFormat) class DirectoryEntry(object): - + def __init__(self): self.uncompressed = False # if True, always embed entry raw def fromFile(self, file): sstruct.unpack(self.format, file.read(self.formatSize), self) - + def fromString(self, str): sstruct.unpack(self.format, str, self) - + def toString(self): return sstruct.pack(self.format, self) - + def __repr__(self): if hasattr(self, "tag"): return "<%s '%s' at %x>" % (self.__class__.__name__, self.tag, id(self)) @@ -385,7 +479,17 @@ class WOFFDirectoryEntry(DirectoryEntry): format = woffDirectoryEntryFormat formatSize = woffDirectoryEntrySize - zlibCompressionLevel = 6 + + def __init__(self): + super(WOFFDirectoryEntry, self).__init__() + # With fonttools<=3.1.2, the only way to set a different zlib + # compression level for WOFF directory entries was to set the class + # attribute 'zlibCompressionLevel'. This is now replaced by a globally + # defined `ZLIB_COMPRESSION_LEVEL`, which is also applied when + # compressing the metadata. For backward compatibility, we still + # use the class attribute if it was already set. + if not hasattr(WOFFDirectoryEntry, 'zlibCompressionLevel'): + self.zlibCompressionLevel = ZLIB_COMPRESSION_LEVEL def decodeData(self, rawData): import zlib @@ -394,14 +498,13 @@ class WOFFDirectoryEntry(DirectoryEntry): else: assert self.length < self.origLength data = zlib.decompress(rawData) - assert len (data) == self.origLength + assert len(data) == self.origLength return data def encodeData(self, data): - import zlib self.origLength = len(data) if not self.uncompressed: - compressedData = zlib.compress(data, self.zlibCompressionLevel) + compressedData = compress(data, self.zlibCompressionLevel) if self.uncompressed or len(compressedData) >= self.origLength: # Encode uncompressed rawData = data @@ -443,13 +546,13 @@ def calcChecksum(data): Optionally takes a 'start' argument, which allows you to calculate a checksum in chunks by feeding it a previous result. - + If the data length is not a multiple of four, it assumes - it is to be padded with null byte. + it is to be padded with null byte. - >>> print calcChecksum(b"abcd") + >>> print(calcChecksum(b"abcd")) 1633837924 - >>> print calcChecksum(b"abcdxyz") + >>> print(calcChecksum(b"abcdxyz")) 3655064932 """ remainder = len(data) % 4 @@ -464,7 +567,33 @@ def calcChecksum(data): value = (value + sum(longs)) & 0xffffffff return value +def readTTCHeader(file): + file.seek(0) + data = file.read(ttcHeaderSize) + if len(data) != ttcHeaderSize: + raise TTLibError("Not a Font Collection (not enough data)") + self = SimpleNamespace() + sstruct.unpack(ttcHeaderFormat, data, self) + if self.TTCTag != "ttcf": + raise TTLibError("Not a Font Collection") + assert self.Version == 0x00010000 or self.Version == 0x00020000, "unrecognized TTC version 0x%08x" % self.Version + self.offsetTable = struct.unpack(">%dL" % self.numFonts, file.read(self.numFonts * 4)) + if self.Version == 0x00020000: + pass # ignoring version 2.0 signatures + return self + +def writeTTCHeader(file, numFonts): + self = SimpleNamespace() + self.TTCTag = 'ttcf' + self.Version = 0x00010000 + self.numFonts = numFonts + file.seek(0) + file.write(sstruct.pack(ttcHeaderFormat, self)) + offset = file.tell() + file.write(struct.pack(">%dL" % self.numFonts, *([0] * self.numFonts))) + return offset if __name__ == "__main__": - import doctest - doctest.testmod() + import sys + import doctest + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/ttLib/standardGlyphOrder.py b/Lib/fontTools/ttLib/standardGlyphOrder.py index fdb666a..510773a 100644 --- a/Lib/fontTools/ttLib/standardGlyphOrder.py +++ b/Lib/fontTools/ttLib/standardGlyphOrder.py @@ -1,3 +1,6 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * + # # 'post' table formats 1.0 and 2.0 rely on this list of "standard" # glyphs. @@ -10,262 +13,262 @@ # standardGlyphOrder = [ - ".notdef", # 0 - ".null", # 1 - "nonmarkingreturn", # 2 - "space", # 3 - "exclam", # 4 - "quotedbl", # 5 - "numbersign", # 6 - "dollar", # 7 - "percent", # 8 - "ampersand", # 9 - "quotesingle", # 10 - "parenleft", # 11 - "parenright", # 12 - "asterisk", # 13 - "plus", # 14 - "comma", # 15 - "hyphen", # 16 - "period", # 17 - "slash", # 18 - "zero", # 19 - "one", # 20 - "two", # 21 - "three", # 22 - "four", # 23 - "five", # 24 - "six", # 25 - "seven", # 26 - "eight", # 27 - "nine", # 28 - "colon", # 29 - "semicolon", # 30 - "less", # 31 - "equal", # 32 - "greater", # 33 - "question", # 34 - "at", # 35 - "A", # 36 - "B", # 37 - "C", # 38 - "D", # 39 - "E", # 40 - "F", # 41 - "G", # 42 - "H", # 43 - "I", # 44 - "J", # 45 - "K", # 46 - "L", # 47 - "M", # 48 - "N", # 49 - "O", # 50 - "P", # 51 - "Q", # 52 - "R", # 53 - "S", # 54 - "T", # 55 - "U", # 56 - "V", # 57 - "W", # 58 - "X", # 59 - "Y", # 60 - "Z", # 61 - "bracketleft", # 62 - "backslash", # 63 - "bracketright", # 64 - "asciicircum", # 65 - "underscore", # 66 - "grave", # 67 - "a", # 68 - "b", # 69 - "c", # 70 - "d", # 71 - "e", # 72 - "f", # 73 - "g", # 74 - "h", # 75 - "i", # 76 - "j", # 77 - "k", # 78 - "l", # 79 - "m", # 80 - "n", # 81 - "o", # 82 - "p", # 83 - "q", # 84 - "r", # 85 - "s", # 86 - "t", # 87 - "u", # 88 - "v", # 89 - "w", # 90 - "x", # 91 - "y", # 92 - "z", # 93 - "braceleft", # 94 - "bar", # 95 - "braceright", # 96 - "asciitilde", # 97 - "Adieresis", # 98 - "Aring", # 99 - "Ccedilla", # 100 - "Eacute", # 101 - "Ntilde", # 102 - "Odieresis", # 103 - "Udieresis", # 104 - "aacute", # 105 - "agrave", # 106 - "acircumflex", # 107 - "adieresis", # 108 - "atilde", # 109 - "aring", # 110 - "ccedilla", # 111 - "eacute", # 112 - "egrave", # 113 - "ecircumflex", # 114 - "edieresis", # 115 - "iacute", # 116 - "igrave", # 117 - "icircumflex", # 118 - "idieresis", # 119 - "ntilde", # 120 - "oacute", # 121 - "ograve", # 122 - "ocircumflex", # 123 - "odieresis", # 124 - "otilde", # 125 - "uacute", # 126 - "ugrave", # 127 - "ucircumflex", # 128 - "udieresis", # 129 - "dagger", # 130 - "degree", # 131 - "cent", # 132 - "sterling", # 133 - "section", # 134 - "bullet", # 135 - "paragraph", # 136 - "germandbls", # 137 - "registered", # 138 - "copyright", # 139 - "trademark", # 140 - "acute", # 141 - "dieresis", # 142 - "notequal", # 143 - "AE", # 144 - "Oslash", # 145 - "infinity", # 146 - "plusminus", # 147 - "lessequal", # 148 - "greaterequal", # 149 - "yen", # 150 - "mu", # 151 - "partialdiff", # 152 - "summation", # 153 - "product", # 154 - "pi", # 155 - "integral", # 156 - "ordfeminine", # 157 - "ordmasculine", # 158 - "Omega", # 159 - "ae", # 160 - "oslash", # 161 - "questiondown", # 162 - "exclamdown", # 163 - "logicalnot", # 164 - "radical", # 165 - "florin", # 166 - "approxequal", # 167 - "Delta", # 168 - "guillemotleft", # 169 - "guillemotright", # 170 - "ellipsis", # 171 - "nonbreakingspace", # 172 - "Agrave", # 173 - "Atilde", # 174 - "Otilde", # 175 - "OE", # 176 - "oe", # 177 - "endash", # 178 - "emdash", # 179 - "quotedblleft", # 180 - "quotedblright", # 181 - "quoteleft", # 182 - "quoteright", # 183 - "divide", # 184 - "lozenge", # 185 - "ydieresis", # 186 - "Ydieresis", # 187 + ".notdef", # 0 + ".null", # 1 + "nonmarkingreturn", # 2 + "space", # 3 + "exclam", # 4 + "quotedbl", # 5 + "numbersign", # 6 + "dollar", # 7 + "percent", # 8 + "ampersand", # 9 + "quotesingle", # 10 + "parenleft", # 11 + "parenright", # 12 + "asterisk", # 13 + "plus", # 14 + "comma", # 15 + "hyphen", # 16 + "period", # 17 + "slash", # 18 + "zero", # 19 + "one", # 20 + "two", # 21 + "three", # 22 + "four", # 23 + "five", # 24 + "six", # 25 + "seven", # 26 + "eight", # 27 + "nine", # 28 + "colon", # 29 + "semicolon", # 30 + "less", # 31 + "equal", # 32 + "greater", # 33 + "question", # 34 + "at", # 35 + "A", # 36 + "B", # 37 + "C", # 38 + "D", # 39 + "E", # 40 + "F", # 41 + "G", # 42 + "H", # 43 + "I", # 44 + "J", # 45 + "K", # 46 + "L", # 47 + "M", # 48 + "N", # 49 + "O", # 50 + "P", # 51 + "Q", # 52 + "R", # 53 + "S", # 54 + "T", # 55 + "U", # 56 + "V", # 57 + "W", # 58 + "X", # 59 + "Y", # 60 + "Z", # 61 + "bracketleft", # 62 + "backslash", # 63 + "bracketright", # 64 + "asciicircum", # 65 + "underscore", # 66 + "grave", # 67 + "a", # 68 + "b", # 69 + "c", # 70 + "d", # 71 + "e", # 72 + "f", # 73 + "g", # 74 + "h", # 75 + "i", # 76 + "j", # 77 + "k", # 78 + "l", # 79 + "m", # 80 + "n", # 81 + "o", # 82 + "p", # 83 + "q", # 84 + "r", # 85 + "s", # 86 + "t", # 87 + "u", # 88 + "v", # 89 + "w", # 90 + "x", # 91 + "y", # 92 + "z", # 93 + "braceleft", # 94 + "bar", # 95 + "braceright", # 96 + "asciitilde", # 97 + "Adieresis", # 98 + "Aring", # 99 + "Ccedilla", # 100 + "Eacute", # 101 + "Ntilde", # 102 + "Odieresis", # 103 + "Udieresis", # 104 + "aacute", # 105 + "agrave", # 106 + "acircumflex", # 107 + "adieresis", # 108 + "atilde", # 109 + "aring", # 110 + "ccedilla", # 111 + "eacute", # 112 + "egrave", # 113 + "ecircumflex", # 114 + "edieresis", # 115 + "iacute", # 116 + "igrave", # 117 + "icircumflex", # 118 + "idieresis", # 119 + "ntilde", # 120 + "oacute", # 121 + "ograve", # 122 + "ocircumflex", # 123 + "odieresis", # 124 + "otilde", # 125 + "uacute", # 126 + "ugrave", # 127 + "ucircumflex", # 128 + "udieresis", # 129 + "dagger", # 130 + "degree", # 131 + "cent", # 132 + "sterling", # 133 + "section", # 134 + "bullet", # 135 + "paragraph", # 136 + "germandbls", # 137 + "registered", # 138 + "copyright", # 139 + "trademark", # 140 + "acute", # 141 + "dieresis", # 142 + "notequal", # 143 + "AE", # 144 + "Oslash", # 145 + "infinity", # 146 + "plusminus", # 147 + "lessequal", # 148 + "greaterequal", # 149 + "yen", # 150 + "mu", # 151 + "partialdiff", # 152 + "summation", # 153 + "product", # 154 + "pi", # 155 + "integral", # 156 + "ordfeminine", # 157 + "ordmasculine", # 158 + "Omega", # 159 + "ae", # 160 + "oslash", # 161 + "questiondown", # 162 + "exclamdown", # 163 + "logicalnot", # 164 + "radical", # 165 + "florin", # 166 + "approxequal", # 167 + "Delta", # 168 + "guillemotleft", # 169 + "guillemotright", # 170 + "ellipsis", # 171 + "nonbreakingspace", # 172 + "Agrave", # 173 + "Atilde", # 174 + "Otilde", # 175 + "OE", # 176 + "oe", # 177 + "endash", # 178 + "emdash", # 179 + "quotedblleft", # 180 + "quotedblright", # 181 + "quoteleft", # 182 + "quoteright", # 183 + "divide", # 184 + "lozenge", # 185 + "ydieresis", # 186 + "Ydieresis", # 187 "fraction", # 188 "currency", # 189 - "guilsinglleft", # 190 - "guilsinglright", # 191 - "fi", # 192 - "fl", # 193 - "daggerdbl", # 194 - "periodcentered", # 195 - "quotesinglbase", # 196 - "quotedblbase", # 197 - "perthousand", # 198 - "Acircumflex", # 199 - "Ecircumflex", # 200 - "Aacute", # 201 - "Edieresis", # 202 - "Egrave", # 203 - "Iacute", # 204 - "Icircumflex", # 205 - "Idieresis", # 206 - "Igrave", # 207 - "Oacute", # 208 - "Ocircumflex", # 209 - "apple", # 210 - "Ograve", # 211 - "Uacute", # 212 - "Ucircumflex", # 213 - "Ugrave", # 214 - "dotlessi", # 215 - "circumflex", # 216 - "tilde", # 217 - "macron", # 218 - "breve", # 219 - "dotaccent", # 220 - "ring", # 221 - "cedilla", # 222 - "hungarumlaut", # 223 - "ogonek", # 224 - "caron", # 225 - "Lslash", # 226 - "lslash", # 227 - "Scaron", # 228 - "scaron", # 229 - "Zcaron", # 230 - "zcaron", # 231 - "brokenbar", # 232 - "Eth", # 233 - "eth", # 234 - "Yacute", # 235 - "yacute", # 236 - "Thorn", # 237 - "thorn", # 238 - "minus", # 239 - "multiply", # 240 - "onesuperior", # 241 - "twosuperior", # 242 - "threesuperior", # 243 - "onehalf", # 244 - "onequarter", # 245 - "threequarters", # 246 - "franc", # 247 - "Gbreve", # 248 - "gbreve", # 249 - "Idotaccent", # 250 - "Scedilla", # 251 - "scedilla", # 252 - "Cacute", # 253 - "cacute", # 254 - "Ccaron", # 255 - "ccaron", # 256 - "dcroat" # 257 + "guilsinglleft", # 190 + "guilsinglright", # 191 + "fi", # 192 + "fl", # 193 + "daggerdbl", # 194 + "periodcentered", # 195 + "quotesinglbase", # 196 + "quotedblbase", # 197 + "perthousand", # 198 + "Acircumflex", # 199 + "Ecircumflex", # 200 + "Aacute", # 201 + "Edieresis", # 202 + "Egrave", # 203 + "Iacute", # 204 + "Icircumflex", # 205 + "Idieresis", # 206 + "Igrave", # 207 + "Oacute", # 208 + "Ocircumflex", # 209 + "apple", # 210 + "Ograve", # 211 + "Uacute", # 212 + "Ucircumflex", # 213 + "Ugrave", # 214 + "dotlessi", # 215 + "circumflex", # 216 + "tilde", # 217 + "macron", # 218 + "breve", # 219 + "dotaccent", # 220 + "ring", # 221 + "cedilla", # 222 + "hungarumlaut", # 223 + "ogonek", # 224 + "caron", # 225 + "Lslash", # 226 + "lslash", # 227 + "Scaron", # 228 + "scaron", # 229 + "Zcaron", # 230 + "zcaron", # 231 + "brokenbar", # 232 + "Eth", # 233 + "eth", # 234 + "Yacute", # 235 + "yacute", # 236 + "Thorn", # 237 + "thorn", # 238 + "minus", # 239 + "multiply", # 240 + "onesuperior", # 241 + "twosuperior", # 242 + "threesuperior", # 243 + "onehalf", # 244 + "onequarter", # 245 + "threequarters", # 246 + "franc", # 247 + "Gbreve", # 248 + "gbreve", # 249 + "Idotaccent", # 250 + "Scedilla", # 251 + "scedilla", # 252 + "Cacute", # 253 + "cacute", # 254 + "Ccaron", # 255 + "ccaron", # 256 + "dcroat" # 257 ] diff --git a/Lib/fontTools/ttLib/tables/B_A_S_E_.py b/Lib/fontTools/ttLib/tables/B_A_S_E_.py index 9551e2c..14906b4 100644 --- a/Lib/fontTools/ttLib/tables/B_A_S_E_.py +++ b/Lib/fontTools/ttLib/tables/B_A_S_E_.py @@ -1,3 +1,5 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * from .otBase import BaseTTXConverter diff --git a/Lib/fontTools/ttLib/tables/BitmapGlyphMetrics.py b/Lib/fontTools/ttLib/tables/BitmapGlyphMetrics.py index dfe86f2..685979a 100644 --- a/Lib/fontTools/ttLib/tables/BitmapGlyphMetrics.py +++ b/Lib/fontTools/ttLib/tables/BitmapGlyphMetrics.py @@ -4,8 +4,11 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.misc import sstruct from fontTools.misc.textTools import safeEval +import logging +log = logging.getLogger(__name__) + bigGlyphMetricsFormat = """ > # big endian height: B @@ -48,11 +51,11 @@ class BitmapGlyphMetrics(object): if name in metricNames: vars(self)[name] = safeEval(attrs['value']) else: - print("Warning: unknown name '%s' being ignored in %s." % name, self.__class__.__name__) + log.warning("unknown name '%s' being ignored in %s.", name, self.__class__.__name__) class BigGlyphMetrics(BitmapGlyphMetrics): binaryFormat = bigGlyphMetricsFormat - + class SmallGlyphMetrics(BitmapGlyphMetrics): binaryFormat = smallGlyphMetricsFormat diff --git a/Lib/fontTools/ttLib/tables/C_B_L_C_.py b/Lib/fontTools/ttLib/tables/C_B_L_C_.py index 2f78571..3d67dd0 100644 --- a/Lib/fontTools/ttLib/tables/C_B_L_C_.py +++ b/Lib/fontTools/ttLib/tables/C_B_L_C_.py @@ -2,6 +2,8 @@ # # Google Author(s): Matt Fontaine +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * from . import E_B_L_C_ class table_C_B_L_C_(E_B_L_C_.table_E_B_L_C_): diff --git a/Lib/fontTools/ttLib/tables/C_F_F_.py b/Lib/fontTools/ttLib/tables/C_F_F_.py index 8167fdf..f175d5c 100644 --- a/Lib/fontTools/ttLib/tables/C_F_F_.py +++ b/Lib/fontTools/ttLib/tables/C_F_F_.py @@ -5,44 +5,43 @@ from . import DefaultTable class table_C_F_F_(DefaultTable.DefaultTable): - - def __init__(self, tag): + + def __init__(self, tag=None): DefaultTable.DefaultTable.__init__(self, tag) self.cff = cffLib.CFFFontSet() self._gaveGlyphOrder = False - + def decompile(self, data, otFont): - self.cff.decompile(StringIO(data), otFont) + self.cff.decompile(BytesIO(data), otFont, isCFF2=False) assert len(self.cff) == 1, "can't deal with multi-font CFF tables." - + def compile(self, otFont): - f = StringIO() - self.cff.compile(f, otFont) + f = BytesIO() + self.cff.compile(f, otFont, isCFF2=False) return f.getvalue() - + def haveGlyphNames(self): if hasattr(self.cff[self.cff.fontNames[0]], "ROS"): return False # CID-keyed font else: return True - + def getGlyphOrder(self): if self._gaveGlyphOrder: from fontTools import ttLib raise ttLib.TTLibError("illegal use of getGlyphOrder()") self._gaveGlyphOrder = True return self.cff[self.cff.fontNames[0]].getGlyphOrder() - + def setGlyphOrder(self, glyphOrder): pass # XXX #self.cff[self.cff.fontNames[0]].setGlyphOrder(glyphOrder) - - def toXML(self, writer, otFont, progress=None): - self.cff.toXML(writer, progress) - + + def toXML(self, writer, otFont): + self.cff.toXML(writer) + def fromXML(self, name, attrs, content, otFont): if not hasattr(self, "cff"): self.cff = cffLib.CFFFontSet() - self.cff.fromXML(name, attrs, content) - + self.cff.fromXML(name, attrs, content, otFont) diff --git a/Lib/fontTools/ttLib/tables/C_F_F__2.py b/Lib/fontTools/ttLib/tables/C_F_F__2.py new file mode 100644 index 0000000..7e30c8f --- /dev/null +++ b/Lib/fontTools/ttLib/tables/C_F_F__2.py @@ -0,0 +1,16 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools import cffLib +from fontTools.ttLib.tables.C_F_F_ import table_C_F_F_ + + +class table_C_F_F__2(table_C_F_F_): + + def decompile(self, data, otFont): + self.cff.decompile(BytesIO(data), otFont, isCFF2=True) + assert len(self.cff) == 1, "can't deal with multi-font CFF tables." + + def compile(self, otFont): + f = BytesIO() + self.cff.compile(f, otFont, isCFF2=True) + return f.getvalue() diff --git a/Lib/fontTools/ttLib/tables/C_O_L_R_.py b/Lib/fontTools/ttLib/tables/C_O_L_R_.py index 139de3c..743fa91 100644 --- a/Lib/fontTools/ttLib/tables/C_O_L_R_.py +++ b/Lib/fontTools/ttLib/tables/C_O_L_R_.py @@ -51,7 +51,6 @@ class table_C_O_L_R_(DefaultTable.DefaultTable): list(map(operator.setitem, [colorLayerLists]*numBaseGlyphRecords, names, layerLists)) - def compile(self, ttFont): ordered = [] ttFont.getReverseGlyphMap(rebuild=True) @@ -117,7 +116,6 @@ class table_C_O_L_R_(DefaultTable.DefaultTable): elif "value" in attrs: setattr(self, name, safeEval(attrs["value"])) - def __getitem__(self, glyphSelector): if isinstance(glyphSelector, int): # its a gid, convert to glyph name @@ -125,7 +123,7 @@ class table_C_O_L_R_(DefaultTable.DefaultTable): if glyphSelector not in self.ColorLayers: return None - + return self.ColorLayers[glyphSelector] def __setitem__(self, glyphSelector, value): @@ -143,7 +141,7 @@ class table_C_O_L_R_(DefaultTable.DefaultTable): class LayerRecord(object): - def __init__(self, name = None, colorID = None): + def __init__(self, name=None, colorID=None): self.name = name self.colorID = colorID diff --git a/Lib/fontTools/ttLib/tables/C_P_A_L_.py b/Lib/fontTools/ttLib/tables/C_P_A_L_.py index 7c2721a..25d50a5 100644 --- a/Lib/fontTools/ttLib/tables/C_P_A_L_.py +++ b/Lib/fontTools/ttLib/tables/C_P_A_L_.py @@ -6,14 +6,23 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.misc.textTools import safeEval from . import DefaultTable +import array import struct +import sys class table_C_P_A_L_(DefaultTable.DefaultTable): + def __init__(self, tag=None): + DefaultTable.DefaultTable.__init__(self, tag) + self.palettes = [] + self.paletteTypes = [] + self.paletteLabels = [] + self.paletteEntryLabels = [] + def decompile(self, data, ttFont): self.version, self.numPaletteEntries, numPalettes, numColorRecords, goffsetFirstColorRecord = struct.unpack(">HHHHL", data[:12]) - assert (self.version == 0), "Version of COLR table is higher than I know how to handle" + assert (self.version <= 1), "Version of CPAL table is higher than I know how to handle" self.palettes = [] pos = 12 for i in range(numPalettes): @@ -26,58 +35,209 @@ class table_C_P_A_L_(DefaultTable.DefaultTable): palette.append( Color(*struct.unpack(">BBBB", data[ppos:ppos+4])) ) ppos += 4 self.palettes.append(palette) + if self.version == 0: + offsetToPaletteTypeArray = 0 + offsetToPaletteLabelArray = 0 + offsetToPaletteEntryLabelArray = 0 + else: + pos = 12 + numPalettes * 2 + (offsetToPaletteTypeArray, offsetToPaletteLabelArray, + offsetToPaletteEntryLabelArray) = ( + struct.unpack(">LLL", data[pos:pos+12])) + self.paletteTypes = self._decompileUInt32Array( + data, offsetToPaletteTypeArray, numPalettes) + self.paletteLabels = self._decompileUInt16Array( + data, offsetToPaletteLabelArray, numPalettes) + self.paletteEntryLabels = self._decompileUInt16Array( + data, offsetToPaletteEntryLabelArray, + self.numPaletteEntries) + + def _decompileUInt16Array(self, data, offset, numElements): + if offset == 0: + return [0] * numElements + result = array.array("H", data[offset : offset + 2 * numElements]) + if sys.byteorder != "big": + result.byteswap() + assert len(result) == numElements, result + return result.tolist() + + def _decompileUInt32Array(self, data, offset, numElements): + if offset == 0: + return [0] * numElements + result = array.array("I", data[offset : offset + 4 * numElements]) + if sys.byteorder != "big": + result.byteswap() + assert len(result) == numElements, result + return result.tolist() def compile(self, ttFont): - dataList = [struct.pack(">HHHHL", self.version, self.numPaletteEntries, len(self.palettes), self.numPaletteEntries * len(self.palettes), 12+2*len(self.palettes))] - for i in range(len(self.palettes)): - dataList.append(struct.pack(">H", i*self.numPaletteEntries)) + colorRecordIndices, colorRecords = self._compileColorRecords() + paletteTypes = self._compilePaletteTypes() + paletteLabels = self._compilePaletteLabels() + paletteEntryLabels = self._compilePaletteEntryLabels() + numColorRecords = len(colorRecords) // 4 + offsetToFirstColorRecord = 12 + len(colorRecordIndices) + if self.version >= 1: + offsetToFirstColorRecord += 12 + header = struct.pack(">HHHHL", self.version, + self.numPaletteEntries, len(self.palettes), + numColorRecords, offsetToFirstColorRecord) + if self.version == 0: + dataList = [header, colorRecordIndices, colorRecords] + else: + pos = offsetToFirstColorRecord + len(colorRecords) + if len(paletteTypes) == 0: + offsetToPaletteTypeArray = 0 + else: + offsetToPaletteTypeArray = pos + pos += len(paletteTypes) + if len(paletteLabels) == 0: + offsetToPaletteLabelArray = 0 + else: + offsetToPaletteLabelArray = pos + pos += len(paletteLabels) + if len(paletteEntryLabels) == 0: + offsetToPaletteEntryLabelArray = 0 + else: + offsetToPaletteEntryLabelArray = pos + pos += len(paletteLabels) + header1 = struct.pack(">LLL", + offsetToPaletteTypeArray, + offsetToPaletteLabelArray, + offsetToPaletteEntryLabelArray) + dataList = [header, colorRecordIndices, header1, + colorRecords, paletteTypes, paletteLabels, + paletteEntryLabels] + return bytesjoin(dataList) + + def _compilePalette(self, palette): + assert(len(palette) == self.numPaletteEntries) + pack = lambda c: struct.pack(">BBBB", c.blue, c.green, c.red, c.alpha) + return bytesjoin([pack(color) for color in palette]) + + def _compileColorRecords(self): + colorRecords, colorRecordIndices, pool = [], [], {} for palette in self.palettes: - assert(len(palette) == self.numPaletteEntries) - for color in palette: - dataList.append(struct.pack(">BBBB", color.blue,color.green,color.red,color.alpha)) - data = bytesjoin(dataList) - return data + packedPalette = self._compilePalette(palette) + if packedPalette in pool: + index = pool[packedPalette] + else: + index = len(colorRecords) + colorRecords.append(packedPalette) + pool[packedPalette] = index + colorRecordIndices.append(struct.pack(">H", index * self.numPaletteEntries)) + return bytesjoin(colorRecordIndices), bytesjoin(colorRecords) + + def _compilePaletteTypes(self): + if self.version == 0 or not any(self.paletteTypes): + return b'' + assert len(self.paletteTypes) == len(self.palettes) + result = bytesjoin([struct.pack(">I", ptype) + for ptype in self.paletteTypes]) + assert len(result) == 4 * len(self.palettes) + return result + + def _compilePaletteLabels(self): + if self.version == 0 or not any(self.paletteLabels): + return b'' + assert len(self.paletteLabels) == len(self.palettes) + result = bytesjoin([struct.pack(">H", label) + for label in self.paletteLabels]) + assert len(result) == 2 * len(self.palettes) + return result + + def _compilePaletteEntryLabels(self): + if self.version == 0 or not any(self.paletteEntryLabels): + return b'' + assert len(self.paletteEntryLabels) == self.numPaletteEntries + result = bytesjoin([struct.pack(">H", label) + for label in self.paletteEntryLabels]) + assert len(result) == 2 * self.numPaletteEntries + return result def toXML(self, writer, ttFont): + numPalettes = len(self.palettes) + paletteLabels = {i: nameID + for (i, nameID) in enumerate(self.paletteLabels)} + paletteTypes = {i: typ for (i, typ) in enumerate(self.paletteTypes)} writer.simpletag("version", value=self.version) writer.newline() - writer.simpletag("numPaletteEntries", value=self.numPaletteEntries) + writer.simpletag("numPaletteEntries", + value=self.numPaletteEntries) writer.newline() for index, palette in enumerate(self.palettes): - writer.begintag("palette", index=index) + attrs = {"index": index} + paletteType = paletteTypes.get(index) + paletteLabel = paletteLabels.get(index) + if self.version > 0 and paletteLabel is not None: + attrs["label"] = paletteLabel + if self.version > 0 and paletteType is not None: + attrs["type"] = paletteType + writer.begintag("palette", **attrs) writer.newline() + if (self.version > 0 and paletteLabel and + ttFont and "name" in ttFont): + name = ttFont["name"].getDebugName(paletteLabel) + if name is not None: + writer.comment(name) + writer.newline() assert(len(palette) == self.numPaletteEntries) for cindex, color in enumerate(palette): color.toXML(writer, ttFont, cindex) writer.endtag("palette") writer.newline() + if self.version > 0 and any(self.paletteEntryLabels): + writer.begintag("paletteEntryLabels") + writer.newline() + for index, label in enumerate(self.paletteEntryLabels): + if label: + writer.simpletag("label", index=index, value=label) + if (self.version > 0 and label and ttFont and "name" in ttFont): + name = ttFont["name"].getDebugName(label) + if name is not None: + writer.comment(name) + writer.newline() + writer.endtag("paletteEntryLabels") + writer.newline() def fromXML(self, name, attrs, content, ttFont): - if not hasattr(self, "palettes"): - self.palettes = [] if name == "palette": - palette = [] - for element in content: - if isinstance(element, basestring): - continue + self.paletteLabels.append(int(attrs.get("label", "0"))) + self.paletteTypes.append(int(attrs.get("type", "0"))) palette = [] for element in content: if isinstance(element, basestring): continue color = Color() color.fromXML(element[0], element[1], element[2], ttFont) - palette.append (color) + palette.append(color) self.palettes.append(palette) + elif name == "paletteEntryLabels": + colorLabels = {} + for element in content: + if isinstance(element, basestring): + continue + elementName, elementAttr, _ = element + if elementName == "label": + labelIndex = safeEval(elementAttr["index"]) + nameID = safeEval(elementAttr["value"]) + colorLabels[labelIndex] = nameID + self.paletteEntryLabels = [ + colorLabels.get(i, 0) + for i in range(self.numPaletteEntries)] elif "value" in attrs: - value = safeEval(attrs["value"]) + value = safeEval(attrs["value"]) setattr(self, name, value) + if name == "numPaletteEntries": + self.paletteEntryLabels = [0] * self.numPaletteEntries + class Color(object): def __init__(self, blue=None, green=None, red=None, alpha=None): - self.blue = blue + self.blue = blue self.green = green - self.red = red + self.red = red self.alpha = alpha def hex(self): @@ -94,7 +254,7 @@ class Color(object): value = attrs["value"] if value[0] == '#': value = value[1:] - self.red = int(value[0:2], 16) + self.red = int(value[0:2], 16) self.green = int(value[2:4], 16) - self.blue = int(value[4:6], 16) + self.blue = int(value[4:6], 16) self.alpha = int(value[6:8], 16) if len (value) >= 8 else 0xFF diff --git a/Lib/fontTools/ttLib/tables/D_S_I_G_.py b/Lib/fontTools/ttLib/tables/D_S_I_G_.py index 7794bda..af802b9 100644 --- a/Lib/fontTools/ttLib/tables/D_S_I_G_.py +++ b/Lib/fontTools/ttLib/tables/D_S_I_G_.py @@ -40,7 +40,7 @@ DSIG_SignatureBlockFormat = """ # class table_D_S_I_G_(DefaultTable.DefaultTable): - + def decompile(self, data, ttFont): dummy, newData = sstruct.unpack2(DSIG_HeaderFormat, data, self) assert self.ulVersion == 1, "DSIG ulVersion must be 1" @@ -55,7 +55,7 @@ class table_D_S_I_G_(DefaultTable.DefaultTable): assert sigrec.usReserved1 == 0, "DSIG signature record #%d usReserverd1 must be 0" % n assert sigrec.usReserved2 == 0, "DSIG signature record #%d usReserverd2 must be 0" % n sigrec.pkcs7 = newData[:sigrec.cbSignature] - + def compile(self, ttFont): packed = sstruct.pack(DSIG_HeaderFormat, self) headers = [packed] @@ -76,7 +76,7 @@ class table_D_S_I_G_(DefaultTable.DefaultTable): # Pad to even bytes data.append(b'\0') return bytesjoin(headers+data) - + def toXML(self, xmlWriter, ttFont): xmlWriter.comment("note that the Digital Signature will be invalid after recompilation!") xmlWriter.newline() @@ -85,7 +85,7 @@ class table_D_S_I_G_(DefaultTable.DefaultTable): xmlWriter.newline() sigrec.toXML(xmlWriter, ttFont) xmlWriter.newline() - + def fromXML(self, name, attrs, content, ttFont): if name == "tableHeader": self.signatureRecords = [] @@ -115,7 +115,7 @@ def b64encode(b): class SignatureRecord(object): def __repr__(self): return "<%s: %s>" % (self.__class__.__name__, self.__dict__) - + def toXML(self, writer, ttFont): writer.begintag(self.__class__.__name__, format=self.ulFormat) writer.newline() @@ -123,7 +123,7 @@ class SignatureRecord(object): writer.write_noindent(b64encode(self.pkcs7)) writer.write_noindent("-----END PKCS7-----\n") writer.endtag(self.__class__.__name__) - + def fromXML(self, name, attrs, content, ttFont): self.ulFormat = safeEval(attrs["format"]) self.usReserved1 = safeEval(attrs.get("reserved1", "0")) diff --git a/Lib/fontTools/ttLib/tables/DefaultTable.py b/Lib/fontTools/ttLib/tables/DefaultTable.py index 3a6886c..f0e82f5 100644 --- a/Lib/fontTools/ttLib/tables/DefaultTable.py +++ b/Lib/fontTools/ttLib/tables/DefaultTable.py @@ -3,21 +3,21 @@ from fontTools.misc.py23 import * from fontTools.ttLib import getClassTag class DefaultTable(object): - + dependencies = [] - + def __init__(self, tag=None): if tag is None: tag = getClassTag(self.__class__) self.tableTag = Tag(tag) - + def decompile(self, data, ttFont): self.data = data - + def compile(self, ttFont): return self.data - - def toXML(self, writer, ttFont, progress=None): + + def toXML(self, writer, ttFont): if hasattr(self, "ERROR"): writer.comment("An error occurred during the decompilation of this table") writer.newline() @@ -28,20 +28,22 @@ class DefaultTable(object): writer.dumphex(self.compile(ttFont)) writer.endtag("hexdata") writer.newline() - + def fromXML(self, name, attrs, content, ttFont): from fontTools.misc.textTools import readHex from fontTools import ttLib if name != "hexdata": raise ttLib.TTLibError("can't handle '%s' element" % name) self.decompile(readHex(content), ttFont) - + def __repr__(self): return "<'%s' table at %x>" % (self.tableTag, id(self)) - - def __ne__(self, other): - return not self.__eq__(other) + def __eq__(self, other): if type(self) != type(other): return NotImplemented return self.__dict__ == other.__dict__ + + def __ne__(self, other): + result = self.__eq__(other) + return result if result is NotImplemented else not result diff --git a/Lib/fontTools/ttLib/tables/E_B_D_T_.py b/Lib/fontTools/ttLib/tables/E_B_D_T_.py index f119291..3a316e5 100644 --- a/Lib/fontTools/ttLib/tables/E_B_D_T_.py +++ b/Lib/fontTools/ttLib/tables/E_B_D_T_.py @@ -7,6 +7,10 @@ from . import DefaultTable import itertools import os import struct +import logging + + +log = logging.getLogger(__name__) ebdtTableVersionFormat = """ > # big endian @@ -158,7 +162,7 @@ class table_E_B_D_T_(DefaultTable.DefaultTable): continue name, attrs, content = element if name[4:].startswith(_bitmapGlyphSubclassPrefix[4:]): - imageFormat = safeEval(name[len(_bitmapGlyphSubclassPrefix):]) + imageFormat = safeEval(name[len(_bitmapGlyphSubclassPrefix):]) glyphName = attrs['name'] imageFormatClass = self.getImageFormatClass(imageFormat) curGlyph = imageFormatClass(None, None) @@ -166,7 +170,7 @@ class table_E_B_D_T_(DefaultTable.DefaultTable): assert glyphName not in bitmapGlyphDict, "Duplicate glyphs with the same name '%s' in the same strike." % glyphName bitmapGlyphDict[glyphName] = curGlyph else: - print("Warning: %s being ignored by %s", name, self.__class__.__name__) + log.warning("%s being ignored by %s", name, self.__class__.__name__) # Grow the strike data array to the appropriate size. The XML # format allows the strike index value to be out of order. @@ -196,7 +200,7 @@ class EbdtComponent(object): if name in componentNames: vars(self)[name] = safeEval(attrs['value']) else: - print("Warning: unknown name '%s' being ignored by EbdtComponent." % name) + log.warning("unknown name '%s' being ignored by EbdtComponent.", name) # Helper functions for dealing with binary. @@ -338,15 +342,20 @@ def _readBitwiseImageData(bitmapObject, name, attrs, content, ttFont): bitmapObject.setRows(dataRows, bitDepth=bitDepth, metrics=metrics, reverseBytes=True) def _writeExtFileImageData(strikeIndex, glyphName, bitmapObject, writer, ttFont): - folder = 'bitmaps/' + try: + folder = os.path.dirname(writer.file.name) + except AttributeError: + # fall back to current directory if output file's directory isn't found + folder = '.' + folder = os.path.join(folder, 'bitmaps') filename = glyphName + bitmapObject.fileExtension if not os.path.isdir(folder): os.makedirs(folder) - folder += 'strike%d/' % strikeIndex + folder = os.path.join(folder, 'strike%d' % strikeIndex) if not os.path.isdir(folder): os.makedirs(folder) - fullPath = folder + filename + fullPath = os.path.join(folder, filename) writer.simpletag('extfileimagedata', value=fullPath) writer.newline() @@ -373,10 +382,10 @@ class BitmapGlyph(object): # Keep track of reading and writing of various forms. xmlDataFunctions = { - 'raw': (_writeRawImageData, _readRawImageData), - 'row': (_writeRowImageData, _readRowImageData), - 'bitwise': (_writeBitwiseImageData, _readBitwiseImageData), - 'extfile': (_writeExtFileImageData, _readExtFileImageData), + 'raw': (_writeRawImageData, _readRawImageData), + 'row': (_writeRowImageData, _readRowImageData), + 'bitwise': (_writeBitwiseImageData, _readBitwiseImageData), + 'extfile': (_writeExtFileImageData, _readExtFileImageData), } def __init__(self, data, ttFont): @@ -423,7 +432,7 @@ class BitmapGlyph(object): # Chop off 'imagedata' from the tag to get just the option. option = name[:-len('imagedata')] assert option in self.__class__.xmlDataFunctions - self.readData(name, attrs, content, ttFont) + self.readData(name, attr, content, ttFont) # Some of the glyphs have the metrics. This allows for metrics to be # added if the glyph format has them. Default behavior is to do nothing. @@ -473,7 +482,7 @@ def _createBitmapPlusMetricsMixin(metricsClass): self.metrics = metricsClass() self.metrics.fromXML(name, attrs, content, ttFont) elif name == oppositeMetricsName: - print("Warning: %s being ignored in format %d." % oppositeMetricsName, self.getFormat()) + log.warning("Warning: %s being ignored in format %d.", oppositeMetricsName, self.getFormat()) return BitmapPlusMetricsMixin @@ -687,7 +696,7 @@ class ComponentBitmapGlyph(BitmapGlyph): curComponent.fromXML(name, attrs, content, ttFont) self.componentArray.append(curComponent) else: - print("Warning: '%s' being ignored in component array." % name) + log.warning("'%s' being ignored in component array.", name) class ebdt_bitmap_format_8(BitmapPlusSmallMetricsMixin, ComponentBitmapGlyph): diff --git a/Lib/fontTools/ttLib/tables/E_B_L_C_.py b/Lib/fontTools/ttLib/tables/E_B_L_C_.py index 28a2635..0c53e7d 100644 --- a/Lib/fontTools/ttLib/tables/E_B_L_C_.py +++ b/Lib/fontTools/ttLib/tables/E_B_L_C_.py @@ -7,6 +7,10 @@ from .BitmapGlyphMetrics import BigGlyphMetrics, bigGlyphMetricsFormat, SmallGly import struct import itertools from collections import deque +import logging + + +log = logging.getLogger(__name__) eblcHeaderFormat = """ > # big endian @@ -71,44 +75,47 @@ class table_E_B_L_C_(DefaultTable.DefaultTable): # Save the original data because offsets are from the start of the table. origData = data + i = 0; - dummy, data = sstruct.unpack2(eblcHeaderFormat, data, self) + dummy = sstruct.unpack(eblcHeaderFormat, data[:8], self) + i += 8; self.strikes = [] for curStrikeIndex in range(self.numSizes): curStrike = Strike() self.strikes.append(curStrike) curTable = curStrike.bitmapSizeTable - dummy, data = sstruct.unpack2(bitmapSizeTableFormatPart1, data, curTable) + dummy = sstruct.unpack2(bitmapSizeTableFormatPart1, data[i:i+16], curTable) + i += 16 for metric in ('hori', 'vert'): metricObj = SbitLineMetrics() vars(curTable)[metric] = metricObj - dummy, data = sstruct.unpack2(sbitLineMetricsFormat, data, metricObj) - dummy, data = sstruct.unpack2(bitmapSizeTableFormatPart2, data, curTable) + dummy = sstruct.unpack2(sbitLineMetricsFormat, data[i:i+12], metricObj) + i += 12 + dummy = sstruct.unpack(bitmapSizeTableFormatPart2, data[i:i+8], curTable) + i += 8 for curStrike in self.strikes: curTable = curStrike.bitmapSizeTable for subtableIndex in range(curTable.numberOfIndexSubTables): - lowerBound = curTable.indexSubTableArrayOffset + subtableIndex * indexSubTableArraySize - upperBound = lowerBound + indexSubTableArraySize - data = origData[lowerBound:upperBound] + i = curTable.indexSubTableArrayOffset + subtableIndex * indexSubTableArraySize - tup = struct.unpack(indexSubTableArrayFormat, data) + tup = struct.unpack(indexSubTableArrayFormat, data[i:i+indexSubTableArraySize]) (firstGlyphIndex, lastGlyphIndex, additionalOffsetToIndexSubtable) = tup - offsetToIndexSubTable = curTable.indexSubTableArrayOffset + additionalOffsetToIndexSubtable - data = origData[offsetToIndexSubTable:] + i = curTable.indexSubTableArrayOffset + additionalOffsetToIndexSubtable - tup = struct.unpack(indexSubHeaderFormat, data[:indexSubHeaderSize]) + tup = struct.unpack(indexSubHeaderFormat, data[i:i+indexSubHeaderSize]) (indexFormat, imageFormat, imageDataOffset) = tup indexFormatClass = self.getIndexFormatClass(indexFormat) - indexSubTable = indexFormatClass(data[indexSubHeaderSize:], ttFont) + indexSubTable = indexFormatClass(data[i+indexSubHeaderSize:], ttFont) indexSubTable.firstGlyphIndex = firstGlyphIndex indexSubTable.lastGlyphIndex = lastGlyphIndex indexSubTable.additionalOffsetToIndexSubtable = additionalOffsetToIndexSubtable indexSubTable.indexFormat = indexFormat indexSubTable.imageFormat = imageFormat indexSubTable.imageDataOffset = imageDataOffset + indexSubTable.decompile() # https://github.com/behdad/fonttools/issues/317 curStrike.indexSubTables.append(indexSubTable) def compile(self, ttFont): @@ -293,7 +300,7 @@ class BitmapSizeTable(object): elif name in dataNames: vars(self)[name] = safeEval(attrs['value']) else: - print("Warning: unknown name '%s' being ignored in BitmapSizeTable." % name) + log.warning("unknown name '%s' being ignored in BitmapSizeTable.", name) class SbitLineMetrics(object): @@ -336,7 +343,6 @@ class EblcIndexSubTable(object): if not hasattr(self, "data"): raise AttributeError(attr) self.decompile() - del self.data, self.ttFont return getattr(self, attr) # This method just takes care of the indexSubHeader. Implementing subclasses @@ -439,6 +445,7 @@ def _createOffsetArrayIndexSubTableMixin(formatStringForDataType): self.names = list(map(self.ttFont.getGlyphName, glyphIds)) self.removeSkipGlyphs() + del self.data, self.ttFont def compile(self, ttFont): # First make sure that all the data lines up properly. Formats 1 and 3 @@ -503,7 +510,7 @@ class FixedSizeIndexSubTableMixin(object): self.metrics = BigGlyphMetrics() self.metrics.fromXML(name, attrs, content, ttFont) elif name == SmallGlyphMetrics.__name__: - print("Warning: SmallGlyphMetrics being ignored in format %d." % self.indexFormat) + log.warning("SmallGlyphMetrics being ignored in format %d.", self.indexFormat) def padBitmapData(self, data): # Make sure that the data isn't bigger than the fixed size. @@ -525,12 +532,13 @@ class eblc_index_sub_table_2(FixedSizeIndexSubTableMixin, EblcIndexSubTable): offsets = [self.imageSize * i + self.imageDataOffset for i in range(len(glyphIds)+1)] self.locations = list(zip(offsets, offsets[1:])) self.names = list(map(self.ttFont.getGlyphName, glyphIds)) + del self.data, self.ttFont def compile(self, ttFont): glyphIds = list(map(ttFont.getGlyphID, self.names)) # Make sure all the ids are consecutive. This is required by Format 2. assert glyphIds == list(range(self.firstGlyphIndex, self.lastGlyphIndex+1)), "Format 2 ids must be consecutive." - self.imageDataOffset = min(zip(*self.locations)[0]) + self.imageDataOffset = min(next(iter(zip(*self.locations)))) dataList = [EblcIndexSubTable.compile(self, ttFont)] dataList.append(struct.pack(">L", self.imageSize)) @@ -556,6 +564,7 @@ class eblc_index_sub_table_4(EblcIndexSubTable): offsets = [offset + self.imageDataOffset for offset in offsets] self.locations = list(zip(offsets, offsets[1:])) self.names = list(map(self.ttFont.getGlyphName, glyphIds)) + del self.data, self.ttFont def compile(self, ttFont): # First make sure that all the data lines up properly. Format 4 @@ -594,9 +603,10 @@ class eblc_index_sub_table_5(FixedSizeIndexSubTableMixin, EblcIndexSubTable): offsets = [self.imageSize * i + self.imageDataOffset for i in range(len(glyphIds)+1)] self.locations = list(zip(offsets, offsets[1:])) self.names = list(map(self.ttFont.getGlyphName, glyphIds)) + del self.data, self.ttFont def compile(self, ttFont): - self.imageDataOffset = min(zip(*self.locations)[0]) + self.imageDataOffset = min(next(iter(zip(*self.locations)))) dataList = [EblcIndexSubTable.compile(self, ttFont)] dataList.append(struct.pack(">L", self.imageSize)) dataList.append(sstruct.pack(bigGlyphMetricsFormat, self.metrics)) diff --git a/Lib/fontTools/ttLib/tables/F_F_T_M_.py b/Lib/fontTools/ttLib/tables/F_F_T_M_.py index e8b1d29..3d110bd 100644 --- a/Lib/fontTools/ttLib/tables/F_F_T_M_.py +++ b/Lib/fontTools/ttLib/tables/F_F_T_M_.py @@ -1,10 +1,9 @@ +from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.misc import sstruct from fontTools.misc.textTools import safeEval -from ._h_e_a_d import mac_epoch_diff +from fontTools.misc.timeTools import timestampFromString, timestampToString from . import DefaultTable -import time -import calendar FFTMFormat = """ > # big endian @@ -16,31 +15,28 @@ FFTMFormat = """ class table_F_F_T_M_(DefaultTable.DefaultTable): - def decompile(self, data, ttFont): - dummy, rest = sstruct.unpack2(FFTMFormat, data, self) + def decompile(self, data, ttFont): + dummy, rest = sstruct.unpack2(FFTMFormat, data, self) - def compile(self, ttFont): - data = sstruct.pack(FFTMFormat, self) - return data + def compile(self, ttFont): + data = sstruct.pack(FFTMFormat, self) + return data - def toXML(self, writer, ttFont): - writer.comment("FontForge's timestamp, font source creation and modification dates") - writer.newline() - formatstring, names, fixes = sstruct.getformat(FFTMFormat) - for name in names: - value = getattr(self, name) - if name in ("FFTimeStamp", "sourceCreated", "sourceModified"): - try: - value = time.asctime(time.gmtime(max(0, value + mac_epoch_diff))) - except ValueError: - value = time.asctime(time.gmtime(0)) - writer.simpletag(name, value=value) - writer.newline() + def toXML(self, writer, ttFont): + writer.comment("FontForge's timestamp, font source creation and modification dates") + writer.newline() + formatstring, names, fixes = sstruct.getformat(FFTMFormat) + for name in names: + value = getattr(self, name) + if name in ("FFTimeStamp", "sourceCreated", "sourceModified"): + value = timestampToString(value) + writer.simpletag(name, value=value) + writer.newline() - def fromXML(self, name, attrs, content, ttFont): - value = attrs["value"] - if name in ("FFTimeStamp", "sourceCreated", "sourceModified"): - value = calendar.timegm(time.strptime(value)) - mac_epoch_diff - else: - value = safeEval(value) - setattr(self, name, value) \ No newline at end of file + def fromXML(self, name, attrs, content, ttFont): + value = attrs["value"] + if name in ("FFTimeStamp", "sourceCreated", "sourceModified"): + value = timestampFromString(value) + else: + value = safeEval(value) + setattr(self, name, value) diff --git a/Lib/fontTools/ttLib/tables/F__e_a_t.py b/Lib/fontTools/ttLib/tables/F__e_a_t.py new file mode 100644 index 0000000..22be4f6 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/F__e_a_t.py @@ -0,0 +1,114 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc import sstruct +from fontTools.misc.textTools import safeEval +from .otBase import BaseTTXConverter +from . import DefaultTable +from . import grUtils +import struct + +Feat_hdr_format=''' + > + version: 16.16F +''' + +class table_F__e_a_t(DefaultTable.DefaultTable): + + def __init__(self, tag=None): + DefaultTable.DefaultTable.__init__(self, tag) + self.features = {} + + def decompile(self, data, ttFont): + (_, data) = sstruct.unpack2(Feat_hdr_format, data, self) + numFeats, = struct.unpack('>H', data[:2]) + data = data[8:] + allfeats = [] + maxsetting = 0 + for i in range(numFeats): + if self.version >= 2.0: + (fid, nums, _, offset, flags, lid) = struct.unpack(">LHHLHH", + data[16*i:16*(i+1)]) + offset = int((offset - 12 - 16 * numFeats) / 4) + else: + (fid, nums, offset, flags, lid) = struct.unpack(">HHLHH", + data[12*i:12*(i+1)]) + offset = int((offset - 12 - 12 * numFeats) / 4) + allfeats.append((fid, nums, offset, flags, lid)) + maxsetting = max(maxsetting, offset + nums) + data = data[16*numFeats:] + allsettings = [] + for i in range(maxsetting): + if len(data) >= 4 * (i + 1): + (val, lid) = struct.unpack(">HH", data[4*i:4*(i+1)]) + allsettings.append((val, lid)) + for i,f in enumerate(allfeats): + (fid, nums, offset, flags, lid) = f + fobj = Feature() + fobj.flags = flags + fobj.label = lid + self.features[grUtils.num2tag(fid)] = fobj + fobj.settings = {} + fobj.default = None + fobj.index = i + for i in range(offset, offset + nums): + if i >= len(allsettings): continue + (vid, vlid) = allsettings[i] + fobj.settings[vid] = vlid + if fobj.default is None: + fobj.default = vid + + def compile(self, ttFont): + fdat = "" + vdat = "" + offset = 0 + for f, v in sorted(self.features.items(), key=lambda x:x[1].index): + fnum = grUtils.tag2num(f) + if self.version >= 2.0: + fdat += struct.pack(">LHHLHH", grUtils.tag2num(f), len(v.settings), + 0, offset * 4 + 12 + 16 * len(self.features), v.flags, v.label) + elif fnum > 65535: # self healing for alphabetic ids + self.version = 2.0 + return self.compile(ttFont) + else: + fdat += struct.pack(">HHLHH", grUtils.tag2num(f), len(v.settings), + offset * 4 + 12 + 12 * len(self.features), v.flags, v.label) + for s, l in sorted(v.settings.items(), key=lambda x:(-1, x[1]) if x[0] == v.default else x): + vdat += struct.pack(">HH", s, l) + offset += len(v.settings) + hdr = sstruct.pack(Feat_hdr_format, self) + return hdr + struct.pack('>HHL', len(self.features), 0, 0) + fdat + vdat + + def toXML(self, writer, ttFont): + writer.simpletag('version', version=self.version) + writer.newline() + for f, v in sorted(self.features.items(), key=lambda x:x[1].index): + writer.begintag('feature', fid=f, label=v.label, flags=v.flags, + default=(v.default if v.default else 0)) + writer.newline() + for s, l in sorted(v.settings.items()): + writer.simpletag('setting', value=s, label=l) + writer.newline() + writer.endtag('feature') + writer.newline() + + def fromXML(self, name, attrs, content, ttFont): + if name == 'version': + self.version = float(safeEval(attrs['version'])) + elif name == 'feature': + fid = attrs['fid'] + fobj = Feature() + fobj.flags = int(safeEval(attrs['flags'])) + fobj.label = int(safeEval(attrs['label'])) + fobj.default = int(safeEval(attrs.get('default','0'))) + fobj.index = len(self.features) + self.features[fid] = fobj + fobj.settings = {} + for element in content: + if not isinstance(element, tuple): continue + tag, a, c = element + if tag == 'setting': + fobj.settings[int(safeEval(a['value']))] = int(safeEval(a['label'])) + +class Feature(object): + pass + diff --git a/Lib/fontTools/ttLib/tables/G_D_E_F_.py b/Lib/fontTools/ttLib/tables/G_D_E_F_.py index d4a5741..08faf62 100644 --- a/Lib/fontTools/ttLib/tables/G_D_E_F_.py +++ b/Lib/fontTools/ttLib/tables/G_D_E_F_.py @@ -1,3 +1,5 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * from .otBase import BaseTTXConverter diff --git a/Lib/fontTools/ttLib/tables/G_M_A_P_.py b/Lib/fontTools/ttLib/tables/G_M_A_P_.py index 5db94d9..afa8c4d 100644 --- a/Lib/fontTools/ttLib/tables/G_M_A_P_.py +++ b/Lib/fontTools/ttLib/tables/G_M_A_P_.py @@ -13,7 +13,7 @@ GMAPFormat = """ recordsOffset: H fontNameLength: H """ -# psFontName is a byte string which follows the record above. This is zero padded +# psFontName is a byte string which follows the record above. This is zero padded # to the beginning of the records array. The recordsOffsst is 32 bit aligned. GMAPRecordFormat1 = """ @@ -24,17 +24,16 @@ GMAPRecordFormat1 = """ ggid: H name: 32s """ - class GMAPRecord(object): - def __init__(self, uv = 0, cid = 0, gid = 0, ggid = 0, name = ""): + def __init__(self, uv=0, cid=0, gid=0, ggid=0, name=""): self.UV = uv self.cid = cid self.gid = gid self.ggid = ggid self.name = name - + def toXML(self, writer, ttFont): writer.begintag("GMAPRecord") writer.newline() @@ -51,19 +50,17 @@ class GMAPRecord(object): writer.endtag("GMAPRecord") writer.newline() - def fromXML(self, name, attrs, content, ttFont): value = attrs["value"] if name == "GlyphletName": self.name = value else: setattr(self, name, safeEval(value)) - def compile(self, ttFont): if self.UV is None: self.UV = 0 - nameLen = len(self.name) + nameLen = len(self.name) if nameLen < 32: self.name = self.name + "\0"*(32 - nameLen) data = sstruct.pack(GMAPRecordFormat1, self) @@ -74,9 +71,9 @@ class GMAPRecord(object): class table_G_M_A_P_(DefaultTable.DefaultTable): - + dependencies = [] - + def decompile(self, data, ttFont): dummy, newData = sstruct.unpack2(GMAPFormat, data, self) self.psFontName = tostr(newData[:self.fontNameLength]) @@ -87,19 +84,17 @@ class table_G_M_A_P_(DefaultTable.DefaultTable): gmapRecord, newData = sstruct.unpack2(GMAPRecordFormat1, newData, GMAPRecord()) gmapRecord.name = gmapRecord.name.strip('\0') self.gmapRecords.append(gmapRecord) - def compile(self, ttFont): self.recordsCount = len(self.gmapRecords) self.fontNameLength = len(self.psFontName) - self.recordsOffset = 4 *(((self.fontNameLength + 12) + 3) // 4) + self.recordsOffset = 4 * (((self.fontNameLength + 12) + 3) // 4) data = sstruct.pack(GMAPFormat, self) data = data + tobytes(self.psFontName) data = data + b"\0" * (self.recordsOffset - len(data)) for record in self.gmapRecords: data = data + record.compile(ttFont) return data - def toXML(self, writer, ttFont): writer.comment("Most of this table will be recalculated by the compiler") @@ -113,7 +108,7 @@ class table_G_M_A_P_(DefaultTable.DefaultTable): writer.newline() for gmapRecord in self.gmapRecords: gmapRecord.toXML(writer, ttFont) - + def fromXML(self, name, attrs, content, ttFont): if name == "GMAPRecord": if not hasattr(self, "gmapRecords"): @@ -129,5 +124,5 @@ class table_G_M_A_P_(DefaultTable.DefaultTable): value = attrs["value"] if name == "PSFontName": self.psFontName = value - else: + else: setattr(self, name, safeEval(value)) diff --git a/Lib/fontTools/ttLib/tables/G_P_K_G_.py b/Lib/fontTools/ttLib/tables/G_P_K_G_.py index 4df666f..4e13830 100644 --- a/Lib/fontTools/ttLib/tables/G_P_K_G_.py +++ b/Lib/fontTools/ttLib/tables/G_P_K_G_.py @@ -13,12 +13,12 @@ GPKGFormat = """ numGMAPs: H numGlyplets: H """ -# psFontName is a byte string which follows the record above. This is zero padded +# psFontName is a byte string which follows the record above. This is zero padded # to the beginning of the records array. The recordsOffsst is 32 bit aligned. class table_G_P_K_G_(DefaultTable.DefaultTable): - + def decompile(self, data, ttFont): dummy, newData = sstruct.unpack2(GPKGFormat, data, self) @@ -44,7 +44,6 @@ class table_G_P_K_G_(DefaultTable.DefaultTable): end = glyphletOffsets[i+1] self.glyphlets.append(data[start:end]) - def compile(self, ttFont): self.numGMAPs = len(self.GMAPs) self.numGlyplets = len(self.glyphlets) @@ -75,7 +74,7 @@ class table_G_P_K_G_(DefaultTable.DefaultTable): dataList += self.glyphlets data = bytesjoin(dataList) return data - + def toXML(self, writer, ttFont): writer.comment("Most of this table will be recalculated by the compiler") writer.newline() @@ -126,5 +125,5 @@ class table_G_P_K_G_(DefaultTable.DefaultTable): itemName, itemAttrs, itemContent = element if itemName == "hexdata": self.glyphlets.append(readHex(itemContent)) - else: - setattr(self, name, safeEval(value)) + else: + setattr(self, name, safeEval(attrs["value"])) diff --git a/Lib/fontTools/ttLib/tables/G_P_O_S_.py b/Lib/fontTools/ttLib/tables/G_P_O_S_.py index 013c820..1c36061 100644 --- a/Lib/fontTools/ttLib/tables/G_P_O_S_.py +++ b/Lib/fontTools/ttLib/tables/G_P_O_S_.py @@ -1,3 +1,5 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * from .otBase import BaseTTXConverter diff --git a/Lib/fontTools/ttLib/tables/G_S_U_B_.py b/Lib/fontTools/ttLib/tables/G_S_U_B_.py index 4403649..d23e8ba 100644 --- a/Lib/fontTools/ttLib/tables/G_S_U_B_.py +++ b/Lib/fontTools/ttLib/tables/G_S_U_B_.py @@ -1,3 +1,5 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * from .otBase import BaseTTXConverter diff --git a/Lib/fontTools/ttLib/tables/G__l_a_t.py b/Lib/fontTools/ttLib/tables/G__l_a_t.py new file mode 100644 index 0000000..36ed6df --- /dev/null +++ b/Lib/fontTools/ttLib/tables/G__l_a_t.py @@ -0,0 +1,221 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc import sstruct +from fontTools.misc.textTools import safeEval +from itertools import * +from functools import partial +from . import DefaultTable +from . import grUtils +import struct, operator, warnings +try: + import lz4 +except: + lz4 = None + + +Glat_format_0 = """ + > # big endian + version: 16.16F +""" + +Glat_format_3 = """ + > + version: 16.16F + compression:L # compression scheme or reserved +""" + +Glat_format_1_entry = """ + > + attNum: B # Attribute number of first attribute + num: B # Number of attributes in this run +""" +Glat_format_23_entry = """ + > + attNum: H # Attribute number of first attribute + num: H # Number of attributes in this run +""" + +Glat_format_3_octabox_metrics = """ + > + subboxBitmap: H # Which subboxes exist on 4x4 grid + diagNegMin: B # Defines minimum negatively-sloped diagonal (si) + diagNegMax: B # Defines maximum negatively-sloped diagonal (sa) + diagPosMin: B # Defines minimum positively-sloped diagonal (di) + diagPosMax: B # Defines maximum positively-sloped diagonal (da) +""" + +Glat_format_3_subbox_entry = """ + > + left: B # xi + right: B # xa + bottom: B # yi + top: B # ya + diagNegMin: B # Defines minimum negatively-sloped diagonal (si) + diagNegMax: B # Defines maximum negatively-sloped diagonal (sa) + diagPosMin: B # Defines minimum positively-sloped diagonal (di) + diagPosMax: B # Defines maximum positively-sloped diagonal (da) +""" + +class _Object() : + pass + +class _Dict(dict) : + pass + +class table_G__l_a_t(DefaultTable.DefaultTable): + ''' + Support Graphite Glat tables + ''' + + def __init__(self, tag=None): + DefaultTable.DefaultTable.__init__(self, tag) + self.scheme = 0 + + def decompile(self, data, ttFont): + sstruct.unpack2(Glat_format_0, data, self) + if self.version <= 1.9: + decoder = partial(self.decompileAttributes12,fmt=Glat_format_1_entry) + elif self.version <= 2.9: + decoder = partial(self.decompileAttributes12,fmt=Glat_format_23_entry) + elif self.version >= 3.0: + (data, self.scheme) = grUtils.decompress(data) + sstruct.unpack2(Glat_format_3, data, self) + self.hasOctaboxes = (self.compression & 1) == 1 + decoder = self.decompileAttributes3 + + gloc = ttFont['Gloc'] + self.attributes = {} + count = 0 + for s,e in zip(gloc,gloc[1:]): + self.attributes[ttFont.getGlyphName(count)] = decoder(data[s:e]) + count += 1 + + def decompileAttributes12(self, data, fmt): + attributes = _Dict() + while len(data) > 3: + e, data = sstruct.unpack2(fmt, data, _Object()) + keys = range(e.attNum, e.attNum+e.num) + if len(data) >= 2 * e.num : + vals = struct.unpack_from(('>%dh' % e.num), data) + attributes.update(zip(keys,vals)) + data = data[2*e.num:] + return attributes + + def decompileAttributes3(self, data): + if self.hasOctaboxes: + o, data = sstruct.unpack2(Glat_format_3_octabox_metrics, data, _Object()) + numsub = bin(o.subboxBitmap).count("1") + o.subboxes = [] + for b in range(numsub): + if len(data) >= 8 : + subbox, data = sstruct.unpack2(Glat_format_3_subbox_entry, + data, _Object()) + o.subboxes.append(subbox) + attrs = self.decompileAttributes12(data, Glat_format_23_entry) + if self.hasOctaboxes: + attrs.octabox = o + return attrs + + def compile(self, ttFont): + data = sstruct.pack(Glat_format_0, self) + if self.version <= 1.9: + encoder = partial(self.compileAttributes12, fmt=Glat_format_1_entry) + elif self.version <= 2.9: + encoder = partial(self.compileAttributes12, fmt=Glat_format_1_entry) + elif self.version >= 3.0: + self.compression = (self.scheme << 27) + (1 if self.hasOctaboxes else 0) + data = sstruct.pack(Glat_format_3, self) + encoder = self.compileAttributes3 + + glocs = [] + for n in range(len(self.attributes)): + glocs.append(len(data)) + data += encoder(self.attributes[ttFont.getGlyphName(n)]) + glocs.append(len(data)) + ttFont['Gloc'].set(glocs) + + if self.version >= 3.0: + data = grUtils.compress(self.scheme, data) + return data + + def compileAttributes12(self, attrs, fmt): + data = [] + for e in grUtils.entries(attrs): + data.extend(sstruct.pack(fmt, {'attNum' : e[0], 'num' : e[1]})) + data.extend(struct.pack(('>%dh' % len(e[2])), *e[2])) + return "".join(data) + + def compileAttributes3(self, attrs): + if self.hasOctaboxes: + o = attrs.octabox + data = sstruct.pack(Glat_format_3_octabox_metrics, o) + numsub = bin(o.subboxBitmap).count("1") + for b in range(numsub) : + data += sstruct.pack(Glat_format_3_subbox_entry, o.subboxes[b]) + else: + data = "" + return data + self.compileAttributes12(attrs, Glat_format_23_entry) + + def toXML(self, writer, ttFont): + writer.simpletag('version', version=self.version, compressionScheme=self.scheme) + writer.newline() + for n, a in sorted(self.attributes.items(), key=lambda x:ttFont.getGlyphID(x[0])): + writer.begintag('glyph', name=n) + writer.newline() + if hasattr(a, 'octabox'): + o = a.octabox + formatstring, names, fixes = sstruct.getformat(Glat_format_3_octabox_metrics) + vals = {} + for k in names: + if k == 'subboxBitmap': continue + vals[k] = "{:.3f}%".format(getattr(o, k) * 100. / 256) + vals['bitmap'] = "{:0X}".format(o.subboxBitmap) + writer.begintag('octaboxes', **vals) + writer.newline() + formatstring, names, fixes = sstruct.getformat(Glat_format_3_subbox_entry) + for s in o.subboxes: + vals = {} + for k in names: + vals[k] = "{:.3f}%".format(getattr(s, k) * 100. / 256) + writer.simpletag('octabox', **vals) + writer.newline() + writer.endtag('octaboxes') + writer.newline() + for k, v in sorted(a.items()): + writer.simpletag('attribute', index=k, value=v) + writer.newline() + writer.endtag('glyph') + writer.newline() + + def fromXML(self, name, attrs, content, ttFont): + if name == 'version' : + self.version = float(safeEval(attrs['version'])) + if name != 'glyph' : return + if not hasattr(self, 'attributes'): + self.attributes = {} + gname = attrs['name'] + attributes = _Dict() + for element in content: + if not isinstance(element, tuple): continue + tag, attrs, subcontent = element + if tag == 'attribute' : + k = int(safeEval(attrs['index'])) + v = int(safeEval(attrs['value'])) + attributes[k]=v + elif tag == 'octaboxes': + self.hasOctaboxes = True + o = _Object() + o.subboxBitmap = int(attrs['bitmap'], 16) + o.subboxes = [] + del attrs['bitmap'] + for k, v in attrs.items(): + setattr(o, k, int(float(v[:-1]) * 256. / 100. + 0.5)) + for element in subcontent: + if not isinstance(element, tuple): continue + (tag, attrs, subcontent) = element + so = _Object() + for k, v in attrs.items(): + setattr(so, k, int(float(v[:-1]) * 256. / 100. + 0.5)) + o.subboxes.append(so) + attributes.octabox = o + self.attributes[gname] = attributes diff --git a/Lib/fontTools/ttLib/tables/G__l_o_c.py b/Lib/fontTools/ttLib/tables/G__l_o_c.py new file mode 100644 index 0000000..d77c483 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/G__l_o_c.py @@ -0,0 +1,71 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc import sstruct +from fontTools.misc.textTools import safeEval +from . import DefaultTable +import array + +Gloc_header = ''' + > # big endian + version: 16.16F # Table version + flags: H # bit 0: 1=long format, 0=short format + # bit 1: 1=attribute names, 0=no names + numAttribs: H # NUmber of attributes +''' + +class table_G__l_o_c(DefaultTable.DefaultTable): + """ + Support Graphite Gloc tables + """ + dependencies = ['Glat'] + + def __init__(self, tag=None): + DefaultTable.DefaultTable.__init__(self, tag) + self.attribIds = None + self.numAttribs = 0 + + def decompile(self, data, ttFont): + _, data = sstruct.unpack2(Gloc_header, data, self) + flags = self.flags + del self.flags + self.locations = array.array('I' if flags & 1 else 'H') + self.locations.fromstring(data[:len(data) - self.numAttribs * (flags & 2)]) + self.locations.byteswap() + self.attribIds = array.array('H') + if flags & 2: + self.attribIds.fromstring(data[-self.numAttribs * 2:]) + self.attribIds.byteswap() + + def compile(self, ttFont): + data = sstruct.pack(Gloc_header, dict(version=1.0, + flags=(bool(self.attribIds) << 1) + (self.locations.typecode == 'I'), + numAttribs=self.numAttribs)) + self.locations.byteswap() + data += self.locations.tostring() + self.locations.byteswap() + if self.attribIds: + self.attribIds.byteswap() + data += self.attribIds.tostring() + self.attribIds.byteswap() + return data + + def set(self, locations): + long_format = max(locations) >= 65536 + self.locations = array.array('I' if long_format else 'H', locations) + + def toXML(self, writer, ttFont): + writer.simpletag("attributes", number=self.numAttribs) + writer.newline() + + def fromXML(self, name, attrs, content, ttFont): + if name == 'attributes': + self.numAttribs = int(safeEval(attrs['number'])) + + def __getitem__(self, index): + return self.locations[index] + + def __len__(self): + return len(self.locations) + + def __iter__(self): + return iter(self.locations) diff --git a/Lib/fontTools/ttLib/tables/H_V_A_R_.py b/Lib/fontTools/ttLib/tables/H_V_A_R_.py new file mode 100644 index 0000000..efab4e7 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/H_V_A_R_.py @@ -0,0 +1,7 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from .otBase import BaseTTXConverter + + +class table_H_V_A_R_(BaseTTXConverter): + pass diff --git a/Lib/fontTools/ttLib/tables/J_S_T_F_.py b/Lib/fontTools/ttLib/tables/J_S_T_F_.py index ddf5405..dffd08b 100644 --- a/Lib/fontTools/ttLib/tables/J_S_T_F_.py +++ b/Lib/fontTools/ttLib/tables/J_S_T_F_.py @@ -1,3 +1,5 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * from .otBase import BaseTTXConverter diff --git a/Lib/fontTools/ttLib/tables/L_T_S_H_.py b/Lib/fontTools/ttLib/tables/L_T_S_H_.py index de79236..dd0f195 100644 --- a/Lib/fontTools/ttLib/tables/L_T_S_H_.py +++ b/Lib/fontTools/ttLib/tables/L_T_S_H_.py @@ -10,7 +10,7 @@ import array # XXX back to normal eventually. class table_L_T_S_H_(DefaultTable.DefaultTable): - + def decompile(self, data, ttFont): version, numGlyphs = struct.unpack(">HH", data[:4]) data = data[4:] @@ -23,7 +23,7 @@ class table_L_T_S_H_(DefaultTable.DefaultTable): self.yPels = {} for i in range(numGlyphs): self.yPels[ttFont.getGlyphName(i)] = yPels[i] - + def compile(self, ttFont): version = 0 names = list(self.yPels.keys()) @@ -35,17 +35,16 @@ class table_L_T_S_H_(DefaultTable.DefaultTable): yPels[ttFont.getGlyphID(name)] = self.yPels[name] yPels = array.array("B", yPels) return struct.pack(">HH", version, numGlyphs) + yPels.tostring() - + def toXML(self, writer, ttFont): names = sorted(self.yPels.keys()) for name in names: writer.simpletag("yPel", name=name, value=self.yPels[name]) writer.newline() - + def fromXML(self, name, attrs, content, ttFont): if not hasattr(self, "yPels"): self.yPels = {} if name != "yPel": return # ignore unknown tags self.yPels[attrs["name"]] = safeEval(attrs["value"]) - diff --git a/Lib/fontTools/ttLib/tables/M_A_T_H_.py b/Lib/fontTools/ttLib/tables/M_A_T_H_.py index d894c08..8c329ba 100644 --- a/Lib/fontTools/ttLib/tables/M_A_T_H_.py +++ b/Lib/fontTools/ttLib/tables/M_A_T_H_.py @@ -1,3 +1,5 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * from .otBase import BaseTTXConverter diff --git a/Lib/fontTools/ttLib/tables/M_E_T_A_.py b/Lib/fontTools/ttLib/tables/M_E_T_A_.py index 60214e8..3eb6550 100644 --- a/Lib/fontTools/ttLib/tables/M_E_T_A_.py +++ b/Lib/fontTools/ttLib/tables/M_E_T_A_.py @@ -3,6 +3,7 @@ from fontTools.misc.py23 import * from fontTools.misc import sstruct from fontTools.misc.textTools import safeEval from . import DefaultTable +import pdb import struct @@ -26,19 +27,19 @@ METAGlyphRecordFormat = """ nMetaEntry: H """ # This record is followd by a variable data length field: -# USHORT or ULONG hdrOffset +# USHORT or ULONG hdrOffset # Offset from start of META table to the beginning # of this glyphs array of ns Metadata string entries. -# Size determined by metaFlags field +# Size determined by metaFlags field # METAGlyphRecordFormat entries must be sorted by glyph ID - + METAStringRecordFormat = """ > # big endian labelID: H stringLen: H """ # This record is followd by a variable data length field: -# USHORT or ULONG stringOffset +# USHORT or ULONG stringOffset # METAStringRecordFormat entries must be sorted in order of labelID # There may be more than one entry with the same labelID # There may be more than one strign with the same content. @@ -46,17 +47,17 @@ METAStringRecordFormat = """ # Strings shall be Unicode UTF-8 encoded, and null-terminated. METALabelDict = { - 0 : "MojikumiX4051", # An integer in the range 1-20 - 1 : "UNIUnifiedBaseChars", - 2 : "BaseFontName", - 3 : "Language", - 4 : "CreationDate", - 5 : "FoundryName", - 6 : "FoundryCopyright", - 7 : "OwnerURI", - 8 : "WritingScript", - 10 : "StrokeCount", - 11 : "IndexingRadical", + 0: "MojikumiX4051", # An integer in the range 1-20 + 1: "UNIUnifiedBaseChars", + 2: "BaseFontName", + 3: "Language", + 4: "CreationDate", + 5: "FoundryName", + 6: "FoundryCopyright", + 7: "OwnerURI", + 8: "WritingScript", + 10: "StrokeCount", + 11: "IndexingRadical", } @@ -69,9 +70,9 @@ def getLabelString(labelID): class table_M_E_T_A_(DefaultTable.DefaultTable): - + dependencies = [] - + def decompile(self, data, ttFont): dummy, newData = sstruct.unpack2(METAHeaderFormat, data, self) self.glyphRecords = [] @@ -97,16 +98,16 @@ class table_M_E_T_A_(DefaultTable.DefaultTable): newData = newData[4:] stringRec.string = data[stringRec.offset:stringRec.offset + stringRec.stringLen] glyphRecord.stringRecs.append(stringRec) - self.glyphRecords.append(glyphRecord) - + self.glyphRecords.append(glyphRecord) + def compile(self, ttFont): offsetOK = 0 self.nMetaRecs = len(self.glyphRecords) count = 0 - while ( offsetOK != 1): + while (offsetOK != 1): count = count + 1 if count > 4: - pdb_set_trace() + pdb.set_trace() metaData = sstruct.pack(METAHeaderFormat, self) stringRecsOffset = len(metaData) + self.nMetaRecs * (6 + 2*(self.metaFlags & 1)) stringRecSize = (6 + 2*(self.metaFlags & 1)) @@ -117,12 +118,12 @@ class table_M_E_T_A_(DefaultTable.DefaultTable): offsetOK = -1 break metaData = metaData + glyphRec.compile(self) - stringRecsOffset = stringRecsOffset + (glyphRec.nMetaEntry * stringRecSize) + stringRecsOffset = stringRecsOffset + (glyphRec.nMetaEntry * stringRecSize) # this will be the String Record offset for the next GlyphRecord. - if offsetOK == -1: + if offsetOK == -1: offsetOK = 0 continue - + # metaData now contains the header and all of the GlyphRecords. Its length should bw # the offset to the first StringRecord. stringOffset = stringRecsOffset @@ -139,23 +140,22 @@ class table_M_E_T_A_(DefaultTable.DefaultTable): if offsetOK == -1: offsetOK = 0 continue - + if ((self.metaFlags & 1) == 1) and (stringOffset < 65536): self.metaFlags = self.metaFlags - 1 continue else: offsetOK = 1 - - + # metaData now contains the header and all of the GlyphRecords and all of the String Records. # Its length should be the offset to the first string datum. for glyphRec in self.glyphRecords: for stringRec in glyphRec.stringRecs: assert (stringRec.offset == len(metaData)), "String offset did not compile correctly! for string:" + str(stringRec.string) metaData = metaData + stringRec.string - + return metaData - + def toXML(self, writer, ttFont): writer.comment("Lengths and number of entries in this table will be recalculated by the compiler") writer.newline() @@ -166,7 +166,7 @@ class table_M_E_T_A_(DefaultTable.DefaultTable): writer.newline() for glyphRec in self.glyphRecords: glyphRec.toXML(writer, ttFont) - + def fromXML(self, name, attrs, content, ttFont): if name == "GlyphRecord": if not hasattr(self, "glyphRecords"): @@ -180,7 +180,7 @@ class table_M_E_T_A_(DefaultTable.DefaultTable): glyphRec.fromXML(name, attrs, content, ttFont) glyphRec.offset = -1 glyphRec.nMetaEntry = len(glyphRec.stringRecs) - else: + else: setattr(self, name, safeEval(attrs["value"])) @@ -190,7 +190,7 @@ class GlyphRecord(object): self.nMetaEntry = -1 self.offset = -1 self.stringRecs = [] - + def toXML(self, writer, ttFont): writer.begintag("GlyphRecord") writer.newline() @@ -203,7 +203,6 @@ class GlyphRecord(object): writer.endtag("GlyphRecord") writer.newline() - def fromXML(self, name, attrs, content, ttFont): if name == "StringRecord": stringRec = StringRecord() @@ -213,7 +212,7 @@ class GlyphRecord(object): continue stringRec.fromXML(name, attrs, content, ttFont) stringRec.stringLen = len(stringRec.string) - else: + else: setattr(self, name, safeEval(attrs["value"])) def compile(self, parentTable): @@ -224,7 +223,7 @@ class GlyphRecord(object): datum = struct.pack(">L", self.offset) data = data + datum return data - + def __repr__(self): return "GlyphRecord[ glyphID: " + str(self.glyphID) + ", nMetaEntry: " + str(self.nMetaEntry) + ", offset: " + str(self.offset) + " ]" @@ -246,17 +245,17 @@ def mapXMLToUTF8(string): while string[i] != ";": i = i+1 valStr = string[j:i] - + uString = uString + unichr(eval('0x' + valStr)) else: uString = uString + unichr(byteord(string[i])) i = i +1 - - return uString.encode('utf8') + + return uString.encode('utf_8') def mapUTF8toXML(string): - uString = string.decode('utf8') + uString = string.decode('utf_8') string = "" for uChar in uString: i = ord(uChar) @@ -300,8 +299,7 @@ class StringRecord(object): datum = struct.pack(">L", self.offset) data = data + datum return data - + def __repr__(self): return "StringRecord [ labelID: " + str(self.labelID) + " aka " + getLabelString(self.labelID) \ + ", offset: " + str(self.offset) + ", length: " + str(self.stringLen) + ", string: " +self.string + " ]" - diff --git a/Lib/fontTools/ttLib/tables/M_V_A_R_.py b/Lib/fontTools/ttLib/tables/M_V_A_R_.py new file mode 100644 index 0000000..8659ae8 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/M_V_A_R_.py @@ -0,0 +1,7 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from .otBase import BaseTTXConverter + + +class table_M_V_A_R_(BaseTTXConverter): + pass diff --git a/Lib/fontTools/ttLib/tables/O_S_2f_2.py b/Lib/fontTools/ttLib/tables/O_S_2f_2.py index d29212f..3e2c30c 100644 --- a/Lib/fontTools/ttLib/tables/O_S_2f_2.py +++ b/Lib/fontTools/ttLib/tables/O_S_2f_2.py @@ -2,10 +2,12 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.misc import sstruct from fontTools.misc.textTools import safeEval, num2binary, binary2num -from . import DefaultTable -import warnings +from fontTools.ttLib.tables import DefaultTable +import logging +log = logging.getLogger(__name__) + # panose classification panoseFormat = """ @@ -22,13 +24,13 @@ panoseFormat = """ """ class Panose(object): - + def toXML(self, writer, ttFont): formatstring, names, fixes = sstruct.getformat(panoseFormat) for name in names: writer.simpletag(name, value=getattr(self, name)) writer.newline() - + def fromXML(self, name, attrs, content, ttFont): setattr(self, name, safeEval(attrs["value"])) @@ -60,8 +62,8 @@ OS2_format_0 = """ ulUnicodeRange4: L # character range achVendID: 4s # font vendor identification fsSelection: H # font selection flags - fsFirstCharIndex: H # first unicode character index - fsLastCharIndex: H # last unicode character index + usFirstCharIndex: H # first unicode character index + usLastCharIndex: H # last unicode character index sTypoAscender: h # typographic ascender sTypoDescender: h # typographic descender sTypoLineGap: h # typographic line gap @@ -79,7 +81,7 @@ OS2_format_2_addition = OS2_format_1_addition + """ sCapHeight: h usDefaultChar: H usBreakChar: H - usMaxContex: H + usMaxContext: H """ OS2_format_5_addition = OS2_format_2_addition + """ @@ -98,9 +100,11 @@ OS2_format_5_addition = bigendian + OS2_format_5_addition class table_O_S_2f_2(DefaultTable.DefaultTable): - + """the OS/2 table""" - + + dependencies = ["head"] + def decompile(self, data, ttFont): dummy, data = sstruct.unpack2(OS2_format_0, data, self) @@ -116,12 +120,26 @@ class table_O_S_2f_2(DefaultTable.DefaultTable): from fontTools import ttLib raise ttLib.TTLibError("unknown format for OS/2 table: version %s" % self.version) if len(data): - warnings.warn("too much 'OS/2' table data") + log.warning("too much 'OS/2' table data") self.panose = sstruct.unpack(panoseFormat, self.panose, Panose()) - + def compile(self, ttFont): + self.updateFirstAndLastCharIndex(ttFont) panose = self.panose + head = ttFont["head"] + if (self.fsSelection & 1) and not (head.macStyle & 1<<1): + log.warning("fsSelection bit 0 (italic) and " + "head table macStyle bit 1 (italic) should match") + if (self.fsSelection & 1<<5) and not (head.macStyle & 1): + log.warning("fsSelection bit 5 (bold) and " + "head table macStyle bit 0 (bold) should match") + if (self.fsSelection & 1<<6) and (self.fsSelection & 1 + (1<<5)): + log.warning("fsSelection bit 6 (regular) is set, " + "bits 0 (italic) and 5 (bold) must be clear") + if self.version < 4 and self.fsSelection & 0b1110000000: + log.warning("fsSelection bits 7, 8 and 9 are only defined in " + "OS/2 table version 4 and up: version %s", self.version) self.panose = sstruct.pack(panoseFormat, self.panose) if self.version == 0: data = sstruct.pack(OS2_format_0, self) @@ -131,16 +149,20 @@ class table_O_S_2f_2(DefaultTable.DefaultTable): data = sstruct.pack(OS2_format_2, self) elif self.version == 5: d = self.__dict__.copy() - d['usLowerOpticalPointSize'] = int(round(self.usLowerOpticalPointSize * 20)) - d['usUpperOpticalPointSize'] = int(round(self.usUpperOpticalPointSize * 20)) + d['usLowerOpticalPointSize'] = round(self.usLowerOpticalPointSize * 20) + d['usUpperOpticalPointSize'] = round(self.usUpperOpticalPointSize * 20) data = sstruct.pack(OS2_format_5, d) else: from fontTools import ttLib raise ttLib.TTLibError("unknown format for OS/2 table: version %s" % self.version) self.panose = panose return data - + def toXML(self, writer, ttFont): + writer.comment( + "The fields 'usFirstCharIndex' and 'usLastCharIndex'\n" + "will be recalculated by the compiler") + writer.newline() if self.version == 1: format = OS2_format_1 elif self.version in (2, 3, 4): @@ -157,7 +179,7 @@ class table_O_S_2f_2(DefaultTable.DefaultTable): writer.newline() value.toXML(writer, ttFont) writer.endtag("panose") - elif name in ("ulUnicodeRange1", "ulUnicodeRange2", + elif name in ("ulUnicodeRange1", "ulUnicodeRange2", "ulUnicodeRange3", "ulUnicodeRange4", "ulCodePageRange1", "ulCodePageRange2"): writer.simpletag(name, value=num2binary(value)) @@ -168,7 +190,7 @@ class table_O_S_2f_2(DefaultTable.DefaultTable): else: writer.simpletag(name, value=value) writer.newline() - + def fromXML(self, name, attrs, content, ttFont): if name == "panose": self.panose = panose = Panose() @@ -176,7 +198,7 @@ class table_O_S_2f_2(DefaultTable.DefaultTable): if isinstance(element, tuple): name, attrs, content = element panose.fromXML(name, attrs, content, ttFont) - elif name in ("ulUnicodeRange1", "ulUnicodeRange2", + elif name in ("ulUnicodeRange1", "ulUnicodeRange2", "ulUnicodeRange3", "ulUnicodeRange4", "ulCodePageRange1", "ulCodePageRange2", "fsType", "fsSelection"): @@ -186,4 +208,314 @@ class table_O_S_2f_2(DefaultTable.DefaultTable): else: setattr(self, name, safeEval(attrs["value"])) + def updateFirstAndLastCharIndex(self, ttFont): + if 'cmap' not in ttFont: + return + codes = set() + for table in getattr(ttFont['cmap'], 'tables', []): + if table.isUnicode(): + codes.update(table.cmap.keys()) + if codes: + minCode = min(codes) + maxCode = max(codes) + # USHORT cannot hold codepoints greater than 0xFFFF + self.usFirstCharIndex = min(0xFFFF, minCode) + self.usLastCharIndex = min(0xFFFF, maxCode) + + # misspelled attributes kept for legacy reasons + + @property + def usMaxContex(self): + return self.usMaxContext + + @usMaxContex.setter + def usMaxContex(self, value): + self.usMaxContext = value + + @property + def fsFirstCharIndex(self): + return self.usFirstCharIndex + + @fsFirstCharIndex.setter + def fsFirstCharIndex(self, value): + self.usFirstCharIndex = value + + @property + def fsLastCharIndex(self): + return self.usLastCharIndex + + @fsLastCharIndex.setter + def fsLastCharIndex(self, value): + self.usLastCharIndex = value + + def getUnicodeRanges(self): + """ Return the set of 'ulUnicodeRange*' bits currently enabled. """ + bits = set() + ul1, ul2 = self.ulUnicodeRange1, self.ulUnicodeRange2 + ul3, ul4 = self.ulUnicodeRange3, self.ulUnicodeRange4 + for i in range(32): + if ul1 & (1 << i): + bits.add(i) + if ul2 & (1 << i): + bits.add(i + 32) + if ul3 & (1 << i): + bits.add(i + 64) + if ul4 & (1 << i): + bits.add(i + 96) + return bits + + def setUnicodeRanges(self, bits): + """ Set the 'ulUnicodeRange*' fields to the specified 'bits'. """ + ul1, ul2, ul3, ul4 = 0, 0, 0, 0 + for bit in bits: + if 0 <= bit < 32: + ul1 |= (1 << bit) + elif 32 <= bit < 64: + ul2 |= (1 << (bit - 32)) + elif 64 <= bit < 96: + ul3 |= (1 << (bit - 64)) + elif 96 <= bit < 123: + ul4 |= (1 << (bit - 96)) + else: + raise ValueError('expected 0 <= int <= 122, found: %r' % bit) + self.ulUnicodeRange1, self.ulUnicodeRange2 = ul1, ul2 + self.ulUnicodeRange3, self.ulUnicodeRange4 = ul3, ul4 + + def recalcUnicodeRanges(self, ttFont, pruneOnly=False): + """ Intersect the codepoints in the font's Unicode cmap subtables with + the Unicode block ranges defined in the OpenType specification (v1.7), + and set the respective 'ulUnicodeRange*' bits if there is at least ONE + intersection. + If 'pruneOnly' is True, only clear unused bits with NO intersection. + """ + unicodes = set() + for table in ttFont['cmap'].tables: + if table.isUnicode(): + unicodes.update(table.cmap.keys()) + if pruneOnly: + empty = intersectUnicodeRanges(unicodes, inverse=True) + bits = self.getUnicodeRanges() - empty + else: + bits = intersectUnicodeRanges(unicodes) + self.setUnicodeRanges(bits) + return bits + + +# Unicode ranges data from the OpenType OS/2 table specification v1.7 + +OS2_UNICODE_RANGES = ( + (('Basic Latin', (0x0000, 0x007F)),), + (('Latin-1 Supplement', (0x0080, 0x00FF)),), + (('Latin Extended-A', (0x0100, 0x017F)),), + (('Latin Extended-B', (0x0180, 0x024F)),), + (('IPA Extensions', (0x0250, 0x02AF)), + ('Phonetic Extensions', (0x1D00, 0x1D7F)), + ('Phonetic Extensions Supplement', (0x1D80, 0x1DBF))), + (('Spacing Modifier Letters', (0x02B0, 0x02FF)), + ('Modifier Tone Letters', (0xA700, 0xA71F))), + (('Combining Diacritical Marks', (0x0300, 0x036F)), + ('Combining Diacritical Marks Supplement', (0x1DC0, 0x1DFF))), + (('Greek and Coptic', (0x0370, 0x03FF)),), + (('Coptic', (0x2C80, 0x2CFF)),), + (('Cyrillic', (0x0400, 0x04FF)), + ('Cyrillic Supplement', (0x0500, 0x052F)), + ('Cyrillic Extended-A', (0x2DE0, 0x2DFF)), + ('Cyrillic Extended-B', (0xA640, 0xA69F))), + (('Armenian', (0x0530, 0x058F)),), + (('Hebrew', (0x0590, 0x05FF)),), + (('Vai', (0xA500, 0xA63F)),), + (('Arabic', (0x0600, 0x06FF)), + ('Arabic Supplement', (0x0750, 0x077F))), + (('NKo', (0x07C0, 0x07FF)),), + (('Devanagari', (0x0900, 0x097F)),), + (('Bengali', (0x0980, 0x09FF)),), + (('Gurmukhi', (0x0A00, 0x0A7F)),), + (('Gujarati', (0x0A80, 0x0AFF)),), + (('Oriya', (0x0B00, 0x0B7F)),), + (('Tamil', (0x0B80, 0x0BFF)),), + (('Telugu', (0x0C00, 0x0C7F)),), + (('Kannada', (0x0C80, 0x0CFF)),), + (('Malayalam', (0x0D00, 0x0D7F)),), + (('Thai', (0x0E00, 0x0E7F)),), + (('Lao', (0x0E80, 0x0EFF)),), + (('Georgian', (0x10A0, 0x10FF)), + ('Georgian Supplement', (0x2D00, 0x2D2F))), + (('Balinese', (0x1B00, 0x1B7F)),), + (('Hangul Jamo', (0x1100, 0x11FF)),), + (('Latin Extended Additional', (0x1E00, 0x1EFF)), + ('Latin Extended-C', (0x2C60, 0x2C7F)), + ('Latin Extended-D', (0xA720, 0xA7FF))), + (('Greek Extended', (0x1F00, 0x1FFF)),), + (('General Punctuation', (0x2000, 0x206F)), + ('Supplemental Punctuation', (0x2E00, 0x2E7F))), + (('Superscripts And Subscripts', (0x2070, 0x209F)),), + (('Currency Symbols', (0x20A0, 0x20CF)),), + (('Combining Diacritical Marks For Symbols', (0x20D0, 0x20FF)),), + (('Letterlike Symbols', (0x2100, 0x214F)),), + (('Number Forms', (0x2150, 0x218F)),), + (('Arrows', (0x2190, 0x21FF)), + ('Supplemental Arrows-A', (0x27F0, 0x27FF)), + ('Supplemental Arrows-B', (0x2900, 0x297F)), + ('Miscellaneous Symbols and Arrows', (0x2B00, 0x2BFF))), + (('Mathematical Operators', (0x2200, 0x22FF)), + ('Supplemental Mathematical Operators', (0x2A00, 0x2AFF)), + ('Miscellaneous Mathematical Symbols-A', (0x27C0, 0x27EF)), + ('Miscellaneous Mathematical Symbols-B', (0x2980, 0x29FF))), + (('Miscellaneous Technical', (0x2300, 0x23FF)),), + (('Control Pictures', (0x2400, 0x243F)),), + (('Optical Character Recognition', (0x2440, 0x245F)),), + (('Enclosed Alphanumerics', (0x2460, 0x24FF)),), + (('Box Drawing', (0x2500, 0x257F)),), + (('Block Elements', (0x2580, 0x259F)),), + (('Geometric Shapes', (0x25A0, 0x25FF)),), + (('Miscellaneous Symbols', (0x2600, 0x26FF)),), + (('Dingbats', (0x2700, 0x27BF)),), + (('CJK Symbols And Punctuation', (0x3000, 0x303F)),), + (('Hiragana', (0x3040, 0x309F)),), + (('Katakana', (0x30A0, 0x30FF)), + ('Katakana Phonetic Extensions', (0x31F0, 0x31FF))), + (('Bopomofo', (0x3100, 0x312F)), + ('Bopomofo Extended', (0x31A0, 0x31BF))), + (('Hangul Compatibility Jamo', (0x3130, 0x318F)),), + (('Phags-pa', (0xA840, 0xA87F)),), + (('Enclosed CJK Letters And Months', (0x3200, 0x32FF)),), + (('CJK Compatibility', (0x3300, 0x33FF)),), + (('Hangul Syllables', (0xAC00, 0xD7AF)),), + (('Non-Plane 0 *', (0xD800, 0xDFFF)),), + (('Phoenician', (0x10900, 0x1091F)),), + (('CJK Unified Ideographs', (0x4E00, 0x9FFF)), + ('CJK Radicals Supplement', (0x2E80, 0x2EFF)), + ('Kangxi Radicals', (0x2F00, 0x2FDF)), + ('Ideographic Description Characters', (0x2FF0, 0x2FFF)), + ('CJK Unified Ideographs Extension A', (0x3400, 0x4DBF)), + ('CJK Unified Ideographs Extension B', (0x20000, 0x2A6DF)), + ('Kanbun', (0x3190, 0x319F))), + (('Private Use Area (plane 0)', (0xE000, 0xF8FF)),), + (('CJK Strokes', (0x31C0, 0x31EF)), + ('CJK Compatibility Ideographs', (0xF900, 0xFAFF)), + ('CJK Compatibility Ideographs Supplement', (0x2F800, 0x2FA1F))), + (('Alphabetic Presentation Forms', (0xFB00, 0xFB4F)),), + (('Arabic Presentation Forms-A', (0xFB50, 0xFDFF)),), + (('Combining Half Marks', (0xFE20, 0xFE2F)),), + (('Vertical Forms', (0xFE10, 0xFE1F)), + ('CJK Compatibility Forms', (0xFE30, 0xFE4F))), + (('Small Form Variants', (0xFE50, 0xFE6F)),), + (('Arabic Presentation Forms-B', (0xFE70, 0xFEFF)),), + (('Halfwidth And Fullwidth Forms', (0xFF00, 0xFFEF)),), + (('Specials', (0xFFF0, 0xFFFF)),), + (('Tibetan', (0x0F00, 0x0FFF)),), + (('Syriac', (0x0700, 0x074F)),), + (('Thaana', (0x0780, 0x07BF)),), + (('Sinhala', (0x0D80, 0x0DFF)),), + (('Myanmar', (0x1000, 0x109F)),), + (('Ethiopic', (0x1200, 0x137F)), + ('Ethiopic Supplement', (0x1380, 0x139F)), + ('Ethiopic Extended', (0x2D80, 0x2DDF))), + (('Cherokee', (0x13A0, 0x13FF)),), + (('Unified Canadian Aboriginal Syllabics', (0x1400, 0x167F)),), + (('Ogham', (0x1680, 0x169F)),), + (('Runic', (0x16A0, 0x16FF)),), + (('Khmer', (0x1780, 0x17FF)), + ('Khmer Symbols', (0x19E0, 0x19FF))), + (('Mongolian', (0x1800, 0x18AF)),), + (('Braille Patterns', (0x2800, 0x28FF)),), + (('Yi Syllables', (0xA000, 0xA48F)), + ('Yi Radicals', (0xA490, 0xA4CF))), + (('Tagalog', (0x1700, 0x171F)), + ('Hanunoo', (0x1720, 0x173F)), + ('Buhid', (0x1740, 0x175F)), + ('Tagbanwa', (0x1760, 0x177F))), + (('Old Italic', (0x10300, 0x1032F)),), + (('Gothic', (0x10330, 0x1034F)),), + (('Deseret', (0x10400, 0x1044F)),), + (('Byzantine Musical Symbols', (0x1D000, 0x1D0FF)), + ('Musical Symbols', (0x1D100, 0x1D1FF)), + ('Ancient Greek Musical Notation', (0x1D200, 0x1D24F))), + (('Mathematical Alphanumeric Symbols', (0x1D400, 0x1D7FF)),), + (('Private Use (plane 15)', (0xF0000, 0xFFFFD)), + ('Private Use (plane 16)', (0x100000, 0x10FFFD))), + (('Variation Selectors', (0xFE00, 0xFE0F)), + ('Variation Selectors Supplement', (0xE0100, 0xE01EF))), + (('Tags', (0xE0000, 0xE007F)),), + (('Limbu', (0x1900, 0x194F)),), + (('Tai Le', (0x1950, 0x197F)),), + (('New Tai Lue', (0x1980, 0x19DF)),), + (('Buginese', (0x1A00, 0x1A1F)),), + (('Glagolitic', (0x2C00, 0x2C5F)),), + (('Tifinagh', (0x2D30, 0x2D7F)),), + (('Yijing Hexagram Symbols', (0x4DC0, 0x4DFF)),), + (('Syloti Nagri', (0xA800, 0xA82F)),), + (('Linear B Syllabary', (0x10000, 0x1007F)), + ('Linear B Ideograms', (0x10080, 0x100FF)), + ('Aegean Numbers', (0x10100, 0x1013F))), + (('Ancient Greek Numbers', (0x10140, 0x1018F)),), + (('Ugaritic', (0x10380, 0x1039F)),), + (('Old Persian', (0x103A0, 0x103DF)),), + (('Shavian', (0x10450, 0x1047F)),), + (('Osmanya', (0x10480, 0x104AF)),), + (('Cypriot Syllabary', (0x10800, 0x1083F)),), + (('Kharoshthi', (0x10A00, 0x10A5F)),), + (('Tai Xuan Jing Symbols', (0x1D300, 0x1D35F)),), + (('Cuneiform', (0x12000, 0x123FF)), + ('Cuneiform Numbers and Punctuation', (0x12400, 0x1247F))), + (('Counting Rod Numerals', (0x1D360, 0x1D37F)),), + (('Sundanese', (0x1B80, 0x1BBF)),), + (('Lepcha', (0x1C00, 0x1C4F)),), + (('Ol Chiki', (0x1C50, 0x1C7F)),), + (('Saurashtra', (0xA880, 0xA8DF)),), + (('Kayah Li', (0xA900, 0xA92F)),), + (('Rejang', (0xA930, 0xA95F)),), + (('Cham', (0xAA00, 0xAA5F)),), + (('Ancient Symbols', (0x10190, 0x101CF)),), + (('Phaistos Disc', (0x101D0, 0x101FF)),), + (('Carian', (0x102A0, 0x102DF)), + ('Lycian', (0x10280, 0x1029F)), + ('Lydian', (0x10920, 0x1093F))), + (('Domino Tiles', (0x1F030, 0x1F09F)), + ('Mahjong Tiles', (0x1F000, 0x1F02F))), +) + + +_unicodeRangeSets = [] + +def _getUnicodeRangeSets(): + # build the sets of codepoints for each unicode range bit, and cache result + if not _unicodeRangeSets: + for bit, blocks in enumerate(OS2_UNICODE_RANGES): + rangeset = set() + for _, (start, stop) in blocks: + rangeset.update(set(range(start, stop+1))) + if bit == 57: + # The spec says that bit 57 ("Non Plane 0") implies that there's + # at least one codepoint beyond the BMP; so I also include all + # the non-BMP codepoints here + rangeset.update(set(range(0x10000, 0x110000))) + _unicodeRangeSets.append(rangeset) + return _unicodeRangeSets + + +def intersectUnicodeRanges(unicodes, inverse=False): + """ Intersect a sequence of (int) Unicode codepoints with the Unicode block + ranges defined in the OpenType specification v1.7, and return the set of + 'ulUnicodeRanges' bits for which there is at least ONE intersection. + If 'inverse' is True, return the the bits for which there is NO intersection. + + >>> intersectUnicodeRanges([0x0410]) == {9} + True + >>> intersectUnicodeRanges([0x0410, 0x1F000]) == {9, 57, 122} + True + >>> intersectUnicodeRanges([0x0410, 0x1F000], inverse=True) == ( + ... set(range(123)) - {9, 57, 122}) + True + """ + unicodes = set(unicodes) + uniranges = _getUnicodeRangeSets() + bits = set([ + bit for bit, unirange in enumerate(uniranges) + if not unirange.isdisjoint(unicodes) ^ inverse]) + return bits + +if __name__ == "__main__": + import doctest, sys + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/ttLib/tables/S_I_N_G_.py b/Lib/fontTools/ttLib/tables/S_I_N_G_.py index d9177e0..413ae48 100644 --- a/Lib/fontTools/ttLib/tables/S_I_N_G_.py +++ b/Lib/fontTools/ttLib/tables/S_I_N_G_.py @@ -19,26 +19,25 @@ SINGFormat = """ nameLength: 1s """ # baseGlyphName is a byte string which follows the record above. - class table_S_I_N_G_(DefaultTable.DefaultTable): - + dependencies = [] - + def decompile(self, data, ttFont): dummy, rest = sstruct.unpack2(SINGFormat, data, self) self.uniqueName = self.decompileUniqueName(self.uniqueName) self.nameLength = byteord(self.nameLength) assert len(rest) == self.nameLength self.baseGlyphName = tostr(rest) - + rawMETAMD5 = self.METAMD5 self.METAMD5 = "[" + hex(byteord(self.METAMD5[0])) for char in rawMETAMD5[1:]: self.METAMD5 = self.METAMD5 + ", " + hex(byteord(char)) self.METAMD5 = self.METAMD5 + "]" - + def decompileUniqueName(self, data): name = "" for char in data: @@ -55,8 +54,7 @@ class table_S_I_N_G_(DefaultTable.DefaultTable): octString.zfill(3) name += "\\" + octString return name - - + def compile(self, ttFont): d = self.__dict__.copy() d["nameLength"] = bytechr(len(self.baseGlyphName)) @@ -69,7 +67,7 @@ class table_S_I_N_G_(DefaultTable.DefaultTable): data = sstruct.pack(SINGFormat, d) data = data + tobytes(self.baseGlyphName) return data - + def compilecompileUniqueName(self, name, length): nameLen = len(name) if length <= nameLen: @@ -78,7 +76,6 @@ class table_S_I_N_G_(DefaultTable.DefaultTable): name += (nameLen - length) * "\000" return name - def toXML(self, writer, ttFont): writer.comment("Most of this table will be recalculated by the compiler") writer.newline() @@ -89,7 +86,7 @@ class table_S_I_N_G_(DefaultTable.DefaultTable): writer.newline() writer.simpletag("baseGlyphName", value=self.baseGlyphName) writer.newline() - + def fromXML(self, name, attrs, content, ttFont): value = attrs["value"] if name in ["uniqueName", "METAMD5", "baseGlyphName"]: diff --git a/Lib/fontTools/ttLib/tables/S_T_A_T_.py b/Lib/fontTools/ttLib/tables/S_T_A_T_.py new file mode 100644 index 0000000..1e044cf --- /dev/null +++ b/Lib/fontTools/ttLib/tables/S_T_A_T_.py @@ -0,0 +1,7 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from .otBase import BaseTTXConverter + + +class table_S_T_A_T_(BaseTTXConverter): + pass diff --git a/Lib/fontTools/ttLib/tables/S_V_G_.py b/Lib/fontTools/ttLib/tables/S_V_G_.py index c3f00dd..b061153 100644 --- a/Lib/fontTools/ttLib/tables/S_V_G_.py +++ b/Lib/fontTools/ttLib/tables/S_V_G_.py @@ -3,11 +3,16 @@ from fontTools.misc.py23 import * from fontTools.misc import sstruct from . import DefaultTable try: - import xml.etree.cElementTree as ET + import xml.etree.cElementTree as ET except ImportError: - import xml.etree.ElementTree as ET + import xml.etree.ElementTree as ET import struct import re +import logging + + +log = logging.getLogger(__name__) + __doc__=""" Compiles/decompiles version 0 and 1 SVG tables from/to XML. @@ -19,34 +24,34 @@ only if you add the secret element "" to the SVG element in the TTF f Version 0 is the joint Adobe-Mozilla proposal, which supports color palettes. The XML format is: - - - <complete SVG doc> ]] - </svgDoc> +<SVG> + <svgDoc endGlyphID="1" startGlyphID="1"> + <![CDATA[ <complete SVG doc> ]] + </svgDoc> ... <svgDoc endGlyphID="n" startGlyphID="m"> - <![CDATA[ <complete SVG doc> ]] - </svgDoc> - - <colorPalettes> - <colorParamUINameID>n</colorParamUINameID> - ... - <colorParamUINameID>m</colorParamUINameID> - <colorPalette uiNameID="n"> - <colorRecord red="<int>" green="<int>" blue="<int>" alpha="<int>" /> - ... - <colorRecord red="<int>" green="<int>" blue="<int>" alpha="<int>" /> - </colorPalette> - ... - <colorPalette uiNameID="m"> - <colorRecord red="<int> green="<int>" blue="<int>" alpha="<int>" /> - ... - <colorRecord red=<int>" green="<int>" blue="<int>" alpha="<int>" /> - </colorPalette> - </colorPalettes> + <![CDATA[ <complete SVG doc> ]] + </svgDoc> + + <colorPalettes> + <colorParamUINameID>n</colorParamUINameID> + ... + <colorParamUINameID>m</colorParamUINameID> + <colorPalette uiNameID="n"> + <colorRecord red="<int>" green="<int>" blue="<int>" alpha="<int>" /> + ... + <colorRecord red="<int>" green="<int>" blue="<int>" alpha="<int>" /> + </colorPalette> + ... + <colorPalette uiNameID="m"> + <colorRecord red="<int> green="<int>" blue="<int>" alpha="<int>" /> + ... + <colorRecord red=<int>" green="<int>" blue="<int>" alpha="<int>" /> + </colorPalette> + </colorPalettes> </SVG> -Color values must be less than 256. +Color values must be less than 256. The number of color records in each </colorPalette> must be the same as the number of <colorParamUINameID> elements. @@ -93,21 +98,29 @@ colorRecord_format_0 = """ class table_S_V_G_(DefaultTable.DefaultTable): - + + def __init__(self, tag=None): + DefaultTable.DefaultTable.__init__(self, tag) + self.colorPalettes = None + def decompile(self, data, ttFont): self.docList = None self.colorPalettes = None pos = 0 self.version = struct.unpack(">H", data[pos:pos+2])[0] - + if self.version == 1: + # This is pre-standardization version of the table; and obsolete. But we decompile it for now. + # https://wiki.mozilla.org/SVGOpenTypeFonts self.decompile_format_1(data, ttFont) else: if self.version != 0: - print("Unknown SVG table version '%s'. Decompiling as version 0." % (self.version)) + log.warning( + "Unknown SVG table version '%s'. Decompiling as version 0.", self.version) + # This is the standardized version of the table; and current. + # https://www.microsoft.com/typography/otspec/svg.htm self.decompile_format_0(data, ttFont) - def decompile_format_0(self, data, ttFont): dummy, data2 = sstruct.unpack2(SVG_format_0, data, self) # read in SVG Documents Index @@ -121,37 +134,29 @@ class table_S_V_G_(DefaultTable.DefaultTable): if numColorParams > 0: colorPalettes.colorParamUINameIDs = colorParamUINameIDs = [] pos = pos + 2 - i = 0 - while i < numColorParams: + for i in range(numColorParams): nameID = struct.unpack(">H", data[pos:pos+2])[0] colorParamUINameIDs.append(nameID) pos = pos + 2 - i += 1 colorPalettes.numColorPalettes = numColorPalettes = struct.unpack(">H", data[pos:pos+2])[0] pos = pos + 2 if numColorPalettes > 0: colorPalettes.colorPaletteList = colorPaletteList = [] - i = 0 - while i < numColorPalettes: + for i in range(numColorPalettes): colorPalette = ColorPalette() colorPaletteList.append(colorPalette) colorPalette.uiNameID = struct.unpack(">H", data[pos:pos+2])[0] pos = pos + 2 colorPalette.paletteColors = paletteColors = [] - j = 0 - while j < numColorParams: + for j in range(numColorParams): colorRecord, colorPaletteData = sstruct.unpack2(colorRecord_format_0, data[pos:], ColorRecord()) paletteColors.append(colorRecord) - j += 1 pos += 4 - i += 1 def decompile_format_1(self, data, ttFont): - pos = 2 - self.numEntries = struct.unpack(">H", data[pos:pos+2])[0] - pos += 2 - self.decompileEntryList(data, pos) + self.offsetToSVGDocIndex = 2 + self.decompileEntryList(data) def decompileEntryList(self, data): # data starts with the first entry of the entry list. @@ -162,16 +167,22 @@ class table_S_V_G_(DefaultTable.DefaultTable): data2 = data[pos:] self.docList = [] self.entries = entries = [] - i = 0 - while i < self.numEntries: + for i in range(self.numEntries): docIndexEntry, data2 = sstruct.unpack2(doc_index_entry_format_0, data2, DocumentIndexEntry()) entries.append(docIndexEntry) - i += 1 for entry in entries: start = entry.svgDocOffset + subTableStart end = start + entry.svgDocLength - doc = tostr(data[start:end], "utf-8") + doc = data[start:end] + if doc.startswith(b"\x1f\x8b"): + import gzip + bytesIO = BytesIO(doc) + with gzip.GzipFile(None, "r", fileobj=bytesIO) as gunzipper: + doc = gunzipper.read() + self.compressed = True + del bytesIO + doc = tostr(doc, "utf_8") self.docList.append( [doc, entry.startGlyphID, entry.endGlyphID] ) def compile(self, ttFont): @@ -193,11 +204,21 @@ class table_S_V_G_(DefaultTable.DefaultTable): curOffset = len(datum) + doc_index_entry_format_0Size*numEntries for doc, startGlyphID, endGlyphID in self.docList: docOffset = curOffset - docLength = len(doc) + docBytes = tobytes(doc, encoding="utf_8") + if getattr(self, "compressed", False) and not docBytes.startswith(b"\x1f\x8b"): + import gzip + bytesIO = BytesIO() + with gzip.GzipFile(None, "w", fileobj=bytesIO) as gzipper: + gzipper.write(docBytes) + gzipped = bytesIO.getvalue() + if len(gzipped) < len(docBytes): + docBytes = gzipped + del gzipped, bytesIO + docLength = len(docBytes) curOffset += docLength entry = struct.pack(">HHLL", startGlyphID, endGlyphID, docOffset, docLength) entryList.append(entry) - docList.append(tobytes(doc, encoding="utf-8")) + docList.append(docBytes) entryList.extend(docList) svgDocData = bytesjoin(entryList) @@ -239,11 +260,12 @@ class table_S_V_G_(DefaultTable.DefaultTable): curOffset = SVG_format_1Size + doc_index_entry_format_0Size*numEntries for doc, startGlyphID, endGlyphID in self.docList: docOffset = curOffset - docLength = len(doc) + docBytes = tobytes(doc, encoding="utf_8") + docLength = len(docBytes) curOffset += docLength entry = struct.pack(">HHLL", startGlyphID, endGlyphID, docOffset, docLength) dataList.append(entry) - docList.append(tobytes(doc, encoding="utf-8")) + docList.append(docBytes) dataList.extend(docList) data = bytesjoin(dataList) return data @@ -263,7 +285,7 @@ class table_S_V_G_(DefaultTable.DefaultTable): writer.newline() for uiNameID in self.colorPalettes.colorParamUINameIDs: writer.begintag("colorParamUINameID") - writer.writeraw(str(uiNameID)) + writer._writeraw(str(uiNameID)) writer.endtag("colorParamUINameID") writer.newline() for colorPalette in self.colorPalettes.colorPaletteList: @@ -284,13 +306,8 @@ class table_S_V_G_(DefaultTable.DefaultTable): writer.endtag("colorPalettes") writer.newline() - else: - writer.begintag("colorPalettes") - writer.endtag("colorPalettes") - writer.newline() def fromXML(self, name, attrs, content, ttFont): - import re if name == "svgDoc": if not hasattr(self, "docList"): self.docList = [] @@ -299,13 +316,13 @@ class table_S_V_G_(DefaultTable.DefaultTable): startGID = int(attrs["startGlyphID"]) endGID = int(attrs["endGlyphID"]) self.docList.append( [doc, startGID, endGID] ) - elif name == "colorPalettes": + elif name == "colorPalettes": self.colorPalettes = ColorPalettes() self.colorPalettes.fromXML(name, attrs, content, ttFont) if self.colorPalettes.numColorParams == 0: self.colorPalettes = None else: - print("Unknown", name, content) + log.warning("Unknown %s %s", name, content) class DocumentIndexEntry(object): def __init__(self): @@ -326,7 +343,7 @@ class ColorPalettes(object): def fromXML(self, name, attrs, content, ttFont): for element in content: - if isinstance(element, type("")): + if not isinstance(element, tuple): continue name, attrib, content = element if name == "colorParamUINameID": @@ -335,7 +352,7 @@ class ColorPalettes(object): elif name == "colorPalette": colorPalette = ColorPalette() self.colorPaletteList.append(colorPalette) - colorPalette.fromXML((name, attrib, content), ttFont) + colorPalette.fromXML(name, attrib, content, ttFont) self.numColorParams = len(self.colorParamUINameIDs) self.numColorPalettes = len(self.colorPaletteList) @@ -345,7 +362,7 @@ class ColorPalettes(object): class ColorPalette(object): def __init__(self): - self.uiNameID = None # USHORT. name table ID that describes user interface strings associated with this color palette. + self.uiNameID = None # USHORT. name table ID that describes user interface strings associated with this color palette. self.paletteColors = [] # list of ColorRecords def fromXML(self, name, attrs, content, ttFont): diff --git a/Lib/fontTools/ttLib/tables/S__i_l_f.py b/Lib/fontTools/ttLib/tables/S__i_l_f.py new file mode 100644 index 0000000..2afd71e --- /dev/null +++ b/Lib/fontTools/ttLib/tables/S__i_l_f.py @@ -0,0 +1,877 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc import sstruct +from fontTools.misc.textTools import safeEval +from itertools import * +from . import DefaultTable +from . import grUtils +from array import array +import struct, operator, warnings, re, sys + +Silf_hdr_format = ''' + > + version: 16.16F +''' + +Silf_hdr_format_3 = ''' + > + version: 16.16F + compilerVersion: L + numSilf: H + x + x +''' + +Silf_part1_format_v3 = ''' + > + ruleVersion: 16.16F + passOffset: H + pseudosOffset: H +''' + +Silf_part1_format = ''' + > + maxGlyphID: H + extraAscent: h + extraDescent: h + numPasses: B + iSubst: B + iPos: B + iJust: B + iBidi: B + flags: B + maxPreContext: B + maxPostContext: B + attrPseudo: B + attrBreakWeight: B + attrDirectionality: B + attrMirroring: B + attrSkipPasses: B + numJLevels: B +''' + +Silf_justify_format = ''' + > + attrStretch: B + attrShrink: B + attrStep: B + attrWeight: B + runto: B + x + x + x +''' + +Silf_part2_format = ''' + > + numLigComp: H + numUserDefn: B + maxCompPerLig: B + direction: B + attCollisions: B + x + x + x + numCritFeatures: B +''' + +Silf_pseudomap_format = ''' + > + unicode: L + nPseudo: H +''' + +Silf_classmap_format = ''' + > + numClass: H + numLinear: H +''' + +Silf_lookupclass_format = ''' + > + numIDs: H + searchRange: H + entrySelector: H + rangeShift: H +''' + +Silf_lookuppair_format = ''' + > + glyphId: H + index: H +''' + +Silf_pass_format = ''' + > + flags: B + maxRuleLoop: B + maxRuleContext: B + maxBackup: B + numRules: H + fsmOffset: H + pcCode: L + rcCode: L + aCode: L + oDebug: L + numRows: H + numTransitional: H + numSuccess: H + numColumns: H +''' + +aCode_info = ( + ("NOP", 0), + ("PUSH_BYTE", "b"), + ("PUSH_BYTE_U", "B"), + ("PUSH_SHORT", ">h"), + ("PUSH_SHORT_U", ">H"), + ("PUSH_LONG", ">L"), + ("ADD", 0), + ("SUB", 0), + ("MUL", 0), + ("DIV", 0), + ("MIN", 0), + ("MAX", 0), + ("NEG", 0), + ("TRUNC8", 0), + ("TRUNC16", 0), + ("COND", 0), + ("AND", 0), # x10 + ("OR", 0), + ("NOT", 0), + ("EQUAL", 0), + ("NOT_EQ", 0), + ("LESS", 0), + ("GTR", 0), + ("LESS_EQ", 0), + ("GTR_EQ", 0), + ("NEXT", 0), + ("NEXT_N", "b"), + ("COPY_NEXT", 0), + ("PUT_GLYPH_8BIT_OBS", "B"), + ("PUT_SUBS_8BIT_OBS", "bBB"), + ("PUT_COPY", "b"), + ("INSERT", 0), + ("DELETE", 0), # x20 + ("ASSOC", -1), + ("CNTXT_ITEM", "bB"), + ("ATTR_SET", "B"), + ("ATTR_ADD", "B"), + ("ATTR_SUB", "B"), + ("ATTR_SET_SLOT", "B"), + ("IATTR_SET_SLOT", "BB"), + ("PUSH_SLOT_ATTR", "Bb"), + ("PUSH_GLYPH_ATTR_OBS", "Bb"), + ("PUSH_GLYPH_METRIC", "Bbb"), + ("PUSH_FEAT", "Bb"), + ("PUSH_ATT_TO_GATTR_OBS", "Bb"), + ("PUSH_ATT_TO_GLYPH_METRIC", "Bbb"), + ("PUSH_ISLOT_ATTR", "Bbb"), + ("PUSH_IGLYPH_ATTR", "Bbb"), + ("POP_RET", 0), # x30 + ("RET_ZERO", 0), + ("RET_TRUE", 0), + ("IATTR_SET", "BB"), + ("IATTR_ADD", "BB"), + ("IATTR_SUB", "BB"), + ("PUSH_PROC_STATE", "B"), + ("PUSH_VERSION", 0), + ("PUT_SUBS", ">bHH"), + ("PUT_SUBS2", 0), + ("PUT_SUBS3", 0), + ("PUT_GLYPH", ">H"), + ("PUSH_GLYPH_ATTR", ">Hb"), + ("PUSH_ATT_TO_GLYPH_ATTR", ">Hb"), + ("BITOR", 0), + ("BITAND", 0), + ("BITNOT", 0), # x40 + ("BITSET", ">HH"), + ("SET_FEAT", "Bb") +) +aCode_map = dict([(x[0], (i, x[1])) for i,x in enumerate(aCode_info)]) + +def disassemble(aCode): + codelen = len(aCode) + pc = 0 + res = [] + while pc < codelen: + opcode = byteord(aCode[pc:pc+1]) + if opcode > len(aCode_info): + instr = aCode_info[0] + else: + instr = aCode_info[opcode] + pc += 1 + if instr[1] != 0 and pc >= codelen : return res + if instr[1] == -1: + count = byteord(aCode[pc]) + fmt = "%dB" % count + pc += 1 + elif instr[1] == 0: + fmt = "" + else : + fmt = instr[1] + if fmt == "": + res.append(instr[0]) + continue + parms = struct.unpack_from(fmt, aCode[pc:]) + res.append(instr[0] + "(" + ", ".join(map(str, parms)) + ")") + pc += struct.calcsize(fmt) + return res + +instre = re.compile("^\s*([^(]+)\s*(?:\(([^)]+)\))?") +def assemble(instrs): + res = [] + for inst in instrs: + m = instre.match(inst) + if not m or not m.group(1) in aCode_map: + continue + opcode, parmfmt = aCode_map[m.group(1)] + res.append(struct.pack("B", opcode)) + if m.group(2): + if parmfmt == 0: + continue + parms = [int(x) for x in re.split(",\s*", m.group(2))] + if parmfmt == -1: + l = len(parms) + res.append(struct.pack(("%dB" % (l+1)), l, *parms)) + else: + res.append(struct.pack(parmfmt, *parms)) + return b"".join(res) + +def writecode(tag, writer, instrs): + writer.begintag(tag) + writer.newline() + for l in disassemble(instrs): + writer.write(l) + writer.newline() + writer.endtag(tag) + writer.newline() + +def readcode(content): + res = [] + for e in content_string(content).split('\n'): + e = e.strip() + if not len(e): continue + res.append(e) + return assemble(res) + +attrs_info=('flags', 'extraAscent', 'extraDescent', 'maxGlyphID', + 'numLigComp', 'numUserDefn', 'maxCompPerLig', 'direction', 'lbGID') +attrs_passindexes = ('iSubst', 'iPos', 'iJust', 'iBidi') +attrs_contexts = ('maxPreContext', 'maxPostContext') +attrs_attributes = ('attrPseudo', 'attrBreakWeight', 'attrDirectionality', + 'attrMirroring', 'attrSkipPasses', 'attCollisions') +pass_attrs_info = ('flags', 'maxRuleLoop', 'maxRuleContext', 'maxBackup', + 'minRulePreContext', 'maxRulePreContext', 'collisionThreshold') +pass_attrs_fsm = ('numRows', 'numTransitional', 'numSuccess', 'numColumns') + +def writesimple(tag, self, writer, *attrkeys): + attrs = dict([(k, getattr(self, k)) for k in attrkeys]) + writer.simpletag(tag, **attrs) + writer.newline() + +def getSimple(self, attrs, *attr_list): + for k in attr_list: + if k in attrs: + setattr(self, k, int(safeEval(attrs[k]))) + +def content_string(contents): + res = "" + for element in contents: + if isinstance(element, tuple): continue + res += element + return res.strip() + +def wrapline(writer, dat, length=80): + currline = "" + for d in dat: + if len(currline) > length: + writer.write(currline[:-1]) + writer.newline() + currline = "" + currline += d + " " + if len(currline): + writer.write(currline[:-1]) + writer.newline() + +class _Object() : + pass + +class table_S__i_l_f(DefaultTable.DefaultTable): + '''Silf table support''' + + def __init__(self, tag=None): + DefaultTable.DefaultTable.__init__(self, tag) + self.silfs = [] + + def decompile(self, data, ttFont): + sstruct.unpack2(Silf_hdr_format, data, self) + if self.version >= 5.0: + (data, self.scheme) = grUtils.decompress(data) + sstruct.unpack2(Silf_hdr_format_3, data, self) + base = sstruct.calcsize(Silf_hdr_format_3) + elif self.version < 3.0: + self.numSilf = struct.unpack('>H', data[4:6]) + self.scheme = 0 + self.compilerVersion = 0 + base = 8 + else: + self.scheme = 0 + sstruct.unpack2(Silf_hdr_format_3, data, self) + base = sstruct.calcsize(Silf_hdr_format_3) + + silfoffsets = struct.unpack_from(('>%dL' % self.numSilf), data[base:]) + for offset in silfoffsets: + s = Silf() + self.silfs.append(s) + s.decompile(data[offset:], ttFont, self.version) + + def compile(self, ttFont): + self.numSilf = len(self.silfs) + if self.version < 3.0: + hdr = sstruct.pack(Silf_hdr_format, self) + hdr += struct.pack(">HH", self.numSilf, 0) + else: + hdr = sstruct.pack(Silf_hdr_format_3, self) + offset = len(hdr) + 4 * self.numSilf + data = "" + for s in self.silfs: + hdr += struct.pack(">L", offset) + subdata = s.compile(ttFont, self.version) + offset += len(subdata) + data += subdata + if self.version >= 5.0: + return grUtils.compress(self.scheme, hdr+data) + return hdr+data + + def toXML(self, writer, ttFont): + writer.comment('Attributes starting with _ are informative only') + writer.newline() + writer.simpletag('version', version=self.version, + compilerVersion=self.compilerVersion, compressionScheme=self.scheme) + writer.newline() + for s in self.silfs: + writer.begintag('silf') + writer.newline() + s.toXML(writer, ttFont, self.version) + writer.endtag('silf') + writer.newline() + + def fromXML(self, name, attrs, content, ttFont): + if name == 'version': + self.scheme=int(safeEval(attrs['compressionScheme'])) + self.version = float(safeEval(attrs['version'])) + self.compilerVersion = int(safeEval(attrs['compilerVersion'])) + return + if name == 'silf': + s = Silf() + self.silfs.append(s) + for element in content: + if not isinstance(element, tuple): continue + tag, attrs, subcontent = element + s.fromXML(tag, attrs, subcontent, ttFont, self.version) + +class Silf(object): + '''A particular Silf subtable''' + + def __init__(self): + self.passes = [] + self.scriptTags = [] + self.critFeatures = [] + self.jLevels = [] + self.pMap = {} + + def decompile(self, data, ttFont, version=2.0): + if version >= 3.0 : + _, data = sstruct.unpack2(Silf_part1_format_v3, data, self) + _, data = sstruct.unpack2(Silf_part1_format, data, self) + for jlevel in range(self.numJLevels): + j, data = sstruct.unpack2(Silf_justify_format, data, _Object()) + self.jLevels.append(j) + _, data = sstruct.unpack2(Silf_part2_format, data, self) + if self.numCritFeatures: + self.critFeatures = struct.unpack_from(('>%dH' % self.numCritFeatures), data) + data = data[self.numCritFeatures * 2 + 1:] + (numScriptTag,) = struct.unpack_from('B', data) + if numScriptTag: + self.scriptTags = [struct.unpack("4s", data[x:x+4])[0] for x in range(1, 1 + 4 * numScriptTag, 4)] + data = data[1 + 4 * numScriptTag:] + (self.lbGID,) = struct.unpack('>H', data[:2]) + if self.numPasses: + self.oPasses = struct.unpack(('>%dL' % (self.numPasses+1)), data[2:6+4*self.numPasses]) + data = data[6 + 4 * self.numPasses:] + (numPseudo,) = struct.unpack(">H", data[:2]) + for i in range(numPseudo): + if version >= 3.0: + pseudo = sstruct.unpack(Silf_pseudomap_format, data[8+6*i:14+6*i], _Object()) + else: + pseudo = struct.unpack('>HH', data[8+4*i:12+4*i], _Object()) + self.pMap[pseudo.unicode] = ttFont.getGlyphName(pseudo.nPseudo) + data = data[8 + 6 * numPseudo:] + currpos = (sstruct.calcsize(Silf_part1_format) + + sstruct.calcsize(Silf_justify_format) * self.numJLevels + + sstruct.calcsize(Silf_part2_format) + 2 * self.numCritFeatures + + 1 + 1 + 4 * numScriptTag + 6 + 4 * self.numPasses + 8 + 6 * numPseudo) + if version >= 3.0: + currpos += sstruct.calcsize(Silf_part1_format_v3) + self.classes = Classes() + self.classes.decompile(data, ttFont, version) + for i in range(self.numPasses): + p = Pass() + self.passes.append(p) + p.decompile(data[self.oPasses[i]-currpos:self.oPasses[i+1]-currpos], + ttFont, version) + + def compile(self, ttFont, version=2.0): + self.numPasses = len(self.passes) + self.numJLevels = len(self.jLevels) + self.numCritFeatures = len(self.critFeatures) + numPseudo = len(self.pMap) + data = "" + if version >= 3.0: + hdroffset = sstruct.calcsize(Silf_part1_format_v3) + else: + hdroffset = 0 + data += sstruct.pack(Silf_part1_format, self) + for j in self.jLevels: + data += sstruct.pack(Silf_justify_format, j) + data += sstruct.pack(Silf_part2_format, self) + if self.numCritFeatures: + data += struct.pack((">%dH" % self.numCritFeaturs), *self.critFeatures) + data += struct.pack("BB", 0, len(self.scriptTags)) + if len(self.scriptTags): + tdata = [struct.pack("4s", x) for x in self.scriptTags] + data += "".join(tdata) + data += struct.pack(">H", self.lbGID) + self.passOffset = len(data) + + data1 = grUtils.bininfo(numPseudo, 6) + currpos = hdroffset + len(data) + 4 * (self.numPasses + 1) + self.pseudosOffset = currpos + len(data1) + for u, p in sorted(self.pMap.items()): + data1 += struct.pack((">LH" if version >= 3.0 else ">HH"), + u, ttFont.getGlyphID(p)) + data1 += self.classes.compile(ttFont, version) + currpos += len(data1) + data2 = "" + datao = "" + for i, p in enumerate(self.passes): + base = currpos + len(data2) + datao += struct.pack(">L", base) + data2 += p.compile(ttFont, base, version) + datao += struct.pack(">L", currpos + len(data2)) + + if version >= 3.0: + data3 = sstruct.pack(Silf_part1_format_v3, self) + else: + data3 = "" + return data3 + data + datao + data1 + data2 + + + def toXML(self, writer, ttFont, version=2.0): + if version >= 3.0: + writer.simpletag('version', ruleVersion=self.ruleVersion) + writer.newline() + writesimple('info', self, writer, *attrs_info) + writesimple('passindexes', self, writer, *attrs_passindexes) + writesimple('contexts', self, writer, *attrs_contexts) + writesimple('attributes', self, writer, *attrs_attributes) + if len(self.jLevels): + writer.begintag('justifications') + writer.newline() + jformat, jnames, jfixes = sstruct.getformat(Silf_justify_format) + for i, j in enumerate(self.jLevels): + attrs = dict([(k, getattr(j, k)) for k in jnames]) + writer.simpletag('justify', **attrs) + writer.newline() + writer.endtag('justifications') + writer.newline() + if len(self.critFeatures): + writer.begintag('critFeatures') + writer.newline() + writer.write(" ".join(map(str, self.critFeatures))) + writer.newline() + writer.endtag('critFeatures') + writer.newline() + if len(self.scriptTags): + writer.begintag('scriptTags') + writer.newline() + writer.write(" ".join(self.scriptTags)) + writer.newline() + writer.endtag('scriptTags') + writer.newline() + if self.pMap: + writer.begintag('pseudoMap') + writer.newline() + for k, v in sorted(self.pMap.items()): + writer.simpletag('pseudo', unicode=hex(k), pseudo=v) + writer.newline() + writer.endtag('pseudoMap') + writer.newline() + self.classes.toXML(writer, ttFont, version) + if len(self.passes): + writer.begintag('passes') + writer.newline() + for i, p in enumerate(self.passes): + writer.begintag('pass', _index=i) + writer.newline() + p.toXML(writer, ttFont, version) + writer.endtag('pass') + writer.newline() + writer.endtag('passes') + writer.newline() + + def fromXML(self, name, attrs, content, ttFont, version=2.0): + if name == 'version': + self.ruleVersion = float(safeEval(attrs.get('ruleVersion', "0"))) + if name == 'info': + getSimple(self, attrs, *attrs_info) + elif name == 'passindexes': + getSimple(self, attrs, *attrs_passindexes) + elif name == 'contexts': + getSimple(self, attrs, *attrs_contexts) + elif name == 'attributes': + getSimple(self, attrs, *attrs_attributes) + elif name == 'justifications': + for element in content: + if not isinstance(element, tuple): continue + (tag, attrs, subcontent) = element + if tag == 'justify': + j = _Object() + for k, v in attrs.items(): + setattr(j, k, int(v)) + self.jLevels.append(j) + elif name == 'critFeatures': + self.critFeatures = [] + element = content_string(content) + self.critFeatures.extend(map(int, element.split())) + elif name == 'scriptTags': + self.scriptTags = [] + element = content_string(content) + for n in element.split(): + self.scriptTags.append(n) + elif name == 'pseudoMap': + self.pMap = {} + for element in content: + if not isinstance(element, tuple): continue + (tag, attrs, subcontent) = element + if tag == 'pseudo': + k = int(attrs['unicode'], 16) + v = attrs['pseudo'] + self.pMap[k] = v + elif name == 'classes': + self.classes = Classes() + for element in content: + if not isinstance(element, tuple): continue + tag, attrs, subcontent = element + self.classes.fromXML(tag, attrs, subcontent, ttFont, version) + elif name == 'passes': + for element in content: + if not isinstance(element, tuple): continue + tag, attrs, subcontent = element + if tag == 'pass': + p = Pass() + for e in subcontent: + if not isinstance(e, tuple): continue + p.fromXML(e[0], e[1], e[2], ttFont, version) + self.passes.append(p) + + +class Classes(object): + + def __init__(self): + self.linear = [] + self.nonLinear = [] + + def decompile(self, data, ttFont, version=2.0): + sstruct.unpack2(Silf_classmap_format, data, self) + if version >= 4.0 : + oClasses = struct.unpack((">%dL" % (self.numClass+1)), + data[4:8+4*self.numClass]) + else: + oClasses = struct.unpack((">%dH" % (self.numClass+1)), + data[4:6+2*self.numClass]) + for s,e in zip(oClasses[:self.numLinear], oClasses[1:self.numLinear+1]): + self.linear.append(map(ttFont.getGlyphName, + struct.unpack((">%dH" % ((e-s)/2)), data[s:e]))) + for s,e in zip(oClasses[self.numLinear:self.numClass], + oClasses[self.numLinear+1:self.numClass+1]): + nonLinids = [struct.unpack(">HH", data[x:x+4]) for x in range(s+8, e, 4)] + nonLin = dict([(ttFont.getGlyphName(x[0]), x[1]) for x in nonLinids]) + self.nonLinear.append(nonLin) + + def compile(self, ttFont, version=2.0): + data = "" + oClasses = [] + if version >= 4.0: + offset = 8 + 4 * (len(self.linear) + len(self.nonLinear)) + else: + offset = 6 + 2 * (len(self.linear) + len(self.nonLinear)) + for l in self.linear: + oClasses.append(len(data) + offset) + gs = map(ttFont.getGlyphID, l) + data += struct.pack((">%dH" % len(l)), *gs) + for l in self.nonLinear: + oClasses.append(len(data) + offset) + gs = [(ttFont.getGlyphID(x[0]), x[1]) for x in l.items()] + data += grUtils.bininfo(len(gs)) + data += "".join([struct.pack(">HH", *x) for x in sorted(gs)]) + oClasses.append(len(data) + offset) + self.numClass = len(oClasses) - 1 + self.numLinear = len(self.linear) + return sstruct.pack(Silf_classmap_format, self) + \ + struct.pack(((">%dL" if version >= 4.0 else ">%dH") % len(oClasses)), + *oClasses) + data + + def toXML(self, writer, ttFont, version=2.0): + writer.begintag('classes') + writer.newline() + writer.begintag('linearClasses') + writer.newline() + for i,l in enumerate(self.linear): + writer.begintag('linear', _index=i) + writer.newline() + wrapline(writer, l) + writer.endtag('linear') + writer.newline() + writer.endtag('linearClasses') + writer.newline() + writer.begintag('nonLinearClasses') + writer.newline() + for i, l in enumerate(self.nonLinear): + writer.begintag('nonLinear', _index=i + self.numLinear) + writer.newline() + for inp, ind in l.items(): + writer.simpletag('map', glyph=inp, index=ind) + writer.newline() + writer.endtag('nonLinear') + writer.newline() + writer.endtag('nonLinearClasses') + writer.newline() + writer.endtag('classes') + writer.newline() + + def fromXML(self, name, attrs, content, ttFont, version=2.0): + if name == 'linearClasses': + for element in content: + if not isinstance(element, tuple): continue + tag, attrs, subcontent = element + if tag == 'linear': + l = content_string(subcontent).split() + self.linear.append(l) + elif name == 'nonLinearClasses': + for element in content: + if not isinstance(element, tuple): continue + tag, attrs, subcontent = element + if tag =='nonLinear': + l = {} + for e in subcontent: + if not isinstance(e, tuple): continue + tag, attrs, subsubcontent = e + if tag == 'map': + l[attrs['glyph']] = int(safeEval(attrs['index'])) + self.nonLinear.append(l) + +class Pass(object): + + def __init__(self): + self.colMap = {} + self.rules = [] + self.rulePreContexts = [] + self.ruleSortKeys = [] + self.ruleConstraints = [] + self.passConstraints = "" + self.actions = [] + self.stateTrans = [] + self.startStates = [] + + def decompile(self, data, ttFont, version=2.0): + _, data = sstruct.unpack2(Silf_pass_format, data, self) + (numRange, _, _, _) = struct.unpack(">4H", data[:8]) + data = data[8:] + for i in range(numRange): + (first, last, col) = struct.unpack(">3H", data[6*i:6*i+6]) + for g in range(first, last+1): + self.colMap[ttFont.getGlyphName(g)] = col + data = data[6*numRange:] + oRuleMap = struct.unpack_from((">%dH" % (self.numSuccess + 1)), data) + data = data[2+2*self.numSuccess:] + rules = struct.unpack_from((">%dH" % oRuleMap[-1]), data) + self.rules = [rules[s:e] for (s,e) in zip(oRuleMap, oRuleMap[1:])] + data = data[2*oRuleMap[-1]:] + (self.minRulePreContext, self.maxRulePreContext) = struct.unpack('BB', data[:2]) + numStartStates = self.maxRulePreContext - self.minRulePreContext + 1 + self.startStates = struct.unpack((">%dH" % numStartStates), + data[2:2 + numStartStates * 2]) + data = data[2+numStartStates*2:] + self.ruleSortKeys = struct.unpack((">%dH" % self.numRules), data[:2 * self.numRules]) + data = data[2*self.numRules:] + self.rulePreContexts = struct.unpack(("%dB" % self.numRules), data[:self.numRules]) + data = data[self.numRules:] + (self.collisionThreshold, pConstraint) = struct.unpack(">BH", data[:3]) + oConstraints = list(struct.unpack((">%dH" % (self.numRules + 1)), + data[3:5 + self.numRules * 2])) + data = data[5 + self.numRules * 2:] + oActions = list(struct.unpack((">%dH" % (self.numRules + 1)), + data[:2 + self.numRules * 2])) + data = data[2 * self.numRules + 2:] + for i in range(self.numTransitional): + a = array("H", data[i*self.numColumns*2:(i+1)*self.numColumns*2]) + a.byteswap() + self.stateTrans.append(a) + data = data[self.numTransitional * self.numColumns * 2 + 1:] + self.passConstraints = data[:pConstraint] + data = data[pConstraint:] + for i in range(len(oConstraints)-2,-1,-1): + if oConstraints[i] == 0 : + oConstraints[i] = oConstraints[i+1] + self.ruleConstraints = [(data[s:e] if (e-s > 1) else "") for (s,e) in zip(oConstraints, oConstraints[1:])] + data = data[oConstraints[-1]:] + self.actions = [(data[s:e] if (e-s > 1) else "") for (s,e) in zip(oActions, oActions[1:])] + data = data[oActions[-1]:] + # not using debug + + def compile(self, ttFont, base, version=2.0): + # build it all up backwards + oActions = reduce(lambda a, x: (a[0]+len(x), a[1]+[a[0]]), self.actions + [""], (0, []))[1] + oConstraints = reduce(lambda a, x: (a[0]+len(x), a[1]+[a[0]]), self.ruleConstraints + [""], (1, []))[1] + constraintCode = "\000" + "".join(self.ruleConstraints) + transes = [] + for t in self.stateTrans: + t.byteswap() + transes.append(t.tostring()) + t.byteswap() + if not len(transes): + self.startStates = [0] + oRuleMap = reduce(lambda a, x: (a[0]+len(x), a[1]+[a[0]]), self.rules+[[]], (0, []))[1] + passRanges = [] + gidcolmap = dict([(ttFont.getGlyphID(x[0]), x[1]) for x in self.colMap.items()]) + for e in grUtils.entries(gidcolmap, sameval = True): + if e[1]: + passRanges.append((e[0], e[0]+e[1]-1, e[2][0])) + self.numRules = len(self.actions) + self.fsmOffset = (sstruct.calcsize(Silf_pass_format) + 8 + len(passRanges) * 6 + + len(oRuleMap) * 2 + 2 * oRuleMap[-1] + 2 + + 2 * len(self.startStates) + 3 * self.numRules + 3 + + 4 * self.numRules + 4) + self.pcCode = self.fsmOffset + 2*self.numTransitional*self.numColumns + 1 + base + self.rcCode = self.pcCode + len(self.passConstraints) + self.aCode = self.rcCode + len(constraintCode) + self.oDebug = 0 + # now generate output + data = sstruct.pack(Silf_pass_format, self) + data += grUtils.bininfo(len(passRanges), 6) + data += "".join(struct.pack(">3H", *p) for p in passRanges) + data += struct.pack((">%dH" % len(oRuleMap)), *oRuleMap) + flatrules = reduce(lambda a,x: a+x, self.rules, []) + data += struct.pack((">%dH" % oRuleMap[-1]), *flatrules) + data += struct.pack("BB", self.minRulePreContext, self.maxRulePreContext) + data += struct.pack((">%dH" % len(self.startStates)), *self.startStates) + data += struct.pack((">%dH" % self.numRules), *self.ruleSortKeys) + data += struct.pack(("%dB" % self.numRules), *self.rulePreContexts) + data += struct.pack(">BH", self.collisionThreshold, len(self.passConstraints)) + data += struct.pack((">%dH" % (self.numRules+1)), *oConstraints) + data += struct.pack((">%dH" % (self.numRules+1)), *oActions) + return data + "".join(transes) + struct.pack("B", 0) + \ + self.passConstraints + constraintCode + "".join(self.actions) + + def toXML(self, writer, ttFont, version=2.0): + writesimple('info', self, writer, *pass_attrs_info) + writesimple('fsminfo', self, writer, *pass_attrs_fsm) + writer.begintag('colmap') + writer.newline() + wrapline(writer, ["{}={}".format(*x) for x in sorted(self.colMap.items(), + key=lambda x:ttFont.getGlyphID(x[0]))]) + writer.endtag('colmap') + writer.newline() + writer.begintag('staterulemap') + writer.newline() + for i, r in enumerate(self.rules): + writer.simpletag('state', number = self.numRows - self.numSuccess + i, + rules = " ".join(map(str, r))) + writer.newline() + writer.endtag('staterulemap') + writer.newline() + writer.begintag('rules') + writer.newline() + for i in range(len(self.actions)): + writer.begintag('rule', index=i, precontext=self.rulePreContexts[i], + sortkey=self.ruleSortKeys[i]) + writer.newline() + if len(self.ruleConstraints[i]): + writecode('constraint', writer, self.ruleConstraints[i]) + writecode('action', writer, self.actions[i]) + writer.endtag('rule') + writer.newline() + writer.endtag('rules') + writer.newline() + if len(self.passConstraints): + writecode('passConstraint', writer, self.passConstraints) + if len(self.stateTrans): + writer.begintag('fsm') + writer.newline() + writer.begintag('starts') + writer.write(" ".join(map(str, self.startStates))) + writer.endtag('starts') + writer.newline() + for i, s in enumerate(self.stateTrans): + writer.begintag('row', _i=i) + # no newlines here + writer.write(" ".join(map(str, s))) + writer.endtag('row') + writer.newline() + writer.endtag('fsm') + writer.newline() + + def fromXML(self, name, attrs, content, ttFont, version=2.0): + if name == 'info': + getSimple(self, attrs, *pass_attrs_info) + elif name == 'fsminfo': + getSimple(self, attrs, *pass_attrs_fsm) + elif name == 'colmap': + e = content_string(content) + for w in e.split(): + x = w.split('=') + if len(x) != 2 or x[0] == '' or x[1] == '': continue + self.colMap[x[0]] = int(x[1]) + elif name == 'staterulemap': + for e in content: + if not isinstance(e, tuple): continue + tag, a, c = e + if tag == 'state': + self.rules.append(map(int, a['rules'].split(" "))) + elif name == 'rules': + for element in content: + if not isinstance(element, tuple): continue + tag, a, c = element + if tag != 'rule': continue + self.rulePreContexts.append(int(a['precontext'])) + self.ruleSortKeys.append(int(a['sortkey'])) + con = "" + act = "" + for e in c: + if not isinstance(e, tuple): continue + tag, a, subc = e + if tag == 'constraint': + con = readcode(subc) + elif tag == 'action': + act = readcode(subc) + self.actions.append(act) + self.ruleConstraints.append(con) + elif name == 'passConstraint': + self.passConstraints = readcode(content) + elif name == 'fsm': + for element in content: + if not isinstance(element, tuple): continue + tag, a, c = element + if tag == 'row': + s = array('H') + e = content_string(c) + s.extend(map(int, e.split())) + self.stateTrans.append(s) + elif tag == 'starts': + s = [] + e = content_string(c) + s.extend(map(int, e.split())) + self.startStates = s + diff --git a/Lib/fontTools/ttLib/tables/S__i_l_l.py b/Lib/fontTools/ttLib/tables/S__i_l_l.py new file mode 100644 index 0000000..7acef1d --- /dev/null +++ b/Lib/fontTools/ttLib/tables/S__i_l_l.py @@ -0,0 +1,79 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc import sstruct +from fontTools.misc.textTools import safeEval +from . import DefaultTable +from . import grUtils +import struct + +Sill_hdr = ''' + > + version: 16.16F +''' + +class table_S__i_l_l(DefaultTable.DefaultTable): + + def __init__(self, tag=None): + DefaultTable.DefaultTable.__init__(self, tag) + self.langs = {} + + def decompile(self, data, ttFont): + (_, data) = sstruct.unpack2(Sill_hdr, data, self) + numLangs, = struct.unpack('>H', data[:2]) + data = data[8:] + maxsetting = 0 + langinfo = [] + for i in range(numLangs): + (langcode, numsettings, offset) = struct.unpack(">4sHH", + data[i * 8:(i+1) * 8]) + offset = int(offset / 8) - (numLangs + 1) + langcode = langcode.replace(b'\000', b'') + langinfo.append((langcode, numsettings, offset)) + maxsetting = max(maxsetting, offset + numsettings) + data = data[numLangs * 8:] + finfo = [] + for i in range(maxsetting): + (fid, val, _) = struct.unpack(">LHH", data[i * 8:(i+1) * 8]) + finfo.append((fid, val)) + self.langs = {} + for c, n, o in langinfo: + self.langs[c] = [] + for i in range(o, o+n): + self.langs[c].append(finfo[i]) + + def compile(self, ttFont): + ldat = "" + fdat = "" + offset = 0 + for c, inf in sorted(self.langs.items()): + ldat += struct.pack(">4sHH", c.encode('utf8'), len(inf), 8 * (offset + len(self.langs) + 1)) + for fid, val in inf: + fdat += struct.pack(">LHH", fid, val, 0) + offset += len(inf) + return sstruct.pack(Sill_hdr, self) + grUtils.bininfo(len(self.langs)) + \ + ldat + fdat + + def toXML(self, writer, ttFont): + writer.simpletag('version', version=self.version) + writer.newline() + for c, inf in sorted(self.langs.items()): + writer.begintag('lang', name=c) + writer.newline() + for fid, val in inf: + writer.simpletag('feature', fid=grUtils.num2tag(fid), val=val) + writer.newline() + writer.endtag('lang') + writer.newline() + + def fromXML(self, name, attrs, content, ttFont): + if name == 'version': + self.version = float(safeEval(attrs['version'])) + elif name == 'lang': + c = attrs['name'] + self.langs[c] = [] + for element in content: + if not isinstance(element, tuple): continue + tag, a, subcontent = element + if tag == 'feature': + self.langs[c].append((grUtils.tag2num(a['fid']), + int(safeEval(a['val'])))) diff --git a/Lib/fontTools/ttLib/tables/T_S_I_B_.py b/Lib/fontTools/ttLib/tables/T_S_I_B_.py index 5cc54e2..7d47e3a 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I_B_.py +++ b/Lib/fontTools/ttLib/tables/T_S_I_B_.py @@ -1,5 +1,6 @@ -from . import asciiTable +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from .T_S_I_V_ import table_T_S_I_V_ -class table_T_S_I_B_(asciiTable.asciiTable): +class table_T_S_I_B_(table_T_S_I_V_): pass - diff --git a/Lib/fontTools/ttLib/tables/T_S_I_D_.py b/Lib/fontTools/ttLib/tables/T_S_I_D_.py index 8228f8a..f989c9e 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I_D_.py +++ b/Lib/fontTools/ttLib/tables/T_S_I_D_.py @@ -1,5 +1,6 @@ -from . import asciiTable +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from .T_S_I_V_ import table_T_S_I_V_ -class table_T_S_I_D_(asciiTable.asciiTable): +class table_T_S_I_D_(table_T_S_I_V_): pass - diff --git a/Lib/fontTools/ttLib/tables/T_S_I_J_.py b/Lib/fontTools/ttLib/tables/T_S_I_J_.py index 0983b57..ca538ba 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I_J_.py +++ b/Lib/fontTools/ttLib/tables/T_S_I_J_.py @@ -1,5 +1,6 @@ -from . import asciiTable +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from .T_S_I_V_ import table_T_S_I_V_ -class table_T_S_I_J_(asciiTable.asciiTable): +class table_T_S_I_J_(table_T_S_I_V_): pass - diff --git a/Lib/fontTools/ttLib/tables/T_S_I_P_.py b/Lib/fontTools/ttLib/tables/T_S_I_P_.py index e34a18c..062d332 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I_P_.py +++ b/Lib/fontTools/ttLib/tables/T_S_I_P_.py @@ -1,5 +1,6 @@ -from . import asciiTable +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from .T_S_I_V_ import table_T_S_I_V_ -class table_T_S_I_P_(asciiTable.asciiTable): +class table_T_S_I_P_(table_T_S_I_V_): pass - diff --git a/Lib/fontTools/ttLib/tables/T_S_I_S_.py b/Lib/fontTools/ttLib/tables/T_S_I_S_.py index 56373e6..68e22ce 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I_S_.py +++ b/Lib/fontTools/ttLib/tables/T_S_I_S_.py @@ -1,5 +1,6 @@ -from . import asciiTable +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from .T_S_I_V_ import table_T_S_I_V_ -class table_T_S_I_S_(asciiTable.asciiTable): +class table_T_S_I_S_(table_T_S_I_V_): pass - diff --git a/Lib/fontTools/ttLib/tables/T_S_I_V_.py b/Lib/fontTools/ttLib/tables/T_S_I_V_.py index a87e3f7..46b2182 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I_V_.py +++ b/Lib/fontTools/ttLib/tables/T_S_I_V_.py @@ -1,5 +1,21 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * from . import asciiTable class table_T_S_I_V_(asciiTable.asciiTable): - pass + def toXML(self, writer, ttFont): + data = tostr(self.data) + # removing null bytes. XXX needed?? + data = data.split('\0') + data = strjoin(data) + writer.begintag("source") + writer.newline() + writer.write_noindent(data.replace("\r", "\n")) + writer.newline() + writer.endtag("source") + writer.newline() + + def fromXML(self, name, attrs, content, ttFont): + lines = strjoin(content).split("\n") + self.data = tobytes("\r".join(lines[1:-1])) diff --git a/Lib/fontTools/ttLib/tables/T_S_I__0.py b/Lib/fontTools/ttLib/tables/T_S_I__0.py index bcd6d15..81808ee 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I__0.py +++ b/Lib/fontTools/ttLib/tables/T_S_I__0.py @@ -1,18 +1,25 @@ +""" TSI{0,1,2,3,5} are private tables used by Microsoft Visual TrueType (VTT) +tool to store its hinting source data. + +TSI0 is the index table containing the lengths and offsets for the glyph +programs and 'extra' programs ('fpgm', 'prep', and 'cvt') that are contained +in the TSI1 table. +""" from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from . import DefaultTable import struct -tsi0Format = '>HHl' +tsi0Format = '>HHL' def fixlongs(glyphID, textLength, textOffset): - return int(glyphID), int(textLength), textOffset + return int(glyphID), int(textLength), textOffset class table_T_S_I__0(DefaultTable.DefaultTable): - + dependencies = ["TSI1"] - + def decompile(self, data, ttFont): numGlyphs = ttFont['maxp'].numGlyphs indices = [] @@ -22,29 +29,28 @@ class table_T_S_I__0(DefaultTable.DefaultTable): indices.append((glyphID, textLength, textOffset)) data = data[size:] assert len(data) == 0 - assert indices[-5] == (0XFFFE, 0, -1409540300), "bad magic number" # 0xABFC1F34 + assert indices[-5] == (0XFFFE, 0, 0xABFC1F34), "bad magic number" self.indices = indices[:-5] self.extra_indices = indices[-4:] - + def compile(self, ttFont): if not hasattr(self, "indices"): # We have no corresponding table (TSI1 or TSI3); let's return # no data, which effectively means "ignore us". - return "" + return b"" data = b"" for index, textLength, textOffset in self.indices: data = data + struct.pack(tsi0Format, index, textLength, textOffset) - data = data + struct.pack(tsi0Format, 0XFFFE, 0, -1409540300) # 0xABFC1F34 + data = data + struct.pack(tsi0Format, 0XFFFE, 0, 0xABFC1F34) for index, textLength, textOffset in self.extra_indices: data = data + struct.pack(tsi0Format, index, textLength, textOffset) return data - + def set(self, indices, extra_indices): # gets called by 'TSI1' or 'TSI3' self.indices = indices self.extra_indices = extra_indices - + def toXML(self, writer, ttFont): writer.comment("This table will be calculated by the compiler") writer.newline() - diff --git a/Lib/fontTools/ttLib/tables/T_S_I__1.py b/Lib/fontTools/ttLib/tables/T_S_I__1.py index 558ce9d..5ef944a 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I__1.py +++ b/Lib/fontTools/ttLib/tables/T_S_I__1.py @@ -1,41 +1,82 @@ +""" TSI{0,1,2,3,5} are private tables used by Microsoft Visual TrueType (VTT) +tool to store its hinting source data. + +TSI1 contains the text of the glyph programs in the form of low-level assembly +code, as well as the 'extra' programs 'fpgm', 'ppgm' (i.e. 'prep'), and 'cvt'. +""" from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from . import DefaultTable +from fontTools.misc.loggingTools import LogMixin + + +class table_T_S_I__1(LogMixin, DefaultTable.DefaultTable): -class table_T_S_I__1(DefaultTable.DefaultTable): - extras = {0xfffa: "ppgm", 0xfffb: "cvt", 0xfffc: "reserved", 0xfffd: "fpgm"} - + indextable = "TSI0" - + def decompile(self, data, ttFont): + totalLength = len(data) indextable = ttFont[self.indextable] - self.glyphPrograms = {} - for i in range(len(indextable.indices)): - glyphID, textLength, textOffset = indextable.indices[i] - if textLength == 0x8000: - # Ugh. Hi Beat! - textLength = indextable.indices[i+1][1] - if textLength > 0x8000: - pass # XXX Hmmm. - text = data[textOffset:textOffset+textLength] - assert len(text) == textLength - if text: - self.glyphPrograms[ttFont.getGlyphName(glyphID)] = text - - self.extraPrograms = {} - for i in range(len(indextable.extra_indices)): - extraCode, textLength, textOffset = indextable.extra_indices[i] - if textLength == 0x8000: - if self.extras[extraCode] == "fpgm": # this is the last one - textLength = len(data) - textOffset + for indices, isExtra in zip( + (indextable.indices, indextable.extra_indices), (False, True)): + programs = {} + for i, (glyphID, textLength, textOffset) in enumerate(indices): + if isExtra: + name = self.extras[glyphID] + else: + name = ttFont.getGlyphName(glyphID) + if textOffset > totalLength: + self.log.warning("textOffset > totalLength; %r skipped" % name) + continue + if textLength < 0x8000: + # If the length stored in the record is less than 32768, then use + # that as the length of the record. + pass + elif textLength == 0x8000: + # If the length is 32768, compute the actual length as follows: + isLast = i == (len(indices)-1) + if isLast: + if isExtra: + # For the last "extra" record (the very last record of the + # table), the length is the difference between the total + # length of the TSI1 table and the textOffset of the final + # record. + nextTextOffset = totalLength + else: + # For the last "normal" record (the last record just prior + # to the record containing the "magic number"), the length + # is the difference between the textOffset of the record + # following the "magic number" (0xFFFE) record (i.e. the + # first "extra" record), and the textOffset of the last + # "normal" record. + nextTextOffset = indextable.extra_indices[0][2] + else: + # For all other records with a length of 0x8000, the length is + # the difference between the textOffset of the record in + # question and the textOffset of the next record. + nextTextOffset = indices[i+1][2] + assert nextTextOffset >= textOffset, "entries not sorted by offset" + if nextTextOffset > totalLength: + self.log.warning( + "nextTextOffset > totalLength; %r truncated" % name) + nextTextOffset = totalLength + textLength = nextTextOffset - textOffset else: - textLength = indextable.extra_indices[i+1][1] - text = data[textOffset:textOffset+textLength] - assert len(text) == textLength - if text: - self.extraPrograms[self.extras[extraCode]] = text - + from fontTools import ttLib + raise ttLib.TTLibError( + "%r textLength (%d) must not be > 32768" % (name, textLength)) + text = data[textOffset:textOffset+textLength] + assert len(text) == textLength + text = tounicode(text, encoding='utf-8') + if text: + programs[name] = text + if isExtra: + self.extraPrograms = programs + else: + self.glyphPrograms = programs + def compile(self, ttFont): if not hasattr(self, "glyphPrograms"): self.glyphPrograms = {} @@ -43,22 +84,22 @@ class table_T_S_I__1(DefaultTable.DefaultTable): data = b'' indextable = ttFont[self.indextable] glyphNames = ttFont.getGlyphOrder() - + indices = [] for i in range(len(glyphNames)): if len(data) % 2: data = data + b"\015" # align on 2-byte boundaries, fill with return chars. Yum. name = glyphNames[i] if name in self.glyphPrograms: - text = self.glyphPrograms[name] + text = tobytes(self.glyphPrograms[name], encoding="utf-8") else: text = b"" textLength = len(text) if textLength >= 0x8000: - textLength = 0x8000 # XXX ??? + textLength = 0x8000 indices.append((i, textLength, len(data))) data = data + text - + extra_indices = [] codes = sorted(self.extras.items()) for i in range(len(codes)): @@ -66,17 +107,17 @@ class table_T_S_I__1(DefaultTable.DefaultTable): data = data + b"\015" # align on 2-byte boundaries, fill with return chars. code, name = codes[i] if name in self.extraPrograms: - text = self.extraPrograms[name] + text = tobytes(self.extraPrograms[name], encoding="utf-8") else: text = b"" textLength = len(text) if textLength >= 0x8000: - textLength = 0x8000 # XXX ??? + textLength = 0x8000 extra_indices.append((code, textLength, len(data))) data = data + text indextable.set(indices, extra_indices) return data - + def toXML(self, writer, ttFont): names = sorted(self.glyphPrograms.keys()) writer.newline() @@ -103,7 +144,7 @@ class table_T_S_I__1(DefaultTable.DefaultTable): writer.endtag("extraProgram") writer.newline() writer.newline() - + def fromXML(self, name, attrs, content, ttFont): if not hasattr(self, "glyphPrograms"): self.glyphPrograms = {} @@ -114,4 +155,3 @@ class table_T_S_I__1(DefaultTable.DefaultTable): self.glyphPrograms[attrs["name"]] = text elif name == "extraProgram": self.extraPrograms[attrs["name"]] = text - diff --git a/Lib/fontTools/ttLib/tables/T_S_I__2.py b/Lib/fontTools/ttLib/tables/T_S_I__2.py index 15c02ab..7d41ee8 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I__2.py +++ b/Lib/fontTools/ttLib/tables/T_S_I__2.py @@ -1,8 +1,16 @@ +""" TSI{0,1,2,3,5} are private tables used by Microsoft Visual TrueType (VTT) +tool to store its hinting source data. + +TSI2 is the index table containing the lengths and offsets for the glyph +programs that are contained in the TSI3 table. It uses the same format as +the TSI0 table. +""" +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * from fontTools import ttLib superclass = ttLib.getTableClass("TSI0") class table_T_S_I__2(superclass): - - dependencies = ["TSI3"] + dependencies = ["TSI3"] diff --git a/Lib/fontTools/ttLib/tables/T_S_I__3.py b/Lib/fontTools/ttLib/tables/T_S_I__3.py index eb4087c..ee95d06 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I__3.py +++ b/Lib/fontTools/ttLib/tables/T_S_I__3.py @@ -1,11 +1,16 @@ +""" TSI{0,1,2,3,5} are private tables used by Microsoft Visual TrueType (VTT) +tool to store its hinting source data. + +TSI3 contains the text of the glyph programs in the form of 'VTTTalk' code. +""" +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * from fontTools import ttLib superclass = ttLib.getTableClass("TSI1") class table_T_S_I__3(superclass): - - extras = {0xfffa: "reserved0", 0xfffb: "reserved1", 0xfffc: "reserved2", 0xfffd: "reserved3"} - - indextable = "TSI2" + extras = {0xfffa: "reserved0", 0xfffb: "reserved1", 0xfffc: "reserved2", 0xfffd: "reserved3"} + indextable = "TSI2" diff --git a/Lib/fontTools/ttLib/tables/T_S_I__5.py b/Lib/fontTools/ttLib/tables/T_S_I__5.py index 8fa801b..dbf9e5a 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I__5.py +++ b/Lib/fontTools/ttLib/tables/T_S_I__5.py @@ -1,3 +1,8 @@ +""" TSI{0,1,2,3,5} are private tables used by Microsoft Visual TrueType (VTT) +tool to store its hinting source data. + +TSI5 contains the VTT character groups. +""" from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.misc.textTools import safeEval @@ -7,7 +12,7 @@ import array class table_T_S_I__5(DefaultTable.DefaultTable): - + def decompile(self, data, ttFont): numGlyphs = ttFont['maxp'].numGlyphs assert len(data) == 2 * numGlyphs @@ -18,26 +23,25 @@ class table_T_S_I__5(DefaultTable.DefaultTable): self.glyphGrouping = {} for i in range(numGlyphs): self.glyphGrouping[ttFont.getGlyphName(i)] = a[i] - + def compile(self, ttFont): glyphNames = ttFont.getGlyphOrder() a = array.array("H") for i in range(len(glyphNames)): - a.append(self.glyphGrouping[glyphNames[i]]) + a.append(self.glyphGrouping.get(glyphNames[i], 0)) if sys.byteorder != "big": a.byteswap() return a.tostring() - + def toXML(self, writer, ttFont): names = sorted(self.glyphGrouping.keys()) for glyphName in names: writer.simpletag("glyphgroup", name=glyphName, value=self.glyphGrouping[glyphName]) writer.newline() - + def fromXML(self, name, attrs, content, ttFont): if not hasattr(self, "glyphGrouping"): self.glyphGrouping = {} if name != "glyphgroup": return self.glyphGrouping[attrs["name"]] = safeEval(attrs["value"]) - diff --git a/Lib/fontTools/ttLib/tables/T_T_F_A_.py b/Lib/fontTools/ttLib/tables/T_T_F_A_.py new file mode 100644 index 0000000..8ff8d1b --- /dev/null +++ b/Lib/fontTools/ttLib/tables/T_T_F_A_.py @@ -0,0 +1,6 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from . import asciiTable + +class table_T_T_F_A_(asciiTable.asciiTable): + pass diff --git a/Lib/fontTools/ttLib/tables/TupleVariation.py b/Lib/fontTools/ttLib/tables/TupleVariation.py new file mode 100644 index 0000000..5fa71c8 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/TupleVariation.py @@ -0,0 +1,623 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import fixedToFloat, floatToFixed, otRound +from fontTools.misc.textTools import safeEval +import array +import io +import logging +import struct +import sys + + +# https://www.microsoft.com/typography/otspec/otvarcommonformats.htm + +EMBEDDED_PEAK_TUPLE = 0x8000 +INTERMEDIATE_REGION = 0x4000 +PRIVATE_POINT_NUMBERS = 0x2000 + +DELTAS_ARE_ZERO = 0x80 +DELTAS_ARE_WORDS = 0x40 +DELTA_RUN_COUNT_MASK = 0x3f + +POINTS_ARE_WORDS = 0x80 +POINT_RUN_COUNT_MASK = 0x7f + +TUPLES_SHARE_POINT_NUMBERS = 0x8000 +TUPLE_COUNT_MASK = 0x0fff +TUPLE_INDEX_MASK = 0x0fff + +log = logging.getLogger(__name__) + + +class TupleVariation(object): + def __init__(self, axes, coordinates): + self.axes = axes.copy() + self.coordinates = coordinates[:] + + def __repr__(self): + axes = ",".join(sorted(["%s=%s" % (name, value) for (name, value) in self.axes.items()])) + return "<TupleVariation %s %s>" % (axes, self.coordinates) + + def __eq__(self, other): + return self.coordinates == other.coordinates and self.axes == other.axes + + def getUsedPoints(self): + result = set() + for i, point in enumerate(self.coordinates): + if point is not None: + result.add(i) + return result + + def hasImpact(self): + """Returns True if this TupleVariation has any visible impact. + + If the result is False, the TupleVariation can be omitted from the font + without making any visible difference. + """ + for c in self.coordinates: + if c is not None: + return True + return False + + def toXML(self, writer, axisTags): + writer.begintag("tuple") + writer.newline() + for axis in axisTags: + value = self.axes.get(axis) + if value is not None: + minValue, value, maxValue = (float(v) for v in value) + defaultMinValue = min(value, 0.0) # -0.3 --> -0.3; 0.7 --> 0.0 + defaultMaxValue = max(value, 0.0) # -0.3 --> 0.0; 0.7 --> 0.7 + if minValue == defaultMinValue and maxValue == defaultMaxValue: + writer.simpletag("coord", axis=axis, value=value) + else: + writer.simpletag("coord", axis=axis, value=value, min=minValue, max=maxValue) + writer.newline() + wrote_any_deltas = False + for i, delta in enumerate(self.coordinates): + if type(delta) == tuple and len(delta) == 2: + writer.simpletag("delta", pt=i, x=delta[0], y=delta[1]) + writer.newline() + wrote_any_deltas = True + elif type(delta) == int: + writer.simpletag("delta", cvt=i, value=delta) + writer.newline() + wrote_any_deltas = True + elif delta is not None: + log.error("bad delta format") + writer.comment("bad delta #%d" % i) + writer.newline() + wrote_any_deltas = True + if not wrote_any_deltas: + writer.comment("no deltas") + writer.newline() + writer.endtag("tuple") + writer.newline() + + def fromXML(self, name, attrs, _content): + if name == "coord": + axis = attrs["axis"] + value = float(attrs["value"]) + defaultMinValue = min(value, 0.0) # -0.3 --> -0.3; 0.7 --> 0.0 + defaultMaxValue = max(value, 0.0) # -0.3 --> 0.0; 0.7 --> 0.7 + minValue = float(attrs.get("min", defaultMinValue)) + maxValue = float(attrs.get("max", defaultMaxValue)) + self.axes[axis] = (minValue, value, maxValue) + elif name == "delta": + if "pt" in attrs: + point = safeEval(attrs["pt"]) + x = safeEval(attrs["x"]) + y = safeEval(attrs["y"]) + self.coordinates[point] = (x, y) + elif "cvt" in attrs: + cvt = safeEval(attrs["cvt"]) + value = safeEval(attrs["value"]) + self.coordinates[cvt] = value + else: + log.warning("bad delta format: %s" % + ", ".join(sorted(attrs.keys()))) + + def compile(self, axisTags, sharedCoordIndices, sharedPoints): + tupleData = [] + + assert all(tag in axisTags for tag in self.axes.keys()), ("Unknown axis tag found.", self.axes.keys(), axisTags) + + coord = self.compileCoord(axisTags) + if coord in sharedCoordIndices: + flags = sharedCoordIndices[coord] + else: + flags = EMBEDDED_PEAK_TUPLE + tupleData.append(coord) + + intermediateCoord = self.compileIntermediateCoord(axisTags) + if intermediateCoord is not None: + flags |= INTERMEDIATE_REGION + tupleData.append(intermediateCoord) + + points = self.getUsedPoints() + if sharedPoints == points: + # Only use the shared points if they are identical to the actually used points + auxData = self.compileDeltas(sharedPoints) + usesSharedPoints = True + else: + flags |= PRIVATE_POINT_NUMBERS + numPointsInGlyph = len(self.coordinates) + auxData = self.compilePoints(points, numPointsInGlyph) + self.compileDeltas(points) + usesSharedPoints = False + + tupleData = struct.pack('>HH', len(auxData), flags) + bytesjoin(tupleData) + return (tupleData, auxData, usesSharedPoints) + + def compileCoord(self, axisTags): + result = [] + for axis in axisTags: + _minValue, value, _maxValue = self.axes.get(axis, (0.0, 0.0, 0.0)) + result.append(struct.pack(">h", floatToFixed(value, 14))) + return bytesjoin(result) + + def compileIntermediateCoord(self, axisTags): + needed = False + for axis in axisTags: + minValue, value, maxValue = self.axes.get(axis, (0.0, 0.0, 0.0)) + defaultMinValue = min(value, 0.0) # -0.3 --> -0.3; 0.7 --> 0.0 + defaultMaxValue = max(value, 0.0) # -0.3 --> 0.0; 0.7 --> 0.7 + if (minValue != defaultMinValue) or (maxValue != defaultMaxValue): + needed = True + break + if not needed: + return None + minCoords = [] + maxCoords = [] + for axis in axisTags: + minValue, value, maxValue = self.axes.get(axis, (0.0, 0.0, 0.0)) + minCoords.append(struct.pack(">h", floatToFixed(minValue, 14))) + maxCoords.append(struct.pack(">h", floatToFixed(maxValue, 14))) + return bytesjoin(minCoords + maxCoords) + + @staticmethod + def decompileCoord_(axisTags, data, offset): + coord = {} + pos = offset + for axis in axisTags: + coord[axis] = fixedToFloat(struct.unpack(">h", data[pos:pos+2])[0], 14) + pos += 2 + return coord, pos + + @staticmethod + def compilePoints(points, numPointsInGlyph): + # If the set consists of all points in the glyph, it gets encoded with + # a special encoding: a single zero byte. + if len(points) == numPointsInGlyph: + return b"\0" + + # In the 'gvar' table, the packing of point numbers is a little surprising. + # It consists of multiple runs, each being a delta-encoded list of integers. + # For example, the point set {17, 18, 19, 20, 21, 22, 23} gets encoded as + # [6, 17, 1, 1, 1, 1, 1, 1]. The first value (6) is the run length minus 1. + # There are two types of runs, with values being either 8 or 16 bit unsigned + # integers. + points = list(points) + points.sort() + numPoints = len(points) + + # The binary representation starts with the total number of points in the set, + # encoded into one or two bytes depending on the value. + if numPoints < 0x80: + result = [bytechr(numPoints)] + else: + result = [bytechr((numPoints >> 8) | 0x80) + bytechr(numPoints & 0xff)] + + MAX_RUN_LENGTH = 127 + pos = 0 + lastValue = 0 + while pos < numPoints: + run = io.BytesIO() + runLength = 0 + useByteEncoding = None + while pos < numPoints and runLength <= MAX_RUN_LENGTH: + curValue = points[pos] + delta = curValue - lastValue + if useByteEncoding is None: + useByteEncoding = 0 <= delta <= 0xff + if useByteEncoding and (delta > 0xff or delta < 0): + # we need to start a new run (which will not use byte encoding) + break + # TODO This never switches back to a byte-encoding from a short-encoding. + # That's suboptimal. + if useByteEncoding: + run.write(bytechr(delta)) + else: + run.write(bytechr(delta >> 8)) + run.write(bytechr(delta & 0xff)) + lastValue = curValue + pos += 1 + runLength += 1 + if useByteEncoding: + runHeader = bytechr(runLength - 1) + else: + runHeader = bytechr((runLength - 1) | POINTS_ARE_WORDS) + result.append(runHeader) + result.append(run.getvalue()) + + return bytesjoin(result) + + @staticmethod + def decompilePoints_(numPoints, data, offset, tableTag): + """(numPoints, data, offset, tableTag) --> ([point1, point2, ...], newOffset)""" + assert tableTag in ('cvar', 'gvar') + pos = offset + numPointsInData = byteord(data[pos]) + pos += 1 + if (numPointsInData & POINTS_ARE_WORDS) != 0: + numPointsInData = (numPointsInData & POINT_RUN_COUNT_MASK) << 8 | byteord(data[pos]) + pos += 1 + if numPointsInData == 0: + return (range(numPoints), pos) + + result = [] + while len(result) < numPointsInData: + runHeader = byteord(data[pos]) + pos += 1 + numPointsInRun = (runHeader & POINT_RUN_COUNT_MASK) + 1 + point = 0 + if (runHeader & POINTS_ARE_WORDS) != 0: + points = array.array("H") + pointsSize = numPointsInRun * 2 + else: + points = array.array("B") + pointsSize = numPointsInRun + points.fromstring(data[pos:pos+pointsSize]) + if sys.byteorder != "big": + points.byteswap() + + assert len(points) == numPointsInRun + pos += pointsSize + + result.extend(points) + + # Convert relative to absolute + absolute = [] + current = 0 + for delta in result: + current += delta + absolute.append(current) + result = absolute + del absolute + + badPoints = {str(p) for p in result if p < 0 or p >= numPoints} + if badPoints: + log.warning("point %s out of range in '%s' table" % + (",".join(sorted(badPoints)), tableTag)) + return (result, pos) + + def compileDeltas(self, points): + deltaX = [] + deltaY = [] + for p in sorted(list(points)): + c = self.coordinates[p] + if type(c) is tuple and len(c) == 2: + deltaX.append(c[0]) + deltaY.append(c[1]) + elif type(c) is int: + deltaX.append(c) + elif c is not None: + raise ValueError("invalid type of delta: %s" % type(c)) + return self.compileDeltaValues_(deltaX) + self.compileDeltaValues_(deltaY) + + @staticmethod + def compileDeltaValues_(deltas): + """[value1, value2, value3, ...] --> bytestring + + Emits a sequence of runs. Each run starts with a + byte-sized header whose 6 least significant bits + (header & 0x3F) indicate how many values are encoded + in this run. The stored length is the actual length + minus one; run lengths are thus in the range [1..64]. + If the header byte has its most significant bit (0x80) + set, all values in this run are zero, and no data + follows. Otherwise, the header byte is followed by + ((header & 0x3F) + 1) signed values. If (header & + 0x40) is clear, the delta values are stored as signed + bytes; if (header & 0x40) is set, the delta values are + signed 16-bit integers. + """ # Explaining the format because the 'gvar' spec is hard to understand. + stream = io.BytesIO() + pos = 0 + while pos < len(deltas): + value = deltas[pos] + if value == 0: + pos = TupleVariation.encodeDeltaRunAsZeroes_(deltas, pos, stream) + elif value >= -128 and value <= 127: + pos = TupleVariation.encodeDeltaRunAsBytes_(deltas, pos, stream) + else: + pos = TupleVariation.encodeDeltaRunAsWords_(deltas, pos, stream) + return stream.getvalue() + + @staticmethod + def encodeDeltaRunAsZeroes_(deltas, offset, stream): + runLength = 0 + pos = offset + numDeltas = len(deltas) + while pos < numDeltas and runLength < 64 and deltas[pos] == 0: + pos += 1 + runLength += 1 + assert runLength >= 1 and runLength <= 64 + stream.write(bytechr(DELTAS_ARE_ZERO | (runLength - 1))) + return pos + + @staticmethod + def encodeDeltaRunAsBytes_(deltas, offset, stream): + runLength = 0 + pos = offset + numDeltas = len(deltas) + while pos < numDeltas and runLength < 64: + value = deltas[pos] + if value < -128 or value > 127: + break + # Within a byte-encoded run of deltas, a single zero + # is best stored literally as 0x00 value. However, + # if are two or more zeroes in a sequence, it is + # better to start a new run. For example, the sequence + # of deltas [15, 15, 0, 15, 15] becomes 6 bytes + # (04 0F 0F 00 0F 0F) when storing the zero value + # literally, but 7 bytes (01 0F 0F 80 01 0F 0F) + # when starting a new run. + if value == 0 and pos+1 < numDeltas and deltas[pos+1] == 0: + break + pos += 1 + runLength += 1 + assert runLength >= 1 and runLength <= 64 + stream.write(bytechr(runLength - 1)) + for i in range(offset, pos): + stream.write(struct.pack('b', otRound(deltas[i]))) + return pos + + @staticmethod + def encodeDeltaRunAsWords_(deltas, offset, stream): + runLength = 0 + pos = offset + numDeltas = len(deltas) + while pos < numDeltas and runLength < 64: + value = deltas[pos] + # Within a word-encoded run of deltas, it is easiest + # to start a new run (with a different encoding) + # whenever we encounter a zero value. For example, + # the sequence [0x6666, 0, 0x7777] needs 7 bytes when + # storing the zero literally (42 66 66 00 00 77 77), + # and equally 7 bytes when starting a new run + # (40 66 66 80 40 77 77). + if value == 0: + break + + # Within a word-encoded run of deltas, a single value + # in the range (-128..127) should be encoded literally + # because it is more compact. For example, the sequence + # [0x6666, 2, 0x7777] becomes 7 bytes when storing + # the value literally (42 66 66 00 02 77 77), but 8 bytes + # when starting a new run (40 66 66 00 02 40 77 77). + isByteEncodable = lambda value: value >= -128 and value <= 127 + if isByteEncodable(value) and pos+1 < numDeltas and isByteEncodable(deltas[pos+1]): + break + pos += 1 + runLength += 1 + assert runLength >= 1 and runLength <= 64 + stream.write(bytechr(DELTAS_ARE_WORDS | (runLength - 1))) + for i in range(offset, pos): + stream.write(struct.pack('>h', otRound(deltas[i]))) + return pos + + @staticmethod + def decompileDeltas_(numDeltas, data, offset): + """(numDeltas, data, offset) --> ([delta, delta, ...], newOffset)""" + result = [] + pos = offset + while len(result) < numDeltas: + runHeader = byteord(data[pos]) + pos += 1 + numDeltasInRun = (runHeader & DELTA_RUN_COUNT_MASK) + 1 + if (runHeader & DELTAS_ARE_ZERO) != 0: + result.extend([0] * numDeltasInRun) + else: + if (runHeader & DELTAS_ARE_WORDS) != 0: + deltas = array.array("h") + deltasSize = numDeltasInRun * 2 + else: + deltas = array.array("b") + deltasSize = numDeltasInRun + deltas.fromstring(data[pos:pos+deltasSize]) + if sys.byteorder != "big": + deltas.byteswap() + assert len(deltas) == numDeltasInRun + pos += deltasSize + result.extend(deltas) + assert len(result) == numDeltas + return (result, pos) + + @staticmethod + def getTupleSize_(flags, axisCount): + size = 4 + if (flags & EMBEDDED_PEAK_TUPLE) != 0: + size += axisCount * 2 + if (flags & INTERMEDIATE_REGION) != 0: + size += axisCount * 4 + return size + + +def decompileSharedTuples(axisTags, sharedTupleCount, data, offset): + result = [] + for _ in range(sharedTupleCount): + t, offset = TupleVariation.decompileCoord_(axisTags, data, offset) + result.append(t) + return result + + +def compileSharedTuples(axisTags, variations): + coordCount = {} + for var in variations: + coord = var.compileCoord(axisTags) + coordCount[coord] = coordCount.get(coord, 0) + 1 + sharedCoords = [(count, coord) + for (coord, count) in coordCount.items() if count > 1] + sharedCoords.sort(reverse=True) + MAX_NUM_SHARED_COORDS = TUPLE_INDEX_MASK + 1 + sharedCoords = sharedCoords[:MAX_NUM_SHARED_COORDS] + return [c[1] for c in sharedCoords] # Strip off counts. + + +def compileTupleVariationStore(variations, pointCount, + axisTags, sharedTupleIndices, + useSharedPoints=True): + variations = [v for v in variations if v.hasImpact()] + if len(variations) == 0: + return (0, b"", b"") + + # Each glyph variation tuples modifies a set of control points. To + # indicate which exact points are getting modified, a single tuple + # can either refer to a shared set of points, or the tuple can + # supply its private point numbers. Because the impact of sharing + # can be positive (no need for a private point list) or negative + # (need to supply 0,0 deltas for unused points), it is not obvious + # how to determine which tuples should take their points from the + # shared pool versus have their own. Perhaps we should resort to + # brute force, and try all combinations? However, if a glyph has n + # variation tuples, we would need to try 2^n combinations (because + # each tuple may or may not be part of the shared set). How many + # variations tuples do glyphs have? + # + # Skia.ttf: {3: 1, 5: 11, 6: 41, 7: 62, 8: 387, 13: 1, 14: 3} + # JamRegular.ttf: {3: 13, 4: 122, 5: 1, 7: 4, 8: 1, 9: 1, 10: 1} + # BuffaloGalRegular.ttf: {1: 16, 2: 13, 4: 2, 5: 4, 6: 19, 7: 1, 8: 3, 9: 8} + # (Reading example: In Skia.ttf, 41 glyphs have 6 variation tuples). + # + + # Is this even worth optimizing? If we never use a shared point + # list, the private lists will consume 112K for Skia, 5K for + # BuffaloGalRegular, and 15K for JamRegular. If we always use a + # shared point list, the shared lists will consume 16K for Skia, + # 3K for BuffaloGalRegular, and 10K for JamRegular. However, in + # the latter case the delta arrays will become larger, but I + # haven't yet measured by how much. From gut feeling (which may be + # wrong), the optimum is to share some but not all points; + # however, then we would need to try all combinations. + # + # For the time being, we try two variants and then pick the better one: + # (a) each tuple supplies its own private set of points; + # (b) all tuples refer to a shared set of points, which consists of + # "every control point in the glyph that has explicit deltas". + usedPoints = set() + for v in variations: + usedPoints |= v.getUsedPoints() + tuples = [] + data = [] + someTuplesSharePoints = False + sharedPointVariation = None # To keep track of a variation that uses shared points + for v in variations: + privateTuple, privateData, _ = v.compile( + axisTags, sharedTupleIndices, sharedPoints=None) + sharedTuple, sharedData, usesSharedPoints = v.compile( + axisTags, sharedTupleIndices, sharedPoints=usedPoints) + if useSharedPoints and (len(sharedTuple) + len(sharedData)) < (len(privateTuple) + len(privateData)): + tuples.append(sharedTuple) + data.append(sharedData) + someTuplesSharePoints |= usesSharedPoints + sharedPointVariation = v + else: + tuples.append(privateTuple) + data.append(privateData) + if someTuplesSharePoints: + # Use the last of the variations that share points for compiling the packed point data + data = sharedPointVariation.compilePoints(usedPoints, len(sharedPointVariation.coordinates)) + bytesjoin(data) + tupleVariationCount = TUPLES_SHARE_POINT_NUMBERS | len(tuples) + else: + data = bytesjoin(data) + tupleVariationCount = len(tuples) + tuples = bytesjoin(tuples) + return tupleVariationCount, tuples, data + + +def decompileTupleVariationStore(tableTag, axisTags, + tupleVariationCount, pointCount, sharedTuples, + data, pos, dataPos): + numAxes = len(axisTags) + result = [] + if (tupleVariationCount & TUPLES_SHARE_POINT_NUMBERS) != 0: + sharedPoints, dataPos = TupleVariation.decompilePoints_( + pointCount, data, dataPos, tableTag) + else: + sharedPoints = [] + for _ in range(tupleVariationCount & TUPLE_COUNT_MASK): + dataSize, flags = struct.unpack(">HH", data[pos:pos+4]) + tupleSize = TupleVariation.getTupleSize_(flags, numAxes) + tupleData = data[pos : pos + tupleSize] + pointDeltaData = data[dataPos : dataPos + dataSize] + result.append(decompileTupleVariation_( + pointCount, sharedTuples, sharedPoints, + tableTag, axisTags, tupleData, pointDeltaData)) + pos += tupleSize + dataPos += dataSize + return result + + +def decompileTupleVariation_(pointCount, sharedTuples, sharedPoints, + tableTag, axisTags, data, tupleData): + assert tableTag in ("cvar", "gvar"), tableTag + flags = struct.unpack(">H", data[2:4])[0] + pos = 4 + if (flags & EMBEDDED_PEAK_TUPLE) == 0: + peak = sharedTuples[flags & TUPLE_INDEX_MASK] + else: + peak, pos = TupleVariation.decompileCoord_(axisTags, data, pos) + if (flags & INTERMEDIATE_REGION) != 0: + start, pos = TupleVariation.decompileCoord_(axisTags, data, pos) + end, pos = TupleVariation.decompileCoord_(axisTags, data, pos) + else: + start, end = inferRegion_(peak) + axes = {} + for axis in axisTags: + region = start[axis], peak[axis], end[axis] + if region != (0.0, 0.0, 0.0): + axes[axis] = region + pos = 0 + if (flags & PRIVATE_POINT_NUMBERS) != 0: + points, pos = TupleVariation.decompilePoints_( + pointCount, tupleData, pos, tableTag) + else: + points = sharedPoints + + deltas = [None] * pointCount + + if tableTag == "cvar": + deltas_cvt, pos = TupleVariation.decompileDeltas_( + len(points), tupleData, pos) + for p, delta in zip(points, deltas_cvt): + if 0 <= p < pointCount: + deltas[p] = delta + + elif tableTag == "gvar": + deltas_x, pos = TupleVariation.decompileDeltas_( + len(points), tupleData, pos) + deltas_y, pos = TupleVariation.decompileDeltas_( + len(points), tupleData, pos) + for p, x, y in zip(points, deltas_x, deltas_y): + if 0 <= p < pointCount: + deltas[p] = (x, y) + + return TupleVariation(axes, deltas) + + +def inferRegion_(peak): + """Infer start and end for a (non-intermediate) region + + This helper function computes the applicability region for + variation tuples whose INTERMEDIATE_REGION flag is not set in the + TupleVariationHeader structure. Variation tuples apply only to + certain regions of the variation space; outside that region, the + tuple has no effect. To make the binary encoding more compact, + TupleVariationHeaders can omit the intermediateStartTuple and + intermediateEndTuple fields. + """ + start, end = {}, {} + for (axis, value) in peak.items(): + start[axis] = min(value, 0.0) # -0.3 --> -0.3; 0.7 --> 0.0 + end[axis] = max(value, 0.0) # -0.3 --> 0.0; 0.7 --> 0.7 + return (start, end) diff --git a/Lib/fontTools/ttLib/tables/V_D_M_X_.py b/Lib/fontTools/ttLib/tables/V_D_M_X_.py new file mode 100644 index 0000000..abca3bf --- /dev/null +++ b/Lib/fontTools/ttLib/tables/V_D_M_X_.py @@ -0,0 +1,234 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from . import DefaultTable +from fontTools.misc import sstruct +from fontTools.misc.textTools import safeEval +import struct + +VDMX_HeaderFmt = """ + > # big endian + version: H # Version number (0 or 1) + numRecs: H # Number of VDMX groups present + numRatios: H # Number of aspect ratio groupings +""" +# the VMDX header is followed by an array of RatRange[numRatios] (i.e. aspect +# ratio ranges); +VDMX_RatRangeFmt = """ + > # big endian + bCharSet: B # Character set + xRatio: B # Value to use for x-Ratio + yStartRatio: B # Starting y-Ratio value + yEndRatio: B # Ending y-Ratio value +""" +# followed by an array of offset[numRatios] from start of VDMX table to the +# VDMX Group for this ratio range (offsets will be re-calculated on compile); +# followed by an array of Group[numRecs] records; +VDMX_GroupFmt = """ + > # big endian + recs: H # Number of height records in this group + startsz: B # Starting yPelHeight + endsz: B # Ending yPelHeight +""" +# followed by an array of vTable[recs] records. +VDMX_vTableFmt = """ + > # big endian + yPelHeight: H # yPelHeight to which values apply + yMax: h # Maximum value (in pels) for this yPelHeight + yMin: h # Minimum value (in pels) for this yPelHeight +""" + + +class table_V_D_M_X_(DefaultTable.DefaultTable): + + def decompile(self, data, ttFont): + pos = 0 # track current position from to start of VDMX table + dummy, data = sstruct.unpack2(VDMX_HeaderFmt, data, self) + pos += sstruct.calcsize(VDMX_HeaderFmt) + self.ratRanges = [] + for i in range(self.numRatios): + ratio, data = sstruct.unpack2(VDMX_RatRangeFmt, data) + pos += sstruct.calcsize(VDMX_RatRangeFmt) + # the mapping between a ratio and a group is defined further below + ratio['groupIndex'] = None + self.ratRanges.append(ratio) + lenOffset = struct.calcsize('>H') + _offsets = [] # temporarily store offsets to groups + for i in range(self.numRatios): + offset = struct.unpack('>H', data[0:lenOffset])[0] + data = data[lenOffset:] + pos += lenOffset + _offsets.append(offset) + self.groups = [] + for groupIndex in range(self.numRecs): + # the offset to this group from beginning of the VDMX table + currOffset = pos + group, data = sstruct.unpack2(VDMX_GroupFmt, data) + # the group lenght and bounding sizes are re-calculated on compile + recs = group.pop('recs') + startsz = group.pop('startsz') + endsz = group.pop('endsz') + pos += sstruct.calcsize(VDMX_GroupFmt) + for j in range(recs): + vTable, data = sstruct.unpack2(VDMX_vTableFmt, data) + vTableLength = sstruct.calcsize(VDMX_vTableFmt) + pos += vTableLength + # group is a dict of (yMax, yMin) tuples keyed by yPelHeight + group[vTable['yPelHeight']] = (vTable['yMax'], vTable['yMin']) + # make sure startsz and endsz match the calculated values + minSize = min(group.keys()) + maxSize = max(group.keys()) + assert startsz == minSize, \ + "startsz (%s) must equal min yPelHeight (%s): group %d" % \ + (group.startsz, minSize, groupIndex) + assert endsz == maxSize, \ + "endsz (%s) must equal max yPelHeight (%s): group %d" % \ + (group.endsz, maxSize, groupIndex) + self.groups.append(group) + # match the defined offsets with the current group's offset + for offsetIndex, offsetValue in enumerate(_offsets): + # when numRecs < numRatios there can more than one ratio range + # sharing the same VDMX group + if currOffset == offsetValue: + # map the group with the ratio range thas has the same + # index as the offset to that group (it took me a while..) + self.ratRanges[offsetIndex]['groupIndex'] = groupIndex + # check that all ratio ranges have a group + for i in range(self.numRatios): + ratio = self.ratRanges[i] + if ratio['groupIndex'] is None: + from fontTools import ttLib + raise ttLib.TTLibError( + "no group defined for ratRange %d" % i) + + def _getOffsets(self): + """ + Calculate offsets to VDMX_Group records. + For each ratRange return a list of offset values from the beginning of + the VDMX table to a VDMX_Group. + """ + lenHeader = sstruct.calcsize(VDMX_HeaderFmt) + lenRatRange = sstruct.calcsize(VDMX_RatRangeFmt) + lenOffset = struct.calcsize('>H') + lenGroupHeader = sstruct.calcsize(VDMX_GroupFmt) + lenVTable = sstruct.calcsize(VDMX_vTableFmt) + # offset to the first group + pos = lenHeader + self.numRatios*lenRatRange + self.numRatios*lenOffset + groupOffsets = [] + for group in self.groups: + groupOffsets.append(pos) + lenGroup = lenGroupHeader + len(group) * lenVTable + pos += lenGroup # offset to next group + offsets = [] + for ratio in self.ratRanges: + groupIndex = ratio['groupIndex'] + offsets.append(groupOffsets[groupIndex]) + return offsets + + def compile(self, ttFont): + if not(self.version == 0 or self.version == 1): + from fontTools import ttLib + raise ttLib.TTLibError( + "unknown format for VDMX table: version %s" % self.version) + data = sstruct.pack(VDMX_HeaderFmt, self) + for ratio in self.ratRanges: + data += sstruct.pack(VDMX_RatRangeFmt, ratio) + # recalculate offsets to VDMX groups + for offset in self._getOffsets(): + data += struct.pack('>H', offset) + for group in self.groups: + recs = len(group) + startsz = min(group.keys()) + endsz = max(group.keys()) + gHeader = {'recs': recs, 'startsz': startsz, 'endsz': endsz} + data += sstruct.pack(VDMX_GroupFmt, gHeader) + for yPelHeight, (yMax, yMin) in sorted(group.items()): + vTable = {'yPelHeight': yPelHeight, 'yMax': yMax, 'yMin': yMin} + data += sstruct.pack(VDMX_vTableFmt, vTable) + return data + + def toXML(self, writer, ttFont): + writer.simpletag("version", value=self.version) + writer.newline() + writer.begintag("ratRanges") + writer.newline() + for ratio in self.ratRanges: + groupIndex = ratio['groupIndex'] + writer.simpletag( + "ratRange", + bCharSet=ratio['bCharSet'], + xRatio=ratio['xRatio'], + yStartRatio=ratio['yStartRatio'], + yEndRatio=ratio['yEndRatio'], + groupIndex=groupIndex + ) + writer.newline() + writer.endtag("ratRanges") + writer.newline() + writer.begintag("groups") + writer.newline() + for groupIndex in range(self.numRecs): + group = self.groups[groupIndex] + recs = len(group) + startsz = min(group.keys()) + endsz = max(group.keys()) + writer.begintag("group", index=groupIndex) + writer.newline() + writer.comment("recs=%d, startsz=%d, endsz=%d" % + (recs, startsz, endsz)) + writer.newline() + for yPelHeight, (yMax, yMin) in sorted(group.items()): + writer.simpletag( + "record", + [('yPelHeight', yPelHeight), ('yMax', yMax), ('yMin', yMin)]) + writer.newline() + writer.endtag("group") + writer.newline() + writer.endtag("groups") + writer.newline() + + def fromXML(self, name, attrs, content, ttFont): + if name == "version": + self.version = safeEval(attrs["value"]) + elif name == "ratRanges": + if not hasattr(self, "ratRanges"): + self.ratRanges = [] + for element in content: + if not isinstance(element, tuple): + continue + name, attrs, content = element + if name == "ratRange": + if not hasattr(self, "numRatios"): + self.numRatios = 1 + else: + self.numRatios += 1 + ratio = { + "bCharSet": safeEval(attrs["bCharSet"]), + "xRatio": safeEval(attrs["xRatio"]), + "yStartRatio": safeEval(attrs["yStartRatio"]), + "yEndRatio": safeEval(attrs["yEndRatio"]), + "groupIndex": safeEval(attrs["groupIndex"]) + } + self.ratRanges.append(ratio) + elif name == "groups": + if not hasattr(self, "groups"): + self.groups = [] + for element in content: + if not isinstance(element, tuple): + continue + name, attrs, content = element + if name == "group": + if not hasattr(self, "numRecs"): + self.numRecs = 1 + else: + self.numRecs += 1 + group = {} + for element in content: + if not isinstance(element, tuple): + continue + name, attrs, content = element + if name == "record": + yPelHeight = safeEval(attrs["yPelHeight"]) + yMax = safeEval(attrs["yMax"]) + yMin = safeEval(attrs["yMin"]) + group[yPelHeight] = (yMax, yMin) + self.groups.append(group) diff --git a/Lib/fontTools/ttLib/tables/V_O_R_G_.py b/Lib/fontTools/ttLib/tables/V_O_R_G_.py index 19f25b5..8b2c317 100644 --- a/Lib/fontTools/ttLib/tables/V_O_R_G_.py +++ b/Lib/fontTools/ttLib/tables/V_O_R_G_.py @@ -37,12 +37,11 @@ class table_V_O_R_G_(DefaultTable.DefaultTable): list(map(operator.setitem, [vOrig]*self.numVertOriginYMetrics, names, vids)) - def compile(self, ttFont): vorgs = list(self.VOriginRecords.values()) names = list(self.VOriginRecords.keys()) nameMap = ttFont.getReverseGlyphMap() - lenRecords = len(vorgs) + lenRecords = len(vorgs) try: gids = map(operator.getitem, [nameMap]*lenRecords, names) except KeyError: @@ -94,7 +93,6 @@ class table_V_O_R_G_(DefaultTable.DefaultTable): elif "value" in attrs: setattr(self, name, safeEval(attrs["value"])) - def __getitem__(self, glyphSelector): if isinstance(glyphSelector, int): # its a gid, convert to glyph name @@ -102,7 +100,7 @@ class table_V_O_R_G_(DefaultTable.DefaultTable): if glyphSelector not in self.VOriginRecords: return self.defaultVertOriginY - + return self.VOriginRecords[glyphSelector] def __setitem__(self, glyphSelector, value): @@ -120,7 +118,7 @@ class table_V_O_R_G_(DefaultTable.DefaultTable): class VOriginRecord(object): - def __init__(self, name = None, vOrigin = None): + def __init__(self, name=None, vOrigin=None): self.glyphName = name self.vOrigin = vOrigin diff --git a/Lib/fontTools/ttLib/tables/V_V_A_R_.py b/Lib/fontTools/ttLib/tables/V_V_A_R_.py new file mode 100644 index 0000000..530f0e3 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/V_V_A_R_.py @@ -0,0 +1,7 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from .otBase import BaseTTXConverter + + +class table_V_V_A_R_(BaseTTXConverter): + pass diff --git a/Lib/fontTools/ttLib/tables/__init__.py b/Lib/fontTools/ttLib/tables/__init__.py index bdf8d96..79a50fd 100644 --- a/Lib/fontTools/ttLib/tables/__init__.py +++ b/Lib/fontTools/ttLib/tables/__init__.py @@ -1,29 +1,45 @@ + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * + # DON'T EDIT! This file is generated by MetaTools/buildTableList.py. def _moduleFinderHint(): """Dummy function to let modulefinder know what tables may be dynamically imported. Generated by MetaTools/buildTableList.py. + + >>> _moduleFinderHint() """ from . import B_A_S_E_ from . import C_B_D_T_ from . import C_B_L_C_ from . import C_F_F_ + from . import C_F_F__2 from . import C_O_L_R_ from . import C_P_A_L_ from . import D_S_I_G_ from . import E_B_D_T_ from . import E_B_L_C_ from . import F_F_T_M_ + from . import F__e_a_t from . import G_D_E_F_ from . import G_M_A_P_ from . import G_P_K_G_ from . import G_P_O_S_ from . import G_S_U_B_ + from . import G__l_a_t + from . import G__l_o_c + from . import H_V_A_R_ from . import J_S_T_F_ from . import L_T_S_H_ + from . import M_A_T_H_ from . import M_E_T_A_ + from . import M_V_A_R_ from . import O_S_2f_2 from . import S_I_N_G_ + from . import S_T_A_T_ from . import S_V_G_ + from . import S__i_l_f + from . import S__i_l_l from . import T_S_I_B_ from . import T_S_I_D_ from . import T_S_I_J_ @@ -35,22 +51,46 @@ def _moduleFinderHint(): from . import T_S_I__2 from . import T_S_I__3 from . import T_S_I__5 + from . import T_T_F_A_ + from . import V_D_M_X_ from . import V_O_R_G_ + from . import V_V_A_R_ + from . import _a_n_k_r + from . import _a_v_a_r + from . import _b_s_l_n + from . import _c_i_d_g from . import _c_m_a_p + from . import _c_v_a_r from . import _c_v_t + from . import _f_e_a_t from . import _f_p_g_m + from . import _f_v_a_r from . import _g_a_s_p + from . import _g_c_i_d from . import _g_l_y_f + from . import _g_v_a_r from . import _h_d_m_x from . import _h_e_a_d from . import _h_h_e_a from . import _h_m_t_x from . import _k_e_r_n + from . import _l_c_a_r from . import _l_o_c_a + from . import _l_t_a_g from . import _m_a_x_p + from . import _m_e_t_a + from . import _m_o_r_t + from . import _m_o_r_x from . import _n_a_m_e + from . import _o_p_b_d from . import _p_o_s_t from . import _p_r_e_p + from . import _p_r_o_p from . import _s_b_i_x + from . import _t_r_a_k from . import _v_h_e_a from . import _v_m_t_x + +if __name__ == "__main__": + import doctest, sys + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/ttLib/tables/_a_n_k_r.py b/Lib/fontTools/ttLib/tables/_a_n_k_r.py new file mode 100644 index 0000000..3a1aa08 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/_a_n_k_r.py @@ -0,0 +1,13 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from .otBase import BaseTTXConverter + + +# The anchor point table provides a way to define anchor points. +# These are points within the coordinate space of a given glyph, +# independent of the control points used to render the glyph. +# Anchor points are used in conjunction with the 'kerx' table. +# +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6ankr.html +class table__a_n_k_r(BaseTTXConverter): + pass diff --git a/Lib/fontTools/ttLib/tables/_a_v_a_r.py b/Lib/fontTools/ttLib/tables/_a_v_a_r.py new file mode 100644 index 0000000..57601c5 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/_a_v_a_r.py @@ -0,0 +1,101 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools import ttLib +from fontTools.misc import sstruct +from fontTools.misc.fixedTools import fixedToFloat, floatToFixed +from fontTools.misc.textTools import safeEval +from fontTools.ttLib import TTLibError +from . import DefaultTable +import array +import struct +import logging + + +log = logging.getLogger(__name__) + +# Apple's documentation of 'avar': +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6avar.html + +AVAR_HEADER_FORMAT = """ + > # big endian + majorVersion: H + minorVersion: H + reserved: H + axisCount: H +""" +assert sstruct.calcsize(AVAR_HEADER_FORMAT) == 8, sstruct.calcsize(AVAR_HEADER_FORMAT) + + +class table__a_v_a_r(DefaultTable.DefaultTable): + + dependencies = ["fvar"] + + def __init__(self, tag=None): + DefaultTable.DefaultTable.__init__(self, tag) + self.segments = {} + + def compile(self, ttFont): + axisTags = [axis.axisTag for axis in ttFont["fvar"].axes] + header = { + "majorVersion": 1, + "minorVersion": 0, + "reserved": 0, + "axisCount": len(axisTags) + } + result = [sstruct.pack(AVAR_HEADER_FORMAT, header)] + for axis in axisTags: + mappings = sorted(self.segments[axis].items()) + result.append(struct.pack(">H", len(mappings))) + for key, value in mappings: + fixedKey = floatToFixed(key, 14) + fixedValue = floatToFixed(value, 14) + result.append(struct.pack(">hh", fixedKey, fixedValue)) + return bytesjoin(result) + + def decompile(self, data, ttFont): + axisTags = [axis.axisTag for axis in ttFont["fvar"].axes] + header = {} + headerSize = sstruct.calcsize(AVAR_HEADER_FORMAT) + header = sstruct.unpack(AVAR_HEADER_FORMAT, data[0:headerSize]) + majorVersion = header["majorVersion"] + if majorVersion != 1: + raise TTLibError("unsupported 'avar' version %d" % majorVersion) + pos = headerSize + for axis in axisTags: + segments = self.segments[axis] = {} + numPairs = struct.unpack(">H", data[pos:pos+2])[0] + pos = pos + 2 + for _ in range(numPairs): + fromValue, toValue = struct.unpack(">hh", data[pos:pos+4]) + segments[fixedToFloat(fromValue, 14)] = fixedToFloat(toValue, 14) + pos = pos + 4 + + def toXML(self, writer, ttFont): + axisTags = [axis.axisTag for axis in ttFont["fvar"].axes] + for axis in axisTags: + writer.begintag("segment", axis=axis) + writer.newline() + for key, value in sorted(self.segments[axis].items()): + # roundtrip float -> fixed -> float to normalize TTX output + # as dumped after decompiling or straight from varLib + key = fixedToFloat(floatToFixed(key, 14), 14) + value = fixedToFloat(floatToFixed(value, 14), 14) + writer.simpletag("mapping", **{"from": key, "to": value}) + writer.newline() + writer.endtag("segment") + writer.newline() + + def fromXML(self, name, attrs, content, ttFont): + if name == "segment": + axis = attrs["axis"] + segment = self.segments[axis] = {} + for element in content: + if isinstance(element, tuple): + elementName, elementAttrs, _ = element + if elementName == "mapping": + fromValue = safeEval(elementAttrs["from"]) + toValue = safeEval(elementAttrs["to"]) + if fromValue in segment: + log.warning("duplicate entry for %s in axis '%s'", + fromValue, axis) + segment[fromValue] = toValue diff --git a/Lib/fontTools/ttLib/tables/_b_s_l_n.py b/Lib/fontTools/ttLib/tables/_b_s_l_n.py new file mode 100644 index 0000000..31d8399 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/_b_s_l_n.py @@ -0,0 +1,8 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from .otBase import BaseTTXConverter + + +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6bsln.html +class table__b_s_l_n(BaseTTXConverter): + pass diff --git a/Lib/fontTools/ttLib/tables/_c_i_d_g.py b/Lib/fontTools/ttLib/tables/_c_i_d_g.py new file mode 100644 index 0000000..1d6c502 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/_c_i_d_g.py @@ -0,0 +1,20 @@ +# coding: utf-8 +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from .otBase import BaseTTXConverter + + +# The AAT ‘cidg’ table has almost the same structure as ‘gidc’, +# just mapping CIDs to GlyphIDs instead of the reverse direction. +# +# It is useful for fonts that may be used by a PDF renderer in lieu of +# a font reference with a known glyph collection but no subsetted +# glyphs. For instance, a PDF can say “please use a font conforming +# to Adobe-Japan-1”; the ‘cidg’ mapping is necessary if the font is, +# say, a TrueType font. ‘gidc’ is lossy for this purpose and is +# obsoleted by ‘cidg’. +# +# For example, the first font in /System/Library/Fonts/PingFang.ttc +# (which Apple ships pre-installed on MacOS 10.12.6) has a ‘cidg’ table. +class table__c_i_d_g(BaseTTXConverter): + pass diff --git a/Lib/fontTools/ttLib/tables/_c_m_a_p.py b/Lib/fontTools/ttLib/tables/_c_m_a_p.py index 0519e78..eccf69e 100644 --- a/Lib/fontTools/ttLib/tables/_c_m_a_p.py +++ b/Lib/fontTools/ttLib/tables/_c_m_a_p.py @@ -1,6 +1,7 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.misc.textTools import safeEval, readHex +from fontTools.misc.encodingTools import getEncoding from fontTools.ttLib import getSearchRange from fontTools.unicode import Unicode from . import DefaultTable @@ -8,17 +9,65 @@ import sys import struct import array import operator +import logging +log = logging.getLogger(__name__) + + +def _make_map(font, chars, gids): + assert len(chars) == len(gids) + cmap = {} + glyphOrder = font.getGlyphOrder() + for char,gid in zip(chars,gids): + if gid is 0: + continue + try: + name = glyphOrder[gid] + except IndexError: + name = font.getGlyphName(gid) + cmap[char] = name + return cmap + class table__c_m_a_p(DefaultTable.DefaultTable): - + def getcmap(self, platformID, platEncID): for subtable in self.tables: - if (subtable.platformID == platformID and + if (subtable.platformID == platformID and subtable.platEncID == platEncID): return subtable return None # not found - + + def getBestCmap(self, cmapPreferences=((3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0))): + """Return the 'best' unicode cmap dictionary available in the font, + or None, if no unicode cmap subtable is available. + + By default it will search for the following (platformID, platEncID) + pairs: + (3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0) + This can be customized via the cmapPreferences argument. + """ + for platformID, platEncID in cmapPreferences: + cmapSubtable = self.getcmap(platformID, platEncID) + if cmapSubtable is not None: + return cmapSubtable.cmap + return None # None of the requested cmap subtables were found + + def buildReversed(self): + """Returns a reverse cmap such as {'one':{0x31}, 'A':{0x41,0x391}}. + + The values are sets of Unicode codepoints because + some fonts map different codepoints to the same glyph. + For example, U+0041 LATIN CAPITAL LETTER A and U+0391 + GREEK CAPITAL LETTER ALPHA are sometimes the same glyph. + """ + result = {} + for subtable in self.tables: + if subtable.isUnicode(): + for codepoint, name in subtable.cmap.items(): + result.setdefault(name, set()).add(codepoint) + return result + def decompile(self, data, ttFont): tableVersion, numSubTables = struct.unpack(">HH", data[:4]) self.tableVersion = int(tableVersion) @@ -33,14 +82,14 @@ class table__c_m_a_p(DefaultTable.DefaultTable): format, reserved, length = struct.unpack(">HHL", data[offset:offset+8]) elif format in [14]: format, length = struct.unpack(">HL", data[offset:offset+6]) - + if not length: - print("Error: cmap subtable is reported as having zero length: platformID %s, platEncID %s, format %s offset %s. Skipping table." % (platformID, platEncID,format, offset)) + log.error( + "cmap subtable is reported as having zero length: platformID %s, " + "platEncID %s, format %s offset %s. Skipping table.", + platformID, platEncID, format, offset) continue - if format not in cmap_classes: - table = cmap_format_unknown(format) - else: - table = cmap_classes[format](format) + table = CmapSubtable.newSubtable(format) table.platformID = platformID table.platEncID = platEncID # Note that by default we decompile only the subtable header info; @@ -48,11 +97,12 @@ class table__c_m_a_p(DefaultTable.DefaultTable): # subtable is referenced. table.decompileHeader(data[offset:offset+int(length)], ttFont) if offset in seenOffsets: + table.data = None # Mark as decompiled table.cmap = tables[seenOffsets[offset]].cmap else: seenOffsets[offset] = i tables.append(table) - + def compile(self, ttFont): self.tables.sort() # sort according to the spec; see CmapSubtable.__lt__() numSubTables = len(self.tables) @@ -73,13 +123,13 @@ class table__c_m_a_p(DefaultTable.DefaultTable): tableData = tableData + chunk data = data + struct.pack(">HHl", table.platformID, table.platEncID, offset) return data + tableData - + def toXML(self, writer, ttFont): writer.simpletag("tableVersion", version=self.tableVersion) writer.newline() for table in self.tables: table.toXML(writer, ttFont) - + def fromXML(self, name, attrs, content, ttFont): if name == "tableVersion": self.tableVersion = safeEval(attrs["version"]) @@ -89,10 +139,7 @@ class table__c_m_a_p(DefaultTable.DefaultTable): if not hasattr(self, "tables"): self.tables = [] format = safeEval(name[12:]) - if format not in cmap_classes: - table = cmap_format_unknown(format) - else: - table = cmap_classes[format](format) + table = CmapSubtable.newSubtable(format) table.platformID = safeEval(attrs["platformID"]) table.platEncID = safeEval(attrs["platEncID"]) table.fromXML(name, attrs, content, ttFont) @@ -100,7 +147,18 @@ class table__c_m_a_p(DefaultTable.DefaultTable): class CmapSubtable(object): - + + @staticmethod + def getSubtableClass(format): + """Return the subtable class for a format.""" + return cmap_classes.get(format, cmap_format_unknown) + + @staticmethod + def newSubtable(format): + """Return a new instance of a subtable for format.""" + subtableClass = CmapSubtable.getSubtableClass(format) + return subtableClass(format) + def __init__(self, format): self.format = format self.data = None @@ -113,11 +171,11 @@ class CmapSubtable(object): if self.data is None: raise AttributeError(attr) self.decompile(None, None) # use saved data. - self.data = None # Once this table has been decompiled, make sure we don't - # just return the original data. Also avoids recursion when - # called with an attribute that the cmap subtable doesn't have. + self.data = None # Once this table has been decompiled, make sure we don't + # just return the original data. Also avoids recursion when + # called with an attribute that the cmap subtable doesn't have. return getattr(self, attr) - + def decompileHeader(self, data, ttFont): format, length, language = struct.unpack(">HHH", data[:6]) assert len(data) == length, "corrupt cmap table format %d (data length: %d, header length: %d)" % (format, len(data), length) @@ -139,9 +197,21 @@ class CmapSubtable(object): writer.endtag(self.__class__.__name__) writer.newline() + def getEncoding(self, default=None): + """Returns the Python encoding name for this cmap subtable based on its platformID, + platEncID, and language. If encoding for these values is not known, by default + None is returned. That can be overriden by passing a value to the default + argument. + + Note that if you want to choose a "preferred" cmap subtable, most of the time + self.isUnicode() is what you want as that one only returns true for the modern, + commonly used, Unicode-compatible triplets, not the legacy ones. + """ + return getEncoding(self.platformID, self.platEncID, self.language, default) + def isUnicode(self): return (self.platformID == 0 or - (self.platformID == 3 and self.platEncID in [1, 10])) + (self.platformID == 3 and self.platEncID in [0, 1, 10])) def isSymbol(self): return self.platformID == 3 and self.platEncID == 0 @@ -153,7 +223,7 @@ class CmapSubtable(object): if isUnicode: writer.comment(Unicode[code]) writer.newline() - + def __lt__(self, other): if not isinstance(other, CmapSubtable): return NotImplemented @@ -173,40 +243,35 @@ class CmapSubtable(object): class cmap_format_0(CmapSubtable): - + def decompile(self, data, ttFont): # we usually get here indirectly from the subtable __getattr__ function, in which case both args must be None. # If not, someone is calling the subtable decompile() directly, and must provide both args. if data is not None and ttFont is not None: - self.decompileHeader(data[offset:offset+int(length)], ttFont) + self.decompileHeader(data, ttFont) else: assert (data is None and ttFont is None), "Need both data and ttFont arguments" data = self.data # decompileHeader assigns the data after the header to self.data assert 262 == self.length, "Format 0 cmap subtable not 262 bytes" - glyphIdArray = array.array("B") - glyphIdArray.fromstring(self.data) - self.cmap = cmap = {} - lenArray = len(glyphIdArray) - charCodes = list(range(lenArray)) - names = map(self.ttFont.getGlyphName, glyphIdArray) - list(map(operator.setitem, [cmap]*lenArray, charCodes, names)) - - + gids = array.array("B") + gids.fromstring(self.data) + charCodes = list(range(len(gids))) + self.cmap = _make_map(self.ttFont, charCodes, gids) + def compile(self, ttFont): if self.data: return struct.pack(">HHH", 0, 262, self.language) + self.data - charCodeList = sorted(self.cmap.items()) - charCodes = [entry[0] for entry in charCodeList] - valueList = [entry[1] for entry in charCodeList] - assert charCodes == list(range(256)) - valueList = map(ttFont.getGlyphID, valueList) + cmap = self.cmap + assert set(cmap.keys()).issubset(range(256)) + getGlyphID = ttFont.getGlyphID + valueList = [getGlyphID(cmap[i]) if i in cmap else 0 for i in range(256)] - glyphIdArray = array.array("B", valueList) - data = struct.pack(">HHH", 0, 262, self.language) + glyphIdArray.tostring() + gids = array.array("B", valueList) + data = struct.pack(">HHH", 0, 262, self.language) + gids.tostring() assert len(data) == 262 return data - + def fromXML(self, name, attrs, content, ttFont): self.language = safeEval(attrs["language"]) if not hasattr(self, "cmap"): @@ -229,9 +294,9 @@ class SubHeader(object): self.idDelta = None self.idRangeOffset = None self.glyphIndexArray = [] - + class cmap_format_2(CmapSubtable): - + def setIDDelta(self, subHeader): subHeader.idDelta = 0 # find the minGI which is not zero. @@ -241,13 +306,13 @@ class cmap_format_2(CmapSubtable): minGI = gid # The lowest gid in glyphIndexArray, after subtracting idDelta, must be 1. # idDelta is a short, and must be between -32K and 32K. minGI can be between 1 and 64K. - # We would like to pick an idDelta such that the first glyphArray GID is 1, + # We would like to pick an idDelta such that the first glyphArray GID is 1, # so that we are more likely to be able to combine glypharray GID subranges. # This means that we have a problem when minGI is > 32K # Since the final gi is reconstructed from the glyphArray GID by: # (short)finalGID = (gid + idDelta) % 0x10000), # we can get from a glypharray GID of 1 to a final GID of 65K by subtracting 2, and casting the - # negative number to an unsigned short. + # negative number to an unsigned short. if (minGI > 1): if minGI > 0x7FFF: @@ -257,15 +322,14 @@ class cmap_format_2(CmapSubtable): idDelta = subHeader.idDelta for i in range(subHeader.entryCount): gid = subHeader.glyphIndexArray[i] - if gid > 0: - subHeader.glyphIndexArray[i] = gid - idDelta - + if gid > 0: + subHeader.glyphIndexArray[i] = gid - idDelta def decompile(self, data, ttFont): # we usually get here indirectly from the subtable __getattr__ function, in which case both args must be None. # If not, someone is calling the subtable decompile() directly, and must provide both args. if data is not None and ttFont is not None: - self.decompileHeader(data[offset:offset+int(length)], ttFont) + self.decompileHeader(data, ttFont) else: assert (data is None and ttFont is None), "Need both data and ttFont arguments" @@ -280,7 +344,7 @@ class cmap_format_2(CmapSubtable): allKeys.byteswap() subHeaderKeys = [ key//8 for key in allKeys] maxSubHeaderindex = max(subHeaderKeys) - + #Load subHeaders subHeaderList = [] pos = 0 @@ -296,14 +360,14 @@ class cmap_format_2(CmapSubtable): giList.byteswap() subHeader.glyphIndexArray = giList subHeaderList.append(subHeader) - # How this gets processed. + # How this gets processed. # Charcodes may be one or two bytes. # The first byte of a charcode is mapped through the subHeaderKeys, to select # a subHeader. For any subheader but 0, the next byte is then mapped through the - # selected subheader. If subheader Index 0 is selected, then the byte itself is + # selected subheader. If subheader Index 0 is selected, then the byte itself is # mapped through the subheader, and there is no second byte. # Then assume that the subsequent byte is the first byte of the next charcode,and repeat. - # + # # Each subheader references a range in the glyphIndexArray whose length is entryCount. # The range in glyphIndexArray referenced by a sunheader may overlap with the range in glyphIndexArray # referenced by another subheader. @@ -315,7 +379,7 @@ class cmap_format_2(CmapSubtable): # firstChar and EntryCount values. If the byte value is outside the subrange, then the glyphIndex is zero # (e.g. glyph not in font). # If the byte index is in the subrange, then an offset index is calculated as (byteIndex - firstChar). - # The index to glyphIndex mapping is a subrange of the glyphIndexArray. You find the start of the subrange by + # The index to glyphIndex mapping is a subrange of the glyphIndexArray. You find the start of the subrange by # counting idRangeOffset bytes from the idRangeOffset word. The first value in this subrange is the # glyphIndex for the index firstChar. The offset index should then be used in this array to get the glyphIndex. # Example for Logocut-Medium @@ -329,12 +393,12 @@ class cmap_format_2(CmapSubtable): # [257], [1]=2 from charcode [129, 65] # [258], [2]=3 from charcode [129, 66] # [259], [3]=4 from charcode [129, 67] - # So, the glyphIndex = 3 from the array. Then if idDelta is not zero and the glyph ID is not zero, + # So, the glyphIndex = 3 from the array. Then if idDelta is not zero and the glyph ID is not zero, # add it to the glyphID to get the final glyphIndex # value. In this case the final glyph index = 3+ 42 -> 45 for the final glyphIndex. Whew! - + self.data = b"" - self.cmap = cmap = {} + cmap = {} notdefGI = 0 for firstByte in range(256): subHeadindex = subHeaderKeys[firstByte] @@ -363,21 +427,13 @@ class cmap_format_2(CmapSubtable): continue cmap[charCode] = gi # If not subHeader.entryCount, then all char codes with this first byte are - # mapped to .notdef. We can skip this subtable, and leave the glyphs un-encoded, which is the + # mapped to .notdef. We can skip this subtable, and leave the glyphs un-encoded, which is the # same as mapping it to .notdef. - # cmap values are GID's. - glyphOrder = self.ttFont.getGlyphOrder() + gids = list(cmap.values()) charCodes = list(cmap.keys()) - lenCmap = len(gids) - try: - names = list(map(operator.getitem, [glyphOrder]*lenCmap, gids )) - except IndexError: - getGlyphName = self.ttFont.getGlyphName - names = list(map(getGlyphName, gids )) - list(map(operator.setitem, [cmap]*lenCmap, charCodes, names)) - - + self.cmap = _make_map(self.ttFont, charCodes, gids) + def compile(self, ttFont): if self.data: return struct.pack(">HHH", self.format, self.length, self.language) + self.data @@ -388,7 +444,7 @@ class cmap_format_2(CmapSubtable): charCodes = [item[0] for item in items] names = [item[1] for item in items] nameMap = ttFont.getReverseGlyphMap() - lenCharCodes = len(charCodes) + lenCharCodes = len(charCodes) try: gids = list(map(operator.getitem, [nameMap]*lenCharCodes, names)) except KeyError: @@ -413,8 +469,8 @@ class cmap_format_2(CmapSubtable): gids.append(gid) # Process the (char code to gid) item list in char code order. - # By definition, all one byte char codes map to subheader 0. - # For all the two byte char codes, we assume that the first byte maps maps to the empty subhead (with an entry count of 0, + # By definition, all one byte char codes map to subheader 0. + # For all the two byte char codes, we assume that the first byte maps maps to the empty subhead (with an entry count of 0, # which defines all char codes in its range to map to notdef) unless proven otherwise. # Note that since the char code items are processed in char code order, all the char codes with the # same first byte are in sequential order. @@ -433,8 +489,7 @@ class cmap_format_2(CmapSubtable): subHeader.idDelta = 0 subHeader.idRangeOffset = 0 subHeaderList.append(subHeader) - - + lastFirstByte = -1 items = zip(charCodes, gids) for charCode, gid in items: @@ -471,7 +526,7 @@ class cmap_format_2(CmapSubtable): subHeader.glyphIndexArray.append(notdefGI) subHeader.glyphIndexArray.append(gid) subHeader.entryCount = subHeader.entryCount + codeDiff + 1 - + # fix GI's and iDelta of last subheader that we we added to the subheader array. self.setIDDelta(subHeader) @@ -488,12 +543,12 @@ class cmap_format_2(CmapSubtable): subHeaderKeys[index] = emptySubheadIndex # Since this is the last subheader, the GlyphIndex Array starts two bytes after the start of the # idRangeOffset word of this subHeader. We can safely point to the first entry in the GlyphIndexArray, - # since the first subrange of the GlyphIndexArray is for subHeader 0, which always starts with + # since the first subrange of the GlyphIndexArray is for subHeader 0, which always starts with # charcode 0 and GID 0. - - idRangeOffset = (len(subHeaderList)-1)*8 + 2 # offset to beginning of glyphIDArray from first subheader idRangeOffset. + + idRangeOffset = (len(subHeaderList)-1)*8 + 2 # offset to beginning of glyphIDArray from first subheader idRangeOffset. subheadRangeLen = len(subHeaderList) -1 # skip last special empty-set subheader; we've already hardocodes its idRangeOffset to 2. - for index in range(subheadRangeLen): + for index in range(subheadRangeLen): subHeader = subHeaderList[index] subHeader.idRangeOffset = 0 for j in range(index): @@ -502,7 +557,7 @@ class cmap_format_2(CmapSubtable): subHeader.idRangeOffset = prevSubhead.idRangeOffset - (index-j)*8 subHeader.glyphIndexArray = [] break - if subHeader.idRangeOffset == 0: # didn't find one. + if subHeader.idRangeOffset == 0: # didn't find one. subHeader.idRangeOffset = idRangeOffset idRangeOffset = (idRangeOffset - 8) + subHeader.entryCount*2 # one less subheader, one more subArray. else: @@ -524,7 +579,6 @@ class cmap_format_2(CmapSubtable): assert (len(data) == length), "Error: cmap format 2 is not same length as calculated! actual: " + str(len(data))+ " calc : " + str(length) return data - def fromXML(self, name, attrs, content, ttFont): self.language = safeEval(attrs["language"]) if not hasattr(self, "cmap"): @@ -556,17 +610,17 @@ def splitRange(startCode, endCode, cmap): # to do well with the fonts I tested: none became bigger, many became smaller. if startCode == endCode: return [], [endCode] - + lastID = cmap[startCode] lastCode = startCode inOrder = None orderedBegin = None subRanges = [] - + # Gather subranges in which the glyph IDs are consecutive. for code in range(startCode + 1, endCode + 1): glyphID = cmap[code] - + if glyphID - 1 == lastID: if inOrder is None or not inOrder: inOrder = 1 @@ -576,14 +630,14 @@ def splitRange(startCode, endCode, cmap): inOrder = 0 subRanges.append((orderedBegin, lastCode)) orderedBegin = None - + lastID = glyphID lastCode = code - + if inOrder: subRanges.append((orderedBegin, lastCode)) assert lastCode == endCode - + # Now filter out those new subranges that would only make the data bigger. # A new segment cost 8 bytes, not using a new segment costs 2 bytes per # character. @@ -598,15 +652,15 @@ def splitRange(startCode, endCode, cmap): if (e - b + 1) > threshold: newRanges.append((b, e)) subRanges = newRanges - + if not subRanges: return [], [endCode] - + if subRanges[0][0] != startCode: subRanges.insert(0, (startCode, subRanges[0][0] - 1)) if subRanges[-1][1] != endCode: subRanges.append((subRanges[-1][1] + 1, endCode)) - + # Fill the "holes" in the segments list -- those are the segments in which # the glyph IDs are _not_ consecutive. i = 1 @@ -615,7 +669,7 @@ def splitRange(startCode, endCode, cmap): subRanges.insert(i, (subRanges[i-1][1] + 1, subRanges[i][0] - 1)) i = i + 1 i = i + 1 - + # Transform the ranges into startCode/endCode lists. start = [] end = [] @@ -623,18 +677,18 @@ def splitRange(startCode, endCode, cmap): start.append(b) end.append(e) start.pop(0) - + assert len(start) + 1 == len(end) return start, end class cmap_format_4(CmapSubtable): - + def decompile(self, data, ttFont): # we usually get here indirectly from the subtable __getattr__ function, in which case both args must be None. # If not, someone is calling the subtable decompile() directly, and must provide both args. if data is not None and ttFont is not None: - self.decompileHeader(self.data[offset:offset+int(length)], ttFont) + self.decompileHeader(data, ttFont) else: assert (data is None and ttFont is None), "Need both data and ttFont arguments" @@ -643,14 +697,14 @@ class cmap_format_4(CmapSubtable): struct.unpack(">4H", data[:8]) data = data[8:] segCount = segCountX2 // 2 - + allCodes = array.array("H") allCodes.fromstring(data) self.data = data = None if sys.byteorder != "big": allCodes.byteswap() - + # divide the data endCode = allCodes[:segCount] allCodes = allCodes[segCount+1:] # the +1 is skipping the reservedPad field @@ -686,21 +740,12 @@ class cmap_format_4(CmapSubtable): glyphID = 0 # missing glyph gids.append(glyphID & 0xFFFF) - self.cmap = cmap = {} - lenCmap = len(gids) - glyphOrder = self.ttFont.getGlyphOrder() - try: - names = list(map(operator.getitem, [glyphOrder]*lenCmap, gids )) - except IndexError: - getGlyphName = self.ttFont.getGlyphName - names = list(map(getGlyphName, gids )) - list(map(operator.setitem, [cmap]*lenCmap, charCodes, names)) - + self.cmap = _make_map(self.ttFont, charCodes, gids) def compile(self, ttFont): if self.data: return struct.pack(">HHH", self.format, self.length, self.language) + self.data - + charCodes = list(self.cmap.keys()) lenCharCodes = len(charCodes) if lenCharCodes == 0: @@ -730,11 +775,11 @@ class cmap_format_4(CmapSubtable): gid = ttFont.getGlyphID(name) except: raise KeyError(name) - + gids.append(gid) cmap = {} # code:glyphID mapping list(map(operator.setitem, [cmap]*len(charCodes), charCodes, gids)) - + # Build startCode and endCode lists. # Split the char codes in ranges of consecutive char codes, then split # each range in more ranges of consecutive/not consecutive glyph IDs. @@ -751,10 +796,12 @@ class cmap_format_4(CmapSubtable): endCode.extend(end) startCode.append(charCode) lastCode = charCode - endCode.append(lastCode) + start, end = splitRange(startCode[-1], lastCode, cmap) + startCode.extend(start) + endCode.extend(end) startCode.append(0xffff) endCode.append(0xffff) - + # build up rest of cruft idDelta = [] idRangeOffset = [] @@ -773,12 +820,12 @@ class cmap_format_4(CmapSubtable): glyphIndexArray.extend(indices) idDelta.append(1) # 0xffff + 1 == (tadaa!) 0. So this end code maps to .notdef idRangeOffset.append(0) - + # Insane. segCount = len(endCode) segCountX2 = segCount * 2 searchRange, entrySelector, rangeShift = getSearchRange(segCount, 2) - + charCodeArray = array.array("H", endCode + [0] + startCode) idDeltaArray = array.array("H", idDelta) restArray = array.array("H", idRangeOffset + glyphIndexArray) @@ -789,10 +836,10 @@ class cmap_format_4(CmapSubtable): data = charCodeArray.tostring() + idDeltaArray.tostring() + restArray.tostring() length = struct.calcsize(cmap_format_4_format) + len(data) - header = struct.pack(cmap_format_4_format, self.format, length, self.language, + header = struct.pack(cmap_format_4_format, self.format, length, self.language, segCountX2, searchRange, entrySelector, rangeShift) return header + data - + def fromXML(self, name, attrs, content, ttFont): self.language = safeEval(attrs["language"]) if not hasattr(self, "cmap"): @@ -809,12 +856,12 @@ class cmap_format_4(CmapSubtable): class cmap_format_6(CmapSubtable): - + def decompile(self, data, ttFont): # we usually get here indirectly from the subtable __getattr__ function, in which case both args must be None. # If not, someone is calling the subtable decompile() directly, and must provide both args. if data is not None and ttFont is not None: - self.decompileHeader(data[offset:offset+int(length)], ttFont) + self.decompileHeader(data, ttFont) else: assert (data is None and ttFont is None), "Need both data and ttFont arguments" @@ -823,45 +870,38 @@ class cmap_format_6(CmapSubtable): firstCode = int(firstCode) data = data[4:] #assert len(data) == 2 * entryCount # XXX not true in Apple's Helvetica!!! - glyphIndexArray = array.array("H") - glyphIndexArray.fromstring(data[:2 * int(entryCount)]) + gids = array.array("H") + gids.fromstring(data[:2 * int(entryCount)]) if sys.byteorder != "big": - glyphIndexArray.byteswap() + gids.byteswap() self.data = data = None - self.cmap = cmap = {} + charCodes = list(range(firstCode, firstCode + len(gids))) + self.cmap = _make_map(self.ttFont, charCodes, gids) - lenArray = len(glyphIndexArray) - charCodes = list(range(firstCode, firstCode + lenArray)) - glyphOrder = self.ttFont.getGlyphOrder() - try: - names = list(map(operator.getitem, [glyphOrder]*lenArray, glyphIndexArray )) - except IndexError: - getGlyphName = self.ttFont.getGlyphName - names = list(map(getGlyphName, glyphIndexArray )) - list(map(operator.setitem, [cmap]*lenArray, charCodes, names)) - def compile(self, ttFont): if self.data: return struct.pack(">HHH", self.format, self.length, self.language) + self.data cmap = self.cmap - codes = list(cmap.keys()) + codes = sorted(cmap.keys()) if codes: # yes, there are empty cmap tables. codes = list(range(codes[0], codes[-1] + 1)) firstCode = codes[0] - valueList = [cmap.get(code, ".notdef") for code in codes] - valueList = map(ttFont.getGlyphID, valueList) - glyphIndexArray = array.array("H", valueList) + valueList = [ + ttFont.getGlyphID(cmap[code]) if code in cmap else 0 + for code in codes + ] + gids = array.array("H", valueList) if sys.byteorder != "big": - glyphIndexArray.byteswap() - data = glyphIndexArray.tostring() + gids.byteswap() + data = gids.tostring() else: data = b"" firstCode = 0 - header = struct.pack(">HHHHH", + header = struct.pack(">HHHHH", 6, len(data) + 10, self.language, firstCode, len(codes)) return header + data - + def fromXML(self, name, attrs, content, ttFont): self.language = safeEval(attrs["language"]) if not hasattr(self, "cmap"): @@ -878,7 +918,7 @@ class cmap_format_6(CmapSubtable): class cmap_format_12_or_13(CmapSubtable): - + def __init__(self, format): self.format = format self.reserved = 0 @@ -887,7 +927,7 @@ class cmap_format_12_or_13(CmapSubtable): def decompileHeader(self, data, ttFont): format, reserved, length, language, nGroups = struct.unpack(">HHLLL", data[:16]) - assert len(data) == (16 + nGroups*12) == (length), "corrupt cmap table format %d (data length: %d, header length: %d)" % (format, len(data), length) + assert len(data) == (16 + nGroups*12) == (length), "corrupt cmap table format %d (data length: %d, header length: %d)" % (self.format, len(data), length) self.format = format self.reserved = reserved self.length = length @@ -900,7 +940,7 @@ class cmap_format_12_or_13(CmapSubtable): # we usually get here indirectly from the subtable __getattr__ function, in which case both args must be None. # If not, someone is calling the subtable decompile() directly, and must provide both args. if data is not None and ttFont is not None: - self.decompileHeader(data[offset:offset+int(length)], ttFont) + self.decompileHeader(data, ttFont) else: assert (data is None and ttFont is None), "Need both data and ttFont arguments" @@ -915,21 +955,13 @@ class cmap_format_12_or_13(CmapSubtable): charCodes.extend(list(range(startCharCode, endCharCode +1))) gids.extend(self._computeGIDs(glyphID, lenGroup)) self.data = data = None - self.cmap = cmap = {} - lenCmap = len(gids) - glyphOrder = self.ttFont.getGlyphOrder() - try: - names = list(map(operator.getitem, [glyphOrder]*lenCmap, gids )) - except IndexError: - getGlyphName = self.ttFont.getGlyphName - names = list(map(getGlyphName, gids )) - list(map(operator.setitem, [cmap]*lenCmap, charCodes, names)) - + self.cmap = _make_map(self.ttFont, charCodes, gids) + def compile(self, ttFont): if self.data: return struct.pack(">HHLLL", self.format, self.reserved, self.length, self.language, self.nGroups) + self.data charCodes = list(self.cmap.keys()) - lenCharCodes = len(charCodes) + lenCharCodes = len(charCodes) names = list(self.cmap.values()) nameMap = ttFont.getReverseGlyphMap() try: @@ -954,7 +986,7 @@ class cmap_format_12_or_13(CmapSubtable): raise KeyError(name) gids.append(gid) - + cmap = {} # code:glyphID mapping list(map(operator.setitem, [cmap]*len(charCodes), charCodes, gids)) @@ -981,9 +1013,9 @@ class cmap_format_12_or_13(CmapSubtable): nGroups = nGroups + 1 data = bytesjoin(dataList) lengthSubtable = len(data) +16 - assert len(data) == (nGroups*12) == (lengthSubtable-16) - return struct.pack(">HHLLL", self.format, self.reserved , lengthSubtable, self.language, nGroups) + data - + assert len(data) == (nGroups*12) == (lengthSubtable-16) + return struct.pack(">HHLLL", self.format, self.reserved, lengthSubtable, self.language, nGroups) + data + def toXML(self, writer, ttFont): writer.begintag(self.__class__.__name__, [ ("platformID", self.platformID), @@ -999,7 +1031,7 @@ class cmap_format_12_or_13(CmapSubtable): self._writeCodes(codes, writer) writer.endtag(self.__class__.__name__) writer.newline() - + def fromXML(self, name, attrs, content, ttFont): self.format = safeEval(attrs["format"]) self.reserved = safeEval(attrs["reserved"]) @@ -1020,9 +1052,11 @@ class cmap_format_12_or_13(CmapSubtable): class cmap_format_12(cmap_format_12_or_13): - def __init__(self, format): + + _format_step = 1 + + def __init__(self, format=12): cmap_format_12_or_13.__init__(self, format) - self._format_step = 1 def _computeGIDs(self, startingGlyph, numberOfGlyphs): return list(range(startingGlyph, startingGlyph + numberOfGlyphs)) @@ -1032,9 +1066,11 @@ class cmap_format_12(cmap_format_12_or_13): class cmap_format_13(cmap_format_12_or_13): - def __init__(self, format): + + _format_step = 0 + + def __init__(self, format=13): cmap_format_12_or_13.__init__(self, format) - self._format_step = 0 def _computeGIDs(self, startingGlyph, numberOfGlyphs): return [startingGlyph] * numberOfGlyphs @@ -1065,21 +1101,21 @@ class cmap_format_14(CmapSubtable): self.language = 0xFF # has no language. def decompile(self, data, ttFont): - if data is not None and ttFont is not None and ttFont.lazy: + if data is not None and ttFont is not None: self.decompileHeader(data, ttFont) else: assert (data is None and ttFont is None), "Need both data and ttFont arguments" data = self.data - + self.cmap = {} # so that clients that expect this to exist in a cmap table won't fail. uvsDict = {} recOffset = 0 for n in range(self.numVarSelectorRecords): - uvs, defOVSOffset, nonDefUVSOffset = struct.unpack(">3sLL", data[recOffset:recOffset +11]) + uvs, defOVSOffset, nonDefUVSOffset = struct.unpack(">3sLL", data[recOffset:recOffset +11]) recOffset += 11 varUVS = cvtToUVS(uvs) if defOVSOffset: - startOffset = defOVSOffset - 10 + startOffset = defOVSOffset - 10 numValues, = struct.unpack(">L", data[startOffset:startOffset+4]) startOffset +=4 for r in range(numValues): @@ -1094,9 +1130,9 @@ class cmap_format_14(CmapSubtable): uvsDict[varUVS].extend(localUVList) except KeyError: uvsDict[varUVS] = list(localUVList) - + if nonDefUVSOffset: - startOffset = nonDefUVSOffset - 10 + startOffset = nonDefUVSOffset - 10 numRecs, = struct.unpack(">L", data[startOffset:startOffset+4]) startOffset +=4 localUVList = [] @@ -1110,9 +1146,9 @@ class cmap_format_14(CmapSubtable): uvsDict[varUVS].extend(localUVList) except KeyError: uvsDict[varUVS] = localUVList - + self.uvsDict = uvsDict - + def toXML(self, writer, ttFont): writer.begintag(self.__class__.__name__, [ ("platformID", self.platformID), @@ -1144,8 +1180,8 @@ class cmap_format_14(CmapSubtable): if not hasattr(self, "cmap"): self.cmap = {} # so that clients that expect this to exist in a cmap table won't fail. if not hasattr(self, "uvsDict"): - self.uvsDict = {} - uvsDict = self.uvsDict + self.uvsDict = {} + uvsDict = self.uvsDict for element in content: if not isinstance(element, tuple): @@ -1162,11 +1198,10 @@ class cmap_format_14(CmapSubtable): uvsDict[uvs].append( [uv, gname]) except KeyError: uvsDict[uvs] = [ [uv, gname] ] - def compile(self, ttFont): if self.data: - return struct.pack(">HLL", self.format, self.length , self.numVarSelectorRecords) + self.data + return struct.pack(">HLL", self.format, self.length, self.numVarSelectorRecords) + self.data uvsDict = self.uvsDict uvsList = sorted(uvsDict.keys()) @@ -1193,7 +1228,7 @@ class cmap_format_14(CmapSubtable): lastUV = defEntry defRecs.append(rec) cnt = 0 - + rec = struct.pack(">3sB", cvtFromUVS(lastUV), cnt) defRecs.append(rec) @@ -1218,20 +1253,19 @@ class cmap_format_14(CmapSubtable): data.append(ndrec) else: nonDefUVSOffset = 0 - + vrec = struct.pack(">3sLL", cvtFromUVS(uvs), defOVSOffset, nonDefUVSOffset) varSelectorRecords.append(vrec) - + data = bytesjoin(varSelectorRecords) + bytesjoin(data) self.length = 10 + len(data) - headerdata = struct.pack(">HLL", self.format, self.length , self.numVarSelectorRecords) - self.data = headerdata + data - - return self.data - - + headerdata = struct.pack(">HLL", self.format, self.length, self.numVarSelectorRecords) + + return headerdata + data + + class cmap_format_unknown(CmapSubtable): - + def toXML(self, writer, ttFont): cmapName = self.__class__.__name__[:12] + str(self.format) writer.begintag(cmapName, [ @@ -1242,20 +1276,20 @@ class cmap_format_unknown(CmapSubtable): writer.dumphex(self.data) writer.endtag(cmapName) writer.newline() - + def fromXML(self, name, attrs, content, ttFont): self.data = readHex(content) self.cmap = {} - + def decompileHeader(self, data, ttFont): self.language = 0 # dummy value self.data = data - + def decompile(self, data, ttFont): # we usually get here indirectly from the subtable __getattr__ function, in which case both args must be None. # If not, someone is calling the subtable decompile() directly, and must provide both args. if data is not None and ttFont is not None: - self.decompileHeader(data[offset:offset+int(length)], ttFont) + self.decompileHeader(data, ttFont) else: assert (data is None and ttFont is None), "Need both data and ttFont arguments" @@ -1273,4 +1307,4 @@ cmap_classes = { 12: cmap_format_12, 13: cmap_format_13, 14: cmap_format_14, - } +} diff --git a/Lib/fontTools/ttLib/tables/_c_v_a_r.py b/Lib/fontTools/ttLib/tables/_c_v_a_r.py new file mode 100644 index 0000000..c4ebbc9 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/_c_v_a_r.py @@ -0,0 +1,84 @@ +from __future__ import \ + print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +from . import DefaultTable +from fontTools.misc import sstruct +from fontTools.ttLib.tables.TupleVariation import \ + compileTupleVariationStore, decompileTupleVariationStore, TupleVariation + + +# https://www.microsoft.com/typography/otspec/cvar.htm +# https://www.microsoft.com/typography/otspec/otvarcommonformats.htm +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6cvar.html + +CVAR_HEADER_FORMAT = """ + > # big endian + majorVersion: H + minorVersion: H + tupleVariationCount: H + offsetToData: H +""" + +CVAR_HEADER_SIZE = sstruct.calcsize(CVAR_HEADER_FORMAT) + + +class table__c_v_a_r(DefaultTable.DefaultTable): + dependencies = ["cvt ", "fvar"] + + def __init__(self, tag=None): + DefaultTable.DefaultTable.__init__(self, tag) + self.majorVersion, self.minorVersion = 1, 0 + self.variations = [] + + def compile(self, ttFont, useSharedPoints=False): + tupleVariationCount, tuples, data = compileTupleVariationStore( + variations=[v for v in self.variations if v.hasImpact()], + pointCount=len(ttFont["cvt "].values), + axisTags=[axis.axisTag for axis in ttFont["fvar"].axes], + sharedTupleIndices={}, + useSharedPoints=useSharedPoints) + header = { + "majorVersion": self.majorVersion, + "minorVersion": self.minorVersion, + "tupleVariationCount": tupleVariationCount, + "offsetToData": CVAR_HEADER_SIZE + len(tuples), + } + return bytesjoin([ + sstruct.pack(CVAR_HEADER_FORMAT, header), + tuples, + data + ]) + + def decompile(self, data, ttFont): + axisTags = [axis.axisTag for axis in ttFont["fvar"].axes] + header = {} + sstruct.unpack(CVAR_HEADER_FORMAT, data[0:CVAR_HEADER_SIZE], header) + self.majorVersion = header["majorVersion"] + self.minorVersion = header["minorVersion"] + assert self.majorVersion == 1, self.majorVersion + self.variations = decompileTupleVariationStore( + tableTag=self.tableTag, axisTags=axisTags, + tupleVariationCount=header["tupleVariationCount"], + pointCount=len(ttFont["cvt "].values), sharedTuples=None, + data=data, pos=CVAR_HEADER_SIZE, dataPos=header["offsetToData"]) + + def fromXML(self, name, attrs, content, ttFont): + if name == "version": + self.majorVersion = int(attrs.get("major", "1")) + self.minorVersion = int(attrs.get("minor", "0")) + elif name == "tuple": + valueCount = len(ttFont["cvt "].values) + var = TupleVariation({}, [None] * valueCount) + self.variations.append(var) + for tupleElement in content: + if isinstance(tupleElement, tuple): + tupleName, tupleAttrs, tupleContent = tupleElement + var.fromXML(tupleName, tupleAttrs, tupleContent) + + def toXML(self, writer, ttFont): + axisTags = [axis.axisTag for axis in ttFont["fvar"].axes] + writer.simpletag("version", + major=self.majorVersion, minor=self.minorVersion) + writer.newline() + for var in self.variations: + var.toXML(writer, axisTags) diff --git a/Lib/fontTools/ttLib/tables/_c_v_t.py b/Lib/fontTools/ttLib/tables/_c_v_t.py index f9f8186..4fbee7b 100644 --- a/Lib/fontTools/ttLib/tables/_c_v_t.py +++ b/Lib/fontTools/ttLib/tables/_c_v_t.py @@ -6,26 +6,26 @@ import sys import array class table__c_v_t(DefaultTable.DefaultTable): - + def decompile(self, data, ttFont): values = array.array("h") values.fromstring(data) if sys.byteorder != "big": values.byteswap() self.values = values - + def compile(self, ttFont): values = self.values[:] if sys.byteorder != "big": values.byteswap() return values.tostring() - + def toXML(self, writer, ttFont): for i in range(len(self.values)): value = self.values[i] writer.simpletag("cv", value=value, index=i) writer.newline() - + def fromXML(self, name, attrs, content, ttFont): if not hasattr(self, "values"): self.values = array.array("h") @@ -35,16 +35,15 @@ class table__c_v_t(DefaultTable.DefaultTable): for i in range(1 + index - len(self.values)): self.values.append(0) self.values[index] = value - + def __len__(self): return len(self.values) - + def __getitem__(self, index): return self.values[index] - + def __setitem__(self, index, value): self.values[index] = value - + def __delitem__(self, index): del self.values[index] - diff --git a/Lib/fontTools/ttLib/tables/_f_e_a_t.py b/Lib/fontTools/ttLib/tables/_f_e_a_t.py new file mode 100644 index 0000000..3715271 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/_f_e_a_t.py @@ -0,0 +1,7 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from .otBase import BaseTTXConverter + + +class table__f_e_a_t(BaseTTXConverter): + pass diff --git a/Lib/fontTools/ttLib/tables/_f_p_g_m.py b/Lib/fontTools/ttLib/tables/_f_p_g_m.py index e4bd5f7..6536dba 100644 --- a/Lib/fontTools/ttLib/tables/_f_p_g_m.py +++ b/Lib/fontTools/ttLib/tables/_f_p_g_m.py @@ -4,24 +4,47 @@ from . import DefaultTable from . import ttProgram class table__f_p_g_m(DefaultTable.DefaultTable): - + def decompile(self, data, ttFont): program = ttProgram.Program() program.fromBytecode(data) self.program = program - + def compile(self, ttFont): return self.program.getBytecode() - + def toXML(self, writer, ttFont): self.program.toXML(writer, ttFont) - writer.newline() - + def fromXML(self, name, attrs, content, ttFont): program = ttProgram.Program() program.fromXML(name, attrs, content, ttFont) self.program = program - - def __len__(self): - return len(self.program) + def __bool__(self): + """ + >>> fpgm = table__f_p_g_m() + >>> bool(fpgm) + False + >>> p = ttProgram.Program() + >>> fpgm.program = p + >>> bool(fpgm) + False + >>> bc = bytearray([0]) + >>> p.fromBytecode(bc) + >>> bool(fpgm) + True + >>> p.bytecode.pop() + 0 + >>> bool(fpgm) + False + """ + return hasattr(self, 'program') and bool(self.program) + + __nonzero__ = __bool__ + + +if __name__ == "__main__": + import sys + import doctest + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/ttLib/tables/_f_v_a_r.py b/Lib/fontTools/ttLib/tables/_f_v_a_r.py new file mode 100644 index 0000000..3bc46e2 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/_f_v_a_r.py @@ -0,0 +1,219 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc import sstruct +from fontTools.misc.fixedTools import fixedToFloat, floatToFixed +from fontTools.misc.textTools import safeEval, num2binary, binary2num +from fontTools.ttLib import TTLibError +from . import DefaultTable +import struct + + +# Apple's documentation of 'fvar': +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6fvar.html + +FVAR_HEADER_FORMAT = """ + > # big endian + version: L + offsetToData: H + countSizePairs: H + axisCount: H + axisSize: H + instanceCount: H + instanceSize: H +""" + +FVAR_AXIS_FORMAT = """ + > # big endian + axisTag: 4s + minValue: 16.16F + defaultValue: 16.16F + maxValue: 16.16F + flags: H + axisNameID: H +""" + +FVAR_INSTANCE_FORMAT = """ + > # big endian + subfamilyNameID: H + flags: H +""" + +class table__f_v_a_r(DefaultTable.DefaultTable): + dependencies = ["name"] + + def __init__(self, tag=None): + DefaultTable.DefaultTable.__init__(self, tag) + self.axes = [] + self.instances = [] + + def compile(self, ttFont): + instanceSize = sstruct.calcsize(FVAR_INSTANCE_FORMAT) + (len(self.axes) * 4) + includePostScriptNames = any(instance.postscriptNameID != 0xFFFF + for instance in self.instances) + if includePostScriptNames: + instanceSize += 2 + header = { + "version": 0x00010000, + "offsetToData": sstruct.calcsize(FVAR_HEADER_FORMAT), + "countSizePairs": 2, + "axisCount": len(self.axes), + "axisSize": sstruct.calcsize(FVAR_AXIS_FORMAT), + "instanceCount": len(self.instances), + "instanceSize": instanceSize, + } + result = [sstruct.pack(FVAR_HEADER_FORMAT, header)] + result.extend([axis.compile() for axis in self.axes]) + axisTags = [axis.axisTag for axis in self.axes] + for instance in self.instances: + result.append(instance.compile(axisTags, includePostScriptNames)) + return bytesjoin(result) + + def decompile(self, data, ttFont): + header = {} + headerSize = sstruct.calcsize(FVAR_HEADER_FORMAT) + header = sstruct.unpack(FVAR_HEADER_FORMAT, data[0:headerSize]) + if header["version"] != 0x00010000: + raise TTLibError("unsupported 'fvar' version %04x" % header["version"]) + pos = header["offsetToData"] + axisSize = header["axisSize"] + for _ in range(header["axisCount"]): + axis = Axis() + axis.decompile(data[pos:pos+axisSize]) + self.axes.append(axis) + pos += axisSize + instanceSize = header["instanceSize"] + axisTags = [axis.axisTag for axis in self.axes] + for _ in range(header["instanceCount"]): + instance = NamedInstance() + instance.decompile(data[pos:pos+instanceSize], axisTags) + self.instances.append(instance) + pos += instanceSize + + def toXML(self, writer, ttFont): + for axis in self.axes: + axis.toXML(writer, ttFont) + for instance in self.instances: + instance.toXML(writer, ttFont) + + def fromXML(self, name, attrs, content, ttFont): + if name == "Axis": + axis = Axis() + axis.fromXML(name, attrs, content, ttFont) + self.axes.append(axis) + elif name == "NamedInstance": + instance = NamedInstance() + instance.fromXML(name, attrs, content, ttFont) + self.instances.append(instance) + +class Axis(object): + def __init__(self): + self.axisTag = None + self.axisNameID = 0 + self.flags = 0 + self.minValue = -1.0 + self.defaultValue = 0.0 + self.maxValue = 1.0 + + def compile(self): + return sstruct.pack(FVAR_AXIS_FORMAT, self) + + def decompile(self, data): + sstruct.unpack2(FVAR_AXIS_FORMAT, data, self) + + def toXML(self, writer, ttFont): + name = ttFont["name"].getDebugName(self.axisNameID) + if name is not None: + writer.newline() + writer.comment(name) + writer.newline() + writer.begintag("Axis") + writer.newline() + for tag, value in [("AxisTag", self.axisTag), + ("Flags", "0x%X" % self.flags), + ("MinValue", str(self.minValue)), + ("DefaultValue", str(self.defaultValue)), + ("MaxValue", str(self.maxValue)), + ("AxisNameID", str(self.axisNameID))]: + writer.begintag(tag) + writer.write(value) + writer.endtag(tag) + writer.newline() + writer.endtag("Axis") + writer.newline() + + def fromXML(self, name, _attrs, content, ttFont): + assert(name == "Axis") + for tag, _, value in filter(lambda t: type(t) is tuple, content): + value = ''.join(value) + if tag == "AxisTag": + self.axisTag = Tag(value) + elif tag in {"Flags", "MinValue", "DefaultValue", "MaxValue", + "AxisNameID"}: + setattr(self, tag[0].lower() + tag[1:], safeEval(value)) + + +class NamedInstance(object): + def __init__(self): + self.subfamilyNameID = 0 + self.postscriptNameID = 0xFFFF + self.flags = 0 + self.coordinates = {} + + def compile(self, axisTags, includePostScriptName): + result = [sstruct.pack(FVAR_INSTANCE_FORMAT, self)] + for axis in axisTags: + fixedCoord = floatToFixed(self.coordinates[axis], 16) + result.append(struct.pack(">l", fixedCoord)) + if includePostScriptName: + result.append(struct.pack(">H", self.postscriptNameID)) + return bytesjoin(result) + + def decompile(self, data, axisTags): + sstruct.unpack2(FVAR_INSTANCE_FORMAT, data, self) + pos = sstruct.calcsize(FVAR_INSTANCE_FORMAT) + for axis in axisTags: + value = struct.unpack(">l", data[pos : pos + 4])[0] + self.coordinates[axis] = fixedToFloat(value, 16) + pos += 4 + if pos + 2 <= len(data): + self.postscriptNameID = struct.unpack(">H", data[pos : pos + 2])[0] + else: + self.postscriptNameID = 0xFFFF + + def toXML(self, writer, ttFont): + name = ttFont["name"].getDebugName(self.subfamilyNameID) + if name is not None: + writer.newline() + writer.comment(name) + writer.newline() + psname = ttFont["name"].getDebugName(self.postscriptNameID) + if psname is not None: + writer.comment(u"PostScript: " + psname) + writer.newline() + if self.postscriptNameID == 0xFFFF: + writer.begintag("NamedInstance", flags=("0x%X" % self.flags), + subfamilyNameID=self.subfamilyNameID) + else: + writer.begintag("NamedInstance", flags=("0x%X" % self.flags), + subfamilyNameID=self.subfamilyNameID, + postscriptNameID=self.postscriptNameID, ) + writer.newline() + for axis in ttFont["fvar"].axes: + writer.simpletag("coord", axis=axis.axisTag, + value=self.coordinates[axis.axisTag]) + writer.newline() + writer.endtag("NamedInstance") + writer.newline() + + def fromXML(self, name, attrs, content, ttFont): + assert(name == "NamedInstance") + self.subfamilyNameID = safeEval(attrs["subfamilyNameID"]) + self.flags = safeEval(attrs.get("flags", "0")) + if "postscriptNameID" in attrs: + self.postscriptNameID = safeEval(attrs["postscriptNameID"]) + else: + self.postscriptNameID = 0xFFFF + + for tag, elementAttrs, _ in filter(lambda t: type(t) is tuple, content): + if tag == "coord": + self.coordinates[elementAttrs["axis"]] = safeEval(elementAttrs["value"]) diff --git a/Lib/fontTools/ttLib/tables/_g_a_s_p.py b/Lib/fontTools/ttLib/tables/_g_a_s_p.py index 54fef90..dce3569 100644 --- a/Lib/fontTools/ttLib/tables/_g_a_s_p.py +++ b/Lib/fontTools/ttLib/tables/_g_a_s_p.py @@ -11,7 +11,7 @@ GASP_DOGRAY = 0x0002 GASP_GRIDFIT = 0x0001 class table__g_a_s_p(DefaultTable.DefaultTable): - + def decompile(self, data, ttFont): self.version, numRanges = struct.unpack(">HH", data[:4]) assert 0 <= self.version <= 1, "unknown 'gasp' format: %s" % self.version @@ -22,7 +22,7 @@ class table__g_a_s_p(DefaultTable.DefaultTable): self.gaspRange[int(rangeMaxPPEM)] = int(rangeGaspBehavior) data = data[4:] assert not data, "too much data" - + def compile(self, ttFont): version = 0 # ignore self.version numRanges = len(self.gaspRange) @@ -34,7 +34,7 @@ class table__g_a_s_p(DefaultTable.DefaultTable): version = 1 data = struct.pack(">HH", version, numRanges) + data return data - + def toXML(self, writer, ttFont): items = sorted(self.gaspRange.items()) for rangeMaxPPEM, rangeGaspBehavior in items: @@ -42,11 +42,10 @@ class table__g_a_s_p(DefaultTable.DefaultTable): ("rangeMaxPPEM", rangeMaxPPEM), ("rangeGaspBehavior", rangeGaspBehavior)]) writer.newline() - + def fromXML(self, name, attrs, content, ttFont): if name != "gaspRange": return if not hasattr(self, "gaspRange"): self.gaspRange = {} self.gaspRange[safeEval(attrs["rangeMaxPPEM"])] = safeEval(attrs["rangeGaspBehavior"]) - diff --git a/Lib/fontTools/ttLib/tables/_g_c_i_d.py b/Lib/fontTools/ttLib/tables/_g_c_i_d.py new file mode 100644 index 0000000..f8b57e2 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/_g_c_i_d.py @@ -0,0 +1,8 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from .otBase import BaseTTXConverter + + +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gcid.html +class table__g_c_i_d(BaseTTXConverter): + pass diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index 970980b..58c1eb2 100644 --- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py +++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py @@ -1,36 +1,58 @@ """_g_l_y_f.py -- Converter classes for the 'glyf' table.""" - from __future__ import print_function, division, absolute_import +from collections import namedtuple from fontTools.misc.py23 import * from fontTools.misc import sstruct from fontTools import ttLib -from fontTools.misc.textTools import safeEval +from fontTools import version +from fontTools.misc.textTools import safeEval, pad from fontTools.misc.arrayTools import calcBounds, calcIntBounds, pointInRect from fontTools.misc.bezierTools import calcQuadraticBounds -from fontTools.misc.fixedTools import fixedToFloat as fi2fl, floatToFixed as fl2fi +from fontTools.misc.fixedTools import ( + fixedToFloat as fi2fl, + floatToFixed as fl2fi, + otRound, +) +from numbers import Number from . import DefaultTable from . import ttProgram import sys import struct import array -import warnings +import logging +import os +from fontTools.misc import xmlWriter +from fontTools.misc.filenames import userNameToFileName + +log = logging.getLogger(__name__) + +# We compute the version the same as is computed in ttlib/__init__ +# so that we can write 'ttLibVersion' attribute of the glyf TTX files +# when glyf is written to separate files. +version = ".".join(version.split('.')[:2]) # -# The Apple and MS rasterizers behave differently for +# The Apple and MS rasterizers behave differently for # scaled composite components: one does scale first and then translate # and the other does it vice versa. MS defined some flags to indicate # the difference, but it seems nobody actually _sets_ those flags. # # Funny thing: Apple seems to _only_ do their thing in the -# WE_HAVE_A_SCALE (eg. Chicago) case, and not when it's WE_HAVE_AN_X_AND_Y_SCALE +# WE_HAVE_A_SCALE (eg. Chicago) case, and not when it's WE_HAVE_AN_X_AND_Y_SCALE # (eg. Charcoal)... # SCALE_COMPONENT_OFFSET_DEFAULT = 0 # 0 == MS, 1 == Apple class table__g_l_y_f(DefaultTable.DefaultTable): - + + # this attribute controls the amount of padding applied to glyph data upon compile. + # Glyph lenghts are aligned to multiples of the specified value. + # Allowed values are (0, 1, 2, 4). '0' means no padding; '1' (default) also means + # no padding, except for when padding would allow to use short loca offsets. + padding = 1 + def decompile(self, data, ttFont): loca = ttFont['loca'] last = int(loca[0]) @@ -50,17 +72,21 @@ class table__g_l_y_f(DefaultTable.DefaultTable): glyph = Glyph(glyphdata) self.glyphs[glyphName] = glyph last = next - if len(data) > next: - warnings.warn("too much 'glyf' table data") + if len(data) - next >= 4: + log.warning( + "too much 'glyf' table data: expected %d, received %d bytes", + next, len(data)) if noname: - warnings.warn('%s glyphs have no name' % i) - if not ttFont.lazy: + log.warning('%s glyphs have no name', noname) + if ttFont.lazy is False: # Be lazy for None and True for glyph in self.glyphs.values(): glyph.expand(self) - + def compile(self, ttFont): if not hasattr(self, "glyphOrder"): self.glyphOrder = ttFont.getGlyphOrder() + padding = self.padding + assert padding in (0, 1, 2, 4) locations = [] currentLocation = 0 dataList = [] @@ -68,49 +94,94 @@ class table__g_l_y_f(DefaultTable.DefaultTable): for glyphName in self.glyphOrder: glyph = self.glyphs[glyphName] glyphData = glyph.compile(self, recalcBBoxes) + if padding > 1: + glyphData = pad(glyphData, size=padding) locations.append(currentLocation) currentLocation = currentLocation + len(glyphData) dataList.append(glyphData) locations.append(currentLocation) + + if padding == 1 and currentLocation < 0x20000: + # See if we can pad any odd-lengthed glyphs to allow loca + # table to use the short offsets. + indices = [i for i,glyphData in enumerate(dataList) if len(glyphData) % 2 == 1] + if indices and currentLocation + len(indices) < 0x20000: + # It fits. Do it. + for i in indices: + dataList[i] += b'\0' + currentLocation = 0 + for i,glyphData in enumerate(dataList): + locations[i] = currentLocation + currentLocation += len(glyphData) + locations[len(dataList)] = currentLocation + data = bytesjoin(dataList) if 'loca' in ttFont: ttFont['loca'].set(locations) - ttFont['maxp'].numGlyphs = len(self.glyphs) + if 'maxp' in ttFont: + ttFont['maxp'].numGlyphs = len(self.glyphs) return data - - def toXML(self, writer, ttFont, progress=None): - writer.newline() + + def toXML(self, writer, ttFont, splitGlyphs=False): + notice = ( + "The xMin, yMin, xMax and yMax values\n" + "will be recalculated by the compiler.") glyphNames = ttFont.getGlyphNames() - writer.comment("The xMin, yMin, xMax and yMax values\nwill be recalculated by the compiler.") - writer.newline() - writer.newline() - counter = 0 - progressStep = 10 + if not splitGlyphs: + writer.newline() + writer.comment(notice) + writer.newline() + writer.newline() numGlyphs = len(glyphNames) + if splitGlyphs: + path, ext = os.path.splitext(writer.file.name) + existingGlyphFiles = set() for glyphName in glyphNames: - if not counter % progressStep and progress is not None: - progress.setLabel("Dumping 'glyf' table... (%s)" % glyphName) - progress.increment(progressStep / numGlyphs) - counter = counter + 1 glyph = self[glyphName] if glyph.numberOfContours: - writer.begintag('TTGlyph', [ - ("name", glyphName), - ("xMin", glyph.xMin), - ("yMin", glyph.yMin), - ("xMax", glyph.xMax), - ("yMax", glyph.yMax), - ]) - writer.newline() - glyph.toXML(writer, ttFont) - writer.endtag('TTGlyph') - writer.newline() + if splitGlyphs: + glyphPath = userNameToFileName( + tounicode(glyphName, 'utf-8'), + existingGlyphFiles, + prefix=path + ".", + suffix=ext) + existingGlyphFiles.add(glyphPath.lower()) + glyphWriter = xmlWriter.XMLWriter( + glyphPath, idlefunc=writer.idlefunc, + newlinestr=writer.newlinestr) + glyphWriter.begintag("ttFont", ttLibVersion=version) + glyphWriter.newline() + glyphWriter.begintag("glyf") + glyphWriter.newline() + glyphWriter.comment(notice) + glyphWriter.newline() + writer.simpletag("TTGlyph", src=os.path.basename(glyphPath)) + else: + glyphWriter = writer + glyphWriter.begintag('TTGlyph', [ + ("name", glyphName), + ("xMin", glyph.xMin), + ("yMin", glyph.yMin), + ("xMax", glyph.xMax), + ("yMax", glyph.yMax), + ]) + glyphWriter.newline() + glyph.toXML(glyphWriter, ttFont) + glyphWriter.endtag('TTGlyph') + glyphWriter.newline() + if splitGlyphs: + glyphWriter.endtag("glyf") + glyphWriter.newline() + glyphWriter.endtag("ttFont") + glyphWriter.newline() + glyphWriter.close() else: writer.simpletag('TTGlyph', name=glyphName) writer.comment("contains no outline data") - writer.newline() + if not splitGlyphs: + writer.newline() writer.newline() - + def fromXML(self, name, attrs, content, ttFont): if name != "TTGlyph": return @@ -119,8 +190,7 @@ class table__g_l_y_f(DefaultTable.DefaultTable): if not hasattr(self, "glyphOrder"): self.glyphOrder = ttFont.getGlyphOrder() glyphName = attrs["name"] - if ttFont.verbose: - ttLib.debugmsg("unpacking glyph '%s'" % glyphName) + log.debug("unpacking glyph '%s'", glyphName) glyph = Glyph() for attr in ['xMin', 'yMin', 'xMax', 'yMax']: setattr(glyph, attr, safeEval(attrs.get(attr, '0'))) @@ -132,39 +202,43 @@ class table__g_l_y_f(DefaultTable.DefaultTable): glyph.fromXML(name, attrs, content, ttFont) if not ttFont.recalcBBoxes: glyph.compact(self, 0) - + def setGlyphOrder(self, glyphOrder): self.glyphOrder = glyphOrder - + def getGlyphName(self, glyphID): return self.glyphOrder[glyphID] - + def getGlyphID(self, glyphName): # XXX optimize with reverse dict!!! return self.glyphOrder.index(glyphName) - + + def removeHinting(self): + for glyph in self.glyphs.values(): + glyph.removeHinting() + def keys(self): return self.glyphs.keys() - + def has_key(self, glyphName): return glyphName in self.glyphs - + __contains__ = has_key - + def __getitem__(self, glyphName): glyph = self.glyphs[glyphName] glyph.expand(self) return glyph - + def __setitem__(self, glyphName, glyph): self.glyphs[glyphName] = glyph if glyphName not in self.glyphOrder: self.glyphOrder.append(glyphName) - + def __delitem__(self, glyphName): del self.glyphs[glyphName] self.glyphOrder.remove(glyphName) - + def __len__(self): assert len(self.glyphOrder) == len(self.glyphs) return len(self.glyphs) @@ -189,54 +263,129 @@ flagYsame = 0x20 flagReserved1 = 0x40 flagReserved2 = 0x80 +_flagSignBytes = { + 0: 2, + flagXsame: 0, + flagXShort|flagXsame: +1, + flagXShort: -1, + flagYsame: 0, + flagYShort|flagYsame: +1, + flagYShort: -1, +} + +def flagBest(x, y, onCurve): + """For a given x,y delta pair, returns the flag that packs this pair + most efficiently, as well as the number of byte cost of such flag.""" + + flag = flagOnCurve if onCurve else 0 + cost = 0 + # do x + if x == 0: + flag = flag | flagXsame + elif -255 <= x <= 255: + flag = flag | flagXShort + if x > 0: + flag = flag | flagXsame + cost += 1 + else: + cost += 2 + # do y + if y == 0: + flag = flag | flagYsame + elif -255 <= y <= 255: + flag = flag | flagYShort + if y > 0: + flag = flag | flagYsame + cost += 1 + else: + cost += 2 + return flag, cost + +def flagFits(newFlag, oldFlag, mask): + newBytes = _flagSignBytes[newFlag & mask] + oldBytes = _flagSignBytes[oldFlag & mask] + return newBytes == oldBytes or abs(newBytes) > abs(oldBytes) + +def flagSupports(newFlag, oldFlag): + return ((oldFlag & flagOnCurve) == (newFlag & flagOnCurve) and + flagFits(newFlag, oldFlag, flagXsame|flagXShort) and + flagFits(newFlag, oldFlag, flagYsame|flagYShort)) + +def flagEncodeCoord(flag, mask, coord, coordBytes): + byteCount = _flagSignBytes[flag & mask] + if byteCount == 1: + coordBytes.append(coord) + elif byteCount == -1: + coordBytes.append(-coord) + elif byteCount == 2: + coordBytes.append((coord >> 8) & 0xFF) + coordBytes.append(coord & 0xFF) + +def flagEncodeCoords(flag, x, y, xBytes, yBytes): + flagEncodeCoord(flag, flagXsame|flagXShort, x, xBytes) + flagEncodeCoord(flag, flagYsame|flagYShort, y, yBytes) -ARG_1_AND_2_ARE_WORDS = 0x0001 # if set args are words otherwise they are bytes -ARGS_ARE_XY_VALUES = 0x0002 # if set args are xy values, otherwise they are points -ROUND_XY_TO_GRID = 0x0004 # for the xy values if above is true -WE_HAVE_A_SCALE = 0x0008 # Sx = Sy, otherwise scale == 1.0 -NON_OVERLAPPING = 0x0010 # set to same value for all components (obsolete!) -MORE_COMPONENTS = 0x0020 # indicates at least one more glyph after this one -WE_HAVE_AN_X_AND_Y_SCALE = 0x0040 # Sx, Sy -WE_HAVE_A_TWO_BY_TWO = 0x0080 # t00, t01, t10, t11 -WE_HAVE_INSTRUCTIONS = 0x0100 # instructions follow -USE_MY_METRICS = 0x0200 # apply these metrics to parent glyph -OVERLAP_COMPOUND = 0x0400 # used by Apple in GX fonts -SCALED_COMPONENT_OFFSET = 0x0800 # composite designed to have the component offset scaled (designed for Apple) -UNSCALED_COMPONENT_OFFSET = 0x1000 # composite designed not to have the component offset scaled (designed for MS) + +ARG_1_AND_2_ARE_WORDS = 0x0001 # if set args are words otherwise they are bytes +ARGS_ARE_XY_VALUES = 0x0002 # if set args are xy values, otherwise they are points +ROUND_XY_TO_GRID = 0x0004 # for the xy values if above is true +WE_HAVE_A_SCALE = 0x0008 # Sx = Sy, otherwise scale == 1.0 +NON_OVERLAPPING = 0x0010 # set to same value for all components (obsolete!) +MORE_COMPONENTS = 0x0020 # indicates at least one more glyph after this one +WE_HAVE_AN_X_AND_Y_SCALE = 0x0040 # Sx, Sy +WE_HAVE_A_TWO_BY_TWO = 0x0080 # t00, t01, t10, t11 +WE_HAVE_INSTRUCTIONS = 0x0100 # instructions follow +USE_MY_METRICS = 0x0200 # apply these metrics to parent glyph +OVERLAP_COMPOUND = 0x0400 # used by Apple in GX fonts +SCALED_COMPONENT_OFFSET = 0x0800 # composite designed to have the component offset scaled (designed for Apple) +UNSCALED_COMPONENT_OFFSET = 0x1000 # composite designed not to have the component offset scaled (designed for MS) + + +CompositeMaxpValues = namedtuple('CompositeMaxpValues', ['nPoints', 'nContours', 'maxComponentDepth']) class Glyph(object): - + def __init__(self, data=""): if not data: # empty char self.numberOfContours = 0 return self.data = data - + def compact(self, glyfTable, recalcBBoxes=True): data = self.compile(glyfTable, recalcBBoxes) self.__dict__.clear() self.data = data - + def expand(self, glyfTable): if not hasattr(self, "data"): # already unpacked return if not self.data: # empty char + del self.data self.numberOfContours = 0 return dummy, data = sstruct.unpack2(glyphHeaderFormat, self.data, self) del self.data + # Some fonts (eg. Neirizi.ttf) have a 0 for numberOfContours in + # some glyphs; decompileCoordinates assumes that there's at least + # one, so short-circuit here. + if self.numberOfContours == 0: + return if self.isComposite(): self.decompileComponents(data, glyfTable) else: self.decompileCoordinates(data) - + def compile(self, glyfTable, recalcBBoxes=True): if hasattr(self, "data"): - return self.data + if recalcBBoxes: + # must unpack glyph in order to recalculate bounding box + self.expand(glyfTable) + else: + return self.data if self.numberOfContours == 0: return "" if recalcBBoxes: @@ -246,24 +395,13 @@ class Glyph(object): data = data + self.compileComponents(glyfTable) else: data = data + self.compileCoordinates() - # From the spec: "Note that the local offsets should be word-aligned" - # From a later MS spec: "Note that the local offsets should be long-aligned" - # Let's be modern and align on 4-byte boundaries. - if len(data) % 4: - # add pad bytes - nPadBytes = 4 - (len(data) % 4) - data = data + b"\0" * nPadBytes return data - + def toXML(self, writer, ttFont): if self.isComposite(): for compo in self.components: compo.toXML(writer, ttFont) - if hasattr(self, "program"): - writer.begintag("instructions") - self.program.toXML(writer, ttFont) - writer.endtag("instructions") - writer.newline() + haveInstructions = hasattr(self, "program") else: last = 0 for i in range(self.numberOfContours): @@ -271,19 +409,24 @@ class Glyph(object): writer.newline() for j in range(last, self.endPtsOfContours[i] + 1): writer.simpletag("pt", [ - ("x", self.coordinates[j][0]), + ("x", self.coordinates[j][0]), ("y", self.coordinates[j][1]), ("on", self.flags[j] & flagOnCurve)]) writer.newline() last = self.endPtsOfContours[i] + 1 writer.endtag("contour") writer.newline() - if self.numberOfContours: + haveInstructions = self.numberOfContours > 0 + if haveInstructions: + if self.program: writer.begintag("instructions") + writer.newline() self.program.toXML(writer, ttFont) writer.endtag("instructions") - writer.newline() - + else: + writer.simpletag("instructions") + writer.newline() + def fromXML(self, name, attrs, content, ttFont): if name == "contour": if self.numberOfContours < 0: @@ -324,7 +467,7 @@ class Glyph(object): continue name, attrs, content = element self.program.fromXML(name, attrs, content, ttFont) - + def getCompositeMaxpValues(self, glyfTable, maxComponentDepth=1): assert self.isComposite() nContours = 0 @@ -340,12 +483,12 @@ class Glyph(object): glyfTable, maxComponentDepth + 1) nPoints = nPoints + nP nContours = nContours + nC - return nPoints, nContours, maxComponentDepth - + return CompositeMaxpValues(nPoints, nContours, maxComponentDepth) + def getMaxpValues(self): assert self.numberOfContours > 0 return len(self.coordinates), len(self.endPtsOfContours) - + def decompileComponents(self, data, glyfTable): self.components = [] more = 1 @@ -361,17 +504,20 @@ class Glyph(object): self.program = ttProgram.Program() self.program.fromBytecode(data[:numInstructions]) data = data[numInstructions:] - assert len(data) < 4, "bad composite data" - + if len(data) >= 4: + log.warning( + "too much glyph data at the end of composite glyph: %d excess bytes", + len(data)) + def decompileCoordinates(self, data): endPtsOfContours = array.array("h") endPtsOfContours.fromstring(data[:2*self.numberOfContours]) if sys.byteorder != "big": endPtsOfContours.byteswap() self.endPtsOfContours = endPtsOfContours.tolist() - + data = data[2*self.numberOfContours:] - + instructionLength, = struct.unpack(">h", data[:2]) data = data[2:] self.program = ttProgram.Program() @@ -380,7 +526,7 @@ class Glyph(object): nCoordinates = self.endPtsOfContours[-1] + 1 flags, xCoordinates, yCoordinates = \ self.decompileCoordinatesRaw(nCoordinates, data) - + # fill in repetitions and apply signs self.coordinates = coordinates = GlyphCoordinates.zeros(nCoordinates) xIndex = 0 @@ -453,11 +599,12 @@ class Glyph(object): xDataLen = struct.calcsize(xFormat) yDataLen = struct.calcsize(yFormat) if len(data) - (xDataLen + yDataLen) >= 4: - warnings.warn("too much glyph data: %d excess bytes" % (len(data) - (xDataLen + yDataLen))) + log.warning( + "too much glyph data: %d excess bytes", len(data) - (xDataLen + yDataLen)) xCoordinates = struct.unpack(xFormat, data[:xDataLen]) yCoordinates = struct.unpack(yFormat, data[xDataLen:xDataLen+yDataLen]) return flags, xCoordinates, yCoordinates - + def compileComponents(self, glyfTable): data = b"" lastcomponent = len(self.components) - 1 @@ -473,33 +620,41 @@ class Glyph(object): instructions = self.program.getBytecode() data = data + struct.pack(">h", len(instructions)) + instructions return data - - + def compileCoordinates(self): assert len(self.coordinates) == len(self.flags) - data = b"" + data = [] endPtsOfContours = array.array("h", self.endPtsOfContours) if sys.byteorder != "big": endPtsOfContours.byteswap() - data = data + endPtsOfContours.tostring() + data.append(endPtsOfContours.tostring()) instructions = self.program.getBytecode() - data = data + struct.pack(">h", len(instructions)) + instructions - nCoordinates = len(self.coordinates) - - coordinates = self.coordinates.copy() - coordinates.absoluteToRelative() - flags = self.flags + data.append(struct.pack(">h", len(instructions))) + data.append(instructions) + + deltas = self.coordinates.copy() + if deltas.isFloat(): + # Warn? + deltas.toInt() + deltas.absoluteToRelative() + + # TODO(behdad): Add a configuration option for this? + deltas = self.compileDeltasGreedy(self.flags, deltas) + #deltas = self.compileDeltasOptimal(self.flags, deltas) + + data.extend(deltas) + return bytesjoin(data) + + def compileDeltasGreedy(self, flags, deltas): + # Implements greedy algorithm for packing coordinate deltas: + # uses shortest representation one coordinate at a time. compressedflags = [] xPoints = [] yPoints = [] - xFormat = ">" - yFormat = ">" lastflag = None repeat = 0 - for i in range(len(coordinates)): + for flag,(x,y) in zip(flags, deltas): # Oh, the horrors of TrueType - flag = flags[i] - x, y = coordinates[i] # do x if x == 0: flag = flag | flagXsame @@ -509,11 +664,9 @@ class Glyph(object): flag = flag | flagXsame else: x = -x - xPoints.append(x) - xFormat = xFormat + 'B' + xPoints.append(bytechr(x)) else: - xPoints.append(x) - xFormat = xFormat + 'h' + xPoints.append(struct.pack(">h", x)) # do y if y == 0: flag = flag | flagYsame @@ -523,11 +676,9 @@ class Glyph(object): flag = flag | flagYsame else: y = -y - yPoints.append(y) - yFormat = yFormat + 'B' + yPoints.append(bytechr(y)) else: - yPoints.append(y) - yFormat = yFormat + 'h' + yPoints.append(struct.pack(">h", y)) # handle repeating flags if flag == lastflag and repeat != 255: repeat = repeat + 1 @@ -540,15 +691,67 @@ class Glyph(object): repeat = 0 compressedflags.append(flag) lastflag = flag - data = data + array.array("B", compressedflags).tostring() - if coordinates.isFloat(): - # Warn? - xPoints = [int(round(x)) for x in xPoints] - yPoints = [int(round(y)) for y in xPoints] - data = data + struct.pack(*(xFormat,)+tuple(xPoints)) - data = data + struct.pack(*(yFormat,)+tuple(yPoints)) - return data - + compressedFlags = array.array("B", compressedflags).tostring() + compressedXs = bytesjoin(xPoints) + compressedYs = bytesjoin(yPoints) + return (compressedFlags, compressedXs, compressedYs) + + def compileDeltasOptimal(self, flags, deltas): + # Implements optimal, dynaic-programming, algorithm for packing coordinate + # deltas. The savings are negligible :(. + candidates = [] + bestTuple = None + bestCost = 0 + repeat = 0 + for flag,(x,y) in zip(flags, deltas): + # Oh, the horrors of TrueType + flag, coordBytes = flagBest(x, y, flag) + bestCost += 1 + coordBytes + newCandidates = [(bestCost, bestTuple, flag, coordBytes), + (bestCost+1, bestTuple, (flag|flagRepeat), coordBytes)] + for lastCost,lastTuple,lastFlag,coordBytes in candidates: + if lastCost + coordBytes <= bestCost + 1 and (lastFlag & flagRepeat) and (lastFlag < 0xff00) and flagSupports(lastFlag, flag): + if (lastFlag & 0xFF) == (flag|flagRepeat) and lastCost == bestCost + 1: + continue + newCandidates.append((lastCost + coordBytes, lastTuple, lastFlag+256, coordBytes)) + candidates = newCandidates + bestTuple = min(candidates, key=lambda t:t[0]) + bestCost = bestTuple[0] + + flags = [] + while bestTuple: + cost, bestTuple, flag, coordBytes = bestTuple + flags.append(flag) + flags.reverse() + + compressedFlags = array.array("B") + compressedXs = array.array("B") + compressedYs = array.array("B") + coords = iter(deltas) + ff = [] + for flag in flags: + repeatCount, flag = flag >> 8, flag & 0xFF + compressedFlags.append(flag) + if flag & flagRepeat: + assert(repeatCount > 0) + compressedFlags.append(repeatCount) + else: + assert(repeatCount == 0) + for i in range(1 + repeatCount): + x,y = next(coords) + flagEncodeCoords(flag, x, y, compressedXs, compressedYs) + ff.append(flag) + try: + next(coords) + raise Exception("internal error") + except StopIteration: + pass + compressedFlags = compressedFlags.tostring() + compressedXs = compressedXs.tostring() + compressedYs = compressedYs.tostring() + + return (compressedFlags, compressedXs, compressedYs) + def recalcBounds(self, glyfTable): coords, endPts, flags = self.getCoordinates(glyfTable) if len(coords) > 0: @@ -564,7 +767,7 @@ class Glyph(object): # Collect on-curve points onCurveCoords = [coords[j] for j in range(len(coords)) - if flags[j] & flagOnCurve] + if flags[j] & flagOnCurve] # Add implicit on-curve points start = 0 for end in endPts: @@ -586,7 +789,7 @@ class Glyph(object): bbox = calcBounds([coords[last], coords[next]]) if not pointInRect(coords[j], bbox): # Ouch! - warnings.warn("Outline has curve with implicit extrema.") + log.warning("Outline has curve with implicit extrema.") # Ouch! Find analytical curve bounds. pthis = coords[j] plast = coords[last] @@ -606,19 +809,19 @@ class Glyph(object): self.xMin, self.yMin, self.xMax, self.yMax = calcIntBounds(coords) else: self.xMin, self.yMin, self.xMax, self.yMax = (0, 0, 0, 0) - + def isComposite(self): """Can be called on compact or expanded glyph.""" - if hasattr(self, "data"): + if hasattr(self, "data") and self.data: return struct.unpack(">h", self.data[:2])[0] == -1 else: return self.numberOfContours == -1 - + def __getitem__(self, componentIndex): if not self.isComposite(): raise ttLib.TTLibError("can't use glyph as sequence") return self.components[componentIndex] - + def getCoordinates(self, glyfTable): if self.numberOfContours > 0: return self.coordinates, self.endPtsOfContours, self.flags @@ -637,7 +840,7 @@ class Glyph(object): move = x1-x2, y1-y2 else: move = compo.x, compo.y - + coordinates = GlyphCoordinates(coordinates) if not hasattr(compo, "transform"): coordinates.translate(move) @@ -696,14 +899,16 @@ class Glyph(object): return components - def removeHinting(self): + def trim(self, remove_hinting=False): + """ Remove padding and, if requested, hinting, from a glyph. + This works on both expanded and compacted glyphs, without + expanding it.""" if not hasattr(self, "data"): - self.program = ttProgram.Program() - self.program.fromBytecode([]) + if remove_hinting: + self.program = ttProgram.Program() + self.program.fromBytecode([]) + # No padding to trim. return - - # Remove instructions without expanding glyph - if not self.data: return numContours = struct.unpack(">h", self.data[:2])[0] @@ -713,48 +918,52 @@ class Glyph(object): i += 2 * numContours # endPtsOfContours nCoordinates = ((data[i-2] << 8) | data[i-1]) + 1 instructionLen = (data[i] << 8) | data[i+1] - # Zero it - data[i] = data [i+1] = 0 - i += 2 - if instructionLen: - # Splice it out - data = data[:i] + data[i+instructionLen:] - if instructionLen % 4: - # We now have to go ahead and drop - # the old padding. Otherwise with - # padding we have to add, we may - # end up with more than 3 bytes of - # padding. - coordBytes = 0 - j = 0 - while True: - flag = data[i] - i = i + 1 - repeat = 1 - if flag & flagRepeat: - repeat = data[i] + 1 - i = i + 1 - xBytes = yBytes = 0 - if flag & flagXShort: - xBytes = 1 - elif not (flag & flagXsame): - xBytes = 2 - if flag & flagYShort: - yBytes = 1 - elif not (flag & flagYsame): - yBytes = 2 - coordBytes += (xBytes + yBytes) * repeat - j += repeat - if j >= nCoordinates: - break - assert j == nCoordinates, "bad glyph flags" - data = data[:i + coordBytes] + if remove_hinting: + # Zero instruction length + data[i] = data [i+1] = 0 + i += 2 + if instructionLen: + # Splice it out + data = data[:i] + data[i+instructionLen:] + instructionLen = 0 + else: + i += 2 + instructionLen + + coordBytes = 0 + j = 0 + while True: + flag = data[i] + i = i + 1 + repeat = 1 + if flag & flagRepeat: + repeat = data[i] + 1 + i = i + 1 + xBytes = yBytes = 0 + if flag & flagXShort: + xBytes = 1 + elif not (flag & flagXsame): + xBytes = 2 + if flag & flagYShort: + yBytes = 1 + elif not (flag & flagYsame): + yBytes = 2 + coordBytes += (xBytes + yBytes) * repeat + j += repeat + if j >= nCoordinates: + break + assert j == nCoordinates, "bad glyph flags" + i += coordBytes + # Remove padding + data = data[:i] else: more = 1 + we_have_instructions = False while more: flags =(data[i] << 8) | data[i+1] - # Turn instruction flag off - flags &= ~WE_HAVE_INSTRUCTIONS + if remove_hinting: + flags &= ~WE_HAVE_INSTRUCTIONS + if flags & WE_HAVE_INSTRUCTIONS: + we_have_instructions = True data[i+0] = flags >> 8 data[i+1] = flags & 0xFF i += 4 @@ -766,36 +975,77 @@ class Glyph(object): elif flags & WE_HAVE_AN_X_AND_Y_SCALE: i += 4 elif flags & WE_HAVE_A_TWO_BY_TWO: i += 8 more = flags & MORE_COMPONENTS - - # Cut off + if we_have_instructions: + instructionLen = (data[i] << 8) | data[i+1] + i += 2 + instructionLen + # Remove padding data = data[:i] - data = data.tostring() + self.data = data.tostring() - if len(data) % 4: - # add pad bytes - nPadBytes = 4 - (len(data) % 4) - data = data + b"\0" * nPadBytes + def removeHinting(self): + self.trim (remove_hinting=True) - self.data = data + def draw(self, pen, glyfTable, offset=0): + + if self.isComposite(): + for component in self.components: + glyphName, transform = component.getComponentInfo() + pen.addComponent(glyphName, transform) + return + + coordinates, endPts, flags = self.getCoordinates(glyfTable) + if offset: + coordinates = coordinates.copy() + coordinates.translate((offset, 0)) + start = 0 + for end in endPts: + end = end + 1 + contour = coordinates[start:end] + cFlags = flags[start:end] + start = end + if 1 not in cFlags: + # There is not a single on-curve point on the curve, + # use pen.qCurveTo's special case by specifying None + # as the on-curve point. + contour.append(None) + pen.qCurveTo(*contour) + else: + # Shuffle the points so that contour the is guaranteed + # to *end* in an on-curve point, which we'll use for + # the moveTo. + firstOnCurve = cFlags.index(1) + 1 + contour = contour[firstOnCurve:] + contour[:firstOnCurve] + cFlags = cFlags[firstOnCurve:] + cFlags[:firstOnCurve] + pen.moveTo(contour[-1]) + while contour: + nextOnCurve = cFlags.index(1) + 1 + if nextOnCurve == 1: + pen.lineTo(contour[0]) + else: + pen.qCurveTo(*contour[:nextOnCurve]) + contour = contour[nextOnCurve:] + cFlags = cFlags[nextOnCurve:] + pen.closePath() - def __ne__(self, other): - return not self.__eq__(other) def __eq__(self, other): if type(self) != type(other): return NotImplemented return self.__dict__ == other.__dict__ + def __ne__(self, other): + result = self.__eq__(other) + return result if result is NotImplemented else not result class GlyphComponent(object): - + def __init__(self): pass - + def getComponentInfo(self): """Return the base glyph name and a transform.""" # XXX Ignoring self.firstPt & self.lastpt for now: I need to implement - # something equivalent in fontTools.objects.glyph (I'd rather not + # something equivalent in fontTools.objects.glyph (I'd rather not # convert it to an absolute offset, since it is valuable information). # This method will now raise "AttributeError: x" on glyphs that use # this TT feature. @@ -805,15 +1055,14 @@ class GlyphComponent(object): else: trans = (1, 0, 0, 1, self.x, self.y) return self.glyphName, trans - + def decompile(self, data, glyfTable): flags, glyphID = struct.unpack(">HH", data[:4]) self.flags = int(flags) glyphID = int(glyphID) self.glyphName = glyfTable.getGlyphName(int(glyphID)) - #print ">>", reprflag(self.flags) data = data[4:] - + if self.flags & ARG_1_AND_2_ARE_WORDS: if self.flags & ARGS_ARE_XY_VALUES: self.x, self.y = struct.unpack(">hh", data[:4]) @@ -828,7 +1077,7 @@ class GlyphComponent(object): x, y = struct.unpack(">BB", data[:2]) self.firstPt, self.secondPt = int(x), int(y) data = data[2:] - + if self.flags & WE_HAVE_A_SCALE: scale, = struct.unpack(">h", data[:2]) self.transform = [[fi2fl(scale,14), 0], [0, fi2fl(scale,14)]] # fixed 2.14 @@ -838,30 +1087,30 @@ class GlyphComponent(object): self.transform = [[fi2fl(xscale,14), 0], [0, fi2fl(yscale,14)]] # fixed 2.14 data = data[4:] elif self.flags & WE_HAVE_A_TWO_BY_TWO: - (xscale, scale01, + (xscale, scale01, scale10, yscale) = struct.unpack(">hhhh", data[:8]) self.transform = [[fi2fl(xscale,14), fi2fl(scale01,14)], - [fi2fl(scale10,14), fi2fl(yscale,14)]] # fixed 2.14 + [fi2fl(scale10,14), fi2fl(yscale,14)]] # fixed 2.14 data = data[8:] more = self.flags & MORE_COMPONENTS haveInstructions = self.flags & WE_HAVE_INSTRUCTIONS - self.flags = self.flags & (ROUND_XY_TO_GRID | USE_MY_METRICS | + self.flags = self.flags & (ROUND_XY_TO_GRID | USE_MY_METRICS | SCALED_COMPONENT_OFFSET | UNSCALED_COMPONENT_OFFSET | - NON_OVERLAPPING) + NON_OVERLAPPING | OVERLAP_COMPOUND) return more, haveInstructions, data - + def compile(self, more, haveInstructions, glyfTable): data = b"" - + # reset all flags we will calculate ourselves - flags = self.flags & (ROUND_XY_TO_GRID | USE_MY_METRICS | + flags = self.flags & (ROUND_XY_TO_GRID | USE_MY_METRICS | SCALED_COMPONENT_OFFSET | UNSCALED_COMPONENT_OFFSET | - NON_OVERLAPPING) + NON_OVERLAPPING | OVERLAP_COMPOUND) if more: flags = flags | MORE_COMPONENTS if haveInstructions: flags = flags | WE_HAVE_INSTRUCTIONS - + if hasattr(self, "firstPt"): if (0 <= self.firstPt <= 255) and (0 <= self.secondPt <= 255): data = data + struct.pack(">BB", self.firstPt, self.secondPt) @@ -869,39 +1118,41 @@ class GlyphComponent(object): data = data + struct.pack(">HH", self.firstPt, self.secondPt) flags = flags | ARG_1_AND_2_ARE_WORDS else: + x = otRound(self.x) + y = otRound(self.y) flags = flags | ARGS_ARE_XY_VALUES - if (-128 <= self.x <= 127) and (-128 <= self.y <= 127): - data = data + struct.pack(">bb", self.x, self.y) + if (-128 <= x <= 127) and (-128 <= y <= 127): + data = data + struct.pack(">bb", x, y) else: - data = data + struct.pack(">hh", self.x, self.y) + data = data + struct.pack(">hh", x, y) flags = flags | ARG_1_AND_2_ARE_WORDS - + if hasattr(self, "transform"): transform = [[fl2fi(x,14) for x in row] for row in self.transform] if transform[0][1] or transform[1][0]: flags = flags | WE_HAVE_A_TWO_BY_TWO - data = data + struct.pack(">hhhh", + data = data + struct.pack(">hhhh", transform[0][0], transform[0][1], transform[1][0], transform[1][1]) elif transform[0][0] != transform[1][1]: flags = flags | WE_HAVE_AN_X_AND_Y_SCALE - data = data + struct.pack(">hh", + data = data + struct.pack(">hh", transform[0][0], transform[1][1]) else: flags = flags | WE_HAVE_A_SCALE - data = data + struct.pack(">h", + data = data + struct.pack(">h", transform[0][0]) - + glyphID = glyfTable.getGlyphID(self.glyphName) return struct.pack(">HH", flags, glyphID) + data - + def toXML(self, writer, ttFont): attrs = [("glyphName", self.glyphName)] if not hasattr(self, "firstPt"): attrs = attrs + [("x", self.x), ("y", self.y)] else: attrs = attrs + [("firstPt", self.firstPt), ("secondPt", self.secondPt)] - + if hasattr(self, "transform"): transform = self.transform if transform[0][1] or transform[1][0]: @@ -918,7 +1169,7 @@ class GlyphComponent(object): attrs = attrs + [("flags", hex(self.flags))] writer.simpletag("component", attrs) writer.newline() - + def fromXML(self, name, attrs, content, ttFont): self.glyphName = attrs["glyphName"] if "firstPt" in attrs: @@ -941,29 +1192,41 @@ class GlyphComponent(object): scale = safeEval(attrs["scale"]) self.transform = [[scale, 0], [0, scale]] self.flags = safeEval(attrs["flags"]) - - def __ne__(self, other): - return not self.__eq__(other) + def __eq__(self, other): if type(self) != type(other): return NotImplemented return self.__dict__ == other.__dict__ + def __ne__(self, other): + result = self.__eq__(other) + return result if result is NotImplemented else not result + class GlyphCoordinates(object): - def __init__(self, iterable=[]): - self._a = array.array("h") + def __init__(self, iterable=[], typecode="h"): + self._a = array.array(typecode) self.extend(iterable) + @property + def array(self): + return self._a + def isFloat(self): - return self._a.typecode == 'f' + return self._a.typecode == 'd' def _ensureFloat(self): if self.isFloat(): return - self._a = array.array("f", self._a) + # The conversion to list() is to work around Jython bug + self._a = array.array("d", list(self._a)) def _checkFloat(self, p): + if self.isFloat(): + return p + if any(v > 0x7FFF or v < -0x8000 for v in p): + self._ensureFloat() + return p if any(isinstance(v, float) for v in p): p = [int(v) if int(v) == v else v for v in p] if any(isinstance(v, float) for v in p): @@ -975,7 +1238,7 @@ class GlyphCoordinates(object): return GlyphCoordinates([(0,0)] * count) def copy(self): - c = GlyphCoordinates() + c = GlyphCoordinates(typecode=self._a.typecode) c._a.extend(self._a) return c @@ -992,13 +1255,17 @@ class GlyphCoordinates(object): if isinstance(k, slice): indices = range(*k.indices(len(self))) # XXX This only works if len(v) == len(indices) - # TODO Implement __delitem__ for j,i in enumerate(indices): self[i] = v[j] return v = self._checkFloat(v) self._a[2*k],self._a[2*k+1] = v + def __delitem__(self, i): + i = (2*i) % len(self._a) + del self._a[i] + del self._a[i] + def __repr__(self): return 'GlyphCoordinates(['+','.join(str(c) for c in self)+'])' @@ -1011,12 +1278,21 @@ class GlyphCoordinates(object): p = self._checkFloat(p) self._a.extend(p) + def toInt(self): + if not self.isFloat(): + return + a = array.array("h") + for n in self._a: + a.append(otRound(n)) + self._a = a + def relativeToAbsolute(self): a = self._a x,y = 0,0 for i in range(len(a) // 2): - a[2*i ] = x = a[2*i ] + x - a[2*i+1] = y = a[2*i+1] + y + x = a[2*i ] + x + y = a[2*i+1] + y + self[i] = (x, y) def absoluteToRelative(self): a = self._a @@ -1026,17 +1302,30 @@ class GlyphCoordinates(object): dy = a[2*i+1] - y x = a[2*i ] y = a[2*i+1] - a[2*i ] = dx - a[2*i+1] = dy + self[i] = (dx, dy) def translate(self, p): - (x,y) = p + """ + >>> GlyphCoordinates([(1,2)]).translate((.5,0)) + """ + (x,y) = self._checkFloat(p) + a = self._a + for i in range(len(a) // 2): + self[i] = (a[2*i] + x, a[2*i+1] + y) + + def scale(self, p): + """ + >>> GlyphCoordinates([(1,2)]).scale((.5,0)) + """ + (x,y) = self._checkFloat(p) a = self._a for i in range(len(a) // 2): - a[2*i ] += x - a[2*i+1] += y + self[i] = (a[2*i] * x, a[2*i+1] * y) def transform(self, t): + """ + >>> GlyphCoordinates([(1,2)]).transform(((.5,0),(.2,.5))) + """ a = self._a for i in range(len(a) // 2): x = a[2*i ] @@ -1045,13 +1334,197 @@ class GlyphCoordinates(object): py = x * t[0][1] + y * t[1][1] self[i] = (px, py) - def __ne__(self, other): - return not self.__eq__(other) def __eq__(self, other): + """ + >>> g = GlyphCoordinates([(1,2)]) + >>> g2 = GlyphCoordinates([(1.0,2)]) + >>> g3 = GlyphCoordinates([(1.5,2)]) + >>> g == g2 + True + >>> g == g3 + False + >>> g2 == g3 + False + """ if type(self) != type(other): return NotImplemented return self._a == other._a + def __ne__(self, other): + """ + >>> g = GlyphCoordinates([(1,2)]) + >>> g2 = GlyphCoordinates([(1.0,2)]) + >>> g3 = GlyphCoordinates([(1.5,2)]) + >>> g != g2 + False + >>> g != g3 + True + >>> g2 != g3 + True + """ + result = self.__eq__(other) + return result if result is NotImplemented else not result + + # Math operations + + def __pos__(self): + """ + >>> g = GlyphCoordinates([(1,2)]) + >>> g + GlyphCoordinates([(1, 2)]) + >>> g2 = +g + >>> g2 + GlyphCoordinates([(1, 2)]) + >>> g2.translate((1,0)) + >>> g2 + GlyphCoordinates([(2, 2)]) + >>> g + GlyphCoordinates([(1, 2)]) + """ + return self.copy() + def __neg__(self): + """ + >>> g = GlyphCoordinates([(1,2)]) + >>> g + GlyphCoordinates([(1, 2)]) + >>> g2 = -g + >>> g2 + GlyphCoordinates([(-1, -2)]) + >>> g + GlyphCoordinates([(1, 2)]) + """ + r = self.copy() + a = r._a + for i in range(len(a)): + a[i] = -a[i] + return r + def __round__(self): + """ + Note: This is Python 3 only. Python 2 does not call __round__. + As such, we cannot test this method either. :( + """ + r = self.copy() + r.toInt() + return r + + def __add__(self, other): return self.copy().__iadd__(other) + def __sub__(self, other): return self.copy().__isub__(other) + def __mul__(self, other): return self.copy().__imul__(other) + def __truediv__(self, other): return self.copy().__itruediv__(other) + + __radd__ = __add__ + __rmul__ = __mul__ + def __rsub__(self, other): return other + (-self) + + def __iadd__(self, other): + """ + >>> g = GlyphCoordinates([(1,2)]) + >>> g += (.5,0) + >>> g + GlyphCoordinates([(1.5, 2.0)]) + >>> g2 = GlyphCoordinates([(3,4)]) + >>> g += g2 + >>> g + GlyphCoordinates([(4.5, 6.0)]) + """ + if isinstance(other, tuple): + assert len(other) == 2 + self.translate(other) + return self + if isinstance(other, GlyphCoordinates): + if other.isFloat(): self._ensureFloat() + other = other._a + a = self._a + assert len(a) == len(other) + for i in range(len(a) // 2): + self[i] = (a[2*i] + other[2*i], a[2*i+1] + other[2*i+1]) + return self + return NotImplemented + + def __isub__(self, other): + """ + >>> g = GlyphCoordinates([(1,2)]) + >>> g -= (.5,0) + >>> g + GlyphCoordinates([(0.5, 2.0)]) + >>> g2 = GlyphCoordinates([(3,4)]) + >>> g -= g2 + >>> g + GlyphCoordinates([(-2.5, -2.0)]) + """ + if isinstance(other, tuple): + assert len(other) == 2 + self.translate((-other[0],-other[1])) + return self + if isinstance(other, GlyphCoordinates): + if other.isFloat(): self._ensureFloat() + other = other._a + a = self._a + assert len(a) == len(other) + for i in range(len(a) // 2): + self[i] = (a[2*i] - other[2*i], a[2*i+1] - other[2*i+1]) + return self + return NotImplemented + + def __imul__(self, other): + """ + >>> g = GlyphCoordinates([(1,2)]) + >>> g *= (2,.5) + >>> g *= 2 + >>> g + GlyphCoordinates([(4.0, 2.0)]) + >>> g = GlyphCoordinates([(1,2)]) + >>> g *= 2 + >>> g + GlyphCoordinates([(2, 4)]) + """ + if isinstance(other, Number): + other = (other, other) + if isinstance(other, tuple): + if other == (1,1): + return self + assert len(other) == 2 + self.scale(other) + return self + return NotImplemented + + def __itruediv__(self, other): + """ + >>> g = GlyphCoordinates([(1,3)]) + >>> g /= (.5,1.5) + >>> g /= 2 + >>> g + GlyphCoordinates([(1.0, 1.0)]) + """ + if isinstance(other, Number): + other = (other, other) + if isinstance(other, tuple): + if other == (1,1): + return self + assert len(other) == 2 + self.scale((1./other[0],1./other[1])) + return self + return NotImplemented + + def __bool__(self): + """ + >>> g = GlyphCoordinates([]) + >>> bool(g) + False + >>> g = GlyphCoordinates([(0,0), (0.,0)]) + >>> bool(g) + True + >>> g = GlyphCoordinates([(0,0), (1,0)]) + >>> bool(g) + True + >>> g = GlyphCoordinates([(0,.5), (0,0)]) + >>> bool(g) + True + """ + return bool(self._a) + + __nonzero__ = __bool__ + def reprflag(flag): bin = "" @@ -1066,3 +1539,7 @@ def reprflag(flag): bin = (14 - len(bin)) * "0" + bin return bin + +if __name__ == "__main__": + import doctest, sys + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/ttLib/tables/_g_v_a_r.py b/Lib/fontTools/ttLib/tables/_g_v_a_r.py new file mode 100644 index 0000000..9f97c31 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/_g_v_a_r.py @@ -0,0 +1,229 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools import ttLib +from fontTools.misc import sstruct +from fontTools.misc.textTools import safeEval +from fontTools.ttLib import TTLibError +from . import DefaultTable +import array +import itertools +import logging +import struct +import sys +import fontTools.ttLib.tables.TupleVariation as tv + + +log = logging.getLogger(__name__) +TupleVariation = tv.TupleVariation + + +# https://www.microsoft.com/typography/otspec/gvar.htm +# https://www.microsoft.com/typography/otspec/otvarcommonformats.htm +# +# Apple's documentation of 'gvar': +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gvar.html +# +# FreeType2 source code for parsing 'gvar': +# http://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/src/truetype/ttgxvar.c + +GVAR_HEADER_FORMAT = """ + > # big endian + version: H + reserved: H + axisCount: H + sharedTupleCount: H + offsetToSharedTuples: I + glyphCount: H + flags: H + offsetToGlyphVariationData: I +""" + +GVAR_HEADER_SIZE = sstruct.calcsize(GVAR_HEADER_FORMAT) + + +class table__g_v_a_r(DefaultTable.DefaultTable): + dependencies = ["fvar", "glyf"] + + def __init__(self, tag=None): + DefaultTable.DefaultTable.__init__(self, tag) + self.version, self.reserved = 1, 0 + self.variations = {} + + def compile(self, ttFont): + axisTags = [axis.axisTag for axis in ttFont["fvar"].axes] + sharedTuples = tv.compileSharedTuples( + axisTags, itertools.chain(*self.variations.values())) + sharedTupleIndices = {coord:i for i, coord in enumerate(sharedTuples)} + sharedTupleSize = sum([len(c) for c in sharedTuples]) + compiledGlyphs = self.compileGlyphs_( + ttFont, axisTags, sharedTupleIndices) + offset = 0 + offsets = [] + for glyph in compiledGlyphs: + offsets.append(offset) + offset += len(glyph) + offsets.append(offset) + compiledOffsets, tableFormat = self.compileOffsets_(offsets) + + header = {} + header["version"] = self.version + header["reserved"] = self.reserved + header["axisCount"] = len(axisTags) + header["sharedTupleCount"] = len(sharedTuples) + header["offsetToSharedTuples"] = GVAR_HEADER_SIZE + len(compiledOffsets) + header["glyphCount"] = len(compiledGlyphs) + header["flags"] = tableFormat + header["offsetToGlyphVariationData"] = header["offsetToSharedTuples"] + sharedTupleSize + compiledHeader = sstruct.pack(GVAR_HEADER_FORMAT, header) + + result = [compiledHeader, compiledOffsets] + result.extend(sharedTuples) + result.extend(compiledGlyphs) + return bytesjoin(result) + + def compileGlyphs_(self, ttFont, axisTags, sharedCoordIndices): + result = [] + for glyphName in ttFont.getGlyphOrder(): + glyph = ttFont["glyf"][glyphName] + pointCount = self.getNumPoints_(glyph) + variations = self.variations.get(glyphName, []) + result.append(compileGlyph_(variations, pointCount, + axisTags, sharedCoordIndices)) + return result + + def decompile(self, data, ttFont): + axisTags = [axis.axisTag for axis in ttFont["fvar"].axes] + glyphs = ttFont.getGlyphOrder() + sstruct.unpack(GVAR_HEADER_FORMAT, data[0:GVAR_HEADER_SIZE], self) + assert len(glyphs) == self.glyphCount + assert len(axisTags) == self.axisCount + offsets = self.decompileOffsets_(data[GVAR_HEADER_SIZE:], tableFormat=(self.flags & 1), glyphCount=self.glyphCount) + sharedCoords = tv.decompileSharedTuples( + axisTags, self.sharedTupleCount, data, self.offsetToSharedTuples) + self.variations = {} + offsetToData = self.offsetToGlyphVariationData + for i in range(self.glyphCount): + glyphName = glyphs[i] + glyph = ttFont["glyf"][glyphName] + numPointsInGlyph = self.getNumPoints_(glyph) + gvarData = data[offsetToData + offsets[i] : offsetToData + offsets[i + 1]] + self.variations[glyphName] = decompileGlyph_( + numPointsInGlyph, sharedCoords, axisTags, gvarData) + + @staticmethod + def decompileOffsets_(data, tableFormat, glyphCount): + if tableFormat == 0: + # Short format: array of UInt16 + offsets = array.array("H") + offsetsSize = (glyphCount + 1) * 2 + else: + # Long format: array of UInt32 + offsets = array.array("I") + offsetsSize = (glyphCount + 1) * 4 + offsets.fromstring(data[0 : offsetsSize]) + if sys.byteorder != "big": + offsets.byteswap() + + # In the short format, offsets need to be multiplied by 2. + # This is not documented in Apple's TrueType specification, + # but can be inferred from the FreeType implementation, and + # we could verify it with two sample GX fonts. + if tableFormat == 0: + offsets = [off * 2 for off in offsets] + + return offsets + + @staticmethod + def compileOffsets_(offsets): + """Packs a list of offsets into a 'gvar' offset table. + + Returns a pair (bytestring, tableFormat). Bytestring is the + packed offset table. Format indicates whether the table + uses short (tableFormat=0) or long (tableFormat=1) integers. + The returned tableFormat should get packed into the flags field + of the 'gvar' header. + """ + assert len(offsets) >= 2 + for i in range(1, len(offsets)): + assert offsets[i - 1] <= offsets[i] + if max(offsets) <= 0xffff * 2: + packed = array.array("H", [n >> 1 for n in offsets]) + tableFormat = 0 + else: + packed = array.array("I", offsets) + tableFormat = 1 + if sys.byteorder != "big": + packed.byteswap() + return (packed.tostring(), tableFormat) + + def toXML(self, writer, ttFont): + writer.simpletag("version", value=self.version) + writer.newline() + writer.simpletag("reserved", value=self.reserved) + writer.newline() + axisTags = [axis.axisTag for axis in ttFont["fvar"].axes] + for glyphName in ttFont.getGlyphOrder(): + variations = self.variations.get(glyphName) + if not variations: + continue + writer.begintag("glyphVariations", glyph=glyphName) + writer.newline() + for gvar in variations: + gvar.toXML(writer, axisTags) + writer.endtag("glyphVariations") + writer.newline() + + def fromXML(self, name, attrs, content, ttFont): + if name == "version": + self.version = safeEval(attrs["value"]) + elif name == "reserved": + self.reserved = safeEval(attrs["value"]) + elif name == "glyphVariations": + if not hasattr(self, "variations"): + self.variations = {} + glyphName = attrs["glyph"] + glyph = ttFont["glyf"][glyphName] + numPointsInGlyph = self.getNumPoints_(glyph) + glyphVariations = [] + for element in content: + if isinstance(element, tuple): + name, attrs, content = element + if name == "tuple": + gvar = TupleVariation({}, [None] * numPointsInGlyph) + glyphVariations.append(gvar) + for tupleElement in content: + if isinstance(tupleElement, tuple): + tupleName, tupleAttrs, tupleContent = tupleElement + gvar.fromXML(tupleName, tupleAttrs, tupleContent) + self.variations[glyphName] = glyphVariations + + @staticmethod + def getNumPoints_(glyph): + NUM_PHANTOM_POINTS = 4 + if glyph.isComposite(): + return len(glyph.components) + NUM_PHANTOM_POINTS + else: + # Empty glyphs (eg. space, nonmarkingreturn) have no "coordinates" attribute. + return len(getattr(glyph, "coordinates", [])) + NUM_PHANTOM_POINTS + + +def compileGlyph_(variations, pointCount, axisTags, sharedCoordIndices): + tupleVariationCount, tuples, data = tv.compileTupleVariationStore( + variations, pointCount, axisTags, sharedCoordIndices) + if tupleVariationCount == 0: + return b"" + result = (struct.pack(">HH", tupleVariationCount, 4 + len(tuples)) + + tuples + data) + if len(result) % 2 != 0: + result = result + b"\0" # padding + return result + + +def decompileGlyph_(pointCount, sharedTuples, axisTags, data): + if len(data) < 4: + return [] + tupleVariationCount, offsetToData = struct.unpack(">HH", data[:4]) + dataPos = offsetToData + return tv.decompileTupleVariationStore("gvar", axisTags, + tupleVariationCount, pointCount, + sharedTuples, data, 4, offsetToData) diff --git a/Lib/fontTools/ttLib/tables/_h_d_m_x.py b/Lib/fontTools/ttLib/tables/_h_d_m_x.py index 06fca7d..e073325 100644 --- a/Lib/fontTools/ttLib/tables/_h_d_m_x.py +++ b/Lib/fontTools/ttLib/tables/_h_d_m_x.py @@ -2,6 +2,7 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.misc import sstruct from . import DefaultTable +import array hdmxHeaderFormat = """ > # big endian! @@ -10,8 +11,31 @@ hdmxHeaderFormat = """ recordSize: l """ +try: + from collections.abc import Mapping +except: + from UserDict import DictMixin as Mapping + +class _GlyphnamedList(Mapping): + + def __init__(self, reverseGlyphOrder, data): + self._array = data + self._map = dict(reverseGlyphOrder) + + def __getitem__(self, k): + return self._array[self._map[k]] + + def __len__(self): + return len(self._map) + + def __iter__(self): + return iter(self._map) + + def keys(self): + return self._map.keys() + class table__h_d_m_x(DefaultTable.DefaultTable): - + def decompile(self, data, ttFont): numGlyphs = ttFont['maxp'].numGlyphs glyphOrder = ttFont.getGlyphOrder() @@ -20,13 +44,11 @@ class table__h_d_m_x(DefaultTable.DefaultTable): for i in range(self.numRecords): ppem = byteord(data[0]) maxSize = byteord(data[1]) - widths = {} - for glyphID in range(numGlyphs): - widths[glyphOrder[glyphID]] = byteord(data[glyphID+2]) + widths = _GlyphnamedList(ttFont.getReverseGlyphMap(), array.array("B", data[2:2+numGlyphs])) self.hdmx[ppem] = widths data = data[self.recordSize:] assert len(data) == 0, "too much hdmx data" - + def compile(self, ttFont): self.version = 0 numGlyphs = ttFont['maxp'].numGlyphs @@ -43,7 +65,7 @@ class table__h_d_m_x(DefaultTable.DefaultTable): data = data + bytechr(width) data = data + pad return data - + def toXML(self, writer, ttFont): writer.begintag("hdmxData") writer.newline() @@ -72,7 +94,7 @@ class table__h_d_m_x(DefaultTable.DefaultTable): writer.newline() writer.endtag("hdmxData") writer.newline() - + def fromXML(self, name, attrs, content, ttFont): if name != "hdmxData": return @@ -97,4 +119,3 @@ class table__h_d_m_x(DefaultTable.DefaultTable): assert len(line) == len(ppems), "illegal hdmx format" for i in range(len(ppems)): hdmx[ppems[i]][glyphName] = line[i] - diff --git a/Lib/fontTools/ttLib/tables/_h_e_a_d.py b/Lib/fontTools/ttLib/tables/_h_e_a_d.py index bf4116d..9275d41 100644 --- a/Lib/fontTools/ttLib/tables/_h_e_a_d.py +++ b/Lib/fontTools/ttLib/tables/_h_e_a_d.py @@ -2,11 +2,14 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.misc import sstruct from fontTools.misc.textTools import safeEval, num2binary, binary2num +from fontTools.misc.timeTools import timestampFromString, timestampToString, timestampNow +from fontTools.misc.timeTools import epoch_diff as mac_epoch_diff # For backward compat from . import DefaultTable -import time -import calendar +import logging +log = logging.getLogger(__name__) + headFormat = """ > # big endian tableVersion: 16.16F @@ -29,21 +32,43 @@ headFormat = """ """ class table__h_e_a_d(DefaultTable.DefaultTable): - - dependencies = ['maxp', 'loca'] - + + dependencies = ['maxp', 'loca', 'CFF '] + def decompile(self, data, ttFont): dummy, rest = sstruct.unpack2(headFormat, data, self) if rest: # this is quite illegal, but there seem to be fonts out there that do this + log.warning("extra bytes at the end of 'head' table") assert rest == "\0\0" - + + # For timestamp fields, ignore the top four bytes. Some fonts have + # bogus values there. Since till 2038 those bytes only can be zero, + # ignore them. + # + # https://github.com/behdad/fonttools/issues/99#issuecomment-66776810 + for stamp in 'created', 'modified': + value = getattr(self, stamp) + if value > 0xFFFFFFFF: + log.warning("'%s' timestamp out of range; ignoring top bytes", stamp) + value &= 0xFFFFFFFF + setattr(self, stamp, value) + if value < 0x7C259DC0: # January 1, 1970 00:00:00 + log.warning("'%s' timestamp seems very low; regarding as unix timestamp", stamp) + value += 0x7C259DC0 + setattr(self, stamp, value) + def compile(self, ttFont): + if ttFont.recalcBBoxes: + # For TT-flavored fonts, xMin, yMin, xMax and yMax are set in table__m_a_x_p.recalc(). + if 'CFF ' in ttFont: + topDict = ttFont['CFF '].cff.topDictIndex[0] + self.xMin, self.yMin, self.xMax, self.yMax = topDict.FontBBox if ttFont.recalcTimestamp: - self.modified = int(time.time() - mac_epoch_diff) + self.modified = timestampNow() data = sstruct.pack(headFormat, self) return data - + def toXML(self, writer, ttFont): writer.comment("Most of this table will be recalculated by the compiler") writer.newline() @@ -51,10 +76,7 @@ class table__h_e_a_d(DefaultTable.DefaultTable): for name in names: value = getattr(self, name) if name in ("created", "modified"): - try: - value = time.asctime(time.gmtime(max(0, value + mac_epoch_diff))) - except ValueError: - value = time.asctime(time.gmtime(0)) + value = timestampToString(value) if name in ("magicNumber", "checkSumAdjustment"): if value < 0: value = value + 0x100000000 @@ -65,17 +87,13 @@ class table__h_e_a_d(DefaultTable.DefaultTable): value = num2binary(value, 16) writer.simpletag(name, value=value) writer.newline() - + def fromXML(self, name, attrs, content, ttFont): value = attrs["value"] if name in ("created", "modified"): - value = calendar.timegm(time.strptime(value)) - mac_epoch_diff + value = timestampFromString(value) elif name in ("macStyle", "flags"): value = binary2num(value) else: value = safeEval(value) setattr(self, name, value) - - -# Difference between the original Mac epoch (1904) to the epoch on this machine. -mac_epoch_diff = calendar.timegm((1904, 1, 1, 0, 0, 0, 0, 0, 0)) diff --git a/Lib/fontTools/ttLib/tables/_h_h_e_a.py b/Lib/fontTools/ttLib/tables/_h_h_e_a.py index f8b7eb3..bde9073 100644 --- a/Lib/fontTools/ttLib/tables/_h_h_e_a.py +++ b/Lib/fontTools/ttLib/tables/_h_h_e_a.py @@ -2,12 +2,15 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.misc import sstruct from fontTools.misc.textTools import safeEval +from fontTools.misc.fixedTools import ( + ensureVersionIsLong as fi2ve, versionToFixed as ve2fi) from . import DefaultTable +import math hheaFormat = """ > # big endian - tableVersion: 16.16F + tableVersion: L ascent: h descent: h lineGap: h @@ -28,30 +31,29 @@ hheaFormat = """ class table__h_h_e_a(DefaultTable.DefaultTable): - - dependencies = ['hmtx', 'glyf'] - + + # Note: Keep in sync with table__v_h_e_a + + dependencies = ['hmtx', 'glyf', 'CFF '] + def decompile(self, data, ttFont): sstruct.unpack(hheaFormat, data, self) - + def compile(self, ttFont): - if ttFont.isLoaded('glyf') and ttFont.recalcBBoxes: + if ttFont.recalcBBoxes and (ttFont.isLoaded('glyf') or ttFont.isLoaded('CFF ')): self.recalc(ttFont) + self.tableVersion = fi2ve(self.tableVersion) return sstruct.pack(hheaFormat, self) - + def recalc(self, ttFont): - hmtxTable = ttFont['hmtx'] + if 'hmtx' in ttFont: + hmtxTable = ttFont['hmtx'] + self.advanceWidthMax = max(adv for adv, _ in hmtxTable.metrics.values()) + + boundsWidthDict = {} if 'glyf' in ttFont: glyfTable = ttFont['glyf'] - INFINITY = 100000 - advanceWidthMax = 0 - minLeftSideBearing = +INFINITY # arbitrary big number - minRightSideBearing = +INFINITY # arbitrary big number - xMaxExtent = -INFINITY # arbitrary big negative number - for name in ttFont.getGlyphOrder(): - width, lsb = hmtxTable[name] - advanceWidthMax = max(advanceWidthMax, width) g = glyfTable[name] if g.numberOfContours == 0: continue @@ -59,33 +61,49 @@ class table__h_h_e_a(DefaultTable.DefaultTable): # Composite glyph without extents set. # Calculate those. g.recalcBounds(glyfTable) + boundsWidthDict[name] = g.xMax - g.xMin + elif 'CFF ' in ttFont: + topDict = ttFont['CFF '].cff.topDictIndex[0] + charStrings = topDict.CharStrings + for name in ttFont.getGlyphOrder(): + cs = charStrings[name] + bounds = cs.calcBounds(charStrings) + if bounds is not None: + boundsWidthDict[name] = int( + math.ceil(bounds[2]) - math.floor(bounds[0])) + + if boundsWidthDict: + minLeftSideBearing = float('inf') + minRightSideBearing = float('inf') + xMaxExtent = -float('inf') + for name, boundsWidth in boundsWidthDict.items(): + advanceWidth, lsb = hmtxTable[name] + rsb = advanceWidth - lsb - boundsWidth + extent = lsb + boundsWidth minLeftSideBearing = min(minLeftSideBearing, lsb) - rsb = width - lsb - (g.xMax - g.xMin) minRightSideBearing = min(minRightSideBearing, rsb) - extent = lsb + (g.xMax - g.xMin) xMaxExtent = max(xMaxExtent, extent) - - if xMaxExtent == -INFINITY: - # No glyph has outlines. - minLeftSideBearing = 0 - minRightSideBearing = 0 - xMaxExtent = 0 - - self.advanceWidthMax = advanceWidthMax self.minLeftSideBearing = minLeftSideBearing self.minRightSideBearing = minRightSideBearing self.xMaxExtent = xMaxExtent - else: - # XXX CFF recalc... - pass - + + else: # No glyph has outlines. + self.minLeftSideBearing = 0 + self.minRightSideBearing = 0 + self.xMaxExtent = 0 + def toXML(self, writer, ttFont): formatstring, names, fixes = sstruct.getformat(hheaFormat) for name in names: value = getattr(self, name) + if name == "tableVersion": + value = fi2ve(value) + value = "0x%08x" % value writer.simpletag(name, value=value) writer.newline() - + def fromXML(self, name, attrs, content, ttFont): + if name == "tableVersion": + setattr(self, name, ve2fi(attrs["value"])) + return setattr(self, name, safeEval(attrs["value"])) - diff --git a/Lib/fontTools/ttLib/tables/_h_m_t_x.py b/Lib/fontTools/ttLib/tables/_h_m_t_x.py index c7b5ee9..e6b8ea9 100644 --- a/Lib/fontTools/ttLib/tables/_h_m_t_x.py +++ b/Lib/fontTools/ttLib/tables/_h_m_t_x.py @@ -1,28 +1,40 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import otRound +from fontTools import ttLib from fontTools.misc.textTools import safeEval from . import DefaultTable import sys +import struct import array -import warnings +import logging + + +log = logging.getLogger(__name__) class table__h_m_t_x(DefaultTable.DefaultTable): - + headerTag = 'hhea' advanceName = 'width' sideBearingName = 'lsb' numberOfMetricsName = 'numberOfHMetrics' - + longMetricFormat = 'Hh' + def decompile(self, data, ttFont): numGlyphs = ttFont['maxp'].numGlyphs numberOfMetrics = int(getattr(ttFont[self.headerTag], self.numberOfMetricsName)) if numberOfMetrics > numGlyphs: - numberOfMetrics = numGlyphs # We warn later. - # Note: advanceWidth is unsigned, but we read/write as signed. - metrics = array.array("h", data[:4 * numberOfMetrics]) - if sys.byteorder != "big": - metrics.byteswap() + log.warning("The %s.%s exceeds the maxp.numGlyphs" % ( + self.headerTag, self.numberOfMetricsName)) + numberOfMetrics = numGlyphs + if len(data) < 4 * numberOfMetrics: + raise ttLib.TTLibError("not enough '%s' table data" % self.tableTag) + # Note: advanceWidth is unsigned, but some font editors might + # read/write as signed. We can't be sure whether it was a mistake + # or not, so we read as unsigned but also issue a warning... + metricsFmt = ">" + self.longMetricFormat * numberOfMetrics + metrics = struct.unpack(metricsFmt, data[:4 * numberOfMetrics]) data = data[4 * numberOfMetrics:] numberOfSideBearings = numGlyphs - numberOfMetrics sideBearings = array.array("h", data[:2 * numberOfSideBearings]) @@ -31,21 +43,33 @@ class table__h_m_t_x(DefaultTable.DefaultTable): if sys.byteorder != "big": sideBearings.byteswap() if data: - warnings.warn("too much 'hmtx'/'vmtx' table data") + log.warning("too much '%s' table data" % self.tableTag) self.metrics = {} glyphOrder = ttFont.getGlyphOrder() for i in range(numberOfMetrics): glyphName = glyphOrder[i] - self.metrics[glyphName] = list(metrics[i*2:i*2+2]) + advanceWidth, lsb = metrics[i*2:i*2+2] + if advanceWidth > 32767: + log.warning( + "Glyph %r has a huge advance %s (%d); is it intentional or " + "an (invalid) negative value?", glyphName, self.advanceName, + advanceWidth) + self.metrics[glyphName] = (advanceWidth, lsb) lastAdvance = metrics[-2] for i in range(numberOfSideBearings): glyphName = glyphOrder[i + numberOfMetrics] - self.metrics[glyphName] = [lastAdvance, sideBearings[i]] - + self.metrics[glyphName] = (lastAdvance, sideBearings[i]) + def compile(self, ttFont): metrics = [] + hasNegativeAdvances = False for glyphName in ttFont.getGlyphOrder(): - metrics.append(self.metrics[glyphName]) + advanceWidth, sideBearing = self.metrics[glyphName] + if advanceWidth < 0: + log.error("Glyph %r has negative advance %s" % ( + glyphName, self.advanceName)) + hasNegativeAdvances = True + metrics.append([advanceWidth, sideBearing]) lastAdvance = metrics[-1][0] lastIndex = len(metrics) while metrics[lastIndex-2][0] == lastAdvance: @@ -55,48 +79,53 @@ class table__h_m_t_x(DefaultTable.DefaultTable): lastIndex = 1 break additionalMetrics = metrics[lastIndex:] - additionalMetrics = [sb for advance, sb in additionalMetrics] + additionalMetrics = [otRound(sb) for _, sb in additionalMetrics] metrics = metrics[:lastIndex] - setattr(ttFont[self.headerTag], self.numberOfMetricsName, len(metrics)) - + numberOfMetrics = len(metrics) + setattr(ttFont[self.headerTag], self.numberOfMetricsName, numberOfMetrics) + allMetrics = [] - for item in metrics: - allMetrics.extend(item) - allMetrics = array.array("h", allMetrics) - if sys.byteorder != "big": - allMetrics.byteswap() - data = allMetrics.tostring() - + for advance, sb in metrics: + allMetrics.extend([otRound(advance), otRound(sb)]) + metricsFmt = ">" + self.longMetricFormat * numberOfMetrics + try: + data = struct.pack(metricsFmt, *allMetrics) + except struct.error as e: + if "out of range" in str(e) and hasNegativeAdvances: + raise ttLib.TTLibError( + "'%s' table can't contain negative advance %ss" + % (self.tableTag, self.advanceName)) + else: + raise additionalMetrics = array.array("h", additionalMetrics) if sys.byteorder != "big": additionalMetrics.byteswap() data = data + additionalMetrics.tostring() return data - + def toXML(self, writer, ttFont): names = sorted(self.metrics.keys()) for glyphName in names: advance, sb = self.metrics[glyphName] writer.simpletag("mtx", [ - ("name", glyphName), - (self.advanceName, advance), + ("name", glyphName), + (self.advanceName, advance), (self.sideBearingName, sb), ]) writer.newline() - + def fromXML(self, name, attrs, content, ttFont): if not hasattr(self, "metrics"): self.metrics = {} if name == "mtx": - self.metrics[attrs["name"]] = [safeEval(attrs[self.advanceName]), - safeEval(attrs[self.sideBearingName])] + self.metrics[attrs["name"]] = (safeEval(attrs[self.advanceName]), + safeEval(attrs[self.sideBearingName])) def __delitem__(self, glyphName): del self.metrics[glyphName] - + def __getitem__(self, glyphName): return self.metrics[glyphName] - + def __setitem__(self, glyphName, advance_sb_pair): self.metrics[glyphName] = tuple(advance_sb_pair) - diff --git a/Lib/fontTools/ttLib/tables/_k_e_r_n.py b/Lib/fontTools/ttLib/tables/_k_e_r_n.py index 928298b..6e21a4b 100644 --- a/Lib/fontTools/ttLib/tables/_k_e_r_n.py +++ b/Lib/fontTools/ttLib/tables/_k_e_r_n.py @@ -2,20 +2,27 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.ttLib import getSearchRange from fontTools.misc.textTools import safeEval, readHex -from fontTools.misc.fixedTools import fixedToFloat as fi2fl, floatToFixed as fl2fi +from fontTools.misc.fixedTools import ( + fixedToFloat as fi2fl, + floatToFixed as fl2fi) from . import DefaultTable import struct -import warnings +import sys +import array +import logging + + +log = logging.getLogger(__name__) class table__k_e_r_n(DefaultTable.DefaultTable): - + def getkern(self, format): for subtable in self.kernTables: - if subtable.version == format: + if subtable.format == format: return subtable return None # not found - + def decompile(self, data, ttFont): version, nTables = struct.unpack(">HH", data[:4]) apple = False @@ -28,25 +35,27 @@ class table__k_e_r_n(DefaultTable.DefaultTable): else: self.version = version data = data[4:] - tablesIndex = [] self.kernTables = [] for i in range(nTables): if self.version == 1.0: # Apple - length, coverage, tupleIndex = struct.unpack(">lHH", data[:8]) - version = coverage & 0xff + length, coverage, subtableFormat = struct.unpack( + ">LBB", data[:6]) else: - version, length = struct.unpack(">HH", data[:4]) - length = int(length) - if version not in kern_classes: - subtable = KernTable_format_unkown(version) + # in OpenType spec the "version" field refers to the common + # subtable header; the actual subtable format is stored in + # the 8-15 mask bits of "coverage" field. + # This "version" is always 0 so we ignore it here + _, length, subtableFormat, coverage = struct.unpack( + ">HHBB", data[:6]) + if subtableFormat not in kern_classes: + subtable = KernTable_format_unkown(subtableFormat) else: - subtable = kern_classes[version]() - subtable.apple = apple + subtable = kern_classes[subtableFormat](apple) subtable.decompile(data[:length], ttFont) self.kernTables.append(subtable) data = data[length:] - + def compile(self, ttFont): if hasattr(self, "kernTables"): nTables = len(self.kernTables) @@ -54,20 +63,20 @@ class table__k_e_r_n(DefaultTable.DefaultTable): nTables = 0 if self.version == 1.0: # AAT Apple's "new" format. - data = struct.pack(">ll", fl2fi(self.version, 16), nTables) + data = struct.pack(">LL", fl2fi(self.version, 16), nTables) else: data = struct.pack(">HH", self.version, nTables) if hasattr(self, "kernTables"): for subtable in self.kernTables: data = data + subtable.compile(ttFont) return data - + def toXML(self, writer, ttFont): writer.simpletag("version", value=self.version) writer.newline() for subtable in self.kernTables: subtable.toXML(writer, ttFont) - + def fromXML(self, name, attrs, content, ttFont): if name == "version": self.version = safeEval(attrs["value"]) @@ -80,69 +89,142 @@ class table__k_e_r_n(DefaultTable.DefaultTable): if format not in kern_classes: subtable = KernTable_format_unkown(format) else: - subtable = kern_classes[format]() + apple = self.version == 1.0 + subtable = kern_classes[format](apple) self.kernTables.append(subtable) subtable.fromXML(name, attrs, content, ttFont) class KernTable_format_0(object): - + + # 'version' is kept for backward compatibility + version = format = 0 + + def __init__(self, apple=False): + self.apple = apple + def decompile(self, data, ttFont): - version, length, coverage = (0,0,0) if not self.apple: - version, length, coverage = struct.unpack(">HHH", data[:6]) + version, length, subtableFormat, coverage = struct.unpack( + ">HHBB", data[:6]) + if version != 0: + from fontTools.ttLib import TTLibError + raise TTLibError( + "unsupported kern subtable version: %d" % version) + tupleIndex = None + # Should we also assert length == len(data)? data = data[6:] else: - version, length, coverage = struct.unpack(">LHH", data[:8]) + length, coverage, subtableFormat, tupleIndex = struct.unpack( + ">LBBH", data[:8]) data = data[8:] - self.version, self.coverage = int(version), int(coverage) - + assert self.format == subtableFormat, "unsupported format" + self.coverage = coverage + self.tupleIndex = tupleIndex + self.kernTable = kernTable = {} - - nPairs, searchRange, entrySelector, rangeShift = struct.unpack(">HHHH", data[:8]) + + nPairs, searchRange, entrySelector, rangeShift = struct.unpack( + ">HHHH", data[:8]) data = data[8:] - + + nPairs = min(nPairs, len(data) // 6) + datas = array.array("H", data[:6 * nPairs]) + if sys.byteorder != "big": # pragma: no cover + datas.byteswap() + it = iter(datas) + glyphOrder = ttFont.getGlyphOrder() for k in range(nPairs): - if len(data) < 6: - # buggy kern table - data = b"" - break - left, right, value = struct.unpack(">HHh", data[:6]) - data = data[6:] - left, right = int(left), int(right) - kernTable[(ttFont.getGlyphName(left), ttFont.getGlyphName(right))] = value - if len(data): - warnings.warn("excess data in 'kern' subtable: %d bytes" % len(data)) - + left, right, value = next(it), next(it), next(it) + if value >= 32768: + value -= 65536 + try: + kernTable[(glyphOrder[left], glyphOrder[right])] = value + except IndexError: + # Slower, but will not throw an IndexError on an invalid + # glyph id. + kernTable[( + ttFont.getGlyphName(left), + ttFont.getGlyphName(right))] = value + if len(data) > 6 * nPairs + 4: # Ignore up to 4 bytes excess + log.warning( + "excess data in 'kern' subtable: %d bytes", + len(data) - 6 * nPairs) + def compile(self, ttFont): nPairs = len(self.kernTable) searchRange, entrySelector, rangeShift = getSearchRange(nPairs, 6) - data = struct.pack(">HHHH", nPairs, searchRange, entrySelector, rangeShift) - + data = struct.pack( + ">HHHH", nPairs, searchRange, entrySelector, rangeShift) + # yeehee! (I mean, turn names into indices) - getGlyphID = ttFont.getGlyphID - kernTable = sorted((getGlyphID(left), getGlyphID(right), value) for ((left,right),value) in self.kernTable.items()) + try: + reverseOrder = ttFont.getReverseGlyphMap() + kernTable = sorted( + (reverseOrder[left], reverseOrder[right], value) + for ((left, right), value) in self.kernTable.items()) + except KeyError: + # Slower, but will not throw KeyError on invalid glyph id. + getGlyphID = ttFont.getGlyphID + kernTable = sorted( + (getGlyphID(left), getGlyphID(right), value) + for ((left, right), value) in self.kernTable.items()) + for left, right, value in kernTable: data = data + struct.pack(">HHh", left, right, value) - return struct.pack(">HHH", self.version, len(data) + 6, self.coverage) + data - + + if not self.apple: + version = 0 + length = len(data) + 6 + header = struct.pack( + ">HHBB", version, length, self.format, self.coverage) + else: + if self.tupleIndex is None: + # sensible default when compiling a TTX from an old fonttools + # or when inserting a Windows-style format 0 subtable into an + # Apple version=1.0 kern table + log.warning("'tupleIndex' is None; default to 0") + self.tupleIndex = 0 + length = len(data) + 8 + header = struct.pack( + ">LBBH", length, self.coverage, self.format, self.tupleIndex) + return header + data + def toXML(self, writer, ttFont): - writer.begintag("kernsubtable", coverage=self.coverage, format=0) + attrs = dict(coverage=self.coverage, format=self.format) + if self.apple: + if self.tupleIndex is None: + log.warning("'tupleIndex' is None; default to 0") + attrs["tupleIndex"] = 0 + else: + attrs["tupleIndex"] = self.tupleIndex + writer.begintag("kernsubtable", **attrs) writer.newline() items = sorted(self.kernTable.items()) for (left, right), value in items: writer.simpletag("pair", [ - ("l", left), - ("r", right), - ("v", value) - ]) + ("l", left), + ("r", right), + ("v", value) + ]) writer.newline() writer.endtag("kernsubtable") writer.newline() - + def fromXML(self, name, attrs, content, ttFont): self.coverage = safeEval(attrs["coverage"]) - self.version = safeEval(attrs["format"]) + subtableFormat = safeEval(attrs["format"]) + if self.apple: + if "tupleIndex" in attrs: + self.tupleIndex = safeEval(attrs["tupleIndex"]) + else: + # previous fontTools versions didn't export tupleIndex + log.warning( + "Apple kern subtable is missing 'tupleIndex' attribute") + self.tupleIndex = None + else: + self.tupleIndex = None + assert subtableFormat == self.format, "unsupported format" if not hasattr(self, "kernTable"): self.kernTable = {} for element in content: @@ -150,47 +232,28 @@ class KernTable_format_0(object): continue name, attrs, content = element self.kernTable[(attrs["l"], attrs["r"])] = safeEval(attrs["v"]) - + def __getitem__(self, pair): return self.kernTable[pair] - + def __setitem__(self, pair, value): self.kernTable[pair] = value - + def __delitem__(self, pair): del self.kernTable[pair] -class KernTable_format_2(object): - - def decompile(self, data, ttFont): - self.data = data - - def compile(self, ttFont): - return self.data - - def toXML(self, writer): - writer.begintag("kernsubtable", format=2) - writer.newline() - writer.dumphex(self.data) - writer.endtag("kernsubtable") - writer.newline() - - def fromXML(self, name, attrs, content, ttFont): - self.decompile(readHex(content), ttFont) - - class KernTable_format_unkown(object): - + def __init__(self, format): self.format = format - + def decompile(self, data, ttFont): self.data = data - + def compile(self, ttFont): return self.data - + def toXML(self, writer, ttFont): writer.begintag("kernsubtable", format=self.format) writer.newline() @@ -199,10 +262,9 @@ class KernTable_format_unkown(object): writer.dumphex(self.data) writer.endtag("kernsubtable") writer.newline() - + def fromXML(self, name, attrs, content, ttFont): self.decompile(readHex(content), ttFont) - -kern_classes = {0: KernTable_format_0, 2: KernTable_format_2} +kern_classes = {0: KernTable_format_0} diff --git a/Lib/fontTools/ttLib/tables/_l_c_a_r.py b/Lib/fontTools/ttLib/tables/_l_c_a_r.py new file mode 100644 index 0000000..4b9c854 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/_l_c_a_r.py @@ -0,0 +1,7 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from .otBase import BaseTTXConverter + + +class table__l_c_a_r(BaseTTXConverter): + pass diff --git a/Lib/fontTools/ttLib/tables/_l_o_c_a.py b/Lib/fontTools/ttLib/tables/_l_o_c_a.py index 1ce9cab..2fcd528 100644 --- a/Lib/fontTools/ttLib/tables/_l_o_c_a.py +++ b/Lib/fontTools/ttLib/tables/_l_o_c_a.py @@ -3,12 +3,16 @@ from fontTools.misc.py23 import * from . import DefaultTable import sys import array -import warnings +import logging + + +log = logging.getLogger(__name__) + class table__l_o_c_a(DefaultTable.DefaultTable): - + dependencies = ['glyf'] - + def decompile(self, data, ttFont): longFormat = ttFont['head'].indexToLocFormat if longFormat: @@ -25,16 +29,17 @@ class table__l_o_c_a(DefaultTable.DefaultTable): l.append(locations[i] * 2) locations = l if len(locations) < (ttFont['maxp'].numGlyphs + 1): - warnings.warn("corrupt 'loca' table, or wrong numGlyphs in 'maxp': %d %d" % (len(locations) - 1, ttFont['maxp'].numGlyphs)) + log.warning("corrupt 'loca' table, or wrong numGlyphs in 'maxp': %d %d", + len(locations) - 1, ttFont['maxp'].numGlyphs) self.locations = locations - + def compile(self, ttFont): try: max_location = max(self.locations) except AttributeError: self.set([]) max_location = 0 - if max_location < 0x20000: + if max_location < 0x20000 and all(l % 2 == 0 for l in self.locations): locations = array.array("H") for i in range(len(self.locations)): locations.append(self.locations[i] // 2) @@ -45,17 +50,16 @@ class table__l_o_c_a(DefaultTable.DefaultTable): if sys.byteorder != "big": locations.byteswap() return locations.tostring() - + def set(self, locations): self.locations = array.array("I", locations) - + def toXML(self, writer, ttFont): writer.comment("The 'loca' table will be calculated by the compiler") writer.newline() - + def __getitem__(self, index): return self.locations[index] - + def __len__(self): return len(self.locations) - diff --git a/Lib/fontTools/ttLib/tables/_l_t_a_g.py b/Lib/fontTools/ttLib/tables/_l_t_a_g.py new file mode 100644 index 0000000..59f8e72 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/_l_t_a_g.py @@ -0,0 +1,65 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.textTools import safeEval +from . import DefaultTable +import struct + +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6ltag.html + +class table__l_t_a_g(DefaultTable.DefaultTable): + def __init__(self, tag=None): + DefaultTable.DefaultTable.__init__(self, tag) + self.version, self.flags = 1, 0 + self.tags = [] + + def addTag(self, tag): + """Add 'tag' to the list of langauge tags if not already there. + + Returns the integer index of 'tag' in the list of all tags. + """ + try: + return self.tags.index(tag) + except ValueError: + self.tags.append(tag) + return len(self.tags) - 1 + + def decompile(self, data, ttFont): + self.version, self.flags, numTags = struct.unpack(">LLL", data[:12]) + assert self.version == 1 + self.tags = [] + for i in range(numTags): + pos = 12 + i * 4 + offset, length = struct.unpack(">HH", data[pos:pos+4]) + tag = data[offset:offset+length].decode("ascii") + self.tags.append(tag) + + def compile(self, ttFont): + dataList = [struct.pack(">LLL", self.version, self.flags, len(self.tags))] + stringPool = "" + for tag in self.tags: + offset = stringPool.find(tag) + if offset < 0: + offset = len(stringPool) + stringPool = stringPool + tag + offset = offset + 12 + len(self.tags) * 4 + dataList.append(struct.pack(">HH", offset, len(tag))) + dataList.append(tobytes(stringPool)) + return bytesjoin(dataList) + + def toXML(self, writer, ttFont): + writer.simpletag("version", value=self.version) + writer.newline() + writer.simpletag("flags", value=self.flags) + writer.newline() + for tag in self.tags: + writer.simpletag("LanguageTag", tag=tag) + writer.newline() + + def fromXML(self, name, attrs, content, ttFont): + if not hasattr(self, "tags"): + self.tags = [] + if name == "LanguageTag": + self.tags.append(attrs["tag"]) + elif "value" in attrs: + value = safeEval(attrs["value"]) + setattr(self, name, value) diff --git a/Lib/fontTools/ttLib/tables/_m_a_x_p.py b/Lib/fontTools/ttLib/tables/_m_a_x_p.py index 1089d64..a94a9cf 100644 --- a/Lib/fontTools/ttLib/tables/_m_a_x_p.py +++ b/Lib/fontTools/ttLib/tables/_m_a_x_p.py @@ -29,16 +29,16 @@ maxpFormat_1_0_add = """ class table__m_a_x_p(DefaultTable.DefaultTable): - + dependencies = ['glyf'] - + def decompile(self, data, ttFont): dummy, data = sstruct.unpack2(maxpFormat_0_5, data, self) self.numGlyphs = int(self.numGlyphs) if self.tableVersion != 0x00005000: dummy, data = sstruct.unpack2(maxpFormat_1_0_add, data, self) assert len(data) == 0 - + def compile(self, ttFont): if 'glyf' in ttFont: if ttFont.isLoaded('glyf') and ttFont.recalcBBoxes: @@ -52,7 +52,7 @@ class table__m_a_x_p(DefaultTable.DefaultTable): if self.tableVersion == 0x00010000: data = data + sstruct.pack(maxpFormat_1_0_add, self) return data - + def recalc(self, ttFont): """Recalculate the font bounding box, and most other maxp values except for the TT instructions values. Also recalculate the value of bit 1 @@ -73,12 +73,12 @@ class table__m_a_x_p(DefaultTable.DefaultTable): maxCompositeContours = 0 maxComponentElements = 0 maxComponentDepth = 0 - allXMaxIsLsb = 1 + allXMinIsLsb = 1 for glyphName in ttFont.getGlyphOrder(): g = glyfTable[glyphName] if g.numberOfContours: if hmtxTable[glyphName][1] != g.xMin: - allXMaxIsLsb = 0 + allXMinIsLsb = 0 xMin = min(xMin, g.xMin) yMin = min(yMin, g.yMin) xMax = max(xMax, g.xMax) @@ -99,27 +99,27 @@ class table__m_a_x_p(DefaultTable.DefaultTable): headTable.xMax = 0 headTable.yMax = 0 else: - headTable.xMin = xMin - headTable.yMin = yMin - headTable.xMax = xMax - headTable.yMax = yMax + headTable.xMin = xMin + headTable.yMin = yMin + headTable.xMax = xMax + headTable.yMax = yMax self.maxPoints = maxPoints self.maxContours = maxContours self.maxCompositePoints = maxCompositePoints self.maxCompositeContours = maxCompositeContours self.maxComponentDepth = maxComponentDepth - if allXMaxIsLsb: + if allXMinIsLsb: headTable.flags = headTable.flags | 0x2 else: headTable.flags = headTable.flags & ~0x2 - + def testrepr(self): items = sorted(self.__dict__.items()) print(". . . . . . . . .") for combo in items: print(" %s: %s" % combo) print(". . . . . . . . .") - + def toXML(self, writer, ttFont): if self.tableVersion != 0x00005000: writer.comment("Most of this table will be recalculated by the compiler") @@ -134,8 +134,6 @@ class table__m_a_x_p(DefaultTable.DefaultTable): value = hex(value) writer.simpletag(name, value=value) writer.newline() - + def fromXML(self, name, attrs, content, ttFont): setattr(self, name, safeEval(attrs["value"])) - - diff --git a/Lib/fontTools/ttLib/tables/_m_e_t_a.py b/Lib/fontTools/ttLib/tables/_m_e_t_a.py new file mode 100644 index 0000000..cc19fe6 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/_m_e_t_a.py @@ -0,0 +1,99 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc import sstruct +from fontTools.misc.textTools import readHex +from fontTools.ttLib import TTLibError +from . import DefaultTable + +# Apple's documentation of 'meta': +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6meta.html + +META_HEADER_FORMAT = """ + > # big endian + version: L + flags: L + dataOffset: L + numDataMaps: L +""" + + +DATA_MAP_FORMAT = """ + > # big endian + tag: 4s + dataOffset: L + dataLength: L +""" + + +class table__m_e_t_a(DefaultTable.DefaultTable): + def __init__(self, tag=None): + DefaultTable.DefaultTable.__init__(self, tag) + self.data = {} + + def decompile(self, data, ttFont): + headerSize = sstruct.calcsize(META_HEADER_FORMAT) + header = sstruct.unpack(META_HEADER_FORMAT, data[0 : headerSize]) + if header["version"] != 1: + raise TTLibError("unsupported 'meta' version %d" % + header["version"]) + dataMapSize = sstruct.calcsize(DATA_MAP_FORMAT) + for i in range(header["numDataMaps"]): + dataMapOffset = headerSize + i * dataMapSize + dataMap = sstruct.unpack( + DATA_MAP_FORMAT, + data[dataMapOffset : dataMapOffset + dataMapSize]) + tag = dataMap["tag"] + offset = dataMap["dataOffset"] + self.data[tag] = data[offset : offset + dataMap["dataLength"]] + if tag in ["dlng", "slng"]: + self.data[tag] = self.data[tag].decode("utf-8") + + def compile(self, ttFont): + keys = sorted(self.data.keys()) + headerSize = sstruct.calcsize(META_HEADER_FORMAT) + dataOffset = headerSize + len(keys) * sstruct.calcsize(DATA_MAP_FORMAT) + header = sstruct.pack(META_HEADER_FORMAT, { + "version": 1, + "flags": 0, + "dataOffset": dataOffset, + "numDataMaps": len(keys) + }) + dataMaps = [] + dataBlocks = [] + for tag in keys: + if tag in ["dlng", "slng"]: + data = self.data[tag].encode("utf-8") + else: + data = self.data[tag] + dataMaps.append(sstruct.pack(DATA_MAP_FORMAT, { + "tag": tag, + "dataOffset": dataOffset, + "dataLength": len(data) + })) + dataBlocks.append(data) + dataOffset += len(data) + return bytesjoin([header] + dataMaps + dataBlocks) + + def toXML(self, writer, ttFont): + for tag in sorted(self.data.keys()): + if tag in ["dlng", "slng"]: + writer.begintag("text", tag=tag) + writer.newline() + writer.write(self.data[tag]) + writer.newline() + writer.endtag("text") + writer.newline() + else: + writer.begintag("hexdata", tag=tag) + writer.newline() + writer.dumphex(self.data[tag]) + writer.endtag("hexdata") + writer.newline() + + def fromXML(self, name, attrs, content, ttFont): + if name == "hexdata": + self.data[attrs["tag"]] = readHex(content) + elif name == "text" and attrs["tag"] in ["dlng", "slng"]: + self.data[attrs["tag"]] = strjoin(content).strip() + else: + raise TTLibError("can't handle '%s' element" % name) diff --git a/Lib/fontTools/ttLib/tables/_m_o_r_t.py b/Lib/fontTools/ttLib/tables/_m_o_r_t.py new file mode 100644 index 0000000..b87b425 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/_m_o_r_t.py @@ -0,0 +1,8 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from .otBase import BaseTTXConverter + + +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6mort.html +class table__m_o_r_t(BaseTTXConverter): + pass diff --git a/Lib/fontTools/ttLib/tables/_m_o_r_x.py b/Lib/fontTools/ttLib/tables/_m_o_r_x.py new file mode 100644 index 0000000..1619d8d --- /dev/null +++ b/Lib/fontTools/ttLib/tables/_m_o_r_x.py @@ -0,0 +1,8 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from .otBase import BaseTTXConverter + + +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6morx.html +class table__m_o_r_x(BaseTTXConverter): + pass diff --git a/Lib/fontTools/ttLib/tables/_n_a_m_e.py b/Lib/fontTools/ttLib/tables/_n_a_m_e.py index 53fde4d..a30291c 100644 --- a/Lib/fontTools/ttLib/tables/_n_a_m_e.py +++ b/Lib/fontTools/ttLib/tables/_n_a_m_e.py @@ -1,9 +1,17 @@ +# -*- coding: utf-8 -*- from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals from fontTools.misc.py23 import * from fontTools.misc import sstruct from fontTools.misc.textTools import safeEval +from fontTools.misc.encodingTools import getEncoding +from fontTools.ttLib import newTable from . import DefaultTable import struct +import logging + + +log = logging.getLogger(__name__) nameRecordFormat = """ > # big endian @@ -19,22 +27,27 @@ nameRecordSize = sstruct.calcsize(nameRecordFormat) class table__n_a_m_e(DefaultTable.DefaultTable): - + dependencies = ["ltag"] + def decompile(self, data, ttFont): - format, n, stringOffset = struct.unpack(">HHH", data[:6]) + format, n, stringOffset = struct.unpack(b">HHH", data[:6]) expectedStringOffset = 6 + n * nameRecordSize if stringOffset != expectedStringOffset: - # XXX we need a warn function - print("Warning: 'name' table stringOffset incorrect. Expected: %s; Actual: %s" % (expectedStringOffset, stringOffset)) + log.error( + "'name' table stringOffset incorrect. Expected: %s; Actual: %s", + expectedStringOffset, stringOffset) stringData = data[stringOffset:] data = data[6:] self.names = [] for i in range(n): if len(data) < 12: - # compensate for buggy font - break + log.error('skipping malformed name record #%d', i) + continue name, data = sstruct.unpack2(nameRecordFormat, data, NameRecord()) name.string = stringData[name.offset:name.offset+name.length] + if name.offset + name.length > len(stringData): + log.error('skipping malformed name record #%d', i) + continue assert len(name.string) == name.length #if (name.platEncID, name.platformID) in ((0, 0), (1, 3)): # if len(name.string) % 2: @@ -42,33 +55,35 @@ class table__n_a_m_e(DefaultTable.DefaultTable): # print name.__dict__ del name.offset, name.length self.names.append(name) - + def compile(self, ttFont): if not hasattr(self, "names"): # only happens when there are NO name table entries read # from the TTX file self.names = [] - self.names.sort() # sort according to the spec; see NameRecord.__lt__() + names = self.names + names.sort() # sort according to the spec; see NameRecord.__lt__() stringData = b"" format = 0 - n = len(self.names) + n = len(names) stringOffset = 6 + n * sstruct.calcsize(nameRecordFormat) - data = struct.pack(">HHH", format, n, stringOffset) + data = struct.pack(b">HHH", format, n, stringOffset) lastoffset = 0 done = {} # remember the data so we can reuse the "pointers" - for name in self.names: - if name.string in done: - name.offset, name.length = done[name.string] + for name in names: + string = name.toBytes() + if string in done: + name.offset, name.length = done[string] else: - name.offset, name.length = done[name.string] = len(stringData), len(name.string) - stringData = stringData + name.string + name.offset, name.length = done[string] = len(stringData), len(string) + stringData = bytesjoin([stringData, string]) data = data + sstruct.pack(nameRecordFormat, name) return data + stringData - + def toXML(self, writer, ttFont): for name in self.names: name.toXML(writer, ttFont) - + def fromXML(self, name, attrs, content, ttFont): if name != "namerecord": return # ignore unknown tags @@ -77,56 +92,347 @@ class table__n_a_m_e(DefaultTable.DefaultTable): name = NameRecord() self.names.append(name) name.fromXML(name, attrs, content, ttFont) - + def getName(self, nameID, platformID, platEncID, langID=None): for namerecord in self.names: - if ( namerecord.nameID == nameID and - namerecord.platformID == platformID and + if ( namerecord.nameID == nameID and + namerecord.platformID == platformID and namerecord.platEncID == platEncID): if langID is None or namerecord.langID == langID: return namerecord return None # not found + def getDebugName(self, nameID): + englishName = someName = None + for name in self.names: + if name.nameID != nameID: + continue + try: + unistr = name.toUnicode() + except UnicodeDecodeError: + continue + + someName = unistr + if (name.platformID, name.langID) in ((1, 0), (3, 0x409)): + englishName = unistr + break + if englishName: + return englishName + elif someName: + return someName + else: + return None + + def setName(self, string, nameID, platformID, platEncID, langID): + """ Set the 'string' for the name record identified by 'nameID', 'platformID', + 'platEncID' and 'langID'. If a record with that nameID doesn't exist, create it + and append to the name table. + + 'string' can be of type `str` (`unicode` in PY2) or `bytes`. In the latter case, + it is assumed to be already encoded with the correct plaform-specific encoding + identified by the (platformID, platEncID, langID) triplet. A warning is issued + to prevent unexpected results. + """ + if not hasattr(self, 'names'): + self.names = [] + if not isinstance(string, unicode): + if isinstance(string, bytes): + log.warning( + "name string is bytes, ensure it's correctly encoded: %r", string) + else: + raise TypeError( + "expected unicode or bytes, found %s: %r" % ( + type(string).__name__, string)) + namerecord = self.getName(nameID, platformID, platEncID, langID) + if namerecord: + namerecord.string = string + else: + self.names.append(makeName(string, nameID, platformID, platEncID, langID)) + + def _findUnusedNameID(self, minNameID=256): + """Finds an unused name id. + + The nameID is assigned in the range between 'minNameID' and 32767 (inclusive), + following the last nameID in the name table. + """ + names = getattr(self, 'names', []) + nameID = 1 + max([n.nameID for n in names] + [minNameID - 1]) + if nameID > 32767: + raise ValueError("nameID must be less than 32768") + return nameID + + def addMultilingualName(self, names, ttFont=None, nameID=None): + """Add a multilingual name, returning its name ID + + 'names' is a dictionary with the name in multiple languages, + such as {'en': 'Pale', 'de': 'Blaß', 'de-CH': 'Blass'}. + The keys can be arbitrary IETF BCP 47 language codes; + the values are Unicode strings. + + 'ttFont' is the TTFont to which the names are added, or None. + If present, the font's 'ltag' table can get populated + to store exotic language codes, which allows encoding + names that otherwise cannot get encoded at all. + + 'nameID' is the name ID to be used, or None to let the library + pick an unused name ID. + """ + if not hasattr(self, 'names'): + self.names = [] + if nameID is None: + nameID = self._findUnusedNameID() + # TODO: Should minimize BCP 47 language codes. + # https://github.com/fonttools/fonttools/issues/930 + for lang, name in sorted(names.items()): + # Apple platforms have been recognizing Windows names + # since early OSX (~2001), so we only add names + # for the Macintosh platform when we cannot not make + # a Windows name. This can happen for exotic BCP47 + # language tags that have no Windows language code. + windowsName = _makeWindowsName(name, nameID, lang) + if windowsName is not None: + self.names.append(windowsName) + else: + macName = _makeMacName(name, nameID, lang, ttFont) + if macName is not None: + self.names.append(macName) + return nameID + + def addName(self, string, platforms=((1, 0, 0), (3, 1, 0x409)), minNameID=255): + """ Add a new name record containing 'string' for each (platformID, platEncID, + langID) tuple specified in the 'platforms' list. + + The nameID is assigned in the range between 'minNameID'+1 and 32767 (inclusive), + following the last nameID in the name table. + If no 'platforms' are specified, two English name records are added, one for the + Macintosh (platformID=0), and one for the Windows platform (3). + + The 'string' must be a Unicode string, so it can be encoded with different, + platform-specific encodings. + + Return the new nameID. + """ + assert len(platforms) > 0, \ + "'platforms' must contain at least one (platformID, platEncID, langID) tuple" + if not hasattr(self, 'names'): + self.names = [] + if not isinstance(string, unicode): + raise TypeError( + "expected %s, found %s: %r" % ( + unicode.__name__, type(string).__name__,string )) + nameID = self._findUnusedNameID(minNameID + 1) + for platformID, platEncID, langID in platforms: + self.names.append(makeName(string, nameID, platformID, platEncID, langID)) + return nameID + + +def makeName(string, nameID, platformID, platEncID, langID): + name = NameRecord() + name.string, name.nameID, name.platformID, name.platEncID, name.langID = ( + string, nameID, platformID, platEncID, langID) + return name + + +def _makeWindowsName(name, nameID, language): + """Create a NameRecord for the Microsoft Windows platform + + 'language' is an arbitrary IETF BCP 47 language identifier such + as 'en', 'de-CH', 'de-AT-1901', or 'fa-Latn'. If Microsoft Windows + does not support the desired language, the result will be None. + Future versions of fonttools might return a NameRecord for the + OpenType 'name' table format 1, but this is not implemented yet. + """ + langID = _WINDOWS_LANGUAGE_CODES.get(language.lower()) + if langID is not None: + return makeName(name, nameID, 3, 1, langID) + else: + log.warning("cannot add Windows name in language %s " + "because fonttools does not yet support " + "name table format 1" % language) + return None + + +def _makeMacName(name, nameID, language, font=None): + """Create a NameRecord for Apple platforms + + 'language' is an arbitrary IETF BCP 47 language identifier such + as 'en', 'de-CH', 'de-AT-1901', or 'fa-Latn'. When possible, we + create a Macintosh NameRecord that is understood by old applications + (platform ID 1 and an old-style Macintosh language enum). If this + is not possible, we create a Unicode NameRecord (platform ID 0) + whose language points to the font’s 'ltag' table. The latter + can encode any string in any language, but legacy applications + might not recognize the format (in which case they will ignore + those names). + + 'font' should be the TTFont for which you want to create a name. + If 'font' is None, we only return NameRecords for legacy Macintosh; + in that case, the result will be None for names that need to + be encoded with an 'ltag' table. + + See the section “The language identifier” in Apple’s specification: + https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6name.html + """ + macLang = _MAC_LANGUAGE_CODES.get(language.lower()) + macScript = _MAC_LANGUAGE_TO_SCRIPT.get(macLang) + if macLang is not None and macScript is not None: + encoding = getEncoding(1, macScript, macLang, default="ascii") + # Check if we can actually encode this name. If we can't, + # for example because we have no support for the legacy + # encoding, or because the name string contains Unicode + # characters that the legacy encoding cannot represent, + # we fall back to encoding the name in Unicode and put + # the language tag into the ltag table. + try: + _ = tobytes(name, encoding, errors="strict") + return makeName(name, nameID, 1, macScript, macLang) + except UnicodeEncodeError: + pass + if font is not None: + ltag = font.tables.get("ltag") + if ltag is None: + ltag = font["ltag"] = newTable("ltag") + # 0 = Unicode; 4 = “Unicode 2.0 or later semantics (non-BMP characters allowed)” + # “The preferred platform-specific code for Unicode would be 3 or 4.” + # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6name.html + return makeName(name, nameID, 0, 4, ltag.addTag(language)) + else: + log.warning("cannot store language %s into 'ltag' table " + "without having access to the TTFont object" % + language) + return None + class NameRecord(object): - + + def getEncoding(self, default='ascii'): + """Returns the Python encoding name for this name entry based on its platformID, + platEncID, and langID. If encoding for these values is not known, by default + 'ascii' is returned. That can be overriden by passing a value to the default + argument. + """ + return getEncoding(self.platformID, self.platEncID, self.langID, default) + + def encodingIsUnicodeCompatible(self): + return self.getEncoding(None) in ['utf_16_be', 'ucs2be', 'ascii', 'latin1'] + + def __str__(self): + return self.toStr(errors='backslashreplace') + def isUnicode(self): return (self.platformID == 0 or (self.platformID == 3 and self.platEncID in [0, 1, 10])) + def toUnicode(self, errors='strict'): + """ + If self.string is a Unicode string, return it; otherwise try decoding the + bytes in self.string to a Unicode string using the encoding of this + entry as returned by self.getEncoding(); Note that self.getEncoding() + returns 'ascii' if the encoding is unknown to the library. + + Certain heuristics are performed to recover data from bytes that are + ill-formed in the chosen encoding, or that otherwise look misencoded + (mostly around bad UTF-16BE encoded bytes, or bytes that look like UTF-16BE + but marked otherwise). If the bytes are ill-formed and the heuristics fail, + the error is handled according to the errors parameter to this function, which is + passed to the underlying decode() function; by default it throws a + UnicodeDecodeError exception. + + Note: The mentioned heuristics mean that roundtripping a font to XML and back + to binary might recover some misencoded data whereas just loading the font + and saving it back will not change them. + """ + def isascii(b): + return (b >= 0x20 and b <= 0x7E) or b in [0x09, 0x0A, 0x0D] + encoding = self.getEncoding() + string = self.string + + if encoding == 'utf_16_be' and len(string) % 2 == 1: + # Recover badly encoded UTF-16 strings that have an odd number of bytes: + # - If the last byte is zero, drop it. Otherwise, + # - If all the odd bytes are zero and all the even bytes are ASCII, + # prepend one zero byte. Otherwise, + # - If first byte is zero and all other bytes are ASCII, insert zero + # bytes between consecutive ASCII bytes. + # + # (Yes, I've seen all of these in the wild... sigh) + if byteord(string[-1]) == 0: + string = string[:-1] + elif all(byteord(b) == 0 if i % 2 else isascii(byteord(b)) for i,b in enumerate(string)): + string = b'\0' + string + elif byteord(string[0]) == 0 and all(isascii(byteord(b)) for b in string[1:]): + string = bytesjoin(b'\0'+bytechr(byteord(b)) for b in string[1:]) + + string = tounicode(string, encoding=encoding, errors=errors) + + # If decoded strings still looks like UTF-16BE, it suggests a double-encoding. + # Fix it up. + if all(ord(c) == 0 if i % 2 == 0 else isascii(ord(c)) for i,c in enumerate(string)): + # If string claims to be Mac encoding, but looks like UTF-16BE with ASCII text, + # narrow it down. + string = ''.join(c for c in string[1::2]) + + return string + + def toBytes(self, errors='strict'): + """ If self.string is a bytes object, return it; otherwise try encoding + the Unicode string in self.string to bytes using the encoding of this + entry as returned by self.getEncoding(); Note that self.getEncoding() + returns 'ascii' if the encoding is unknown to the library. + + If the Unicode string cannot be encoded to bytes in the chosen encoding, + the error is handled according to the errors parameter to this function, + which is passed to the underlying encode() function; by default it throws a + UnicodeEncodeError exception. + """ + return tobytes(self.string, encoding=self.getEncoding(), errors=errors) + + def toStr(self, errors='strict'): + if str == bytes: + # python 2 + return self.toBytes(errors) + else: + # python 3 + return self.toUnicode(errors) + def toXML(self, writer, ttFont): - writer.begintag("namerecord", [ + try: + unistr = self.toUnicode() + except UnicodeDecodeError: + unistr = None + attrs = [ ("nameID", self.nameID), ("platformID", self.platformID), ("platEncID", self.platEncID), ("langID", hex(self.langID)), - ]) + ] + + if unistr is None or not self.encodingIsUnicodeCompatible(): + attrs.append(("unicode", unistr is not None)) + + writer.begintag("namerecord", attrs) writer.newline() - if self.isUnicode(): - if len(self.string) % 2: - # no, shouldn't happen, but some of the Apple - # tools cause this anyway :-( - writer.write16bit(self.string + b"\0", strip=True) - else: - writer.write16bit(self.string, strip=True) + if unistr is not None: + writer.write(unistr) else: - writer.write8bit(self.string, strip=True) + writer.write8bit(self.string) writer.newline() writer.endtag("namerecord") writer.newline() - + def fromXML(self, name, attrs, content, ttFont): self.nameID = safeEval(attrs["nameID"]) self.platformID = safeEval(attrs["platformID"]) self.platEncID = safeEval(attrs["platEncID"]) self.langID = safeEval(attrs["langID"]) s = strjoin(content).strip() - if self.isUnicode(): - self.string = s.encode("utf_16_be") + encoding = self.getEncoding() + if self.encodingIsUnicodeCompatible() or safeEval(attrs.get("unicode", "False")): + self.string = s.encode(encoding) else: # This is the inverse of write8bit... self.string = s.encode("latin1") - + def __lt__(self, other): if type(self) != type(other): return NotImplemented @@ -147,7 +453,497 @@ class NameRecord(object): getattr(other, "string", None), ) return selfTuple < otherTuple - + def __repr__(self): return "<NameRecord NameID=%d; PlatformID=%d; LanguageID=%d>" % ( self.nameID, self.platformID, self.langID) + + +# Windows language ID → IETF BCP-47 language tag +# +# While Microsoft indicates a region/country for all its language +# IDs, we follow Unicode practice by omitting “most likely subtags” +# as per Unicode CLDR. For example, English is simply “en” and not +# “en-Latn” because according to Unicode, the default script +# for English is Latin. +# +# http://www.unicode.org/cldr/charts/latest/supplemental/likely_subtags.html +# http://www.iana.org/assignments/language-subtag-registry/language-subtag-registry +_WINDOWS_LANGUAGES = { + 0x0436: 'af', + 0x041C: 'sq', + 0x0484: 'gsw', + 0x045E: 'am', + 0x1401: 'ar-DZ', + 0x3C01: 'ar-BH', + 0x0C01: 'ar', + 0x0801: 'ar-IQ', + 0x2C01: 'ar-JO', + 0x3401: 'ar-KW', + 0x3001: 'ar-LB', + 0x1001: 'ar-LY', + 0x1801: 'ary', + 0x2001: 'ar-OM', + 0x4001: 'ar-QA', + 0x0401: 'ar-SA', + 0x2801: 'ar-SY', + 0x1C01: 'aeb', + 0x3801: 'ar-AE', + 0x2401: 'ar-YE', + 0x042B: 'hy', + 0x044D: 'as', + 0x082C: 'az-Cyrl', + 0x042C: 'az', + 0x046D: 'ba', + 0x042D: 'eu', + 0x0423: 'be', + 0x0845: 'bn', + 0x0445: 'bn-IN', + 0x201A: 'bs-Cyrl', + 0x141A: 'bs', + 0x047E: 'br', + 0x0402: 'bg', + 0x0403: 'ca', + 0x0C04: 'zh-HK', + 0x1404: 'zh-MO', + 0x0804: 'zh', + 0x1004: 'zh-SG', + 0x0404: 'zh-TW', + 0x0483: 'co', + 0x041A: 'hr', + 0x101A: 'hr-BA', + 0x0405: 'cs', + 0x0406: 'da', + 0x048C: 'prs', + 0x0465: 'dv', + 0x0813: 'nl-BE', + 0x0413: 'nl', + 0x0C09: 'en-AU', + 0x2809: 'en-BZ', + 0x1009: 'en-CA', + 0x2409: 'en-029', + 0x4009: 'en-IN', + 0x1809: 'en-IE', + 0x2009: 'en-JM', + 0x4409: 'en-MY', + 0x1409: 'en-NZ', + 0x3409: 'en-PH', + 0x4809: 'en-SG', + 0x1C09: 'en-ZA', + 0x2C09: 'en-TT', + 0x0809: 'en-GB', + 0x0409: 'en', + 0x3009: 'en-ZW', + 0x0425: 'et', + 0x0438: 'fo', + 0x0464: 'fil', + 0x040B: 'fi', + 0x080C: 'fr-BE', + 0x0C0C: 'fr-CA', + 0x040C: 'fr', + 0x140C: 'fr-LU', + 0x180C: 'fr-MC', + 0x100C: 'fr-CH', + 0x0462: 'fy', + 0x0456: 'gl', + 0x0437: 'ka', + 0x0C07: 'de-AT', + 0x0407: 'de', + 0x1407: 'de-LI', + 0x1007: 'de-LU', + 0x0807: 'de-CH', + 0x0408: 'el', + 0x046F: 'kl', + 0x0447: 'gu', + 0x0468: 'ha', + 0x040D: 'he', + 0x0439: 'hi', + 0x040E: 'hu', + 0x040F: 'is', + 0x0470: 'ig', + 0x0421: 'id', + 0x045D: 'iu', + 0x085D: 'iu-Latn', + 0x083C: 'ga', + 0x0434: 'xh', + 0x0435: 'zu', + 0x0410: 'it', + 0x0810: 'it-CH', + 0x0411: 'ja', + 0x044B: 'kn', + 0x043F: 'kk', + 0x0453: 'km', + 0x0486: 'quc', + 0x0487: 'rw', + 0x0441: 'sw', + 0x0457: 'kok', + 0x0412: 'ko', + 0x0440: 'ky', + 0x0454: 'lo', + 0x0426: 'lv', + 0x0427: 'lt', + 0x082E: 'dsb', + 0x046E: 'lb', + 0x042F: 'mk', + 0x083E: 'ms-BN', + 0x043E: 'ms', + 0x044C: 'ml', + 0x043A: 'mt', + 0x0481: 'mi', + 0x047A: 'arn', + 0x044E: 'mr', + 0x047C: 'moh', + 0x0450: 'mn', + 0x0850: 'mn-CN', + 0x0461: 'ne', + 0x0414: 'nb', + 0x0814: 'nn', + 0x0482: 'oc', + 0x0448: 'or', + 0x0463: 'ps', + 0x0415: 'pl', + 0x0416: 'pt', + 0x0816: 'pt-PT', + 0x0446: 'pa', + 0x046B: 'qu-BO', + 0x086B: 'qu-EC', + 0x0C6B: 'qu', + 0x0418: 'ro', + 0x0417: 'rm', + 0x0419: 'ru', + 0x243B: 'smn', + 0x103B: 'smj-NO', + 0x143B: 'smj', + 0x0C3B: 'se-FI', + 0x043B: 'se', + 0x083B: 'se-SE', + 0x203B: 'sms', + 0x183B: 'sma-NO', + 0x1C3B: 'sms', + 0x044F: 'sa', + 0x1C1A: 'sr-Cyrl-BA', + 0x0C1A: 'sr', + 0x181A: 'sr-Latn-BA', + 0x081A: 'sr-Latn', + 0x046C: 'nso', + 0x0432: 'tn', + 0x045B: 'si', + 0x041B: 'sk', + 0x0424: 'sl', + 0x2C0A: 'es-AR', + 0x400A: 'es-BO', + 0x340A: 'es-CL', + 0x240A: 'es-CO', + 0x140A: 'es-CR', + 0x1C0A: 'es-DO', + 0x300A: 'es-EC', + 0x440A: 'es-SV', + 0x100A: 'es-GT', + 0x480A: 'es-HN', + 0x080A: 'es-MX', + 0x4C0A: 'es-NI', + 0x180A: 'es-PA', + 0x3C0A: 'es-PY', + 0x280A: 'es-PE', + 0x500A: 'es-PR', + + # Microsoft has defined two different language codes for + # “Spanish with modern sorting” and “Spanish with traditional + # sorting”. This makes sense for collation APIs, and it would be + # possible to express this in BCP 47 language tags via Unicode + # extensions (eg., “es-u-co-trad” is “Spanish with traditional + # sorting”). However, for storing names in fonts, this distinction + # does not make sense, so we use “es” in both cases. + 0x0C0A: 'es', + 0x040A: 'es', + + 0x540A: 'es-US', + 0x380A: 'es-UY', + 0x200A: 'es-VE', + 0x081D: 'sv-FI', + 0x041D: 'sv', + 0x045A: 'syr', + 0x0428: 'tg', + 0x085F: 'tzm', + 0x0449: 'ta', + 0x0444: 'tt', + 0x044A: 'te', + 0x041E: 'th', + 0x0451: 'bo', + 0x041F: 'tr', + 0x0442: 'tk', + 0x0480: 'ug', + 0x0422: 'uk', + 0x042E: 'hsb', + 0x0420: 'ur', + 0x0843: 'uz-Cyrl', + 0x0443: 'uz', + 0x042A: 'vi', + 0x0452: 'cy', + 0x0488: 'wo', + 0x0485: 'sah', + 0x0478: 'ii', + 0x046A: 'yo', +} + + +_MAC_LANGUAGES = { + 0: 'en', + 1: 'fr', + 2: 'de', + 3: 'it', + 4: 'nl', + 5: 'sv', + 6: 'es', + 7: 'da', + 8: 'pt', + 9: 'no', + 10: 'he', + 11: 'ja', + 12: 'ar', + 13: 'fi', + 14: 'el', + 15: 'is', + 16: 'mt', + 17: 'tr', + 18: 'hr', + 19: 'zh-Hant', + 20: 'ur', + 21: 'hi', + 22: 'th', + 23: 'ko', + 24: 'lt', + 25: 'pl', + 26: 'hu', + 27: 'es', + 28: 'lv', + 29: 'se', + 30: 'fo', + 31: 'fa', + 32: 'ru', + 33: 'zh', + 34: 'nl-BE', + 35: 'ga', + 36: 'sq', + 37: 'ro', + 38: 'cz', + 39: 'sk', + 40: 'sl', + 41: 'yi', + 42: 'sr', + 43: 'mk', + 44: 'bg', + 45: 'uk', + 46: 'be', + 47: 'uz', + 48: 'kk', + 49: 'az-Cyrl', + 50: 'az-Arab', + 51: 'hy', + 52: 'ka', + 53: 'mo', + 54: 'ky', + 55: 'tg', + 56: 'tk', + 57: 'mn-CN', + 58: 'mn', + 59: 'ps', + 60: 'ks', + 61: 'ku', + 62: 'sd', + 63: 'bo', + 64: 'ne', + 65: 'sa', + 66: 'mr', + 67: 'bn', + 68: 'as', + 69: 'gu', + 70: 'pa', + 71: 'or', + 72: 'ml', + 73: 'kn', + 74: 'ta', + 75: 'te', + 76: 'si', + 77: 'my', + 78: 'km', + 79: 'lo', + 80: 'vi', + 81: 'id', + 82: 'tl', + 83: 'ms', + 84: 'ms-Arab', + 85: 'am', + 86: 'ti', + 87: 'om', + 88: 'so', + 89: 'sw', + 90: 'rw', + 91: 'rn', + 92: 'ny', + 93: 'mg', + 94: 'eo', + 128: 'cy', + 129: 'eu', + 130: 'ca', + 131: 'la', + 132: 'qu', + 133: 'gn', + 134: 'ay', + 135: 'tt', + 136: 'ug', + 137: 'dz', + 138: 'jv', + 139: 'su', + 140: 'gl', + 141: 'af', + 142: 'br', + 143: 'iu', + 144: 'gd', + 145: 'gv', + 146: 'ga', + 147: 'to', + 148: 'el-polyton', + 149: 'kl', + 150: 'az', + 151: 'nn', +} + + +_WINDOWS_LANGUAGE_CODES = {lang.lower(): code for code, lang in _WINDOWS_LANGUAGES.items()} +_MAC_LANGUAGE_CODES = {lang.lower(): code for code, lang in _MAC_LANGUAGES.items()} + + +# MacOS language ID → MacOS script ID +# +# Note that the script ID is not sufficient to determine what encoding +# to use in TrueType files. For some languages, MacOS used a modification +# of a mainstream script. For example, an Icelandic name would be stored +# with smRoman in the TrueType naming table, but the actual encoding +# is a special Icelandic version of the normal Macintosh Roman encoding. +# As another example, Inuktitut uses an 8-bit encoding for Canadian Aboriginal +# Syllables but MacOS had run out of available script codes, so this was +# done as a (pretty radical) “modification” of Ethiopic. +# +# http://unicode.org/Public/MAPPINGS/VENDORS/APPLE/Readme.txt +_MAC_LANGUAGE_TO_SCRIPT = { + 0: 0, # langEnglish → smRoman + 1: 0, # langFrench → smRoman + 2: 0, # langGerman → smRoman + 3: 0, # langItalian → smRoman + 4: 0, # langDutch → smRoman + 5: 0, # langSwedish → smRoman + 6: 0, # langSpanish → smRoman + 7: 0, # langDanish → smRoman + 8: 0, # langPortuguese → smRoman + 9: 0, # langNorwegian → smRoman + 10: 5, # langHebrew → smHebrew + 11: 1, # langJapanese → smJapanese + 12: 4, # langArabic → smArabic + 13: 0, # langFinnish → smRoman + 14: 6, # langGreek → smGreek + 15: 0, # langIcelandic → smRoman (modified) + 16: 0, # langMaltese → smRoman + 17: 0, # langTurkish → smRoman (modified) + 18: 0, # langCroatian → smRoman (modified) + 19: 2, # langTradChinese → smTradChinese + 20: 4, # langUrdu → smArabic + 21: 9, # langHindi → smDevanagari + 22: 21, # langThai → smThai + 23: 3, # langKorean → smKorean + 24: 29, # langLithuanian → smCentralEuroRoman + 25: 29, # langPolish → smCentralEuroRoman + 26: 29, # langHungarian → smCentralEuroRoman + 27: 29, # langEstonian → smCentralEuroRoman + 28: 29, # langLatvian → smCentralEuroRoman + 29: 0, # langSami → smRoman + 30: 0, # langFaroese → smRoman (modified) + 31: 4, # langFarsi → smArabic (modified) + 32: 7, # langRussian → smCyrillic + 33: 25, # langSimpChinese → smSimpChinese + 34: 0, # langFlemish → smRoman + 35: 0, # langIrishGaelic → smRoman (modified) + 36: 0, # langAlbanian → smRoman + 37: 0, # langRomanian → smRoman (modified) + 38: 29, # langCzech → smCentralEuroRoman + 39: 29, # langSlovak → smCentralEuroRoman + 40: 0, # langSlovenian → smRoman (modified) + 41: 5, # langYiddish → smHebrew + 42: 7, # langSerbian → smCyrillic + 43: 7, # langMacedonian → smCyrillic + 44: 7, # langBulgarian → smCyrillic + 45: 7, # langUkrainian → smCyrillic (modified) + 46: 7, # langByelorussian → smCyrillic + 47: 7, # langUzbek → smCyrillic + 48: 7, # langKazakh → smCyrillic + 49: 7, # langAzerbaijani → smCyrillic + 50: 4, # langAzerbaijanAr → smArabic + 51: 24, # langArmenian → smArmenian + 52: 23, # langGeorgian → smGeorgian + 53: 7, # langMoldavian → smCyrillic + 54: 7, # langKirghiz → smCyrillic + 55: 7, # langTajiki → smCyrillic + 56: 7, # langTurkmen → smCyrillic + 57: 27, # langMongolian → smMongolian + 58: 7, # langMongolianCyr → smCyrillic + 59: 4, # langPashto → smArabic + 60: 4, # langKurdish → smArabic + 61: 4, # langKashmiri → smArabic + 62: 4, # langSindhi → smArabic + 63: 26, # langTibetan → smTibetan + 64: 9, # langNepali → smDevanagari + 65: 9, # langSanskrit → smDevanagari + 66: 9, # langMarathi → smDevanagari + 67: 13, # langBengali → smBengali + 68: 13, # langAssamese → smBengali + 69: 11, # langGujarati → smGujarati + 70: 10, # langPunjabi → smGurmukhi + 71: 12, # langOriya → smOriya + 72: 17, # langMalayalam → smMalayalam + 73: 16, # langKannada → smKannada + 74: 14, # langTamil → smTamil + 75: 15, # langTelugu → smTelugu + 76: 18, # langSinhalese → smSinhalese + 77: 19, # langBurmese → smBurmese + 78: 20, # langKhmer → smKhmer + 79: 22, # langLao → smLao + 80: 30, # langVietnamese → smVietnamese + 81: 0, # langIndonesian → smRoman + 82: 0, # langTagalog → smRoman + 83: 0, # langMalayRoman → smRoman + 84: 4, # langMalayArabic → smArabic + 85: 28, # langAmharic → smEthiopic + 86: 28, # langTigrinya → smEthiopic + 87: 28, # langOromo → smEthiopic + 88: 0, # langSomali → smRoman + 89: 0, # langSwahili → smRoman + 90: 0, # langKinyarwanda → smRoman + 91: 0, # langRundi → smRoman + 92: 0, # langNyanja → smRoman + 93: 0, # langMalagasy → smRoman + 94: 0, # langEsperanto → smRoman + 128: 0, # langWelsh → smRoman (modified) + 129: 0, # langBasque → smRoman + 130: 0, # langCatalan → smRoman + 131: 0, # langLatin → smRoman + 132: 0, # langQuechua → smRoman + 133: 0, # langGuarani → smRoman + 134: 0, # langAymara → smRoman + 135: 7, # langTatar → smCyrillic + 136: 4, # langUighur → smArabic + 137: 26, # langDzongkha → smTibetan + 138: 0, # langJavaneseRom → smRoman + 139: 0, # langSundaneseRom → smRoman + 140: 0, # langGalician → smRoman + 141: 0, # langAfrikaans → smRoman + 142: 0, # langBreton → smRoman (modified) + 143: 28, # langInuktitut → smEthiopic (modified) + 144: 0, # langScottishGaelic → smRoman (modified) + 145: 0, # langManxGaelic → smRoman (modified) + 146: 0, # langIrishGaelicScript → smRoman (modified) + 147: 0, # langTongan → smRoman + 148: 6, # langGreekAncient → smRoman + 149: 0, # langGreenlandic → smRoman + 150: 0, # langAzerbaijanRoman → smRoman + 151: 0, # langNynorsk → smRoman +} diff --git a/Lib/fontTools/ttLib/tables/_o_p_b_d.py b/Lib/fontTools/ttLib/tables/_o_p_b_d.py new file mode 100644 index 0000000..60bb6c5 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/_o_p_b_d.py @@ -0,0 +1,8 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from .otBase import BaseTTXConverter + + +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6opbd.html +class table__o_p_b_d(BaseTTXConverter): + pass diff --git a/Lib/fontTools/ttLib/tables/_p_o_s_t.py b/Lib/fontTools/ttLib/tables/_p_o_s_t.py index 248983f..ede62da 100644 --- a/Lib/fontTools/ttLib/tables/_p_o_s_t.py +++ b/Lib/fontTools/ttLib/tables/_p_o_s_t.py @@ -13,7 +13,7 @@ import array postFormat = """ > formatType: 16.16F - italicAngle: 16.16F # italic angle in degrees + italicAngle: 16.16F # italic angle in degrees underlinePosition: h underlineThickness: h isFixedPitch: L @@ -27,7 +27,7 @@ postFormatSize = sstruct.calcsize(postFormat) class table__p_o_s_t(DefaultTable.DefaultTable): - + def decompile(self, data, ttFont): sstruct.unpack(postFormat, data[:postFormatSize], self) data = data[postFormatSize:] @@ -42,7 +42,7 @@ class table__p_o_s_t(DefaultTable.DefaultTable): else: # supported format raise ttLib.TTLibError("'post' table format %f not supported" % self.formatType) - + def compile(self, ttFont): data = sstruct.pack(postFormat, self) if self.formatType == 1.0: @@ -57,7 +57,7 @@ class table__p_o_s_t(DefaultTable.DefaultTable): # supported format raise ttLib.TTLibError("'post' table format %f not supported" % self.formatType) return data - + def getGlyphOrder(self): """This function will get called by a ttLib.TTFont instance. Do not call this function yourself, use TTFont().getGlyphOrder() @@ -68,10 +68,10 @@ class table__p_o_s_t(DefaultTable.DefaultTable): glyphOrder = self.glyphOrder del self.glyphOrder return glyphOrder - + def decode_format_1_0(self, data, ttFont): self.glyphOrder = standardGlyphOrder[:ttFont["maxp"].numGlyphs] - + def decode_format_2_0(self, data, ttFont): numGlyphs, = struct.unpack(">H", data[:2]) numGlyphs = int(numGlyphs) @@ -88,44 +88,47 @@ class table__p_o_s_t(DefaultTable.DefaultTable): indices.byteswap() data = data[2*numGlyphs:] self.extraNames = extraNames = unpackPStrings(data) - self.glyphOrder = glyphOrder = [None] * int(ttFont['maxp'].numGlyphs) + self.glyphOrder = glyphOrder = [""] * int(ttFont['maxp'].numGlyphs) for glyphID in range(numGlyphs): index = indices[glyphID] if index > 257: - name = extraNames[index-258] + try: + name = extraNames[index-258] + except IndexError: + name = "" else: # fetch names from standard list name = standardGlyphOrder[index] glyphOrder[glyphID] = name - #AL990511: code added to handle the case of new glyphs without - # entries into the 'post' table - if numGlyphs < ttFont['maxp'].numGlyphs: - for i in range(numGlyphs, ttFont['maxp'].numGlyphs): - glyphOrder[i] = "glyph#%.5d" % i - self.extraNames.append(glyphOrder[i]) self.build_psNameMapping(ttFont) - + def build_psNameMapping(self, ttFont): mapping = {} allNames = {} for i in range(ttFont['maxp'].numGlyphs): glyphName = psName = self.glyphOrder[i] + if glyphName == "": + glyphName = "glyph%.5d" % i if glyphName in allNames: # make up a new glyphName that's unique n = allNames[glyphName] + while (glyphName + "#" + str(n)) in allNames: + n += 1 allNames[glyphName] = n + 1 - glyphName = glyphName + "#" + repr(n) - self.glyphOrder[i] = glyphName + glyphName = glyphName + "#" + str(n) + + self.glyphOrder[i] = glyphName + allNames[glyphName] = 1 + if glyphName != psName: mapping[glyphName] = psName - else: - allNames[glyphName] = 1 + self.mapping = mapping - + def decode_format_3_0(self, data, ttFont): # Setting self.glyphOrder to None will cause the TTFont object # try and construct glyph names from a Unicode cmap table. self.glyphOrder = None - + def decode_format_4_0(self, data, ttFont): from fontTools import agl numGlyphs = ttFont['maxp'].numGlyphs @@ -151,7 +154,8 @@ class table__p_o_s_t(DefaultTable.DefaultTable): assert len(glyphOrder) == numGlyphs indices = array.array("H") extraDict = {} - extraNames = self.extraNames + extraNames = self.extraNames = [ + n for n in self.extraNames if n not in standardGlyphOrder] for i in range(len(extraNames)): extraDict[extraNames[i]] = i for glyphID in range(numGlyphs): @@ -172,7 +176,7 @@ class table__p_o_s_t(DefaultTable.DefaultTable): if sys.byteorder != "big": indices.byteswap() return struct.pack(">H", numGlyphs) + indices.tostring() + packPStrings(extraNames) - + def encode_format_4_0(self, ttFont): from fontTools import agl numGlyphs = ttFont['maxp'].numGlyphs @@ -229,7 +233,7 @@ class table__p_o_s_t(DefaultTable.DefaultTable): writer.dumphex(self.data) writer.endtag("hexdata") writer.newline() - + def fromXML(self, name, attrs, content, ttFont): if name not in ("psNames", "extraNames", "hexdata"): setattr(self, name, safeEval(attrs["value"])) @@ -269,4 +273,3 @@ def packPStrings(strings): for s in strings: data = data + bytechr(len(s)) + tobytes(s, encoding="latin1") return data - diff --git a/Lib/fontTools/ttLib/tables/_p_r_e_p.py b/Lib/fontTools/ttLib/tables/_p_r_e_p.py index fc92665..f4e8925 100644 --- a/Lib/fontTools/ttLib/tables/_p_r_e_p.py +++ b/Lib/fontTools/ttLib/tables/_p_r_e_p.py @@ -1,7 +1,8 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * from fontTools import ttLib superclass = ttLib.getTableClass("fpgm") class table__p_r_e_p(superclass): pass - diff --git a/Lib/fontTools/ttLib/tables/_p_r_o_p.py b/Lib/fontTools/ttLib/tables/_p_r_o_p.py new file mode 100644 index 0000000..7da18ea --- /dev/null +++ b/Lib/fontTools/ttLib/tables/_p_r_o_p.py @@ -0,0 +1,8 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from .otBase import BaseTTXConverter + + +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6prop.html +class table__p_r_o_p(BaseTTXConverter): + pass diff --git a/Lib/fontTools/ttLib/tables/_s_b_i_x.py b/Lib/fontTools/ttLib/tables/_s_b_i_x.py index 23cb6df..6efd1f2 100644 --- a/Lib/fontTools/ttLib/tables/_s_b_i_x.py +++ b/Lib/fontTools/ttLib/tables/_s_b_i_x.py @@ -1,136 +1,112 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.misc import sstruct -from fontTools.misc.textTools import readHex +from fontTools.misc.textTools import safeEval, num2binary, binary2num from . import DefaultTable -from .sbixBitmap import * -from .sbixBitmapSet import * -import struct +from .sbixGlyph import * +from .sbixStrike import * -""" -sbix Table organization: - -USHORT version? -USHORT version? -USHORT count number of bitmap sets -offsetEntry offsetEntry[count] offsetEntries -(Variable) storage for bitmap sets - - -offsetEntry: - -ULONG offset offset from table start to bitmap set - - -bitmap set: - -USHORT size height and width in pixels -USHORT resolution ? -offsetRecord offsetRecord[] -(Variable) storage for bitmaps - - -offsetRecord: - -ULONG bitmapOffset offset from start of bitmap set to individual bitmap - - -bitmap: - -ULONG reserved 00 00 00 00 -char[4] format data type, e.g. "png " -(Variable) bitmap data -""" sbixHeaderFormat = """ > - usVal1: H # 00 01 - usVal2: H # 00 01 - numSets: L # 00 00 00 02 # number of bitmap sets + version: H # Version number (set to 1) + flags: H # The only two bits used in the flags field are bits 0 + # and 1. For historical reasons, bit 0 must always be 1. + # Bit 1 is a sbixDrawOutlines flag and is interpreted as + # follows: + # 0: Draw only 'sbix' bitmaps + # 1: Draw both 'sbix' bitmaps and outlines, in that + # order + numStrikes: L # Number of bitmap strikes to follow """ sbixHeaderFormatSize = sstruct.calcsize(sbixHeaderFormat) -sbixBitmapSetOffsetFormat = """ +sbixStrikeOffsetFormat = """ > - offset: L # 00 00 00 10 # offset from table start to each bitmap set + strikeOffset: L # Offset from begining of table to data for the + # individual strike """ -sbixBitmapSetOffsetFormatSize = sstruct.calcsize(sbixBitmapSetOffsetFormat) +sbixStrikeOffsetFormatSize = sstruct.calcsize(sbixStrikeOffsetFormat) class table__s_b_i_x(DefaultTable.DefaultTable): - def __init__(self, tag): - self.tableTag = tag - self.usVal1 = 1 - self.usVal2 = 1 - self.numSets = 0 - self.bitmapSets = {} - self.bitmapSetOffsets = [] + + def __init__(self, tag=None): + DefaultTable.DefaultTable.__init__(self, tag) + self.version = 1 + self.flags = 1 + self.numStrikes = 0 + self.strikes = {} + self.strikeOffsets = [] def decompile(self, data, ttFont): # read table header sstruct.unpack(sbixHeaderFormat, data[ : sbixHeaderFormatSize], self) - # collect offsets to individual bitmap sets in self.bitmapSetOffsets - for i in range(self.numSets): - myOffset = sbixHeaderFormatSize + i * sbixBitmapSetOffsetFormatSize - offsetEntry = sbixBitmapSetOffset() - sstruct.unpack(sbixBitmapSetOffsetFormat, \ - data[myOffset : myOffset+sbixBitmapSetOffsetFormatSize], \ - offsetEntry) - self.bitmapSetOffsets.append(offsetEntry.offset) - - # decompile BitmapSets - for i in range(self.numSets-1, -1, -1): - myBitmapSet = BitmapSet(rawdata=data[self.bitmapSetOffsets[i]:]) - data = data[:self.bitmapSetOffsets[i]] - myBitmapSet.decompile(ttFont) - #print " BitmapSet length: %xh" % len(bitmapSetData) - #print "Number of Bitmaps:", myBitmapSet.numBitmaps - if myBitmapSet.size in self.bitmapSets: + # collect offsets to individual strikes in self.strikeOffsets + for i in range(self.numStrikes): + current_offset = sbixHeaderFormatSize + i * sbixStrikeOffsetFormatSize + offset_entry = sbixStrikeOffset() + sstruct.unpack(sbixStrikeOffsetFormat, \ + data[current_offset:current_offset+sbixStrikeOffsetFormatSize], \ + offset_entry) + self.strikeOffsets.append(offset_entry.strikeOffset) + + # decompile Strikes + for i in range(self.numStrikes-1, -1, -1): + current_strike = Strike(rawdata=data[self.strikeOffsets[i]:]) + data = data[:self.strikeOffsets[i]] + current_strike.decompile(ttFont) + #print " Strike length: %xh" % len(bitmapSetData) + #print "Number of Glyph entries:", len(current_strike.glyphs) + if current_strike.ppem in self.strikes: from fontTools import ttLib - raise ttLib.TTLibError("Pixel 'size' must be unique for each BitmapSet") - self.bitmapSets[myBitmapSet.size] = myBitmapSet + raise ttLib.TTLibError("Pixel 'ppem' must be unique for each Strike") + self.strikes[current_strike.ppem] = current_strike - # after the bitmaps have been extracted, we don't need the offsets anymore - del self.bitmapSetOffsets + # after the glyph data records have been extracted, we don't need the offsets anymore + del self.strikeOffsets + del self.numStrikes def compile(self, ttFont): - sbixData = "" - self.numSets = len(self.bitmapSets) + sbixData = b"" + self.numStrikes = len(self.strikes) sbixHeader = sstruct.pack(sbixHeaderFormat, self) - # calculate offset to start of first bitmap set - setOffset = sbixHeaderFormatSize + sbixBitmapSetOffsetFormatSize * self.numSets + # calculate offset to start of first strike + setOffset = sbixHeaderFormatSize + sbixStrikeOffsetFormatSize * self.numStrikes - for si in sorted(self.bitmapSets.keys()): - myBitmapSet = self.bitmapSets[si] - myBitmapSet.compile(ttFont) - # append offset to this bitmap set to table header - myBitmapSet.offset = setOffset - sbixHeader += sstruct.pack(sbixBitmapSetOffsetFormat, myBitmapSet) - setOffset += sbixBitmapSetHeaderFormatSize + len(myBitmapSet.data) - sbixData += myBitmapSet.data + for si in sorted(self.strikes.keys()): + current_strike = self.strikes[si] + current_strike.compile(ttFont) + # append offset to this strike to table header + current_strike.strikeOffset = setOffset + sbixHeader += sstruct.pack(sbixStrikeOffsetFormat, current_strike) + setOffset += len(current_strike.data) + sbixData += current_strike.data return sbixHeader + sbixData def toXML(self, xmlWriter, ttFont): - xmlWriter.simpletag("usVal1", value=self.usVal1) + xmlWriter.simpletag("version", value=self.version) xmlWriter.newline() - xmlWriter.simpletag("usVal2", value=self.usVal2) + xmlWriter.simpletag("flags", value=num2binary(self.flags, 16)) xmlWriter.newline() - for i in sorted(self.bitmapSets.keys()): - self.bitmapSets[i].toXML(xmlWriter, ttFont) + for i in sorted(self.strikes.keys()): + self.strikes[i].toXML(xmlWriter, ttFont) def fromXML(self, name, attrs, content, ttFont): - if name in ["usVal1", "usVal2"]: - setattr(self, name, int(attrs["value"])) - elif name == "bitmapSet": - myBitmapSet = BitmapSet() + if name =="version": + setattr(self, name, safeEval(attrs["value"])) + elif name == "flags": + setattr(self, name, binary2num(attrs["value"])) + elif name == "strike": + current_strike = Strike() for element in content: if isinstance(element, tuple): name, attrs, content = element - myBitmapSet.fromXML(name, attrs, content, ttFont) - self.bitmapSets[myBitmapSet.size] = myBitmapSet + current_strike.fromXML(name, attrs, content, ttFont) + self.strikes[current_strike.ppem] = current_strike else: from fontTools import ttLib raise ttLib.TTLibError("can't handle '%s' element" % name) @@ -138,5 +114,5 @@ class table__s_b_i_x(DefaultTable.DefaultTable): # Helper classes -class sbixBitmapSetOffset(object): +class sbixStrikeOffset(object): pass diff --git a/Lib/fontTools/ttLib/tables/_t_r_a_k.py b/Lib/fontTools/ttLib/tables/_t_r_a_k.py new file mode 100644 index 0000000..b5820b2 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/_t_r_a_k.py @@ -0,0 +1,314 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc import sstruct +from fontTools.misc.fixedTools import fixedToFloat as fi2fl, floatToFixed as fl2fi +from fontTools.misc.textTools import safeEval +from fontTools.ttLib import TTLibError +from . import DefaultTable +import struct +try: + from collections.abc import MutableMapping +except ImportError: + from UserDict import DictMixin as MutableMapping + + +# Apple's documentation of 'trak': +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html + +TRAK_HEADER_FORMAT = """ + > # big endian + version: 16.16F + format: H + horizOffset: H + vertOffset: H + reserved: H +""" + +TRAK_HEADER_FORMAT_SIZE = sstruct.calcsize(TRAK_HEADER_FORMAT) + + +TRACK_DATA_FORMAT = """ + > # big endian + nTracks: H + nSizes: H + sizeTableOffset: L +""" + +TRACK_DATA_FORMAT_SIZE = sstruct.calcsize(TRACK_DATA_FORMAT) + + +TRACK_TABLE_ENTRY_FORMAT = """ + > # big endian + track: 16.16F + nameIndex: H + offset: H +""" + +TRACK_TABLE_ENTRY_FORMAT_SIZE = sstruct.calcsize(TRACK_TABLE_ENTRY_FORMAT) + + +# size values are actually '16.16F' fixed-point values, but here I do the +# fixedToFloat conversion manually instead of relying on sstruct +SIZE_VALUE_FORMAT = ">l" +SIZE_VALUE_FORMAT_SIZE = struct.calcsize(SIZE_VALUE_FORMAT) + +# per-Size values are in 'FUnits', i.e. 16-bit signed integers +PER_SIZE_VALUE_FORMAT = ">h" +PER_SIZE_VALUE_FORMAT_SIZE = struct.calcsize(PER_SIZE_VALUE_FORMAT) + + +class table__t_r_a_k(DefaultTable.DefaultTable): + dependencies = ['name'] + + def compile(self, ttFont): + dataList = [] + offset = TRAK_HEADER_FORMAT_SIZE + for direction in ('horiz', 'vert'): + trackData = getattr(self, direction + 'Data', TrackData()) + offsetName = direction + 'Offset' + # set offset to 0 if None or empty + if not trackData: + setattr(self, offsetName, 0) + continue + # TrackData table format must be longword aligned + alignedOffset = (offset + 3) & ~3 + padding, offset = b"\x00"*(alignedOffset - offset), alignedOffset + setattr(self, offsetName, offset) + + data = trackData.compile(offset) + offset += len(data) + dataList.append(padding + data) + + self.reserved = 0 + tableData = bytesjoin([sstruct.pack(TRAK_HEADER_FORMAT, self)] + dataList) + return tableData + + def decompile(self, data, ttFont): + sstruct.unpack(TRAK_HEADER_FORMAT, data[:TRAK_HEADER_FORMAT_SIZE], self) + for direction in ('horiz', 'vert'): + trackData = TrackData() + offset = getattr(self, direction + 'Offset') + if offset != 0: + trackData.decompile(data, offset) + setattr(self, direction + 'Data', trackData) + + def toXML(self, writer, ttFont): + writer.simpletag('version', value=self.version) + writer.newline() + writer.simpletag('format', value=self.format) + writer.newline() + for direction in ('horiz', 'vert'): + dataName = direction + 'Data' + writer.begintag(dataName) + writer.newline() + trackData = getattr(self, dataName, TrackData()) + trackData.toXML(writer, ttFont) + writer.endtag(dataName) + writer.newline() + + def fromXML(self, name, attrs, content, ttFont): + if name == 'version': + self.version = safeEval(attrs['value']) + elif name == 'format': + self.format = safeEval(attrs['value']) + elif name in ('horizData', 'vertData'): + trackData = TrackData() + setattr(self, name, trackData) + for element in content: + if not isinstance(element, tuple): + continue + name, attrs, content_ = element + trackData.fromXML(name, attrs, content_, ttFont) + + +class TrackData(MutableMapping): + + def __init__(self, initialdata={}): + self._map = dict(initialdata) + + def compile(self, offset): + nTracks = len(self) + sizes = self.sizes() + nSizes = len(sizes) + + # offset to the start of the size subtable + offset += TRACK_DATA_FORMAT_SIZE + TRACK_TABLE_ENTRY_FORMAT_SIZE*nTracks + trackDataHeader = sstruct.pack( + TRACK_DATA_FORMAT, + {'nTracks': nTracks, 'nSizes': nSizes, 'sizeTableOffset': offset}) + + entryDataList = [] + perSizeDataList = [] + # offset to per-size tracking values + offset += SIZE_VALUE_FORMAT_SIZE*nSizes + # sort track table entries by track value + for track, entry in sorted(self.items()): + assert entry.nameIndex is not None + entry.track = track + entry.offset = offset + entryDataList += [sstruct.pack(TRACK_TABLE_ENTRY_FORMAT, entry)] + # sort per-size values by size + for size, value in sorted(entry.items()): + perSizeDataList += [struct.pack(PER_SIZE_VALUE_FORMAT, value)] + offset += PER_SIZE_VALUE_FORMAT_SIZE*nSizes + # sort size values + sizeDataList = [struct.pack(SIZE_VALUE_FORMAT, fl2fi(sv, 16)) for sv in sorted(sizes)] + + data = bytesjoin([trackDataHeader] + entryDataList + sizeDataList + perSizeDataList) + return data + + def decompile(self, data, offset): + # initial offset is from the start of trak table to the current TrackData + trackDataHeader = data[offset:offset+TRACK_DATA_FORMAT_SIZE] + if len(trackDataHeader) != TRACK_DATA_FORMAT_SIZE: + raise TTLibError('not enough data to decompile TrackData header') + sstruct.unpack(TRACK_DATA_FORMAT, trackDataHeader, self) + offset += TRACK_DATA_FORMAT_SIZE + + nSizes = self.nSizes + sizeTableOffset = self.sizeTableOffset + sizeTable = [] + for i in range(nSizes): + sizeValueData = data[sizeTableOffset:sizeTableOffset+SIZE_VALUE_FORMAT_SIZE] + if len(sizeValueData) < SIZE_VALUE_FORMAT_SIZE: + raise TTLibError('not enough data to decompile TrackData size subtable') + sizeValue, = struct.unpack(SIZE_VALUE_FORMAT, sizeValueData) + sizeTable.append(fi2fl(sizeValue, 16)) + sizeTableOffset += SIZE_VALUE_FORMAT_SIZE + + for i in range(self.nTracks): + entry = TrackTableEntry() + entryData = data[offset:offset+TRACK_TABLE_ENTRY_FORMAT_SIZE] + if len(entryData) < TRACK_TABLE_ENTRY_FORMAT_SIZE: + raise TTLibError('not enough data to decompile TrackTableEntry record') + sstruct.unpack(TRACK_TABLE_ENTRY_FORMAT, entryData, entry) + perSizeOffset = entry.offset + for j in range(nSizes): + size = sizeTable[j] + perSizeValueData = data[perSizeOffset:perSizeOffset+PER_SIZE_VALUE_FORMAT_SIZE] + if len(perSizeValueData) < PER_SIZE_VALUE_FORMAT_SIZE: + raise TTLibError('not enough data to decompile per-size track values') + perSizeValue, = struct.unpack(PER_SIZE_VALUE_FORMAT, perSizeValueData) + entry[size] = perSizeValue + perSizeOffset += PER_SIZE_VALUE_FORMAT_SIZE + self[entry.track] = entry + offset += TRACK_TABLE_ENTRY_FORMAT_SIZE + + def toXML(self, writer, ttFont): + nTracks = len(self) + nSizes = len(self.sizes()) + writer.comment("nTracks=%d, nSizes=%d" % (nTracks, nSizes)) + writer.newline() + for track, entry in sorted(self.items()): + assert entry.nameIndex is not None + entry.track = track + entry.toXML(writer, ttFont) + + def fromXML(self, name, attrs, content, ttFont): + if name != 'trackEntry': + return + entry = TrackTableEntry() + entry.fromXML(name, attrs, content, ttFont) + self[entry.track] = entry + + def sizes(self): + if not self: + return frozenset() + tracks = list(self.tracks()) + sizes = self[tracks.pop(0)].sizes() + for track in tracks: + entrySizes = self[track].sizes() + if sizes != entrySizes: + raise TTLibError( + "'trak' table entries must specify the same sizes: " + "%s != %s" % (sorted(sizes), sorted(entrySizes))) + return frozenset(sizes) + + def __getitem__(self, track): + return self._map[track] + + def __delitem__(self, track): + del self._map[track] + + def __setitem__(self, track, entry): + self._map[track] = entry + + def __len__(self): + return len(self._map) + + def __iter__(self): + return iter(self._map) + + def keys(self): + return self._map.keys() + + tracks = keys + + def __repr__(self): + return "TrackData({})".format(self._map if self else "") + + +class TrackTableEntry(MutableMapping): + + def __init__(self, values={}, nameIndex=None): + self.nameIndex = nameIndex + self._map = dict(values) + + def toXML(self, writer, ttFont): + name = ttFont["name"].getDebugName(self.nameIndex) + writer.begintag( + "trackEntry", + (('value', self.track), ('nameIndex', self.nameIndex))) + writer.newline() + if name: + writer.comment(name) + writer.newline() + for size, perSizeValue in sorted(self.items()): + writer.simpletag("track", size=size, value=perSizeValue) + writer.newline() + writer.endtag("trackEntry") + writer.newline() + + def fromXML(self, name, attrs, content, ttFont): + self.track = safeEval(attrs['value']) + self.nameIndex = safeEval(attrs['nameIndex']) + for element in content: + if not isinstance(element, tuple): + continue + name, attrs, _ = element + if name != 'track': + continue + size = safeEval(attrs['size']) + self[size] = safeEval(attrs['value']) + + def __getitem__(self, size): + return self._map[size] + + def __delitem__(self, size): + del self._map[size] + + def __setitem__(self, size, value): + self._map[size] = value + + def __len__(self): + return len(self._map) + + def __iter__(self): + return iter(self._map) + + def keys(self): + return self._map.keys() + + sizes = keys + + def __repr__(self): + return "TrackTableEntry({}, nameIndex={})".format(self._map, self.nameIndex) + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return NotImplemented + return self.nameIndex == other.nameIndex and dict(self) == dict(other) + + def __ne__(self, other): + result = self.__eq__(other) + return result if result is NotImplemented else not result diff --git a/Lib/fontTools/ttLib/tables/_v_h_e_a.py b/Lib/fontTools/ttLib/tables/_v_h_e_a.py index 8131ad3..312c5ec 100644 --- a/Lib/fontTools/ttLib/tables/_v_h_e_a.py +++ b/Lib/fontTools/ttLib/tables/_v_h_e_a.py @@ -2,77 +2,116 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.misc import sstruct from fontTools.misc.textTools import safeEval +from fontTools.misc.fixedTools import ( + ensureVersionIsLong as fi2ve, versionToFixed as ve2fi) from . import DefaultTable +import math + vheaFormat = """ > # big endian - tableVersion: 16.16F - ascent: h - descent: h - lineGap: h - advanceHeightMax: H - minTopSideBearing: h + tableVersion: L + ascent: h + descent: h + lineGap: h + advanceHeightMax: H + minTopSideBearing: h minBottomSideBearing: h - yMaxExtent: h - caretSlopeRise: h - caretSlopeRun: h - reserved0: h - reserved1: h - reserved2: h - reserved3: h - reserved4: h - metricDataFormat: h - numberOfVMetrics: H + yMaxExtent: h + caretSlopeRise: h + caretSlopeRun: h + caretOffset: h + reserved1: h + reserved2: h + reserved3: h + reserved4: h + metricDataFormat: h + numberOfVMetrics: H """ class table__v_h_e_a(DefaultTable.DefaultTable): - - dependencies = ['vmtx', 'glyf'] - + + # Note: Keep in sync with table__h_h_e_a + + dependencies = ['vmtx', 'glyf', 'CFF '] + def decompile(self, data, ttFont): sstruct.unpack(vheaFormat, data, self) - + def compile(self, ttFont): - self.recalc(ttFont) + if ttFont.recalcBBoxes and (ttFont.isLoaded('glyf') or ttFont.isLoaded('CFF ')): + self.recalc(ttFont) + self.tableVersion = fi2ve(self.tableVersion) return sstruct.pack(vheaFormat, self) - + def recalc(self, ttFont): - vtmxTable = ttFont['vmtx'] + if 'vmtx' in ttFont: + vmtxTable = ttFont['vmtx'] + self.advanceHeightMax = max(adv for adv, _ in vmtxTable.metrics.values()) + + boundsHeightDict = {} if 'glyf' in ttFont: - if not ttFont.isLoaded('glyf'): - return glyfTable = ttFont['glyf'] - advanceHeightMax = -100000 # arbitrary big negative number - minTopSideBearing = 100000 # arbitrary big number - minBottomSideBearing = 100000 # arbitrary big number - yMaxExtent = -100000 # arbitrary big negative number - for name in ttFont.getGlyphOrder(): - height, tsb = vtmxTable[name] g = glyfTable[name] - if g.numberOfContours <= 0: + if g.numberOfContours == 0: continue - advanceHeightMax = max(advanceHeightMax, height) + if g.numberOfContours < 0 and not hasattr(g, "yMax"): + # Composite glyph without extents set. + # Calculate those. + g.recalcBounds(glyfTable) + boundsHeightDict[name] = g.yMax - g.yMin + elif 'CFF ' in ttFont: + topDict = ttFont['CFF '].cff.topDictIndex[0] + charStrings = topDict.CharStrings + for name in ttFont.getGlyphOrder(): + cs = charStrings[name] + bounds = cs.calcBounds(charStrings) + if bounds is not None: + boundsHeightDict[name] = int( + math.ceil(bounds[3]) - math.floor(bounds[1])) + + if boundsHeightDict: + minTopSideBearing = float('inf') + minBottomSideBearing = float('inf') + yMaxExtent = -float('inf') + for name, boundsHeight in boundsHeightDict.items(): + advanceHeight, tsb = vmtxTable[name] + bsb = advanceHeight - tsb - boundsHeight + extent = tsb + boundsHeight minTopSideBearing = min(minTopSideBearing, tsb) - rsb = height - tsb - (g.yMax - g.yMin) - minBottomSideBearing = min(minBottomSideBearing, rsb) - extent = tsb + (g.yMax - g.yMin) + minBottomSideBearing = min(minBottomSideBearing, bsb) yMaxExtent = max(yMaxExtent, extent) - self.advanceHeightMax = advanceHeightMax self.minTopSideBearing = minTopSideBearing self.minBottomSideBearing = minBottomSideBearing self.yMaxExtent = yMaxExtent - else: - # XXX CFF recalc... - pass - + + else: # No glyph has outlines. + self.minTopSideBearing = 0 + self.minBottomSideBearing = 0 + self.yMaxExtent = 0 + def toXML(self, writer, ttFont): formatstring, names, fixes = sstruct.getformat(vheaFormat) for name in names: value = getattr(self, name) + if name == "tableVersion": + value = fi2ve(value) + value = "0x%08x" % value writer.simpletag(name, value=value) writer.newline() - + def fromXML(self, name, attrs, content, ttFont): + if name == "tableVersion": + setattr(self, name, ve2fi(attrs["value"])) + return setattr(self, name, safeEval(attrs["value"])) + # reserved0 is caretOffset for legacy reasons + @property + def reserved0(self): + return self.caretOffset + + @reserved0.setter + def reserved0(self, value): + self.caretOffset = value diff --git a/Lib/fontTools/ttLib/tables/_v_m_t_x.py b/Lib/fontTools/ttLib/tables/_v_m_t_x.py index c204de6..5573225 100644 --- a/Lib/fontTools/ttLib/tables/_v_m_t_x.py +++ b/Lib/fontTools/ttLib/tables/_v_m_t_x.py @@ -1,9 +1,11 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * from fontTools import ttLib superclass = ttLib.getTableClass("hmtx") class table__v_m_t_x(superclass): - + headerTag = 'vhea' advanceName = 'height' sideBearingName = 'tsb' diff --git a/Lib/fontTools/ttLib/tables/asciiTable.py b/Lib/fontTools/ttLib/tables/asciiTable.py index e5f3136..87266a0 100644 --- a/Lib/fontTools/ttLib/tables/asciiTable.py +++ b/Lib/fontTools/ttLib/tables/asciiTable.py @@ -4,7 +4,7 @@ from . import DefaultTable class asciiTable(DefaultTable.DefaultTable): - + def toXML(self, writer, ttFont): data = tostr(self.data) # removing null bytes. XXX needed?? @@ -12,12 +12,11 @@ class asciiTable(DefaultTable.DefaultTable): data = strjoin(data) writer.begintag("source") writer.newline() - writer.write_noindent(data.replace("\r", "\n")) + writer.write_noindent(data) writer.newline() writer.endtag("source") writer.newline() - - def fromXML(self, name, attrs, content, ttFont): - lines = strjoin(content).replace("\r", "\n").split("\n") - self.data = tobytes("\r".join(lines[1:-1])) + def fromXML(self, name, attrs, content, ttFont): + lines = strjoin(content).split("\n") + self.data = tobytes("\n".join(lines[1:-1])) diff --git a/Lib/fontTools/ttLib/tables/grUtils.py b/Lib/fontTools/ttLib/tables/grUtils.py new file mode 100644 index 0000000..1ce2c9e --- /dev/null +++ b/Lib/fontTools/ttLib/tables/grUtils.py @@ -0,0 +1,79 @@ +import struct, warnings +try: + import lz4 +except: + lz4 = None + +#old scheme for VERSION < 0.9 otherwise use lz4.block + +def decompress(data): + (compression,) = struct.unpack(">L", data[4:8]) + scheme = compression >> 27 + size = compression & 0x07ffffff + if scheme == 0: + pass + elif scheme == 1 and lz4: + res = lz4.decompress(struct.pack("<L", size) + data[8:]) + if len(res) != size: + warnings.warn("Table decompression failed.") + else: + data = res + else: + warnings.warn("Table is compressed with an unsupported compression scheme") + return (data, scheme) + +def compress(scheme, data): + hdr = data[:4] + struct.pack(">L", (scheme << 27) + (len(data) & 0x07ffffff)) + if scheme == 0 : + return data + elif scheme == 1 and lz4: + res = lz4.compress(hdr + data) + return res + else: + warnings.warn("Table failed to compress by unsupported compression scheme") + return data + +def _entries(attrs, sameval): + ak = 0 + vals = [] + lastv = 0 + for k,v in attrs: + if len(vals) and (k != ak + 1 or (sameval and v != lastv)) : + yield (ak - len(vals) + 1, len(vals), vals) + vals = [] + ak = k + vals.append(v) + lastv = v + yield (ak - len(vals) + 1, len(vals), vals) + +def entries(attributes, sameval = False): + g = _entries(sorted(attributes.iteritems(), key=lambda x:int(x[0])), sameval) + return g + +def bininfo(num, size=1): + if num == 0: + return struct.pack(">4H", 0, 0, 0, 0) + srange = 1; + select = 0 + while srange <= num: + srange *= 2 + select += 1 + select -= 1 + srange /= 2 + srange *= size + shift = num * size - srange + return struct.pack(">4H", num, srange, select, shift) + +def num2tag(n): + if n < 0x200000: + return str(n) + else: + return struct.unpack('4s', struct.pack('>L', n))[0].replace(b'\000', b'').decode() + +def tag2num(n): + try: + return int(n) + except ValueError: + n = (n+" ")[:4] + return struct.unpack('>L', n.encode('ascii'))[0] + diff --git a/Lib/fontTools/ttLib/tables/otBase.py b/Lib/fontTools/ttLib/tables/otBase.py index cbe574a..2064e24 100644 --- a/Lib/fontTools/ttLib/tables/otBase.py +++ b/Lib/fontTools/ttLib/tables/otBase.py @@ -1,7 +1,12 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from .DefaultTable import DefaultTable +import sys +import array import struct +import logging + +log = logging.getLogger(__name__) class OverflowErrorRecord(object): def __init__(self, overflowTuple): @@ -23,43 +28,27 @@ class OTLOffsetOverflowError(Exception): class BaseTTXConverter(DefaultTable): - + """Generic base class for TTX table converters. It functions as an adapter between the TTX (ttLib actually) table model and the model we use for OpenType tables, which is necessarily subtly different. """ - + def decompile(self, data, font): from . import otTables - cachingStats = None if True else {} - class GlobalState(object): - def __init__(self, tableType, cachingStats): - self.tableType = tableType - self.cachingStats = cachingStats - globalState = GlobalState(tableType=self.tableTag, - cachingStats=cachingStats) - reader = OTTableReader(data, globalState) + reader = OTTableReader(data, tableTag=self.tableTag) tableClass = getattr(otTables, self.tableTag) self.table = tableClass() self.table.decompile(reader, font) - if cachingStats: - stats = sorted([(v, k) for k, v in cachingStats.items()]) - stats.reverse() - print("cachingsstats for ", self.tableTag) - for v, k in stats: - if v < 2: - break - print(v, k) - print("---", len(stats)) - + def compile(self, font): - """ Create a top-level OTFWriter for the GPOS/GSUB table. + """ Create a top-level OTTableWriter for the GPOS/GSUB table. Call the compile method for the the table for each 'converter' record in the table converter list - call converter's write method for each item in the value. + call converter's write method for each item in the value. - For simple items, the write method adds a string to the - writer's self.items list. - - For Struct/Table/Subtable items, it add first adds new writer to the + writer's self.items list. + - For Struct/Table/Subtable items, it add first adds new writer to the to the writer's self.items, then calls the item's compile method. This creates a tree of writers, rooted at the GUSB/GPOS writer, with each writer representing a table, and the writer.items list containing @@ -71,17 +60,13 @@ class BaseTTXConverter(DefaultTable): Traverse the flat list of tables again, calling getData each get the data in the table, now that pos's and offset are known. - If a lookup subtable overflows an offset, we have to start all over. + If a lookup subtable overflows an offset, we have to start all over. """ - class GlobalState(object): - def __init__(self, tableType): - self.tableType = tableType - globalState = GlobalState(tableType=self.tableTag) overflowRecord = None while True: try: - writer = OTTableWriter(globalState) + writer = OTTableWriter(tableTag=self.tableTag) self.table.compile(writer, font) return writer.getAllData() @@ -91,7 +76,7 @@ class BaseTTXConverter(DefaultTable): raise # Oh well... overflowRecord = e.value - print("Attempting to fix OTLOffsetOverflowError", e) + log.info("Attempting to fix OTLOffsetOverflowError %s", e) lastItem = overflowRecord ok = 0 @@ -106,7 +91,7 @@ class BaseTTXConverter(DefaultTable): def toXML(self, writer, font): self.table.toXML2(writer, font) - + def fromXML(self, name, attrs, content, font): from . import otTables if not hasattr(self, "table"): @@ -119,21 +104,29 @@ class OTTableReader(object): """Helper class to retrieve data from an OpenType table.""" - __slots__ = ('data', 'offset', 'pos', 'globalState', 'localState') + __slots__ = ('data', 'offset', 'pos', 'localState', 'tableTag') - def __init__(self, data, globalState={}, localState=None, offset=0): + def __init__(self, data, localState=None, offset=0, tableTag=None): self.data = data self.offset = offset self.pos = offset - self.globalState = globalState self.localState = localState + self.tableTag = tableTag + + def advance(self, count): + self.pos += count + + def seek(self, pos): + self.pos = pos + + def copy(self): + other = self.__class__(self.data, self.localState, self.offset, self.tableTag) + other.pos = self.pos + return other def getSubReader(self, offset): offset = self.offset + offset - cachingStats = self.globalState.cachingStats - if cachingStats is not None: - cachingStats[offset] = cachingStats.get(offset, 0) + 1 - return self.__class__(self.data, self.globalState, self.localState, offset) + return self.__class__(self.data, self.localState, offset, self.tableTag) def readUShort(self): pos = self.pos @@ -142,6 +135,22 @@ class OTTableReader(object): self.pos = newpos return value + def readUShortArray(self, count): + pos = self.pos + newpos = pos + count * 2 + value = array.array("H", self.data[pos:newpos]) + if sys.byteorder != "big": + value.byteswap() + self.pos = newpos + return value + + def readInt8(self): + pos = self.pos + newpos = pos + 1 + value, = struct.unpack(">b", self.data[pos:newpos]) + self.pos = newpos + return value + def readShort(self): pos = self.pos newpos = pos + 2 @@ -156,6 +165,13 @@ class OTTableReader(object): self.pos = newpos return value + def readUInt8(self): + pos = self.pos + newpos = pos + 1 + value, = struct.unpack(">B", self.data[pos:newpos]) + self.pos = newpos + return value + def readUInt24(self): pos = self.pos newpos = pos + 3 @@ -169,12 +185,19 @@ class OTTableReader(object): value, = struct.unpack(">L", self.data[pos:newpos]) self.pos = newpos return value - + def readTag(self): pos = self.pos newpos = pos + 4 value = Tag(self.data[pos:newpos]) - assert len(value) == 4 + assert len(value) == 4, value + self.pos = newpos + return value + + def readData(self, count): + pos = self.pos + newpos = pos + count + value = self.data[pos:newpos] self.pos = newpos return value @@ -184,18 +207,22 @@ class OTTableReader(object): self.localState = state def __getitem__(self, name): - return self.localState[name] + return self.localState and self.localState[name] + + def __contains__(self, name): + return self.localState and name in self.localState class OTTableWriter(object): - + """Helper class to gather and assemble data for OpenType tables.""" - - def __init__(self, globalState, localState=None): + + def __init__(self, localState=None, tableTag=None): self.items = [] self.pos = None - self.globalState = globalState self.localState = localState + self.tableTag = tableTag + self.longOffset = False self.parent = None def __setitem__(self, name, value): @@ -206,50 +233,23 @@ class OTTableWriter(object): def __getitem__(self, name): return self.localState[name] - # assembler interface - - def getAllData(self): - """Assemble all data, including all subtables.""" - self._doneWriting() - tables, extTables = self._gatherTables() - tables.reverse() - extTables.reverse() - # Gather all data in two passes: the absolute positions of all - # subtable are needed before the actual data can be assembled. - pos = 0 - for table in tables: - table.pos = pos - pos = pos + table.getDataLength() - - for table in extTables: - table.pos = pos - pos = pos + table.getDataLength() - + def __delitem__(self, name): + del self.localState[name] - data = [] - for table in tables: - tableData = table.getData() - data.append(tableData) - - for table in extTables: - tableData = table.getData() - data.append(tableData) + # assembler interface - return bytesjoin(data) - def getDataLength(self): """Return the length of this table in bytes, without subtables.""" l = 0 for item in self.items: - if hasattr(item, "getData") or hasattr(item, "getCountData"): - if item.longOffset: - l = l + 4 # sizeof(ULong) - else: - l = l + 2 # sizeof(UShort) + if hasattr(item, "getCountData"): + l += item.size + elif hasattr(item, "getData"): + l += 4 if item.longOffset else 2 else: l = l + len(item) return l - + def getData(self): """Assemble the data for this writer/table, without subtables.""" items = list(self.items) # make a shallow copy @@ -257,7 +257,7 @@ class OTTableWriter(object): numItems = len(items) for i in range(numItems): item = items[i] - + if hasattr(item, "getData"): if item.longOffset: items[i] = packULong(item.pos - pos) @@ -266,55 +266,26 @@ class OTTableWriter(object): items[i] = packUShort(item.pos - pos) except struct.error: # provide data to fix overflow problem. - # If the overflow is to a lookup, or from a lookup to a subtable, - # just report the current item. Otherwise... - if self.name not in [ 'LookupList', 'Lookup']: - # overflow is within a subTable. Life is more complicated. - # If we split the sub-table just before the current item, we may still suffer overflow. - # This is because duplicate table merging is done only within an Extension subTable tree; - # when we split the subtable in two, some items may no longer be duplicates. - # Get worst case by adding up all the item lengths, depth first traversal. - # and then report the first item that overflows a short. - def getDeepItemLength(table): - if hasattr(table, "getDataLength"): - length = 0 - for item in table.items: - length = length + getDeepItemLength(item) - else: - length = len(table) - return length - - length = self.getDataLength() - if hasattr(self, "sortCoverageLast") and item.name == "Coverage": - # Coverage is first in the item list, but last in the table list, - # The original overflow is really in the item list. Skip the Coverage - # table in the following test. - items = items[i+1:] - - for j in range(len(items)): - item = items[j] - length = length + getDeepItemLength(item) - if length > 65535: - break overflowErrorRecord = self.getOverflowErrorRecord(item) - - + raise OTLOffsetOverflowError(overflowErrorRecord) return bytesjoin(items) - + def __hash__(self): # only works after self._doneWriting() has been called return hash(self.items) - + def __ne__(self, other): - return not self.__eq__(other) + result = self.__eq__(other) + return result if result is NotImplemented else not result + def __eq__(self, other): if type(self) != type(other): return NotImplemented - return self.items == other.items - - def _doneWriting(self, internedTables=None): + return self.longOffset == other.longOffset and self.items == other.items + + def _doneWriting(self, internedTables): # Convert CountData references to data string items # collapse duplicate table references to a unique entry # "tables" are OTTableWriter objects. @@ -322,54 +293,51 @@ class OTTableWriter(object): # For Extension Lookup types, we can # eliminate duplicates only within the tree under the Extension Lookup, # as offsets may exceed 64K even between Extension LookupTable subtables. - if internedTables is None: + isExtension = hasattr(self, "Extension") + + # Certain versions of Uniscribe reject the font if the GSUB/GPOS top-level + # arrays (ScriptList, FeatureList, LookupList) point to the same, possibly + # empty, array. So, we don't share those. + # See: https://github.com/behdad/fonttools/issues/518 + dontShare = hasattr(self, 'DontShare') + + if isExtension: internedTables = {} + items = self.items - iRange = list(range(len(items))) - - if hasattr(self, "Extension"): - newTree = 1 - else: - newTree = 0 - for i in iRange: + for i in range(len(items)): item = items[i] if hasattr(item, "getCountData"): items[i] = item.getCountData() elif hasattr(item, "getData"): - if newTree: - item._doneWriting() - else: - item._doneWriting(internedTables) - internedItem = internedTables.get(item) - if internedItem: - items[i] = item = internedItem - else: - internedTables[item] = item + item._doneWriting(internedTables) + if not dontShare: + items[i] = item = internedTables.setdefault(item, item) self.items = tuple(items) - - def _gatherTables(self, tables=None, extTables=None, done=None): + + def _gatherTables(self, tables, extTables, done): # Convert table references in self.items tree to a flat # list of tables in depth-first traversal order. # "tables" are OTTableWriter objects. - # We do the traversal in reverse order at each level, in order to + # We do the traversal in reverse order at each level, in order to # resolve duplicate references to be the last reference in the list of tables. # For extension lookups, duplicate references can be merged only within the # writer tree under the extension lookup. - if tables is None: # init call for first time. - tables = [] - extTables = [] - done = {} - done[self] = 1 + done[id(self)] = True numItems = len(self.items) iRange = list(range(numItems)) iRange.reverse() - if hasattr(self, "Extension"): - appendExtensions = 1 - else: - appendExtensions = 0 + isExtension = hasattr(self, "Extension") + dontShare = hasattr(self, 'DontShare') + + selfTables = tables + + if isExtension: + assert extTables is not None, "Program or XML editing error. Extension subtables cannot contain extensions subtables" + tables, extTables, done = extTables, None, {} # add Coverage table if it is sorted last. sortCoverageLast = 0 @@ -380,7 +348,7 @@ class OTTableWriter(object): if hasattr(item, "name") and (item.name == "Coverage"): sortCoverageLast = 1 break - if item not in done: + if id(item) not in done: item._gatherTables(tables, extTables, done) else: # We're a new parent of item @@ -395,70 +363,104 @@ class OTTableWriter(object): # we've already 'gathered' it above continue - if appendExtensions: - assert extTables is not None, "Program or XML editing error. Extension subtables cannot contain extensions subtables" - newDone = {} - item._gatherTables(extTables, None, newDone) - - elif item not in done: + if id(item) not in done: item._gatherTables(tables, extTables, done) else: - # We're a new parent of item + # Item is already written out by other parent pass + selfTables.append(self) + + def getAllData(self): + """Assemble all data, including all subtables.""" + internedTables = {} + self._doneWriting(internedTables) + tables = [] + extTables = [] + done = {} + self._gatherTables(tables, extTables, done) + tables.reverse() + extTables.reverse() + # Gather all data in two passes: the absolute positions of all + # subtable are needed before the actual data can be assembled. + pos = 0 + for table in tables: + table.pos = pos + pos = pos + table.getDataLength() + + for table in extTables: + table.pos = pos + pos = pos + table.getDataLength() + + data = [] + for table in tables: + tableData = table.getData() + data.append(tableData) + + for table in extTables: + tableData = table.getData() + data.append(tableData) + + return bytesjoin(data) - tables.append(self) - return tables, extTables - # interface for gathering data, as used by table.compile() - + def getSubWriter(self): - subwriter = self.__class__(self.globalState, self.localState) + subwriter = self.__class__(self.localState, self.tableTag) subwriter.parent = self # because some subtables have idential values, we discard # the duplicates under the getAllData method. Hence some # subtable writers can have more than one parent writer. # But we just care about first one right now. return subwriter - + def writeUShort(self, value): - assert 0 <= value < 0x10000 + assert 0 <= value < 0x10000, value self.items.append(struct.pack(">H", value)) - + def writeShort(self, value): + assert -32768 <= value < 32768, value self.items.append(struct.pack(">h", value)) + def writeUInt8(self, value): + assert 0 <= value < 256, value + self.items.append(struct.pack(">B", value)) + + def writeInt8(self, value): + assert -128 <= value < 128, value + self.items.append(struct.pack(">b", value)) + def writeUInt24(self, value): - assert 0 <= value < 0x1000000 + assert 0 <= value < 0x1000000, value b = struct.pack(">L", value) self.items.append(b[1:]) - + def writeLong(self, value): self.items.append(struct.pack(">l", value)) - + def writeULong(self, value): self.items.append(struct.pack(">L", value)) - + def writeTag(self, tag): tag = Tag(tag).tobytes() - assert len(tag) == 4 + assert len(tag) == 4, tag self.items.append(tag) - + def writeSubTable(self, subWriter): self.items.append(subWriter) - - def writeCountReference(self, table, name): - ref = CountReference(table, name) + + def writeCountReference(self, table, name, size=2, value=None): + ref = CountReference(table, name, size=size, value=value) self.items.append(ref) return ref - + def writeStruct(self, format, values): data = struct.pack(*(format,) + values) self.items.append(data) - + def writeData(self, data): self.items.append(data) - def getOverflowErrorRecord(self, item): + def getOverflowErrorRecord(self, item): LookupListIndex = SubTableIndex = itemName = itemIndex = None if self.name == 'LookupList': LookupListIndex = item.repeatIndex @@ -466,7 +468,7 @@ class OTTableWriter(object): LookupListIndex = self.repeatIndex SubTableIndex = item.repeatIndex else: - itemName = item.name + itemName = getattr(item, 'name', '<none>') if hasattr(item, 'repeatIndex'): itemIndex = item.repeatIndex if self.name == 'SubTable': @@ -476,10 +478,10 @@ class OTTableWriter(object): LookupListIndex = self.parent.parent.repeatIndex SubTableIndex = self.parent.repeatIndex else: # who knows how far below the SubTable level we are! Climb back up to the nearest subtable. - itemName = ".".join([self.name, item.name]) + itemName = ".".join([self.name, itemName]) p1 = self.parent while p1 and p1.name not in ['ExtSubTable', 'SubTable']: - itemName = ".".join([p1.name, item.name]) + itemName = ".".join([p1.name, itemName]) p1 = p1.parent if p1: if p1.name == 'ExtSubTable': @@ -489,14 +491,17 @@ class OTTableWriter(object): LookupListIndex = p1.parent.repeatIndex SubTableIndex = p1.repeatIndex - return OverflowErrorRecord( (self.globalState.tableType, LookupListIndex, SubTableIndex, itemName, itemIndex) ) + return OverflowErrorRecord( (self.tableTag, LookupListIndex, SubTableIndex, itemName, itemIndex) ) class CountReference(object): """A reference to a Count value, not a count of references.""" - def __init__(self, table, name): + def __init__(self, table, name, size=None, value=None): self.table = table self.name = name + self.size = size + if value is not None: + self.setValue(value) def setValue(self, value): table = self.table name = self.name @@ -505,13 +510,17 @@ class CountReference(object): else: assert table[name] == value, (name, table[name], value) def getCountData(self): - return packUShort(self.table[self.name]) + v = self.table[self.name] + if v is None: v = 0 + return {1:packUInt8, 2:packUShort, 4:packULong}[self.size](v) + +def packUInt8 (value): + return struct.pack(">B", value) def packUShort(value): return struct.pack(">H", value) - def packULong(value): assert 0 <= value < 0x100000000, value return struct.pack(">L", value) @@ -519,6 +528,8 @@ def packULong(value): class BaseTable(object): + """Generic base class for all OpenType (sub)tables.""" + def __getattr__(self, attr): reader = self.__dict__.get("reader") if reader: @@ -530,81 +541,143 @@ class BaseTable(object): raise AttributeError(attr) - """Generic base class for all OpenType (sub)tables.""" - + def ensureDecompiled(self): + reader = self.__dict__.get("reader") + if reader: + del self.reader + font = self.font + del self.font + self.decompile(reader, font) + + @classmethod + def getRecordSize(cls, reader): + totalSize = 0 + for conv in cls.converters: + size = conv.getRecordSize(reader) + if size is NotImplemented: return NotImplemented + countValue = 1 + if conv.repeat: + if conv.repeat in reader: + countValue = reader[conv.repeat] + else: + return NotImplemented + totalSize += size * countValue + return totalSize + def getConverters(self): return self.converters - + def getConverterByName(self, name): return self.convertersByName[name] - + + def populateDefaults(self, propagator=None): + for conv in self.getConverters(): + if conv.repeat: + if not hasattr(self, conv.name): + setattr(self, conv.name, []) + countValue = len(getattr(self, conv.name)) - conv.aux + try: + count_conv = self.getConverterByName(conv.repeat) + setattr(self, conv.repeat, countValue) + except KeyError: + # conv.repeat is a propagated count + if propagator and conv.repeat in propagator: + propagator[conv.repeat].setValue(countValue) + else: + if conv.aux and not eval(conv.aux, None, self.__dict__): + continue + if hasattr(self, conv.name): + continue # Warn if it should NOT be present?! + if hasattr(conv, 'writeNullOffset'): + setattr(self, conv.name, None) # Warn? + #elif not conv.isCount: + # # Warn? + # pass + def decompile(self, reader, font): self.readFormat(reader) table = {} self.__rawTable = table # for debugging - converters = self.getConverters() - for conv in converters: + for conv in self.getConverters(): if conv.name == "SubTable": - conv = conv.getConverter(reader.globalState.tableType, + conv = conv.getConverter(reader.tableTag, table["LookupType"]) if conv.name == "ExtSubTable": - conv = conv.getConverter(reader.globalState.tableType, + conv = conv.getConverter(reader.tableTag, table["ExtensionLookupType"]) if conv.name == "FeatureParams": conv = conv.getConverter(reader["FeatureTag"]) - if conv.repeat: - l = [] - if conv.repeat in table: - countValue = table[conv.repeat] + if conv.name == "SubStruct": + conv = conv.getConverter(reader.tableTag, + table["MorphType"]) + try: + if conv.repeat: + if isinstance(conv.repeat, int): + countValue = conv.repeat + elif conv.repeat in table: + countValue = table[conv.repeat] + else: + # conv.repeat is a propagated count + countValue = reader[conv.repeat] + countValue += conv.aux + table[conv.name] = conv.readArray(reader, font, table, countValue) else: - # conv.repeat is a propagated count - countValue = reader[conv.repeat] - for i in range(countValue + conv.aux): - l.append(conv.read(reader, font, table)) - table[conv.name] = l - else: - if conv.aux and not eval(conv.aux, None, table): - continue - table[conv.name] = conv.read(reader, font, table) - if conv.isPropagated: - reader[conv.name] = table[conv.name] - - self.postRead(table, font) + if conv.aux and not eval(conv.aux, None, table): + continue + table[conv.name] = conv.read(reader, font, table) + if conv.isPropagated: + reader[conv.name] = table[conv.name] + except Exception as e: + name = conv.name + e.args = e.args + (name,) + raise + + if hasattr(self, 'postRead'): + self.postRead(table, font) + else: + self.__dict__.update(table) del self.__rawTable # succeeded, get rid of debugging info - def ensureDecompiled(self): - reader = self.__dict__.get("reader") - if reader: - del self.reader - font = self.font - del self.font - self.decompile(reader, font) - def compile(self, writer, font): self.ensureDecompiled() - table = self.preWrite(font) + if hasattr(self, 'preWrite'): + table = self.preWrite(font) + else: + table = self.__dict__.copy() + if hasattr(self, 'sortCoverageLast'): writer.sortCoverageLast = 1 + if hasattr(self, 'DontShare'): + writer.DontShare = True + if hasattr(self.__class__, 'LookupType'): writer['LookupType'].setValue(self.__class__.LookupType) self.writeFormat(writer) for conv in self.getConverters(): - value = table.get(conv.name) + value = table.get(conv.name) # TODO Handle defaults instead of defaulting to None! if conv.repeat: if value is None: value = [] countValue = len(value) - conv.aux - if conv.repeat in table: - CountReference(table, conv.repeat).setValue(countValue) + if isinstance(conv.repeat, int): + assert len(value) == conv.repeat, 'expected %d values, got %d' % (conv.repeat, len(value)) + elif conv.repeat in table: + CountReference(table, conv.repeat, value=countValue) else: # conv.repeat is a propagated count writer[conv.repeat].setValue(countValue) - for i in range(len(value)): - conv.write(writer, font, table, value[i], i) + values = value + for i, value in enumerate(values): + try: + conv.write(writer, font, table, value, i) + except Exception as e: + name = value.__class__.__name__ if value is not None else conv.name + e.args = e.args + (name+'['+str(i)+']',) + raise elif conv.isCount: # Special-case Count values. # Assumption: a Count field will *always* precede @@ -613,33 +686,36 @@ class BaseTable(object): # table. We will later store it here. # We add a reference: by the time the data is assembled # the Count value will be filled in. - ref = writer.writeCountReference(table, conv.name) + ref = writer.writeCountReference(table, conv.name, conv.staticSize) table[conv.name] = None if conv.isPropagated: writer[conv.name] = ref elif conv.isLookupType: - ref = writer.writeCountReference(table, conv.name) - table[conv.name] = None + # We make sure that subtables have the same lookup type, + # and that the type is the same as the one set on the + # Lookup object, if any is set. + if conv.name not in table: + table[conv.name] = None + ref = writer.writeCountReference(table, conv.name, conv.staticSize, table[conv.name]) writer['LookupType'] = ref else: if conv.aux and not eval(conv.aux, None, table): continue - conv.write(writer, font, table, value) + try: + conv.write(writer, font, table, value) + except Exception as e: + name = value.__class__.__name__ if value is not None else conv.name + e.args = e.args + (name,) + raise if conv.isPropagated: writer[conv.name] = value - + def readFormat(self, reader): pass - + def writeFormat(self, writer): pass - - def postRead(self, table, font): - self.__dict__.update(table) - - def preWrite(self, font): - return self.__dict__.copy() - + def toXML(self, xmlWriter, font, attrs=None, name=None): tableName = name if name else self.__class__.__name__ if attrs is None: @@ -651,14 +727,14 @@ class BaseTable(object): self.toXML2(xmlWriter, font) xmlWriter.endtag(tableName) xmlWriter.newline() - + def toXML2(self, xmlWriter, font): # Simpler variant of toXML, *only* for the top level tables (like GPOS, GSUB). # This is because in TTX our parent writes our main tag, and in otBase.py we # do it ourselves. I think I'm getting schizophrenic... for conv in self.getConverters(): if conv.repeat: - value = getattr(self, conv.name) + value = getattr(self, conv.name, []) for i in range(len(value)): item = value[i] conv.xmlWrite(xmlWriter, font, item, conv.name, @@ -666,9 +742,9 @@ class BaseTable(object): else: if conv.aux and not eval(conv.aux, None, vars(self)): continue - value = getattr(self, conv.name) + value = getattr(self, conv.name, None) # TODO Handle defaults instead of defaulting to None! conv.xmlWrite(xmlWriter, font, value, conv.name, []) - + def fromXML(self, name, attrs, content, font): try: conv = self.getConverterByName(name) @@ -683,9 +759,11 @@ class BaseTable(object): seq.append(value) else: setattr(self, conv.name, value) - + def __ne__(self, other): - return not self.__eq__(other) + result = self.__eq__(other) + return result if result is NotImplemented else not result + def __eq__(self, other): if type(self) != type(other): return NotImplemented @@ -697,25 +775,28 @@ class BaseTable(object): class FormatSwitchingBaseTable(BaseTable): - + """Minor specialization of BaseTable, for tables that have multiple formats, eg. CoverageFormat1 vs. CoverageFormat2.""" - + + @classmethod + def getRecordSize(cls, reader): + return NotImplemented + def getConverters(self): - return self.converters[self.Format] - + return self.converters.get(self.Format, []) + def getConverterByName(self, name): return self.convertersByName[self.Format][name] - + def readFormat(self, reader): self.Format = reader.readUShort() - assert self.Format != 0, (self, reader.pos, len(reader.data)) - + def writeFormat(self, writer): writer.writeUShort(self.Format) def toXML(self, xmlWriter, font, attrs=None, name=None): - BaseTable.toXML(self, xmlWriter, font, attrs, name=self.__class__.__name__) + BaseTable.toXML(self, xmlWriter, font, attrs, name) # @@ -727,24 +808,24 @@ class FormatSwitchingBaseTable(BaseTable): # valueRecordFormat = [ -# Mask Name isDevice signed - (0x0001, "XPlacement", 0, 1), - (0x0002, "YPlacement", 0, 1), - (0x0004, "XAdvance", 0, 1), - (0x0008, "YAdvance", 0, 1), - (0x0010, "XPlaDevice", 1, 0), - (0x0020, "YPlaDevice", 1, 0), - (0x0040, "XAdvDevice", 1, 0), - (0x0080, "YAdvDevice", 1, 0), -# reserved: - (0x0100, "Reserved1", 0, 0), - (0x0200, "Reserved2", 0, 0), - (0x0400, "Reserved3", 0, 0), - (0x0800, "Reserved4", 0, 0), - (0x1000, "Reserved5", 0, 0), - (0x2000, "Reserved6", 0, 0), - (0x4000, "Reserved7", 0, 0), - (0x8000, "Reserved8", 0, 0), +# Mask Name isDevice signed + (0x0001, "XPlacement", 0, 1), + (0x0002, "YPlacement", 0, 1), + (0x0004, "XAdvance", 0, 1), + (0x0008, "YAdvance", 0, 1), + (0x0010, "XPlaDevice", 1, 0), + (0x0020, "YPlaDevice", 1, 0), + (0x0040, "XAdvDevice", 1, 0), + (0x0080, "YAdvDevice", 1, 0), +# reserved: + (0x0100, "Reserved1", 0, 0), + (0x0200, "Reserved2", 0, 0), + (0x0400, "Reserved3", 0, 0), + (0x0800, "Reserved4", 0, 0), + (0x1000, "Reserved5", 0, 0), + (0x2000, "Reserved6", 0, 0), + (0x4000, "Reserved7", 0, 0), + (0x8000, "Reserved8", 0, 0), ] def _buildDict(): @@ -757,7 +838,7 @@ valueRecordFormatDict = _buildDict() class ValueRecordFactory(object): - + """Given a format code, this object convert ValueRecords.""" def __init__(self, valueFormat): @@ -766,7 +847,10 @@ class ValueRecordFactory(object): if valueFormat & mask: format.append((name, isDevice, signed)) self.format = format - + + def __len__(self): + return len(self.format) + def readValueRecord(self, reader, font): format = self.format if not format: @@ -787,7 +871,7 @@ class ValueRecordFactory(object): value = None setattr(valueRecord, name, value) return valueRecord - + def writeValueRecord(self, writer, font, valueRecord): for name, isDevice, signed in self.format: value = getattr(valueRecord, name, 0) @@ -805,15 +889,28 @@ class ValueRecordFactory(object): class ValueRecord(object): - + # see ValueRecordFactory - + + def __init__(self, valueFormat=None, src=None): + if valueFormat is not None: + for mask, name, isDevice, signed in valueRecordFormat: + if valueFormat & mask: + setattr(self, name, None if isDevice else 0) + if src is not None: + for key,val in src.__dict__.items(): + if not hasattr(self, key): + continue + setattr(self, key, val) + elif src is not None: + self.__dict__ = src.__dict__.copy() + def getFormat(self): format = 0 for name in self.__dict__.keys(): format = format | valueRecordFormatDict[name][0] return format - + def toXML(self, xmlWriter, font, valueName, attrs=None): if attrs is None: simpleItems = [] @@ -833,13 +930,13 @@ class ValueRecord(object): xmlWriter.newline() for name, deviceRecord in deviceItems: if deviceRecord is not None: - deviceRecord.toXML(xmlWriter, font) + deviceRecord.toXML(xmlWriter, font, name=name) xmlWriter.endtag(valueName) xmlWriter.newline() else: xmlWriter.simpletag(valueName, simpleItems) xmlWriter.newline() - + def fromXML(self, name, attrs, content, font): from . import otTables for k, v in attrs.items(): @@ -855,9 +952,11 @@ class ValueRecord(object): name2, attrs2, content2 = elem2 value.fromXML(name2, attrs2, content2, font) setattr(self, name, value) - + def __ne__(self, other): - return not self.__eq__(other) + result = self.__eq__(other) + return result if result is NotImplemented else not result + def __eq__(self, other): if type(self) != type(other): return NotImplemented diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index d6ac461..ac65945 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -1,8 +1,22 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * -from fontTools.misc.textTools import safeEval -from fontTools.misc.fixedTools import fixedToFloat as fi2fl, floatToFixed as fl2fi -from .otBase import ValueRecordFactory +from fontTools.misc.fixedTools import ( + fixedToFloat as fi2fl, floatToFixed as fl2fi, ensureVersionIsLong as fi2ve, + versionToFixed as ve2fi) +from fontTools.misc.textTools import pad, safeEval +from fontTools.ttLib import getSearchRange +from .otBase import (CountReference, FormatSwitchingBaseTable, + OTTableReader, OTTableWriter, ValueRecordFactory) +from .otTables import (lookupTypes, AATStateTable, AATState, AATAction, + ContextualMorphAction, LigatureMorphAction, + MorxSubtable) +from functools import partial +import struct +import logging + + +log = logging.getLogger(__name__) +istuple = lambda t: isinstance(t, tuple) def buildConverters(tableSpec, tableNamespace): @@ -16,24 +30,37 @@ def buildConverters(tableSpec, tableNamespace): if name.startswith("ValueFormat"): assert tp == "uint16" converterClass = ValueFormat - elif name.endswith("Count") or name.endswith("LookupType"): - assert tp == "uint16" - converterClass = ComputedUShort + elif name.endswith("Count") or name in ("StructLength", "MorphType"): + converterClass = { + "uint8": ComputedUInt8, + "uint16": ComputedUShort, + "uint32": ComputedULong, + }[tp] elif name == "SubTable": converterClass = SubTable elif name == "ExtSubTable": converterClass = ExtSubTable + elif name == "SubStruct": + converterClass = SubStruct elif name == "FeatureParams": converterClass = FeatureParams + elif name in ("CIDGlyphMapping", "GlyphCIDMapping"): + converterClass = StructWithLength else: - if not tp in converterMapping: + if not tp in converterMapping and '(' not in tp: tableName = tp converterClass = Struct else: - converterClass = converterMapping[tp] - tableClass = tableNamespace.get(tableName) - conv = converterClass(name, repeat, aux, tableClass) - if name in ["SubTable", "ExtSubTable"]: + converterClass = eval(tp, tableNamespace, converterMapping) + if tp in ('MortChain', 'MortSubtable', 'MorxChain'): + tableClass = tableNamespace.get(tp) + else: + tableClass = tableNamespace.get(tableName) + if tableClass is not None: + conv = converterClass(name, repeat, aux, tableClass=tableClass) + else: + conv = converterClass(name, repeat, aux) + if name in ["SubTable", "ExtSubTable", "SubStruct"]: conv.lookupTypes = tableNamespace['lookupTypes'] # also create reverse mapping for t in conv.lookupTypes.values(): @@ -50,32 +77,104 @@ def buildConverters(tableSpec, tableNamespace): return converters, convertersByName +class _MissingItem(tuple): + __slots__ = () + + +try: + from collections import UserList +except ImportError: + from UserList import UserList + + +class _LazyList(UserList): + + def __getslice__(self, i, j): + return self.__getitem__(slice(i, j)) + + def __getitem__(self, k): + if isinstance(k, slice): + indices = range(*k.indices(len(self))) + return [self[i] for i in indices] + item = self.data[k] + if isinstance(item, _MissingItem): + self.reader.seek(self.pos + item[0] * self.recordSize) + item = self.conv.read(self.reader, self.font, {}) + self.data[k] = item + return item + + def __add__(self, other): + if isinstance(other, _LazyList): + other = list(other) + elif isinstance(other, list): + pass + else: + return NotImplemented + return list(self) + other + + def __radd__(self, other): + if not isinstance(other, list): + return NotImplemented + return other + list(self) + + class BaseConverter(object): - + """Base class for converter objects. Apart from the constructor, this is an abstract class.""" - - def __init__(self, name, repeat, aux, tableClass): + + def __init__(self, name, repeat, aux, tableClass=None): self.name = name self.repeat = repeat self.aux = aux self.tableClass = tableClass - self.isCount = name.endswith("Count") - self.isLookupType = name.endswith("LookupType") - self.isPropagated = name in ["ClassCount", "Class2Count", "FeatureTag"] - + self.isCount = name.endswith("Count") or name in ['DesignAxisRecordSize', 'ValueRecordSize'] + self.isLookupType = name.endswith("LookupType") or name == "MorphType" + self.isPropagated = name in ["ClassCount", "Class2Count", "FeatureTag", "SettingsCount", "VarRegionCount", "MappingCount", "RegionAxisCount", 'DesignAxisCount', 'DesignAxisRecordSize', 'AxisValueCount', 'ValueRecordSize'] + + def readArray(self, reader, font, tableDict, count): + """Read an array of values from the reader.""" + lazy = font.lazy and count > 8 + if lazy: + recordSize = self.getRecordSize(reader) + if recordSize is NotImplemented: + lazy = False + if not lazy: + l = [] + for i in range(count): + l.append(self.read(reader, font, tableDict)) + return l + else: + l = _LazyList() + l.reader = reader.copy() + l.pos = l.reader.pos + l.font = font + l.conv = self + l.recordSize = recordSize + l.extend(_MissingItem([i]) for i in range(count)) + reader.advance(count * recordSize) + return l + + def getRecordSize(self, reader): + if hasattr(self, 'staticSize'): return self.staticSize + return NotImplemented + def read(self, reader, font, tableDict): """Read a value from the reader.""" raise NotImplementedError(self) - + + def writeArray(self, writer, font, tableDict, values): + for i, value in enumerate(values): + self.write(writer, font, tableDict, value, i) + def write(self, writer, font, tableDict, value, repeatIndex=None): """Write a value to the writer.""" raise NotImplementedError(self) - + def xmlRead(self, attrs, content, font): """Read a value from XML.""" raise NotImplementedError(self) - + def xmlWrite(self, xmlWriter, font, value, name, attrs): """Write a value to XML.""" raise NotImplementedError(self) @@ -93,130 +192,298 @@ class IntValue(SimpleValue): return int(attrs["value"], 0) class Long(IntValue): + staticSize = 4 def read(self, reader, font, tableDict): return reader.readLong() def write(self, writer, font, tableDict, value, repeatIndex=None): writer.writeLong(value) -class Version(BaseConverter): +class ULong(IntValue): + staticSize = 4 def read(self, reader, font, tableDict): - value = reader.readLong() - assert (value >> 16) == 1, "Unsupported version 0x%08x" % value - return fi2fl(value, 16) + return reader.readULong() def write(self, writer, font, tableDict, value, repeatIndex=None): - if value < 0x10000: - value = fl2fi(value, 16) - value = int(round(value)) - assert (value >> 16) == 1, "Unsupported version 0x%08x" % value - writer.writeLong(value) - def xmlRead(self, attrs, content, font): - value = attrs["value"] - value = float(int(value, 0)) if value.startswith("0") else float(value) - if value >= 0x10000: - value = fi2fl(value, 16) - return value + writer.writeULong(value) + +class Flags32(ULong): def xmlWrite(self, xmlWriter, font, value, name, attrs): - if value >= 0x10000: - value = fi2fl(value, 16) - if value % 1 != 0: - # Write as hex - value = "0x%08x" % fl2fi(value, 16) - xmlWriter.simpletag(name, attrs + [("value", value)]) + xmlWriter.simpletag(name, attrs + [("value", "0x%08X" % value)]) xmlWriter.newline() class Short(IntValue): + staticSize = 2 def read(self, reader, font, tableDict): return reader.readShort() def write(self, writer, font, tableDict, value, repeatIndex=None): writer.writeShort(value) class UShort(IntValue): + staticSize = 2 def read(self, reader, font, tableDict): return reader.readUShort() def write(self, writer, font, tableDict, value, repeatIndex=None): writer.writeUShort(value) +class Int8(IntValue): + staticSize = 1 + def read(self, reader, font, tableDict): + return reader.readInt8() + def write(self, writer, font, tableDict, value, repeatIndex=None): + writer.writeInt8(value) + +class UInt8(IntValue): + staticSize = 1 + def read(self, reader, font, tableDict): + return reader.readUInt8() + def write(self, writer, font, tableDict, value, repeatIndex=None): + writer.writeUInt8(value) + class UInt24(IntValue): + staticSize = 3 def read(self, reader, font, tableDict): return reader.readUInt24() def write(self, writer, font, tableDict, value, repeatIndex=None): writer.writeUInt24(value) -class ComputedUShort(UShort): +class ComputedInt(IntValue): def xmlWrite(self, xmlWriter, font, value, name, attrs): - xmlWriter.comment("%s=%s" % (name, value)) - xmlWriter.newline() + if value is not None: + xmlWriter.comment("%s=%s" % (name, value)) + xmlWriter.newline() + +class ComputedUInt8(ComputedInt, UInt8): + pass +class ComputedUShort(ComputedInt, UShort): + pass +class ComputedULong(ComputedInt, ULong): + pass class Tag(SimpleValue): + staticSize = 4 def read(self, reader, font, tableDict): return reader.readTag() def write(self, writer, font, tableDict, value, repeatIndex=None): writer.writeTag(value) class GlyphID(SimpleValue): + staticSize = 2 + def readArray(self, reader, font, tableDict, count): + glyphOrder = font.getGlyphOrder() + gids = reader.readUShortArray(count) + try: + l = [glyphOrder[gid] for gid in gids] + except IndexError: + # Slower, but will not throw an IndexError on an invalid glyph id. + l = [font.getGlyphName(gid) for gid in gids] + return l def read(self, reader, font, tableDict): - value = reader.readUShort() - value = font.getGlyphName(value) - return value - + return font.getGlyphName(reader.readUShort()) def write(self, writer, font, tableDict, value, repeatIndex=None): - value = font.getGlyphID(value) - writer.writeUShort(value) + writer.writeUShort(font.getGlyphID(value)) + + +class NameID(UShort): + def xmlWrite(self, xmlWriter, font, value, name, attrs): + xmlWriter.simpletag(name, attrs + [("value", value)]) + if font and value: + nameTable = font.get("name") + if nameTable: + name = nameTable.getDebugName(value) + xmlWriter.write(" ") + if name: + xmlWriter.comment(name) + else: + xmlWriter.comment("missing from name table") + log.warning("name id %d missing from name table" % value) + xmlWriter.newline() + class FloatValue(SimpleValue): def xmlRead(self, attrs, content, font): return float(attrs["value"]) class DeciPoints(FloatValue): + staticSize = 2 def read(self, reader, font, tableDict): - value = reader.readUShort() - return value / 10 + return reader.readUShort() / 10 def write(self, writer, font, tableDict, value, repeatIndex=None): - writer.writeUShort(int(round(value * 10))) + writer.writeUShort(round(value * 10)) + +class Fixed(FloatValue): + staticSize = 4 + def read(self, reader, font, tableDict): + return fi2fl(reader.readLong(), 16) + def write(self, writer, font, tableDict, value, repeatIndex=None): + writer.writeLong(fl2fi(value, 16)) + +class F2Dot14(FloatValue): + staticSize = 2 + def read(self, reader, font, tableDict): + return fi2fl(reader.readShort(), 14) + def write(self, writer, font, tableDict, value, repeatIndex=None): + writer.writeShort(fl2fi(value, 14)) + +class Version(BaseConverter): + staticSize = 4 + def read(self, reader, font, tableDict): + value = reader.readLong() + assert (value >> 16) == 1, "Unsupported version 0x%08x" % value + return value + def write(self, writer, font, tableDict, value, repeatIndex=None): + value = fi2ve(value) + assert (value >> 16) == 1, "Unsupported version 0x%08x" % value + writer.writeLong(value) + def xmlRead(self, attrs, content, font): + value = attrs["value"] + value = ve2fi(value) + return value + def xmlWrite(self, xmlWriter, font, value, name, attrs): + value = fi2ve(value) + value = "0x%08x" % value + xmlWriter.simpletag(name, attrs + [("value", value)]) + xmlWriter.newline() + + @staticmethod + def fromFloat(v): + return fl2fi(v, 16) + + +class Char64(SimpleValue): + """An ASCII string with up to 64 characters. + + Unused character positions are filled with 0x00 bytes. + Used in Apple AAT fonts in the `gcid` table. + """ + staticSize = 64 + + def read(self, reader, font, tableDict): + data = reader.readData(self.staticSize) + zeroPos = data.find(b"\0") + if zeroPos >= 0: + data = data[:zeroPos] + s = tounicode(data, encoding="ascii", errors="replace") + if s != tounicode(data, encoding="ascii", errors="ignore"): + log.warning('replaced non-ASCII characters in "%s"' % + s) + return s + + def write(self, writer, font, tableDict, value, repeatIndex=None): + data = tobytes(value, encoding="ascii", errors="replace") + if data != tobytes(value, encoding="ascii", errors="ignore"): + log.warning('replacing non-ASCII characters in "%s"' % + value) + if len(data) > self.staticSize: + log.warning('truncating overlong "%s" to %d bytes' % + (value, self.staticSize)) + data = (data + b"\0" * self.staticSize)[:self.staticSize] + writer.writeData(data) + class Struct(BaseConverter): - + + def getRecordSize(self, reader): + return self.tableClass and self.tableClass.getRecordSize(reader) + def read(self, reader, font, tableDict): table = self.tableClass() table.decompile(reader, font) return table - + def write(self, writer, font, tableDict, value, repeatIndex=None): value.compile(writer, font) - + def xmlWrite(self, xmlWriter, font, value, name, attrs): if value is None: if attrs: # If there are attributes (probably index), then # don't drop this even if it's NULL. It will mess # up the array indices of the containing element. - xmlWriter.simpletag(name, attrs + [("empty", True)]) + xmlWriter.simpletag(name, attrs + [("empty", 1)]) xmlWriter.newline() else: pass # NULL table, ignore else: value.toXML(xmlWriter, font, attrs, name=name) - + def xmlRead(self, attrs, content, font): - table = self.tableClass() - if attrs.get("empty"): + if "empty" in attrs and safeEval(attrs["empty"]): return None + table = self.tableClass() Format = attrs.get("Format") if Format is not None: table.Format = int(Format) + + noPostRead = not hasattr(table, 'postRead') + if noPostRead: + # TODO Cache table.hasPropagated. + cleanPropagation = False + for conv in table.getConverters(): + if conv.isPropagated: + cleanPropagation = True + if not hasattr(font, '_propagator'): + font._propagator = {} + propagator = font._propagator + assert conv.name not in propagator, (conv.name, propagator) + setattr(table, conv.name, None) + propagator[conv.name] = CountReference(table.__dict__, conv.name) + for element in content: if isinstance(element, tuple): name, attrs, content = element table.fromXML(name, attrs, content, font) else: pass + + table.populateDefaults(propagator=getattr(font, '_propagator', None)) + + if noPostRead: + if cleanPropagation: + for conv in table.getConverters(): + if conv.isPropagated: + propagator = font._propagator + del propagator[conv.name] + if not propagator: + del font._propagator + + return table + + def __repr__(self): + return "Struct of " + repr(self.tableClass) + + +class StructWithLength(Struct): + def read(self, reader, font, tableDict): + pos = reader.pos + table = self.tableClass() + table.decompile(reader, font) + reader.seek(pos + table.StructLength) return table + def write(self, writer, font, tableDict, value, repeatIndex=None): + for convIndex, conv in enumerate(value.getConverters()): + if conv.name == "StructLength": + break + lengthIndex = len(writer.items) + convIndex + if isinstance(value, FormatSwitchingBaseTable): + lengthIndex += 1 # implicit Format field + deadbeef = {1:0xDE, 2:0xDEAD, 4:0xDEADBEEF}[conv.staticSize] + + before = writer.getDataLength() + value.StructLength = deadbeef + value.compile(writer, font) + length = writer.getDataLength() - before + lengthWriter = writer.getSubWriter() + conv.write(lengthWriter, font, tableDict, length) + assert(writer.items[lengthIndex] == + b"\xde\xad\xbe\xef"[:conv.staticSize]) + writer.items[lengthIndex] = lengthWriter.getAllData() + class Table(Struct): longOffset = False + staticSize = 2 def readOffset(self, reader): return reader.readUShort() @@ -226,16 +493,11 @@ class Table(Struct): writer.writeULong(0) else: writer.writeUShort(0) - + def read(self, reader, font, tableDict): offset = self.readOffset(reader) if offset == 0: return None - if offset <= 3: - # XXX hack to work around buggy pala.ttf - print("*** Warning: offset is not 0, yet suspiciously low (%s). table: %s" \ - % (offset, self.tableClass.__name__)) - return None table = self.tableClass() reader = reader.getSubReader(offset) if font.lazy: @@ -244,7 +506,7 @@ class Table(Struct): else: table.decompile(reader, font) return table - + def write(self, writer, font, tableDict, value, repeatIndex=None): if value is None: self.writeNullOffset(writer) @@ -260,23 +522,37 @@ class Table(Struct): class LTable(Table): longOffset = True + staticSize = 4 def readOffset(self, reader): return reader.readULong() +# TODO Clean / merge the SubTable and SubStruct + +class SubStruct(Struct): + def getConverter(self, tableType, lookupType): + tableClass = self.lookupTypes[tableType][lookupType] + return self.__class__(self.name, self.repeat, self.aux, tableClass) + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + super(SubStruct, self).xmlWrite(xmlWriter, font, value, None, attrs) + class SubTable(Table): def getConverter(self, tableType, lookupType): tableClass = self.lookupTypes[tableType][lookupType] return self.__class__(self.name, self.repeat, self.aux, tableClass) + def xmlWrite(self, xmlWriter, font, value, name, attrs): + super(SubTable, self).xmlWrite(xmlWriter, font, value, None, attrs) class ExtSubTable(LTable, SubTable): - + def write(self, writer, font, tableDict, value, repeatIndex=None): - writer.Extension = 1 # actually, mere presence of the field flags it as an Ext Subtable writer. + writer.Extension = True # actually, mere presence of the field flags it as an Ext Subtable writer. Table.write(self, writer, font, tableDict, value, repeatIndex) + class FeatureParams(Table): def getConverter(self, featureTag): tableClass = self.featureParamTypes.get(featureTag, self.defaultFeatureParams) @@ -284,7 +560,8 @@ class FeatureParams(Table): class ValueFormat(IntValue): - def __init__(self, name, repeat, aux, tableClass): + staticSize = 2 + def __init__(self, name, repeat, aux, tableClass=None): BaseConverter.__init__(self, name, repeat, aux, tableClass) self.which = "ValueFormat" + ("2" if name[-1] == "2" else "1") def read(self, reader, font, tableDict): @@ -297,6 +574,8 @@ class ValueFormat(IntValue): class ValueRecord(ValueFormat): + def getRecordSize(self, reader): + return 2 * len(reader[self.which]) def read(self, reader, font, tableDict): return reader[self.which].readValueRecord(reader, font) def write(self, writer, font, tableDict, value, repeatIndex=None): @@ -313,8 +592,859 @@ class ValueRecord(ValueFormat): return value +class AATLookup(BaseConverter): + BIN_SEARCH_HEADER_SIZE = 10 + + def __init__(self, name, repeat, aux, tableClass): + BaseConverter.__init__(self, name, repeat, aux, tableClass) + if issubclass(self.tableClass, SimpleValue): + self.converter = self.tableClass(name='Value', repeat=None, aux=None) + else: + self.converter = Table(name='Value', repeat=None, aux=None, tableClass=self.tableClass) + + def read(self, reader, font, tableDict): + format = reader.readUShort() + if format == 0: + return self.readFormat0(reader, font) + elif format == 2: + return self.readFormat2(reader, font) + elif format == 4: + return self.readFormat4(reader, font) + elif format == 6: + return self.readFormat6(reader, font) + elif format == 8: + return self.readFormat8(reader, font) + else: + assert False, "unsupported lookup format: %d" % format + + def write(self, writer, font, tableDict, value, repeatIndex=None): + values = list(sorted([(font.getGlyphID(glyph), val) + for glyph, val in value.items()])) + # TODO: Also implement format 4. + formats = list(sorted(filter(None, [ + self.buildFormat0(writer, font, values), + self.buildFormat2(writer, font, values), + self.buildFormat6(writer, font, values), + self.buildFormat8(writer, font, values), + ]))) + # We use the format ID as secondary sort key to make the output + # deterministic when multiple formats have same encoded size. + dataSize, lookupFormat, writeMethod = formats[0] + pos = writer.getDataLength() + writeMethod() + actualSize = writer.getDataLength() - pos + assert actualSize == dataSize, ( + "AATLookup format %d claimed to write %d bytes, but wrote %d" % + (lookupFormat, dataSize, actualSize)) + + @staticmethod + def writeBinSearchHeader(writer, numUnits, unitSize): + writer.writeUShort(unitSize) + writer.writeUShort(numUnits) + searchRange, entrySelector, rangeShift = \ + getSearchRange(n=numUnits, itemSize=unitSize) + writer.writeUShort(searchRange) + writer.writeUShort(entrySelector) + writer.writeUShort(rangeShift) + + def buildFormat0(self, writer, font, values): + numGlyphs = len(font.getGlyphOrder()) + if len(values) != numGlyphs: + return None + valueSize = self.converter.staticSize + return (2 + numGlyphs * valueSize, 0, + lambda: self.writeFormat0(writer, font, values)) + + def writeFormat0(self, writer, font, values): + writer.writeUShort(0) + for glyphID_, value in values: + self.converter.write( + writer, font, tableDict=None, + value=value, repeatIndex=None) + + def buildFormat2(self, writer, font, values): + segStart, segValue = values[0] + segEnd = segStart + segments = [] + for glyphID, curValue in values[1:]: + if glyphID != segEnd + 1 or curValue != segValue: + segments.append((segStart, segEnd, segValue)) + segStart = segEnd = glyphID + segValue = curValue + else: + segEnd = glyphID + segments.append((segStart, segEnd, segValue)) + valueSize = self.converter.staticSize + numUnits, unitSize = len(segments) + 1, valueSize + 4 + return (2 + self.BIN_SEARCH_HEADER_SIZE + numUnits * unitSize, 2, + lambda: self.writeFormat2(writer, font, segments)) + + def writeFormat2(self, writer, font, segments): + writer.writeUShort(2) + valueSize = self.converter.staticSize + numUnits, unitSize = len(segments), valueSize + 4 + self.writeBinSearchHeader(writer, numUnits, unitSize) + for firstGlyph, lastGlyph, value in segments: + writer.writeUShort(lastGlyph) + writer.writeUShort(firstGlyph) + self.converter.write( + writer, font, tableDict=None, + value=value, repeatIndex=None) + writer.writeUShort(0xFFFF) + writer.writeUShort(0xFFFF) + writer.writeData(b'\x00' * valueSize) + + def buildFormat6(self, writer, font, values): + valueSize = self.converter.staticSize + numUnits, unitSize = len(values), valueSize + 2 + return (2 + self.BIN_SEARCH_HEADER_SIZE + (numUnits + 1) * unitSize, 6, + lambda: self.writeFormat6(writer, font, values)) + + def writeFormat6(self, writer, font, values): + writer.writeUShort(6) + valueSize = self.converter.staticSize + numUnits, unitSize = len(values), valueSize + 2 + self.writeBinSearchHeader(writer, numUnits, unitSize) + for glyphID, value in values: + writer.writeUShort(glyphID) + self.converter.write( + writer, font, tableDict=None, + value=value, repeatIndex=None) + writer.writeUShort(0xFFFF) + writer.writeData(b'\x00' * valueSize) + + def buildFormat8(self, writer, font, values): + minGlyphID, maxGlyphID = values[0][0], values[-1][0] + if len(values) != maxGlyphID - minGlyphID + 1: + return None + valueSize = self.converter.staticSize + return (6 + len(values) * valueSize, 8, + lambda: self.writeFormat8(writer, font, values)) + + def writeFormat8(self, writer, font, values): + firstGlyphID = values[0][0] + writer.writeUShort(8) + writer.writeUShort(firstGlyphID) + writer.writeUShort(len(values)) + for _, value in values: + self.converter.write( + writer, font, tableDict=None, + value=value, repeatIndex=None) + + def readFormat0(self, reader, font): + numGlyphs = len(font.getGlyphOrder()) + data = self.converter.readArray( + reader, font, tableDict=None, count=numGlyphs) + return {font.getGlyphName(k): value + for k, value in enumerate(data)} + + def readFormat2(self, reader, font): + mapping = {} + pos = reader.pos - 2 # start of table is at UShort for format + unitSize, numUnits = reader.readUShort(), reader.readUShort() + assert unitSize >= 4 + self.converter.staticSize, unitSize + for i in range(numUnits): + reader.seek(pos + i * unitSize + 12) + last = reader.readUShort() + first = reader.readUShort() + value = self.converter.read(reader, font, tableDict=None) + if last != 0xFFFF: + for k in range(first, last + 1): + mapping[font.getGlyphName(k)] = value + return mapping + + def readFormat4(self, reader, font): + mapping = {} + pos = reader.pos - 2 # start of table is at UShort for format + unitSize = reader.readUShort() + assert unitSize >= 6, unitSize + for i in range(reader.readUShort()): + reader.seek(pos + i * unitSize + 12) + last = reader.readUShort() + first = reader.readUShort() + offset = reader.readUShort() + if last != 0xFFFF: + dataReader = reader.getSubReader(0) # relative to current position + dataReader.seek(pos + offset) # relative to start of table + data = self.converter.readArray( + dataReader, font, tableDict=None, + count=last - first + 1) + for k, v in enumerate(data): + mapping[font.getGlyphName(first + k)] = v + return mapping + + def readFormat6(self, reader, font): + mapping = {} + pos = reader.pos - 2 # start of table is at UShort for format + unitSize = reader.readUShort() + assert unitSize >= 2 + self.converter.staticSize, unitSize + for i in range(reader.readUShort()): + reader.seek(pos + i * unitSize + 12) + glyphID = reader.readUShort() + value = self.converter.read( + reader, font, tableDict=None) + if glyphID != 0xFFFF: + mapping[font.getGlyphName(glyphID)] = value + return mapping + + def readFormat8(self, reader, font): + first = reader.readUShort() + count = reader.readUShort() + data = self.converter.readArray( + reader, font, tableDict=None, count=count) + return {font.getGlyphName(first + k): value + for (k, value) in enumerate(data)} + + def xmlRead(self, attrs, content, font): + value = {} + for element in content: + if isinstance(element, tuple): + name, a, eltContent = element + if name == "Lookup": + value[a["glyph"]] = self.converter.xmlRead(a, eltContent, font) + return value + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + xmlWriter.begintag(name, attrs) + xmlWriter.newline() + for glyph, value in sorted(value.items()): + self.converter.xmlWrite( + xmlWriter, font, value=value, + name="Lookup", attrs=[("glyph", glyph)]) + xmlWriter.endtag(name) + xmlWriter.newline() + + +# The AAT 'ankr' table has an unusual structure: An offset to an AATLookup +# followed by an offset to a glyph data table. Other than usual, the +# offsets in the AATLookup are not relative to the beginning of +# the beginning of the 'ankr' table, but relative to the glyph data table. +# So, to find the anchor data for a glyph, one needs to add the offset +# to the data table to the offset found in the AATLookup, and then use +# the sum of these two offsets to find the actual data. +class AATLookupWithDataOffset(BaseConverter): + def read(self, reader, font, tableDict): + lookupOffset = reader.readULong() + dataOffset = reader.readULong() + lookupReader = reader.getSubReader(lookupOffset) + lookup = AATLookup('DataOffsets', None, None, UShort) + offsets = lookup.read(lookupReader, font, tableDict) + result = {} + for glyph, offset in offsets.items(): + dataReader = reader.getSubReader(offset + dataOffset) + item = self.tableClass() + item.decompile(dataReader, font) + result[glyph] = item + return result + + def write(self, writer, font, tableDict, value, repeatIndex=None): + # We do not work with OTTableWriter sub-writers because + # the offsets in our AATLookup are relative to our data + # table, for which we need to provide an offset value itself. + # It might have been possible to somehow make a kludge for + # performing this indirect offset computation directly inside + # OTTableWriter. But this would have made the internal logic + # of OTTableWriter even more complex than it already is, + # so we decided to roll our own offset computation for the + # contents of the AATLookup and associated data table. + offsetByGlyph, offsetByData, dataLen = {}, {}, 0 + compiledData = [] + for glyph in sorted(value, key=font.getGlyphID): + subWriter = OTTableWriter() + value[glyph].compile(subWriter, font) + data = subWriter.getAllData() + offset = offsetByData.get(data, None) + if offset == None: + offset = dataLen + dataLen = dataLen + len(data) + offsetByData[data] = offset + compiledData.append(data) + offsetByGlyph[glyph] = offset + # For calculating the offsets to our AATLookup and data table, + # we can use the regular OTTableWriter infrastructure. + lookupWriter = writer.getSubWriter() + lookupWriter.longOffset = True + lookup = AATLookup('DataOffsets', None, None, UShort) + lookup.write(lookupWriter, font, tableDict, offsetByGlyph, None) + + dataWriter = writer.getSubWriter() + dataWriter.longOffset = True + writer.writeSubTable(lookupWriter) + writer.writeSubTable(dataWriter) + for d in compiledData: + dataWriter.writeData(d) + + def xmlRead(self, attrs, content, font): + lookup = AATLookup('DataOffsets', None, None, self.tableClass) + return lookup.xmlRead(attrs, content, font) + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + lookup = AATLookup('DataOffsets', None, None, self.tableClass) + lookup.xmlWrite(xmlWriter, font, value, name, attrs) + + +class MorxSubtableConverter(BaseConverter): + _PROCESSING_ORDERS = { + # bits 30 and 28 of morx.CoverageFlags; see morx spec + (False, False): "LayoutOrder", + (True, False): "ReversedLayoutOrder", + (False, True): "LogicalOrder", + (True, True): "ReversedLogicalOrder", + } + + _PROCESSING_ORDERS_REVERSED = { + val: key for key, val in _PROCESSING_ORDERS.items() + } + + def __init__(self, name, repeat, aux): + BaseConverter.__init__(self, name, repeat, aux) + + def _setTextDirectionFromCoverageFlags(self, flags, subtable): + if (flags & 0x20) != 0: + subtable.TextDirection = "Any" + elif (flags & 0x80) != 0: + subtable.TextDirection = "Vertical" + else: + subtable.TextDirection = "Horizontal" + + def read(self, reader, font, tableDict): + pos = reader.pos + m = MorxSubtable() + m.StructLength = reader.readULong() + flags = reader.readUInt8() + orderKey = ((flags & 0x40) != 0, (flags & 0x10) != 0) + m.ProcessingOrder = self._PROCESSING_ORDERS[orderKey] + self._setTextDirectionFromCoverageFlags(flags, m) + m.Reserved = reader.readUShort() + m.Reserved |= (flags & 0xF) << 16 + m.MorphType = reader.readUInt8() + m.SubFeatureFlags = reader.readULong() + tableClass = lookupTypes["morx"].get(m.MorphType) + if tableClass is None: + assert False, ("unsupported 'morx' lookup type %s" % + m.MorphType) + # To decode AAT ligatures, we need to know the subtable size. + # The easiest way to pass this along is to create a new reader + # that works on just the subtable as its data. + headerLength = reader.pos - pos + data = reader.data[ + reader.pos + : reader.pos + m.StructLength - headerLength] + assert len(data) == m.StructLength - headerLength + subReader = OTTableReader(data=data, tableTag=reader.tableTag) + m.SubStruct = tableClass() + m.SubStruct.decompile(subReader, font) + reader.seek(pos + m.StructLength) + return m + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + xmlWriter.begintag(name, attrs) + xmlWriter.newline() + xmlWriter.comment("StructLength=%d" % value.StructLength) + xmlWriter.newline() + xmlWriter.simpletag("TextDirection", value=value.TextDirection) + xmlWriter.newline() + xmlWriter.simpletag("ProcessingOrder", + value=value.ProcessingOrder) + xmlWriter.newline() + if value.Reserved != 0: + xmlWriter.simpletag("Reserved", + value="0x%04x" % value.Reserved) + xmlWriter.newline() + xmlWriter.comment("MorphType=%d" % value.MorphType) + xmlWriter.newline() + xmlWriter.simpletag("SubFeatureFlags", + value="0x%08x" % value.SubFeatureFlags) + xmlWriter.newline() + value.SubStruct.toXML(xmlWriter, font) + xmlWriter.endtag(name) + xmlWriter.newline() + + def xmlRead(self, attrs, content, font): + m = MorxSubtable() + covFlags = 0 + m.Reserved = 0 + for eltName, eltAttrs, eltContent in filter(istuple, content): + if eltName == "CoverageFlags": + # Only in XML from old versions of fonttools. + covFlags = safeEval(eltAttrs["value"]) + orderKey = ((covFlags & 0x40) != 0, + (covFlags & 0x10) != 0) + m.ProcessingOrder = self._PROCESSING_ORDERS[ + orderKey] + self._setTextDirectionFromCoverageFlags( + covFlags, m) + elif eltName == "ProcessingOrder": + m.ProcessingOrder = eltAttrs["value"] + assert m.ProcessingOrder in self._PROCESSING_ORDERS_REVERSED, "unknown ProcessingOrder: %s" % m.ProcessingOrder + elif eltName == "TextDirection": + m.TextDirection = eltAttrs["value"] + assert m.TextDirection in {"Horizontal", "Vertical", "Any"}, "unknown TextDirection %s" % m.TextDirection + elif eltName == "Reserved": + m.Reserved = safeEval(eltAttrs["value"]) + elif eltName == "SubFeatureFlags": + m.SubFeatureFlags = safeEval(eltAttrs["value"]) + elif eltName.endswith("Morph"): + m.fromXML(eltName, eltAttrs, eltContent, font) + else: + assert False, eltName + m.Reserved = (covFlags & 0xF) << 16 | m.Reserved + return m + + def write(self, writer, font, tableDict, value, repeatIndex=None): + covFlags = (value.Reserved & 0x000F0000) >> 16 + reverseOrder, logicalOrder = self._PROCESSING_ORDERS_REVERSED[ + value.ProcessingOrder] + covFlags |= 0x80 if value.TextDirection == "Vertical" else 0 + covFlags |= 0x40 if reverseOrder else 0 + covFlags |= 0x20 if value.TextDirection == "Any" else 0 + covFlags |= 0x10 if logicalOrder else 0 + value.CoverageFlags = covFlags + lengthIndex = len(writer.items) + before = writer.getDataLength() + value.StructLength = 0xdeadbeef + # The high nibble of value.Reserved is actuallly encoded + # into coverageFlags, so we need to clear it here. + origReserved = value.Reserved # including high nibble + value.Reserved = value.Reserved & 0xFFFF # without high nibble + value.compile(writer, font) + value.Reserved = origReserved # restore original value + assert writer.items[lengthIndex] == b"\xde\xad\xbe\xef" + length = writer.getDataLength() - before + writer.items[lengthIndex] = struct.pack(">L", length) + + +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6Tables.html#ExtendedStateHeader +# TODO: Untangle the implementation of the various lookup-specific formats. +class STXHeader(BaseConverter): + def __init__(self, name, repeat, aux, tableClass): + BaseConverter.__init__(self, name, repeat, aux, tableClass) + assert issubclass(self.tableClass, AATAction) + self.classLookup = AATLookup("GlyphClasses", None, None, UShort) + if issubclass(self.tableClass, ContextualMorphAction): + self.perGlyphLookup = AATLookup("PerGlyphLookup", + None, None, GlyphID) + else: + self.perGlyphLookup = None + + def read(self, reader, font, tableDict): + table = AATStateTable() + pos = reader.pos + classTableReader = reader.getSubReader(0) + stateArrayReader = reader.getSubReader(0) + entryTableReader = reader.getSubReader(0) + actionReader = None + ligaturesReader = None + table.GlyphClassCount = reader.readULong() + classTableReader.seek(pos + reader.readULong()) + stateArrayReader.seek(pos + reader.readULong()) + entryTableReader.seek(pos + reader.readULong()) + if self.perGlyphLookup is not None: + perGlyphTableReader = reader.getSubReader(0) + perGlyphTableReader.seek(pos + reader.readULong()) + if issubclass(self.tableClass, LigatureMorphAction): + actionReader = reader.getSubReader(0) + actionReader.seek(pos + reader.readULong()) + ligComponentReader = reader.getSubReader(0) + ligComponentReader.seek(pos + reader.readULong()) + ligaturesReader = reader.getSubReader(0) + ligaturesReader.seek(pos + reader.readULong()) + numLigComponents = (ligaturesReader.pos + - ligComponentReader.pos) // 2 + assert numLigComponents >= 0 + table.LigComponents = \ + ligComponentReader.readUShortArray(numLigComponents) + table.Ligatures = self._readLigatures(ligaturesReader, font) + table.GlyphClasses = self.classLookup.read(classTableReader, + font, tableDict) + numStates = int((entryTableReader.pos - stateArrayReader.pos) + / (table.GlyphClassCount * 2)) + for stateIndex in range(numStates): + state = AATState() + table.States.append(state) + for glyphClass in range(table.GlyphClassCount): + entryIndex = stateArrayReader.readUShort() + state.Transitions[glyphClass] = \ + self._readTransition(entryTableReader, + entryIndex, font, + actionReader) + if self.perGlyphLookup is not None: + table.PerGlyphLookups = self._readPerGlyphLookups( + table, perGlyphTableReader, font) + return table + + def _readTransition(self, reader, entryIndex, font, actionReader): + transition = self.tableClass() + entryReader = reader.getSubReader( + reader.pos + entryIndex * transition.staticSize) + transition.decompile(entryReader, font, actionReader) + return transition + + def _readLigatures(self, reader, font): + limit = len(reader.data) + numLigatureGlyphs = (limit - reader.pos) // 2 + return [font.getGlyphName(g) + for g in reader.readUShortArray(numLigatureGlyphs)] + + def _countPerGlyphLookups(self, table): + # Somewhat annoyingly, the morx table does not encode + # the size of the per-glyph table. So we need to find + # the maximum value that MorphActions use as index + # into this table. + numLookups = 0 + for state in table.States: + for t in state.Transitions.values(): + if isinstance(t, ContextualMorphAction): + if t.MarkIndex != 0xFFFF: + numLookups = max( + numLookups, + t.MarkIndex + 1) + if t.CurrentIndex != 0xFFFF: + numLookups = max( + numLookups, + t.CurrentIndex + 1) + return numLookups + + def _readPerGlyphLookups(self, table, reader, font): + pos = reader.pos + lookups = [] + for _ in range(self._countPerGlyphLookups(table)): + lookupReader = reader.getSubReader(0) + lookupReader.seek(pos + reader.readULong()) + lookups.append( + self.perGlyphLookup.read(lookupReader, font, {})) + return lookups + + def write(self, writer, font, tableDict, value, repeatIndex=None): + glyphClassWriter = OTTableWriter() + self.classLookup.write(glyphClassWriter, font, tableDict, + value.GlyphClasses, repeatIndex=None) + glyphClassData = pad(glyphClassWriter.getAllData(), 4) + glyphClassCount = max(value.GlyphClasses.values()) + 1 + glyphClassTableOffset = 16 # size of STXHeader + if self.perGlyphLookup is not None: + glyphClassTableOffset += 4 + + actionData, actionIndex = None, None + if issubclass(self.tableClass, LigatureMorphAction): + glyphClassTableOffset += 12 + actionData, actionIndex = \ + self._compileLigActions(value, font) + actionData = pad(actionData, 4) + + stateArrayData, entryTableData = self._compileStates( + font, value.States, glyphClassCount, actionIndex) + stateArrayOffset = glyphClassTableOffset + len(glyphClassData) + entryTableOffset = stateArrayOffset + len(stateArrayData) + perGlyphOffset = entryTableOffset + len(entryTableData) + perGlyphData = \ + pad(self._compilePerGlyphLookups(value, font), 4) + ligComponentsData = self._compileLigComponents(value, font) + ligaturesData = self._compileLigatures(value, font) + if actionData is None: + actionOffset = None + ligComponentsOffset = None + ligaturesOffset = None + else: + assert len(perGlyphData) == 0 + actionOffset = entryTableOffset + len(entryTableData) + ligComponentsOffset = actionOffset + len(actionData) + ligaturesOffset = ligComponentsOffset + len(ligComponentsData) + writer.writeULong(glyphClassCount) + writer.writeULong(glyphClassTableOffset) + writer.writeULong(stateArrayOffset) + writer.writeULong(entryTableOffset) + if self.perGlyphLookup is not None: + writer.writeULong(perGlyphOffset) + if actionOffset is not None: + writer.writeULong(actionOffset) + writer.writeULong(ligComponentsOffset) + writer.writeULong(ligaturesOffset) + writer.writeData(glyphClassData) + writer.writeData(stateArrayData) + writer.writeData(entryTableData) + writer.writeData(perGlyphData) + if actionData is not None: + writer.writeData(actionData) + if ligComponentsData is not None: + writer.writeData(ligComponentsData) + if ligaturesData is not None: + writer.writeData(ligaturesData) + + def _compileStates(self, font, states, glyphClassCount, actionIndex): + stateArrayWriter = OTTableWriter() + entries, entryIDs = [], {} + for state in states: + for glyphClass in range(glyphClassCount): + transition = state.Transitions[glyphClass] + entryWriter = OTTableWriter() + transition.compile(entryWriter, font, + actionIndex) + entryData = entryWriter.getAllData() + assert len(entryData) == transition.staticSize, ( \ + "%s has staticSize %d, " + "but actually wrote %d bytes" % ( + repr(transition), + transition.staticSize, + len(entryData))) + entryIndex = entryIDs.get(entryData) + if entryIndex is None: + entryIndex = len(entries) + entryIDs[entryData] = entryIndex + entries.append(entryData) + stateArrayWriter.writeUShort(entryIndex) + stateArrayData = pad(stateArrayWriter.getAllData(), 4) + entryTableData = pad(bytesjoin(entries), 4) + return stateArrayData, entryTableData + + def _compilePerGlyphLookups(self, table, font): + if self.perGlyphLookup is None: + return b"" + numLookups = self._countPerGlyphLookups(table) + assert len(table.PerGlyphLookups) == numLookups, ( + "len(AATStateTable.PerGlyphLookups) is %d, " + "but the actions inside the table refer to %d" % + (len(table.PerGlyphLookups), numLookups)) + writer = OTTableWriter() + for lookup in table.PerGlyphLookups: + lookupWriter = writer.getSubWriter() + lookupWriter.longOffset = True + self.perGlyphLookup.write(lookupWriter, font, + {}, lookup, None) + writer.writeSubTable(lookupWriter) + return writer.getAllData() + + def _compileLigActions(self, table, font): + assert issubclass(self.tableClass, LigatureMorphAction) + actions = set() + for state in table.States: + for _glyphClass, trans in state.Transitions.items(): + actions.add(trans.compileLigActions()) + result, actionIndex = b"", {} + # Sort the compiled actions in decreasing order of + # length, so that the longer sequence come before the + # shorter ones. For each compiled action ABCD, its + # suffixes BCD, CD, and D do not be encoded separately + # (in case they occur); instead, we can just store an + # index that points into the middle of the longer + # sequence. Every compiled AAT ligature sequence is + # terminated with an end-of-sequence flag, which can + # only be set on the last element of the sequence. + # Therefore, it is sufficient to consider just the + # suffixes. + for a in sorted(actions, key=lambda x:(-len(x), x)): + if a not in actionIndex: + for i in range(0, len(a), 4): + suffix = a[i:] + suffixIndex = (len(result) + i) // 4 + actionIndex.setdefault( + suffix, suffixIndex) + result += a + return (result, actionIndex) + + def _compileLigComponents(self, table, font): + if not hasattr(table, "LigComponents"): + return None + writer = OTTableWriter() + for component in table.LigComponents: + writer.writeUShort(component) + return writer.getAllData() + + def _compileLigatures(self, table, font): + if not hasattr(table, "Ligatures"): + return None + writer = OTTableWriter() + for glyphName in table.Ligatures: + writer.writeUShort(font.getGlyphID(glyphName)) + return writer.getAllData() + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + xmlWriter.begintag(name, attrs) + xmlWriter.newline() + xmlWriter.comment("GlyphClassCount=%s" %value.GlyphClassCount) + xmlWriter.newline() + for g, klass in sorted(value.GlyphClasses.items()): + xmlWriter.simpletag("GlyphClass", glyph=g, value=klass) + xmlWriter.newline() + for stateIndex, state in enumerate(value.States): + xmlWriter.begintag("State", index=stateIndex) + xmlWriter.newline() + for glyphClass, trans in sorted(state.Transitions.items()): + trans.toXML(xmlWriter, font=font, + attrs={"onGlyphClass": glyphClass}, + name="Transition") + xmlWriter.endtag("State") + xmlWriter.newline() + for i, lookup in enumerate(value.PerGlyphLookups): + xmlWriter.begintag("PerGlyphLookup", index=i) + xmlWriter.newline() + for glyph, val in sorted(lookup.items()): + xmlWriter.simpletag("Lookup", glyph=glyph, + value=val) + xmlWriter.newline() + xmlWriter.endtag("PerGlyphLookup") + xmlWriter.newline() + if hasattr(value, "LigComponents"): + xmlWriter.begintag("LigComponents") + xmlWriter.newline() + for i, val in enumerate(getattr(value, "LigComponents")): + xmlWriter.simpletag("LigComponent", index=i, + value=val) + xmlWriter.newline() + xmlWriter.endtag("LigComponents") + xmlWriter.newline() + self._xmlWriteLigatures(xmlWriter, font, value, name, attrs) + xmlWriter.endtag(name) + xmlWriter.newline() + + def _xmlWriteLigatures(self, xmlWriter, font, value, name, attrs): + if not hasattr(value, "Ligatures"): + return + xmlWriter.begintag("Ligatures") + xmlWriter.newline() + for i, g in enumerate(getattr(value, "Ligatures")): + xmlWriter.simpletag("Ligature", index=i, glyph=g) + xmlWriter.newline() + xmlWriter.endtag("Ligatures") + xmlWriter.newline() + + def xmlRead(self, attrs, content, font): + table = AATStateTable() + for eltName, eltAttrs, eltContent in filter(istuple, content): + if eltName == "GlyphClass": + glyph = eltAttrs["glyph"] + value = eltAttrs["value"] + table.GlyphClasses[glyph] = safeEval(value) + elif eltName == "State": + state = self._xmlReadState(eltAttrs, eltContent, font) + table.States.append(state) + elif eltName == "PerGlyphLookup": + lookup = self.perGlyphLookup.xmlRead( + eltAttrs, eltContent, font) + table.PerGlyphLookups.append(lookup) + elif eltName == "LigComponents": + table.LigComponents = \ + self._xmlReadLigComponents( + eltAttrs, eltContent, font) + elif eltName == "Ligatures": + table.Ligatures = \ + self._xmlReadLigatures( + eltAttrs, eltContent, font) + table.GlyphClassCount = max(table.GlyphClasses.values()) + 1 + return table + + def _xmlReadState(self, attrs, content, font): + state = AATState() + for eltName, eltAttrs, eltContent in filter(istuple, content): + if eltName == "Transition": + glyphClass = safeEval(eltAttrs["onGlyphClass"]) + transition = self.tableClass() + transition.fromXML(eltName, eltAttrs, + eltContent, font) + state.Transitions[glyphClass] = transition + return state + + def _xmlReadLigComponents(self, attrs, content, font): + ligComponents = [] + for eltName, eltAttrs, _eltContent in filter(istuple, content): + if eltName == "LigComponent": + ligComponents.append( + safeEval(eltAttrs["value"])) + return ligComponents + + def _xmlReadLigatures(self, attrs, content, font): + ligs = [] + for eltName, eltAttrs, _eltContent in filter(istuple, content): + if eltName == "Ligature": + ligs.append(eltAttrs["glyph"]) + return ligs + + +class CIDGlyphMap(BaseConverter): + def read(self, reader, font, tableDict): + numCIDs = reader.readUShort() + result = {} + for cid, glyphID in enumerate(reader.readUShortArray(numCIDs)): + if glyphID != 0xFFFF: + result[cid] = font.getGlyphName(glyphID) + return result + + def write(self, writer, font, tableDict, value, repeatIndex=None): + items = {cid: font.getGlyphID(glyph) + for cid, glyph in value.items()} + count = max(items) + 1 if items else 0 + writer.writeUShort(count) + for cid in range(count): + writer.writeUShort(items.get(cid, 0xFFFF)) + + def xmlRead(self, attrs, content, font): + result = {} + for eName, eAttrs, _eContent in filter(istuple, content): + if eName == "CID": + result[safeEval(eAttrs["cid"])] = \ + eAttrs["glyph"].strip() + return result + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + xmlWriter.begintag(name, attrs) + xmlWriter.newline() + for cid, glyph in sorted(value.items()): + if glyph is not None and glyph != 0xFFFF: + xmlWriter.simpletag( + "CID", cid=cid, glyph=glyph) + xmlWriter.newline() + xmlWriter.endtag(name) + xmlWriter.newline() + + +class GlyphCIDMap(BaseConverter): + def read(self, reader, font, tableDict): + glyphOrder = font.getGlyphOrder() + count = reader.readUShort() + cids = reader.readUShortArray(count) + if count > len(glyphOrder): + log.warning("GlyphCIDMap has %d elements, " + "but the font has only %d glyphs; " + "ignoring the rest" % + (count, len(glyphOrder))) + result = {} + for glyphID in range(min(len(cids), len(glyphOrder))): + cid = cids[glyphID] + if cid != 0xFFFF: + result[glyphOrder[glyphID]] = cid + return result + + def write(self, writer, font, tableDict, value, repeatIndex=None): + items = {font.getGlyphID(g): cid + for g, cid in value.items() + if cid is not None and cid != 0xFFFF} + count = max(items) + 1 if items else 0 + writer.writeUShort(count) + for glyphID in range(count): + writer.writeUShort(items.get(glyphID, 0xFFFF)) + + def xmlRead(self, attrs, content, font): + result = {} + for eName, eAttrs, _eContent in filter(istuple, content): + if eName == "CID": + result[eAttrs["glyph"]] = \ + safeEval(eAttrs["value"]) + return result + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + xmlWriter.begintag(name, attrs) + xmlWriter.newline() + for glyph, cid in sorted(value.items()): + if cid is not None and cid != 0xFFFF: + xmlWriter.simpletag( + "CID", glyph=glyph, value=cid) + xmlWriter.newline() + xmlWriter.endtag(name) + xmlWriter.newline() + + class DeltaValue(BaseConverter): - + def read(self, reader, font, tableDict): StartSize = tableDict["StartSize"] EndSize = tableDict["EndSize"] @@ -325,7 +1455,7 @@ class DeltaValue(BaseConverter): minusOffset = 1 << nBits mask = (1 << nBits) - 1 signMask = 1 << (nBits - 1) - + DeltaValue = [] tmp, shift = 0, 0 for i in range(nItems): @@ -337,7 +1467,7 @@ class DeltaValue(BaseConverter): value = value - minusOffset DeltaValue.append(value) return DeltaValue - + def write(self, writer, font, tableDict, value, repeatIndex=None): StartSize = tableDict["StartSize"] EndSize = tableDict["EndSize"] @@ -348,7 +1478,7 @@ class DeltaValue(BaseConverter): nBits = 1 << DeltaFormat assert len(DeltaValue) == nItems mask = (1 << nBits) - 1 - + tmp, shift = 0, 16 for value in DeltaValue: shift = shift - nBits @@ -358,28 +1488,138 @@ class DeltaValue(BaseConverter): tmp, shift = 0, 16 if shift != 16: writer.writeUShort(tmp) - + def xmlWrite(self, xmlWriter, font, value, name, attrs): xmlWriter.simpletag(name, attrs + [("value", value)]) xmlWriter.newline() - + + def xmlRead(self, attrs, content, font): + return safeEval(attrs["value"]) + + +class VarIdxMapValue(BaseConverter): + + def read(self, reader, font, tableDict): + fmt = tableDict['EntryFormat'] + nItems = tableDict['MappingCount'] + + innerBits = 1 + (fmt & 0x000F) + innerMask = (1<<innerBits) - 1 + outerMask = 0xFFFFFFFF - innerMask + outerShift = 16 - innerBits + + entrySize = 1 + ((fmt & 0x0030) >> 4) + read = { + 1: reader.readUInt8, + 2: reader.readUShort, + 3: reader.readUInt24, + 4: reader.readULong, + }[entrySize] + + mapping = [] + for i in range(nItems): + raw = read() + idx = ((raw & outerMask) << outerShift) | (raw & innerMask) + mapping.append(idx) + + return mapping + + def write(self, writer, font, tableDict, value, repeatIndex=None): + fmt = tableDict['EntryFormat'] + mapping = value + writer['MappingCount'].setValue(len(mapping)) + + innerBits = 1 + (fmt & 0x000F) + innerMask = (1<<innerBits) - 1 + outerShift = 16 - innerBits + + entrySize = 1 + ((fmt & 0x0030) >> 4) + write = { + 1: writer.writeUInt8, + 2: writer.writeUShort, + 3: writer.writeUInt24, + 4: writer.writeULong, + }[entrySize] + + for idx in mapping: + raw = ((idx & 0xFFFF0000) >> outerShift) | (idx & innerMask) + write(raw) + + +class VarDataValue(BaseConverter): + + def read(self, reader, font, tableDict): + values = [] + + regionCount = tableDict["VarRegionCount"] + shortCount = tableDict["NumShorts"] + + for i in range(min(regionCount, shortCount)): + values.append(reader.readShort()) + for i in range(min(regionCount, shortCount), regionCount): + values.append(reader.readInt8()) + for i in range(regionCount, shortCount): + reader.readInt8() + + return values + + def write(self, writer, font, tableDict, value, repeatIndex=None): + regionCount = tableDict["VarRegionCount"] + shortCount = tableDict["NumShorts"] + + for i in range(min(regionCount, shortCount)): + writer.writeShort(value[i]) + for i in range(min(regionCount, shortCount), regionCount): + writer.writeInt8(value[i]) + for i in range(regionCount, shortCount): + writer.writeInt8(0) + + def xmlWrite(self, xmlWriter, font, value, name, attrs): + xmlWriter.simpletag(name, attrs + [("value", value)]) + xmlWriter.newline() + def xmlRead(self, attrs, content, font): return safeEval(attrs["value"]) converterMapping = { - # type class - "int16": Short, - "uint16": UShort, - "uint24": UInt24, - "Version": Version, - "Tag": Tag, - "GlyphID": GlyphID, - "DeciPoints": DeciPoints, - "struct": Struct, - "Offset": Table, - "LOffset": LTable, - "ValueRecord": ValueRecord, - "DeltaValue": DeltaValue, -} + # type class + "int8": Int8, + "int16": Short, + "uint8": UInt8, + "uint8": UInt8, + "uint16": UShort, + "uint24": UInt24, + "uint32": ULong, + "char64": Char64, + "Flags32": Flags32, + "Version": Version, + "Tag": Tag, + "GlyphID": GlyphID, + "NameID": NameID, + "DeciPoints": DeciPoints, + "Fixed": Fixed, + "F2Dot14": F2Dot14, + "struct": Struct, + "Offset": Table, + "LOffset": LTable, + "ValueRecord": ValueRecord, + "DeltaValue": DeltaValue, + "VarIdxMapValue": VarIdxMapValue, + "VarDataValue": VarDataValue, + # AAT + "CIDGlyphMap": CIDGlyphMap, + "GlyphCIDMap": GlyphCIDMap, + "MortChain": StructWithLength, + "MortSubtable": StructWithLength, + "MorxChain": StructWithLength, + "MorxSubtable": MorxSubtableConverter, + + # "Template" types + "AATLookup": lambda C: partial(AATLookup, tableClass=C), + "AATLookupWithDataOffset": lambda C: partial(AATLookupWithDataOffset, tableClass=C), + "STXHeader": lambda C: partial(STXHeader, tableClass=C), + "OffsetTo": lambda C: partial(Table, tableClass=C), + "LOffsetTo": lambda C: partial(LTable, tableClass=C), +} diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py old mode 100644 new mode 100755 index 10046d8..a0d0552 --- a/Lib/fontTools/ttLib/tables/otData.py +++ b/Lib/fontTools/ttLib/tables/otData.py @@ -1,9 +1,15 @@ +# coding: utf-8 +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * + otData = [ # # common # + ('LookupOrder', []), + ('ScriptList', [ ('uint16', 'ScriptCount', None, None, 'Number of ScriptRecords'), ('struct', 'ScriptRecord', 'ScriptCount', 0, 'Array of ScriptRecords -listed alphabetically by ScriptTag'), @@ -54,23 +60,23 @@ otData = [ ('FeatureParamsSize', [ ('DeciPoints', 'DesignSize', None, None, 'The design size in 720/inch units (decipoints).'), ('uint16', 'SubfamilyID', None, None, 'Serves as an identifier that associates fonts in a subfamily.'), - ('uint16', 'SubfamilyNameID', None, None, 'Subfamily NameID.'), + ('NameID', 'SubfamilyNameID', None, None, 'Subfamily NameID.'), ('DeciPoints', 'RangeStart', None, None, 'Small end of recommended usage range (exclusive) in 720/inch units.'), ('DeciPoints', 'RangeEnd', None, None, 'Large end of recommended usage range (inclusive) in 720/inch units.'), ]), ('FeatureParamsStylisticSet', [ ('uint16', 'Version', None, None, 'Set to 0.'), - ('uint16', 'UINameID', None, None, 'UI NameID.'), + ('NameID', 'UINameID', None, None, 'UI NameID.'), ]), ('FeatureParamsCharacterVariants', [ ('uint16', 'Format', None, None, 'Set to 0.'), - ('uint16', 'FeatUILabelNameID', None, None, 'Feature UI label NameID.'), - ('uint16', 'FeatUITooltipTextNameID', None, None, 'Feature UI tooltip text NameID.'), - ('uint16', 'SampleTextNameID', None, None, 'Sample text NameID.'), + ('NameID', 'FeatUILabelNameID', None, None, 'Feature UI label NameID.'), + ('NameID', 'FeatUITooltipTextNameID', None, None, 'Feature UI tooltip text NameID.'), + ('NameID', 'SampleTextNameID', None, None, 'Sample text NameID.'), ('uint16', 'NumNamedParameters', None, None, 'Number of named parameters.'), - ('uint16', 'FirstParamUILabelNameID', None, None, 'First NameID of UI feature parameters.'), + ('NameID', 'FirstParamUILabelNameID', None, None, 'First NameID of UI feature parameters.'), ('uint16', 'CharCount', None, None, 'Count of characters this feature provides glyph variants for.'), ('uint24', 'Character', 'CharCount', 0, 'Unicode characters for which this feature provides glyph variants.'), ]), @@ -129,7 +135,7 @@ otData = [ ('uint16', 'StartSize', None, None, 'Smallest size to correct-in ppem'), ('uint16', 'EndSize', None, None, 'Largest size to correct-in ppem'), ('uint16', 'DeltaFormat', None, None, 'Format of DeltaValue array data: 1, 2, or 3'), - ('DeltaValue', 'DeltaValue', '', 0, 'Array of compressed data'), + ('DeltaValue', 'DeltaValue', '', 'DeltaFormat in (1,2,3)', 'Array of compressed data'), ]), @@ -138,10 +144,11 @@ otData = [ # ('GPOS', [ - ('Version', 'Version', None, None, 'Version of the GPOS table-initially = 0x00010000'), + ('Version', 'Version', None, None, 'Version of the GPOS table- 0x00010000 or 0x00010001'), ('Offset', 'ScriptList', None, None, 'Offset to ScriptList table-from beginning of GPOS table'), ('Offset', 'FeatureList', None, None, 'Offset to FeatureList table-from beginning of GPOS table'), ('Offset', 'LookupList', None, None, 'Offset to LookupList table-from beginning of GPOS table'), + ('LOffset', 'FeatureVariations', None, 'Version >= 0x00010001', 'Offset to FeatureVariations table-from beginning of GPOS table'), ]), ('SinglePosFormat1', [ @@ -390,16 +397,16 @@ otData = [ ('LOffset', 'ExtSubTable', None, None, 'Offset to SubTable'), ]), - ('ValueRecord', [ - ('int16', 'XPlacement', None, None, 'Horizontal adjustment for placement-in design units'), - ('int16', 'YPlacement', None, None, 'Vertical adjustment for placement-in design units'), - ('int16', 'XAdvance', None, None, 'Horizontal adjustment for advance-in design units (only used for horizontal writing)'), - ('int16', 'YAdvance', None, None, 'Vertical adjustment for advance-in design units (only used for vertical writing)'), - ('Offset', 'XPlaDevice', None, None, 'Offset to Device table for horizontal placement-measured from beginning of PosTable (may be NULL)'), - ('Offset', 'YPlaDevice', None, None, 'Offset to Device table for vertical placement-measured from beginning of PosTable (may be NULL)'), - ('Offset', 'XAdvDevice', None, None, 'Offset to Device table for horizontal advance-measured from beginning of PosTable (may be NULL)'), - ('Offset', 'YAdvDevice', None, None, 'Offset to Device table for vertical advance-measured from beginning of PosTable (may be NULL)'), - ]), +# ('ValueRecord', [ +# ('int16', 'XPlacement', None, None, 'Horizontal adjustment for placement-in design units'), +# ('int16', 'YPlacement', None, None, 'Vertical adjustment for placement-in design units'), +# ('int16', 'XAdvance', None, None, 'Horizontal adjustment for advance-in design units (only used for horizontal writing)'), +# ('int16', 'YAdvance', None, None, 'Vertical adjustment for advance-in design units (only used for vertical writing)'), +# ('Offset', 'XPlaDevice', None, None, 'Offset to Device table for horizontal placement-measured from beginning of PosTable (may be NULL)'), +# ('Offset', 'YPlaDevice', None, None, 'Offset to Device table for vertical placement-measured from beginning of PosTable (may be NULL)'), +# ('Offset', 'XAdvDevice', None, None, 'Offset to Device table for horizontal advance-measured from beginning of PosTable (may be NULL)'), +# ('Offset', 'YAdvDevice', None, None, 'Offset to Device table for vertical advance-measured from beginning of PosTable (may be NULL)'), +# ]), ('AnchorFormat1', [ ('uint16', 'AnchorFormat', None, None, 'Format identifier-format = 1'), @@ -438,16 +445,17 @@ otData = [ # ('GSUB', [ - ('Version', 'Version', None, None, 'Version of the GSUB table-initially set to 0x00010000'), + ('Version', 'Version', None, None, 'Version of the GSUB table- 0x00010000 or 0x00010001'), ('Offset', 'ScriptList', None, None, 'Offset to ScriptList table-from beginning of GSUB table'), ('Offset', 'FeatureList', None, None, 'Offset to FeatureList table-from beginning of GSUB table'), ('Offset', 'LookupList', None, None, 'Offset to LookupList table-from beginning of GSUB table'), + ('LOffset', 'FeatureVariations', None, 'Version >= 0x00010001', 'Offset to FeatureVariations table-from beginning of GSUB table'), ]), ('SingleSubstFormat1', [ ('uint16', 'SubstFormat', None, None, 'Format identifier-format = 1'), ('Offset', 'Coverage', None, None, 'Offset to Coverage table-from beginning of Substitution table'), - ('int16', 'DeltaGlyphID', None, None, 'Add to original GlyphID to get substitute GlyphID'), + ('uint16', 'DeltaGlyphID', None, None, 'Add to original GlyphID modulo 65536 to get substitute GlyphID'), ]), ('SingleSubstFormat2', [ @@ -634,12 +642,13 @@ otData = [ # ('GDEF', [ - ('Version', 'Version', None, None, 'Version of the GDEF table-initially 0x00010000'), + ('Version', 'Version', None, None, 'Version of the GDEF table- 0x00010000, 0x00010002, or 0x00010003'), ('Offset', 'GlyphClassDef', None, None, 'Offset to class definition table for glyph type-from beginning of GDEF header (may be NULL)'), ('Offset', 'AttachList', None, None, 'Offset to list of glyphs with attachment points-from beginning of GDEF header (may be NULL)'), ('Offset', 'LigCaretList', None, None, 'Offset to list of positioning points for ligature carets-from beginning of GDEF header (may be NULL)'), ('Offset', 'MarkAttachClassDef', None, None, 'Offset to class definition table for mark attachment type-from beginning of GDEF header (may be NULL)'), - ('Offset', 'MarkGlyphSetsDef', None, 'int(round(Version*0x10000)) >= 0x00010002', 'Offset to the table of mark set definitions-from beginning of GDEF header (may be NULL)'), + ('Offset', 'MarkGlyphSetsDef', None, 'Version >= 0x00010002', 'Offset to the table of mark set definitions-from beginning of GDEF header (may be NULL)'), + ('LOffset', 'VarStore', None, 'Version >= 0x00010003', 'Offset to variation store (may be NULL)'), ]), ('AttachList', [ @@ -831,6 +840,193 @@ otData = [ ('Offset', 'Lookup', 'LookupCount', 0, 'Array of offsets to GPOS-type lookup tables-from beginning of JstfMax table-in design order'), ]), + + # + # STAT + # + ('STAT', [ + ('Version', 'Version', None, None, 'Version of the table-initially set to 0x00010000, currently 0x00010002.'), + ('uint16', 'DesignAxisRecordSize', None, None, 'Size in bytes of each design axis record'), + ('uint16', 'DesignAxisCount', None, None, 'Number of design axis records'), + ('LOffsetTo(AxisRecordArray)', 'DesignAxisRecord', None, None, 'Offset in bytes from the beginning of the STAT table to the start of the design axes array'), + ('uint16', 'AxisValueCount', None, None, 'Number of axis value tables'), + ('LOffsetTo(AxisValueArray)', 'AxisValueArray', None, None, 'Offset in bytes from the beginning of the STAT table to the start of the axes value offset array'), + ('NameID', 'ElidedFallbackNameID', None, 'Version >= 0x00010001', 'NameID to use when all style attributes are elided.'), + ]), + + ('AxisRecordArray', [ + ('AxisRecord', 'Axis', 'DesignAxisCount', 0, 'Axis records'), + ]), + + ('AxisRecord', [ + ('Tag', 'AxisTag', None, None, 'A tag identifying the axis of design variation'), + ('NameID', 'AxisNameID', None, None, 'The name ID for entries in the "name" table that provide a display string for this axis'), + ('uint16', 'AxisOrdering', None, None, 'A value that applications can use to determine primary sorting of face names, or for ordering of descriptors when composing family or face names'), + ('uint8', 'MoreBytes', 'DesignAxisRecordSize', -8, 'Extra bytes. Set to empty array.'), + ]), + + ('AxisValueArray', [ + ('Offset', 'AxisValue', 'AxisValueCount', 0, 'Axis values'), + ]), + + ('AxisValueFormat1', [ + ('uint16', 'Format', None, None, 'Format, = 1'), + ('uint16', 'AxisIndex', None, None, 'Index into the axis record array identifying the axis of design variation to which the axis value record applies.'), + ('uint16', 'Flags', None, None, 'Flags.'), + ('NameID', 'ValueNameID', None, None, ''), + ('Fixed', 'Value', None, None, ''), + ]), + + ('AxisValueFormat2', [ + ('uint16', 'Format', None, None, 'Format, = 2'), + ('uint16', 'AxisIndex', None, None, 'Index into the axis record array identifying the axis of design variation to which the axis value record applies.'), + ('uint16', 'Flags', None, None, 'Flags.'), + ('NameID', 'ValueNameID', None, None, ''), + ('Fixed', 'NominalValue', None, None, ''), + ('Fixed', 'RangeMinValue', None, None, ''), + ('Fixed', 'RangeMaxValue', None, None, ''), + ]), + + ('AxisValueFormat3', [ + ('uint16', 'Format', None, None, 'Format, = 3'), + ('uint16', 'AxisIndex', None, None, 'Index into the axis record array identifying the axis of design variation to which the axis value record applies.'), + ('uint16', 'Flags', None, None, 'Flags.'), + ('NameID', 'ValueNameID', None, None, ''), + ('Fixed', 'Value', None, None, ''), + ('Fixed', 'LinkedValue', None, None, ''), + ]), + + ('AxisValueFormat4', [ + ('uint16', 'Format', None, None, 'Format, = 4'), + ('uint16', 'AxisCount', None, None, 'The total number of axes contributing to this axis-values combination.'), + ('uint16', 'Flags', None, None, 'Flags.'), + ('NameID', 'ValueNameID', None, None, ''), + ('struct', 'AxisValueRecord', 'AxisCount', 0, 'Array of AxisValue records that provide the combination of axis values, one for each contributing axis. '), + ]), + + ('AxisValueRecord', [ + ('uint16', 'AxisIndex', None, None, 'Index into the axis record array identifying the axis of design variation to which the axis value record applies.'), + ('Fixed', 'Value', None, None, 'A numeric value for this attribute value.'), + ]), + + + # + # Variation fonts + # + + # GSUB/GPOS FeatureVariations + + ('FeatureVariations', [ + ('Version', 'Version', None, None, 'Version of the table-initially set to 0x00010000'), + ('uint32', 'FeatureVariationCount', None, None, 'Number of records in the FeatureVariationRecord array'), + ('struct', 'FeatureVariationRecord', 'FeatureVariationCount', 0, 'Array of FeatureVariationRecord'), + ]), + + ('FeatureVariationRecord', [ + ('LOffset', 'ConditionSet', None, None, 'Offset to a ConditionSet table, from beginning of the FeatureVariations table.'), + ('LOffset', 'FeatureTableSubstitution', None, None, 'Offset to a FeatureTableSubstitution table, from beginning of the FeatureVariations table'), + ]), + + ('ConditionSet', [ + ('uint16', 'ConditionCount', None, None, 'Number of condition tables in the ConditionTable array'), + ('LOffset', 'ConditionTable', 'ConditionCount', 0, 'Array of condition tables.'), + ]), + + ('ConditionTableFormat1', [ + ('uint16', 'Format', None, None, 'Format, = 1'), + ('uint16', 'AxisIndex', None, None, 'Index for the variation axis within the fvar table, base 0.'), + ('F2Dot14', 'FilterRangeMinValue', None, None, 'Minimum normalized axis value of the font variation instances that satisfy this condition.'), + ('F2Dot14', 'FilterRangeMaxValue', None, None, 'Maximum value that satisfies this condition.'), + ]), + + ('FeatureTableSubstitution', [ + ('Version', 'Version', None, None, 'Version of the table-initially set to 0x00010000'), + ('uint16', 'SubstitutionCount', None, None, 'Number of records in the FeatureVariationRecords array'), + ('FeatureTableSubstitutionRecord', 'SubstitutionRecord', 'SubstitutionCount', 0, 'Array of FeatureTableSubstitutionRecord'), + ]), + + ('FeatureTableSubstitutionRecord', [ + ('uint16', 'FeatureIndex', None, None, 'The feature table index to match.'), + ('LOffset', 'Feature', None, None, 'Offset to an alternate feature table, from start of the FeatureTableSubstitution table.'), + ]), + + # VariationStore + + ('VarRegionAxis', [ + ('F2Dot14', 'StartCoord', None, None, ''), + ('F2Dot14', 'PeakCoord', None, None, ''), + ('F2Dot14', 'EndCoord', None, None, ''), + ]), + + ('VarRegion', [ + ('struct', 'VarRegionAxis', 'RegionAxisCount', 0, ''), + ]), + + ('VarRegionList', [ + ('uint16', 'RegionAxisCount', None, None, ''), + ('uint16', 'RegionCount', None, None, ''), + ('VarRegion', 'Region', 'RegionCount', 0, ''), + ]), + + ('VarData', [ + ('uint16', 'ItemCount', None, None, ''), + ('uint16', 'NumShorts', None, None, ''), + ('uint16', 'VarRegionCount', None, None, ''), + ('uint16', 'VarRegionIndex', 'VarRegionCount', 0, ''), + ('VarDataValue', 'Item', 'ItemCount', 0, ''), + ]), + + ('VarStore', [ + ('uint16', 'Format', None, None, 'Set to 1.'), + ('LOffset', 'VarRegionList', None, None, ''), + ('uint16', 'VarDataCount', None, None, ''), + ('LOffset', 'VarData', 'VarDataCount', 0, ''), + ]), + + # Variation helpers + + ('VarIdxMap', [ + ('uint16', 'EntryFormat', None, None, ''), # Automatically computed + ('uint16', 'MappingCount', None, None, ''), # Automatically computed + ('VarIdxMapValue', 'mapping', '', 0, 'Array of compressed data'), + ]), + + # Glyph advance variations + + ('HVAR', [ + ('Version', 'Version', None, None, 'Version of the HVAR table-initially = 0x00010000'), + ('LOffset', 'VarStore', None, None, ''), + ('LOffsetTo(VarIdxMap)', 'AdvWidthMap', None, None, ''), + ('LOffsetTo(VarIdxMap)', 'LsbMap', None, None, ''), + ('LOffsetTo(VarIdxMap)', 'RsbMap', None, None, ''), + ]), + ('VVAR', [ + ('Version', 'Version', None, None, 'Version of the VVAR table-initially = 0x00010000'), + ('LOffset', 'VarStore', None, None, ''), + ('LOffsetTo(VarIdxMap)', 'AdvHeightMap', None, None, ''), + ('LOffsetTo(VarIdxMap)', 'TsbMap', None, None, ''), + ('LOffsetTo(VarIdxMap)', 'BsbMap', None, None, ''), + ('LOffsetTo(VarIdxMap)', 'VOrgMap', None, None, 'Vertical origin mapping.'), + ]), + + # Font-wide metrics variations + + ('MetricsValueRecord', [ + ('Tag', 'ValueTag', None, None, '4-byte font-wide measure identifier'), + ('uint32', 'VarIdx', None, None, 'Combined outer-inner variation index'), + ('uint8', 'MoreBytes', 'ValueRecordSize', -8, 'Extra bytes. Set to empty array.'), + ]), + + ('MVAR', [ + ('Version', 'Version', None, None, 'Version of the MVAR table-initially = 0x00010000'), + ('uint16', 'Reserved', None, None, 'Set to 0'), + ('uint16', 'ValueRecordSize', None, None, ''), + ('uint16', 'ValueRecordCount', None, None, ''), + ('Offset', 'VarStore', None, None, ''), + ('MetricsValueRecord', 'ValueRecord', 'ValueRecordCount', 0, ''), + ]), + + # # math # @@ -979,4 +1175,342 @@ otData = [ ('uint16', 'PartFlags', None, None, 'Part qualifiers. PartFlags enumeration currently uses only one bit: 0x0001 fExtender: If set, the part can be skipped or repeated. 0xFFFE Reserved'), ]), + + ## + ## Apple Advanced Typography (AAT) tables + ## + + ('AATLookupSegment', [ + ('uint16', 'lastGlyph', None, None, 'Last glyph index in this segment.'), + ('uint16', 'firstGlyph', None, None, 'First glyph index in this segment.'), + ('uint16', 'value', None, None, 'A 16-bit offset from the start of the table to the data.'), + ]), + + + # + # ankr + # + + ('ankr', [ + ('struct', 'AnchorPoints', None, None, 'Anchor points table.'), + ]), + + ('AnchorPointsFormat0', [ + ('uint16', 'Format', None, None, 'Format of the anchor points table, = 0.'), + ('uint16', 'Flags', None, None, 'Flags. Currenty unused, set to zero.'), + ('AATLookupWithDataOffset(AnchorGlyphData)', 'Anchors', None, None, 'Table of with anchor overrides for each glyph.'), + ]), + + ('AnchorGlyphData', [ + ('uint32', 'AnchorPointCount', None, None, 'Number of anchor points for this glyph.'), + ('struct', 'AnchorPoint', 'AnchorPointCount', 0, 'Individual anchor points.'), + ]), + + ('AnchorPoint', [ + ('int16', 'XCoordinate', None, None, 'X coordinate of this anchor point.'), + ('int16', 'YCoordinate', None, None, 'Y coordinate of this anchor point.'), + ]), + + # + # bsln + # + + ('bsln', [ + ('Version', 'Version', None, None, 'Version number of the AAT baseline table (0x00010000 for the initial version).'), + ('struct', 'Baseline', None, None, 'Baseline table.'), + ]), + + ('BaselineFormat0', [ + ('uint16', 'Format', None, None, 'Format of the baseline table, = 0.'), + ('uint16', 'DefaultBaseline', None, None, 'Default baseline value for all glyphs. This value can be from 0 through 31.'), + ('uint16', 'Delta', 32, 0, u'These are the FUnit distance deltas from the font’s natural baseline to the other baselines used in the font. A total of 32 deltas must be assigned.'), + ]), + + ('BaselineFormat1', [ + ('uint16', 'Format', None, None, 'Format of the baseline table, = 1.'), + ('uint16', 'DefaultBaseline', None, None, 'Default baseline value for all glyphs. This value can be from 0 through 31.'), + ('uint16', 'Delta', 32, 0, u'These are the FUnit distance deltas from the font’s natural baseline to the other baselines used in the font. A total of 32 deltas must be assigned.'), + ('AATLookup(uint16)', 'BaselineValues', None, None, 'Lookup table that maps glyphs to their baseline values.'), + ]), + + ('BaselineFormat2', [ + ('uint16', 'Format', None, None, 'Format of the baseline table, = 1.'), + ('uint16', 'DefaultBaseline', None, None, 'Default baseline value for all glyphs. This value can be from 0 through 31.'), + ('GlyphID', 'StandardGlyph', None, None, 'Glyph index of the glyph in this font to be used to set the baseline values. This glyph must contain a set of control points (whose numbers are contained in the following field) that determines baseline distances.'), + ('uint16', 'ControlPoint', 32, 0, 'Array of 32 control point numbers, associated with the standard glyph. A value of 0xFFFF means there is no corresponding control point in the standard glyph.'), + ]), + + ('BaselineFormat3', [ + ('uint16', 'Format', None, None, 'Format of the baseline table, = 1.'), + ('uint16', 'DefaultBaseline', None, None, 'Default baseline value for all glyphs. This value can be from 0 through 31.'), + ('GlyphID', 'StandardGlyph', None, None, 'Glyph index of the glyph in this font to be used to set the baseline values. This glyph must contain a set of control points (whose numbers are contained in the following field) that determines baseline distances.'), + ('uint16', 'ControlPoint', 32, 0, 'Array of 32 control point numbers, associated with the standard glyph. A value of 0xFFFF means there is no corresponding control point in the standard glyph.'), + ('AATLookup(uint16)', 'BaselineValues', None, None, 'Lookup table that maps glyphs to their baseline values.'), + ]), + + + # + # cidg + # + + ('cidg', [ + ('struct', 'CIDGlyphMapping', None, None, 'CID-to-glyph mapping table.'), + ]), + + ('CIDGlyphMappingFormat0', [ + ('uint16', 'Format', None, None, 'Format of the CID-to-glyph mapping table, = 0.'), + ('uint16', 'DataFormat', None, None, 'Currenty unused, set to zero.'), + ('uint32', 'StructLength', None, None, 'Size of the table in bytes.'), + ('uint16', 'Registry', None, None, 'The registry ID.'), + ('char64', 'RegistryName', None, None, 'The registry name in ASCII; unused bytes should be set to 0.'), + ('uint16', 'Order', None, None, 'The order ID.'), + ('char64', 'OrderName', None, None, 'The order name in ASCII; unused bytes should be set to 0.'), + ('uint16', 'SupplementVersion', None, None, 'The supplement version.'), + ('CIDGlyphMap', 'Mapping', None, None, 'A mapping from CIDs to the glyphs in the font, starting with CID 0. If a CID from the identified collection has no glyph in the font, 0xFFFF is used'), + ]), + + + # + # feat + # + + ('feat', [ + ('Version', 'Version', None, None, 'Version of the feat table-initially set to 0x00010000.'), + ('FeatureNames', 'FeatureNames', None, None, 'The feature names.'), + ]), + + ('FeatureNames', [ + ('uint16', 'FeatureNameCount', None, None, 'Number of entries in the feature name array.'), + ('uint16', 'Reserved1', None, None, 'Reserved (set to zero).'), + ('uint32', 'Reserved2', None, None, 'Reserved (set to zero).'), + ('FeatureName', 'FeatureName', 'FeatureNameCount', 0, 'The feature name array.'), + ]), + + ('FeatureName', [ + ('uint16', 'FeatureType', None, None, 'Feature type.'), + ('uint16', 'SettingsCount', None, None, 'The number of records in the setting name array.'), + ('LOffset', 'Settings', None, None, 'Offset to setting table for this feature.'), + ('uint16', 'FeatureFlags', None, None, 'Single-bit flags associated with the feature type.'), + ('NameID', 'FeatureNameID', None, None, 'The name table index for the feature name.'), + ]), + + ('Settings', [ + ('Setting', 'Setting', 'SettingsCount', 0, 'The setting array.'), + ]), + + ('Setting', [ + ('uint16', 'SettingValue', None, None, 'The setting.'), + ('NameID', 'SettingNameID', None, None, 'The name table index for the setting name.'), + ]), + + + # + # gcid + # + + ('gcid', [ + ('struct', 'GlyphCIDMapping', None, None, 'Glyph to CID mapping table.'), + ]), + + ('GlyphCIDMappingFormat0', [ + ('uint16', 'Format', None, None, 'Format of the glyph-to-CID mapping table, = 0.'), + ('uint16', 'DataFormat', None, None, 'Currenty unused, set to zero.'), + ('uint32', 'StructLength', None, None, 'Size of the table in bytes.'), + ('uint16', 'Registry', None, None, 'The registry ID.'), + ('char64', 'RegistryName', None, None, 'The registry name in ASCII; unused bytes should be set to 0.'), + ('uint16', 'Order', None, None, 'The order ID.'), + ('char64', 'OrderName', None, None, 'The order name in ASCII; unused bytes should be set to 0.'), + ('uint16', 'SupplementVersion', None, None, 'The supplement version.'), + ('GlyphCIDMap', 'Mapping', None, None, 'The CIDs for the glyphs in the font, starting with glyph 0. If a glyph does not correspond to a CID in the identified collection, 0xFFFF is used'), + ]), + + + # + # lcar + # + + ('lcar', [ + ('Version', 'Version', None, None, 'Version number of the ligature caret table (0x00010000 for the initial version).'), + ('struct', 'LigatureCarets', None, None, 'Ligature carets table.'), + ]), + + ('LigatureCaretsFormat0', [ + ('uint16', 'Format', None, None, 'Format of the ligature caret table. Format 0 indicates division points are distances in font units, Format 1 indicates division points are indexes of control points.'), + ('AATLookup(LigCaretDistances)', 'Carets', None, None, 'Lookup table associating ligature glyphs with their caret positions, in font unit distances.'), + ]), + + ('LigatureCaretsFormat1', [ + ('uint16', 'Format', None, None, 'Format of the ligature caret table. Format 0 indicates division points are distances in font units, Format 1 indicates division points are indexes of control points.'), + ('AATLookup(LigCaretPoints)', 'Carets', None, None, 'Lookup table associating ligature glyphs with their caret positions, as control points.'), + ]), + + ('LigCaretDistances', [ + ('uint16', 'DivsionPointCount', None, None, 'Number of division points.'), + ('int16', 'DivisionPoint', 'DivsionPointCount', 0, 'Distance in font units through which a subdivision is made orthogonally to the baseline.'), + ]), + + ('LigCaretPoints', [ + ('uint16', 'DivsionPointCount', None, None, 'Number of division points.'), + ('int16', 'DivisionPoint', 'DivsionPointCount', 0, 'The number of the control point through which a subdivision is made orthogonally to the baseline.'), + ]), + + + # + # mort + # + + ('mort', [ + ('Version', 'Version', None, None, 'Version of the mort table.'), + ('uint32', 'MorphChainCount', None, None, 'Number of metamorphosis chains.'), + ('MortChain', 'MorphChain', 'MorphChainCount', 0, 'Array of metamorphosis chains.'), + ]), + + ('MortChain', [ + ('Flags32', 'DefaultFlags', None, None, 'The default specification for subtables.'), + ('uint32', 'StructLength', None, None, 'Total byte count, including this header; must be a multiple of 4.'), + ('uint16', 'MorphFeatureCount', None, None, 'Number of metamorphosis feature entries.'), + ('uint16', 'MorphSubtableCount', None, None, 'The number of subtables in the chain.'), + ('struct', 'MorphFeature', 'MorphFeatureCount', 0, 'Array of metamorphosis features.'), + ('MortSubtable', 'MorphSubtable', 'MorphSubtableCount', 0, 'Array of metamorphosis subtables.'), + ]), + + ('MortSubtable', [ + ('uint16', 'StructLength', None, None, 'Total subtable length, including this header.'), + ('uint8', 'CoverageFlags', None, None, 'Most significant byte of coverage flags.'), + ('uint8', 'MorphType', None, None, 'Subtable type.'), + ('Flags32', 'SubFeatureFlags', None, None, 'The 32-bit mask identifying which subtable this is (the subtable being executed if the AND of this value and the processed defaultFlags is nonzero).'), + ('SubStruct', 'SubStruct', None, None, 'SubTable.'), + ]), + + # + # morx + # + + ('morx', [ + ('uint16', 'Version', None, None, 'Version of the morx table.'), + ('uint16', 'Reserved', None, None, 'Reserved (set to zero).'), + ('uint32', 'MorphChainCount', None, None, 'Number of extended metamorphosis chains.'), + ('MorxChain', 'MorphChain', 'MorphChainCount', 0, 'Array of extended metamorphosis chains.'), + ]), + + ('MorxChain', [ + ('Flags32', 'DefaultFlags', None, None, 'The default specification for subtables.'), + ('uint32', 'StructLength', None, None, 'Total byte count, including this header; must be a multiple of 4.'), + ('uint32', 'MorphFeatureCount', None, None, 'Number of feature subtable entries.'), + ('uint32', 'MorphSubtableCount', None, None, 'The number of subtables in the chain.'), + ('MorphFeature', 'MorphFeature', 'MorphFeatureCount', 0, 'Array of metamorphosis features.'), + ('MorxSubtable', 'MorphSubtable', 'MorphSubtableCount', 0, 'Array of extended metamorphosis subtables.'), + ]), + + ('MorphFeature', [ + ('uint16', 'FeatureType', None, None, 'The type of feature.'), + ('uint16', 'FeatureSetting', None, None, "The feature's setting (aka selector)."), + ('Flags32', 'EnableFlags', None, None, 'Flags for the settings that this feature and setting enables.'), + ('Flags32', 'DisableFlags', None, None, 'Complement of flags for the settings that this feature and setting disable.'), + ]), + + # Apple TrueType Reference Manual, chapter “The ‘morx’ table”, + # section “Metamorphosis Subtables”. + # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6morx.html + ('MorxSubtable', [ + ('uint32', 'StructLength', None, None, 'Total subtable length, including this header.'), + ('uint8', 'CoverageFlags', None, None, 'Most significant byte of coverage flags.'), + ('uint16', 'Reserved', None, None, 'Unused.'), + ('uint8', 'MorphType', None, None, 'Subtable type.'), + ('Flags32', 'SubFeatureFlags', None, None, 'The 32-bit mask identifying which subtable this is (the subtable being executed if the AND of this value and the processed defaultFlags is nonzero).'), + ('SubStruct', 'SubStruct', None, None, 'SubTable.'), + ]), + + ('StateHeader', [ + ('uint32', 'ClassCount', None, None, 'Number of classes, which is the number of 16-bit entry indices in a single line in the state array.'), + ('uint32', 'MorphClass', None, None, 'Offset from the start of this state table header to the start of the class table.'), + ('uint32', 'StateArrayOffset', None, None, 'Offset from the start of this state table header to the start of the state array.'), + ('uint32', 'EntryTableOffset', None, None, 'Offset from the start of this state table header to the start of the entry table.'), + ]), + + ('RearrangementMorph', [ + ('STXHeader(RearrangementMorphAction)', 'StateTable', None, None, 'Finite-state transducer table for indic rearrangement.'), + ]), + + ('ContextualMorph', [ + ('STXHeader(ContextualMorphAction)', 'StateTable', None, None, 'Finite-state transducer for contextual glyph substitution.'), + ]), + + ('LigatureMorph', [ + ('STXHeader(LigatureMorphAction)', 'StateTable', None, None, 'Finite-state transducer for ligature substitution.'), + ]), + + ('NoncontextualMorph', [ + ('AATLookup(GlyphID)', 'Substitution', None, None, 'The noncontextual glyph substitution table.'), + ]), + + ('InsertionMorph', [ + ('struct', 'StateHeader', None, None, 'Header.'), + # TODO: Add missing parts. + ]), + + ('MorphClass', [ + ('uint16', 'FirstGlyph', None, None, 'Glyph index of the first glyph in the class table.'), + #('uint16', 'GlyphCount', None, None, 'Number of glyphs in class table.'), + #('uint8', 'GlyphClass', 'GlyphCount', 0, 'The class codes (indexed by glyph index minus firstGlyph). Class codes range from 0 to the value of stateSize minus 1.'), + ]), + + # If the 'morx' table version is 3 or greater, then the last subtable in the chain is followed by a subtableGlyphCoverageArray, as described below. + # ('Offset', 'MarkGlyphSetsDef', None, 'round(Version*0x10000) >= 0x00010002', 'Offset to the table of mark set definitions-from beginning of GDEF header (may be NULL)'), + + + # + # prop + # + + ('prop', [ + ('Fixed', 'Version', None, None, 'Version number of the AAT glyphs property table. Version 1.0 is the initial table version. Version 2.0, which is recognized by macOS 8.5 and later, adds support for the “attaches on right” bit. Version 3.0, which gets recognized by macOS X and iOS, adds support for the additional directional properties defined in Unicode 3.0.'), + ('struct', 'GlyphProperties', None, None, 'Glyph properties.'), + ]), + + ('GlyphPropertiesFormat0', [ + ('uint16', 'Format', None, None, 'Format, = 0.'), + ('uint16', 'DefaultProperties', None, None, 'Default properties applied to a glyph. Since there is no lookup table in prop format 0, the default properties get applied to every glyph in the font.'), + ]), + + ('GlyphPropertiesFormat1', [ + ('uint16', 'Format', None, None, 'Format, = 1.'), + ('uint16', 'DefaultProperties', None, None, 'Default properties applied to a glyph if that glyph is not present in the Properties lookup table.'), + ('AATLookup(uint16)', 'Properties', None, None, 'Lookup data associating glyphs with their properties.'), + ]), + + + # + # opbd + # + + ('opbd', [ + ('Version', 'Version', None, None, 'Version number of the optical bounds table (0x00010000 for the initial version).'), + ('struct', 'OpticalBounds', None, None, 'Optical bounds table.'), + ]), + + ('OpticalBoundsFormat0', [ + ('uint16', 'Format', None, None, 'Format of the optical bounds table, = 0.'), + ('AATLookup(OpticalBoundsDeltas)', 'OpticalBoundsDeltas', None, None, 'Lookup table associating glyphs with their optical bounds, given as deltas in font units.'), + ]), + + ('OpticalBoundsFormat1', [ + ('uint16', 'Format', None, None, 'Format of the optical bounds table, = 1.'), + ('AATLookup(OpticalBoundsPoints)', 'OpticalBoundsPoints', None, None, 'Lookup table associating glyphs with their optical bounds, given as references to control points.'), + ]), + + ('OpticalBoundsDeltas', [ + ('int16', 'Left', None, None, 'Delta value for the left-side optical edge.'), + ('int16', 'Top', None, None, 'Delta value for the top-side optical edge.'), + ('int16', 'Right', None, None, 'Delta value for the right-side optical edge.'), + ('int16', 'Bottom', None, None, 'Delta value for the bottom-side optical edge.'), + ]), + + ('OpticalBoundsPoints', [ + ('int16', 'Left', None, None, 'Control point index for the left-side optical edge, or -1 if this glyph has none.'), + ('int16', 'Top', None, None, 'Control point index for the top-side optical edge, or -1 if this glyph has none.'), + ('int16', 'Right', None, None, 'Control point index for the right-side optical edge, or -1 if this glyph has none.'), + ('int16', 'Bottom', None, None, 'Control point index for the bottom-side optical edge, or -1 if this glyph has none.'), + ]), + ] diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index 2afc2cc..d00c233 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -1,18 +1,422 @@ +# coding: utf-8 """fontTools.ttLib.tables.otTables -- A collection of classes representing the various OpenType subtables. Most are constructed upon import from data in otData.py, all are populated with converter objects from otConverters.py. """ -from __future__ import print_function, division, absolute_import +from __future__ import print_function, division, absolute_import, unicode_literals from fontTools.misc.py23 import * -from .otBase import BaseTable, FormatSwitchingBaseTable +from fontTools.misc.textTools import safeEval +from .otBase import BaseTable, FormatSwitchingBaseTable, ValueRecord import operator -import warnings +import logging +import struct -class LookupOrder(BaseTable): - """Dummy class; this table isn't defined, but is used, and is always NULL.""" +log = logging.getLogger(__name__) + + +class AATStateTable(object): + def __init__(self): + self.GlyphClasses = {} # GlyphID --> GlyphClass + self.States = [] # List of AATState, indexed by state number + self.PerGlyphLookups = [] # [{GlyphID:GlyphID}, ...] + + +class AATState(object): + def __init__(self): + self.Transitions = {} # GlyphClass --> AATAction + + +class AATAction(object): + _FLAGS = None + + def _writeFlagsToXML(self, xmlWriter): + flags = [f for f in self._FLAGS if self.__dict__[f]] + if flags: + xmlWriter.simpletag("Flags", value=",".join(flags)) + xmlWriter.newline() + if self.ReservedFlags != 0: + xmlWriter.simpletag( + "ReservedFlags", + value='0x%04X' % self.ReservedFlags) + xmlWriter.newline() + + def _setFlag(self, flag): + assert flag in self._FLAGS, "unsupported flag %s" % flag + self.__dict__[flag] = True + + +class RearrangementMorphAction(AATAction): + staticSize = 4 + _FLAGS = ["MarkFirst", "DontAdvance", "MarkLast"] + + _VERBS = { + 0: "no change", + 1: "Ax ⇒ xA", + 2: "xD ⇒ Dx", + 3: "AxD ⇒ DxA", + 4: "ABx ⇒ xAB", + 5: "ABx ⇒ xBA", + 6: "xCD ⇒ CDx", + 7: "xCD ⇒ DCx", + 8: "AxCD ⇒ CDxA", + 9: "AxCD ⇒ DCxA", + 10: "ABxD ⇒ DxAB", + 11: "ABxD ⇒ DxBA", + 12: "ABxCD ⇒ CDxAB", + 13: "ABxCD ⇒ CDxBA", + 14: "ABxCD ⇒ DCxAB", + 15: "ABxCD ⇒ DCxBA", + } + + def __init__(self): + self.NewState = 0 + self.Verb = 0 + self.MarkFirst = False + self.DontAdvance = False + self.MarkLast = False + self.ReservedFlags = 0 + + def compile(self, writer, font, actionIndex): + assert actionIndex is None + writer.writeUShort(self.NewState) + assert self.Verb >= 0 and self.Verb <= 15, self.Verb + flags = self.Verb | self.ReservedFlags + if self.MarkFirst: flags |= 0x8000 + if self.DontAdvance: flags |= 0x4000 + if self.MarkLast: flags |= 0x2000 + writer.writeUShort(flags) + + def decompile(self, reader, font, actionReader): + assert actionReader is None + self.NewState = reader.readUShort() + flags = reader.readUShort() + self.Verb = flags & 0xF + self.MarkFirst = bool(flags & 0x8000) + self.DontAdvance = bool(flags & 0x4000) + self.MarkLast = bool(flags & 0x2000) + self.ReservedFlags = flags & 0x1FF0 + + def toXML(self, xmlWriter, font, attrs, name): + xmlWriter.begintag(name, **attrs) + xmlWriter.newline() + xmlWriter.simpletag("NewState", value=self.NewState) + xmlWriter.newline() + self._writeFlagsToXML(xmlWriter) + xmlWriter.simpletag("Verb", value=self.Verb) + verbComment = self._VERBS.get(self.Verb) + if verbComment is not None: + xmlWriter.comment(verbComment) + xmlWriter.newline() + xmlWriter.endtag(name) + xmlWriter.newline() + + def fromXML(self, name, attrs, content, font): + self.NewState = self.Verb = self.ReservedFlags = 0 + self.MarkFirst = self.DontAdvance = self.MarkLast = False + content = [t for t in content if isinstance(t, tuple)] + for eltName, eltAttrs, eltContent in content: + if eltName == "NewState": + self.NewState = safeEval(eltAttrs["value"]) + elif eltName == "Verb": + self.Verb = safeEval(eltAttrs["value"]) + elif eltName == "ReservedFlags": + self.ReservedFlags = safeEval(eltAttrs["value"]) + elif eltName == "Flags": + for flag in eltAttrs["value"].split(","): + self._setFlag(flag.strip()) + + +class ContextualMorphAction(AATAction): + staticSize = 8 + _FLAGS = ["SetMark", "DontAdvance"] + + def __init__(self): + self.NewState = 0 + self.SetMark, self.DontAdvance = False, False + self.ReservedFlags = 0 + self.MarkIndex, self.CurrentIndex = 0xFFFF, 0xFFFF + + def compile(self, writer, font, actionIndex): + assert actionIndex is None + writer.writeUShort(self.NewState) + flags = self.ReservedFlags + if self.SetMark: flags |= 0x8000 + if self.DontAdvance: flags |= 0x4000 + writer.writeUShort(flags) + writer.writeUShort(self.MarkIndex) + writer.writeUShort(self.CurrentIndex) + + def decompile(self, reader, font, actionReader): + assert actionReader is None + self.NewState = reader.readUShort() + flags = reader.readUShort() + self.SetMark = bool(flags & 0x8000) + self.DontAdvance = bool(flags & 0x4000) + self.ReservedFlags = flags & 0x3FFF + self.MarkIndex = reader.readUShort() + self.CurrentIndex = reader.readUShort() + + def toXML(self, xmlWriter, font, attrs, name): + xmlWriter.begintag(name, **attrs) + xmlWriter.newline() + xmlWriter.simpletag("NewState", value=self.NewState) + xmlWriter.newline() + self._writeFlagsToXML(xmlWriter) + xmlWriter.simpletag("MarkIndex", value=self.MarkIndex) + xmlWriter.newline() + xmlWriter.simpletag("CurrentIndex", + value=self.CurrentIndex) + xmlWriter.newline() + xmlWriter.endtag(name) + xmlWriter.newline() + + def fromXML(self, name, attrs, content, font): + self.NewState = self.ReservedFlags = 0 + self.SetMark = self.DontAdvance = False + self.MarkIndex, self.CurrentIndex = 0xFFFF, 0xFFFF + content = [t for t in content if isinstance(t, tuple)] + for eltName, eltAttrs, eltContent in content: + if eltName == "NewState": + self.NewState = safeEval(eltAttrs["value"]) + elif eltName == "Flags": + for flag in eltAttrs["value"].split(","): + self._setFlag(flag.strip()) + elif eltName == "ReservedFlags": + self.ReservedFlags = safeEval(eltAttrs["value"]) + elif eltName == "MarkIndex": + self.MarkIndex = safeEval(eltAttrs["value"]) + elif eltName == "CurrentIndex": + self.CurrentIndex = safeEval(eltAttrs["value"]) + + +class LigAction(object): + def __init__(self): + self.Store = False + # GlyphIndexDelta is a (possibly negative) delta that gets + # added to the glyph ID at the top of the AAT runtime + # execution stack. It is *not* a byte offset into the + # morx table. The result of the addition, which is performed + # at run time by the shaping engine, is an index into + # the ligature components table. See 'morx' specification. + # In the AAT specification, this field is called Offset; + # but its meaning is quite different from other offsets + # in either AAT or OpenType, so we use a different name. + self.GlyphIndexDelta = 0 + + +class LigatureMorphAction(AATAction): + staticSize = 6 + _FLAGS = ["SetComponent", "DontAdvance"] + + def __init__(self): + self.NewState = 0 + self.SetComponent, self.DontAdvance = False, False + self.ReservedFlags = 0 + self.Actions = [] + + def compile(self, writer, font, actionIndex): + assert actionIndex is not None + writer.writeUShort(self.NewState) + flags = self.ReservedFlags + if self.SetComponent: flags |= 0x8000 + if self.DontAdvance: flags |= 0x4000 + if len(self.Actions) > 0: flags |= 0x2000 + writer.writeUShort(flags) + if len(self.Actions) > 0: + actions = self.compileLigActions() + writer.writeUShort(actionIndex[actions]) + else: + writer.writeUShort(0) + + def decompile(self, reader, font, actionReader): + assert actionReader is not None + self.NewState = reader.readUShort() + flags = reader.readUShort() + self.SetComponent = bool(flags & 0x8000) + self.DontAdvance = bool(flags & 0x4000) + performAction = bool(flags & 0x2000) + # As of 2017-09-12, the 'morx' specification says that + # the reserved bitmask in ligature subtables is 0x3FFF. + # However, the specification also defines a flag 0x2000, + # so the reserved value should actually be 0x1FFF. + # TODO: Report this specification bug to Apple. + self.ReservedFlags = flags & 0x1FFF + actionIndex = reader.readUShort() + if performAction: + self.Actions = self._decompileLigActions( + actionReader, actionIndex) + else: + self.Actions = [] + + def compileLigActions(self): + result = [] + for i, action in enumerate(self.Actions): + last = (i == len(self.Actions) - 1) + value = action.GlyphIndexDelta & 0x3FFFFFFF + value |= 0x80000000 if last else 0 + value |= 0x40000000 if action.Store else 0 + result.append(struct.pack(">L", value)) + return bytesjoin(result) + + def _decompileLigActions(self, actionReader, actionIndex): + actions = [] + last = False + reader = actionReader.getSubReader( + actionReader.pos + actionIndex * 4) + while not last: + value = reader.readULong() + last = bool(value & 0x80000000) + action = LigAction() + actions.append(action) + action.Store = bool(value & 0x40000000) + delta = value & 0x3FFFFFFF + if delta >= 0x20000000: # sign-extend 30-bit value + delta = -0x40000000 + delta + action.GlyphIndexDelta = delta + return actions + + def fromXML(self, name, attrs, content, font): + self.NewState = self.ReservedFlags = 0 + self.SetComponent = self.DontAdvance = False + self.ReservedFlags = 0 + self.Actions = [] + content = [t for t in content if isinstance(t, tuple)] + for eltName, eltAttrs, eltContent in content: + if eltName == "NewState": + self.NewState = safeEval(eltAttrs["value"]) + elif eltName == "Flags": + for flag in eltAttrs["value"].split(","): + self._setFlag(flag.strip()) + elif eltName == "ReservedFlags": + self.ReservedFlags = safeEval(eltAttrs["value"]) + elif eltName == "Action": + action = LigAction() + flags = eltAttrs.get("Flags", "").split(",") + flags = [f.strip() for f in flags] + action.Store = "Store" in flags + action.GlyphIndexDelta = safeEval( + eltAttrs["GlyphIndexDelta"]) + self.Actions.append(action) + + def toXML(self, xmlWriter, font, attrs, name): + xmlWriter.begintag(name, **attrs) + xmlWriter.newline() + xmlWriter.simpletag("NewState", value=self.NewState) + xmlWriter.newline() + self._writeFlagsToXML(xmlWriter) + for action in self.Actions: + attribs = [("GlyphIndexDelta", action.GlyphIndexDelta)] + if action.Store: + attribs.append(("Flags", "Store")) + xmlWriter.simpletag("Action", attribs) + xmlWriter.newline() + xmlWriter.endtag(name) + xmlWriter.newline() + + +class InsertionMorphAction(AATAction): + staticSize = 8 + + _FLAGS = ["SetMark", "DontAdvance", + "CurrentIsKashidaLike", "MarkedIsKashidaLike", + "CurrentInsertBefore", "MarkedInsertBefore"] + + def __init__(self): + self.NewState = 0 + for flag in self._FLAGS: + setattr(self, flag, False) + self.ReservedFlags = 0 + self.CurrentInsertionAction, self.MarkedInsertionAction = [], [] + + def compile(self, writer, font, actionIndex): + assert actionIndex is not None + writer.writeUShort(self.NewState) + flags = self.ReservedFlags + if self.SetMark: flags |= 0x8000 + if self.DontAdvance: flags |= 0x4000 + if self.CurrentIsKashidaLike: flags |= 0x2000 + if self.MarkedIsKashidaLike: flags |= 0x1000 + if self.CurrentInsertBefore: flags |= 0x0800 + if self.MarkedInsertBefore: flags |= 0x0400 + flags |= len(self.CurrentInsertionAction) << 5 + flags |= len(self.MarkedInsertionAction) + writer.writeUShort(flags) + if len(self.CurrentInsertionAction) > 0: + currentIndex = actionIndex[ + tuple(self.CurrentInsertionAction)] + else: + currentIndex = 0xFFFF + writer.writeUShort(currentIndex) + if len(self.MarkedInsertionAction) > 0: + markedIndex = actionIndex[ + tuple(self.MarkedInsertionAction)] + else: + markedIndex = 0xFFFF + writer.writeUShort(markedIndex) + + def decompile(self, reader, font, actionReader): + assert actionReader is not None + self.NewState = reader.readUShort() + flags = reader.readUShort() + self.SetMark = bool(flags & 0x8000) + self.DontAdvance = bool(flags & 0x4000) + self.CurrentIsKashidaLike = bool(flags & 0x2000) + self.MarkedIsKashidaLike = bool(flags & 0x1000) + self.CurrentInsertBefore = bool(flags & 0x0800) + self.MarkedInsertBefore = bool(flags & 0x0400) + self.CurrentInsertionAction = self._decompileInsertionAction( + actionReader, font, + index=reader.readUShort(), + count=((flags & 0x03E0) >> 5)) + self.MarkedInsertionAction = self._decompileInsertionAction( + actionReader, font, + index=reader.readUShort(), + count=(flags & 0x001F)) + + def _decompileInsertionAction(self, actionReader, font, index, count): + if index == 0xFFFF or count == 0: + return [] + reader = actionReader.getSubReader( + actionReader.pos + index * 2) + return [font.getGlyphName(glyphID) + for glyphID in reader.readUShortArray(count)] + + def toXML(self, xmlWriter, font, attrs, name): + xmlWriter.begintag(name, **attrs) + xmlWriter.newline() + xmlWriter.simpletag("NewState", value=self.NewState) + xmlWriter.newline() + self._writeFlagsToXML(xmlWriter) + for g in self.CurrentInsertionAction: + xmlWriter.simpletag("CurrentInsertionAction", glyph=g) + xmlWriter.newline() + for g in self.MarkedInsertionAction: + xmlWriter.simpletag("MarkedInsertionAction", glyph=g) + xmlWriter.newline() + xmlWriter.endtag(name) + xmlWriter.newline() + + def fromXML(self, name, attrs, content, font): + self.__init__() + content = [t for t in content if isinstance(t, tuple)] + for eltName, eltAttrs, eltContent in content: + if eltName == "NewState": + self.NewState = safeEval(eltAttrs["value"]) + elif eltName == "Flags": + for flag in eltAttrs["value"].split(","): + self._setFlag(flag.strip()) + elif eltName == "CurrentInsertionAction": + self.CurrentInsertionAction.append( + eltAttrs["glyph"]) + elif eltName == "MarkedInsertionAction": + self.MarkedInsertionAction.append( + eltAttrs["glyph"]) + else: + assert False, eltName + class FeatureParams(BaseTable): @@ -33,9 +437,13 @@ class FeatureParamsCharacterVariants(FeatureParams): pass class Coverage(FormatSwitchingBaseTable): - + # manual implementation to get rid of glyphID dependencies - + + def populateDefaults(self, propagator=None): + if not hasattr(self, 'glyphs'): + self.glyphs = [] + def postRead(self, rawTable, font): if self.Format == 1: # TODO only allow glyphs that are valid? @@ -49,7 +457,7 @@ class Coverage(FormatSwitchingBaseTable): # this when writing font out. sorted_ranges = sorted(ranges, key=lambda a: a.StartCoverageIndex) if ranges != sorted_ranges: - warnings.warn("GSUB/GPOS Coverage is not sorted by glyph ids.") + log.warning("GSUB/GPOS Coverage is not sorted by glyph ids.") ranges = sorted_ranges del sorted_ranges for r in ranges: @@ -60,22 +468,22 @@ class Coverage(FormatSwitchingBaseTable): try: startID = font.getGlyphID(start, requireReal=True) except KeyError: - warnings.warn("Coverage table has start glyph ID out of range: %s." % start) + log.warning("Coverage table has start glyph ID out of range: %s.", start) continue try: endID = font.getGlyphID(end, requireReal=True) + 1 except KeyError: # Apparently some tools use 65535 to "match all" the range if end != 'glyph65535': - warnings.warn("Coverage table has end glyph ID out of range: %s." % end) + log.warning("Coverage table has end glyph ID out of range: %s.", end) # NOTE: We clobber out-of-range things here. There are legit uses for those, # but none that we have seen in the wild. endID = len(glyphOrder) glyphs.extend(glyphOrder[glyphID] for glyphID in range(startID, endID)) else: - assert 0, "unknown format: %s" % self.Format - del self.Format # Don't need this anymore - + self.glyphs = [] + log.warning("Unknown Coverage format: %s", self.Format) + def preWrite(self, font): glyphs = getattr(self, "glyphs", None) if glyphs is None: @@ -87,7 +495,7 @@ class Coverage(FormatSwitchingBaseTable): # find out whether Format 2 is more compact or not glyphIDs = [getGlyphID(glyphName) for glyphName in glyphs ] brokenOrder = sorted(glyphIDs) != glyphIDs - + last = glyphIDs[0] ranges = [[last]] for glyphID in glyphIDs[1:]: @@ -96,7 +504,7 @@ class Coverage(FormatSwitchingBaseTable): ranges.append([glyphID]) last = glyphID ranges[-1].append(last) - + if brokenOrder or len(ranges) * 3 < len(glyphs): # 3 words vs. 1 word # Format 2 is more compact index = 0 @@ -110,7 +518,7 @@ class Coverage(FormatSwitchingBaseTable): ranges[i] = r index = index + end - start + 1 if brokenOrder: - warnings.warn("GSUB/GPOS Coverage is not sorted by glyph ids.") + log.warning("GSUB/GPOS Coverage is not sorted by glyph ids.") ranges.sort(key=lambda a: a.StartID) for r in ranges: del r.StartID @@ -120,12 +528,12 @@ class Coverage(FormatSwitchingBaseTable): # fallthrough; Format 1 is more compact self.Format = format return rawTable - + def toXML2(self, xmlWriter, font): for glyphName in getattr(self, "glyphs", []): xmlWriter.simpletag("Glyph", value=glyphName) xmlWriter.newline() - + def fromXML(self, name, attrs, content, font): glyphs = getattr(self, "glyphs", None) if glyphs is None: @@ -134,13 +542,90 @@ class Coverage(FormatSwitchingBaseTable): glyphs.append(attrs["value"]) -def doModulo(value): - if value < 0: - return value + 65536 - return value +class VarIdxMap(BaseTable): + + def populateDefaults(self, propagator=None): + if not hasattr(self, 'mapping'): + self.mapping = {} + + def postRead(self, rawTable, font): + assert (rawTable['EntryFormat'] & 0xFFC0) == 0 + glyphOrder = font.getGlyphOrder() + mapList = rawTable['mapping'] + mapList.extend([mapList[-1]] * (len(glyphOrder) - len(mapList))) + self.mapping = dict(zip(glyphOrder, mapList)) + + def preWrite(self, font): + mapping = getattr(self, "mapping", None) + if mapping is None: + mapping = self.mapping = {} + + glyphOrder = font.getGlyphOrder() + mapping = [mapping[g] for g in glyphOrder] + while len(mapping) > 1 and mapping[-2] == mapping[-1]: + del mapping[-1] + + rawTable = { 'mapping': mapping } + rawTable['MappingCount'] = len(mapping) + + ored = 0 + for idx in mapping: + ored |= idx + + inner = ored & 0xFFFF + innerBits = 0 + while inner: + innerBits += 1 + inner >>= 1 + innerBits = max(innerBits, 1) + assert innerBits <= 16 + + ored = (ored >> (16-innerBits)) | (ored & ((1<<innerBits)-1)) + if ored <= 0x000000FF: + entrySize = 1 + elif ored <= 0x0000FFFF: + entrySize = 2 + elif ored <= 0x00FFFFFF: + entrySize = 3 + else: + entrySize = 4 + + entryFormat = ((entrySize - 1) << 4) | (innerBits - 1) + + rawTable['EntryFormat'] = entryFormat + return rawTable + + def toXML2(self, xmlWriter, font): + for glyph, value in sorted(getattr(self, "mapping", {}).items()): + attrs = ( + ('glyph', glyph), + ('outer', value >> 16), + ('inner', value & 0xFFFF), + ) + xmlWriter.simpletag("Map", attrs) + xmlWriter.newline() + + def fromXML(self, name, attrs, content, font): + mapping = getattr(self, "mapping", None) + if mapping is None: + mapping = {} + self.mapping = mapping + try: + glyph = attrs['glyph'] + except: # https://github.com/fonttools/fonttools/commit/21cbab8ce9ded3356fef3745122da64dcaf314e9#commitcomment-27649836 + glyph = font.getGlyphOrder()[attrs['index']] + outer = safeEval(attrs['outer']) + inner = safeEval(attrs['inner']) + assert inner <= 0xFFFF + mapping[glyph] = (outer << 16) | inner + class SingleSubst(FormatSwitchingBaseTable): + def populateDefaults(self, propagator=None): + if not hasattr(self, 'mapping'): + self.mapping = {} + def postRead(self, rawTable, font): mapping = {} input = _getGlyphsFromCoverageTable(rawTable["Coverage"]) @@ -148,8 +633,7 @@ class SingleSubst(FormatSwitchingBaseTable): if self.Format == 1: delta = rawTable["DeltaGlyphID"] inputGIDS = [ font.getGlyphID(name) for name in input ] - outGIDS = [ glyphID + delta for glyphID in inputGIDS ] - outGIDS = map(doModulo, outGIDS) + outGIDS = [ (glyphID + delta) % 65536 for glyphID in inputGIDS ] outNames = [ font.getGlyphName(glyphID) for glyphID in outGIDS ] list(map(operator.setitem, [mapping]*lenMapping, input, outNames)) elif self.Format == 2: @@ -160,8 +644,7 @@ class SingleSubst(FormatSwitchingBaseTable): else: assert 0, "unknown format: %s" % self.Format self.mapping = mapping - del self.Format # Don't need this anymore - + def preWrite(self, font): mapping = getattr(self, "mapping", None) if mapping is None: @@ -176,16 +659,16 @@ class SingleSubst(FormatSwitchingBaseTable): delta = None for inID, outID in gidItems: if delta is None: - delta = outID - inID - if delta < -32768: - delta += 65536 - elif delta > 32767: - delta -= 65536 - else: - if delta != outID - inID: + delta = (outID - inID) % 65536 + + if (inID + delta) % 65536 != outID: break else: - format = 1 + if delta is None: + # the mapping is empty, better use format 2 + format = 2 + else: + format = 1 rawTable = {} self.Format = format @@ -200,14 +683,14 @@ class SingleSubst(FormatSwitchingBaseTable): else: rawTable["Substitute"] = subst return rawTable - + def toXML2(self, xmlWriter, font): items = sorted(self.mapping.items()) for inGlyph, outGlyph in items: xmlWriter.simpletag("Substitution", [("in", inGlyph), ("out", outGlyph)]) xmlWriter.newline() - + def fromXML(self, name, attrs, content, font): mapping = getattr(self, "mapping", None) if mapping is None: @@ -216,8 +699,89 @@ class SingleSubst(FormatSwitchingBaseTable): mapping[attrs["in"]] = attrs["out"] +class MultipleSubst(FormatSwitchingBaseTable): + + def populateDefaults(self, propagator=None): + if not hasattr(self, 'mapping'): + self.mapping = {} + + def postRead(self, rawTable, font): + mapping = {} + if self.Format == 1: + glyphs = _getGlyphsFromCoverageTable(rawTable["Coverage"]) + subst = [s.Substitute for s in rawTable["Sequence"]] + mapping = dict(zip(glyphs, subst)) + else: + assert 0, "unknown format: %s" % self.Format + self.mapping = mapping + + def preWrite(self, font): + mapping = getattr(self, "mapping", None) + if mapping is None: + mapping = self.mapping = {} + cov = Coverage() + cov.glyphs = sorted(list(mapping.keys()), key=font.getGlyphID) + self.Format = 1 + rawTable = { + "Coverage": cov, + "Sequence": [self.makeSequence_(mapping[glyph]) + for glyph in cov.glyphs], + } + return rawTable + + def toXML2(self, xmlWriter, font): + items = sorted(self.mapping.items()) + for inGlyph, outGlyphs in items: + out = ",".join(outGlyphs) + xmlWriter.simpletag("Substitution", + [("in", inGlyph), ("out", out)]) + xmlWriter.newline() + + def fromXML(self, name, attrs, content, font): + mapping = getattr(self, "mapping", None) + if mapping is None: + mapping = {} + self.mapping = mapping + + # TTX v3.0 and earlier. + if name == "Coverage": + self.old_coverage_ = [] + for element in content: + if not isinstance(element, tuple): + continue + element_name, element_attrs, _ = element + if element_name == "Glyph": + self.old_coverage_.append(element_attrs["value"]) + return + if name == "Sequence": + index = int(attrs.get("index", len(mapping))) + glyph = self.old_coverage_[index] + glyph_mapping = mapping[glyph] = [] + for element in content: + if not isinstance(element, tuple): + continue + element_name, element_attrs, _ = element + if element_name == "Substitute": + glyph_mapping.append(element_attrs["value"]) + return + + # TTX v3.1 and later. + outGlyphs = attrs["out"].split(",") if attrs["out"] else [] + mapping[attrs["in"]] = [g.strip() for g in outGlyphs] + + @staticmethod + def makeSequence_(g): + seq = Sequence() + seq.Substitute = g + return seq + + class ClassDef(FormatSwitchingBaseTable): - + + def populateDefaults(self, propagator=None): + if not hasattr(self, 'classDefs'): + self.classDefs = {} + def postRead(self, rawTable, font): classDefs = {} glyphOrder = font.getGlyphOrder() @@ -228,17 +792,19 @@ class ClassDef(FormatSwitchingBaseTable): try: startID = font.getGlyphID(start, requireReal=True) except KeyError: - warnings.warn("ClassDef table has start glyph ID out of range: %s." % start) + log.warning("ClassDef table has start glyph ID out of range: %s.", start) startID = len(glyphOrder) endID = startID + len(classList) if endID > len(glyphOrder): - warnings.warn("ClassDef table has entries for out of range glyph IDs: %s,%s." % (start, len(classList))) + log.warning("ClassDef table has entries for out of range glyph IDs: %s,%s.", + start, len(classList)) # NOTE: We clobber out-of-range things here. There are legit uses for those, # but none that we have seen in the wild. endID = len(glyphOrder) for glyphID, cls in zip(range(startID, endID), classList): - classDefs[glyphOrder[glyphID]] = cls + if cls: + classDefs[glyphOrder[glyphID]] = cls elif self.Format == 2: records = rawTable["ClassRangeRecord"] @@ -249,37 +815,37 @@ class ClassDef(FormatSwitchingBaseTable): try: startID = font.getGlyphID(start, requireReal=True) except KeyError: - warnings.warn("ClassDef table has start glyph ID out of range: %s." % start) + log.warning("ClassDef table has start glyph ID out of range: %s.", start) continue try: endID = font.getGlyphID(end, requireReal=True) + 1 except KeyError: # Apparently some tools use 65535 to "match all" the range if end != 'glyph65535': - warnings.warn("ClassDef table has end glyph ID out of range: %s." % end) + log.warning("ClassDef table has end glyph ID out of range: %s.", end) # NOTE: We clobber out-of-range things here. There are legit uses for those, # but none that we have seen in the wild. endID = len(glyphOrder) for glyphID in range(startID, endID): - classDefs[glyphOrder[glyphID]] = cls + if cls: + classDefs[glyphOrder[glyphID]] = cls else: - assert 0, "unknown format: %s" % self.Format + log.warning("Unknown ClassDef format: %s", self.Format) self.classDefs = classDefs - del self.Format # Don't need this anymore - - def preWrite(self, font): + + def _getClassRanges(self, font): classDefs = getattr(self, "classDefs", None) if classDefs is None: - classDefs = self.classDefs = {} - items = list(classDefs.items()) - format = 2 - rawTable = {"ClassRangeRecord": []} + self.classDefs = {} + return getGlyphID = font.getGlyphID - for i in range(len(items)): - glyphName, cls = items[i] - items[i] = getGlyphID(glyphName), glyphName, cls - items.sort() + items = [] + for glyphName, cls in classDefs.items(): + if not cls: + continue + items.append((getGlyphID(glyphName), glyphName, cls)) if items: + items.sort() last, lastName, lastCls = items[0] ranges = [[lastCls, last, lastName]] for glyphID, glyphName, cls in items[1:]: @@ -290,7 +856,13 @@ class ClassDef(FormatSwitchingBaseTable): lastName = glyphName lastCls = cls ranges[-1].extend([last, lastName]) + return ranges + def preWrite(self, font): + format = 2 + rawTable = {"ClassRangeRecord": []} + ranges = self._getClassRanges(font) + if ranges: startGlyph = ranges[0][1] endGlyph = ranges[-1][3] glyphCount = endGlyph - startGlyph + 1 @@ -316,13 +888,13 @@ class ClassDef(FormatSwitchingBaseTable): rawTable = {"StartGlyph": startGlyphName, "ClassValueArray": classes} self.Format = format return rawTable - + def toXML2(self, xmlWriter, font): items = sorted(self.classDefs.items()) for glyphName, cls in items: xmlWriter.simpletag("ClassDef", [("glyph", glyphName), ("class", cls)]) xmlWriter.newline() - + def fromXML(self, name, attrs, content, font): classDefs = getattr(self, "classDefs", None) if classDefs is None: @@ -332,21 +904,23 @@ class ClassDef(FormatSwitchingBaseTable): class AlternateSubst(FormatSwitchingBaseTable): - + + def populateDefaults(self, propagator=None): + if not hasattr(self, 'alternates'): + self.alternates = {} + def postRead(self, rawTable, font): alternates = {} if self.Format == 1: input = _getGlyphsFromCoverageTable(rawTable["Coverage"]) alts = rawTable["AlternateSet"] - if len(input) != len(alts): - assert len(input) == len(alts) - for i in range(len(input)): - alternates[input[i]] = alts[i].Alternate + assert len(input) == len(alts) + for inp,alt in zip(input,alts): + alternates[inp] = alt.Alternate else: assert 0, "unknown format: %s" % self.Format self.alternates = alternates - del self.Format # Don't need this anymore - + def preWrite(self, font): self.Format = 1 alternates = getattr(self, "alternates", None) @@ -361,7 +935,7 @@ class AlternateSubst(FormatSwitchingBaseTable): cov.glyphs = [ item[1] for item in items] alternates = [] setList = [ item[-1] for item in items] - for set in setList: + for set in setList: alts = AlternateSet() alts.Alternate = set alternates.append(alts) @@ -370,9 +944,9 @@ class AlternateSubst(FormatSwitchingBaseTable): # Also useful in that when splitting a sub-table because of an offset overflow # I don't need to calculate the change in the subtable offset due to the change in the coverage table size. # Allows packing more rules in subtable. - self.sortCoverageLast = 1 + self.sortCoverageLast = 1 return {"Coverage": cov, "AlternateSet": alternates} - + def toXML2(self, xmlWriter, font): items = sorted(self.alternates.items()) for glyphName, alternates in items: @@ -383,7 +957,7 @@ class AlternateSubst(FormatSwitchingBaseTable): xmlWriter.newline() xmlWriter.endtag("AlternateSet") xmlWriter.newline() - + def fromXML(self, name, attrs, content, font): alternates = getattr(self, "alternates", None) if alternates is None: @@ -400,7 +974,11 @@ class AlternateSubst(FormatSwitchingBaseTable): class LigatureSubst(FormatSwitchingBaseTable): - + + def populateDefaults(self, propagator=None): + if not hasattr(self, 'ligatures'): + self.ligatures = {} + def postRead(self, rawTable, font): ligatures = {} if self.Format == 1: @@ -412,13 +990,27 @@ class LigatureSubst(FormatSwitchingBaseTable): else: assert 0, "unknown format: %s" % self.Format self.ligatures = ligatures - del self.Format # Don't need this anymore - + def preWrite(self, font): self.Format = 1 ligatures = getattr(self, "ligatures", None) if ligatures is None: ligatures = self.ligatures = {} + + if ligatures and isinstance(next(iter(ligatures)), tuple): + # New high-level API in v3.1 and later. Note that we just support compiling this + # for now. We don't load to this API, and don't do XML with it. + + # ligatures is map from components-sequence to lig-glyph + newLigatures = dict() + for comps,lig in sorted(ligatures.items(), key=lambda item: (-len(item[0]), item[0])): + ligature = Ligature() + ligature.Component = comps[1:] + ligature.CompCount = len(comps) + ligature.LigGlyph = lig + newLigatures.setdefault(comps[0], []).append(ligature) + ligatures = newLigatures + items = list(ligatures.items()) for i in range(len(items)): glyphName, set = items[i] @@ -438,9 +1030,9 @@ class LigatureSubst(FormatSwitchingBaseTable): # Useful in that when splitting a sub-table because of an offset overflow # I don't need to calculate the change in subtabl offset due to the coverage table size. # Allows packing more rules in subtable. - self.sortCoverageLast = 1 + self.sortCoverageLast = 1 return {"Coverage": cov, "LigatureSet": ligSets} - + def toXML2(self, xmlWriter, font): items = sorted(self.ligatures.items()) for glyphName, ligSets in items: @@ -452,7 +1044,7 @@ class LigatureSubst(FormatSwitchingBaseTable): xmlWriter.newline() xmlWriter.endtag("LigatureSet") xmlWriter.newline() - + def fromXML(self, name, attrs, content, font): ligatures = getattr(self, "ligatures", None) if ligatures is None: @@ -467,11 +1059,11 @@ class LigatureSubst(FormatSwitchingBaseTable): name, attrs, content = element lig = Ligature() lig.LigGlyph = attrs["glyph"] - lig.Component = attrs["components"].split(",") + components = attrs["components"] + lig.Component = components.split(",") if components else [] ligs.append(lig) -# # For each subtable format there is a class. However, we don't really distinguish # between "field name" and "format name": often these are the same. Yet there's # a whole bunch of fields with different names. The following dict is a mapping @@ -512,7 +1104,7 @@ _equivalents = { def fixLookupOverFlows(ttf, overflowRecord): """ Either the offset from the LookupList to a lookup overflowed, or - an offset from a lookup to a subtable overflowed. + an offset from a lookup to a subtable overflowed. The table layout is: GPSO/GUSB Script List @@ -531,7 +1123,7 @@ def fixLookupOverFlows(ttf, overflowRecord): SubTable[n] and contents If the offset to a lookup overflowed (SubTableIndex is None) we must promote the *previous* lookup to an Extension type. - If the offset from a lookup to subtable overflowed, then we must promote it + If the offset from a lookup to subtable overflowed, then we must promote it to an Extension Lookup type. """ ok = 0 @@ -553,7 +1145,8 @@ def fixLookupOverFlows(ttf, overflowRecord): if lookupIndex < 0: return ok lookup = lookups[lookupIndex] - + + lookup.LookupType = extType for si in range(len(lookup.SubTable)): subTable = lookup.SubTable[si] extSubTableClass = lookupTypes[overflowRecord.tableType][extType] @@ -569,7 +1162,7 @@ def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord): newSubTable.Format = oldSubTable.Format if hasattr(oldSubTable, 'sortCoverageLast'): newSubTable.sortCoverageLast = oldSubTable.sortCoverageLast - + oldAlts = sorted(oldSubTable.alternates.items()) oldLen = len(oldAlts) @@ -579,10 +1172,10 @@ def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord): newLen = oldLen//2 elif overflowRecord.itemName == 'AlternateSet': - # We just need to back up by two items + # We just need to back up by two items # from the overflowed AlternateSet index to make sure the offset # to the Coverage table doesn't overflow. - newLen = overflowRecord.itemIndex - 1 + newLen = overflowRecord.itemIndex - 1 newSubTable.alternates = {} for i in range(newLen, oldLen): @@ -591,7 +1184,6 @@ def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord): newSubTable.alternates[key] = item[1] del oldSubTable.alternates[key] - return ok @@ -607,10 +1199,10 @@ def splitLigatureSubst(oldSubTable, newSubTable, overflowRecord): newLen = oldLen//2 elif overflowRecord.itemName == 'LigatureSet': - # We just need to back up by two items + # We just need to back up by two items # from the overflowed AlternateSet index to make sure the offset # to the Coverage table doesn't overflow. - newLen = overflowRecord.itemIndex - 1 + newLen = overflowRecord.itemIndex - 1 newSubTable.ligatures = {} for i in range(newLen, oldLen): @@ -622,6 +1214,73 @@ def splitLigatureSubst(oldSubTable, newSubTable, overflowRecord): return ok +def splitPairPos(oldSubTable, newSubTable, overflowRecord): + st = oldSubTable + ok = False + newSubTable.Format = oldSubTable.Format + if oldSubTable.Format == 1 and len(oldSubTable.PairSet) > 1: + for name in 'ValueFormat1', 'ValueFormat2': + setattr(newSubTable, name, getattr(oldSubTable, name)) + + # Move top half of coverage to new subtable + + newSubTable.Coverage = oldSubTable.Coverage.__class__() + + coverage = oldSubTable.Coverage.glyphs + records = oldSubTable.PairSet + + oldCount = len(oldSubTable.PairSet) // 2 + + oldSubTable.Coverage.glyphs = coverage[:oldCount] + oldSubTable.PairSet = records[:oldCount] + + newSubTable.Coverage.glyphs = coverage[oldCount:] + newSubTable.PairSet = records[oldCount:] + + oldSubTable.PairSetCount = len(oldSubTable.PairSet) + newSubTable.PairSetCount = len(newSubTable.PairSet) + + ok = True + + elif oldSubTable.Format == 2 and len(oldSubTable.Class1Record) > 1: + if not hasattr(oldSubTable, 'Class2Count'): + oldSubTable.Class2Count = len(oldSubTable.Class1Record[0].Class2Record) + for name in 'Class2Count', 'ClassDef2', 'ValueFormat1', 'ValueFormat2': + setattr(newSubTable, name, getattr(oldSubTable, name)) + + # The two subtables will still have the same ClassDef2 and the table + # sharing will still cause the sharing to overflow. As such, disable + # sharing on the one that is serialized second (that's oldSubTable). + oldSubTable.DontShare = True + + # Move top half of class numbers to new subtable + + newSubTable.Coverage = oldSubTable.Coverage.__class__() + newSubTable.ClassDef1 = oldSubTable.ClassDef1.__class__() + + coverage = oldSubTable.Coverage.glyphs + classDefs = oldSubTable.ClassDef1.classDefs + records = oldSubTable.Class1Record + + oldCount = len(oldSubTable.Class1Record) // 2 + newGlyphs = set(k for k,v in classDefs.items() if v >= oldCount) + + oldSubTable.Coverage.glyphs = [g for g in coverage if g not in newGlyphs] + oldSubTable.ClassDef1.classDefs = {k:v for k,v in classDefs.items() if v < oldCount} + oldSubTable.Class1Record = records[:oldCount] + + newSubTable.Coverage.glyphs = [g for g in coverage if g in newGlyphs] + newSubTable.ClassDef1.classDefs = {k:(v-oldCount) for k,v in classDefs.items() if v > oldCount} + newSubTable.Class1Record = records[oldCount:] + + oldSubTable.Class1Count = len(oldSubTable.Class1Record) + newSubTable.Class1Count = len(newSubTable.Class1Record) + + ok = True + + return ok + + splitTable = { 'GSUB': { # 1: splitSingleSubst, # 2: splitMultipleSubst, @@ -634,7 +1293,7 @@ splitTable = { 'GSUB': { }, 'GPOS': { # 1: splitSinglePos, -# 2: splitPairPos, + 2: splitPairPos, # 3: splitCursivePos, # 4: splitMarkBasePos, # 5: splitMarkLigPos, @@ -647,7 +1306,7 @@ splitTable = { 'GSUB': { } def fixSubTableOverFlows(ttf, overflowRecord): - """ + """ An offset has overflowed within a sub-table. We need to divide this subtable into smaller parts. """ ok = 0 @@ -656,6 +1315,11 @@ def fixSubTableOverFlows(ttf, overflowRecord): subIndex = overflowRecord.SubTableIndex subtable = lookup.SubTable[subIndex] + # First, try not sharing anything for this subtable... + if not hasattr(subtable, "DontShare"): + subtable.DontShare = True + return True + if hasattr(subtable, 'ExtSubTable'): # We split the subtable of the Extension table, and add a new Extension table # to contain the new subtable. @@ -663,7 +1327,7 @@ def fixSubTableOverFlows(ttf, overflowRecord): subTableType = subtable.ExtSubTable.__class__.LookupType extSubTable = subtable subtable = extSubTable.ExtSubTable - newExtSubTableClass = lookupTypes[overflowRecord.tableType][subtable.__class__.LookupType] + newExtSubTableClass = lookupTypes[overflowRecord.tableType][extSubTable.__class__.LookupType] newExtSubTable = newExtSubTableClass() newExtSubTable.Format = extSubTable.Format lookup.SubTable.insert(subIndex + 1, newExtSubTable) @@ -694,10 +1358,10 @@ def fixSubTableOverFlows(ttf, overflowRecord): def _buildClasses(): import re from .otData import otData - + formatPat = re.compile("([A-Za-z0-9]+)Format(\d+)$") namespace = globals() - + # populate module with classes for name, table in otData: baseClass = BaseTable @@ -709,13 +1373,15 @@ def _buildClasses(): if name not in namespace: # the class doesn't exist yet, so the base implementation is used. cls = type(name, (baseClass,), {}) + if name in ('GSUB', 'GPOS'): + cls.DontShare = True namespace[name] = cls - + for base, alts in _equivalents.items(): base = namespace[base] for alt in alts: - namespace[alt] = type(alt, (base,), {}) - + namespace[alt] = base + global lookupTypes lookupTypes = { 'GSUB': { @@ -739,6 +1405,17 @@ def _buildClasses(): 8: ChainContextPos, 9: ExtensionPos, }, + 'mort': { + 4: NoncontextualMorph, + }, + 'morx': { + 0: RearrangementMorph, + 1: ContextualMorph, + 2: LigatureMorph, + # 3: Reserved, + 4: NoncontextualMorph, + # 5: InsertionMorph, + }, } lookupTypes['JSTF'] = lookupTypes['GPOS'] # JSTF contains GPOS for lookupEnum in lookupTypes.values(): @@ -753,7 +1430,7 @@ def _buildClasses(): featureParamTypes['ss%02d' % i] = FeatureParamsStylisticSet for i in range(1, 99+1): featureParamTypes['cv%02d' % i] = FeatureParamsCharacterVariants - + # add converters to classes from .otConverters import buildConverters for name, table in otData: @@ -769,9 +1446,11 @@ def _buildClasses(): converters, convertersByName = buildConverters(table[1:], namespace) cls.converters[format] = converters cls.convertersByName[format] = convertersByName + # XXX Add staticSize? else: cls = namespace[name] cls.converters, cls.convertersByName = buildConverters(table, namespace) + # XXX Add staticSize? _buildClasses() diff --git a/Lib/fontTools/ttLib/tables/sbixBitmap.py b/Lib/fontTools/ttLib/tables/sbixBitmap.py deleted file mode 100644 index 96da4e1..0000000 --- a/Lib/fontTools/ttLib/tables/sbixBitmap.py +++ /dev/null @@ -1,104 +0,0 @@ -from __future__ import print_function, division, absolute_import -from fontTools.misc.py23 import * -from fontTools.misc import sstruct -from fontTools.misc.textTools import readHex -import struct - - -sbixBitmapHeaderFormat = """ - > - usReserved1: H # 00 00 - usReserved2: H # 00 00 - imageFormatTag: 4s # e.g. "png " -""" - -sbixBitmapHeaderFormatSize = sstruct.calcsize(sbixBitmapHeaderFormat) - - -class Bitmap(object): - def __init__(self, glyphName=None, referenceGlyphName=None, usReserved1=0, usReserved2=0, imageFormatTag=None, imageData=None, rawdata=None, gid=0): - self.gid = gid - self.glyphName = glyphName - self.referenceGlyphName = referenceGlyphName - self.usReserved1 = usReserved1 - self.usReserved2 = usReserved2 - self.rawdata = rawdata - self.imageFormatTag = imageFormatTag - self.imageData = imageData - - def decompile(self, ttFont): - self.glyphName = ttFont.getGlyphName(self.gid) - if self.rawdata is None: - from fontTools import ttLib - raise ttLib.TTLibError("No table data to decompile") - if len(self.rawdata) > 0: - if len(self.rawdata) < sbixBitmapHeaderFormatSize: - from fontTools import ttLib - #print "Bitmap %i header too short: Expected %x, got %x." % (self.gid, sbixBitmapHeaderFormatSize, len(self.rawdata)) - raise ttLib.TTLibError("Bitmap header too short.") - - sstruct.unpack(sbixBitmapHeaderFormat, self.rawdata[:sbixBitmapHeaderFormatSize], self) - - if self.imageFormatTag == "dupe": - # bitmap is a reference to another glyph's bitmap - gid, = struct.unpack(">H", self.rawdata[sbixBitmapHeaderFormatSize:]) - self.referenceGlyphName = ttFont.getGlyphName(gid) - else: - self.imageData = self.rawdata[sbixBitmapHeaderFormatSize:] - self.referenceGlyphName = None - # clean up - del self.rawdata - del self.gid - - def compile(self, ttFont): - if self.glyphName is None: - from fontTools import ttLib - raise ttLib.TTLibError("Can't compile bitmap without glyph name") - # TODO: if ttFont has no maxp, cmap etc., ignore glyph names and compile by index? - # (needed if you just want to compile the sbix table on its own) - self.gid = struct.pack(">H", ttFont.getGlyphID(self.glyphName)) - if self.imageFormatTag is None: - self.rawdata = "" - else: - self.rawdata = sstruct.pack(sbixBitmapHeaderFormat, self) + self.imageData - - def toXML(self, xmlWriter, ttFont): - if self.imageFormatTag == None: - # TODO: ignore empty bitmaps? - # a bitmap entry is required for each glyph, - # but empty ones can be calculated at compile time - xmlWriter.simpletag("bitmap", glyphname=self.glyphName) - xmlWriter.newline() - return - xmlWriter.begintag("bitmap", format=self.imageFormatTag, glyphname=self.glyphName) - xmlWriter.newline() - #xmlWriter.simpletag("usReserved1", value=self.usReserved1) - #xmlWriter.newline() - #xmlWriter.simpletag("usReserved2", value=self.usReserved2) - #xmlWriter.newline() - if self.imageFormatTag == "dupe": - # format == "dupe" is apparently a reference to another glyph id. - xmlWriter.simpletag("ref", glyphname=self.referenceGlyphName) - else: - xmlWriter.begintag("hexdata") - xmlWriter.newline() - xmlWriter.dumphex(self.imageData) - xmlWriter.endtag("hexdata") - xmlWriter.newline() - xmlWriter.endtag("bitmap") - xmlWriter.newline() - - def fromXML(self, name, attrs, content, ttFont): - #if name in ["usReserved1", "usReserved2"]: - # setattr(self, name, int(attrs["value"])) - #elif - if name == "ref": - # bitmap is a "dupe", i.e. a reference to another bitmap. - # in this case imageData contains the glyph id of the reference glyph - # get glyph id from glyphname - self.imageData = struct.pack(">H", ttFont.getGlyphID(attrs["glyphname"])) - elif name == "hexdata": - self.imageData = readHex(content) - else: - from fontTools import ttLib - raise ttLib.TTLibError("can't handle '%s' element" % name) diff --git a/Lib/fontTools/ttLib/tables/sbixBitmapSet.py b/Lib/fontTools/ttLib/tables/sbixBitmapSet.py deleted file mode 100644 index b5786ec..0000000 --- a/Lib/fontTools/ttLib/tables/sbixBitmapSet.py +++ /dev/null @@ -1,138 +0,0 @@ -from __future__ import print_function, division, absolute_import -from fontTools.misc.py23 import * -from fontTools.misc import sstruct -from fontTools.misc.textTools import readHex -from .sbixBitmap import * -import struct - -sbixBitmapSetHeaderFormat = """ - > - size: H # 00 28 - resolution: H # 00 48 -""" - -sbixBitmapOffsetEntryFormat = """ - > - ulOffset: L # 00 00 07 E0 # Offset from start of first offset entry to each bitmap -""" - -sbixBitmapSetHeaderFormatSize = sstruct.calcsize(sbixBitmapSetHeaderFormat) -sbixBitmapOffsetEntryFormatSize = sstruct.calcsize(sbixBitmapOffsetEntryFormat) - - -class BitmapSet(object): - def __init__(self, rawdata=None, size=0, resolution=72): - self.data = rawdata - self.size = size - self.resolution = resolution - self.bitmaps = {} - - def decompile(self, ttFont): - if self.data is None: - from fontTools import ttLib - raise ttLib.TTLibError - if len(self.data) < sbixBitmapSetHeaderFormatSize: - from fontTools import ttLib - raise(ttLib.TTLibError, "BitmapSet header too short: Expected %x, got %x.") \ - % (sbixBitmapSetHeaderFormatSize, len(self.data)) - - # read BitmapSet header from raw data - sstruct.unpack(sbixBitmapSetHeaderFormat, self.data[:sbixBitmapSetHeaderFormatSize], self) - - # calculate number of bitmaps - firstBitmapOffset, = struct.unpack(">L", \ - self.data[sbixBitmapSetHeaderFormatSize : sbixBitmapSetHeaderFormatSize + sbixBitmapOffsetEntryFormatSize]) - self.numBitmaps = (firstBitmapOffset - sbixBitmapSetHeaderFormatSize) // sbixBitmapOffsetEntryFormatSize - 1 - # ^ -1 because there's one more offset than bitmaps - - # build offset list for single bitmap offsets - self.bitmapOffsets = [] - for i in range(self.numBitmaps + 1): # + 1 because there's one more offset than bitmaps - start = i * sbixBitmapOffsetEntryFormatSize + sbixBitmapSetHeaderFormatSize - myOffset, = struct.unpack(">L", self.data[start : start + sbixBitmapOffsetEntryFormatSize]) - self.bitmapOffsets.append(myOffset) - - # iterate through offset list and slice raw data into bitmaps - for i in range(self.numBitmaps): - myBitmap = Bitmap(rawdata=self.data[self.bitmapOffsets[i] : self.bitmapOffsets[i+1]], gid=i) - myBitmap.decompile(ttFont) - self.bitmaps[myBitmap.glyphName] = myBitmap - del self.bitmapOffsets - del self.data - - def compile(self, ttFont): - self.bitmapOffsets = "" - self.bitmapData = "" - - glyphOrder = ttFont.getGlyphOrder() - - # first bitmap starts right after the header - bitmapOffset = sbixBitmapSetHeaderFormatSize + sbixBitmapOffsetEntryFormatSize * (len(glyphOrder) + 1) - for glyphName in glyphOrder: - if glyphName in self.bitmaps: - # we have a bitmap for this glyph - myBitmap = self.bitmaps[glyphName] - else: - # must add empty bitmap for this glyph - myBitmap = Bitmap(glyphName=glyphName) - myBitmap.compile(ttFont) - myBitmap.ulOffset = bitmapOffset - self.bitmapData += myBitmap.rawdata - bitmapOffset += len(myBitmap.rawdata) - self.bitmapOffsets += sstruct.pack(sbixBitmapOffsetEntryFormat, myBitmap) - - # add last "offset", really the end address of the last bitmap - dummy = Bitmap() - dummy.ulOffset = bitmapOffset - self.bitmapOffsets += sstruct.pack(sbixBitmapOffsetEntryFormat, dummy) - - # bitmap sets are padded to 4 byte boundaries - dataLength = len(self.bitmapOffsets) + len(self.bitmapData) - if dataLength % 4 != 0: - padding = 4 - (dataLength % 4) - else: - padding = 0 - - # pack header - self.data = sstruct.pack(sbixBitmapSetHeaderFormat, self) - # add offset, image data and padding after header - self.data += self.bitmapOffsets + self.bitmapData + "\0" * padding - - def toXML(self, xmlWriter, ttFont): - xmlWriter.begintag("bitmapSet") - xmlWriter.newline() - xmlWriter.simpletag("size", value=self.size) - xmlWriter.newline() - xmlWriter.simpletag("resolution", value=self.resolution) - xmlWriter.newline() - glyphOrder = ttFont.getGlyphOrder() - for i in range(len(glyphOrder)): - if glyphOrder[i] in self.bitmaps: - self.bitmaps[glyphOrder[i]].toXML(xmlWriter, ttFont) - # TODO: what if there are more bitmaps than glyphs? - xmlWriter.endtag("bitmapSet") - xmlWriter.newline() - - def fromXML(self, name, attrs, content, ttFont): - if name in ["size", "resolution"]: - setattr(self, name, int(attrs["value"])) - elif name == "bitmap": - if "format" in attrs: - myFormat = attrs["format"] - else: - myFormat = None - if "glyphname" in attrs: - myGlyphName = attrs["glyphname"] - else: - from fontTools import ttLib - raise ttLib.TTLibError("Bitmap must have a glyph name.") - myBitmap = Bitmap(glyphName=myGlyphName, imageFormatTag=myFormat) - for element in content: - if isinstance(element, tuple): - name, attrs, content = element - myBitmap.fromXML(name, attrs, content, ttFont) - myBitmap.compile(ttFont) - self.bitmaps[myBitmap.glyphName] = myBitmap - else: - from fontTools import ttLib - raise ttLib.TTLibError("can't handle '%s' element" % name) diff --git a/Lib/fontTools/ttLib/tables/sbixGlyph.py b/Lib/fontTools/ttLib/tables/sbixGlyph.py new file mode 100644 index 0000000..c053908 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/sbixGlyph.py @@ -0,0 +1,119 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc import sstruct +from fontTools.misc.textTools import readHex, safeEval +import struct + + +sbixGlyphHeaderFormat = """ + > + originOffsetX: h # The x-value of the point in the glyph relative to its + # lower-left corner which corresponds to the origin of + # the glyph on the screen, that is the point on the + # baseline at the left edge of the glyph. + originOffsetY: h # The y-value of the point in the glyph relative to its + # lower-left corner which corresponds to the origin of + # the glyph on the screen, that is the point on the + # baseline at the left edge of the glyph. + graphicType: 4s # e.g. "png " +""" + +sbixGlyphHeaderFormatSize = sstruct.calcsize(sbixGlyphHeaderFormat) + + +class Glyph(object): + def __init__(self, glyphName=None, referenceGlyphName=None, originOffsetX=0, originOffsetY=0, graphicType=None, imageData=None, rawdata=None, gid=0): + self.gid = gid + self.glyphName = glyphName + self.referenceGlyphName = referenceGlyphName + self.originOffsetX = originOffsetX + self.originOffsetY = originOffsetY + self.rawdata = rawdata + self.graphicType = graphicType + self.imageData = imageData + + # fix self.graphicType if it is null terminated or too short + if self.graphicType is not None: + if self.graphicType[-1] == "\0": + self.graphicType = self.graphicType[:-1] + if len(self.graphicType) > 4: + from fontTools import ttLib + raise ttLib.TTLibError("Glyph.graphicType must not be longer than 4 characters.") + elif len(self.graphicType) < 4: + # pad with spaces + self.graphicType += " "[:(4 - len(self.graphicType))] + + def decompile(self, ttFont): + self.glyphName = ttFont.getGlyphName(self.gid) + if self.rawdata is None: + from fontTools import ttLib + raise ttLib.TTLibError("No table data to decompile") + if len(self.rawdata) > 0: + if len(self.rawdata) < sbixGlyphHeaderFormatSize: + from fontTools import ttLib + #print "Glyph %i header too short: Expected %x, got %x." % (self.gid, sbixGlyphHeaderFormatSize, len(self.rawdata)) + raise ttLib.TTLibError("Glyph header too short.") + + sstruct.unpack(sbixGlyphHeaderFormat, self.rawdata[:sbixGlyphHeaderFormatSize], self) + + if self.graphicType == "dupe": + # this glyph is a reference to another glyph's image data + gid, = struct.unpack(">H", self.rawdata[sbixGlyphHeaderFormatSize:]) + self.referenceGlyphName = ttFont.getGlyphName(gid) + else: + self.imageData = self.rawdata[sbixGlyphHeaderFormatSize:] + self.referenceGlyphName = None + # clean up + del self.rawdata + del self.gid + + def compile(self, ttFont): + if self.glyphName is None: + from fontTools import ttLib + raise ttLib.TTLibError("Can't compile Glyph without glyph name") + # TODO: if ttFont has no maxp, cmap etc., ignore glyph names and compile by index? + # (needed if you just want to compile the sbix table on its own) + self.gid = struct.pack(">H", ttFont.getGlyphID(self.glyphName)) + if self.graphicType is None: + self.rawdata = b"" + else: + self.rawdata = sstruct.pack(sbixGlyphHeaderFormat, self) + self.imageData + + def toXML(self, xmlWriter, ttFont): + if self.graphicType == None: + # TODO: ignore empty glyphs? + # a glyph data entry is required for each glyph, + # but empty ones can be calculated at compile time + xmlWriter.simpletag("glyph", name=self.glyphName) + xmlWriter.newline() + return + xmlWriter.begintag("glyph", + graphicType=self.graphicType, + name=self.glyphName, + originOffsetX=self.originOffsetX, + originOffsetY=self.originOffsetY, + ) + xmlWriter.newline() + if self.graphicType == "dupe": + # graphicType == "dupe" is a reference to another glyph id. + xmlWriter.simpletag("ref", glyphname=self.referenceGlyphName) + else: + xmlWriter.begintag("hexdata") + xmlWriter.newline() + xmlWriter.dumphex(self.imageData) + xmlWriter.endtag("hexdata") + xmlWriter.newline() + xmlWriter.endtag("glyph") + xmlWriter.newline() + + def fromXML(self, name, attrs, content, ttFont): + if name == "ref": + # glyph is a "dupe", i.e. a reference to another glyph's image data. + # in this case imageData contains the glyph id of the reference glyph + # get glyph id from glyphname + self.imageData = struct.pack(">H", ttFont.getGlyphID(safeEval("'''" + attrs["glyphname"] + "'''"))) + elif name == "hexdata": + self.imageData = readHex(content) + else: + from fontTools import ttLib + raise ttLib.TTLibError("can't handle '%s' element" % name) diff --git a/Lib/fontTools/ttLib/tables/sbixStrike.py b/Lib/fontTools/ttLib/tables/sbixStrike.py new file mode 100644 index 0000000..6c1ac77 --- /dev/null +++ b/Lib/fontTools/ttLib/tables/sbixStrike.py @@ -0,0 +1,150 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc import sstruct +from fontTools.misc.textTools import readHex +from .sbixGlyph import * +import struct + +sbixStrikeHeaderFormat = """ + > + ppem: H # The PPEM for which this strike was designed (e.g., 9, + # 12, 24) + resolution: H # The screen resolution (in dpi) for which this strike + # was designed (e.g., 72) +""" + +sbixGlyphDataOffsetFormat = """ + > + glyphDataOffset: L # Offset from the beginning of the strike data record + # to data for the individual glyph +""" + +sbixStrikeHeaderFormatSize = sstruct.calcsize(sbixStrikeHeaderFormat) +sbixGlyphDataOffsetFormatSize = sstruct.calcsize(sbixGlyphDataOffsetFormat) + + +class Strike(object): + def __init__(self, rawdata=None, ppem=0, resolution=72): + self.data = rawdata + self.ppem = ppem + self.resolution = resolution + self.glyphs = {} + + def decompile(self, ttFont): + if self.data is None: + from fontTools import ttLib + raise ttLib.TTLibError + if len(self.data) < sbixStrikeHeaderFormatSize: + from fontTools import ttLib + raise(ttLib.TTLibError, "Strike header too short: Expected %x, got %x.") \ + % (sbixStrikeHeaderFormatSize, len(self.data)) + + # read Strike header from raw data + sstruct.unpack(sbixStrikeHeaderFormat, self.data[:sbixStrikeHeaderFormatSize], self) + + # calculate number of glyphs + firstGlyphDataOffset, = struct.unpack(">L", \ + self.data[sbixStrikeHeaderFormatSize:sbixStrikeHeaderFormatSize + sbixGlyphDataOffsetFormatSize]) + self.numGlyphs = (firstGlyphDataOffset - sbixStrikeHeaderFormatSize) // sbixGlyphDataOffsetFormatSize - 1 + # ^ -1 because there's one more offset than glyphs + + # build offset list for single glyph data offsets + self.glyphDataOffsets = [] + for i in range(self.numGlyphs + 1): # + 1 because there's one more offset than glyphs + start = i * sbixGlyphDataOffsetFormatSize + sbixStrikeHeaderFormatSize + current_offset, = struct.unpack(">L", self.data[start:start + sbixGlyphDataOffsetFormatSize]) + self.glyphDataOffsets.append(current_offset) + + # iterate through offset list and slice raw data into glyph data records + for i in range(self.numGlyphs): + current_glyph = Glyph(rawdata=self.data[self.glyphDataOffsets[i]:self.glyphDataOffsets[i+1]], gid=i) + current_glyph.decompile(ttFont) + self.glyphs[current_glyph.glyphName] = current_glyph + del self.glyphDataOffsets + del self.numGlyphs + del self.data + + def compile(self, ttFont): + self.glyphDataOffsets = b"" + self.bitmapData = b"" + + glyphOrder = ttFont.getGlyphOrder() + + # first glyph starts right after the header + currentGlyphDataOffset = sbixStrikeHeaderFormatSize + sbixGlyphDataOffsetFormatSize * (len(glyphOrder) + 1) + for glyphName in glyphOrder: + if glyphName in self.glyphs: + # we have glyph data for this glyph + current_glyph = self.glyphs[glyphName] + else: + # must add empty glyph data record for this glyph + current_glyph = Glyph(glyphName=glyphName) + current_glyph.compile(ttFont) + current_glyph.glyphDataOffset = currentGlyphDataOffset + self.bitmapData += current_glyph.rawdata + currentGlyphDataOffset += len(current_glyph.rawdata) + self.glyphDataOffsets += sstruct.pack(sbixGlyphDataOffsetFormat, current_glyph) + + # add last "offset", really the end address of the last glyph data record + dummy = Glyph() + dummy.glyphDataOffset = currentGlyphDataOffset + self.glyphDataOffsets += sstruct.pack(sbixGlyphDataOffsetFormat, dummy) + + # pack header + self.data = sstruct.pack(sbixStrikeHeaderFormat, self) + # add offsets and image data after header + self.data += self.glyphDataOffsets + self.bitmapData + + def toXML(self, xmlWriter, ttFont): + xmlWriter.begintag("strike") + xmlWriter.newline() + xmlWriter.simpletag("ppem", value=self.ppem) + xmlWriter.newline() + xmlWriter.simpletag("resolution", value=self.resolution) + xmlWriter.newline() + glyphOrder = ttFont.getGlyphOrder() + for i in range(len(glyphOrder)): + if glyphOrder[i] in self.glyphs: + self.glyphs[glyphOrder[i]].toXML(xmlWriter, ttFont) + # TODO: what if there are more glyph data records than (glyf table) glyphs? + xmlWriter.endtag("strike") + xmlWriter.newline() + + def fromXML(self, name, attrs, content, ttFont): + if name in ["ppem", "resolution"]: + setattr(self, name, safeEval(attrs["value"])) + elif name == "glyph": + if "graphicType" in attrs: + myFormat = safeEval("'''" + attrs["graphicType"] + "'''") + else: + myFormat = None + if "glyphname" in attrs: + myGlyphName = safeEval("'''" + attrs["glyphname"] + "'''") + elif "name" in attrs: + myGlyphName = safeEval("'''" + attrs["name"] + "'''") + else: + from fontTools import ttLib + raise ttLib.TTLibError("Glyph must have a glyph name.") + if "originOffsetX" in attrs: + myOffsetX = safeEval(attrs["originOffsetX"]) + else: + myOffsetX = 0 + if "originOffsetY" in attrs: + myOffsetY = safeEval(attrs["originOffsetY"]) + else: + myOffsetY = 0 + current_glyph = Glyph( + glyphName=myGlyphName, + graphicType=myFormat, + originOffsetX=myOffsetX, + originOffsetY=myOffsetY, + ) + for element in content: + if isinstance(element, tuple): + name, attrs, content = element + current_glyph.fromXML(name, attrs, content, ttFont) + current_glyph.compile(ttFont) + self.glyphs[current_glyph.glyphName] = current_glyph + else: + from fontTools import ttLib + raise ttLib.TTLibError("can't handle '%s' element" % name) diff --git a/Lib/fontTools/ttLib/tables/ttProgram.py b/Lib/fontTools/ttLib/tables/ttProgram.py index 03f778e..182982f 100644 --- a/Lib/fontTools/ttLib/tables/ttProgram.py +++ b/Lib/fontTools/ttLib/tables/ttProgram.py @@ -5,146 +5,148 @@ from fontTools.misc.py23 import * from fontTools.misc.textTools import num2binary, binary2num, readHex import array import re +import logging + + +log = logging.getLogger(__name__) # first, the list of instructions that eat bytes or words from the instruction stream streamInstructions = [ -# ------ ----------- ----- ------------------------ --- ------ ---------------------------------- -------------- -# opcode mnemonic argBits descriptive name pops pushes eats from instruction stream pushes -# ------ ----------- ----- ------------------------ --- ------ ---------------------------------- -------------- - (0x40, 'NPUSHB', 0, 'PushNBytes', 0, -1), # n, b1, b2,...bn b1,b2...bn - (0x41, 'NPUSHW', 0, 'PushNWords', 0, -1), # n, w1, w2,...w w1,w2...wn - (0xb0, 'PUSHB', 3, 'PushBytes', 0, -1), # b0, b1,..bn b0, b1, ...,bn - (0xb8, 'PUSHW', 3, 'PushWords', 0, -1), # w0,w1,..wn w0 ,w1, ...wn -# ------ ----------- ----- ------------------------ --- ------ ---------------------------------- -------------- +# +# opcode mnemonic argBits descriptive name pops pushes eats from instruction stream pushes +# + (0x40, 'NPUSHB', 0, 'PushNBytes', 0, -1), # n, b1, b2,...bn b1,b2...bn + (0x41, 'NPUSHW', 0, 'PushNWords', 0, -1), # n, w1, w2,...w w1,w2...wn + (0xb0, 'PUSHB', 3, 'PushBytes', 0, -1), # b0, b1,..bn b0, b1, ...,bn + (0xb8, 'PUSHW', 3, 'PushWords', 0, -1), # w0,w1,..wn w0 ,w1, ...wn ] -# next, the list of "normal" instructions +# next, the list of "normal" instructions instructions = [ -# ------ ----------- ----- ------------------------ --- ------ ---------------------------------- -------------- -# opcode mnemonic argBits descriptive name pops pushes pops pushes -# ------ ----------- ----- ------------------------ --- ------ ---------------------------------- -------------- - (0x7f, 'AA', 0, 'AdjustAngle', 1, 0), # p - - (0x64, 'ABS', 0, 'Absolute', 1, 1), # n |n| - (0x60, 'ADD', 0, 'Add', 2, 1), # n2, n1 (n1 + n2) - (0x27, 'ALIGNPTS', 0, 'AlignPts', 2, 0), # p2, p1 - - (0x3c, 'ALIGNRP', 0, 'AlignRelativePt', -1, 0), # p1, p2, ... , ploopvalue - - (0x5a, 'AND', 0, 'LogicalAnd', 2, 1), # e2, e1 b - (0x2b, 'CALL', 0, 'CallFunction', 1, 0), # f - - (0x67, 'CEILING', 0, 'Ceiling', 1, 1), # n ceil(n) - (0x25, 'CINDEX', 0, 'CopyXToTopStack', 1, 1), # k ek - (0x22, 'CLEAR', 0, 'ClearStack', -1, 0), # all items on the stack - - (0x4f, 'DEBUG', 0, 'DebugCall', 1, 0), # n - - (0x73, 'DELTAC1', 0, 'DeltaExceptionC1', -1, 0), # argn, cn, argn-1,cn-1, , arg1, c1 - - (0x74, 'DELTAC2', 0, 'DeltaExceptionC2', -1, 0), # argn, cn, argn-1,cn-1, , arg1, c1 - - (0x75, 'DELTAC3', 0, 'DeltaExceptionC3', -1, 0), # argn, cn, argn-1,cn-1, , arg1, c1 - - (0x5d, 'DELTAP1', 0, 'DeltaExceptionP1', -1, 0), # argn, pn, argn-1, pn-1, , arg1, p1 - - (0x71, 'DELTAP2', 0, 'DeltaExceptionP2', -1, 0), # argn, pn, argn-1, pn-1, , arg1, p1 - - (0x72, 'DELTAP3', 0, 'DeltaExceptionP3', -1, 0), # argn, pn, argn-1, pn-1, , arg1, p1 - - (0x24, 'DEPTH', 0, 'GetDepthStack', 0, 1), # - n - (0x62, 'DIV', 0, 'Divide', 2, 1), # n2, n1 (n1 * 64)/ n2 - (0x20, 'DUP', 0, 'DuplicateTopStack', 1, 2), # e e, e - (0x59, 'EIF', 0, 'EndIf', 0, 0), # - - - (0x1b, 'ELSE', 0, 'Else', 0, 0), # - - - (0x2d, 'ENDF', 0, 'EndFunctionDefinition', 0, 0), # - - - (0x54, 'EQ', 0, 'Equal', 2, 1), # e2, e1 b - (0x57, 'EVEN', 0, 'Even', 1, 1), # e b - (0x2c, 'FDEF', 0, 'FunctionDefinition', 1, 0), # f - - (0x4e, 'FLIPOFF', 0, 'SetAutoFlipOff', 0, 0), # - - - (0x4d, 'FLIPON', 0, 'SetAutoFlipOn', 0, 0), # - - - (0x80, 'FLIPPT', 0, 'FlipPoint', -1, 0), # p1, p2, ..., ploopvalue - - (0x82, 'FLIPRGOFF', 0, 'FlipRangeOff', 2, 0), # h, l - - (0x81, 'FLIPRGON', 0, 'FlipRangeOn', 2, 0), # h, l - - (0x66, 'FLOOR', 0, 'Floor', 1, 1), # n floor(n) - (0x46, 'GC', 1, 'GetCoordOnPVector', 1, 1), # p c - (0x88, 'GETINFO', 0, 'GetInfo', 1, 1), # selector result - (0x0d, 'GFV', 0, 'GetFVector', 0, 2), # - px, py - (0x0c, 'GPV', 0, 'GetPVector', 0, 2), # - px, py - (0x52, 'GT', 0, 'GreaterThan', 2, 1), # e2, e1 b - (0x53, 'GTEQ', 0, 'GreaterThanOrEqual', 2, 1), # e2, e1 b - (0x89, 'IDEF', 0, 'InstructionDefinition', 1, 0), # f - - (0x58, 'IF', 0, 'If', 1, 0), # e - - (0x8e, 'INSTCTRL', 0, 'SetInstrExecControl', 2, 0), # s, v - - (0x39, 'IP', 0, 'InterpolatePts', -1, 0), # p1, p2, ... , ploopvalue - - (0x0f, 'ISECT', 0, 'MovePtToIntersect', 5, 0), # a1, a0, b1, b0, p - - (0x30, 'IUP', 1, 'InterpolateUntPts', 0, 0), # - - - (0x1c, 'JMPR', 0, 'Jump', 1, 0), # offset - - (0x79, 'JROF', 0, 'JumpRelativeOnFalse', 2, 0), # e, offset - - (0x78, 'JROT', 0, 'JumpRelativeOnTrue', 2, 0), # e, offset - - (0x2a, 'LOOPCALL', 0, 'LoopAndCallFunction', 2, 0), # f, count - - (0x50, 'LT', 0, 'LessThan', 2, 1), # e2, e1 b - (0x51, 'LTEQ', 0, 'LessThenOrEqual', 2, 1), # e2, e1 b - (0x8b, 'MAX', 0, 'Maximum', 2, 1), # e2, e1 max(e1, e2) - (0x49, 'MD', 1, 'MeasureDistance', 2, 1), # p2,p1 d - (0x2e, 'MDAP', 1, 'MoveDirectAbsPt', 1, 0), # p - - (0xc0, 'MDRP', 5, 'MoveDirectRelPt', 1, 0), # p - - (0x3e, 'MIAP', 1, 'MoveIndirectAbsPt', 2, 0), # n, p - - (0x8c, 'MIN', 0, 'Minimum', 2, 1), # e2, e1 min(e1, e2) - (0x26, 'MINDEX', 0, 'MoveXToTopStack', 1, 1), # k ek - (0xe0, 'MIRP', 5, 'MoveIndirectRelPt', 2, 0), # n, p - - (0x4b, 'MPPEM', 0, 'MeasurePixelPerEm', 0, 1), # - ppem - (0x4c, 'MPS', 0, 'MeasurePointSize', 0, 1), # - pointSize - (0x3a, 'MSIRP', 1, 'MoveStackIndirRelPt', 2, 0), # d, p - - (0x63, 'MUL', 0, 'Multiply', 2, 1), # n2, n1 (n1 * n2)/64 - (0x65, 'NEG', 0, 'Negate', 1, 1), # n -n - (0x55, 'NEQ', 0, 'NotEqual', 2, 1), # e2, e1 b - (0x5c, 'NOT', 0, 'LogicalNot', 1, 1), # e ( not e ) - (0x6c, 'NROUND', 2, 'NoRound', 1, 1), # n1 n2 - (0x56, 'ODD', 0, 'Odd', 1, 1), # e b - (0x5b, 'OR', 0, 'LogicalOr', 2, 1), # e2, e1 b - (0x21, 'POP', 0, 'PopTopStack', 1, 0), # e - - (0x45, 'RCVT', 0, 'ReadCVT', 1, 1), # location value - (0x7d, 'RDTG', 0, 'RoundDownToGrid', 0, 0), # - - - (0x7a, 'ROFF', 0, 'RoundOff', 0, 0), # - - - (0x8a, 'ROLL', 0, 'RollTopThreeStack', 3, 3), # a,b,c b,a,c - (0x68, 'ROUND', 2, 'Round', 1, 1), # n1 n2 - (0x43, 'RS', 0, 'ReadStore', 1, 1), # n v - (0x3d, 'RTDG', 0, 'RoundToDoubleGrid', 0, 0), # - - - (0x18, 'RTG', 0, 'RoundToGrid', 0, 0), # - - - (0x19, 'RTHG', 0, 'RoundToHalfGrid', 0, 0), # - - - (0x7c, 'RUTG', 0, 'RoundUpToGrid', 0, 0), # - - - (0x77, 'S45ROUND', 0, 'SuperRound45Degrees', 1, 0), # n - - (0x7e, 'SANGW', 0, 'SetAngleWeight', 1, 0), # weight - - (0x85, 'SCANCTRL', 0, 'ScanConversionControl', 1, 0), # n - - (0x8d, 'SCANTYPE', 0, 'ScanType', 1, 0), # n - - (0x48, 'SCFS', 0, 'SetCoordFromStackFP', 2, 0), # c, p - - (0x1d, 'SCVTCI', 0, 'SetCVTCutIn', 1, 0), # n - - (0x5e, 'SDB', 0, 'SetDeltaBaseInGState', 1, 0), # n - - (0x86, 'SDPVTL', 1, 'SetDualPVectorToLine', 2, 0), # p2, p1 - - (0x5f, 'SDS', 0, 'SetDeltaShiftInGState', 1, 0), # n - - (0x0b, 'SFVFS', 0, 'SetFVectorFromStack', 2, 0), # y, x - - (0x04, 'SFVTCA', 1, 'SetFVectorToAxis', 0, 0), # - - - (0x08, 'SFVTL', 1, 'SetFVectorToLine', 2, 0), # p2, p1 - - (0x0e, 'SFVTPV', 0, 'SetFVectorToPVector', 0, 0), # - - - (0x34, 'SHC', 1, 'ShiftContourByLastPt', 1, 0), # c - - (0x32, 'SHP', 1, 'ShiftPointByLastPoint', -1, 0), # p1, p2, ..., ploopvalue - - (0x38, 'SHPIX', 0, 'ShiftZoneByPixel', -1, 0), # d, p1, p2, ..., ploopvalue - - (0x36, 'SHZ', 1, 'ShiftZoneByLastPoint', 1, 0), # e - - (0x17, 'SLOOP', 0, 'SetLoopVariable', 1, 0), # n - - (0x1a, 'SMD', 0, 'SetMinimumDistance', 1, 0), # distance - - (0x0a, 'SPVFS', 0, 'SetPVectorFromStack', 2, 0), # y, x - - (0x02, 'SPVTCA', 1, 'SetPVectorToAxis', 0, 0), # - - - (0x06, 'SPVTL', 1, 'SetPVectorToLine', 2, 0), # p2, p1 - - (0x76, 'SROUND', 0, 'SuperRound', 1, 0), # n - - (0x10, 'SRP0', 0, 'SetRefPoint0', 1, 0), # p - - (0x11, 'SRP1', 0, 'SetRefPoint1', 1, 0), # p - - (0x12, 'SRP2', 0, 'SetRefPoint2', 1, 0), # p - - (0x1f, 'SSW', 0, 'SetSingleWidth', 1, 0), # n - - (0x1e, 'SSWCI', 0, 'SetSingleWidthCutIn', 1, 0), # n - - (0x61, 'SUB', 0, 'Subtract', 2, 1), # n2, n1 (n1 - n2) - (0x00, 'SVTCA', 1, 'SetFPVectorToAxis', 0, 0), # - - - (0x23, 'SWAP', 0, 'SwapTopStack', 2, 2), # e2, e1 e1, e2 - (0x13, 'SZP0', 0, 'SetZonePointer0', 1, 0), # n - - (0x14, 'SZP1', 0, 'SetZonePointer1', 1, 0), # n - - (0x15, 'SZP2', 0, 'SetZonePointer2', 1, 0), # n - - (0x16, 'SZPS', 0, 'SetZonePointerS', 1, 0), # n - - (0x29, 'UTP', 0, 'UnTouchPt', 1, 0), # p - - (0x70, 'WCVTF', 0, 'WriteCVTInFUnits', 2, 0), # n, l - - (0x44, 'WCVTP', 0, 'WriteCVTInPixels', 2, 0), # v, l - - (0x42, 'WS', 0, 'WriteStore', 2, 0), # v, l - -# ------ ----------- ----- ------------------------ --- ------ ---------------------------------- -------------- +# +#, opcode mnemonic argBits descriptive name pops pushes eats from instruction stream pushes +# + (0x7f, 'AA', 0, 'AdjustAngle', 1, 0), # p - + (0x64, 'ABS', 0, 'Absolute', 1, 1), # n |n| + (0x60, 'ADD', 0, 'Add', 2, 1), # n2, n1 (n1 + n2) + (0x27, 'ALIGNPTS', 0, 'AlignPts', 2, 0), # p2, p1 - + (0x3c, 'ALIGNRP', 0, 'AlignRelativePt', -1, 0), # p1, p2, ... , ploopvalue - + (0x5a, 'AND', 0, 'LogicalAnd', 2, 1), # e2, e1 b + (0x2b, 'CALL', 0, 'CallFunction', 1, 0), # f - + (0x67, 'CEILING', 0, 'Ceiling', 1, 1), # n ceil(n) + (0x25, 'CINDEX', 0, 'CopyXToTopStack', 1, 1), # k ek + (0x22, 'CLEAR', 0, 'ClearStack', -1, 0), # all items on the stack - + (0x4f, 'DEBUG', 0, 'DebugCall', 1, 0), # n - + (0x73, 'DELTAC1', 0, 'DeltaExceptionC1', -1, 0), # argn, cn, argn-1,cn-1, , arg1, c1 - + (0x74, 'DELTAC2', 0, 'DeltaExceptionC2', -1, 0), # argn, cn, argn-1,cn-1, , arg1, c1 - + (0x75, 'DELTAC3', 0, 'DeltaExceptionC3', -1, 0), # argn, cn, argn-1,cn-1, , arg1, c1 - + (0x5d, 'DELTAP1', 0, 'DeltaExceptionP1', -1, 0), # argn, pn, argn-1, pn-1, , arg1, p1 - + (0x71, 'DELTAP2', 0, 'DeltaExceptionP2', -1, 0), # argn, pn, argn-1, pn-1, , arg1, p1 - + (0x72, 'DELTAP3', 0, 'DeltaExceptionP3', -1, 0), # argn, pn, argn-1, pn-1, , arg1, p1 - + (0x24, 'DEPTH', 0, 'GetDepthStack', 0, 1), # - n + (0x62, 'DIV', 0, 'Divide', 2, 1), # n2, n1 (n1 * 64)/ n2 + (0x20, 'DUP', 0, 'DuplicateTopStack', 1, 2), # e e, e + (0x59, 'EIF', 0, 'EndIf', 0, 0), # - - + (0x1b, 'ELSE', 0, 'Else', 0, 0), # - - + (0x2d, 'ENDF', 0, 'EndFunctionDefinition', 0, 0), # - - + (0x54, 'EQ', 0, 'Equal', 2, 1), # e2, e1 b + (0x57, 'EVEN', 0, 'Even', 1, 1), # e b + (0x2c, 'FDEF', 0, 'FunctionDefinition', 1, 0), # f - + (0x4e, 'FLIPOFF', 0, 'SetAutoFlipOff', 0, 0), # - - + (0x4d, 'FLIPON', 0, 'SetAutoFlipOn', 0, 0), # - - + (0x80, 'FLIPPT', 0, 'FlipPoint', -1, 0), # p1, p2, ..., ploopvalue - + (0x82, 'FLIPRGOFF', 0, 'FlipRangeOff', 2, 0), # h, l - + (0x81, 'FLIPRGON', 0, 'FlipRangeOn', 2, 0), # h, l - + (0x66, 'FLOOR', 0, 'Floor', 1, 1), # n floor(n) + (0x46, 'GC', 1, 'GetCoordOnPVector', 1, 1), # p c + (0x88, 'GETINFO', 0, 'GetInfo', 1, 1), # selector result + (0x0d, 'GFV', 0, 'GetFVector', 0, 2), # - px, py + (0x0c, 'GPV', 0, 'GetPVector', 0, 2), # - px, py + (0x52, 'GT', 0, 'GreaterThan', 2, 1), # e2, e1 b + (0x53, 'GTEQ', 0, 'GreaterThanOrEqual', 2, 1), # e2, e1 b + (0x89, 'IDEF', 0, 'InstructionDefinition', 1, 0), # f - + (0x58, 'IF', 0, 'If', 1, 0), # e - + (0x8e, 'INSTCTRL', 0, 'SetInstrExecControl', 2, 0), # s, v - + (0x39, 'IP', 0, 'InterpolatePts', -1, 0), # p1, p2, ... , ploopvalue - + (0x0f, 'ISECT', 0, 'MovePtToIntersect', 5, 0), # a1, a0, b1, b0, p - + (0x30, 'IUP', 1, 'InterpolateUntPts', 0, 0), # - - + (0x1c, 'JMPR', 0, 'Jump', 1, 0), # offset - + (0x79, 'JROF', 0, 'JumpRelativeOnFalse', 2, 0), # e, offset - + (0x78, 'JROT', 0, 'JumpRelativeOnTrue', 2, 0), # e, offset - + (0x2a, 'LOOPCALL', 0, 'LoopAndCallFunction', 2, 0), # f, count - + (0x50, 'LT', 0, 'LessThan', 2, 1), # e2, e1 b + (0x51, 'LTEQ', 0, 'LessThenOrEqual', 2, 1), # e2, e1 b + (0x8b, 'MAX', 0, 'Maximum', 2, 1), # e2, e1 max(e1, e2) + (0x49, 'MD', 1, 'MeasureDistance', 2, 1), # p2,p1 d + (0x2e, 'MDAP', 1, 'MoveDirectAbsPt', 1, 0), # p - + (0xc0, 'MDRP', 5, 'MoveDirectRelPt', 1, 0), # p - + (0x3e, 'MIAP', 1, 'MoveIndirectAbsPt', 2, 0), # n, p - + (0x8c, 'MIN', 0, 'Minimum', 2, 1), # e2, e1 min(e1, e2) + (0x26, 'MINDEX', 0, 'MoveXToTopStack', 1, 1), # k ek + (0xe0, 'MIRP', 5, 'MoveIndirectRelPt', 2, 0), # n, p - + (0x4b, 'MPPEM', 0, 'MeasurePixelPerEm', 0, 1), # - ppem + (0x4c, 'MPS', 0, 'MeasurePointSize', 0, 1), # - pointSize + (0x3a, 'MSIRP', 1, 'MoveStackIndirRelPt', 2, 0), # d, p - + (0x63, 'MUL', 0, 'Multiply', 2, 1), # n2, n1 (n1 * n2)/64 + (0x65, 'NEG', 0, 'Negate', 1, 1), # n -n + (0x55, 'NEQ', 0, 'NotEqual', 2, 1), # e2, e1 b + (0x5c, 'NOT', 0, 'LogicalNot', 1, 1), # e ( not e ) + (0x6c, 'NROUND', 2, 'NoRound', 1, 1), # n1 n2 + (0x56, 'ODD', 0, 'Odd', 1, 1), # e b + (0x5b, 'OR', 0, 'LogicalOr', 2, 1), # e2, e1 b + (0x21, 'POP', 0, 'PopTopStack', 1, 0), # e - + (0x45, 'RCVT', 0, 'ReadCVT', 1, 1), # location value + (0x7d, 'RDTG', 0, 'RoundDownToGrid', 0, 0), # - - + (0x7a, 'ROFF', 0, 'RoundOff', 0, 0), # - - + (0x8a, 'ROLL', 0, 'RollTopThreeStack', 3, 3), # a,b,c b,a,c + (0x68, 'ROUND', 2, 'Round', 1, 1), # n1 n2 + (0x43, 'RS', 0, 'ReadStore', 1, 1), # n v + (0x3d, 'RTDG', 0, 'RoundToDoubleGrid', 0, 0), # - - + (0x18, 'RTG', 0, 'RoundToGrid', 0, 0), # - - + (0x19, 'RTHG', 0, 'RoundToHalfGrid', 0, 0), # - - + (0x7c, 'RUTG', 0, 'RoundUpToGrid', 0, 0), # - - + (0x77, 'S45ROUND', 0, 'SuperRound45Degrees', 1, 0), # n - + (0x7e, 'SANGW', 0, 'SetAngleWeight', 1, 0), # weight - + (0x85, 'SCANCTRL', 0, 'ScanConversionControl', 1, 0), # n - + (0x8d, 'SCANTYPE', 0, 'ScanType', 1, 0), # n - + (0x48, 'SCFS', 0, 'SetCoordFromStackFP', 2, 0), # c, p - + (0x1d, 'SCVTCI', 0, 'SetCVTCutIn', 1, 0), # n - + (0x5e, 'SDB', 0, 'SetDeltaBaseInGState', 1, 0), # n - + (0x86, 'SDPVTL', 1, 'SetDualPVectorToLine', 2, 0), # p2, p1 - + (0x5f, 'SDS', 0, 'SetDeltaShiftInGState',1, 0), # n - + (0x0b, 'SFVFS', 0, 'SetFVectorFromStack', 2, 0), # y, x - + (0x04, 'SFVTCA', 1, 'SetFVectorToAxis', 0, 0), # - - + (0x08, 'SFVTL', 1, 'SetFVectorToLine', 2, 0), # p2, p1 - + (0x0e, 'SFVTPV', 0, 'SetFVectorToPVector', 0, 0), # - - + (0x34, 'SHC', 1, 'ShiftContourByLastPt', 1, 0), # c - + (0x32, 'SHP', 1, 'ShiftPointByLastPoint',-1, 0), # p1, p2, ..., ploopvalue - + (0x38, 'SHPIX', 0, 'ShiftZoneByPixel', -1, 0), # d, p1, p2, ..., ploopvalue - + (0x36, 'SHZ', 1, 'ShiftZoneByLastPoint', 1, 0), # e - + (0x17, 'SLOOP', 0, 'SetLoopVariable', 1, 0), # n - + (0x1a, 'SMD', 0, 'SetMinimumDistance', 1, 0), # distance - + (0x0a, 'SPVFS', 0, 'SetPVectorFromStack', 2, 0), # y, x - + (0x02, 'SPVTCA', 1, 'SetPVectorToAxis', 0, 0), # - - + (0x06, 'SPVTL', 1, 'SetPVectorToLine', 2, 0), # p2, p1 - + (0x76, 'SROUND', 0, 'SuperRound', 1, 0), # n - + (0x10, 'SRP0', 0, 'SetRefPoint0', 1, 0), # p - + (0x11, 'SRP1', 0, 'SetRefPoint1', 1, 0), # p - + (0x12, 'SRP2', 0, 'SetRefPoint2', 1, 0), # p - + (0x1f, 'SSW', 0, 'SetSingleWidth', 1, 0), # n - + (0x1e, 'SSWCI', 0, 'SetSingleWidthCutIn', 1, 0), # n - + (0x61, 'SUB', 0, 'Subtract', 2, 1), # n2, n1 (n1 - n2) + (0x00, 'SVTCA', 1, 'SetFPVectorToAxis', 0, 0), # - - + (0x23, 'SWAP', 0, 'SwapTopStack', 2, 2), # e2, e1 e1, e2 + (0x13, 'SZP0', 0, 'SetZonePointer0', 1, 0), # n - + (0x14, 'SZP1', 0, 'SetZonePointer1', 1, 0), # n - + (0x15, 'SZP2', 0, 'SetZonePointer2', 1, 0), # n - + (0x16, 'SZPS', 0, 'SetZonePointerS', 1, 0), # n - + (0x29, 'UTP', 0, 'UnTouchPt', 1, 0), # p - + (0x70, 'WCVTF', 0, 'WriteCVTInFUnits', 2, 0), # n, l - + (0x44, 'WCVTP', 0, 'WriteCVTInPixels', 2, 0), # v, l - + (0x42, 'WS', 0, 'WriteStore', 2, 0), # v, l - ] @@ -163,13 +165,13 @@ def _makeDict(instructionList): mnemonicDict = {} for op, mnemonic, argBits, name, pops, pushes in instructionList: assert _mnemonicPat.match(mnemonic) - mnemonicDict[mnemonic] = op, argBits + mnemonicDict[mnemonic] = op, argBits, name if argBits: argoffset = op for i in range(1 << argBits): - opcodeDict[op+i] = mnemonic, argBits, argoffset + opcodeDict[op+i] = mnemonic, argBits, argoffset, name else: - opcodeDict[op] = mnemonic, 0, 0 + opcodeDict[op] = mnemonic, 0, 0, name return opcodeDict, mnemonicDict streamOpcodeDict, streamMnemonicDict = _makeDict(streamInstructions) @@ -190,8 +192,10 @@ _token = "(%s)|(%s)|(%s)" % (_instruction, _number, _comment) _tokenRE = re.compile(_token) _whiteRE = re.compile(r"\s*") -_pushCountPat = re.compile(r"[A-Z][A-Z0-9]*\s*\[.*?\]\s*/\* ([0-9]*).*?\*/") +_pushCountPat = re.compile(r"[A-Z][A-Z0-9]*\s*\[.*?\]\s*/\* ([0-9]+).*?\*/") +_indentRE = re.compile("^FDEF|IF|ELSE\[ \]\t.+") +_unindentRE = re.compile("^ELSE|ENDF|EIF\[ \]\t.+") def _skipWhite(data, pos): m = _whiteRE.match(data, pos) @@ -201,63 +205,94 @@ def _skipWhite(data, pos): class Program(object): - + def __init__(self): pass - + def fromBytecode(self, bytecode): self.bytecode = array.array("B", bytecode) if hasattr(self, "assembly"): del self.assembly - + def fromAssembly(self, assembly): self.assembly = assembly if hasattr(self, "bytecode"): del self.bytecode - + def getBytecode(self): if not hasattr(self, "bytecode"): self._assemble() return self.bytecode.tostring() - - def getAssembly(self): + + def getAssembly(self, preserve=True): if not hasattr(self, "assembly"): - self._disassemble() + self._disassemble(preserve=preserve) return self.assembly - + def toXML(self, writer, ttFont): if not hasattr (ttFont, "disassembleInstructions") or ttFont.disassembleInstructions: - assembly = self.getAssembly() - writer.begintag("assembly") - writer.newline() - i = 0 - nInstr = len(assembly) - while i < nInstr: - instr = assembly[i] - writer.write(instr) + try: + assembly = self.getAssembly() + except: + import traceback + tmp = StringIO() + traceback.print_exc(file=tmp) + msg = "An exception occurred during the decompilation of glyph program:\n\n" + msg += tmp.getvalue() + log.error(msg) + writer.begintag("bytecode") writer.newline() - m = _pushCountPat.match(instr) - i = i + 1 - if m: - nValues = int(m.group(1)) - line = [] - j = 0 - for j in range(nValues): - if j and not (j % 25): - writer.write(' '.join(line)) - writer.newline() - line = [] - line.append(assembly[i+j]) - writer.write(' '.join(line)) + writer.comment(msg.strip()) + writer.newline() + writer.dumphex(self.getBytecode()) + writer.endtag("bytecode") + writer.newline() + else: + if not assembly: + return + writer.begintag("assembly") + writer.newline() + i = 0 + indent = 0 + nInstr = len(assembly) + while i < nInstr: + instr = assembly[i] + if _unindentRE.match(instr): + indent -= 1 + writer.write(writer.indentwhite * indent) + writer.write(instr) writer.newline() - i = i + j + 1 - writer.endtag("assembly") + m = _pushCountPat.match(instr) + i = i + 1 + if m: + nValues = int(m.group(1)) + line = [] + j = 0 + for j in range(nValues): + if j and not (j % 25): + writer.write(writer.indentwhite * indent) + writer.write(' '.join(line)) + writer.newline() + line = [] + line.append(assembly[i+j]) + writer.write(writer.indentwhite * indent) + writer.write(' '.join(line)) + writer.newline() + i = i + j + 1 + if _indentRE.match(instr): + indent += 1 + writer.endtag("assembly") + writer.newline() else: + bytecode = self.getBytecode() + if not bytecode: + return writer.begintag("bytecode") writer.newline() - writer.dumphex(self.getBytecode()) + writer.dumphex(bytecode) writer.endtag("bytecode") - + writer.newline() + def fromXML(self, name, attrs, content, ttFont): if name == "assembly": self.fromAssembly(strjoin(content)) @@ -266,9 +301,9 @@ class Program(object): else: assert name == "bytecode" self.fromBytecode(readHex(content)) - + def _assemble(self): - assembly = self.assembly + assembly = getattr(self, 'assembly', []) if isinstance(assembly, type([])): assembly = ' '.join(assembly) bytecode = [] @@ -282,15 +317,16 @@ class Program(object): dummy, mnemonic, arg, number, comment = m.groups() pos = m.regs[0][1] if comment: + pos = _skipWhite(assembly, pos) continue - + arg = arg.strip() if mnemonic.startswith("INSTR"): # Unknown instruction op = int(mnemonic[5:]) push(op) elif mnemonic not in ("PUSH", "NPUSHB", "NPUSHW", "PUSHB", "PUSHW"): - op, argBits = mnemonicDict[mnemonic] + op, argBits, name = mnemonicDict[mnemonic] if len(arg) != argBits: raise tt_instructions_error("Incorrect number of argument bits (%s[%s])" % (mnemonic, arg)) if arg: @@ -331,11 +367,11 @@ class Program(object): # Write words if nWords: if nWords <= 8: - op, argBits = streamMnemonicDict["PUSHW"] + op, argBits, name = streamMnemonicDict["PUSHW"] op = op + nWords - 1 push(op) else: - op, argBits = streamMnemonicDict["NPUSHW"] + op, argBits, name = streamMnemonicDict["NPUSHW"] push(op) push(nWords) for value in args[:nWords]: @@ -347,11 +383,11 @@ class Program(object): if nBytes: pass if nBytes <= 8: - op, argBits = streamMnemonicDict["PUSHB"] + op, argBits, name = streamMnemonicDict["PUSHB"] op = op + nBytes - 1 push(op) else: - op, argBits = streamMnemonicDict["NPUSHB"] + op, argBits, name = streamMnemonicDict["NPUSHB"] push(op) push(nBytes) for value in args[nWords:nWords+nBytes]: @@ -364,7 +400,7 @@ class Program(object): else: # Write exactly what we've been asked to words = mnemonic[-1] == "W" - op, argBits = streamMnemonicDict[mnemonic] + op, argBits, name = streamMnemonicDict[mnemonic] if mnemonic[0] != "N": assert nArgs <= 8, nArgs op = op + nArgs - 1 @@ -384,20 +420,20 @@ class Program(object): push(value) pos = _skipWhite(assembly, pos) - + if bytecode: assert max(bytecode) < 256 and min(bytecode) >= 0 self.bytecode = array.array("B", bytecode) - + def _disassemble(self, preserve=False): assembly = [] i = 0 - bytecode = self.bytecode + bytecode = getattr(self, 'bytecode', []) numBytecode = len(bytecode) while i < numBytecode: op = bytecode[i] try: - mnemonic, argBits, argoffset = opcodeDict[op] + mnemonic, argBits, argoffset, name = opcodeDict[op] except KeyError: if op in streamOpcodeDict: values = [] @@ -405,7 +441,7 @@ class Program(object): # Merge consecutive PUSH operations while bytecode[i] in streamOpcodeDict: op = bytecode[i] - mnemonic, argBits, argoffset = streamOpcodeDict[op] + mnemonic, argBits, argoffset, name = streamOpcodeDict[op] words = mnemonic[-1] == "W" if argBits: nValues = op - argoffset + 1 @@ -434,28 +470,75 @@ class Program(object): mnemonic = "PUSH" nValues = len(values) if nValues == 1: - assembly.append("%s[ ]" % mnemonic) + assembly.append("%s[ ] /* 1 value pushed */" % mnemonic) else: - assembly.append("%s[ ] /* %s values pushed */" % (mnemonic, nValues)) + assembly.append("%s[ ] /* %s values pushed */" % (mnemonic, nValues)) assembly.extend(values) else: assembly.append("INSTR%d[ ]" % op) i = i + 1 else: if argBits: - assembly.append(mnemonic + "[%s]" % num2binary(op - argoffset, argBits)) + assembly.append(mnemonic + "[%s] /* %s */" % (num2binary(op - argoffset, argBits), name)) else: - assembly.append(mnemonic + "[ ]") + assembly.append(mnemonic + "[ ] /* %s */" % name) i = i + 1 self.assembly = assembly + def __bool__(self): + """ + >>> p = Program() + >>> bool(p) + False + >>> bc = array.array("B", [0]) + >>> p.fromBytecode(bc) + >>> bool(p) + True + >>> p.bytecode.pop() + 0 + >>> bool(p) + False + + >>> p = Program() + >>> asm = ['SVTCA[0]'] + >>> p.fromAssembly(asm) + >>> bool(p) + True + >>> p.assembly.pop() + 'SVTCA[0]' + >>> bool(p) + False + """ + return ((hasattr(self, 'assembly') and len(self.assembly) > 0) or + (hasattr(self, 'bytecode') and len(self.bytecode) > 0)) + + __nonzero__ = __bool__ + + def __eq__(self, other): + if type(self) != type(other): + return NotImplemented + return self.__dict__ == other.__dict__ + + def __ne__(self, other): + result = self.__eq__(other) + return result if result is NotImplemented else not result + + +def _test(): + """ + >>> _test() + True + """ + + bc = b"""@;:9876543210/.-,+*)(\'&%$#"! \037\036\035\034\033\032\031\030\027\026\025\024\023\022\021\020\017\016\015\014\013\012\011\010\007\006\005\004\003\002\001\000,\001\260\030CXEj\260\031C`\260F#D#\020 \260FN\360M/\260\000\022\033!#\0213Y-,\001\260\030CX\260\005+\260\000\023K\260\024PX\261\000@8Y\260\006+\033!#\0213Y-,\001\260\030CXN\260\003%\020\362!\260\000\022M\033 E\260\004%\260\004%#Jad\260(RX!#\020\326\033\260\003%\020\362!\260\000\022YY-,\260\032CX!!\033\260\002%\260\002%I\260\003%\260\003%Ja d\260\020PX!!!\033\260\003%\260\003%I\260\000PX\260\000PX\270\377\3428!\033\260\0208!Y\033\260\000RX\260\0368!\033\270\377\3608!YYYY-,\001\260\030CX\260\005+\260\000\023K\260\024PX\271\000\000\377\3008Y\260\006+\033!#\0213Y-,N\001\212\020\261F\031CD\260\000\024\261\000F\342\260\000\025\271\000\000\377\3608\000\260\000<\260(+\260\002%\020\260\000<-,\001\030\260\000/\260\001\024\362\260\001\023\260\001\025M\260\000\022-,\001\260\030CX\260\005+\260\000\023\271\000\000\377\3408\260\006+\033!#\0213Y-,\001\260\030CXEdj#Edi\260\031Cd``\260F#D#\020 \260F\360/\260\000\022\033!! \212 \212RX\0213\033!!YY-,\001\261\013\012C#Ce\012-,\000\261\012\013C#C\013-,\000\260F#p\261\001F>\001\260F#p\261\002FE:\261\002\000\010\015-,\260\022+\260\002%E\260\002%Ej\260@\213`\260\002%#D!!!-,\260\023+\260\002%E\260\002%Ej\270\377\300\214`\260\002%#D!!!-,\260\000\260\022+!!!-,\260\000\260\023+!!!-,\001\260\006C\260\007Ce\012-, i\260@a\260\000\213 \261,\300\212\214\270\020\000b`+\014d#da\\X\260\003aY-,\261\000\003%EhT\260\034KPZX\260\003%E\260\003%E`h \260\004%#D\260\004%#D\033\260\003% Eh \212#D\260\003%Eh`\260\003%#DY-,\260\003% Eh \212#D\260\003%Edhe`\260\004%\260\001`#D-,\260\011CX\207!\300\033\260\022CX\207E\260\021+\260G#D\260Gz\344\033\003\212E\030i \260G#D\212\212\207 \260\240QX\260\021+\260G#D\260Gz\344\033!\260Gz\344YYY\030-, \212E#Eh`D-,EjB-,\001\030/-,\001\260\030CX\260\004%\260\004%Id#Edi\260@\213a \260\200bj\260\002%\260\002%a\214\260\031C`\260F#D!\212\020\260F\366!\033!!!!Y-,\001\260\030CX\260\002%E\260\002%Ed`j\260\003%Eja \260\004%Ej \212\213e\260\004%#D\214\260\003%#D!!\033 EjD EjDY-,\001 E\260\000U\260\030CZXEh#Ei\260@\213a \260\200bj \212#a \260\003%\213e\260\004%#D\214\260\003%#D!!\033!!\260\031+Y-,\001\212\212Ed#EdadB-,\260\004%\260\004%\260\031+\260\030CX\260\004%\260\004%\260\003%\260\033+\001\260\002%C\260@T\260\002%C\260\000TZX\260\003% E\260@aDY\260\002%C\260\000T\260\002%C\260@TZX\260\004% E\260@`DYY!!!!-,\001KRXC\260\002%E#aD\033!!Y-,\001KRXC\260\002%E#`D\033!!Y-,KRXED\033!!Y-,\001 \260\003%#I\260@`\260 c \260\000RX#\260\002%8#\260\002%e8\000\212c8\033!!!!!Y\001-,KPXED\033!!Y-,\001\260\005%\020# \212\365\000\260\001`#\355\354-,\001\260\005%\020# \212\365\000\260\001a#\355\354-,\001\260\006%\020\365\000\355\354-,F#F`\212\212F# F\212`\212a\270\377\200b# \020#\212\261KK\212pE` \260\000PX\260\001a\270\377\272\213\033\260F\214Y\260\020`h\001:-, E\260\003%FRX\260\002%F ha\260\003%\260\003%?#!8\033!\021Y-, E\260\003%FPX\260\002%F ha\260\003%\260\003%?#!8\033!\021Y-,\000\260\007C\260\006C\013-,\212\020\354-,\260\014CX!\033 F\260\000RX\270\377\3608\033\260\0208YY-, \260\000UX\270\020\000c\260\003%Ed\260\003%Eda\260\000SX\260\002\033\260@a\260\003Y%EiSXED\033!!Y\033!\260\002%E\260\002%Ead\260(QXED\033!!YY-,!!\014d#d\213\270@\000b-,!\260\200QX\014d#d\213\270 \000b\033\262\000@/+Y\260\002`-,!\260\300QX\014d#d\213\270\025Ub\033\262\000\200/+Y\260\002`-,\014d#d\213\270@\000b`#!-,KSX\260\004%\260\004%Id#Edi\260@\213a \260\200bj\260\002%\260\002%a\214\260F#D!\212\020\260F\366!\033!\212\021#\022 9/Y-,\260\002%\260\002%Id\260\300TX\270\377\3708\260\0108\033!!Y-,\260\023CX\003\033\002Y-,\260\023CX\002\033\003Y-,\260\012+#\020 <\260\027+-,\260\002%\270\377\3608\260(+\212\020# \320#\260\020+\260\005CX\300\033<Y \020\021\260\000\022\001-,KS#KQZX8\033!!Y-,\001\260\002%\020\320#\311\001\260\001\023\260\000\024\020\260\001<\260\001\026-,\001\260\000\023\260\001\260\003%I\260\003\0278\260\001\023-,KS#KQZX E\212`D\033!!Y-, 9/-""" -if __name__ == "__main__": - bc = """@;:9876543210/.-,+*)(\'&%$#"! \037\036\035\034\033\032\031\030\027\026\025\024\023\022\021\020\017\016\015\014\013\012\011\010\007\006\005\004\003\002\001\000,\001\260\030CXEj\260\031C`\260F#D#\020 \260FN\360M/\260\000\022\033!#\0213Y-,\001\260\030CX\260\005+\260\000\023K\260\024PX\261\000@8Y\260\006+\033!#\0213Y-,\001\260\030CXN\260\003%\020\362!\260\000\022M\033 E\260\004%\260\004%#Jad\260(RX!#\020\326\033\260\003%\020\362!\260\000\022YY-,\260\032CX!!\033\260\002%\260\002%I\260\003%\260\003%Ja d\260\020PX!!!\033\260\003%\260\003%I\260\000PX\260\000PX\270\377\3428!\033\260\0208!Y\033\260\000RX\260\0368!\033\270\377\3608!YYYY-,\001\260\030CX\260\005+\260\000\023K\260\024PX\271\000\000\377\3008Y\260\006+\033!#\0213Y-,N\001\212\020\261F\031CD\260\000\024\261\000F\342\260\000\025\271\000\000\377\3608\000\260\000<\260(+\260\002%\020\260\000<-,\001\030\260\000/\260\001\024\362\260\001\023\260\001\025M\260\000\022-,\001\260\030CX\260\005+\260\000\023\271\000\000\377\3408\260\006+\033!#\0213Y-,\001\260\030CXEdj#Edi\260\031Cd``\260F#D#\020 \260F\360/\260\000\022\033!! \212 \212RX\0213\033!!YY-,\001\261\013\012C#Ce\012-,\000\261\012\013C#C\013-,\000\260F#p\261\001F>\001\260F#p\261\002FE:\261\002\000\010\015-,\260\022+\260\002%E\260\002%Ej\260@\213`\260\002%#D!!!-,\260\023+\260\002%E\260\002%Ej\270\377\300\214`\260\002%#D!!!-,\260\000\260\022+!!!-,\260\000\260\023+!!!-,\001\260\006C\260\007Ce\012-, i\260@a\260\000\213 \261,\300\212\214\270\020\000b`+\014d#da\\X\260\003aY-,\261\000\003%EhT\260\034KPZX\260\003%E\260\003%E`h \260\004%#D\260\004%#D\033\260\003% Eh \212#D\260\003%Eh`\260\003%#DY-,\260\003% Eh \212#D\260\003%Edhe`\260\004%\260\001`#D-,\260\011CX\207!\300\033\260\022CX\207E\260\021+\260G#D\260Gz\344\033\003\212E\030i \260G#D\212\212\207 \260\240QX\260\021+\260G#D\260Gz\344\033!\260Gz\344YYY\030-, \212E#Eh`D-,EjB-,\001\030/-,\001\260\030CX\260\004%\260\004%Id#Edi\260@\213a \260\200bj\260\002%\260\002%a\214\260\031C`\260F#D!\212\020\260F\366!\033!!!!Y-,\001\260\030CX\260\002%E\260\002%Ed`j\260\003%Eja \260\004%Ej \212\213e\260\004%#D\214\260\003%#D!!\033 EjD EjDY-,\001 E\260\000U\260\030CZXEh#Ei\260@\213a \260\200bj \212#a \260\003%\213e\260\004%#D\214\260\003%#D!!\033!!\260\031+Y-,\001\212\212Ed#EdadB-,\260\004%\260\004%\260\031+\260\030CX\260\004%\260\004%\260\003%\260\033+\001\260\002%C\260@T\260\002%C\260\000TZX\260\003% E\260@aDY\260\002%C\260\000T\260\002%C\260@TZX\260\004% E\260@`DYY!!!!-,\001KRXC\260\002%E#aD\033!!Y-,\001KRXC\260\002%E#`D\033!!Y-,KRXED\033!!Y-,\001 \260\003%#I\260@`\260 c \260\000RX#\260\002%8#\260\002%e8\000\212c8\033!!!!!Y\001-,KPXED\033!!Y-,\001\260\005%\020# \212\365\000\260\001`#\355\354-,\001\260\005%\020# \212\365\000\260\001a#\355\354-,\001\260\006%\020\365\000\355\354-,F#F`\212\212F# F\212`\212a\270\377\200b# \020#\212\261KK\212pE` \260\000PX\260\001a\270\377\272\213\033\260F\214Y\260\020`h\001:-, E\260\003%FRX\260\002%F ha\260\003%\260\003%?#!8\033!\021Y-, E\260\003%FPX\260\002%F ha\260\003%\260\003%?#!8\033!\021Y-,\000\260\007C\260\006C\013-,\212\020\354-,\260\014CX!\033 F\260\000RX\270\377\3608\033\260\0208YY-, \260\000UX\270\020\000c\260\003%Ed\260\003%Eda\260\000SX\260\002\033\260@a\260\003Y%EiSXED\033!!Y\033!\260\002%E\260\002%Ead\260(QXED\033!!YY-,!!\014d#d\213\270@\000b-,!\260\200QX\014d#d\213\270 \000b\033\262\000@/+Y\260\002`-,!\260\300QX\014d#d\213\270\025Ub\033\262\000\200/+Y\260\002`-,\014d#d\213\270@\000b`#!-,KSX\260\004%\260\004%Id#Edi\260@\213a \260\200bj\260\002%\260\002%a\214\260F#D!\212\020\260F\366!\033!\212\021#\022 9/Y-,\260\002%\260\002%Id\260\300TX\270\377\3708\260\0108\033!!Y-,\260\023CX\003\033\002Y-,\260\023CX\002\033\003Y-,\260\012+#\020 <\260\027+-,\260\002%\270\377\3608\260(+\212\020# \320#\260\020+\260\005CX\300\033<Y \020\021\260\000\022\001-,KS#KQZX8\033!!Y-,\001\260\002%\020\320#\311\001\260\001\023\260\000\024\020\260\001<\260\001\026-,\001\260\000\023\260\001\260\003%I\260\003\0278\260\001\023-,KS#KQZX E\212`D\033!!Y-, 9/-""" - p = Program() p.fromBytecode(bc) - asm = p.getAssembly() + asm = p.getAssembly(preserve=True) p.fromAssembly(asm) print(bc == p.getBytecode()) +if __name__ == "__main__": + import sys + import doctest + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/ttLib/ttCollection.py b/Lib/fontTools/ttLib/ttCollection.py new file mode 100644 index 0000000..2507fe3 --- /dev/null +++ b/Lib/fontTools/ttLib/ttCollection.py @@ -0,0 +1,107 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.ttLib.ttFont import TTFont +from fontTools.ttLib.sfnt import readTTCHeader, writeTTCHeader +import struct +import logging + +log = logging.getLogger(__name__) + + +class TTCollection(object): + + """Object representing a TrueType Collection / OpenType Collection. + The main API is self.fonts being a list of TTFont instances. + + If shareTables is True, then different fonts in the collection + might point to the same table object if the data for the table was + the same in the font file. Note, however, that this might result + in suprises and incorrect behavior if the different fonts involved + have different GlyphOrder. Use only if you know what you are doing. + """ + + def __init__(self, file=None, shareTables=False, **kwargs): + fonts = self.fonts = [] + if file is None: + return + + assert 'fontNumber' not in kwargs, kwargs + + if not hasattr(file, "read"): + file = open(file, "rb") + + tableCache = {} if shareTables else None + + header = readTTCHeader(file) + for i in range(header.numFonts): + font = TTFont(file, fontNumber=i, _tableCache=tableCache, **kwargs) + fonts.append(font) + + def save(self, file, shareTables=True): + """Save the font to disk. Similarly to the constructor, + the 'file' argument can be either a pathname or a writable + file object. + """ + if not hasattr(file, "write"): + final = None + file = open(file, "wb") + else: + # assume "file" is a writable file object + # write to a temporary stream to allow saving to unseekable streams + final = file + file = BytesIO() + + tableCache = {} if shareTables else None + + offsets_offset = writeTTCHeader(file, len(self.fonts)) + offsets = [] + for font in self.fonts: + offsets.append(file.tell()) + font._save(file, tableCache=tableCache) + file.seek(0,2) + + file.seek(offsets_offset) + file.write(struct.pack(">%dL" % len(self.fonts), *offsets)) + + if final: + final.write(file.getvalue()) + file.close() + + def saveXML(self, fileOrPath, newlinestr=None, writeVersion=True, **kwargs): + + from fontTools.misc import xmlWriter + writer = xmlWriter.XMLWriter(fileOrPath, newlinestr=newlinestr) + + if writeVersion: + from fontTools import version + version = ".".join(version.split('.')[:2]) + writer.begintag("ttCollection", ttLibVersion=version) + else: + writer.begintag("ttCollection") + writer.newline() + writer.newline() + + for font in self.fonts: + font._saveXML(writer, writeVersion=False, **kwargs) + writer.newline() + + writer.endtag("ttCollection") + writer.newline() + + writer.close() + + + def __getitem__(self, item): + return self.fonts[item] + + def __setitem__(self, item, value): + self.fonts[item] = values + + def __delitem__(self, item): + return self.fonts[item] + + def __len__(self): + return len(self.fonts) + + def __iter__(self): + return iter(self.fonts) diff --git a/Lib/fontTools/ttLib/ttFont.py b/Lib/fontTools/ttLib/ttFont.py new file mode 100644 index 0000000..87538fb --- /dev/null +++ b/Lib/fontTools/ttLib/ttFont.py @@ -0,0 +1,1001 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc import xmlWriter +from fontTools.misc.py23 import * +from fontTools.misc.loggingTools import deprecateArgument +from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter +import os +import logging +import itertools + +log = logging.getLogger(__name__) + +class TTFont(object): + + """The main font object. It manages file input and output, and offers + a convenient way of accessing tables. + Tables will be only decompiled when necessary, ie. when they're actually + accessed. This means that simple operations can be extremely fast. + """ + + def __init__(self, file=None, res_name_or_index=None, + sfntVersion="\000\001\000\000", flavor=None, checkChecksums=False, + verbose=None, recalcBBoxes=True, allowVID=False, ignoreDecompileErrors=False, + recalcTimestamp=True, fontNumber=-1, lazy=None, quiet=None, + _tableCache=None): + + """The constructor can be called with a few different arguments. + When reading a font from disk, 'file' should be either a pathname + pointing to a file, or a readable file object. + + It we're running on a Macintosh, 'res_name_or_index' maybe an sfnt + resource name or an sfnt resource index number or zero. The latter + case will cause TTLib to autodetect whether the file is a flat file + or a suitcase. (If it's a suitcase, only the first 'sfnt' resource + will be read!) + + The 'checkChecksums' argument is used to specify how sfnt + checksums are treated upon reading a file from disk: + 0: don't check (default) + 1: check, print warnings if a wrong checksum is found + 2: check, raise an exception if a wrong checksum is found. + + The TTFont constructor can also be called without a 'file' + argument: this is the way to create a new empty font. + In this case you can optionally supply the 'sfntVersion' argument, + and a 'flavor' which can be None, 'woff', or 'woff2'. + + If the recalcBBoxes argument is false, a number of things will *not* + be recalculated upon save/compile: + 1) 'glyf' glyph bounding boxes + 2) 'CFF ' font bounding box + 3) 'head' font bounding box + 4) 'hhea' min/max values + 5) 'vhea' min/max values + (1) is needed for certain kinds of CJK fonts (ask Werner Lemberg ;-). + Additionally, upon importing an TTX file, this option cause glyphs + to be compiled right away. This should reduce memory consumption + greatly, and therefore should have some impact on the time needed + to parse/compile large fonts. + + If the recalcTimestamp argument is false, the modified timestamp in the + 'head' table will *not* be recalculated upon save/compile. + + If the allowVID argument is set to true, then virtual GID's are + supported. Asking for a glyph ID with a glyph name or GID that is not in + the font will return a virtual GID. This is valid for GSUB and cmap + tables. For SING glyphlets, the cmap table is used to specify Unicode + values for virtual GI's used in GSUB/GPOS rules. If the gid N is requested + and does not exist in the font, or the glyphname has the form glyphN + and does not exist in the font, then N is used as the virtual GID. + Else, the first virtual GID is assigned as 0x1000 -1; for subsequent new + virtual GIDs, the next is one less than the previous. + + If ignoreDecompileErrors is set to True, exceptions raised in + individual tables during decompilation will be ignored, falling + back to the DefaultTable implementation, which simply keeps the + binary data. + + If lazy is set to True, many data structures are loaded lazily, upon + access only. If it is set to False, many data structures are loaded + immediately. The default is lazy=None which is somewhere in between. + """ + + for name in ("verbose", "quiet"): + val = locals().get(name) + if val is not None: + deprecateArgument(name, "configure logging instead") + setattr(self, name, val) + + self.lazy = lazy + self.recalcBBoxes = recalcBBoxes + self.recalcTimestamp = recalcTimestamp + self.tables = {} + self.reader = None + + # Permit the user to reference glyphs that are not int the font. + self.last_vid = 0xFFFE # Can't make it be 0xFFFF, as the world is full unsigned short integer counters that get incremented after the last seen GID value. + self.reverseVIDDict = {} + self.VIDDict = {} + self.allowVID = allowVID + self.ignoreDecompileErrors = ignoreDecompileErrors + + if not file: + self.sfntVersion = sfntVersion + self.flavor = flavor + self.flavorData = None + return + if not hasattr(file, "read"): + closeStream = True + # assume file is a string + if res_name_or_index is not None: + # see if it contains 'sfnt' resources in the resource or data fork + from . import macUtils + if res_name_or_index == 0: + if macUtils.getSFNTResIndices(file): + # get the first available sfnt font. + file = macUtils.SFNTResourceReader(file, 1) + else: + file = open(file, "rb") + else: + file = macUtils.SFNTResourceReader(file, res_name_or_index) + else: + file = open(file, "rb") + else: + # assume "file" is a readable file object + closeStream = False + file.seek(0) + + if not self.lazy: + # read input file in memory and wrap a stream around it to allow overwriting + file.seek(0) + tmp = BytesIO(file.read()) + if hasattr(file, 'name'): + # save reference to input file name + tmp.name = file.name + if closeStream: + file.close() + file = tmp + self._tableCache = _tableCache + self.reader = SFNTReader(file, checkChecksums, fontNumber=fontNumber) + self.sfntVersion = self.reader.sfntVersion + self.flavor = self.reader.flavor + self.flavorData = self.reader.flavorData + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() + + def close(self): + """If we still have a reader object, close it.""" + if self.reader is not None: + self.reader.close() + + def save(self, file, reorderTables=True): + """Save the font to disk. Similarly to the constructor, + the 'file' argument can be either a pathname or a writable + file object. + """ + if not hasattr(file, "write"): + if self.lazy and self.reader.file.name == file: + raise TTLibError( + "Can't overwrite TTFont when 'lazy' attribute is True") + closeStream = True + file = open(file, "wb") + else: + # assume "file" is a writable file object + closeStream = False + + tmp = BytesIO() + + writer_reordersTables = self._save(tmp) + + if (reorderTables is None or writer_reordersTables or + (reorderTables is False and self.reader is None)): + # don't reorder tables and save as is + file.write(tmp.getvalue()) + tmp.close() + else: + if reorderTables is False: + # sort tables using the original font's order + tableOrder = list(self.reader.keys()) + else: + # use the recommended order from the OpenType specification + tableOrder = None + tmp.flush() + tmp2 = BytesIO() + reorderFontTables(tmp, tmp2, tableOrder) + file.write(tmp2.getvalue()) + tmp.close() + tmp2.close() + + if closeStream: + file.close() + + def _save(self, file, tableCache=None): + """Internal function, to be shared by save() and TTCollection.save()""" + + if self.recalcTimestamp and 'head' in self: + self['head'] # make sure 'head' is loaded so the recalculation is actually done + + tags = list(self.keys()) + if "GlyphOrder" in tags: + tags.remove("GlyphOrder") + numTables = len(tags) + # write to a temporary stream to allow saving to unseekable streams + writer = SFNTWriter(file, numTables, self.sfntVersion, self.flavor, self.flavorData) + + done = [] + for tag in tags: + self._writeTable(tag, writer, done, tableCache) + + writer.close() + + return writer.reordersTables() + + def saveXML(self, fileOrPath, newlinestr=None, **kwargs): + """Export the font as TTX (an XML-based text file), or as a series of text + files when splitTables is true. In the latter case, the 'fileOrPath' + argument should be a path to a directory. + The 'tables' argument must either be false (dump all tables) or a + list of tables to dump. The 'skipTables' argument may be a list of tables + to skip, but only when the 'tables' argument is false. + """ + + writer = xmlWriter.XMLWriter(fileOrPath, newlinestr=newlinestr) + self._saveXML(writer, **kwargs) + writer.close() + + def _saveXML(self, writer, + writeVersion=True, + quiet=None, tables=None, skipTables=None, splitTables=False, + splitGlyphs=False, disassembleInstructions=True, + bitmapGlyphDataFormat='raw'): + + if quiet is not None: + deprecateArgument("quiet", "configure logging instead") + + self.disassembleInstructions = disassembleInstructions + self.bitmapGlyphDataFormat = bitmapGlyphDataFormat + if not tables: + tables = list(self.keys()) + if "GlyphOrder" not in tables: + tables = ["GlyphOrder"] + tables + if skipTables: + for tag in skipTables: + if tag in tables: + tables.remove(tag) + numTables = len(tables) + + if writeVersion: + from fontTools import version + version = ".".join(version.split('.')[:2]) + writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1], + ttLibVersion=version) + else: + writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1]) + writer.newline() + + # always splitTables if splitGlyphs is enabled + splitTables = splitTables or splitGlyphs + + if not splitTables: + writer.newline() + else: + path, ext = os.path.splitext(writer.filename) + fileNameTemplate = path + ".%s" + ext + + for i in range(numTables): + tag = tables[i] + if splitTables: + tablePath = fileNameTemplate % tagToIdentifier(tag) + tableWriter = xmlWriter.XMLWriter(tablePath, + newlinestr=writer.newlinestr) + tableWriter.begintag("ttFont", ttLibVersion=version) + tableWriter.newline() + tableWriter.newline() + writer.simpletag(tagToXML(tag), src=os.path.basename(tablePath)) + writer.newline() + else: + tableWriter = writer + self._tableToXML(tableWriter, tag, splitGlyphs=splitGlyphs) + if splitTables: + tableWriter.endtag("ttFont") + tableWriter.newline() + tableWriter.close() + writer.endtag("ttFont") + writer.newline() + + def _tableToXML(self, writer, tag, quiet=None, splitGlyphs=False): + if quiet is not None: + deprecateArgument("quiet", "configure logging instead") + if tag in self: + table = self[tag] + report = "Dumping '%s' table..." % tag + else: + report = "No '%s' table found." % tag + log.info(report) + if tag not in self: + return + xmlTag = tagToXML(tag) + attrs = dict() + if hasattr(table, "ERROR"): + attrs['ERROR'] = "decompilation error" + from .tables.DefaultTable import DefaultTable + if table.__class__ == DefaultTable: + attrs['raw'] = True + writer.begintag(xmlTag, **attrs) + writer.newline() + if tag == "glyf": + table.toXML(writer, self, splitGlyphs=splitGlyphs) + else: + table.toXML(writer, self) + writer.endtag(xmlTag) + writer.newline() + writer.newline() + + def importXML(self, fileOrPath, quiet=None): + """Import a TTX file (an XML-based text format), so as to recreate + a font object. + """ + if quiet is not None: + deprecateArgument("quiet", "configure logging instead") + + if "maxp" in self and "post" in self: + # Make sure the glyph order is loaded, as it otherwise gets + # lost if the XML doesn't contain the glyph order, yet does + # contain the table which was originally used to extract the + # glyph names from (ie. 'post', 'cmap' or 'CFF '). + self.getGlyphOrder() + + from fontTools.misc import xmlReader + + reader = xmlReader.XMLReader(fileOrPath, self) + reader.read() + + def isLoaded(self, tag): + """Return true if the table identified by 'tag' has been + decompiled and loaded into memory.""" + return tag in self.tables + + def has_key(self, tag): + if self.isLoaded(tag): + return True + elif self.reader and tag in self.reader: + return True + elif tag == "GlyphOrder": + return True + else: + return False + + __contains__ = has_key + + def keys(self): + keys = list(self.tables.keys()) + if self.reader: + for key in list(self.reader.keys()): + if key not in keys: + keys.append(key) + + if "GlyphOrder" in keys: + keys.remove("GlyphOrder") + keys = sortedTagList(keys) + return ["GlyphOrder"] + keys + + def __len__(self): + return len(list(self.keys())) + + def __getitem__(self, tag): + tag = Tag(tag) + try: + return self.tables[tag] + except KeyError: + if tag == "GlyphOrder": + table = GlyphOrder(tag) + self.tables[tag] = table + return table + if self.reader is not None: + import traceback + log.debug("Reading '%s' table from disk", tag) + data = self.reader[tag] + if self._tableCache is not None: + table = self._tableCache.get((Tag(tag), data)) + if table is not None: + return table + tableClass = getTableClass(tag) + table = tableClass(tag) + self.tables[tag] = table + log.debug("Decompiling '%s' table", tag) + try: + table.decompile(data, self) + except: + if not self.ignoreDecompileErrors: + raise + # fall back to DefaultTable, retaining the binary table data + log.exception( + "An exception occurred during the decompilation of the '%s' table", tag) + from .tables.DefaultTable import DefaultTable + file = StringIO() + traceback.print_exc(file=file) + table = DefaultTable(tag) + table.ERROR = file.getvalue() + self.tables[tag] = table + table.decompile(data, self) + if self._tableCache is not None: + self._tableCache[(Tag(tag), data)] = table + return table + else: + raise KeyError("'%s' table not found" % tag) + + def __setitem__(self, tag, table): + self.tables[Tag(tag)] = table + + def __delitem__(self, tag): + if tag not in self: + raise KeyError("'%s' table not found" % tag) + if tag in self.tables: + del self.tables[tag] + if self.reader and tag in self.reader: + del self.reader[tag] + + def get(self, tag, default=None): + try: + return self[tag] + except KeyError: + return default + + def setGlyphOrder(self, glyphOrder): + self.glyphOrder = glyphOrder + + def getGlyphOrder(self): + try: + return self.glyphOrder + except AttributeError: + pass + if 'CFF ' in self: + cff = self['CFF '] + self.glyphOrder = cff.getGlyphOrder() + elif 'post' in self: + # TrueType font + glyphOrder = self['post'].getGlyphOrder() + if glyphOrder is None: + # + # No names found in the 'post' table. + # Try to create glyph names from the unicode cmap (if available) + # in combination with the Adobe Glyph List (AGL). + # + self._getGlyphNamesFromCmap() + else: + self.glyphOrder = glyphOrder + else: + self._getGlyphNamesFromCmap() + return self.glyphOrder + + def _getGlyphNamesFromCmap(self): + # + # This is rather convoluted, but then again, it's an interesting problem: + # - we need to use the unicode values found in the cmap table to + # build glyph names (eg. because there is only a minimal post table, + # or none at all). + # - but the cmap parser also needs glyph names to work with... + # So here's what we do: + # - make up glyph names based on glyphID + # - load a temporary cmap table based on those names + # - extract the unicode values, build the "real" glyph names + # - unload the temporary cmap table + # + if self.isLoaded("cmap"): + # Bootstrapping: we're getting called by the cmap parser + # itself. This means self.tables['cmap'] contains a partially + # loaded cmap, making it impossible to get at a unicode + # subtable here. We remove the partially loaded cmap and + # restore it later. + # This only happens if the cmap table is loaded before any + # other table that does f.getGlyphOrder() or f.getGlyphName(). + cmapLoading = self.tables['cmap'] + del self.tables['cmap'] + else: + cmapLoading = None + # Make up glyph names based on glyphID, which will be used by the + # temporary cmap and by the real cmap in case we don't find a unicode + # cmap. + numGlyphs = int(self['maxp'].numGlyphs) + glyphOrder = [None] * numGlyphs + glyphOrder[0] = ".notdef" + for i in range(1, numGlyphs): + glyphOrder[i] = "glyph%.5d" % i + # Set the glyph order, so the cmap parser has something + # to work with (so we don't get called recursively). + self.glyphOrder = glyphOrder + + # Make up glyph names based on the reversed cmap table. Because some + # glyphs (eg. ligatures or alternates) may not be reachable via cmap, + # this naming table will usually not cover all glyphs in the font. + # If the font has no Unicode cmap table, reversecmap will be empty. + if 'cmap' in self: + reversecmap = self['cmap'].buildReversed() + else: + reversecmap = {} + useCount = {} + for i in range(numGlyphs): + tempName = glyphOrder[i] + if tempName in reversecmap: + # If a font maps both U+0041 LATIN CAPITAL LETTER A and + # U+0391 GREEK CAPITAL LETTER ALPHA to the same glyph, + # we prefer naming the glyph as "A". + glyphName = self._makeGlyphName(min(reversecmap[tempName])) + numUses = useCount[glyphName] = useCount.get(glyphName, 0) + 1 + if numUses > 1: + glyphName = "%s.alt%d" % (glyphName, numUses - 1) + glyphOrder[i] = glyphName + + if 'cmap' in self: + # Delete the temporary cmap table from the cache, so it can + # be parsed again with the right names. + del self.tables['cmap'] + self.glyphOrder = glyphOrder + if cmapLoading: + # restore partially loaded cmap, so it can continue loading + # using the proper names. + self.tables['cmap'] = cmapLoading + + @staticmethod + def _makeGlyphName(codepoint): + from fontTools import agl # Adobe Glyph List + if codepoint in agl.UV2AGL: + return agl.UV2AGL[codepoint] + elif codepoint <= 0xFFFF: + return "uni%04X" % codepoint + else: + return "u%X" % codepoint + + def getGlyphNames(self): + """Get a list of glyph names, sorted alphabetically.""" + glyphNames = sorted(self.getGlyphOrder()) + return glyphNames + + def getGlyphNames2(self): + """Get a list of glyph names, sorted alphabetically, + but not case sensitive. + """ + from fontTools.misc import textTools + return textTools.caselessSort(self.getGlyphOrder()) + + def getGlyphName(self, glyphID, requireReal=False): + try: + return self.getGlyphOrder()[glyphID] + except IndexError: + if requireReal or not self.allowVID: + # XXX The ??.W8.otf font that ships with OSX uses higher glyphIDs in + # the cmap table than there are glyphs. I don't think it's legal... + return "glyph%.5d" % glyphID + else: + # user intends virtual GID support + try: + glyphName = self.VIDDict[glyphID] + except KeyError: + glyphName ="glyph%.5d" % glyphID + self.last_vid = min(glyphID, self.last_vid ) + self.reverseVIDDict[glyphName] = glyphID + self.VIDDict[glyphID] = glyphName + return glyphName + + def getGlyphID(self, glyphName, requireReal=False): + if not hasattr(self, "_reverseGlyphOrderDict"): + self._buildReverseGlyphOrderDict() + glyphOrder = self.getGlyphOrder() + d = self._reverseGlyphOrderDict + if glyphName not in d: + if glyphName in glyphOrder: + self._buildReverseGlyphOrderDict() + return self.getGlyphID(glyphName) + else: + if requireReal: + raise KeyError(glyphName) + elif not self.allowVID: + # Handle glyphXXX only + if glyphName[:5] == "glyph": + try: + return int(glyphName[5:]) + except (NameError, ValueError): + raise KeyError(glyphName) + else: + # user intends virtual GID support + try: + glyphID = self.reverseVIDDict[glyphName] + except KeyError: + # if name is in glyphXXX format, use the specified name. + if glyphName[:5] == "glyph": + try: + glyphID = int(glyphName[5:]) + except (NameError, ValueError): + glyphID = None + if glyphID is None: + glyphID = self.last_vid -1 + self.last_vid = glyphID + self.reverseVIDDict[glyphName] = glyphID + self.VIDDict[glyphID] = glyphName + return glyphID + + glyphID = d[glyphName] + if glyphName != glyphOrder[glyphID]: + self._buildReverseGlyphOrderDict() + return self.getGlyphID(glyphName) + return glyphID + + def getReverseGlyphMap(self, rebuild=False): + if rebuild or not hasattr(self, "_reverseGlyphOrderDict"): + self._buildReverseGlyphOrderDict() + return self._reverseGlyphOrderDict + + def _buildReverseGlyphOrderDict(self): + self._reverseGlyphOrderDict = d = {} + glyphOrder = self.getGlyphOrder() + for glyphID in range(len(glyphOrder)): + d[glyphOrder[glyphID]] = glyphID + + def _writeTable(self, tag, writer, done, tableCache=None): + """Internal helper function for self.save(). Keeps track of + inter-table dependencies. + """ + if tag in done: + return + tableClass = getTableClass(tag) + for masterTable in tableClass.dependencies: + if masterTable not in done: + if masterTable in self: + self._writeTable(masterTable, writer, done, tableCache) + else: + done.append(masterTable) + done.append(tag) + tabledata = self.getTableData(tag) + if tableCache is not None: + entry = tableCache.get((Tag(tag), tabledata)) + if entry is not None: + log.debug("reusing '%s' table", tag) + writer.setEntry(tag, entry) + return + log.debug("writing '%s' table to disk", tag) + writer[tag] = tabledata + if tableCache is not None: + tableCache[(Tag(tag), tabledata)] = writer[tag] + + def getTableData(self, tag): + """Returns raw table data, whether compiled or directly read from disk. + """ + tag = Tag(tag) + if self.isLoaded(tag): + log.debug("compiling '%s' table", tag) + return self.tables[tag].compile(self) + elif self.reader and tag in self.reader: + log.debug("Reading '%s' table from disk", tag) + return self.reader[tag] + else: + raise KeyError(tag) + + def getGlyphSet(self, preferCFF=True): + """Return a generic GlyphSet, which is a dict-like object + mapping glyph names to glyph objects. The returned glyph objects + have a .draw() method that supports the Pen protocol, and will + have an attribute named 'width'. + + If the font is CFF-based, the outlines will be taken from the 'CFF ' or + 'CFF2' tables. Otherwise the outlines will be taken from the 'glyf' table. + If the font contains both a 'CFF '/'CFF2' and a 'glyf' table, you can use + the 'preferCFF' argument to specify which one should be taken. If the + font contains both a 'CFF ' and a 'CFF2' table, the latter is taken. + """ + glyphs = None + if (preferCFF and any(tb in self for tb in ["CFF ", "CFF2"]) or + ("glyf" not in self and any(tb in self for tb in ["CFF ", "CFF2"]))): + table_tag = "CFF2" if "CFF2" in self else "CFF " + glyphs = _TTGlyphSet(self, + list(self[table_tag].cff.values())[0].CharStrings, _TTGlyphCFF) + + if glyphs is None and "glyf" in self: + glyphs = _TTGlyphSet(self, self["glyf"], _TTGlyphGlyf) + + if glyphs is None: + raise TTLibError("Font contains no outlines") + + return glyphs + + def getBestCmap(self, cmapPreferences=((3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0))): + """Return the 'best' unicode cmap dictionary available in the font, + or None, if no unicode cmap subtable is available. + + By default it will search for the following (platformID, platEncID) + pairs: + (3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0) + This can be customized via the cmapPreferences argument. + """ + return self["cmap"].getBestCmap(cmapPreferences=cmapPreferences) + + +class _TTGlyphSet(object): + + """Generic dict-like GlyphSet class that pulls metrics from hmtx and + glyph shape from TrueType or CFF. + """ + + def __init__(self, ttFont, glyphs, glyphType): + self._glyphs = glyphs + self._hmtx = ttFont['hmtx'] + self._vmtx = ttFont['vmtx'] if 'vmtx' in ttFont else None + self._glyphType = glyphType + + def keys(self): + return list(self._glyphs.keys()) + + def has_key(self, glyphName): + return glyphName in self._glyphs + + __contains__ = has_key + + def __getitem__(self, glyphName): + horizontalMetrics = self._hmtx[glyphName] + verticalMetrics = self._vmtx[glyphName] if self._vmtx else None + return self._glyphType( + self, self._glyphs[glyphName], horizontalMetrics, verticalMetrics) + + def __len__(self): + return len(self._glyphs) + + def get(self, glyphName, default=None): + try: + return self[glyphName] + except KeyError: + return default + +class _TTGlyph(object): + + """Wrapper for a TrueType glyph that supports the Pen protocol, meaning + that it has a .draw() method that takes a pen object as its only + argument. Additionally there are 'width' and 'lsb' attributes, read from + the 'hmtx' table. + + If the font contains a 'vmtx' table, there will also be 'height' and 'tsb' + attributes. + """ + + def __init__(self, glyphset, glyph, horizontalMetrics, verticalMetrics=None): + self._glyphset = glyphset + self._glyph = glyph + self.width, self.lsb = horizontalMetrics + if verticalMetrics: + self.height, self.tsb = verticalMetrics + else: + self.height, self.tsb = None, None + + def draw(self, pen): + """Draw the glyph onto Pen. See fontTools.pens.basePen for details + how that works. + """ + self._glyph.draw(pen) + +class _TTGlyphCFF(_TTGlyph): + pass + +class _TTGlyphGlyf(_TTGlyph): + + def draw(self, pen): + """Draw the glyph onto Pen. See fontTools.pens.basePen for details + how that works. + """ + glyfTable = self._glyphset._glyphs + glyph = self._glyph + offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0 + glyph.draw(pen, glyfTable, offset) + + +class GlyphOrder(object): + + """A pseudo table. The glyph order isn't in the font as a separate + table, but it's nice to present it as such in the TTX format. + """ + + def __init__(self, tag=None): + pass + + def toXML(self, writer, ttFont): + glyphOrder = ttFont.getGlyphOrder() + writer.comment("The 'id' attribute is only for humans; " + "it is ignored when parsed.") + writer.newline() + for i in range(len(glyphOrder)): + glyphName = glyphOrder[i] + writer.simpletag("GlyphID", id=i, name=glyphName) + writer.newline() + + def fromXML(self, name, attrs, content, ttFont): + if not hasattr(self, "glyphOrder"): + self.glyphOrder = [] + ttFont.setGlyphOrder(self.glyphOrder) + if name == "GlyphID": + self.glyphOrder.append(attrs["name"]) + + +def getTableModule(tag): + """Fetch the packer/unpacker module for a table. + Return None when no module is found. + """ + from . import tables + pyTag = tagToIdentifier(tag) + try: + __import__("fontTools.ttLib.tables." + pyTag) + except ImportError as err: + # If pyTag is found in the ImportError message, + # means table is not implemented. If it's not + # there, then some other module is missing, don't + # suppress the error. + if str(err).find(pyTag) >= 0: + return None + else: + raise err + else: + return getattr(tables, pyTag) + + +def getTableClass(tag): + """Fetch the packer/unpacker class for a table. + Return None when no class is found. + """ + module = getTableModule(tag) + if module is None: + from .tables.DefaultTable import DefaultTable + return DefaultTable + pyTag = tagToIdentifier(tag) + tableClass = getattr(module, "table_" + pyTag) + return tableClass + + +def getClassTag(klass): + """Fetch the table tag for a class object.""" + name = klass.__name__ + assert name[:6] == 'table_' + name = name[6:] # Chop 'table_' + return identifierToTag(name) + + +def newTable(tag): + """Return a new instance of a table.""" + tableClass = getTableClass(tag) + return tableClass(tag) + + +def _escapechar(c): + """Helper function for tagToIdentifier()""" + import re + if re.match("[a-z0-9]", c): + return "_" + c + elif re.match("[A-Z]", c): + return c + "_" + else: + return hex(byteord(c))[2:] + + +def tagToIdentifier(tag): + """Convert a table tag to a valid (but UGLY) python identifier, + as well as a filename that's guaranteed to be unique even on a + caseless file system. Each character is mapped to two characters. + Lowercase letters get an underscore before the letter, uppercase + letters get an underscore after the letter. Trailing spaces are + trimmed. Illegal characters are escaped as two hex bytes. If the + result starts with a number (as the result of a hex escape), an + extra underscore is prepended. Examples: + 'glyf' -> '_g_l_y_f' + 'cvt ' -> '_c_v_t' + 'OS/2' -> 'O_S_2f_2' + """ + import re + tag = Tag(tag) + if tag == "GlyphOrder": + return tag + assert len(tag) == 4, "tag should be 4 characters long" + while len(tag) > 1 and tag[-1] == ' ': + tag = tag[:-1] + ident = "" + for c in tag: + ident = ident + _escapechar(c) + if re.match("[0-9]", ident): + ident = "_" + ident + return ident + + +def identifierToTag(ident): + """the opposite of tagToIdentifier()""" + if ident == "GlyphOrder": + return ident + if len(ident) % 2 and ident[0] == "_": + ident = ident[1:] + assert not (len(ident) % 2) + tag = "" + for i in range(0, len(ident), 2): + if ident[i] == "_": + tag = tag + ident[i+1] + elif ident[i+1] == "_": + tag = tag + ident[i] + else: + # assume hex + tag = tag + chr(int(ident[i:i+2], 16)) + # append trailing spaces + tag = tag + (4 - len(tag)) * ' ' + return Tag(tag) + + +def tagToXML(tag): + """Similarly to tagToIdentifier(), this converts a TT tag + to a valid XML element name. Since XML element names are + case sensitive, this is a fairly simple/readable translation. + """ + import re + tag = Tag(tag) + if tag == "OS/2": + return "OS_2" + elif tag == "GlyphOrder": + return tag + if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): + return tag.strip() + else: + return tagToIdentifier(tag) + + +def xmlToTag(tag): + """The opposite of tagToXML()""" + if tag == "OS_2": + return Tag("OS/2") + if len(tag) == 8: + return identifierToTag(tag) + else: + return Tag(tag + " " * (4 - len(tag))) + + + +# Table order as recommended in the OpenType specification 1.4 +TTFTableOrder = ["head", "hhea", "maxp", "OS/2", "hmtx", "LTSH", "VDMX", + "hdmx", "cmap", "fpgm", "prep", "cvt ", "loca", "glyf", + "kern", "name", "post", "gasp", "PCLT"] + +OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", + "CFF "] + +def sortedTagList(tagList, tableOrder=None): + """Return a sorted copy of tagList, sorted according to the OpenType + specification, or according to a custom tableOrder. If given and not + None, tableOrder needs to be a list of tag names. + """ + tagList = sorted(tagList) + if tableOrder is None: + if "DSIG" in tagList: + # DSIG should be last (XXX spec reference?) + tagList.remove("DSIG") + tagList.append("DSIG") + if "CFF " in tagList: + tableOrder = OTFTableOrder + else: + tableOrder = TTFTableOrder + orderedTables = [] + for tag in tableOrder: + if tag in tagList: + orderedTables.append(tag) + tagList.remove(tag) + orderedTables.extend(tagList) + return orderedTables + + +def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=False): + """Rewrite a font file, ordering the tables as recommended by the + OpenType specification 1.4. + """ + inFile.seek(0) + outFile.seek(0) + reader = SFNTReader(inFile, checkChecksums=checkChecksums) + writer = SFNTWriter(outFile, len(reader.tables), reader.sfntVersion, reader.flavor, reader.flavorData) + tables = list(reader.keys()) + for tag in sortedTagList(tables, tableOrder): + writer[tag] = reader[tag] + writer.close() + + +def maxPowerOfTwo(x): + """Return the highest exponent of two, so that + (2 ** exponent) <= x. Return 0 if x is 0. + """ + exponent = 0 + while x: + x = x >> 1 + exponent = exponent + 1 + return max(exponent - 1, 0) + + +def getSearchRange(n, itemSize=16): + """Calculate searchRange, entrySelector, rangeShift. + """ + # itemSize defaults to 16, for backward compatibility + # with upstream fonttools. + exponent = maxPowerOfTwo(n) + searchRange = (2 ** exponent) * itemSize + entrySelector = exponent + rangeShift = max(0, n * itemSize - searchRange) + return searchRange, entrySelector, rangeShift diff --git a/Lib/fontTools/ttLib/woff2.py b/Lib/fontTools/ttLib/woff2.py new file mode 100644 index 0000000..1952682 --- /dev/null +++ b/Lib/fontTools/ttLib/woff2.py @@ -0,0 +1,1098 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +import sys +import array +import struct +from collections import OrderedDict +from fontTools.misc import sstruct +from fontTools.misc.arrayTools import calcIntBounds +from fontTools.misc.textTools import pad +from fontTools.ttLib import (TTFont, TTLibError, getTableModule, getTableClass, + getSearchRange) +from fontTools.ttLib.sfnt import (SFNTReader, SFNTWriter, DirectoryEntry, + WOFFFlavorData, sfntDirectoryFormat, sfntDirectorySize, SFNTDirectoryEntry, + sfntDirectoryEntrySize, calcChecksum) +from fontTools.ttLib.tables import ttProgram +import logging + + +log = logging.getLogger(__name__) + +haveBrotli = False +try: + import brotli + haveBrotli = True +except ImportError: + pass + + +class WOFF2Reader(SFNTReader): + + flavor = "woff2" + + def __init__(self, file, checkChecksums=1, fontNumber=-1): + if not haveBrotli: + log.error( + 'The WOFF2 decoder requires the Brotli Python extension, available at: ' + 'https://github.com/google/brotli') + raise ImportError("No module named brotli") + + self.file = file + + signature = Tag(self.file.read(4)) + if signature != b"wOF2": + raise TTLibError("Not a WOFF2 font (bad signature)") + + self.file.seek(0) + self.DirectoryEntry = WOFF2DirectoryEntry + data = self.file.read(woff2DirectorySize) + if len(data) != woff2DirectorySize: + raise TTLibError('Not a WOFF2 font (not enough data)') + sstruct.unpack(woff2DirectoryFormat, data, self) + + self.tables = OrderedDict() + offset = 0 + for i in range(self.numTables): + entry = self.DirectoryEntry() + entry.fromFile(self.file) + tag = Tag(entry.tag) + self.tables[tag] = entry + entry.offset = offset + offset += entry.length + + totalUncompressedSize = offset + compressedData = self.file.read(self.totalCompressedSize) + decompressedData = brotli.decompress(compressedData) + if len(decompressedData) != totalUncompressedSize: + raise TTLibError( + 'unexpected size for decompressed font data: expected %d, found %d' + % (totalUncompressedSize, len(decompressedData))) + self.transformBuffer = BytesIO(decompressedData) + + self.file.seek(0, 2) + if self.length != self.file.tell(): + raise TTLibError("reported 'length' doesn't match the actual file size") + + self.flavorData = WOFF2FlavorData(self) + + # make empty TTFont to store data while reconstructing tables + self.ttFont = TTFont(recalcBBoxes=False, recalcTimestamp=False) + + def __getitem__(self, tag): + """Fetch the raw table data. Reconstruct transformed tables.""" + entry = self.tables[Tag(tag)] + if not hasattr(entry, 'data'): + if tag in woff2TransformedTableTags: + entry.data = self.reconstructTable(tag) + else: + entry.data = entry.loadData(self.transformBuffer) + return entry.data + + def reconstructTable(self, tag): + """Reconstruct table named 'tag' from transformed data.""" + if tag not in woff2TransformedTableTags: + raise TTLibError("transform for table '%s' is unknown" % tag) + entry = self.tables[Tag(tag)] + rawData = entry.loadData(self.transformBuffer) + if tag == 'glyf': + # no need to pad glyph data when reconstructing + padding = self.padding if hasattr(self, 'padding') else None + data = self._reconstructGlyf(rawData, padding) + elif tag == 'loca': + data = self._reconstructLoca() + else: + raise NotImplementedError + return data + + def _reconstructGlyf(self, data, padding=None): + """ Return recostructed glyf table data, and set the corresponding loca's + locations. Optionally pad glyph offsets to the specified number of bytes. + """ + self.ttFont['loca'] = WOFF2LocaTable() + glyfTable = self.ttFont['glyf'] = WOFF2GlyfTable() + glyfTable.reconstruct(data, self.ttFont) + if padding: + glyfTable.padding = padding + data = glyfTable.compile(self.ttFont) + return data + + def _reconstructLoca(self): + """ Return reconstructed loca table data. """ + if 'loca' not in self.ttFont: + # make sure glyf is reconstructed first + self.tables['glyf'].data = self.reconstructTable('glyf') + locaTable = self.ttFont['loca'] + data = locaTable.compile(self.ttFont) + if len(data) != self.tables['loca'].origLength: + raise TTLibError( + "reconstructed 'loca' table doesn't match original size: " + "expected %d, found %d" + % (self.tables['loca'].origLength, len(data))) + return data + + +class WOFF2Writer(SFNTWriter): + + flavor = "woff2" + + def __init__(self, file, numTables, sfntVersion="\000\001\000\000", + flavor=None, flavorData=None): + if not haveBrotli: + log.error( + 'The WOFF2 encoder requires the Brotli Python extension, available at: ' + 'https://github.com/google/brotli') + raise ImportError("No module named brotli") + + self.file = file + self.numTables = numTables + self.sfntVersion = Tag(sfntVersion) + self.flavorData = flavorData or WOFF2FlavorData() + + self.directoryFormat = woff2DirectoryFormat + self.directorySize = woff2DirectorySize + self.DirectoryEntry = WOFF2DirectoryEntry + + self.signature = Tag("wOF2") + + self.nextTableOffset = 0 + self.transformBuffer = BytesIO() + + self.tables = OrderedDict() + + # make empty TTFont to store data while normalising and transforming tables + self.ttFont = TTFont(recalcBBoxes=False, recalcTimestamp=False) + + def __setitem__(self, tag, data): + """Associate new entry named 'tag' with raw table data.""" + if tag in self.tables: + raise TTLibError("cannot rewrite '%s' table" % tag) + if tag == 'DSIG': + # always drop DSIG table, since the encoding process can invalidate it + self.numTables -= 1 + return + + entry = self.DirectoryEntry() + entry.tag = Tag(tag) + entry.flags = getKnownTagIndex(entry.tag) + # WOFF2 table data are written to disk only on close(), after all tags + # have been specified + entry.data = data + + self.tables[tag] = entry + + def close(self): + """ All tags must have been specified. Now write the table data and directory. + """ + if len(self.tables) != self.numTables: + raise TTLibError("wrong number of tables; expected %d, found %d" % (self.numTables, len(self.tables))) + + if self.sfntVersion in ("\x00\x01\x00\x00", "true"): + isTrueType = True + elif self.sfntVersion == "OTTO": + isTrueType = False + else: + raise TTLibError("Not a TrueType or OpenType font (bad sfntVersion)") + + # The WOFF2 spec no longer requires the glyph offsets to be 4-byte aligned. + # However, the reference WOFF2 implementation still fails to reconstruct + # 'unpadded' glyf tables, therefore we need to 'normalise' them. + # See: + # https://github.com/khaledhosny/ots/issues/60 + # https://github.com/google/woff2/issues/15 + if isTrueType: + self._normaliseGlyfAndLoca(padding=4) + self._setHeadTransformFlag() + + # To pass the legacy OpenType Sanitiser currently included in browsers, + # we must sort the table directory and data alphabetically by tag. + # See: + # https://github.com/google/woff2/pull/3 + # https://lists.w3.org/Archives/Public/public-webfonts-wg/2015Mar/0000.html + # TODO(user): remove to match spec once browsers are on newer OTS + self.tables = OrderedDict(sorted(self.tables.items())) + + self.totalSfntSize = self._calcSFNTChecksumsLengthsAndOffsets() + + fontData = self._transformTables() + compressedFont = brotli.compress(fontData, mode=brotli.MODE_FONT) + + self.totalCompressedSize = len(compressedFont) + self.length = self._calcTotalSize() + self.majorVersion, self.minorVersion = self._getVersion() + self.reserved = 0 + + directory = self._packTableDirectory() + self.file.seek(0) + self.file.write(pad(directory + compressedFont, size=4)) + self._writeFlavorData() + + def _normaliseGlyfAndLoca(self, padding=4): + """ Recompile glyf and loca tables, aligning glyph offsets to multiples of + 'padding' size. Update the head table's 'indexToLocFormat' accordingly while + compiling loca. + """ + if self.sfntVersion == "OTTO": + return + + # make up glyph names required to decompile glyf table + self._decompileTable('maxp') + numGlyphs = self.ttFont['maxp'].numGlyphs + glyphOrder = ['.notdef'] + ["glyph%.5d" % i for i in range(1, numGlyphs)] + self.ttFont.setGlyphOrder(glyphOrder) + + for tag in ('head', 'loca', 'glyf'): + self._decompileTable(tag) + self.ttFont['glyf'].padding = padding + for tag in ('glyf', 'loca'): + self._compileTable(tag) + + def _setHeadTransformFlag(self): + """ Set bit 11 of 'head' table flags to indicate that the font has undergone + a lossless modifying transform. Re-compile head table data.""" + self._decompileTable('head') + self.ttFont['head'].flags |= (1 << 11) + self._compileTable('head') + + def _decompileTable(self, tag): + """ Fetch table data, decompile it, and store it inside self.ttFont. """ + tag = Tag(tag) + if tag not in self.tables: + raise TTLibError("missing required table: %s" % tag) + if self.ttFont.isLoaded(tag): + return + data = self.tables[tag].data + if tag == 'loca': + tableClass = WOFF2LocaTable + elif tag == 'glyf': + tableClass = WOFF2GlyfTable + else: + tableClass = getTableClass(tag) + table = tableClass(tag) + self.ttFont.tables[tag] = table + table.decompile(data, self.ttFont) + + def _compileTable(self, tag): + """ Compile table and store it in its 'data' attribute. """ + self.tables[tag].data = self.ttFont[tag].compile(self.ttFont) + + def _calcSFNTChecksumsLengthsAndOffsets(self): + """ Compute the 'original' SFNT checksums, lengths and offsets for checksum + adjustment calculation. Return the total size of the uncompressed font. + """ + offset = sfntDirectorySize + sfntDirectoryEntrySize * len(self.tables) + for tag, entry in self.tables.items(): + data = entry.data + entry.origOffset = offset + entry.origLength = len(data) + if tag == 'head': + entry.checkSum = calcChecksum(data[:8] + b'\0\0\0\0' + data[12:]) + else: + entry.checkSum = calcChecksum(data) + offset += (entry.origLength + 3) & ~3 + return offset + + def _transformTables(self): + """Return transformed font data.""" + for tag, entry in self.tables.items(): + if tag in woff2TransformedTableTags: + data = self.transformTable(tag) + else: + data = entry.data + entry.offset = self.nextTableOffset + entry.saveData(self.transformBuffer, data) + self.nextTableOffset += entry.length + self.writeMasterChecksum() + fontData = self.transformBuffer.getvalue() + return fontData + + def transformTable(self, tag): + """Return transformed table data.""" + if tag not in woff2TransformedTableTags: + raise TTLibError("Transform for table '%s' is unknown" % tag) + if tag == "loca": + data = b"" + elif tag == "glyf": + for tag in ('maxp', 'head', 'loca', 'glyf'): + self._decompileTable(tag) + glyfTable = self.ttFont['glyf'] + data = glyfTable.transform(self.ttFont) + else: + raise NotImplementedError + return data + + def _calcMasterChecksum(self): + """Calculate checkSumAdjustment.""" + tags = list(self.tables.keys()) + checksums = [] + for i in range(len(tags)): + checksums.append(self.tables[tags[i]].checkSum) + + # Create a SFNT directory for checksum calculation purposes + self.searchRange, self.entrySelector, self.rangeShift = getSearchRange(self.numTables, 16) + directory = sstruct.pack(sfntDirectoryFormat, self) + tables = sorted(self.tables.items()) + for tag, entry in tables: + sfntEntry = SFNTDirectoryEntry() + sfntEntry.tag = entry.tag + sfntEntry.checkSum = entry.checkSum + sfntEntry.offset = entry.origOffset + sfntEntry.length = entry.origLength + directory = directory + sfntEntry.toString() + + directory_end = sfntDirectorySize + len(self.tables) * sfntDirectoryEntrySize + assert directory_end == len(directory) + + checksums.append(calcChecksum(directory)) + checksum = sum(checksums) & 0xffffffff + # BiboAfba! + checksumadjustment = (0xB1B0AFBA - checksum) & 0xffffffff + return checksumadjustment + + def writeMasterChecksum(self): + """Write checkSumAdjustment to the transformBuffer.""" + checksumadjustment = self._calcMasterChecksum() + self.transformBuffer.seek(self.tables['head'].offset + 8) + self.transformBuffer.write(struct.pack(">L", checksumadjustment)) + + def _calcTotalSize(self): + """Calculate total size of WOFF2 font, including any meta- and/or private data.""" + offset = self.directorySize + for entry in self.tables.values(): + offset += len(entry.toString()) + offset += self.totalCompressedSize + offset = (offset + 3) & ~3 + offset = self._calcFlavorDataOffsetsAndSize(offset) + return offset + + def _calcFlavorDataOffsetsAndSize(self, start): + """Calculate offsets and lengths for any meta- and/or private data.""" + offset = start + data = self.flavorData + if data.metaData: + self.metaOrigLength = len(data.metaData) + self.metaOffset = offset + self.compressedMetaData = brotli.compress( + data.metaData, mode=brotli.MODE_TEXT) + self.metaLength = len(self.compressedMetaData) + offset += self.metaLength + else: + self.metaOffset = self.metaLength = self.metaOrigLength = 0 + self.compressedMetaData = b"" + if data.privData: + # make sure private data is padded to 4-byte boundary + offset = (offset + 3) & ~3 + self.privOffset = offset + self.privLength = len(data.privData) + offset += self.privLength + else: + self.privOffset = self.privLength = 0 + return offset + + def _getVersion(self): + """Return the WOFF2 font's (majorVersion, minorVersion) tuple.""" + data = self.flavorData + if data.majorVersion is not None and data.minorVersion is not None: + return data.majorVersion, data.minorVersion + else: + # if None, return 'fontRevision' from 'head' table + if 'head' in self.tables: + return struct.unpack(">HH", self.tables['head'].data[4:8]) + else: + return 0, 0 + + def _packTableDirectory(self): + """Return WOFF2 table directory data.""" + directory = sstruct.pack(self.directoryFormat, self) + for entry in self.tables.values(): + directory = directory + entry.toString() + return directory + + def _writeFlavorData(self): + """Write metadata and/or private data using appropiate padding.""" + compressedMetaData = self.compressedMetaData + privData = self.flavorData.privData + if compressedMetaData and privData: + compressedMetaData = pad(compressedMetaData, size=4) + if compressedMetaData: + self.file.seek(self.metaOffset) + assert self.file.tell() == self.metaOffset + self.file.write(compressedMetaData) + if privData: + self.file.seek(self.privOffset) + assert self.file.tell() == self.privOffset + self.file.write(privData) + + def reordersTables(self): + return True + + +# -- woff2 directory helpers and cruft + +woff2DirectoryFormat = """ + > # big endian + signature: 4s # "wOF2" + sfntVersion: 4s + length: L # total woff2 file size + numTables: H # number of tables + reserved: H # set to 0 + totalSfntSize: L # uncompressed size + totalCompressedSize: L # compressed size + majorVersion: H # major version of WOFF file + minorVersion: H # minor version of WOFF file + metaOffset: L # offset to metadata block + metaLength: L # length of compressed metadata + metaOrigLength: L # length of uncompressed metadata + privOffset: L # offset to private data block + privLength: L # length of private data block +""" + +woff2DirectorySize = sstruct.calcsize(woff2DirectoryFormat) + +woff2KnownTags = ( + "cmap", "head", "hhea", "hmtx", "maxp", "name", "OS/2", "post", "cvt ", + "fpgm", "glyf", "loca", "prep", "CFF ", "VORG", "EBDT", "EBLC", "gasp", + "hdmx", "kern", "LTSH", "PCLT", "VDMX", "vhea", "vmtx", "BASE", "GDEF", + "GPOS", "GSUB", "EBSC", "JSTF", "MATH", "CBDT", "CBLC", "COLR", "CPAL", + "SVG ", "sbix", "acnt", "avar", "bdat", "bloc", "bsln", "cvar", "fdsc", + "feat", "fmtx", "fvar", "gvar", "hsty", "just", "lcar", "mort", "morx", + "opbd", "prop", "trak", "Zapf", "Silf", "Glat", "Gloc", "Feat", "Sill") + +woff2FlagsFormat = """ + > # big endian + flags: B # table type and flags +""" + +woff2FlagsSize = sstruct.calcsize(woff2FlagsFormat) + +woff2UnknownTagFormat = """ + > # big endian + tag: 4s # 4-byte tag (optional) +""" + +woff2UnknownTagSize = sstruct.calcsize(woff2UnknownTagFormat) + +woff2UnknownTagIndex = 0x3F + +woff2Base128MaxSize = 5 +woff2DirectoryEntryMaxSize = woff2FlagsSize + woff2UnknownTagSize + 2 * woff2Base128MaxSize + +woff2TransformedTableTags = ('glyf', 'loca') + +woff2GlyfTableFormat = """ + > # big endian + version: L # = 0x00000000 + numGlyphs: H # Number of glyphs + indexFormat: H # Offset format for loca table + nContourStreamSize: L # Size of nContour stream + nPointsStreamSize: L # Size of nPoints stream + flagStreamSize: L # Size of flag stream + glyphStreamSize: L # Size of glyph stream + compositeStreamSize: L # Size of composite stream + bboxStreamSize: L # Comnined size of bboxBitmap and bboxStream + instructionStreamSize: L # Size of instruction stream +""" + +woff2GlyfTableFormatSize = sstruct.calcsize(woff2GlyfTableFormat) + +bboxFormat = """ + > # big endian + xMin: h + yMin: h + xMax: h + yMax: h +""" + + +def getKnownTagIndex(tag): + """Return index of 'tag' in woff2KnownTags list. Return 63 if not found.""" + for i in range(len(woff2KnownTags)): + if tag == woff2KnownTags[i]: + return i + return woff2UnknownTagIndex + + +class WOFF2DirectoryEntry(DirectoryEntry): + + def fromFile(self, file): + pos = file.tell() + data = file.read(woff2DirectoryEntryMaxSize) + left = self.fromString(data) + consumed = len(data) - len(left) + file.seek(pos + consumed) + + def fromString(self, data): + if len(data) < 1: + raise TTLibError("can't read table 'flags': not enough data") + dummy, data = sstruct.unpack2(woff2FlagsFormat, data, self) + if self.flags & 0x3F == 0x3F: + # if bits [0..5] of the flags byte == 63, read a 4-byte arbitrary tag value + if len(data) < woff2UnknownTagSize: + raise TTLibError("can't read table 'tag': not enough data") + dummy, data = sstruct.unpack2(woff2UnknownTagFormat, data, self) + else: + # otherwise, tag is derived from a fixed 'Known Tags' table + self.tag = woff2KnownTags[self.flags & 0x3F] + self.tag = Tag(self.tag) + if self.flags & 0xC0 != 0: + raise TTLibError('bits 6-7 are reserved and must be 0') + self.origLength, data = unpackBase128(data) + self.length = self.origLength + if self.tag in woff2TransformedTableTags: + self.length, data = unpackBase128(data) + if self.tag == 'loca' and self.length != 0: + raise TTLibError( + "the transformLength of the 'loca' table must be 0") + # return left over data + return data + + def toString(self): + data = bytechr(self.flags) + if (self.flags & 0x3F) == 0x3F: + data += struct.pack('>4s', self.tag.tobytes()) + data += packBase128(self.origLength) + if self.tag in woff2TransformedTableTags: + data += packBase128(self.length) + return data + + +class WOFF2LocaTable(getTableClass('loca')): + """Same as parent class. The only difference is that it attempts to preserve + the 'indexFormat' as encoded in the WOFF2 glyf table. + """ + + def __init__(self, tag=None): + self.tableTag = Tag(tag or 'loca') + + def compile(self, ttFont): + try: + max_location = max(self.locations) + except AttributeError: + self.set([]) + max_location = 0 + if 'glyf' in ttFont and hasattr(ttFont['glyf'], 'indexFormat'): + # copile loca using the indexFormat specified in the WOFF2 glyf table + indexFormat = ttFont['glyf'].indexFormat + if indexFormat == 0: + if max_location >= 0x20000: + raise TTLibError("indexFormat is 0 but local offsets > 0x20000") + if not all(l % 2 == 0 for l in self.locations): + raise TTLibError("indexFormat is 0 but local offsets not multiples of 2") + locations = array.array("H") + for i in range(len(self.locations)): + locations.append(self.locations[i] // 2) + else: + locations = array.array("I", self.locations) + if sys.byteorder != "big": + locations.byteswap() + data = locations.tostring() + else: + # use the most compact indexFormat given the current glyph offsets + data = super(WOFF2LocaTable, self).compile(ttFont) + return data + + +class WOFF2GlyfTable(getTableClass('glyf')): + """Decoder/Encoder for WOFF2 'glyf' table transform.""" + + subStreams = ( + 'nContourStream', 'nPointsStream', 'flagStream', 'glyphStream', + 'compositeStream', 'bboxStream', 'instructionStream') + + def __init__(self, tag=None): + self.tableTag = Tag(tag or 'glyf') + + def reconstruct(self, data, ttFont): + """ Decompile transformed 'glyf' data. """ + inputDataSize = len(data) + + if inputDataSize < woff2GlyfTableFormatSize: + raise TTLibError("not enough 'glyf' data") + dummy, data = sstruct.unpack2(woff2GlyfTableFormat, data, self) + offset = woff2GlyfTableFormatSize + + for stream in self.subStreams: + size = getattr(self, stream + 'Size') + setattr(self, stream, data[:size]) + data = data[size:] + offset += size + + if offset != inputDataSize: + raise TTLibError( + "incorrect size of transformed 'glyf' table: expected %d, received %d bytes" + % (offset, inputDataSize)) + + bboxBitmapSize = ((self.numGlyphs + 31) >> 5) << 2 + bboxBitmap = self.bboxStream[:bboxBitmapSize] + self.bboxBitmap = array.array('B', bboxBitmap) + self.bboxStream = self.bboxStream[bboxBitmapSize:] + + self.nContourStream = array.array("h", self.nContourStream) + if sys.byteorder != "big": + self.nContourStream.byteswap() + assert len(self.nContourStream) == self.numGlyphs + + if 'head' in ttFont: + ttFont['head'].indexToLocFormat = self.indexFormat + try: + self.glyphOrder = ttFont.getGlyphOrder() + except: + self.glyphOrder = None + if self.glyphOrder is None: + self.glyphOrder = [".notdef"] + self.glyphOrder.extend(["glyph%.5d" % i for i in range(1, self.numGlyphs)]) + else: + if len(self.glyphOrder) != self.numGlyphs: + raise TTLibError( + "incorrect glyphOrder: expected %d glyphs, found %d" % + (len(self.glyphOrder), self.numGlyphs)) + + glyphs = self.glyphs = {} + for glyphID, glyphName in enumerate(self.glyphOrder): + glyph = self._decodeGlyph(glyphID) + glyphs[glyphName] = glyph + + def transform(self, ttFont): + """ Return transformed 'glyf' data """ + self.numGlyphs = len(self.glyphs) + if not hasattr(self, "glyphOrder"): + try: + self.glyphOrder = ttFont.getGlyphOrder() + except: + self.glyphOrder = None + if self.glyphOrder is None: + self.glyphOrder = [".notdef"] + self.glyphOrder.extend(["glyph%.5d" % i for i in range(1, self.numGlyphs)]) + if len(self.glyphOrder) != self.numGlyphs: + raise TTLibError( + "incorrect glyphOrder: expected %d glyphs, found %d" % + (len(self.glyphOrder), self.numGlyphs)) + + if 'maxp' in ttFont: + ttFont['maxp'].numGlyphs = self.numGlyphs + self.indexFormat = ttFont['head'].indexToLocFormat + + for stream in self.subStreams: + setattr(self, stream, b"") + bboxBitmapSize = ((self.numGlyphs + 31) >> 5) << 2 + self.bboxBitmap = array.array('B', [0]*bboxBitmapSize) + + for glyphID in range(self.numGlyphs): + self._encodeGlyph(glyphID) + + self.bboxStream = self.bboxBitmap.tostring() + self.bboxStream + for stream in self.subStreams: + setattr(self, stream + 'Size', len(getattr(self, stream))) + self.version = 0 + data = sstruct.pack(woff2GlyfTableFormat, self) + data += bytesjoin([getattr(self, s) for s in self.subStreams]) + return data + + def _decodeGlyph(self, glyphID): + glyph = getTableModule('glyf').Glyph() + glyph.numberOfContours = self.nContourStream[glyphID] + if glyph.numberOfContours == 0: + return glyph + elif glyph.isComposite(): + self._decodeComponents(glyph) + else: + self._decodeCoordinates(glyph) + self._decodeBBox(glyphID, glyph) + return glyph + + def _decodeComponents(self, glyph): + data = self.compositeStream + glyph.components = [] + more = 1 + haveInstructions = 0 + while more: + component = getTableModule('glyf').GlyphComponent() + more, haveInstr, data = component.decompile(data, self) + haveInstructions = haveInstructions | haveInstr + glyph.components.append(component) + self.compositeStream = data + if haveInstructions: + self._decodeInstructions(glyph) + + def _decodeCoordinates(self, glyph): + data = self.nPointsStream + endPtsOfContours = [] + endPoint = -1 + for i in range(glyph.numberOfContours): + ptsOfContour, data = unpack255UShort(data) + endPoint += ptsOfContour + endPtsOfContours.append(endPoint) + glyph.endPtsOfContours = endPtsOfContours + self.nPointsStream = data + self._decodeTriplets(glyph) + self._decodeInstructions(glyph) + + def _decodeInstructions(self, glyph): + glyphStream = self.glyphStream + instructionStream = self.instructionStream + instructionLength, glyphStream = unpack255UShort(glyphStream) + glyph.program = ttProgram.Program() + glyph.program.fromBytecode(instructionStream[:instructionLength]) + self.glyphStream = glyphStream + self.instructionStream = instructionStream[instructionLength:] + + def _decodeBBox(self, glyphID, glyph): + haveBBox = bool(self.bboxBitmap[glyphID >> 3] & (0x80 >> (glyphID & 7))) + if glyph.isComposite() and not haveBBox: + raise TTLibError('no bbox values for composite glyph %d' % glyphID) + if haveBBox: + dummy, self.bboxStream = sstruct.unpack2(bboxFormat, self.bboxStream, glyph) + else: + glyph.recalcBounds(self) + + def _decodeTriplets(self, glyph): + + def withSign(flag, baseval): + assert 0 <= baseval and baseval < 65536, 'integer overflow' + return baseval if flag & 1 else -baseval + + nPoints = glyph.endPtsOfContours[-1] + 1 + flagSize = nPoints + if flagSize > len(self.flagStream): + raise TTLibError("not enough 'flagStream' data") + flagsData = self.flagStream[:flagSize] + self.flagStream = self.flagStream[flagSize:] + flags = array.array('B', flagsData) + + triplets = array.array('B', self.glyphStream) + nTriplets = len(triplets) + assert nPoints <= nTriplets + + x = 0 + y = 0 + glyph.coordinates = getTableModule('glyf').GlyphCoordinates.zeros(nPoints) + glyph.flags = array.array("B") + tripletIndex = 0 + for i in range(nPoints): + flag = flags[i] + onCurve = not bool(flag >> 7) + flag &= 0x7f + if flag < 84: + nBytes = 1 + elif flag < 120: + nBytes = 2 + elif flag < 124: + nBytes = 3 + else: + nBytes = 4 + assert ((tripletIndex + nBytes) <= nTriplets) + if flag < 10: + dx = 0 + dy = withSign(flag, ((flag & 14) << 7) + triplets[tripletIndex]) + elif flag < 20: + dx = withSign(flag, (((flag - 10) & 14) << 7) + triplets[tripletIndex]) + dy = 0 + elif flag < 84: + b0 = flag - 20 + b1 = triplets[tripletIndex] + dx = withSign(flag, 1 + (b0 & 0x30) + (b1 >> 4)) + dy = withSign(flag >> 1, 1 + ((b0 & 0x0c) << 2) + (b1 & 0x0f)) + elif flag < 120: + b0 = flag - 84 + dx = withSign(flag, 1 + ((b0 // 12) << 8) + triplets[tripletIndex]) + dy = withSign(flag >> 1, + 1 + (((b0 % 12) >> 2) << 8) + triplets[tripletIndex + 1]) + elif flag < 124: + b2 = triplets[tripletIndex + 1] + dx = withSign(flag, (triplets[tripletIndex] << 4) + (b2 >> 4)) + dy = withSign(flag >> 1, + ((b2 & 0x0f) << 8) + triplets[tripletIndex + 2]) + else: + dx = withSign(flag, + (triplets[tripletIndex] << 8) + triplets[tripletIndex + 1]) + dy = withSign(flag >> 1, + (triplets[tripletIndex + 2] << 8) + triplets[tripletIndex + 3]) + tripletIndex += nBytes + x += dx + y += dy + glyph.coordinates[i] = (x, y) + glyph.flags.append(int(onCurve)) + bytesConsumed = tripletIndex + self.glyphStream = self.glyphStream[bytesConsumed:] + + def _encodeGlyph(self, glyphID): + glyphName = self.getGlyphName(glyphID) + glyph = self[glyphName] + self.nContourStream += struct.pack(">h", glyph.numberOfContours) + if glyph.numberOfContours == 0: + return + elif glyph.isComposite(): + self._encodeComponents(glyph) + else: + self._encodeCoordinates(glyph) + self._encodeBBox(glyphID, glyph) + + def _encodeComponents(self, glyph): + lastcomponent = len(glyph.components) - 1 + more = 1 + haveInstructions = 0 + for i in range(len(glyph.components)): + if i == lastcomponent: + haveInstructions = hasattr(glyph, "program") + more = 0 + component = glyph.components[i] + self.compositeStream += component.compile(more, haveInstructions, self) + if haveInstructions: + self._encodeInstructions(glyph) + + def _encodeCoordinates(self, glyph): + lastEndPoint = -1 + for endPoint in glyph.endPtsOfContours: + ptsOfContour = endPoint - lastEndPoint + self.nPointsStream += pack255UShort(ptsOfContour) + lastEndPoint = endPoint + self._encodeTriplets(glyph) + self._encodeInstructions(glyph) + + def _encodeInstructions(self, glyph): + instructions = glyph.program.getBytecode() + self.glyphStream += pack255UShort(len(instructions)) + self.instructionStream += instructions + + def _encodeBBox(self, glyphID, glyph): + assert glyph.numberOfContours != 0, "empty glyph has no bbox" + if not glyph.isComposite(): + # for simple glyphs, compare the encoded bounding box info with the calculated + # values, and if they match omit the bounding box info + currentBBox = glyph.xMin, glyph.yMin, glyph.xMax, glyph.yMax + calculatedBBox = calcIntBounds(glyph.coordinates) + if currentBBox == calculatedBBox: + return + self.bboxBitmap[glyphID >> 3] |= 0x80 >> (glyphID & 7) + self.bboxStream += sstruct.pack(bboxFormat, glyph) + + def _encodeTriplets(self, glyph): + assert len(glyph.coordinates) == len(glyph.flags) + coordinates = glyph.coordinates.copy() + coordinates.absoluteToRelative() + + flags = array.array('B') + triplets = array.array('B') + for i in range(len(coordinates)): + onCurve = glyph.flags[i] + x, y = coordinates[i] + absX = abs(x) + absY = abs(y) + onCurveBit = 0 if onCurve else 128 + xSignBit = 0 if (x < 0) else 1 + ySignBit = 0 if (y < 0) else 1 + xySignBits = xSignBit + 2 * ySignBit + + if x == 0 and absY < 1280: + flags.append(onCurveBit + ((absY & 0xf00) >> 7) + ySignBit) + triplets.append(absY & 0xff) + elif y == 0 and absX < 1280: + flags.append(onCurveBit + 10 + ((absX & 0xf00) >> 7) + xSignBit) + triplets.append(absX & 0xff) + elif absX < 65 and absY < 65: + flags.append(onCurveBit + 20 + ((absX - 1) & 0x30) + (((absY - 1) & 0x30) >> 2) + xySignBits) + triplets.append((((absX - 1) & 0xf) << 4) | ((absY - 1) & 0xf)) + elif absX < 769 and absY < 769: + flags.append(onCurveBit + 84 + 12 * (((absX - 1) & 0x300) >> 8) + (((absY - 1) & 0x300) >> 6) + xySignBits) + triplets.append((absX - 1) & 0xff) + triplets.append((absY - 1) & 0xff) + elif absX < 4096 and absY < 4096: + flags.append(onCurveBit + 120 + xySignBits) + triplets.append(absX >> 4) + triplets.append(((absX & 0xf) << 4) | (absY >> 8)) + triplets.append(absY & 0xff) + else: + flags.append(onCurveBit + 124 + xySignBits) + triplets.append(absX >> 8) + triplets.append(absX & 0xff) + triplets.append(absY >> 8) + triplets.append(absY & 0xff) + + self.flagStream += flags.tostring() + self.glyphStream += triplets.tostring() + + +class WOFF2FlavorData(WOFFFlavorData): + + Flavor = 'woff2' + + def __init__(self, reader=None): + if not haveBrotli: + raise ImportError("No module named brotli") + self.majorVersion = None + self.minorVersion = None + self.metaData = None + self.privData = None + if reader: + self.majorVersion = reader.majorVersion + self.minorVersion = reader.minorVersion + if reader.metaLength: + reader.file.seek(reader.metaOffset) + rawData = reader.file.read(reader.metaLength) + assert len(rawData) == reader.metaLength + data = brotli.decompress(rawData) + assert len(data) == reader.metaOrigLength + self.metaData = data + if reader.privLength: + reader.file.seek(reader.privOffset) + data = reader.file.read(reader.privLength) + assert len(data) == reader.privLength + self.privData = data + + +def unpackBase128(data): + r""" Read one to five bytes from UIntBase128-encoded input string, and return + a tuple containing the decoded integer plus any leftover data. + + >>> unpackBase128(b'\x3f\x00\x00') == (63, b"\x00\x00") + True + >>> unpackBase128(b'\x8f\xff\xff\xff\x7f')[0] == 4294967295 + True + >>> unpackBase128(b'\x80\x80\x3f') # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + File "<stdin>", line 1, in ? + TTLibError: UIntBase128 value must not start with leading zeros + >>> unpackBase128(b'\x8f\xff\xff\xff\xff\x7f')[0] # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + File "<stdin>", line 1, in ? + TTLibError: UIntBase128-encoded sequence is longer than 5 bytes + >>> unpackBase128(b'\x90\x80\x80\x80\x00')[0] # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + File "<stdin>", line 1, in ? + TTLibError: UIntBase128 value exceeds 2**32-1 + """ + if len(data) == 0: + raise TTLibError('not enough data to unpack UIntBase128') + result = 0 + if byteord(data[0]) == 0x80: + # font must be rejected if UIntBase128 value starts with 0x80 + raise TTLibError('UIntBase128 value must not start with leading zeros') + for i in range(woff2Base128MaxSize): + if len(data) == 0: + raise TTLibError('not enough data to unpack UIntBase128') + code = byteord(data[0]) + data = data[1:] + # if any of the top seven bits are set then we're about to overflow + if result & 0xFE000000: + raise TTLibError('UIntBase128 value exceeds 2**32-1') + # set current value = old value times 128 bitwise-or (byte bitwise-and 127) + result = (result << 7) | (code & 0x7f) + # repeat until the most significant bit of byte is false + if (code & 0x80) == 0: + # return result plus left over data + return result, data + # make sure not to exceed the size bound + raise TTLibError('UIntBase128-encoded sequence is longer than 5 bytes') + + +def base128Size(n): + """ Return the length in bytes of a UIntBase128-encoded sequence with value n. + + >>> base128Size(0) + 1 + >>> base128Size(24567) + 3 + >>> base128Size(2**32-1) + 5 + """ + assert n >= 0 + size = 1 + while n >= 128: + size += 1 + n >>= 7 + return size + + +def packBase128(n): + r""" Encode unsigned integer in range 0 to 2**32-1 (inclusive) to a string of + bytes using UIntBase128 variable-length encoding. Produce the shortest possible + encoding. + + >>> packBase128(63) == b"\x3f" + True + >>> packBase128(2**32-1) == b'\x8f\xff\xff\xff\x7f' + True + """ + if n < 0 or n >= 2**32: + raise TTLibError( + "UIntBase128 format requires 0 <= integer <= 2**32-1") + data = b'' + size = base128Size(n) + for i in range(size): + b = (n >> (7 * (size - i - 1))) & 0x7f + if i < size - 1: + b |= 0x80 + data += struct.pack('B', b) + return data + + +def unpack255UShort(data): + """ Read one to three bytes from 255UInt16-encoded input string, and return a + tuple containing the decoded integer plus any leftover data. + + >>> unpack255UShort(bytechr(252))[0] + 252 + + Note that some numbers (e.g. 506) can have multiple encodings: + >>> unpack255UShort(struct.pack("BB", 254, 0))[0] + 506 + >>> unpack255UShort(struct.pack("BB", 255, 253))[0] + 506 + >>> unpack255UShort(struct.pack("BBB", 253, 1, 250))[0] + 506 + """ + code = byteord(data[:1]) + data = data[1:] + if code == 253: + # read two more bytes as an unsigned short + if len(data) < 2: + raise TTLibError('not enough data to unpack 255UInt16') + result, = struct.unpack(">H", data[:2]) + data = data[2:] + elif code == 254: + # read another byte, plus 253 * 2 + if len(data) == 0: + raise TTLibError('not enough data to unpack 255UInt16') + result = byteord(data[:1]) + result += 506 + data = data[1:] + elif code == 255: + # read another byte, plus 253 + if len(data) == 0: + raise TTLibError('not enough data to unpack 255UInt16') + result = byteord(data[:1]) + result += 253 + data = data[1:] + else: + # leave as is if lower than 253 + result = code + # return result plus left over data + return result, data + + +def pack255UShort(value): + r""" Encode unsigned integer in range 0 to 65535 (inclusive) to a bytestring + using 255UInt16 variable-length encoding. + + >>> pack255UShort(252) == b'\xfc' + True + >>> pack255UShort(506) == b'\xfe\x00' + True + >>> pack255UShort(762) == b'\xfd\x02\xfa' + True + """ + if value < 0 or value > 0xFFFF: + raise TTLibError( + "255UInt16 format requires 0 <= integer <= 65535") + if value < 253: + return struct.pack(">B", value) + elif value < 506: + return struct.pack(">BB", 255, value - 253) + elif value < 762: + return struct.pack(">BB", 254, value - 506) + else: + return struct.pack(">BH", 253, value) + + +if __name__ == "__main__": + import doctest + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/ttx.py b/Lib/fontTools/ttx.py index e0b5edd..0e3eaf3 100644 --- a/Lib/fontTools/ttx.py +++ b/Lib/fontTools/ttx.py @@ -1,21 +1,24 @@ """\ usage: ttx [options] inputfile1 [... inputfileN] - TTX %s -- From OpenType To XML And Back + TTX -- From OpenType To XML And Back If an input file is a TrueType or OpenType font file, it will be - dumped to an TTX file (an XML-based text format). - If an input file is a TTX file, it will be compiled to a TrueType - or OpenType font file. + decompiled to a TTX file (an XML-based text format). + If an input file is a TTX file, it will be compiled to whatever + format the data is in, a TrueType or OpenType/CFF font file. Output files are created so they are unique: an existing file is never overwritten. General options: - -h Help: print this message + -h Help: print this message. + --version: show version and exit. -d <outputfolder> Specify a directory where the output files are to be created. - -o <outputfile> Specify a file to write the output to. + -o <outputfile> Specify a file to write the output to. A special + value of - would use the standard output. + -f Overwrite existing output file(s), ie. don't append numbers. -v Verbose: more messages will be written to stdout about what is being done. -q Quiet: No messages will be written to stdout about what @@ -35,6 +38,10 @@ usage: ttx [options] inputfile1 [... inputfileN] to the individual table dumps. This file can be used as input to ttx, as long as the table files are in the same directory. + -g Split glyf table: Save the glyf data into separate TTX files + per glyph and write a small TTX for the glyf table which + contains references to the individual TTGlyph elements. + NOTE: specifying -g implies -s (no need for -s together with -g) -i Do NOT disassemble TT instructions: when this option is given, all TrueType programs (glyph programs, the font program and the pre-program) will be written to the TTX file as hex data @@ -50,12 +57,17 @@ usage: ttx [options] inputfile1 [... inputfileN] -z bitwise * export each row as binary in an ASCII art style -z extfile - * export the data as external files with XML refences + * export the data as external files with XML references If no export format is specified 'raw' format is used. -e Don't ignore decompilation errors, but show a full traceback and abort. - -y <number> Select font number for TrueType Collection, + -y <number> Select font number for TrueType Collection (.ttc/.otc), starting from 0. + --unicodedata <UnicodeData.txt> Use custom database file to write + character names in the comments of the cmap TTX output. + --newline <value> Control how line endings are written in the XML + file. It can be 'LF', 'CR', or 'CRLF'. If not specified, the + default platform-specific line endings are used. Compile options: -m Merge with TrueType-input-file: specify a TrueType or OpenType @@ -63,6 +75,13 @@ usage: ttx [options] inputfile1 [... inputfileN] valid when at most one TTX file is specified. -b Don't recalc glyph bounding boxes: use the values in the TTX file as-is. + --recalc-timestamp Set font 'modified' timestamp to current time. + By default, the modification time of the TTX file will be used. + --flavor <type> Specify flavor of output font file. May be 'woff' + or 'woff2'. Note that WOFF2 requires the Brotli Python extension, + available at https://github.com/google/brotli + --with-zopfli Use Zopfli instead of Zlib to compress WOFF. The Python + extension is available at https://pypi.python.org/pypi/zopfli """ @@ -70,32 +89,20 @@ from __future__ import print_function, division, absolute_import from fontTools.misc.py23 import * from fontTools.ttLib import TTFont, TTLibError from fontTools.misc.macCreatorType import getMacCreatorAndType +from fontTools.unicode import setUnicodeData +from fontTools.misc.timeTools import timestampSinceEpoch +from fontTools.misc.loggingTools import Timer +from fontTools.misc.cliTools import makeOutputFileName import os import sys import getopt import re +import logging -def usage(): - from fontTools import version - print(__doc__ % version) - sys.exit(2) - -numberAddedRE = re.compile("#\d+$") -opentypeheaderRE = re.compile('''sfntVersion=['"]OTTO["']''') +log = logging.getLogger("fontTools.ttx") -def makeOutputFileName(input, outputDir, extension): - dirName, fileName = os.path.split(input) - fileName, ext = os.path.splitext(fileName) - if outputDir: - dirName = outputDir - fileName = numberAddedRE.split(fileName)[0] - output = os.path.join(dirName, fileName + extension) - n = 1 - while os.path.exists(output): - output = os.path.join(dirName, fileName + "#" + repr(n) + extension) - n = n + 1 - return output +opentypeheaderRE = re.compile('''sfntVersion=['"]OTTO["']''') class Options(object): @@ -103,15 +110,22 @@ class Options(object): listTables = False outputDir = None outputFile = None + overWrite = False verbose = False quiet = False splitTables = False + splitGlyphs = False disassembleInstructions = True mergeFile = None recalcBBoxes = True allowVID = False ignoreDecompileErrors = True bitmapGlyphDataFormat = 'raw' + unicodedata = None + newlinestr = None + recalcTimestamp = False + flavor = None + useZopfli = False def __init__(self, rawOptions, numFiles): self.onlyTables = [] @@ -120,16 +134,20 @@ class Options(object): for option, value in rawOptions: # general options if option == "-h": + print(__doc__) + sys.exit(0) + elif option == "--version": from fontTools import version - print(__doc__ % version) + print(version) sys.exit(0) elif option == "-d": if not os.path.isdir(value): - print("The -d option value must be an existing directory") - sys.exit(2) + raise getopt.GetoptError("The -d option value must be an existing directory") self.outputDir = value elif option == "-o": self.outputFile = value + elif option == "-f": + self.overWrite = True elif option == "-v": self.verbose = True elif option == "-q": @@ -138,18 +156,26 @@ class Options(object): elif option == "-l": self.listTables = True elif option == "-t": + # pad with space if table tag length is less than 4 + value = value.ljust(4) self.onlyTables.append(value) elif option == "-x": + # pad with space if table tag length is less than 4 + value = value.ljust(4) self.skipTables.append(value) elif option == "-s": self.splitTables = True + elif option == "-g": + # -g implies (and forces) splitTables + self.splitGlyphs = True + self.splitTables = True elif option == "-i": self.disassembleInstructions = False elif option == "-z": validOptions = ('raw', 'row', 'bitwise', 'extfile') if value not in validOptions: - print("-z does not allow %s as a format. Use %s" % (option, validOptions)) - sys.exit(2) + raise getopt.GetoptError( + "-z does not allow %s as a format. Use %s" % (option, validOptions)) self.bitmapGlyphDataFormat = value elif option == "-y": self.fontNumber = int(value) @@ -162,12 +188,42 @@ class Options(object): self.allowVID = True elif option == "-e": self.ignoreDecompileErrors = False + elif option == "--unicodedata": + self.unicodedata = value + elif option == "--newline": + validOptions = ('LF', 'CR', 'CRLF') + if value == "LF": + self.newlinestr = "\n" + elif value == "CR": + self.newlinestr = "\r" + elif value == "CRLF": + self.newlinestr = "\r\n" + else: + raise getopt.GetoptError( + "Invalid choice for --newline: %r (choose from %s)" + % (value, ", ".join(map(repr, validOptions)))) + elif option == "--recalc-timestamp": + self.recalcTimestamp = True + elif option == "--flavor": + self.flavor = value + elif option == "--with-zopfli": + self.useZopfli = True + if self.verbose and self.quiet: + raise getopt.GetoptError("-q and -v options are mutually exclusive") + if self.verbose: + self.logLevel = logging.DEBUG + elif self.quiet: + self.logLevel = logging.WARNING + else: + self.logLevel = logging.INFO + if self.mergeFile and self.flavor: + raise getopt.GetoptError("-m and --flavor options are mutually exclusive") if self.onlyTables and self.skipTables: - print("-t and -x options are mutually exclusive") - sys.exit(2) + raise getopt.GetoptError("-t and -x options are mutually exclusive") if self.mergeFile and numFiles > 1: - print("Must specify exactly one TTX source file when using -m") - sys.exit(2) + raise getopt.GetoptError("Must specify exactly one TTX source file when using -m") + if self.flavor != 'woff' and self.useZopfli: + raise getopt.GetoptError("--with-zopfli option requires --flavor 'woff'") def ttList(input, output, options): @@ -175,12 +231,18 @@ def ttList(input, output, options): reader = ttf.reader tags = sorted(reader.keys()) print('Listing table info for "%s":' % input) - format = " %4s %10s %7s %7s" - print(format % ("tag ", " checksum", " length", " offset")) - print(format % ("----", "----------", "-------", "-------")) + format = " %4s %10s %8s %8s" + print(format % ("tag ", " checksum", " length", " offset")) + print(format % ("----", "----------", "--------", "--------")) for tag in tags: entry = reader.tables[tag] - checkSum = int(entry.checkSum) + if ttf.flavor == "woff2": + # WOFF2 doesn't store table checksums, so they must be calculated + from fontTools.ttLib.sfnt import calcChecksum + data = entry.loadData(reader.transformBuffer) + checkSum = calcChecksum(data) + else: + checkSum = int(entry.checkSum) if checkSum < 0: checkSum = checkSum + 0x100000000 checksum = "0x%08X" % checkSum @@ -189,35 +251,43 @@ def ttList(input, output, options): ttf.close() +@Timer(log, 'Done dumping TTX in %(time).3f seconds') def ttDump(input, output, options): - if not options.quiet: - print('Dumping "%s" to "%s"...' % (input, output)) - ttf = TTFont(input, 0, verbose=options.verbose, allowVID=options.allowVID, - quiet=options.quiet, + log.info('Dumping "%s" to "%s"...', input, output) + if options.unicodedata: + setUnicodeData(options.unicodedata) + ttf = TTFont(input, 0, allowVID=options.allowVID, ignoreDecompileErrors=options.ignoreDecompileErrors, fontNumber=options.fontNumber) ttf.saveXML(output, - quiet=options.quiet, tables=options.onlyTables, skipTables=options.skipTables, splitTables=options.splitTables, + splitGlyphs=options.splitGlyphs, disassembleInstructions=options.disassembleInstructions, - bitmapGlyphDataFormat=options.bitmapGlyphDataFormat) + bitmapGlyphDataFormat=options.bitmapGlyphDataFormat, + newlinestr=options.newlinestr) ttf.close() +@Timer(log, 'Done compiling TTX in %(time).3f seconds') def ttCompile(input, output, options): - if not options.quiet: - print('Compiling "%s" to "%s"...' % (input, output)) - ttf = TTFont(options.mergeFile, + log.info('Compiling "%s" to "%s"...' % (input, output)) + if options.useZopfli: + from fontTools.ttLib import sfnt + sfnt.USE_ZOPFLI = True + ttf = TTFont(options.mergeFile, flavor=options.flavor, recalcBBoxes=options.recalcBBoxes, - verbose=options.verbose, allowVID=options.allowVID) - ttf.importXML(input, quiet=options.quiet) - ttf.save(output) + recalcTimestamp=options.recalcTimestamp, + allowVID=options.allowVID) + ttf.importXML(input) - if options.verbose: - import time - print("finished at", time.strftime("%H:%M:%S", time.localtime(time.time()))) + if not options.recalcTimestamp and 'head' in ttf: + # use TTX file modification time for head "modified" timestamp + mtime = os.path.getmtime(input) + ttf['head'].modified = timestampSinceEpoch(mtime) + + ttf.save(output) def guessFileType(fileName): @@ -226,12 +296,15 @@ def guessFileType(fileName): f = open(fileName, "rb") except IOError: return None + header = f.read(256) + f.close() + if header.startswith(b'\xef\xbb\xbf<?xml'): + header = header.lstrip(b'\xef\xbb\xbf') cr, tp = getMacCreatorAndType(fileName) if tp in ("sfnt", "FFIL"): return "TTF" if ext == ".dfont": return "TTF" - header = f.read(256) head = Tag(header[:4]) if head == "OTTO": return "OTF" @@ -241,7 +314,9 @@ def guessFileType(fileName): return "TTF" elif head == "wOFF": return "WOFF" - elif head.lower() == "<?xm": + elif head == "wOF2": + return "WOFF2" + elif head == "<?xm": # Use 'latin1' because that can't fail. header = tostr(header, 'latin1') if opentypeheaderRE.search(header): @@ -252,39 +327,42 @@ def guessFileType(fileName): def parseOptions(args): - try: - rawOptions, files = getopt.getopt(args, "ld:o:vqht:x:sim:z:baey:") - except getopt.GetoptError: - usage() - - if not files: - usage() - + rawOptions, files = getopt.getopt(args, "ld:o:fvqht:x:sgim:z:baey:", + ['unicodedata=', "recalc-timestamp", 'flavor=', 'version', + 'with-zopfli', 'newline=']) + options = Options(rawOptions, len(files)) jobs = [] - + + if not files: + raise getopt.GetoptError('Must specify at least one input file') + for input in files: + if not os.path.isfile(input): + raise getopt.GetoptError('File not found: "%s"' % input) tp = guessFileType(input) - if tp in ("OTF", "TTF", "TTC", "WOFF"): + if tp in ("OTF", "TTF", "TTC", "WOFF", "WOFF2"): extension = ".ttx" if options.listTables: action = ttList else: action = ttDump elif tp == "TTX": - extension = ".ttf" + extension = "."+options.flavor if options.flavor else ".ttf" action = ttCompile elif tp == "OTX": - extension = ".otf" + extension = "."+options.flavor if options.flavor else ".otf" action = ttCompile else: - print('Unknown file type: "%s"' % input) - continue - + raise getopt.GetoptError('Unknown file type: "%s"' % input) + if options.outputFile: output = options.outputFile else: - output = makeOutputFileName(input, options.outputDir, extension) + output = makeOutputFileName(input, options.outputDir, extension, options.overWrite) + # 'touch' output file to avoid race condition in choosing file names + if action != ttList: + open(output, 'a').close() jobs.append((action, input, output)) return jobs, options @@ -298,32 +376,42 @@ def waitForKeyPress(): """Force the DOS Prompt window to stay open so the user gets a chance to see what's wrong.""" import msvcrt - print('(Hit any key to exit)') + print('(Hit any key to exit)', file=sys.stderr) while not msvcrt.kbhit(): pass -def main(args): - jobs, options = parseOptions(args) +def main(args=None): + from fontTools import configLogger + + if args is None: + args = sys.argv[1:] + try: + jobs, options = parseOptions(args) + except getopt.GetoptError as e: + print("%s\nERROR: %s" % (__doc__, e), file=sys.stderr) + sys.exit(2) + + configLogger(level=options.logLevel) + try: process(jobs, options) except KeyboardInterrupt: - print("(Cancelled.)") + log.error("(Cancelled.)") + sys.exit(1) except SystemExit: if sys.platform == "win32": waitForKeyPress() - else: - raise + raise except TTLibError as e: - print("Error:",e) + log.error(e) + sys.exit(1) except: + log.exception('Unhandled exception has occurred') if sys.platform == "win32": - import traceback - traceback.print_exc() waitForKeyPress() - else: - raise - + sys.exit(1) + if __name__ == "__main__": - main(sys.argv[1:]) + sys.exit(main()) diff --git a/Lib/fontTools/unicode.py b/Lib/fontTools/unicode.py index b599051..50dfc53 100644 --- a/Lib/fontTools/unicode.py +++ b/Lib/fontTools/unicode.py @@ -30,7 +30,12 @@ class _UnicodeCustom(object): class _UnicodeBuiltin(object): def __getitem__(self, charCode): - import unicodedata + try: + # use unicodedata backport to python2, if available: + # https://github.com/mikekap/unicodedata2 + import unicodedata2 as unicodedata + except ImportError: + import unicodedata try: return unicodedata.name(unichr(charCode)) except ValueError: diff --git a/Lib/fontTools/unicodedata/Blocks.py b/Lib/fontTools/unicodedata/Blocks.py new file mode 100644 index 0000000..cba4e9e --- /dev/null +++ b/Lib/fontTools/unicodedata/Blocks.py @@ -0,0 +1,677 @@ +# -*- coding: utf-8 -*- +# +# NOTE: This file was auto-generated with MetaTools/buildUCD.py. +# Source: https://unicode.org/Public/UNIDATA/Blocks.txt +# License: http://unicode.org/copyright.html#License +# +# Blocks-10.0.0.txt +# Date: 2017-04-12, 17:30:00 GMT [KW] +# © 2017 Unicode®, Inc. +# For terms of use, see http://www.unicode.org/terms_of_use.html +# +# Unicode Character Database +# For documentation, see http://www.unicode.org/reports/tr44/ +# +# Format: +# Start Code..End Code; Block Name + + +RANGES = [ + 0x0000, # .. 0x007F ; Basic Latin + 0x0080, # .. 0x00FF ; Latin-1 Supplement + 0x0100, # .. 0x017F ; Latin Extended-A + 0x0180, # .. 0x024F ; Latin Extended-B + 0x0250, # .. 0x02AF ; IPA Extensions + 0x02B0, # .. 0x02FF ; Spacing Modifier Letters + 0x0300, # .. 0x036F ; Combining Diacritical Marks + 0x0370, # .. 0x03FF ; Greek and Coptic + 0x0400, # .. 0x04FF ; Cyrillic + 0x0500, # .. 0x052F ; Cyrillic Supplement + 0x0530, # .. 0x058F ; Armenian + 0x0590, # .. 0x05FF ; Hebrew + 0x0600, # .. 0x06FF ; Arabic + 0x0700, # .. 0x074F ; Syriac + 0x0750, # .. 0x077F ; Arabic Supplement + 0x0780, # .. 0x07BF ; Thaana + 0x07C0, # .. 0x07FF ; NKo + 0x0800, # .. 0x083F ; Samaritan + 0x0840, # .. 0x085F ; Mandaic + 0x0860, # .. 0x086F ; Syriac Supplement + 0x0870, # .. 0x089F ; No_Block + 0x08A0, # .. 0x08FF ; Arabic Extended-A + 0x0900, # .. 0x097F ; Devanagari + 0x0980, # .. 0x09FF ; Bengali + 0x0A00, # .. 0x0A7F ; Gurmukhi + 0x0A80, # .. 0x0AFF ; Gujarati + 0x0B00, # .. 0x0B7F ; Oriya + 0x0B80, # .. 0x0BFF ; Tamil + 0x0C00, # .. 0x0C7F ; Telugu + 0x0C80, # .. 0x0CFF ; Kannada + 0x0D00, # .. 0x0D7F ; Malayalam + 0x0D80, # .. 0x0DFF ; Sinhala + 0x0E00, # .. 0x0E7F ; Thai + 0x0E80, # .. 0x0EFF ; Lao + 0x0F00, # .. 0x0FFF ; Tibetan + 0x1000, # .. 0x109F ; Myanmar + 0x10A0, # .. 0x10FF ; Georgian + 0x1100, # .. 0x11FF ; Hangul Jamo + 0x1200, # .. 0x137F ; Ethiopic + 0x1380, # .. 0x139F ; Ethiopic Supplement + 0x13A0, # .. 0x13FF ; Cherokee + 0x1400, # .. 0x167F ; Unified Canadian Aboriginal Syllabics + 0x1680, # .. 0x169F ; Ogham + 0x16A0, # .. 0x16FF ; Runic + 0x1700, # .. 0x171F ; Tagalog + 0x1720, # .. 0x173F ; Hanunoo + 0x1740, # .. 0x175F ; Buhid + 0x1760, # .. 0x177F ; Tagbanwa + 0x1780, # .. 0x17FF ; Khmer + 0x1800, # .. 0x18AF ; Mongolian + 0x18B0, # .. 0x18FF ; Unified Canadian Aboriginal Syllabics Extended + 0x1900, # .. 0x194F ; Limbu + 0x1950, # .. 0x197F ; Tai Le + 0x1980, # .. 0x19DF ; New Tai Lue + 0x19E0, # .. 0x19FF ; Khmer Symbols + 0x1A00, # .. 0x1A1F ; Buginese + 0x1A20, # .. 0x1AAF ; Tai Tham + 0x1AB0, # .. 0x1AFF ; Combining Diacritical Marks Extended + 0x1B00, # .. 0x1B7F ; Balinese + 0x1B80, # .. 0x1BBF ; Sundanese + 0x1BC0, # .. 0x1BFF ; Batak + 0x1C00, # .. 0x1C4F ; Lepcha + 0x1C50, # .. 0x1C7F ; Ol Chiki + 0x1C80, # .. 0x1C8F ; Cyrillic Extended-C + 0x1C90, # .. 0x1CBF ; No_Block + 0x1CC0, # .. 0x1CCF ; Sundanese Supplement + 0x1CD0, # .. 0x1CFF ; Vedic Extensions + 0x1D00, # .. 0x1D7F ; Phonetic Extensions + 0x1D80, # .. 0x1DBF ; Phonetic Extensions Supplement + 0x1DC0, # .. 0x1DFF ; Combining Diacritical Marks Supplement + 0x1E00, # .. 0x1EFF ; Latin Extended Additional + 0x1F00, # .. 0x1FFF ; Greek Extended + 0x2000, # .. 0x206F ; General Punctuation + 0x2070, # .. 0x209F ; Superscripts and Subscripts + 0x20A0, # .. 0x20CF ; Currency Symbols + 0x20D0, # .. 0x20FF ; Combining Diacritical Marks for Symbols + 0x2100, # .. 0x214F ; Letterlike Symbols + 0x2150, # .. 0x218F ; Number Forms + 0x2190, # .. 0x21FF ; Arrows + 0x2200, # .. 0x22FF ; Mathematical Operators + 0x2300, # .. 0x23FF ; Miscellaneous Technical + 0x2400, # .. 0x243F ; Control Pictures + 0x2440, # .. 0x245F ; Optical Character Recognition + 0x2460, # .. 0x24FF ; Enclosed Alphanumerics + 0x2500, # .. 0x257F ; Box Drawing + 0x2580, # .. 0x259F ; Block Elements + 0x25A0, # .. 0x25FF ; Geometric Shapes + 0x2600, # .. 0x26FF ; Miscellaneous Symbols + 0x2700, # .. 0x27BF ; Dingbats + 0x27C0, # .. 0x27EF ; Miscellaneous Mathematical Symbols-A + 0x27F0, # .. 0x27FF ; Supplemental Arrows-A + 0x2800, # .. 0x28FF ; Braille Patterns + 0x2900, # .. 0x297F ; Supplemental Arrows-B + 0x2980, # .. 0x29FF ; Miscellaneous Mathematical Symbols-B + 0x2A00, # .. 0x2AFF ; Supplemental Mathematical Operators + 0x2B00, # .. 0x2BFF ; Miscellaneous Symbols and Arrows + 0x2C00, # .. 0x2C5F ; Glagolitic + 0x2C60, # .. 0x2C7F ; Latin Extended-C + 0x2C80, # .. 0x2CFF ; Coptic + 0x2D00, # .. 0x2D2F ; Georgian Supplement + 0x2D30, # .. 0x2D7F ; Tifinagh + 0x2D80, # .. 0x2DDF ; Ethiopic Extended + 0x2DE0, # .. 0x2DFF ; Cyrillic Extended-A + 0x2E00, # .. 0x2E7F ; Supplemental Punctuation + 0x2E80, # .. 0x2EFF ; CJK Radicals Supplement + 0x2F00, # .. 0x2FDF ; Kangxi Radicals + 0x2FE0, # .. 0x2FEF ; No_Block + 0x2FF0, # .. 0x2FFF ; Ideographic Description Characters + 0x3000, # .. 0x303F ; CJK Symbols and Punctuation + 0x3040, # .. 0x309F ; Hiragana + 0x30A0, # .. 0x30FF ; Katakana + 0x3100, # .. 0x312F ; Bopomofo + 0x3130, # .. 0x318F ; Hangul Compatibility Jamo + 0x3190, # .. 0x319F ; Kanbun + 0x31A0, # .. 0x31BF ; Bopomofo Extended + 0x31C0, # .. 0x31EF ; CJK Strokes + 0x31F0, # .. 0x31FF ; Katakana Phonetic Extensions + 0x3200, # .. 0x32FF ; Enclosed CJK Letters and Months + 0x3300, # .. 0x33FF ; CJK Compatibility + 0x3400, # .. 0x4DBF ; CJK Unified Ideographs Extension A + 0x4DC0, # .. 0x4DFF ; Yijing Hexagram Symbols + 0x4E00, # .. 0x9FFF ; CJK Unified Ideographs + 0xA000, # .. 0xA48F ; Yi Syllables + 0xA490, # .. 0xA4CF ; Yi Radicals + 0xA4D0, # .. 0xA4FF ; Lisu + 0xA500, # .. 0xA63F ; Vai + 0xA640, # .. 0xA69F ; Cyrillic Extended-B + 0xA6A0, # .. 0xA6FF ; Bamum + 0xA700, # .. 0xA71F ; Modifier Tone Letters + 0xA720, # .. 0xA7FF ; Latin Extended-D + 0xA800, # .. 0xA82F ; Syloti Nagri + 0xA830, # .. 0xA83F ; Common Indic Number Forms + 0xA840, # .. 0xA87F ; Phags-pa + 0xA880, # .. 0xA8DF ; Saurashtra + 0xA8E0, # .. 0xA8FF ; Devanagari Extended + 0xA900, # .. 0xA92F ; Kayah Li + 0xA930, # .. 0xA95F ; Rejang + 0xA960, # .. 0xA97F ; Hangul Jamo Extended-A + 0xA980, # .. 0xA9DF ; Javanese + 0xA9E0, # .. 0xA9FF ; Myanmar Extended-B + 0xAA00, # .. 0xAA5F ; Cham + 0xAA60, # .. 0xAA7F ; Myanmar Extended-A + 0xAA80, # .. 0xAADF ; Tai Viet + 0xAAE0, # .. 0xAAFF ; Meetei Mayek Extensions + 0xAB00, # .. 0xAB2F ; Ethiopic Extended-A + 0xAB30, # .. 0xAB6F ; Latin Extended-E + 0xAB70, # .. 0xABBF ; Cherokee Supplement + 0xABC0, # .. 0xABFF ; Meetei Mayek + 0xAC00, # .. 0xD7AF ; Hangul Syllables + 0xD7B0, # .. 0xD7FF ; Hangul Jamo Extended-B + 0xD800, # .. 0xDB7F ; High Surrogates + 0xDB80, # .. 0xDBFF ; High Private Use Surrogates + 0xDC00, # .. 0xDFFF ; Low Surrogates + 0xE000, # .. 0xF8FF ; Private Use Area + 0xF900, # .. 0xFAFF ; CJK Compatibility Ideographs + 0xFB00, # .. 0xFB4F ; Alphabetic Presentation Forms + 0xFB50, # .. 0xFDFF ; Arabic Presentation Forms-A + 0xFE00, # .. 0xFE0F ; Variation Selectors + 0xFE10, # .. 0xFE1F ; Vertical Forms + 0xFE20, # .. 0xFE2F ; Combining Half Marks + 0xFE30, # .. 0xFE4F ; CJK Compatibility Forms + 0xFE50, # .. 0xFE6F ; Small Form Variants + 0xFE70, # .. 0xFEFF ; Arabic Presentation Forms-B + 0xFF00, # .. 0xFFEF ; Halfwidth and Fullwidth Forms + 0xFFF0, # .. 0xFFFF ; Specials + 0x10000, # .. 0x1007F ; Linear B Syllabary + 0x10080, # .. 0x100FF ; Linear B Ideograms + 0x10100, # .. 0x1013F ; Aegean Numbers + 0x10140, # .. 0x1018F ; Ancient Greek Numbers + 0x10190, # .. 0x101CF ; Ancient Symbols + 0x101D0, # .. 0x101FF ; Phaistos Disc + 0x10200, # .. 0x1027F ; No_Block + 0x10280, # .. 0x1029F ; Lycian + 0x102A0, # .. 0x102DF ; Carian + 0x102E0, # .. 0x102FF ; Coptic Epact Numbers + 0x10300, # .. 0x1032F ; Old Italic + 0x10330, # .. 0x1034F ; Gothic + 0x10350, # .. 0x1037F ; Old Permic + 0x10380, # .. 0x1039F ; Ugaritic + 0x103A0, # .. 0x103DF ; Old Persian + 0x103E0, # .. 0x103FF ; No_Block + 0x10400, # .. 0x1044F ; Deseret + 0x10450, # .. 0x1047F ; Shavian + 0x10480, # .. 0x104AF ; Osmanya + 0x104B0, # .. 0x104FF ; Osage + 0x10500, # .. 0x1052F ; Elbasan + 0x10530, # .. 0x1056F ; Caucasian Albanian + 0x10570, # .. 0x105FF ; No_Block + 0x10600, # .. 0x1077F ; Linear A + 0x10780, # .. 0x107FF ; No_Block + 0x10800, # .. 0x1083F ; Cypriot Syllabary + 0x10840, # .. 0x1085F ; Imperial Aramaic + 0x10860, # .. 0x1087F ; Palmyrene + 0x10880, # .. 0x108AF ; Nabataean + 0x108B0, # .. 0x108DF ; No_Block + 0x108E0, # .. 0x108FF ; Hatran + 0x10900, # .. 0x1091F ; Phoenician + 0x10920, # .. 0x1093F ; Lydian + 0x10940, # .. 0x1097F ; No_Block + 0x10980, # .. 0x1099F ; Meroitic Hieroglyphs + 0x109A0, # .. 0x109FF ; Meroitic Cursive + 0x10A00, # .. 0x10A5F ; Kharoshthi + 0x10A60, # .. 0x10A7F ; Old South Arabian + 0x10A80, # .. 0x10A9F ; Old North Arabian + 0x10AA0, # .. 0x10ABF ; No_Block + 0x10AC0, # .. 0x10AFF ; Manichaean + 0x10B00, # .. 0x10B3F ; Avestan + 0x10B40, # .. 0x10B5F ; Inscriptional Parthian + 0x10B60, # .. 0x10B7F ; Inscriptional Pahlavi + 0x10B80, # .. 0x10BAF ; Psalter Pahlavi + 0x10BB0, # .. 0x10BFF ; No_Block + 0x10C00, # .. 0x10C4F ; Old Turkic + 0x10C50, # .. 0x10C7F ; No_Block + 0x10C80, # .. 0x10CFF ; Old Hungarian + 0x10D00, # .. 0x10E5F ; No_Block + 0x10E60, # .. 0x10E7F ; Rumi Numeral Symbols + 0x10E80, # .. 0x10FFF ; No_Block + 0x11000, # .. 0x1107F ; Brahmi + 0x11080, # .. 0x110CF ; Kaithi + 0x110D0, # .. 0x110FF ; Sora Sompeng + 0x11100, # .. 0x1114F ; Chakma + 0x11150, # .. 0x1117F ; Mahajani + 0x11180, # .. 0x111DF ; Sharada + 0x111E0, # .. 0x111FF ; Sinhala Archaic Numbers + 0x11200, # .. 0x1124F ; Khojki + 0x11250, # .. 0x1127F ; No_Block + 0x11280, # .. 0x112AF ; Multani + 0x112B0, # .. 0x112FF ; Khudawadi + 0x11300, # .. 0x1137F ; Grantha + 0x11380, # .. 0x113FF ; No_Block + 0x11400, # .. 0x1147F ; Newa + 0x11480, # .. 0x114DF ; Tirhuta + 0x114E0, # .. 0x1157F ; No_Block + 0x11580, # .. 0x115FF ; Siddham + 0x11600, # .. 0x1165F ; Modi + 0x11660, # .. 0x1167F ; Mongolian Supplement + 0x11680, # .. 0x116CF ; Takri + 0x116D0, # .. 0x116FF ; No_Block + 0x11700, # .. 0x1173F ; Ahom + 0x11740, # .. 0x1189F ; No_Block + 0x118A0, # .. 0x118FF ; Warang Citi + 0x11900, # .. 0x119FF ; No_Block + 0x11A00, # .. 0x11A4F ; Zanabazar Square + 0x11A50, # .. 0x11AAF ; Soyombo + 0x11AB0, # .. 0x11ABF ; No_Block + 0x11AC0, # .. 0x11AFF ; Pau Cin Hau + 0x11B00, # .. 0x11BFF ; No_Block + 0x11C00, # .. 0x11C6F ; Bhaiksuki + 0x11C70, # .. 0x11CBF ; Marchen + 0x11CC0, # .. 0x11CFF ; No_Block + 0x11D00, # .. 0x11D5F ; Masaram Gondi + 0x11D60, # .. 0x11FFF ; No_Block + 0x12000, # .. 0x123FF ; Cuneiform + 0x12400, # .. 0x1247F ; Cuneiform Numbers and Punctuation + 0x12480, # .. 0x1254F ; Early Dynastic Cuneiform + 0x12550, # .. 0x12FFF ; No_Block + 0x13000, # .. 0x1342F ; Egyptian Hieroglyphs + 0x13430, # .. 0x143FF ; No_Block + 0x14400, # .. 0x1467F ; Anatolian Hieroglyphs + 0x14680, # .. 0x167FF ; No_Block + 0x16800, # .. 0x16A3F ; Bamum Supplement + 0x16A40, # .. 0x16A6F ; Mro + 0x16A70, # .. 0x16ACF ; No_Block + 0x16AD0, # .. 0x16AFF ; Bassa Vah + 0x16B00, # .. 0x16B8F ; Pahawh Hmong + 0x16B90, # .. 0x16EFF ; No_Block + 0x16F00, # .. 0x16F9F ; Miao + 0x16FA0, # .. 0x16FDF ; No_Block + 0x16FE0, # .. 0x16FFF ; Ideographic Symbols and Punctuation + 0x17000, # .. 0x187FF ; Tangut + 0x18800, # .. 0x18AFF ; Tangut Components + 0x18B00, # .. 0x1AFFF ; No_Block + 0x1B000, # .. 0x1B0FF ; Kana Supplement + 0x1B100, # .. 0x1B12F ; Kana Extended-A + 0x1B130, # .. 0x1B16F ; No_Block + 0x1B170, # .. 0x1B2FF ; Nushu + 0x1B300, # .. 0x1BBFF ; No_Block + 0x1BC00, # .. 0x1BC9F ; Duployan + 0x1BCA0, # .. 0x1BCAF ; Shorthand Format Controls + 0x1BCB0, # .. 0x1CFFF ; No_Block + 0x1D000, # .. 0x1D0FF ; Byzantine Musical Symbols + 0x1D100, # .. 0x1D1FF ; Musical Symbols + 0x1D200, # .. 0x1D24F ; Ancient Greek Musical Notation + 0x1D250, # .. 0x1D2FF ; No_Block + 0x1D300, # .. 0x1D35F ; Tai Xuan Jing Symbols + 0x1D360, # .. 0x1D37F ; Counting Rod Numerals + 0x1D380, # .. 0x1D3FF ; No_Block + 0x1D400, # .. 0x1D7FF ; Mathematical Alphanumeric Symbols + 0x1D800, # .. 0x1DAAF ; Sutton SignWriting + 0x1DAB0, # .. 0x1DFFF ; No_Block + 0x1E000, # .. 0x1E02F ; Glagolitic Supplement + 0x1E030, # .. 0x1E7FF ; No_Block + 0x1E800, # .. 0x1E8DF ; Mende Kikakui + 0x1E8E0, # .. 0x1E8FF ; No_Block + 0x1E900, # .. 0x1E95F ; Adlam + 0x1E960, # .. 0x1EDFF ; No_Block + 0x1EE00, # .. 0x1EEFF ; Arabic Mathematical Alphabetic Symbols + 0x1EF00, # .. 0x1EFFF ; No_Block + 0x1F000, # .. 0x1F02F ; Mahjong Tiles + 0x1F030, # .. 0x1F09F ; Domino Tiles + 0x1F0A0, # .. 0x1F0FF ; Playing Cards + 0x1F100, # .. 0x1F1FF ; Enclosed Alphanumeric Supplement + 0x1F200, # .. 0x1F2FF ; Enclosed Ideographic Supplement + 0x1F300, # .. 0x1F5FF ; Miscellaneous Symbols and Pictographs + 0x1F600, # .. 0x1F64F ; Emoticons + 0x1F650, # .. 0x1F67F ; Ornamental Dingbats + 0x1F680, # .. 0x1F6FF ; Transport and Map Symbols + 0x1F700, # .. 0x1F77F ; Alchemical Symbols + 0x1F780, # .. 0x1F7FF ; Geometric Shapes Extended + 0x1F800, # .. 0x1F8FF ; Supplemental Arrows-C + 0x1F900, # .. 0x1F9FF ; Supplemental Symbols and Pictographs + 0x1FA00, # .. 0x1FFFF ; No_Block + 0x20000, # .. 0x2A6DF ; CJK Unified Ideographs Extension B + 0x2A6E0, # .. 0x2A6FF ; No_Block + 0x2A700, # .. 0x2B73F ; CJK Unified Ideographs Extension C + 0x2B740, # .. 0x2B81F ; CJK Unified Ideographs Extension D + 0x2B820, # .. 0x2CEAF ; CJK Unified Ideographs Extension E + 0x2CEB0, # .. 0x2EBEF ; CJK Unified Ideographs Extension F + 0x2EBF0, # .. 0x2F7FF ; No_Block + 0x2F800, # .. 0x2FA1F ; CJK Compatibility Ideographs Supplement + 0x2FA20, # .. 0xDFFFF ; No_Block + 0xE0000, # .. 0xE007F ; Tags + 0xE0080, # .. 0xE00FF ; No_Block + 0xE0100, # .. 0xE01EF ; Variation Selectors Supplement + 0xE01F0, # .. 0xEFFFF ; No_Block + 0xF0000, # .. 0xFFFFF ; Supplementary Private Use Area-A + 0x100000, # .. 0x10FFFF ; Supplementary Private Use Area-B +] + +VALUES = [ + 'Basic Latin', # 0000..007F + 'Latin-1 Supplement', # 0080..00FF + 'Latin Extended-A', # 0100..017F + 'Latin Extended-B', # 0180..024F + 'IPA Extensions', # 0250..02AF + 'Spacing Modifier Letters', # 02B0..02FF + 'Combining Diacritical Marks', # 0300..036F + 'Greek and Coptic', # 0370..03FF + 'Cyrillic', # 0400..04FF + 'Cyrillic Supplement', # 0500..052F + 'Armenian', # 0530..058F + 'Hebrew', # 0590..05FF + 'Arabic', # 0600..06FF + 'Syriac', # 0700..074F + 'Arabic Supplement', # 0750..077F + 'Thaana', # 0780..07BF + 'NKo', # 07C0..07FF + 'Samaritan', # 0800..083F + 'Mandaic', # 0840..085F + 'Syriac Supplement', # 0860..086F + 'No_Block', # 0870..089F + 'Arabic Extended-A', # 08A0..08FF + 'Devanagari', # 0900..097F + 'Bengali', # 0980..09FF + 'Gurmukhi', # 0A00..0A7F + 'Gujarati', # 0A80..0AFF + 'Oriya', # 0B00..0B7F + 'Tamil', # 0B80..0BFF + 'Telugu', # 0C00..0C7F + 'Kannada', # 0C80..0CFF + 'Malayalam', # 0D00..0D7F + 'Sinhala', # 0D80..0DFF + 'Thai', # 0E00..0E7F + 'Lao', # 0E80..0EFF + 'Tibetan', # 0F00..0FFF + 'Myanmar', # 1000..109F + 'Georgian', # 10A0..10FF + 'Hangul Jamo', # 1100..11FF + 'Ethiopic', # 1200..137F + 'Ethiopic Supplement', # 1380..139F + 'Cherokee', # 13A0..13FF + 'Unified Canadian Aboriginal Syllabics', # 1400..167F + 'Ogham', # 1680..169F + 'Runic', # 16A0..16FF + 'Tagalog', # 1700..171F + 'Hanunoo', # 1720..173F + 'Buhid', # 1740..175F + 'Tagbanwa', # 1760..177F + 'Khmer', # 1780..17FF + 'Mongolian', # 1800..18AF + 'Unified Canadian Aboriginal Syllabics Extended', # 18B0..18FF + 'Limbu', # 1900..194F + 'Tai Le', # 1950..197F + 'New Tai Lue', # 1980..19DF + 'Khmer Symbols', # 19E0..19FF + 'Buginese', # 1A00..1A1F + 'Tai Tham', # 1A20..1AAF + 'Combining Diacritical Marks Extended', # 1AB0..1AFF + 'Balinese', # 1B00..1B7F + 'Sundanese', # 1B80..1BBF + 'Batak', # 1BC0..1BFF + 'Lepcha', # 1C00..1C4F + 'Ol Chiki', # 1C50..1C7F + 'Cyrillic Extended-C', # 1C80..1C8F + 'No_Block', # 1C90..1CBF + 'Sundanese Supplement', # 1CC0..1CCF + 'Vedic Extensions', # 1CD0..1CFF + 'Phonetic Extensions', # 1D00..1D7F + 'Phonetic Extensions Supplement', # 1D80..1DBF + 'Combining Diacritical Marks Supplement', # 1DC0..1DFF + 'Latin Extended Additional', # 1E00..1EFF + 'Greek Extended', # 1F00..1FFF + 'General Punctuation', # 2000..206F + 'Superscripts and Subscripts', # 2070..209F + 'Currency Symbols', # 20A0..20CF + 'Combining Diacritical Marks for Symbols', # 20D0..20FF + 'Letterlike Symbols', # 2100..214F + 'Number Forms', # 2150..218F + 'Arrows', # 2190..21FF + 'Mathematical Operators', # 2200..22FF + 'Miscellaneous Technical', # 2300..23FF + 'Control Pictures', # 2400..243F + 'Optical Character Recognition', # 2440..245F + 'Enclosed Alphanumerics', # 2460..24FF + 'Box Drawing', # 2500..257F + 'Block Elements', # 2580..259F + 'Geometric Shapes', # 25A0..25FF + 'Miscellaneous Symbols', # 2600..26FF + 'Dingbats', # 2700..27BF + 'Miscellaneous Mathematical Symbols-A', # 27C0..27EF + 'Supplemental Arrows-A', # 27F0..27FF + 'Braille Patterns', # 2800..28FF + 'Supplemental Arrows-B', # 2900..297F + 'Miscellaneous Mathematical Symbols-B', # 2980..29FF + 'Supplemental Mathematical Operators', # 2A00..2AFF + 'Miscellaneous Symbols and Arrows', # 2B00..2BFF + 'Glagolitic', # 2C00..2C5F + 'Latin Extended-C', # 2C60..2C7F + 'Coptic', # 2C80..2CFF + 'Georgian Supplement', # 2D00..2D2F + 'Tifinagh', # 2D30..2D7F + 'Ethiopic Extended', # 2D80..2DDF + 'Cyrillic Extended-A', # 2DE0..2DFF + 'Supplemental Punctuation', # 2E00..2E7F + 'CJK Radicals Supplement', # 2E80..2EFF + 'Kangxi Radicals', # 2F00..2FDF + 'No_Block', # 2FE0..2FEF + 'Ideographic Description Characters', # 2FF0..2FFF + 'CJK Symbols and Punctuation', # 3000..303F + 'Hiragana', # 3040..309F + 'Katakana', # 30A0..30FF + 'Bopomofo', # 3100..312F + 'Hangul Compatibility Jamo', # 3130..318F + 'Kanbun', # 3190..319F + 'Bopomofo Extended', # 31A0..31BF + 'CJK Strokes', # 31C0..31EF + 'Katakana Phonetic Extensions', # 31F0..31FF + 'Enclosed CJK Letters and Months', # 3200..32FF + 'CJK Compatibility', # 3300..33FF + 'CJK Unified Ideographs Extension A', # 3400..4DBF + 'Yijing Hexagram Symbols', # 4DC0..4DFF + 'CJK Unified Ideographs', # 4E00..9FFF + 'Yi Syllables', # A000..A48F + 'Yi Radicals', # A490..A4CF + 'Lisu', # A4D0..A4FF + 'Vai', # A500..A63F + 'Cyrillic Extended-B', # A640..A69F + 'Bamum', # A6A0..A6FF + 'Modifier Tone Letters', # A700..A71F + 'Latin Extended-D', # A720..A7FF + 'Syloti Nagri', # A800..A82F + 'Common Indic Number Forms', # A830..A83F + 'Phags-pa', # A840..A87F + 'Saurashtra', # A880..A8DF + 'Devanagari Extended', # A8E0..A8FF + 'Kayah Li', # A900..A92F + 'Rejang', # A930..A95F + 'Hangul Jamo Extended-A', # A960..A97F + 'Javanese', # A980..A9DF + 'Myanmar Extended-B', # A9E0..A9FF + 'Cham', # AA00..AA5F + 'Myanmar Extended-A', # AA60..AA7F + 'Tai Viet', # AA80..AADF + 'Meetei Mayek Extensions', # AAE0..AAFF + 'Ethiopic Extended-A', # AB00..AB2F + 'Latin Extended-E', # AB30..AB6F + 'Cherokee Supplement', # AB70..ABBF + 'Meetei Mayek', # ABC0..ABFF + 'Hangul Syllables', # AC00..D7AF + 'Hangul Jamo Extended-B', # D7B0..D7FF + 'High Surrogates', # D800..DB7F + 'High Private Use Surrogates', # DB80..DBFF + 'Low Surrogates', # DC00..DFFF + 'Private Use Area', # E000..F8FF + 'CJK Compatibility Ideographs', # F900..FAFF + 'Alphabetic Presentation Forms', # FB00..FB4F + 'Arabic Presentation Forms-A', # FB50..FDFF + 'Variation Selectors', # FE00..FE0F + 'Vertical Forms', # FE10..FE1F + 'Combining Half Marks', # FE20..FE2F + 'CJK Compatibility Forms', # FE30..FE4F + 'Small Form Variants', # FE50..FE6F + 'Arabic Presentation Forms-B', # FE70..FEFF + 'Halfwidth and Fullwidth Forms', # FF00..FFEF + 'Specials', # FFF0..FFFF + 'Linear B Syllabary', # 10000..1007F + 'Linear B Ideograms', # 10080..100FF + 'Aegean Numbers', # 10100..1013F + 'Ancient Greek Numbers', # 10140..1018F + 'Ancient Symbols', # 10190..101CF + 'Phaistos Disc', # 101D0..101FF + 'No_Block', # 10200..1027F + 'Lycian', # 10280..1029F + 'Carian', # 102A0..102DF + 'Coptic Epact Numbers', # 102E0..102FF + 'Old Italic', # 10300..1032F + 'Gothic', # 10330..1034F + 'Old Permic', # 10350..1037F + 'Ugaritic', # 10380..1039F + 'Old Persian', # 103A0..103DF + 'No_Block', # 103E0..103FF + 'Deseret', # 10400..1044F + 'Shavian', # 10450..1047F + 'Osmanya', # 10480..104AF + 'Osage', # 104B0..104FF + 'Elbasan', # 10500..1052F + 'Caucasian Albanian', # 10530..1056F + 'No_Block', # 10570..105FF + 'Linear A', # 10600..1077F + 'No_Block', # 10780..107FF + 'Cypriot Syllabary', # 10800..1083F + 'Imperial Aramaic', # 10840..1085F + 'Palmyrene', # 10860..1087F + 'Nabataean', # 10880..108AF + 'No_Block', # 108B0..108DF + 'Hatran', # 108E0..108FF + 'Phoenician', # 10900..1091F + 'Lydian', # 10920..1093F + 'No_Block', # 10940..1097F + 'Meroitic Hieroglyphs', # 10980..1099F + 'Meroitic Cursive', # 109A0..109FF + 'Kharoshthi', # 10A00..10A5F + 'Old South Arabian', # 10A60..10A7F + 'Old North Arabian', # 10A80..10A9F + 'No_Block', # 10AA0..10ABF + 'Manichaean', # 10AC0..10AFF + 'Avestan', # 10B00..10B3F + 'Inscriptional Parthian', # 10B40..10B5F + 'Inscriptional Pahlavi', # 10B60..10B7F + 'Psalter Pahlavi', # 10B80..10BAF + 'No_Block', # 10BB0..10BFF + 'Old Turkic', # 10C00..10C4F + 'No_Block', # 10C50..10C7F + 'Old Hungarian', # 10C80..10CFF + 'No_Block', # 10D00..10E5F + 'Rumi Numeral Symbols', # 10E60..10E7F + 'No_Block', # 10E80..10FFF + 'Brahmi', # 11000..1107F + 'Kaithi', # 11080..110CF + 'Sora Sompeng', # 110D0..110FF + 'Chakma', # 11100..1114F + 'Mahajani', # 11150..1117F + 'Sharada', # 11180..111DF + 'Sinhala Archaic Numbers', # 111E0..111FF + 'Khojki', # 11200..1124F + 'No_Block', # 11250..1127F + 'Multani', # 11280..112AF + 'Khudawadi', # 112B0..112FF + 'Grantha', # 11300..1137F + 'No_Block', # 11380..113FF + 'Newa', # 11400..1147F + 'Tirhuta', # 11480..114DF + 'No_Block', # 114E0..1157F + 'Siddham', # 11580..115FF + 'Modi', # 11600..1165F + 'Mongolian Supplement', # 11660..1167F + 'Takri', # 11680..116CF + 'No_Block', # 116D0..116FF + 'Ahom', # 11700..1173F + 'No_Block', # 11740..1189F + 'Warang Citi', # 118A0..118FF + 'No_Block', # 11900..119FF + 'Zanabazar Square', # 11A00..11A4F + 'Soyombo', # 11A50..11AAF + 'No_Block', # 11AB0..11ABF + 'Pau Cin Hau', # 11AC0..11AFF + 'No_Block', # 11B00..11BFF + 'Bhaiksuki', # 11C00..11C6F + 'Marchen', # 11C70..11CBF + 'No_Block', # 11CC0..11CFF + 'Masaram Gondi', # 11D00..11D5F + 'No_Block', # 11D60..11FFF + 'Cuneiform', # 12000..123FF + 'Cuneiform Numbers and Punctuation', # 12400..1247F + 'Early Dynastic Cuneiform', # 12480..1254F + 'No_Block', # 12550..12FFF + 'Egyptian Hieroglyphs', # 13000..1342F + 'No_Block', # 13430..143FF + 'Anatolian Hieroglyphs', # 14400..1467F + 'No_Block', # 14680..167FF + 'Bamum Supplement', # 16800..16A3F + 'Mro', # 16A40..16A6F + 'No_Block', # 16A70..16ACF + 'Bassa Vah', # 16AD0..16AFF + 'Pahawh Hmong', # 16B00..16B8F + 'No_Block', # 16B90..16EFF + 'Miao', # 16F00..16F9F + 'No_Block', # 16FA0..16FDF + 'Ideographic Symbols and Punctuation', # 16FE0..16FFF + 'Tangut', # 17000..187FF + 'Tangut Components', # 18800..18AFF + 'No_Block', # 18B00..1AFFF + 'Kana Supplement', # 1B000..1B0FF + 'Kana Extended-A', # 1B100..1B12F + 'No_Block', # 1B130..1B16F + 'Nushu', # 1B170..1B2FF + 'No_Block', # 1B300..1BBFF + 'Duployan', # 1BC00..1BC9F + 'Shorthand Format Controls', # 1BCA0..1BCAF + 'No_Block', # 1BCB0..1CFFF + 'Byzantine Musical Symbols', # 1D000..1D0FF + 'Musical Symbols', # 1D100..1D1FF + 'Ancient Greek Musical Notation', # 1D200..1D24F + 'No_Block', # 1D250..1D2FF + 'Tai Xuan Jing Symbols', # 1D300..1D35F + 'Counting Rod Numerals', # 1D360..1D37F + 'No_Block', # 1D380..1D3FF + 'Mathematical Alphanumeric Symbols', # 1D400..1D7FF + 'Sutton SignWriting', # 1D800..1DAAF + 'No_Block', # 1DAB0..1DFFF + 'Glagolitic Supplement', # 1E000..1E02F + 'No_Block', # 1E030..1E7FF + 'Mende Kikakui', # 1E800..1E8DF + 'No_Block', # 1E8E0..1E8FF + 'Adlam', # 1E900..1E95F + 'No_Block', # 1E960..1EDFF + 'Arabic Mathematical Alphabetic Symbols', # 1EE00..1EEFF + 'No_Block', # 1EF00..1EFFF + 'Mahjong Tiles', # 1F000..1F02F + 'Domino Tiles', # 1F030..1F09F + 'Playing Cards', # 1F0A0..1F0FF + 'Enclosed Alphanumeric Supplement', # 1F100..1F1FF + 'Enclosed Ideographic Supplement', # 1F200..1F2FF + 'Miscellaneous Symbols and Pictographs', # 1F300..1F5FF + 'Emoticons', # 1F600..1F64F + 'Ornamental Dingbats', # 1F650..1F67F + 'Transport and Map Symbols', # 1F680..1F6FF + 'Alchemical Symbols', # 1F700..1F77F + 'Geometric Shapes Extended', # 1F780..1F7FF + 'Supplemental Arrows-C', # 1F800..1F8FF + 'Supplemental Symbols and Pictographs', # 1F900..1F9FF + 'No_Block', # 1FA00..1FFFF + 'CJK Unified Ideographs Extension B', # 20000..2A6DF + 'No_Block', # 2A6E0..2A6FF + 'CJK Unified Ideographs Extension C', # 2A700..2B73F + 'CJK Unified Ideographs Extension D', # 2B740..2B81F + 'CJK Unified Ideographs Extension E', # 2B820..2CEAF + 'CJK Unified Ideographs Extension F', # 2CEB0..2EBEF + 'No_Block', # 2EBF0..2F7FF + 'CJK Compatibility Ideographs Supplement', # 2F800..2FA1F + 'No_Block', # 2FA20..DFFFF + 'Tags', # E0000..E007F + 'No_Block', # E0080..E00FF + 'Variation Selectors Supplement', # E0100..E01EF + 'No_Block', # E01F0..EFFFF + 'Supplementary Private Use Area-A', # F0000..FFFFF + 'Supplementary Private Use Area-B', # 100000..10FFFF +] diff --git a/Lib/fontTools/unicodedata/OTTags.py b/Lib/fontTools/unicodedata/OTTags.py new file mode 100644 index 0000000..3922680 --- /dev/null +++ b/Lib/fontTools/unicodedata/OTTags.py @@ -0,0 +1,41 @@ +# Data updated to OpenType 1.8.2 as of January 2018. + +# Complete list of OpenType script tags at: +# https://www.microsoft.com/typography/otspec/scripttags.htm + +# Most of the script tags are the same as the ISO 15924 tag but lowercased, +# so we only have to handle the exceptional cases: +# - KATAKANA and HIRAGANA both map to 'kana'; +# - spaces at the end are preserved, unlike ISO 15924; +# - we map special script codes for Inherited, Common and Unknown to DFLT. + +DEFAULT_SCRIPT = "DFLT" + +SCRIPT_EXCEPTIONS = { + "Hira": "kana", + "Hrkt": "kana", + "Laoo": "lao ", + "Yiii": "yi ", + "Nkoo": "nko ", + "Vaii": "vai ", + "Zinh": DEFAULT_SCRIPT, + "Zyyy": DEFAULT_SCRIPT, + "Zzzz": DEFAULT_SCRIPT, +} + +NEW_SCRIPT_TAGS = { + "Beng": ("bng2",), + "Deva": ("dev2",), + "Gujr": ("gjr2",), + "Guru": ("gur2",), + "Knda": ("knd2",), + "Mlym": ("mlm2",), + "Orya": ("ory2",), + "Taml": ("tml2",), + "Telu": ("tel2",), + "Mymr": ("mym2",), +} + +NEW_SCRIPT_TAGS_REVERSED = { + value: key for key, values in NEW_SCRIPT_TAGS.items() for value in values +} diff --git a/Lib/fontTools/unicodedata/ScriptExtensions.py b/Lib/fontTools/unicodedata/ScriptExtensions.py new file mode 100644 index 0000000..a92cc80 --- /dev/null +++ b/Lib/fontTools/unicodedata/ScriptExtensions.py @@ -0,0 +1,389 @@ +# -*- coding: utf-8 -*- +# +# NOTE: This file was auto-generated with MetaTools/buildUCD.py. +# Source: https://unicode.org/Public/UNIDATA/ScriptExtensions.txt +# License: http://unicode.org/copyright.html#License +# +# ScriptExtensions-10.0.0.txt +# Date: 2017-05-31, 01:07:00 GMT [RP] +# © 2017 Unicode®, Inc. +# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. +# For terms of use, see http://www.unicode.org/terms_of_use.html +# +# Unicode Character Database +# For documentation, see http://www.unicode.org/reports/tr44/ +# +# The Script_Extensions property indicates which characters are commonly used +# with more than one script, but with a limited number of scripts. +# For each code point, there is one or more property values. Each such value is a Script property value. +# For more information, see: +# UAX #24, Unicode Script Property: http://www.unicode.org/reports/tr24/ +# Especially the sections: +# http://www.unicode.org/reports/tr24/#Assignment_Script_Values +# http://www.unicode.org/reports/tr24/#Assignment_ScriptX_Values +# +# Each Script_Extensions value in this file consists of a set +# of one or more abbreviated Script property values. The ordering of the +# values in that set is not material, but for stability in presentation +# it is given here as alphabetical. +# +# The Script_Extensions values are presented in sorted order in the file. +# They are sorted first by the number of Script property values in their sets, +# and then alphabetically by first differing Script property value. +# +# Following each distinct Script_Extensions value is the list of code +# points associated with that value, listed in code point order. +# +# All code points not explicitly listed for Script_Extensions +# have as their value the corresponding Script property value +# +# @missing: 0000..10FFFF; <script> + + +RANGES = [ + 0x0000, # .. 0x0341 ; None + 0x0342, # .. 0x0342 ; {'Grek'} + 0x0343, # .. 0x0344 ; None + 0x0345, # .. 0x0345 ; {'Grek'} + 0x0346, # .. 0x0362 ; None + 0x0363, # .. 0x036F ; {'Latn'} + 0x0370, # .. 0x0482 ; None + 0x0483, # .. 0x0483 ; {'Cyrl', 'Perm'} + 0x0484, # .. 0x0484 ; {'Cyrl', 'Glag'} + 0x0485, # .. 0x0486 ; {'Cyrl', 'Latn'} + 0x0487, # .. 0x0487 ; {'Cyrl', 'Glag'} + 0x0488, # .. 0x0588 ; None + 0x0589, # .. 0x0589 ; {'Armn', 'Geor'} + 0x058A, # .. 0x060B ; None + 0x060C, # .. 0x060C ; {'Arab', 'Syrc', 'Thaa'} + 0x060D, # .. 0x061A ; None + 0x061B, # .. 0x061C ; {'Arab', 'Syrc', 'Thaa'} + 0x061D, # .. 0x061E ; None + 0x061F, # .. 0x061F ; {'Arab', 'Syrc', 'Thaa'} + 0x0620, # .. 0x063F ; None + 0x0640, # .. 0x0640 ; {'Adlm', 'Arab', 'Mand', 'Mani', 'Phlp', 'Syrc'} + 0x0641, # .. 0x064A ; None + 0x064B, # .. 0x0655 ; {'Arab', 'Syrc'} + 0x0656, # .. 0x065F ; None + 0x0660, # .. 0x0669 ; {'Arab', 'Thaa'} + 0x066A, # .. 0x066F ; None + 0x0670, # .. 0x0670 ; {'Arab', 'Syrc'} + 0x0671, # .. 0x0950 ; None + 0x0951, # .. 0x0951 ; {'Beng', 'Deva', 'Gran', 'Gujr', 'Guru', 'Knda', 'Latn', 'Mlym', 'Orya', 'Shrd', 'Taml', 'Telu'} + 0x0952, # .. 0x0952 ; {'Beng', 'Deva', 'Gran', 'Gujr', 'Guru', 'Knda', 'Latn', 'Mlym', 'Orya', 'Taml', 'Telu'} + 0x0953, # .. 0x0963 ; None + 0x0964, # .. 0x0964 ; {'Beng', 'Deva', 'Gran', 'Gujr', 'Guru', 'Knda', 'Mahj', 'Mlym', 'Orya', 'Sind', 'Sinh', 'Sylo', 'Takr', 'Taml', 'Telu', 'Tirh'} + 0x0965, # .. 0x0965 ; {'Beng', 'Deva', 'Gran', 'Gujr', 'Guru', 'Knda', 'Limb', 'Mahj', 'Mlym', 'Orya', 'Sind', 'Sinh', 'Sylo', 'Takr', 'Taml', 'Telu', 'Tirh'} + 0x0966, # .. 0x096F ; {'Deva', 'Kthi', 'Mahj'} + 0x0970, # .. 0x09E5 ; None + 0x09E6, # .. 0x09EF ; {'Beng', 'Cakm', 'Sylo'} + 0x09F0, # .. 0x0A65 ; None + 0x0A66, # .. 0x0A6F ; {'Guru', 'Mult'} + 0x0A70, # .. 0x0AE5 ; None + 0x0AE6, # .. 0x0AEF ; {'Gujr', 'Khoj'} + 0x0AF0, # .. 0x0BA9 ; None + 0x0BAA, # .. 0x0BAA ; {'Gran', 'Taml'} + 0x0BAB, # .. 0x0BB4 ; None + 0x0BB5, # .. 0x0BB5 ; {'Gran', 'Taml'} + 0x0BB6, # .. 0x0BE5 ; None + 0x0BE6, # .. 0x0BF2 ; {'Gran', 'Taml'} + 0x0BF3, # .. 0x103F ; None + 0x1040, # .. 0x1049 ; {'Cakm', 'Mymr', 'Tale'} + 0x104A, # .. 0x10FA ; None + 0x10FB, # .. 0x10FB ; {'Geor', 'Latn'} + 0x10FC, # .. 0x1734 ; None + 0x1735, # .. 0x1736 ; {'Buhd', 'Hano', 'Tagb', 'Tglg'} + 0x1737, # .. 0x1801 ; None + 0x1802, # .. 0x1803 ; {'Mong', 'Phag'} + 0x1804, # .. 0x1804 ; None + 0x1805, # .. 0x1805 ; {'Mong', 'Phag'} + 0x1806, # .. 0x1CCF ; None + 0x1CD0, # .. 0x1CD0 ; {'Deva', 'Gran'} + 0x1CD1, # .. 0x1CD1 ; {'Deva'} + 0x1CD2, # .. 0x1CD3 ; {'Deva', 'Gran'} + 0x1CD4, # .. 0x1CD6 ; {'Deva'} + 0x1CD7, # .. 0x1CD7 ; {'Deva', 'Shrd'} + 0x1CD8, # .. 0x1CD8 ; {'Deva'} + 0x1CD9, # .. 0x1CD9 ; {'Deva', 'Shrd'} + 0x1CDA, # .. 0x1CDA ; {'Deva', 'Knda', 'Mlym', 'Taml', 'Telu'} + 0x1CDB, # .. 0x1CDB ; {'Deva'} + 0x1CDC, # .. 0x1CDD ; {'Deva', 'Shrd'} + 0x1CDE, # .. 0x1CDF ; {'Deva'} + 0x1CE0, # .. 0x1CE0 ; {'Deva', 'Shrd'} + 0x1CE1, # .. 0x1CF1 ; {'Deva'} + 0x1CF2, # .. 0x1CF4 ; {'Deva', 'Gran'} + 0x1CF5, # .. 0x1CF5 ; {'Deva', 'Knda'} + 0x1CF6, # .. 0x1CF6 ; {'Deva'} + 0x1CF7, # .. 0x1CF7 ; {'Beng'} + 0x1CF8, # .. 0x1CF9 ; {'Deva', 'Gran'} + 0x1CFA, # .. 0x1DBF ; None + 0x1DC0, # .. 0x1DC1 ; {'Grek'} + 0x1DC2, # .. 0x20EF ; None + 0x20F0, # .. 0x20F0 ; {'Deva', 'Gran', 'Latn'} + 0x20F1, # .. 0x2E42 ; None + 0x2E43, # .. 0x2E43 ; {'Cyrl', 'Glag'} + 0x2E44, # .. 0x3000 ; None + 0x3001, # .. 0x3002 ; {'Bopo', 'Hang', 'Hani', 'Hira', 'Kana', 'Yiii'} + 0x3003, # .. 0x3003 ; {'Bopo', 'Hang', 'Hani', 'Hira', 'Kana'} + 0x3004, # .. 0x3005 ; None + 0x3006, # .. 0x3006 ; {'Hani'} + 0x3007, # .. 0x3007 ; None + 0x3008, # .. 0x3011 ; {'Bopo', 'Hang', 'Hani', 'Hira', 'Kana', 'Yiii'} + 0x3012, # .. 0x3012 ; None + 0x3013, # .. 0x3013 ; {'Bopo', 'Hang', 'Hani', 'Hira', 'Kana'} + 0x3014, # .. 0x301B ; {'Bopo', 'Hang', 'Hani', 'Hira', 'Kana', 'Yiii'} + 0x301C, # .. 0x301F ; {'Bopo', 'Hang', 'Hani', 'Hira', 'Kana'} + 0x3020, # .. 0x3029 ; None + 0x302A, # .. 0x302D ; {'Bopo', 'Hani'} + 0x302E, # .. 0x302F ; None + 0x3030, # .. 0x3030 ; {'Bopo', 'Hang', 'Hani', 'Hira', 'Kana'} + 0x3031, # .. 0x3035 ; {'Hira', 'Kana'} + 0x3036, # .. 0x3036 ; None + 0x3037, # .. 0x3037 ; {'Bopo', 'Hang', 'Hani', 'Hira', 'Kana'} + 0x3038, # .. 0x303B ; None + 0x303C, # .. 0x303D ; {'Hani', 'Hira', 'Kana'} + 0x303E, # .. 0x303F ; {'Hani'} + 0x3040, # .. 0x3098 ; None + 0x3099, # .. 0x309C ; {'Hira', 'Kana'} + 0x309D, # .. 0x309F ; None + 0x30A0, # .. 0x30A0 ; {'Hira', 'Kana'} + 0x30A1, # .. 0x30FA ; None + 0x30FB, # .. 0x30FB ; {'Bopo', 'Hang', 'Hani', 'Hira', 'Kana', 'Yiii'} + 0x30FC, # .. 0x30FC ; {'Hira', 'Kana'} + 0x30FD, # .. 0x318F ; None + 0x3190, # .. 0x319F ; {'Hani'} + 0x31A0, # .. 0x31BF ; None + 0x31C0, # .. 0x31E3 ; {'Hani'} + 0x31E4, # .. 0x321F ; None + 0x3220, # .. 0x3247 ; {'Hani'} + 0x3248, # .. 0x327F ; None + 0x3280, # .. 0x32B0 ; {'Hani'} + 0x32B1, # .. 0x32BF ; None + 0x32C0, # .. 0x32CB ; {'Hani'} + 0x32CC, # .. 0x3357 ; None + 0x3358, # .. 0x3370 ; {'Hani'} + 0x3371, # .. 0x337A ; None + 0x337B, # .. 0x337F ; {'Hani'} + 0x3380, # .. 0x33DF ; None + 0x33E0, # .. 0x33FE ; {'Hani'} + 0x33FF, # .. 0xA66E ; None + 0xA66F, # .. 0xA66F ; {'Cyrl', 'Glag'} + 0xA670, # .. 0xA82F ; None + 0xA830, # .. 0xA835 ; {'Deva', 'Gujr', 'Guru', 'Knda', 'Kthi', 'Mahj', 'Modi', 'Sind', 'Takr', 'Tirh'} + 0xA836, # .. 0xA839 ; {'Deva', 'Gujr', 'Guru', 'Kthi', 'Mahj', 'Modi', 'Sind', 'Takr', 'Tirh'} + 0xA83A, # .. 0xA8F0 ; None + 0xA8F1, # .. 0xA8F1 ; {'Beng', 'Deva'} + 0xA8F2, # .. 0xA8F2 ; None + 0xA8F3, # .. 0xA8F3 ; {'Deva', 'Taml'} + 0xA8F4, # .. 0xA92D ; None + 0xA92E, # .. 0xA92E ; {'Kali', 'Latn', 'Mymr'} + 0xA92F, # .. 0xA9CE ; None + 0xA9CF, # .. 0xA9CF ; {'Bugi', 'Java'} + 0xA9D0, # .. 0xFDF1 ; None + 0xFDF2, # .. 0xFDF2 ; {'Arab', 'Thaa'} + 0xFDF3, # .. 0xFDFC ; None + 0xFDFD, # .. 0xFDFD ; {'Arab', 'Thaa'} + 0xFDFE, # .. 0xFE44 ; None + 0xFE45, # .. 0xFE46 ; {'Bopo', 'Hang', 'Hani', 'Hira', 'Kana'} + 0xFE47, # .. 0xFF60 ; None + 0xFF61, # .. 0xFF65 ; {'Bopo', 'Hang', 'Hani', 'Hira', 'Kana', 'Yiii'} + 0xFF66, # .. 0xFF6F ; None + 0xFF70, # .. 0xFF70 ; {'Hira', 'Kana'} + 0xFF71, # .. 0xFF9D ; None + 0xFF9E, # .. 0xFF9F ; {'Hira', 'Kana'} + 0xFFA0, # .. 0x100FF ; None + 0x10100, # .. 0x10102 ; {'Cprt', 'Linb'} + 0x10103, # .. 0x10106 ; None + 0x10107, # .. 0x10133 ; {'Cprt', 'Lina', 'Linb'} + 0x10134, # .. 0x10136 ; None + 0x10137, # .. 0x1013F ; {'Cprt', 'Linb'} + 0x10140, # .. 0x102DF ; None + 0x102E0, # .. 0x102FB ; {'Arab', 'Copt'} + 0x102FC, # .. 0x11300 ; None + 0x11301, # .. 0x11301 ; {'Gran', 'Taml'} + 0x11302, # .. 0x11302 ; None + 0x11303, # .. 0x11303 ; {'Gran', 'Taml'} + 0x11304, # .. 0x1133B ; None + 0x1133C, # .. 0x1133C ; {'Gran', 'Taml'} + 0x1133D, # .. 0x1BC9F ; None + 0x1BCA0, # .. 0x1BCA3 ; {'Dupl'} + 0x1BCA4, # .. 0x1D35F ; None + 0x1D360, # .. 0x1D371 ; {'Hani'} + 0x1D372, # .. 0x1F24F ; None + 0x1F250, # .. 0x1F251 ; {'Hani'} + 0x1F252, # .. 0x10FFFF ; None +] + +VALUES = [ + None, # 0000..0341 + {'Grek'}, # 0342..0342 + None, # 0343..0344 + {'Grek'}, # 0345..0345 + None, # 0346..0362 + {'Latn'}, # 0363..036F + None, # 0370..0482 + {'Cyrl', 'Perm'}, # 0483..0483 + {'Cyrl', 'Glag'}, # 0484..0484 + {'Cyrl', 'Latn'}, # 0485..0486 + {'Cyrl', 'Glag'}, # 0487..0487 + None, # 0488..0588 + {'Armn', 'Geor'}, # 0589..0589 + None, # 058A..060B + {'Arab', 'Syrc', 'Thaa'}, # 060C..060C + None, # 060D..061A + {'Arab', 'Syrc', 'Thaa'}, # 061B..061C + None, # 061D..061E + {'Arab', 'Syrc', 'Thaa'}, # 061F..061F + None, # 0620..063F + {'Adlm', 'Arab', 'Mand', 'Mani', 'Phlp', 'Syrc'}, # 0640..0640 + None, # 0641..064A + {'Arab', 'Syrc'}, # 064B..0655 + None, # 0656..065F + {'Arab', 'Thaa'}, # 0660..0669 + None, # 066A..066F + {'Arab', 'Syrc'}, # 0670..0670 + None, # 0671..0950 + {'Beng', 'Deva', 'Gran', 'Gujr', 'Guru', 'Knda', 'Latn', 'Mlym', 'Orya', 'Shrd', 'Taml', 'Telu'}, # 0951..0951 + {'Beng', 'Deva', 'Gran', 'Gujr', 'Guru', 'Knda', 'Latn', 'Mlym', 'Orya', 'Taml', 'Telu'}, # 0952..0952 + None, # 0953..0963 + {'Beng', 'Deva', 'Gran', 'Gujr', 'Guru', 'Knda', 'Mahj', 'Mlym', 'Orya', 'Sind', 'Sinh', 'Sylo', 'Takr', 'Taml', 'Telu', 'Tirh'}, # 0964..0964 + {'Beng', 'Deva', 'Gran', 'Gujr', 'Guru', 'Knda', 'Limb', 'Mahj', 'Mlym', 'Orya', 'Sind', 'Sinh', 'Sylo', 'Takr', 'Taml', 'Telu', 'Tirh'}, # 0965..0965 + {'Deva', 'Kthi', 'Mahj'}, # 0966..096F + None, # 0970..09E5 + {'Beng', 'Cakm', 'Sylo'}, # 09E6..09EF + None, # 09F0..0A65 + {'Guru', 'Mult'}, # 0A66..0A6F + None, # 0A70..0AE5 + {'Gujr', 'Khoj'}, # 0AE6..0AEF + None, # 0AF0..0BA9 + {'Gran', 'Taml'}, # 0BAA..0BAA + None, # 0BAB..0BB4 + {'Gran', 'Taml'}, # 0BB5..0BB5 + None, # 0BB6..0BE5 + {'Gran', 'Taml'}, # 0BE6..0BF2 + None, # 0BF3..103F + {'Cakm', 'Mymr', 'Tale'}, # 1040..1049 + None, # 104A..10FA + {'Geor', 'Latn'}, # 10FB..10FB + None, # 10FC..1734 + {'Buhd', 'Hano', 'Tagb', 'Tglg'}, # 1735..1736 + None, # 1737..1801 + {'Mong', 'Phag'}, # 1802..1803 + None, # 1804..1804 + {'Mong', 'Phag'}, # 1805..1805 + None, # 1806..1CCF + {'Deva', 'Gran'}, # 1CD0..1CD0 + {'Deva'}, # 1CD1..1CD1 + {'Deva', 'Gran'}, # 1CD2..1CD3 + {'Deva'}, # 1CD4..1CD6 + {'Deva', 'Shrd'}, # 1CD7..1CD7 + {'Deva'}, # 1CD8..1CD8 + {'Deva', 'Shrd'}, # 1CD9..1CD9 + {'Deva', 'Knda', 'Mlym', 'Taml', 'Telu'}, # 1CDA..1CDA + {'Deva'}, # 1CDB..1CDB + {'Deva', 'Shrd'}, # 1CDC..1CDD + {'Deva'}, # 1CDE..1CDF + {'Deva', 'Shrd'}, # 1CE0..1CE0 + {'Deva'}, # 1CE1..1CF1 + {'Deva', 'Gran'}, # 1CF2..1CF4 + {'Deva', 'Knda'}, # 1CF5..1CF5 + {'Deva'}, # 1CF6..1CF6 + {'Beng'}, # 1CF7..1CF7 + {'Deva', 'Gran'}, # 1CF8..1CF9 + None, # 1CFA..1DBF + {'Grek'}, # 1DC0..1DC1 + None, # 1DC2..20EF + {'Deva', 'Gran', 'Latn'}, # 20F0..20F0 + None, # 20F1..2E42 + {'Cyrl', 'Glag'}, # 2E43..2E43 + None, # 2E44..3000 + {'Bopo', 'Hang', 'Hani', 'Hira', 'Kana', 'Yiii'}, # 3001..3002 + {'Bopo', 'Hang', 'Hani', 'Hira', 'Kana'}, # 3003..3003 + None, # 3004..3005 + {'Hani'}, # 3006..3006 + None, # 3007..3007 + {'Bopo', 'Hang', 'Hani', 'Hira', 'Kana', 'Yiii'}, # 3008..3011 + None, # 3012..3012 + {'Bopo', 'Hang', 'Hani', 'Hira', 'Kana'}, # 3013..3013 + {'Bopo', 'Hang', 'Hani', 'Hira', 'Kana', 'Yiii'}, # 3014..301B + {'Bopo', 'Hang', 'Hani', 'Hira', 'Kana'}, # 301C..301F + None, # 3020..3029 + {'Bopo', 'Hani'}, # 302A..302D + None, # 302E..302F + {'Bopo', 'Hang', 'Hani', 'Hira', 'Kana'}, # 3030..3030 + {'Hira', 'Kana'}, # 3031..3035 + None, # 3036..3036 + {'Bopo', 'Hang', 'Hani', 'Hira', 'Kana'}, # 3037..3037 + None, # 3038..303B + {'Hani', 'Hira', 'Kana'}, # 303C..303D + {'Hani'}, # 303E..303F + None, # 3040..3098 + {'Hira', 'Kana'}, # 3099..309C + None, # 309D..309F + {'Hira', 'Kana'}, # 30A0..30A0 + None, # 30A1..30FA + {'Bopo', 'Hang', 'Hani', 'Hira', 'Kana', 'Yiii'}, # 30FB..30FB + {'Hira', 'Kana'}, # 30FC..30FC + None, # 30FD..318F + {'Hani'}, # 3190..319F + None, # 31A0..31BF + {'Hani'}, # 31C0..31E3 + None, # 31E4..321F + {'Hani'}, # 3220..3247 + None, # 3248..327F + {'Hani'}, # 3280..32B0 + None, # 32B1..32BF + {'Hani'}, # 32C0..32CB + None, # 32CC..3357 + {'Hani'}, # 3358..3370 + None, # 3371..337A + {'Hani'}, # 337B..337F + None, # 3380..33DF + {'Hani'}, # 33E0..33FE + None, # 33FF..A66E + {'Cyrl', 'Glag'}, # A66F..A66F + None, # A670..A82F + {'Deva', 'Gujr', 'Guru', 'Knda', 'Kthi', 'Mahj', 'Modi', 'Sind', 'Takr', 'Tirh'}, # A830..A835 + {'Deva', 'Gujr', 'Guru', 'Kthi', 'Mahj', 'Modi', 'Sind', 'Takr', 'Tirh'}, # A836..A839 + None, # A83A..A8F0 + {'Beng', 'Deva'}, # A8F1..A8F1 + None, # A8F2..A8F2 + {'Deva', 'Taml'}, # A8F3..A8F3 + None, # A8F4..A92D + {'Kali', 'Latn', 'Mymr'}, # A92E..A92E + None, # A92F..A9CE + {'Bugi', 'Java'}, # A9CF..A9CF + None, # A9D0..FDF1 + {'Arab', 'Thaa'}, # FDF2..FDF2 + None, # FDF3..FDFC + {'Arab', 'Thaa'}, # FDFD..FDFD + None, # FDFE..FE44 + {'Bopo', 'Hang', 'Hani', 'Hira', 'Kana'}, # FE45..FE46 + None, # FE47..FF60 + {'Bopo', 'Hang', 'Hani', 'Hira', 'Kana', 'Yiii'}, # FF61..FF65 + None, # FF66..FF6F + {'Hira', 'Kana'}, # FF70..FF70 + None, # FF71..FF9D + {'Hira', 'Kana'}, # FF9E..FF9F + None, # FFA0..100FF + {'Cprt', 'Linb'}, # 10100..10102 + None, # 10103..10106 + {'Cprt', 'Lina', 'Linb'}, # 10107..10133 + None, # 10134..10136 + {'Cprt', 'Linb'}, # 10137..1013F + None, # 10140..102DF + {'Arab', 'Copt'}, # 102E0..102FB + None, # 102FC..11300 + {'Gran', 'Taml'}, # 11301..11301 + None, # 11302..11302 + {'Gran', 'Taml'}, # 11303..11303 + None, # 11304..1133B + {'Gran', 'Taml'}, # 1133C..1133C + None, # 1133D..1BC9F + {'Dupl'}, # 1BCA0..1BCA3 + None, # 1BCA4..1D35F + {'Hani'}, # 1D360..1D371 + None, # 1D372..1F24F + {'Hani'}, # 1F250..1F251 + None, # 1F252..10FFFF +] diff --git a/Lib/fontTools/unicodedata/Scripts.py b/Lib/fontTools/unicodedata/Scripts.py new file mode 100644 index 0000000..f39b430 --- /dev/null +++ b/Lib/fontTools/unicodedata/Scripts.py @@ -0,0 +1,3201 @@ +# -*- coding: utf-8 -*- +# +# NOTE: This file was auto-generated with MetaTools/buildUCD.py. +# Source: https://unicode.org/Public/UNIDATA/Scripts.txt +# License: http://unicode.org/copyright.html#License +# +# Scripts-10.0.0.txt +# Date: 2017-03-11, 06:40:37 GMT +# © 2017 Unicode®, Inc. +# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. +# For terms of use, see http://www.unicode.org/terms_of_use.html +# +# Unicode Character Database +# For documentation, see http://www.unicode.org/reports/tr44/ +# For more information, see: +# UAX #24, Unicode Script Property: http://www.unicode.org/reports/tr24/ +# Especially the sections: +# http://www.unicode.org/reports/tr24/#Assignment_Script_Values +# http://www.unicode.org/reports/tr24/#Assignment_ScriptX_Values +# + + +RANGES = [ + 0x0000, # .. 0x0040 ; Common + 0x0041, # .. 0x005A ; Latin + 0x005B, # .. 0x0060 ; Common + 0x0061, # .. 0x007A ; Latin + 0x007B, # .. 0x00A9 ; Common + 0x00AA, # .. 0x00AA ; Latin + 0x00AB, # .. 0x00B9 ; Common + 0x00BA, # .. 0x00BA ; Latin + 0x00BB, # .. 0x00BF ; Common + 0x00C0, # .. 0x00D6 ; Latin + 0x00D7, # .. 0x00D7 ; Common + 0x00D8, # .. 0x00F6 ; Latin + 0x00F7, # .. 0x00F7 ; Common + 0x00F8, # .. 0x02B8 ; Latin + 0x02B9, # .. 0x02DF ; Common + 0x02E0, # .. 0x02E4 ; Latin + 0x02E5, # .. 0x02E9 ; Common + 0x02EA, # .. 0x02EB ; Bopomofo + 0x02EC, # .. 0x02FF ; Common + 0x0300, # .. 0x036F ; Inherited + 0x0370, # .. 0x0373 ; Greek + 0x0374, # .. 0x0374 ; Common + 0x0375, # .. 0x0377 ; Greek + 0x0378, # .. 0x0379 ; Unknown + 0x037A, # .. 0x037D ; Greek + 0x037E, # .. 0x037E ; Common + 0x037F, # .. 0x037F ; Greek + 0x0380, # .. 0x0383 ; Unknown + 0x0384, # .. 0x0384 ; Greek + 0x0385, # .. 0x0385 ; Common + 0x0386, # .. 0x0386 ; Greek + 0x0387, # .. 0x0387 ; Common + 0x0388, # .. 0x038A ; Greek + 0x038B, # .. 0x038B ; Unknown + 0x038C, # .. 0x038C ; Greek + 0x038D, # .. 0x038D ; Unknown + 0x038E, # .. 0x03A1 ; Greek + 0x03A2, # .. 0x03A2 ; Unknown + 0x03A3, # .. 0x03E1 ; Greek + 0x03E2, # .. 0x03EF ; Coptic + 0x03F0, # .. 0x03FF ; Greek + 0x0400, # .. 0x0484 ; Cyrillic + 0x0485, # .. 0x0486 ; Inherited + 0x0487, # .. 0x052F ; Cyrillic + 0x0530, # .. 0x0530 ; Unknown + 0x0531, # .. 0x0556 ; Armenian + 0x0557, # .. 0x0558 ; Unknown + 0x0559, # .. 0x055F ; Armenian + 0x0560, # .. 0x0560 ; Unknown + 0x0561, # .. 0x0587 ; Armenian + 0x0588, # .. 0x0588 ; Unknown + 0x0589, # .. 0x0589 ; Common + 0x058A, # .. 0x058A ; Armenian + 0x058B, # .. 0x058C ; Unknown + 0x058D, # .. 0x058F ; Armenian + 0x0590, # .. 0x0590 ; Unknown + 0x0591, # .. 0x05C7 ; Hebrew + 0x05C8, # .. 0x05CF ; Unknown + 0x05D0, # .. 0x05EA ; Hebrew + 0x05EB, # .. 0x05EF ; Unknown + 0x05F0, # .. 0x05F4 ; Hebrew + 0x05F5, # .. 0x05FF ; Unknown + 0x0600, # .. 0x0604 ; Arabic + 0x0605, # .. 0x0605 ; Common + 0x0606, # .. 0x060B ; Arabic + 0x060C, # .. 0x060C ; Common + 0x060D, # .. 0x061A ; Arabic + 0x061B, # .. 0x061B ; Common + 0x061C, # .. 0x061C ; Arabic + 0x061D, # .. 0x061D ; Unknown + 0x061E, # .. 0x061E ; Arabic + 0x061F, # .. 0x061F ; Common + 0x0620, # .. 0x063F ; Arabic + 0x0640, # .. 0x0640 ; Common + 0x0641, # .. 0x064A ; Arabic + 0x064B, # .. 0x0655 ; Inherited + 0x0656, # .. 0x066F ; Arabic + 0x0670, # .. 0x0670 ; Inherited + 0x0671, # .. 0x06DC ; Arabic + 0x06DD, # .. 0x06DD ; Common + 0x06DE, # .. 0x06FF ; Arabic + 0x0700, # .. 0x070D ; Syriac + 0x070E, # .. 0x070E ; Unknown + 0x070F, # .. 0x074A ; Syriac + 0x074B, # .. 0x074C ; Unknown + 0x074D, # .. 0x074F ; Syriac + 0x0750, # .. 0x077F ; Arabic + 0x0780, # .. 0x07B1 ; Thaana + 0x07B2, # .. 0x07BF ; Unknown + 0x07C0, # .. 0x07FA ; Nko + 0x07FB, # .. 0x07FF ; Unknown + 0x0800, # .. 0x082D ; Samaritan + 0x082E, # .. 0x082F ; Unknown + 0x0830, # .. 0x083E ; Samaritan + 0x083F, # .. 0x083F ; Unknown + 0x0840, # .. 0x085B ; Mandaic + 0x085C, # .. 0x085D ; Unknown + 0x085E, # .. 0x085E ; Mandaic + 0x085F, # .. 0x085F ; Unknown + 0x0860, # .. 0x086A ; Syriac + 0x086B, # .. 0x089F ; Unknown + 0x08A0, # .. 0x08B4 ; Arabic + 0x08B5, # .. 0x08B5 ; Unknown + 0x08B6, # .. 0x08BD ; Arabic + 0x08BE, # .. 0x08D3 ; Unknown + 0x08D4, # .. 0x08E1 ; Arabic + 0x08E2, # .. 0x08E2 ; Common + 0x08E3, # .. 0x08FF ; Arabic + 0x0900, # .. 0x0950 ; Devanagari + 0x0951, # .. 0x0952 ; Inherited + 0x0953, # .. 0x0963 ; Devanagari + 0x0964, # .. 0x0965 ; Common + 0x0966, # .. 0x097F ; Devanagari + 0x0980, # .. 0x0983 ; Bengali + 0x0984, # .. 0x0984 ; Unknown + 0x0985, # .. 0x098C ; Bengali + 0x098D, # .. 0x098E ; Unknown + 0x098F, # .. 0x0990 ; Bengali + 0x0991, # .. 0x0992 ; Unknown + 0x0993, # .. 0x09A8 ; Bengali + 0x09A9, # .. 0x09A9 ; Unknown + 0x09AA, # .. 0x09B0 ; Bengali + 0x09B1, # .. 0x09B1 ; Unknown + 0x09B2, # .. 0x09B2 ; Bengali + 0x09B3, # .. 0x09B5 ; Unknown + 0x09B6, # .. 0x09B9 ; Bengali + 0x09BA, # .. 0x09BB ; Unknown + 0x09BC, # .. 0x09C4 ; Bengali + 0x09C5, # .. 0x09C6 ; Unknown + 0x09C7, # .. 0x09C8 ; Bengali + 0x09C9, # .. 0x09CA ; Unknown + 0x09CB, # .. 0x09CE ; Bengali + 0x09CF, # .. 0x09D6 ; Unknown + 0x09D7, # .. 0x09D7 ; Bengali + 0x09D8, # .. 0x09DB ; Unknown + 0x09DC, # .. 0x09DD ; Bengali + 0x09DE, # .. 0x09DE ; Unknown + 0x09DF, # .. 0x09E3 ; Bengali + 0x09E4, # .. 0x09E5 ; Unknown + 0x09E6, # .. 0x09FD ; Bengali + 0x09FE, # .. 0x0A00 ; Unknown + 0x0A01, # .. 0x0A03 ; Gurmukhi + 0x0A04, # .. 0x0A04 ; Unknown + 0x0A05, # .. 0x0A0A ; Gurmukhi + 0x0A0B, # .. 0x0A0E ; Unknown + 0x0A0F, # .. 0x0A10 ; Gurmukhi + 0x0A11, # .. 0x0A12 ; Unknown + 0x0A13, # .. 0x0A28 ; Gurmukhi + 0x0A29, # .. 0x0A29 ; Unknown + 0x0A2A, # .. 0x0A30 ; Gurmukhi + 0x0A31, # .. 0x0A31 ; Unknown + 0x0A32, # .. 0x0A33 ; Gurmukhi + 0x0A34, # .. 0x0A34 ; Unknown + 0x0A35, # .. 0x0A36 ; Gurmukhi + 0x0A37, # .. 0x0A37 ; Unknown + 0x0A38, # .. 0x0A39 ; Gurmukhi + 0x0A3A, # .. 0x0A3B ; Unknown + 0x0A3C, # .. 0x0A3C ; Gurmukhi + 0x0A3D, # .. 0x0A3D ; Unknown + 0x0A3E, # .. 0x0A42 ; Gurmukhi + 0x0A43, # .. 0x0A46 ; Unknown + 0x0A47, # .. 0x0A48 ; Gurmukhi + 0x0A49, # .. 0x0A4A ; Unknown + 0x0A4B, # .. 0x0A4D ; Gurmukhi + 0x0A4E, # .. 0x0A50 ; Unknown + 0x0A51, # .. 0x0A51 ; Gurmukhi + 0x0A52, # .. 0x0A58 ; Unknown + 0x0A59, # .. 0x0A5C ; Gurmukhi + 0x0A5D, # .. 0x0A5D ; Unknown + 0x0A5E, # .. 0x0A5E ; Gurmukhi + 0x0A5F, # .. 0x0A65 ; Unknown + 0x0A66, # .. 0x0A75 ; Gurmukhi + 0x0A76, # .. 0x0A80 ; Unknown + 0x0A81, # .. 0x0A83 ; Gujarati + 0x0A84, # .. 0x0A84 ; Unknown + 0x0A85, # .. 0x0A8D ; Gujarati + 0x0A8E, # .. 0x0A8E ; Unknown + 0x0A8F, # .. 0x0A91 ; Gujarati + 0x0A92, # .. 0x0A92 ; Unknown + 0x0A93, # .. 0x0AA8 ; Gujarati + 0x0AA9, # .. 0x0AA9 ; Unknown + 0x0AAA, # .. 0x0AB0 ; Gujarati + 0x0AB1, # .. 0x0AB1 ; Unknown + 0x0AB2, # .. 0x0AB3 ; Gujarati + 0x0AB4, # .. 0x0AB4 ; Unknown + 0x0AB5, # .. 0x0AB9 ; Gujarati + 0x0ABA, # .. 0x0ABB ; Unknown + 0x0ABC, # .. 0x0AC5 ; Gujarati + 0x0AC6, # .. 0x0AC6 ; Unknown + 0x0AC7, # .. 0x0AC9 ; Gujarati + 0x0ACA, # .. 0x0ACA ; Unknown + 0x0ACB, # .. 0x0ACD ; Gujarati + 0x0ACE, # .. 0x0ACF ; Unknown + 0x0AD0, # .. 0x0AD0 ; Gujarati + 0x0AD1, # .. 0x0ADF ; Unknown + 0x0AE0, # .. 0x0AE3 ; Gujarati + 0x0AE4, # .. 0x0AE5 ; Unknown + 0x0AE6, # .. 0x0AF1 ; Gujarati + 0x0AF2, # .. 0x0AF8 ; Unknown + 0x0AF9, # .. 0x0AFF ; Gujarati + 0x0B00, # .. 0x0B00 ; Unknown + 0x0B01, # .. 0x0B03 ; Oriya + 0x0B04, # .. 0x0B04 ; Unknown + 0x0B05, # .. 0x0B0C ; Oriya + 0x0B0D, # .. 0x0B0E ; Unknown + 0x0B0F, # .. 0x0B10 ; Oriya + 0x0B11, # .. 0x0B12 ; Unknown + 0x0B13, # .. 0x0B28 ; Oriya + 0x0B29, # .. 0x0B29 ; Unknown + 0x0B2A, # .. 0x0B30 ; Oriya + 0x0B31, # .. 0x0B31 ; Unknown + 0x0B32, # .. 0x0B33 ; Oriya + 0x0B34, # .. 0x0B34 ; Unknown + 0x0B35, # .. 0x0B39 ; Oriya + 0x0B3A, # .. 0x0B3B ; Unknown + 0x0B3C, # .. 0x0B44 ; Oriya + 0x0B45, # .. 0x0B46 ; Unknown + 0x0B47, # .. 0x0B48 ; Oriya + 0x0B49, # .. 0x0B4A ; Unknown + 0x0B4B, # .. 0x0B4D ; Oriya + 0x0B4E, # .. 0x0B55 ; Unknown + 0x0B56, # .. 0x0B57 ; Oriya + 0x0B58, # .. 0x0B5B ; Unknown + 0x0B5C, # .. 0x0B5D ; Oriya + 0x0B5E, # .. 0x0B5E ; Unknown + 0x0B5F, # .. 0x0B63 ; Oriya + 0x0B64, # .. 0x0B65 ; Unknown + 0x0B66, # .. 0x0B77 ; Oriya + 0x0B78, # .. 0x0B81 ; Unknown + 0x0B82, # .. 0x0B83 ; Tamil + 0x0B84, # .. 0x0B84 ; Unknown + 0x0B85, # .. 0x0B8A ; Tamil + 0x0B8B, # .. 0x0B8D ; Unknown + 0x0B8E, # .. 0x0B90 ; Tamil + 0x0B91, # .. 0x0B91 ; Unknown + 0x0B92, # .. 0x0B95 ; Tamil + 0x0B96, # .. 0x0B98 ; Unknown + 0x0B99, # .. 0x0B9A ; Tamil + 0x0B9B, # .. 0x0B9B ; Unknown + 0x0B9C, # .. 0x0B9C ; Tamil + 0x0B9D, # .. 0x0B9D ; Unknown + 0x0B9E, # .. 0x0B9F ; Tamil + 0x0BA0, # .. 0x0BA2 ; Unknown + 0x0BA3, # .. 0x0BA4 ; Tamil + 0x0BA5, # .. 0x0BA7 ; Unknown + 0x0BA8, # .. 0x0BAA ; Tamil + 0x0BAB, # .. 0x0BAD ; Unknown + 0x0BAE, # .. 0x0BB9 ; Tamil + 0x0BBA, # .. 0x0BBD ; Unknown + 0x0BBE, # .. 0x0BC2 ; Tamil + 0x0BC3, # .. 0x0BC5 ; Unknown + 0x0BC6, # .. 0x0BC8 ; Tamil + 0x0BC9, # .. 0x0BC9 ; Unknown + 0x0BCA, # .. 0x0BCD ; Tamil + 0x0BCE, # .. 0x0BCF ; Unknown + 0x0BD0, # .. 0x0BD0 ; Tamil + 0x0BD1, # .. 0x0BD6 ; Unknown + 0x0BD7, # .. 0x0BD7 ; Tamil + 0x0BD8, # .. 0x0BE5 ; Unknown + 0x0BE6, # .. 0x0BFA ; Tamil + 0x0BFB, # .. 0x0BFF ; Unknown + 0x0C00, # .. 0x0C03 ; Telugu + 0x0C04, # .. 0x0C04 ; Unknown + 0x0C05, # .. 0x0C0C ; Telugu + 0x0C0D, # .. 0x0C0D ; Unknown + 0x0C0E, # .. 0x0C10 ; Telugu + 0x0C11, # .. 0x0C11 ; Unknown + 0x0C12, # .. 0x0C28 ; Telugu + 0x0C29, # .. 0x0C29 ; Unknown + 0x0C2A, # .. 0x0C39 ; Telugu + 0x0C3A, # .. 0x0C3C ; Unknown + 0x0C3D, # .. 0x0C44 ; Telugu + 0x0C45, # .. 0x0C45 ; Unknown + 0x0C46, # .. 0x0C48 ; Telugu + 0x0C49, # .. 0x0C49 ; Unknown + 0x0C4A, # .. 0x0C4D ; Telugu + 0x0C4E, # .. 0x0C54 ; Unknown + 0x0C55, # .. 0x0C56 ; Telugu + 0x0C57, # .. 0x0C57 ; Unknown + 0x0C58, # .. 0x0C5A ; Telugu + 0x0C5B, # .. 0x0C5F ; Unknown + 0x0C60, # .. 0x0C63 ; Telugu + 0x0C64, # .. 0x0C65 ; Unknown + 0x0C66, # .. 0x0C6F ; Telugu + 0x0C70, # .. 0x0C77 ; Unknown + 0x0C78, # .. 0x0C7F ; Telugu + 0x0C80, # .. 0x0C83 ; Kannada + 0x0C84, # .. 0x0C84 ; Unknown + 0x0C85, # .. 0x0C8C ; Kannada + 0x0C8D, # .. 0x0C8D ; Unknown + 0x0C8E, # .. 0x0C90 ; Kannada + 0x0C91, # .. 0x0C91 ; Unknown + 0x0C92, # .. 0x0CA8 ; Kannada + 0x0CA9, # .. 0x0CA9 ; Unknown + 0x0CAA, # .. 0x0CB3 ; Kannada + 0x0CB4, # .. 0x0CB4 ; Unknown + 0x0CB5, # .. 0x0CB9 ; Kannada + 0x0CBA, # .. 0x0CBB ; Unknown + 0x0CBC, # .. 0x0CC4 ; Kannada + 0x0CC5, # .. 0x0CC5 ; Unknown + 0x0CC6, # .. 0x0CC8 ; Kannada + 0x0CC9, # .. 0x0CC9 ; Unknown + 0x0CCA, # .. 0x0CCD ; Kannada + 0x0CCE, # .. 0x0CD4 ; Unknown + 0x0CD5, # .. 0x0CD6 ; Kannada + 0x0CD7, # .. 0x0CDD ; Unknown + 0x0CDE, # .. 0x0CDE ; Kannada + 0x0CDF, # .. 0x0CDF ; Unknown + 0x0CE0, # .. 0x0CE3 ; Kannada + 0x0CE4, # .. 0x0CE5 ; Unknown + 0x0CE6, # .. 0x0CEF ; Kannada + 0x0CF0, # .. 0x0CF0 ; Unknown + 0x0CF1, # .. 0x0CF2 ; Kannada + 0x0CF3, # .. 0x0CFF ; Unknown + 0x0D00, # .. 0x0D03 ; Malayalam + 0x0D04, # .. 0x0D04 ; Unknown + 0x0D05, # .. 0x0D0C ; Malayalam + 0x0D0D, # .. 0x0D0D ; Unknown + 0x0D0E, # .. 0x0D10 ; Malayalam + 0x0D11, # .. 0x0D11 ; Unknown + 0x0D12, # .. 0x0D44 ; Malayalam + 0x0D45, # .. 0x0D45 ; Unknown + 0x0D46, # .. 0x0D48 ; Malayalam + 0x0D49, # .. 0x0D49 ; Unknown + 0x0D4A, # .. 0x0D4F ; Malayalam + 0x0D50, # .. 0x0D53 ; Unknown + 0x0D54, # .. 0x0D63 ; Malayalam + 0x0D64, # .. 0x0D65 ; Unknown + 0x0D66, # .. 0x0D7F ; Malayalam + 0x0D80, # .. 0x0D81 ; Unknown + 0x0D82, # .. 0x0D83 ; Sinhala + 0x0D84, # .. 0x0D84 ; Unknown + 0x0D85, # .. 0x0D96 ; Sinhala + 0x0D97, # .. 0x0D99 ; Unknown + 0x0D9A, # .. 0x0DB1 ; Sinhala + 0x0DB2, # .. 0x0DB2 ; Unknown + 0x0DB3, # .. 0x0DBB ; Sinhala + 0x0DBC, # .. 0x0DBC ; Unknown + 0x0DBD, # .. 0x0DBD ; Sinhala + 0x0DBE, # .. 0x0DBF ; Unknown + 0x0DC0, # .. 0x0DC6 ; Sinhala + 0x0DC7, # .. 0x0DC9 ; Unknown + 0x0DCA, # .. 0x0DCA ; Sinhala + 0x0DCB, # .. 0x0DCE ; Unknown + 0x0DCF, # .. 0x0DD4 ; Sinhala + 0x0DD5, # .. 0x0DD5 ; Unknown + 0x0DD6, # .. 0x0DD6 ; Sinhala + 0x0DD7, # .. 0x0DD7 ; Unknown + 0x0DD8, # .. 0x0DDF ; Sinhala + 0x0DE0, # .. 0x0DE5 ; Unknown + 0x0DE6, # .. 0x0DEF ; Sinhala + 0x0DF0, # .. 0x0DF1 ; Unknown + 0x0DF2, # .. 0x0DF4 ; Sinhala + 0x0DF5, # .. 0x0E00 ; Unknown + 0x0E01, # .. 0x0E3A ; Thai + 0x0E3B, # .. 0x0E3E ; Unknown + 0x0E3F, # .. 0x0E3F ; Common + 0x0E40, # .. 0x0E5B ; Thai + 0x0E5C, # .. 0x0E80 ; Unknown + 0x0E81, # .. 0x0E82 ; Lao + 0x0E83, # .. 0x0E83 ; Unknown + 0x0E84, # .. 0x0E84 ; Lao + 0x0E85, # .. 0x0E86 ; Unknown + 0x0E87, # .. 0x0E88 ; Lao + 0x0E89, # .. 0x0E89 ; Unknown + 0x0E8A, # .. 0x0E8A ; Lao + 0x0E8B, # .. 0x0E8C ; Unknown + 0x0E8D, # .. 0x0E8D ; Lao + 0x0E8E, # .. 0x0E93 ; Unknown + 0x0E94, # .. 0x0E97 ; Lao + 0x0E98, # .. 0x0E98 ; Unknown + 0x0E99, # .. 0x0E9F ; Lao + 0x0EA0, # .. 0x0EA0 ; Unknown + 0x0EA1, # .. 0x0EA3 ; Lao + 0x0EA4, # .. 0x0EA4 ; Unknown + 0x0EA5, # .. 0x0EA5 ; Lao + 0x0EA6, # .. 0x0EA6 ; Unknown + 0x0EA7, # .. 0x0EA7 ; Lao + 0x0EA8, # .. 0x0EA9 ; Unknown + 0x0EAA, # .. 0x0EAB ; Lao + 0x0EAC, # .. 0x0EAC ; Unknown + 0x0EAD, # .. 0x0EB9 ; Lao + 0x0EBA, # .. 0x0EBA ; Unknown + 0x0EBB, # .. 0x0EBD ; Lao + 0x0EBE, # .. 0x0EBF ; Unknown + 0x0EC0, # .. 0x0EC4 ; Lao + 0x0EC5, # .. 0x0EC5 ; Unknown + 0x0EC6, # .. 0x0EC6 ; Lao + 0x0EC7, # .. 0x0EC7 ; Unknown + 0x0EC8, # .. 0x0ECD ; Lao + 0x0ECE, # .. 0x0ECF ; Unknown + 0x0ED0, # .. 0x0ED9 ; Lao + 0x0EDA, # .. 0x0EDB ; Unknown + 0x0EDC, # .. 0x0EDF ; Lao + 0x0EE0, # .. 0x0EFF ; Unknown + 0x0F00, # .. 0x0F47 ; Tibetan + 0x0F48, # .. 0x0F48 ; Unknown + 0x0F49, # .. 0x0F6C ; Tibetan + 0x0F6D, # .. 0x0F70 ; Unknown + 0x0F71, # .. 0x0F97 ; Tibetan + 0x0F98, # .. 0x0F98 ; Unknown + 0x0F99, # .. 0x0FBC ; Tibetan + 0x0FBD, # .. 0x0FBD ; Unknown + 0x0FBE, # .. 0x0FCC ; Tibetan + 0x0FCD, # .. 0x0FCD ; Unknown + 0x0FCE, # .. 0x0FD4 ; Tibetan + 0x0FD5, # .. 0x0FD8 ; Common + 0x0FD9, # .. 0x0FDA ; Tibetan + 0x0FDB, # .. 0x0FFF ; Unknown + 0x1000, # .. 0x109F ; Myanmar + 0x10A0, # .. 0x10C5 ; Georgian + 0x10C6, # .. 0x10C6 ; Unknown + 0x10C7, # .. 0x10C7 ; Georgian + 0x10C8, # .. 0x10CC ; Unknown + 0x10CD, # .. 0x10CD ; Georgian + 0x10CE, # .. 0x10CF ; Unknown + 0x10D0, # .. 0x10FA ; Georgian + 0x10FB, # .. 0x10FB ; Common + 0x10FC, # .. 0x10FF ; Georgian + 0x1100, # .. 0x11FF ; Hangul + 0x1200, # .. 0x1248 ; Ethiopic + 0x1249, # .. 0x1249 ; Unknown + 0x124A, # .. 0x124D ; Ethiopic + 0x124E, # .. 0x124F ; Unknown + 0x1250, # .. 0x1256 ; Ethiopic + 0x1257, # .. 0x1257 ; Unknown + 0x1258, # .. 0x1258 ; Ethiopic + 0x1259, # .. 0x1259 ; Unknown + 0x125A, # .. 0x125D ; Ethiopic + 0x125E, # .. 0x125F ; Unknown + 0x1260, # .. 0x1288 ; Ethiopic + 0x1289, # .. 0x1289 ; Unknown + 0x128A, # .. 0x128D ; Ethiopic + 0x128E, # .. 0x128F ; Unknown + 0x1290, # .. 0x12B0 ; Ethiopic + 0x12B1, # .. 0x12B1 ; Unknown + 0x12B2, # .. 0x12B5 ; Ethiopic + 0x12B6, # .. 0x12B7 ; Unknown + 0x12B8, # .. 0x12BE ; Ethiopic + 0x12BF, # .. 0x12BF ; Unknown + 0x12C0, # .. 0x12C0 ; Ethiopic + 0x12C1, # .. 0x12C1 ; Unknown + 0x12C2, # .. 0x12C5 ; Ethiopic + 0x12C6, # .. 0x12C7 ; Unknown + 0x12C8, # .. 0x12D6 ; Ethiopic + 0x12D7, # .. 0x12D7 ; Unknown + 0x12D8, # .. 0x1310 ; Ethiopic + 0x1311, # .. 0x1311 ; Unknown + 0x1312, # .. 0x1315 ; Ethiopic + 0x1316, # .. 0x1317 ; Unknown + 0x1318, # .. 0x135A ; Ethiopic + 0x135B, # .. 0x135C ; Unknown + 0x135D, # .. 0x137C ; Ethiopic + 0x137D, # .. 0x137F ; Unknown + 0x1380, # .. 0x1399 ; Ethiopic + 0x139A, # .. 0x139F ; Unknown + 0x13A0, # .. 0x13F5 ; Cherokee + 0x13F6, # .. 0x13F7 ; Unknown + 0x13F8, # .. 0x13FD ; Cherokee + 0x13FE, # .. 0x13FF ; Unknown + 0x1400, # .. 0x167F ; Canadian_Aboriginal + 0x1680, # .. 0x169C ; Ogham + 0x169D, # .. 0x169F ; Unknown + 0x16A0, # .. 0x16EA ; Runic + 0x16EB, # .. 0x16ED ; Common + 0x16EE, # .. 0x16F8 ; Runic + 0x16F9, # .. 0x16FF ; Unknown + 0x1700, # .. 0x170C ; Tagalog + 0x170D, # .. 0x170D ; Unknown + 0x170E, # .. 0x1714 ; Tagalog + 0x1715, # .. 0x171F ; Unknown + 0x1720, # .. 0x1734 ; Hanunoo + 0x1735, # .. 0x1736 ; Common + 0x1737, # .. 0x173F ; Unknown + 0x1740, # .. 0x1753 ; Buhid + 0x1754, # .. 0x175F ; Unknown + 0x1760, # .. 0x176C ; Tagbanwa + 0x176D, # .. 0x176D ; Unknown + 0x176E, # .. 0x1770 ; Tagbanwa + 0x1771, # .. 0x1771 ; Unknown + 0x1772, # .. 0x1773 ; Tagbanwa + 0x1774, # .. 0x177F ; Unknown + 0x1780, # .. 0x17DD ; Khmer + 0x17DE, # .. 0x17DF ; Unknown + 0x17E0, # .. 0x17E9 ; Khmer + 0x17EA, # .. 0x17EF ; Unknown + 0x17F0, # .. 0x17F9 ; Khmer + 0x17FA, # .. 0x17FF ; Unknown + 0x1800, # .. 0x1801 ; Mongolian + 0x1802, # .. 0x1803 ; Common + 0x1804, # .. 0x1804 ; Mongolian + 0x1805, # .. 0x1805 ; Common + 0x1806, # .. 0x180E ; Mongolian + 0x180F, # .. 0x180F ; Unknown + 0x1810, # .. 0x1819 ; Mongolian + 0x181A, # .. 0x181F ; Unknown + 0x1820, # .. 0x1877 ; Mongolian + 0x1878, # .. 0x187F ; Unknown + 0x1880, # .. 0x18AA ; Mongolian + 0x18AB, # .. 0x18AF ; Unknown + 0x18B0, # .. 0x18F5 ; Canadian_Aboriginal + 0x18F6, # .. 0x18FF ; Unknown + 0x1900, # .. 0x191E ; Limbu + 0x191F, # .. 0x191F ; Unknown + 0x1920, # .. 0x192B ; Limbu + 0x192C, # .. 0x192F ; Unknown + 0x1930, # .. 0x193B ; Limbu + 0x193C, # .. 0x193F ; Unknown + 0x1940, # .. 0x1940 ; Limbu + 0x1941, # .. 0x1943 ; Unknown + 0x1944, # .. 0x194F ; Limbu + 0x1950, # .. 0x196D ; Tai_Le + 0x196E, # .. 0x196F ; Unknown + 0x1970, # .. 0x1974 ; Tai_Le + 0x1975, # .. 0x197F ; Unknown + 0x1980, # .. 0x19AB ; New_Tai_Lue + 0x19AC, # .. 0x19AF ; Unknown + 0x19B0, # .. 0x19C9 ; New_Tai_Lue + 0x19CA, # .. 0x19CF ; Unknown + 0x19D0, # .. 0x19DA ; New_Tai_Lue + 0x19DB, # .. 0x19DD ; Unknown + 0x19DE, # .. 0x19DF ; New_Tai_Lue + 0x19E0, # .. 0x19FF ; Khmer + 0x1A00, # .. 0x1A1B ; Buginese + 0x1A1C, # .. 0x1A1D ; Unknown + 0x1A1E, # .. 0x1A1F ; Buginese + 0x1A20, # .. 0x1A5E ; Tai_Tham + 0x1A5F, # .. 0x1A5F ; Unknown + 0x1A60, # .. 0x1A7C ; Tai_Tham + 0x1A7D, # .. 0x1A7E ; Unknown + 0x1A7F, # .. 0x1A89 ; Tai_Tham + 0x1A8A, # .. 0x1A8F ; Unknown + 0x1A90, # .. 0x1A99 ; Tai_Tham + 0x1A9A, # .. 0x1A9F ; Unknown + 0x1AA0, # .. 0x1AAD ; Tai_Tham + 0x1AAE, # .. 0x1AAF ; Unknown + 0x1AB0, # .. 0x1ABE ; Inherited + 0x1ABF, # .. 0x1AFF ; Unknown + 0x1B00, # .. 0x1B4B ; Balinese + 0x1B4C, # .. 0x1B4F ; Unknown + 0x1B50, # .. 0x1B7C ; Balinese + 0x1B7D, # .. 0x1B7F ; Unknown + 0x1B80, # .. 0x1BBF ; Sundanese + 0x1BC0, # .. 0x1BF3 ; Batak + 0x1BF4, # .. 0x1BFB ; Unknown + 0x1BFC, # .. 0x1BFF ; Batak + 0x1C00, # .. 0x1C37 ; Lepcha + 0x1C38, # .. 0x1C3A ; Unknown + 0x1C3B, # .. 0x1C49 ; Lepcha + 0x1C4A, # .. 0x1C4C ; Unknown + 0x1C4D, # .. 0x1C4F ; Lepcha + 0x1C50, # .. 0x1C7F ; Ol_Chiki + 0x1C80, # .. 0x1C88 ; Cyrillic + 0x1C89, # .. 0x1CBF ; Unknown + 0x1CC0, # .. 0x1CC7 ; Sundanese + 0x1CC8, # .. 0x1CCF ; Unknown + 0x1CD0, # .. 0x1CD2 ; Inherited + 0x1CD3, # .. 0x1CD3 ; Common + 0x1CD4, # .. 0x1CE0 ; Inherited + 0x1CE1, # .. 0x1CE1 ; Common + 0x1CE2, # .. 0x1CE8 ; Inherited + 0x1CE9, # .. 0x1CEC ; Common + 0x1CED, # .. 0x1CED ; Inherited + 0x1CEE, # .. 0x1CF3 ; Common + 0x1CF4, # .. 0x1CF4 ; Inherited + 0x1CF5, # .. 0x1CF7 ; Common + 0x1CF8, # .. 0x1CF9 ; Inherited + 0x1CFA, # .. 0x1CFF ; Unknown + 0x1D00, # .. 0x1D25 ; Latin + 0x1D26, # .. 0x1D2A ; Greek + 0x1D2B, # .. 0x1D2B ; Cyrillic + 0x1D2C, # .. 0x1D5C ; Latin + 0x1D5D, # .. 0x1D61 ; Greek + 0x1D62, # .. 0x1D65 ; Latin + 0x1D66, # .. 0x1D6A ; Greek + 0x1D6B, # .. 0x1D77 ; Latin + 0x1D78, # .. 0x1D78 ; Cyrillic + 0x1D79, # .. 0x1DBE ; Latin + 0x1DBF, # .. 0x1DBF ; Greek + 0x1DC0, # .. 0x1DF9 ; Inherited + 0x1DFA, # .. 0x1DFA ; Unknown + 0x1DFB, # .. 0x1DFF ; Inherited + 0x1E00, # .. 0x1EFF ; Latin + 0x1F00, # .. 0x1F15 ; Greek + 0x1F16, # .. 0x1F17 ; Unknown + 0x1F18, # .. 0x1F1D ; Greek + 0x1F1E, # .. 0x1F1F ; Unknown + 0x1F20, # .. 0x1F45 ; Greek + 0x1F46, # .. 0x1F47 ; Unknown + 0x1F48, # .. 0x1F4D ; Greek + 0x1F4E, # .. 0x1F4F ; Unknown + 0x1F50, # .. 0x1F57 ; Greek + 0x1F58, # .. 0x1F58 ; Unknown + 0x1F59, # .. 0x1F59 ; Greek + 0x1F5A, # .. 0x1F5A ; Unknown + 0x1F5B, # .. 0x1F5B ; Greek + 0x1F5C, # .. 0x1F5C ; Unknown + 0x1F5D, # .. 0x1F5D ; Greek + 0x1F5E, # .. 0x1F5E ; Unknown + 0x1F5F, # .. 0x1F7D ; Greek + 0x1F7E, # .. 0x1F7F ; Unknown + 0x1F80, # .. 0x1FB4 ; Greek + 0x1FB5, # .. 0x1FB5 ; Unknown + 0x1FB6, # .. 0x1FC4 ; Greek + 0x1FC5, # .. 0x1FC5 ; Unknown + 0x1FC6, # .. 0x1FD3 ; Greek + 0x1FD4, # .. 0x1FD5 ; Unknown + 0x1FD6, # .. 0x1FDB ; Greek + 0x1FDC, # .. 0x1FDC ; Unknown + 0x1FDD, # .. 0x1FEF ; Greek + 0x1FF0, # .. 0x1FF1 ; Unknown + 0x1FF2, # .. 0x1FF4 ; Greek + 0x1FF5, # .. 0x1FF5 ; Unknown + 0x1FF6, # .. 0x1FFE ; Greek + 0x1FFF, # .. 0x1FFF ; Unknown + 0x2000, # .. 0x200B ; Common + 0x200C, # .. 0x200D ; Inherited + 0x200E, # .. 0x2064 ; Common + 0x2065, # .. 0x2065 ; Unknown + 0x2066, # .. 0x2070 ; Common + 0x2071, # .. 0x2071 ; Latin + 0x2072, # .. 0x2073 ; Unknown + 0x2074, # .. 0x207E ; Common + 0x207F, # .. 0x207F ; Latin + 0x2080, # .. 0x208E ; Common + 0x208F, # .. 0x208F ; Unknown + 0x2090, # .. 0x209C ; Latin + 0x209D, # .. 0x209F ; Unknown + 0x20A0, # .. 0x20BF ; Common + 0x20C0, # .. 0x20CF ; Unknown + 0x20D0, # .. 0x20F0 ; Inherited + 0x20F1, # .. 0x20FF ; Unknown + 0x2100, # .. 0x2125 ; Common + 0x2126, # .. 0x2126 ; Greek + 0x2127, # .. 0x2129 ; Common + 0x212A, # .. 0x212B ; Latin + 0x212C, # .. 0x2131 ; Common + 0x2132, # .. 0x2132 ; Latin + 0x2133, # .. 0x214D ; Common + 0x214E, # .. 0x214E ; Latin + 0x214F, # .. 0x215F ; Common + 0x2160, # .. 0x2188 ; Latin + 0x2189, # .. 0x218B ; Common + 0x218C, # .. 0x218F ; Unknown + 0x2190, # .. 0x2426 ; Common + 0x2427, # .. 0x243F ; Unknown + 0x2440, # .. 0x244A ; Common + 0x244B, # .. 0x245F ; Unknown + 0x2460, # .. 0x27FF ; Common + 0x2800, # .. 0x28FF ; Braille + 0x2900, # .. 0x2B73 ; Common + 0x2B74, # .. 0x2B75 ; Unknown + 0x2B76, # .. 0x2B95 ; Common + 0x2B96, # .. 0x2B97 ; Unknown + 0x2B98, # .. 0x2BB9 ; Common + 0x2BBA, # .. 0x2BBC ; Unknown + 0x2BBD, # .. 0x2BC8 ; Common + 0x2BC9, # .. 0x2BC9 ; Unknown + 0x2BCA, # .. 0x2BD2 ; Common + 0x2BD3, # .. 0x2BEB ; Unknown + 0x2BEC, # .. 0x2BEF ; Common + 0x2BF0, # .. 0x2BFF ; Unknown + 0x2C00, # .. 0x2C2E ; Glagolitic + 0x2C2F, # .. 0x2C2F ; Unknown + 0x2C30, # .. 0x2C5E ; Glagolitic + 0x2C5F, # .. 0x2C5F ; Unknown + 0x2C60, # .. 0x2C7F ; Latin + 0x2C80, # .. 0x2CF3 ; Coptic + 0x2CF4, # .. 0x2CF8 ; Unknown + 0x2CF9, # .. 0x2CFF ; Coptic + 0x2D00, # .. 0x2D25 ; Georgian + 0x2D26, # .. 0x2D26 ; Unknown + 0x2D27, # .. 0x2D27 ; Georgian + 0x2D28, # .. 0x2D2C ; Unknown + 0x2D2D, # .. 0x2D2D ; Georgian + 0x2D2E, # .. 0x2D2F ; Unknown + 0x2D30, # .. 0x2D67 ; Tifinagh + 0x2D68, # .. 0x2D6E ; Unknown + 0x2D6F, # .. 0x2D70 ; Tifinagh + 0x2D71, # .. 0x2D7E ; Unknown + 0x2D7F, # .. 0x2D7F ; Tifinagh + 0x2D80, # .. 0x2D96 ; Ethiopic + 0x2D97, # .. 0x2D9F ; Unknown + 0x2DA0, # .. 0x2DA6 ; Ethiopic + 0x2DA7, # .. 0x2DA7 ; Unknown + 0x2DA8, # .. 0x2DAE ; Ethiopic + 0x2DAF, # .. 0x2DAF ; Unknown + 0x2DB0, # .. 0x2DB6 ; Ethiopic + 0x2DB7, # .. 0x2DB7 ; Unknown + 0x2DB8, # .. 0x2DBE ; Ethiopic + 0x2DBF, # .. 0x2DBF ; Unknown + 0x2DC0, # .. 0x2DC6 ; Ethiopic + 0x2DC7, # .. 0x2DC7 ; Unknown + 0x2DC8, # .. 0x2DCE ; Ethiopic + 0x2DCF, # .. 0x2DCF ; Unknown + 0x2DD0, # .. 0x2DD6 ; Ethiopic + 0x2DD7, # .. 0x2DD7 ; Unknown + 0x2DD8, # .. 0x2DDE ; Ethiopic + 0x2DDF, # .. 0x2DDF ; Unknown + 0x2DE0, # .. 0x2DFF ; Cyrillic + 0x2E00, # .. 0x2E49 ; Common + 0x2E4A, # .. 0x2E7F ; Unknown + 0x2E80, # .. 0x2E99 ; Han + 0x2E9A, # .. 0x2E9A ; Unknown + 0x2E9B, # .. 0x2EF3 ; Han + 0x2EF4, # .. 0x2EFF ; Unknown + 0x2F00, # .. 0x2FD5 ; Han + 0x2FD6, # .. 0x2FEF ; Unknown + 0x2FF0, # .. 0x2FFB ; Common + 0x2FFC, # .. 0x2FFF ; Unknown + 0x3000, # .. 0x3004 ; Common + 0x3005, # .. 0x3005 ; Han + 0x3006, # .. 0x3006 ; Common + 0x3007, # .. 0x3007 ; Han + 0x3008, # .. 0x3020 ; Common + 0x3021, # .. 0x3029 ; Han + 0x302A, # .. 0x302D ; Inherited + 0x302E, # .. 0x302F ; Hangul + 0x3030, # .. 0x3037 ; Common + 0x3038, # .. 0x303B ; Han + 0x303C, # .. 0x303F ; Common + 0x3040, # .. 0x3040 ; Unknown + 0x3041, # .. 0x3096 ; Hiragana + 0x3097, # .. 0x3098 ; Unknown + 0x3099, # .. 0x309A ; Inherited + 0x309B, # .. 0x309C ; Common + 0x309D, # .. 0x309F ; Hiragana + 0x30A0, # .. 0x30A0 ; Common + 0x30A1, # .. 0x30FA ; Katakana + 0x30FB, # .. 0x30FC ; Common + 0x30FD, # .. 0x30FF ; Katakana + 0x3100, # .. 0x3104 ; Unknown + 0x3105, # .. 0x312E ; Bopomofo + 0x312F, # .. 0x3130 ; Unknown + 0x3131, # .. 0x318E ; Hangul + 0x318F, # .. 0x318F ; Unknown + 0x3190, # .. 0x319F ; Common + 0x31A0, # .. 0x31BA ; Bopomofo + 0x31BB, # .. 0x31BF ; Unknown + 0x31C0, # .. 0x31E3 ; Common + 0x31E4, # .. 0x31EF ; Unknown + 0x31F0, # .. 0x31FF ; Katakana + 0x3200, # .. 0x321E ; Hangul + 0x321F, # .. 0x321F ; Unknown + 0x3220, # .. 0x325F ; Common + 0x3260, # .. 0x327E ; Hangul + 0x327F, # .. 0x32CF ; Common + 0x32D0, # .. 0x32FE ; Katakana + 0x32FF, # .. 0x32FF ; Unknown + 0x3300, # .. 0x3357 ; Katakana + 0x3358, # .. 0x33FF ; Common + 0x3400, # .. 0x4DB5 ; Han + 0x4DB6, # .. 0x4DBF ; Unknown + 0x4DC0, # .. 0x4DFF ; Common + 0x4E00, # .. 0x9FEA ; Han + 0x9FEB, # .. 0x9FFF ; Unknown + 0xA000, # .. 0xA48C ; Yi + 0xA48D, # .. 0xA48F ; Unknown + 0xA490, # .. 0xA4C6 ; Yi + 0xA4C7, # .. 0xA4CF ; Unknown + 0xA4D0, # .. 0xA4FF ; Lisu + 0xA500, # .. 0xA62B ; Vai + 0xA62C, # .. 0xA63F ; Unknown + 0xA640, # .. 0xA69F ; Cyrillic + 0xA6A0, # .. 0xA6F7 ; Bamum + 0xA6F8, # .. 0xA6FF ; Unknown + 0xA700, # .. 0xA721 ; Common + 0xA722, # .. 0xA787 ; Latin + 0xA788, # .. 0xA78A ; Common + 0xA78B, # .. 0xA7AE ; Latin + 0xA7AF, # .. 0xA7AF ; Unknown + 0xA7B0, # .. 0xA7B7 ; Latin + 0xA7B8, # .. 0xA7F6 ; Unknown + 0xA7F7, # .. 0xA7FF ; Latin + 0xA800, # .. 0xA82B ; Syloti_Nagri + 0xA82C, # .. 0xA82F ; Unknown + 0xA830, # .. 0xA839 ; Common + 0xA83A, # .. 0xA83F ; Unknown + 0xA840, # .. 0xA877 ; Phags_Pa + 0xA878, # .. 0xA87F ; Unknown + 0xA880, # .. 0xA8C5 ; Saurashtra + 0xA8C6, # .. 0xA8CD ; Unknown + 0xA8CE, # .. 0xA8D9 ; Saurashtra + 0xA8DA, # .. 0xA8DF ; Unknown + 0xA8E0, # .. 0xA8FD ; Devanagari + 0xA8FE, # .. 0xA8FF ; Unknown + 0xA900, # .. 0xA92D ; Kayah_Li + 0xA92E, # .. 0xA92E ; Common + 0xA92F, # .. 0xA92F ; Kayah_Li + 0xA930, # .. 0xA953 ; Rejang + 0xA954, # .. 0xA95E ; Unknown + 0xA95F, # .. 0xA95F ; Rejang + 0xA960, # .. 0xA97C ; Hangul + 0xA97D, # .. 0xA97F ; Unknown + 0xA980, # .. 0xA9CD ; Javanese + 0xA9CE, # .. 0xA9CE ; Unknown + 0xA9CF, # .. 0xA9CF ; Common + 0xA9D0, # .. 0xA9D9 ; Javanese + 0xA9DA, # .. 0xA9DD ; Unknown + 0xA9DE, # .. 0xA9DF ; Javanese + 0xA9E0, # .. 0xA9FE ; Myanmar + 0xA9FF, # .. 0xA9FF ; Unknown + 0xAA00, # .. 0xAA36 ; Cham + 0xAA37, # .. 0xAA3F ; Unknown + 0xAA40, # .. 0xAA4D ; Cham + 0xAA4E, # .. 0xAA4F ; Unknown + 0xAA50, # .. 0xAA59 ; Cham + 0xAA5A, # .. 0xAA5B ; Unknown + 0xAA5C, # .. 0xAA5F ; Cham + 0xAA60, # .. 0xAA7F ; Myanmar + 0xAA80, # .. 0xAAC2 ; Tai_Viet + 0xAAC3, # .. 0xAADA ; Unknown + 0xAADB, # .. 0xAADF ; Tai_Viet + 0xAAE0, # .. 0xAAF6 ; Meetei_Mayek + 0xAAF7, # .. 0xAB00 ; Unknown + 0xAB01, # .. 0xAB06 ; Ethiopic + 0xAB07, # .. 0xAB08 ; Unknown + 0xAB09, # .. 0xAB0E ; Ethiopic + 0xAB0F, # .. 0xAB10 ; Unknown + 0xAB11, # .. 0xAB16 ; Ethiopic + 0xAB17, # .. 0xAB1F ; Unknown + 0xAB20, # .. 0xAB26 ; Ethiopic + 0xAB27, # .. 0xAB27 ; Unknown + 0xAB28, # .. 0xAB2E ; Ethiopic + 0xAB2F, # .. 0xAB2F ; Unknown + 0xAB30, # .. 0xAB5A ; Latin + 0xAB5B, # .. 0xAB5B ; Common + 0xAB5C, # .. 0xAB64 ; Latin + 0xAB65, # .. 0xAB65 ; Greek + 0xAB66, # .. 0xAB6F ; Unknown + 0xAB70, # .. 0xABBF ; Cherokee + 0xABC0, # .. 0xABED ; Meetei_Mayek + 0xABEE, # .. 0xABEF ; Unknown + 0xABF0, # .. 0xABF9 ; Meetei_Mayek + 0xABFA, # .. 0xABFF ; Unknown + 0xAC00, # .. 0xD7A3 ; Hangul + 0xD7A4, # .. 0xD7AF ; Unknown + 0xD7B0, # .. 0xD7C6 ; Hangul + 0xD7C7, # .. 0xD7CA ; Unknown + 0xD7CB, # .. 0xD7FB ; Hangul + 0xD7FC, # .. 0xF8FF ; Unknown + 0xF900, # .. 0xFA6D ; Han + 0xFA6E, # .. 0xFA6F ; Unknown + 0xFA70, # .. 0xFAD9 ; Han + 0xFADA, # .. 0xFAFF ; Unknown + 0xFB00, # .. 0xFB06 ; Latin + 0xFB07, # .. 0xFB12 ; Unknown + 0xFB13, # .. 0xFB17 ; Armenian + 0xFB18, # .. 0xFB1C ; Unknown + 0xFB1D, # .. 0xFB36 ; Hebrew + 0xFB37, # .. 0xFB37 ; Unknown + 0xFB38, # .. 0xFB3C ; Hebrew + 0xFB3D, # .. 0xFB3D ; Unknown + 0xFB3E, # .. 0xFB3E ; Hebrew + 0xFB3F, # .. 0xFB3F ; Unknown + 0xFB40, # .. 0xFB41 ; Hebrew + 0xFB42, # .. 0xFB42 ; Unknown + 0xFB43, # .. 0xFB44 ; Hebrew + 0xFB45, # .. 0xFB45 ; Unknown + 0xFB46, # .. 0xFB4F ; Hebrew + 0xFB50, # .. 0xFBC1 ; Arabic + 0xFBC2, # .. 0xFBD2 ; Unknown + 0xFBD3, # .. 0xFD3D ; Arabic + 0xFD3E, # .. 0xFD3F ; Common + 0xFD40, # .. 0xFD4F ; Unknown + 0xFD50, # .. 0xFD8F ; Arabic + 0xFD90, # .. 0xFD91 ; Unknown + 0xFD92, # .. 0xFDC7 ; Arabic + 0xFDC8, # .. 0xFDEF ; Unknown + 0xFDF0, # .. 0xFDFD ; Arabic + 0xFDFE, # .. 0xFDFF ; Unknown + 0xFE00, # .. 0xFE0F ; Inherited + 0xFE10, # .. 0xFE19 ; Common + 0xFE1A, # .. 0xFE1F ; Unknown + 0xFE20, # .. 0xFE2D ; Inherited + 0xFE2E, # .. 0xFE2F ; Cyrillic + 0xFE30, # .. 0xFE52 ; Common + 0xFE53, # .. 0xFE53 ; Unknown + 0xFE54, # .. 0xFE66 ; Common + 0xFE67, # .. 0xFE67 ; Unknown + 0xFE68, # .. 0xFE6B ; Common + 0xFE6C, # .. 0xFE6F ; Unknown + 0xFE70, # .. 0xFE74 ; Arabic + 0xFE75, # .. 0xFE75 ; Unknown + 0xFE76, # .. 0xFEFC ; Arabic + 0xFEFD, # .. 0xFEFE ; Unknown + 0xFEFF, # .. 0xFEFF ; Common + 0xFF00, # .. 0xFF00 ; Unknown + 0xFF01, # .. 0xFF20 ; Common + 0xFF21, # .. 0xFF3A ; Latin + 0xFF3B, # .. 0xFF40 ; Common + 0xFF41, # .. 0xFF5A ; Latin + 0xFF5B, # .. 0xFF65 ; Common + 0xFF66, # .. 0xFF6F ; Katakana + 0xFF70, # .. 0xFF70 ; Common + 0xFF71, # .. 0xFF9D ; Katakana + 0xFF9E, # .. 0xFF9F ; Common + 0xFFA0, # .. 0xFFBE ; Hangul + 0xFFBF, # .. 0xFFC1 ; Unknown + 0xFFC2, # .. 0xFFC7 ; Hangul + 0xFFC8, # .. 0xFFC9 ; Unknown + 0xFFCA, # .. 0xFFCF ; Hangul + 0xFFD0, # .. 0xFFD1 ; Unknown + 0xFFD2, # .. 0xFFD7 ; Hangul + 0xFFD8, # .. 0xFFD9 ; Unknown + 0xFFDA, # .. 0xFFDC ; Hangul + 0xFFDD, # .. 0xFFDF ; Unknown + 0xFFE0, # .. 0xFFE6 ; Common + 0xFFE7, # .. 0xFFE7 ; Unknown + 0xFFE8, # .. 0xFFEE ; Common + 0xFFEF, # .. 0xFFF8 ; Unknown + 0xFFF9, # .. 0xFFFD ; Common + 0xFFFE, # .. 0xFFFF ; Unknown + 0x10000, # .. 0x1000B ; Linear_B + 0x1000C, # .. 0x1000C ; Unknown + 0x1000D, # .. 0x10026 ; Linear_B + 0x10027, # .. 0x10027 ; Unknown + 0x10028, # .. 0x1003A ; Linear_B + 0x1003B, # .. 0x1003B ; Unknown + 0x1003C, # .. 0x1003D ; Linear_B + 0x1003E, # .. 0x1003E ; Unknown + 0x1003F, # .. 0x1004D ; Linear_B + 0x1004E, # .. 0x1004F ; Unknown + 0x10050, # .. 0x1005D ; Linear_B + 0x1005E, # .. 0x1007F ; Unknown + 0x10080, # .. 0x100FA ; Linear_B + 0x100FB, # .. 0x100FF ; Unknown + 0x10100, # .. 0x10102 ; Common + 0x10103, # .. 0x10106 ; Unknown + 0x10107, # .. 0x10133 ; Common + 0x10134, # .. 0x10136 ; Unknown + 0x10137, # .. 0x1013F ; Common + 0x10140, # .. 0x1018E ; Greek + 0x1018F, # .. 0x1018F ; Unknown + 0x10190, # .. 0x1019B ; Common + 0x1019C, # .. 0x1019F ; Unknown + 0x101A0, # .. 0x101A0 ; Greek + 0x101A1, # .. 0x101CF ; Unknown + 0x101D0, # .. 0x101FC ; Common + 0x101FD, # .. 0x101FD ; Inherited + 0x101FE, # .. 0x1027F ; Unknown + 0x10280, # .. 0x1029C ; Lycian + 0x1029D, # .. 0x1029F ; Unknown + 0x102A0, # .. 0x102D0 ; Carian + 0x102D1, # .. 0x102DF ; Unknown + 0x102E0, # .. 0x102E0 ; Inherited + 0x102E1, # .. 0x102FB ; Common + 0x102FC, # .. 0x102FF ; Unknown + 0x10300, # .. 0x10323 ; Old_Italic + 0x10324, # .. 0x1032C ; Unknown + 0x1032D, # .. 0x1032F ; Old_Italic + 0x10330, # .. 0x1034A ; Gothic + 0x1034B, # .. 0x1034F ; Unknown + 0x10350, # .. 0x1037A ; Old_Permic + 0x1037B, # .. 0x1037F ; Unknown + 0x10380, # .. 0x1039D ; Ugaritic + 0x1039E, # .. 0x1039E ; Unknown + 0x1039F, # .. 0x1039F ; Ugaritic + 0x103A0, # .. 0x103C3 ; Old_Persian + 0x103C4, # .. 0x103C7 ; Unknown + 0x103C8, # .. 0x103D5 ; Old_Persian + 0x103D6, # .. 0x103FF ; Unknown + 0x10400, # .. 0x1044F ; Deseret + 0x10450, # .. 0x1047F ; Shavian + 0x10480, # .. 0x1049D ; Osmanya + 0x1049E, # .. 0x1049F ; Unknown + 0x104A0, # .. 0x104A9 ; Osmanya + 0x104AA, # .. 0x104AF ; Unknown + 0x104B0, # .. 0x104D3 ; Osage + 0x104D4, # .. 0x104D7 ; Unknown + 0x104D8, # .. 0x104FB ; Osage + 0x104FC, # .. 0x104FF ; Unknown + 0x10500, # .. 0x10527 ; Elbasan + 0x10528, # .. 0x1052F ; Unknown + 0x10530, # .. 0x10563 ; Caucasian_Albanian + 0x10564, # .. 0x1056E ; Unknown + 0x1056F, # .. 0x1056F ; Caucasian_Albanian + 0x10570, # .. 0x105FF ; Unknown + 0x10600, # .. 0x10736 ; Linear_A + 0x10737, # .. 0x1073F ; Unknown + 0x10740, # .. 0x10755 ; Linear_A + 0x10756, # .. 0x1075F ; Unknown + 0x10760, # .. 0x10767 ; Linear_A + 0x10768, # .. 0x107FF ; Unknown + 0x10800, # .. 0x10805 ; Cypriot + 0x10806, # .. 0x10807 ; Unknown + 0x10808, # .. 0x10808 ; Cypriot + 0x10809, # .. 0x10809 ; Unknown + 0x1080A, # .. 0x10835 ; Cypriot + 0x10836, # .. 0x10836 ; Unknown + 0x10837, # .. 0x10838 ; Cypriot + 0x10839, # .. 0x1083B ; Unknown + 0x1083C, # .. 0x1083C ; Cypriot + 0x1083D, # .. 0x1083E ; Unknown + 0x1083F, # .. 0x1083F ; Cypriot + 0x10840, # .. 0x10855 ; Imperial_Aramaic + 0x10856, # .. 0x10856 ; Unknown + 0x10857, # .. 0x1085F ; Imperial_Aramaic + 0x10860, # .. 0x1087F ; Palmyrene + 0x10880, # .. 0x1089E ; Nabataean + 0x1089F, # .. 0x108A6 ; Unknown + 0x108A7, # .. 0x108AF ; Nabataean + 0x108B0, # .. 0x108DF ; Unknown + 0x108E0, # .. 0x108F2 ; Hatran + 0x108F3, # .. 0x108F3 ; Unknown + 0x108F4, # .. 0x108F5 ; Hatran + 0x108F6, # .. 0x108FA ; Unknown + 0x108FB, # .. 0x108FF ; Hatran + 0x10900, # .. 0x1091B ; Phoenician + 0x1091C, # .. 0x1091E ; Unknown + 0x1091F, # .. 0x1091F ; Phoenician + 0x10920, # .. 0x10939 ; Lydian + 0x1093A, # .. 0x1093E ; Unknown + 0x1093F, # .. 0x1093F ; Lydian + 0x10940, # .. 0x1097F ; Unknown + 0x10980, # .. 0x1099F ; Meroitic_Hieroglyphs + 0x109A0, # .. 0x109B7 ; Meroitic_Cursive + 0x109B8, # .. 0x109BB ; Unknown + 0x109BC, # .. 0x109CF ; Meroitic_Cursive + 0x109D0, # .. 0x109D1 ; Unknown + 0x109D2, # .. 0x109FF ; Meroitic_Cursive + 0x10A00, # .. 0x10A03 ; Kharoshthi + 0x10A04, # .. 0x10A04 ; Unknown + 0x10A05, # .. 0x10A06 ; Kharoshthi + 0x10A07, # .. 0x10A0B ; Unknown + 0x10A0C, # .. 0x10A13 ; Kharoshthi + 0x10A14, # .. 0x10A14 ; Unknown + 0x10A15, # .. 0x10A17 ; Kharoshthi + 0x10A18, # .. 0x10A18 ; Unknown + 0x10A19, # .. 0x10A33 ; Kharoshthi + 0x10A34, # .. 0x10A37 ; Unknown + 0x10A38, # .. 0x10A3A ; Kharoshthi + 0x10A3B, # .. 0x10A3E ; Unknown + 0x10A3F, # .. 0x10A47 ; Kharoshthi + 0x10A48, # .. 0x10A4F ; Unknown + 0x10A50, # .. 0x10A58 ; Kharoshthi + 0x10A59, # .. 0x10A5F ; Unknown + 0x10A60, # .. 0x10A7F ; Old_South_Arabian + 0x10A80, # .. 0x10A9F ; Old_North_Arabian + 0x10AA0, # .. 0x10ABF ; Unknown + 0x10AC0, # .. 0x10AE6 ; Manichaean + 0x10AE7, # .. 0x10AEA ; Unknown + 0x10AEB, # .. 0x10AF6 ; Manichaean + 0x10AF7, # .. 0x10AFF ; Unknown + 0x10B00, # .. 0x10B35 ; Avestan + 0x10B36, # .. 0x10B38 ; Unknown + 0x10B39, # .. 0x10B3F ; Avestan + 0x10B40, # .. 0x10B55 ; Inscriptional_Parthian + 0x10B56, # .. 0x10B57 ; Unknown + 0x10B58, # .. 0x10B5F ; Inscriptional_Parthian + 0x10B60, # .. 0x10B72 ; Inscriptional_Pahlavi + 0x10B73, # .. 0x10B77 ; Unknown + 0x10B78, # .. 0x10B7F ; Inscriptional_Pahlavi + 0x10B80, # .. 0x10B91 ; Psalter_Pahlavi + 0x10B92, # .. 0x10B98 ; Unknown + 0x10B99, # .. 0x10B9C ; Psalter_Pahlavi + 0x10B9D, # .. 0x10BA8 ; Unknown + 0x10BA9, # .. 0x10BAF ; Psalter_Pahlavi + 0x10BB0, # .. 0x10BFF ; Unknown + 0x10C00, # .. 0x10C48 ; Old_Turkic + 0x10C49, # .. 0x10C7F ; Unknown + 0x10C80, # .. 0x10CB2 ; Old_Hungarian + 0x10CB3, # .. 0x10CBF ; Unknown + 0x10CC0, # .. 0x10CF2 ; Old_Hungarian + 0x10CF3, # .. 0x10CF9 ; Unknown + 0x10CFA, # .. 0x10CFF ; Old_Hungarian + 0x10D00, # .. 0x10E5F ; Unknown + 0x10E60, # .. 0x10E7E ; Arabic + 0x10E7F, # .. 0x10FFF ; Unknown + 0x11000, # .. 0x1104D ; Brahmi + 0x1104E, # .. 0x11051 ; Unknown + 0x11052, # .. 0x1106F ; Brahmi + 0x11070, # .. 0x1107E ; Unknown + 0x1107F, # .. 0x1107F ; Brahmi + 0x11080, # .. 0x110C1 ; Kaithi + 0x110C2, # .. 0x110CF ; Unknown + 0x110D0, # .. 0x110E8 ; Sora_Sompeng + 0x110E9, # .. 0x110EF ; Unknown + 0x110F0, # .. 0x110F9 ; Sora_Sompeng + 0x110FA, # .. 0x110FF ; Unknown + 0x11100, # .. 0x11134 ; Chakma + 0x11135, # .. 0x11135 ; Unknown + 0x11136, # .. 0x11143 ; Chakma + 0x11144, # .. 0x1114F ; Unknown + 0x11150, # .. 0x11176 ; Mahajani + 0x11177, # .. 0x1117F ; Unknown + 0x11180, # .. 0x111CD ; Sharada + 0x111CE, # .. 0x111CF ; Unknown + 0x111D0, # .. 0x111DF ; Sharada + 0x111E0, # .. 0x111E0 ; Unknown + 0x111E1, # .. 0x111F4 ; Sinhala + 0x111F5, # .. 0x111FF ; Unknown + 0x11200, # .. 0x11211 ; Khojki + 0x11212, # .. 0x11212 ; Unknown + 0x11213, # .. 0x1123E ; Khojki + 0x1123F, # .. 0x1127F ; Unknown + 0x11280, # .. 0x11286 ; Multani + 0x11287, # .. 0x11287 ; Unknown + 0x11288, # .. 0x11288 ; Multani + 0x11289, # .. 0x11289 ; Unknown + 0x1128A, # .. 0x1128D ; Multani + 0x1128E, # .. 0x1128E ; Unknown + 0x1128F, # .. 0x1129D ; Multani + 0x1129E, # .. 0x1129E ; Unknown + 0x1129F, # .. 0x112A9 ; Multani + 0x112AA, # .. 0x112AF ; Unknown + 0x112B0, # .. 0x112EA ; Khudawadi + 0x112EB, # .. 0x112EF ; Unknown + 0x112F0, # .. 0x112F9 ; Khudawadi + 0x112FA, # .. 0x112FF ; Unknown + 0x11300, # .. 0x11303 ; Grantha + 0x11304, # .. 0x11304 ; Unknown + 0x11305, # .. 0x1130C ; Grantha + 0x1130D, # .. 0x1130E ; Unknown + 0x1130F, # .. 0x11310 ; Grantha + 0x11311, # .. 0x11312 ; Unknown + 0x11313, # .. 0x11328 ; Grantha + 0x11329, # .. 0x11329 ; Unknown + 0x1132A, # .. 0x11330 ; Grantha + 0x11331, # .. 0x11331 ; Unknown + 0x11332, # .. 0x11333 ; Grantha + 0x11334, # .. 0x11334 ; Unknown + 0x11335, # .. 0x11339 ; Grantha + 0x1133A, # .. 0x1133B ; Unknown + 0x1133C, # .. 0x11344 ; Grantha + 0x11345, # .. 0x11346 ; Unknown + 0x11347, # .. 0x11348 ; Grantha + 0x11349, # .. 0x1134A ; Unknown + 0x1134B, # .. 0x1134D ; Grantha + 0x1134E, # .. 0x1134F ; Unknown + 0x11350, # .. 0x11350 ; Grantha + 0x11351, # .. 0x11356 ; Unknown + 0x11357, # .. 0x11357 ; Grantha + 0x11358, # .. 0x1135C ; Unknown + 0x1135D, # .. 0x11363 ; Grantha + 0x11364, # .. 0x11365 ; Unknown + 0x11366, # .. 0x1136C ; Grantha + 0x1136D, # .. 0x1136F ; Unknown + 0x11370, # .. 0x11374 ; Grantha + 0x11375, # .. 0x113FF ; Unknown + 0x11400, # .. 0x11459 ; Newa + 0x1145A, # .. 0x1145A ; Unknown + 0x1145B, # .. 0x1145B ; Newa + 0x1145C, # .. 0x1145C ; Unknown + 0x1145D, # .. 0x1145D ; Newa + 0x1145E, # .. 0x1147F ; Unknown + 0x11480, # .. 0x114C7 ; Tirhuta + 0x114C8, # .. 0x114CF ; Unknown + 0x114D0, # .. 0x114D9 ; Tirhuta + 0x114DA, # .. 0x1157F ; Unknown + 0x11580, # .. 0x115B5 ; Siddham + 0x115B6, # .. 0x115B7 ; Unknown + 0x115B8, # .. 0x115DD ; Siddham + 0x115DE, # .. 0x115FF ; Unknown + 0x11600, # .. 0x11644 ; Modi + 0x11645, # .. 0x1164F ; Unknown + 0x11650, # .. 0x11659 ; Modi + 0x1165A, # .. 0x1165F ; Unknown + 0x11660, # .. 0x1166C ; Mongolian + 0x1166D, # .. 0x1167F ; Unknown + 0x11680, # .. 0x116B7 ; Takri + 0x116B8, # .. 0x116BF ; Unknown + 0x116C0, # .. 0x116C9 ; Takri + 0x116CA, # .. 0x116FF ; Unknown + 0x11700, # .. 0x11719 ; Ahom + 0x1171A, # .. 0x1171C ; Unknown + 0x1171D, # .. 0x1172B ; Ahom + 0x1172C, # .. 0x1172F ; Unknown + 0x11730, # .. 0x1173F ; Ahom + 0x11740, # .. 0x1189F ; Unknown + 0x118A0, # .. 0x118F2 ; Warang_Citi + 0x118F3, # .. 0x118FE ; Unknown + 0x118FF, # .. 0x118FF ; Warang_Citi + 0x11900, # .. 0x119FF ; Unknown + 0x11A00, # .. 0x11A47 ; Zanabazar_Square + 0x11A48, # .. 0x11A4F ; Unknown + 0x11A50, # .. 0x11A83 ; Soyombo + 0x11A84, # .. 0x11A85 ; Unknown + 0x11A86, # .. 0x11A9C ; Soyombo + 0x11A9D, # .. 0x11A9D ; Unknown + 0x11A9E, # .. 0x11AA2 ; Soyombo + 0x11AA3, # .. 0x11ABF ; Unknown + 0x11AC0, # .. 0x11AF8 ; Pau_Cin_Hau + 0x11AF9, # .. 0x11BFF ; Unknown + 0x11C00, # .. 0x11C08 ; Bhaiksuki + 0x11C09, # .. 0x11C09 ; Unknown + 0x11C0A, # .. 0x11C36 ; Bhaiksuki + 0x11C37, # .. 0x11C37 ; Unknown + 0x11C38, # .. 0x11C45 ; Bhaiksuki + 0x11C46, # .. 0x11C4F ; Unknown + 0x11C50, # .. 0x11C6C ; Bhaiksuki + 0x11C6D, # .. 0x11C6F ; Unknown + 0x11C70, # .. 0x11C8F ; Marchen + 0x11C90, # .. 0x11C91 ; Unknown + 0x11C92, # .. 0x11CA7 ; Marchen + 0x11CA8, # .. 0x11CA8 ; Unknown + 0x11CA9, # .. 0x11CB6 ; Marchen + 0x11CB7, # .. 0x11CFF ; Unknown + 0x11D00, # .. 0x11D06 ; Masaram_Gondi + 0x11D07, # .. 0x11D07 ; Unknown + 0x11D08, # .. 0x11D09 ; Masaram_Gondi + 0x11D0A, # .. 0x11D0A ; Unknown + 0x11D0B, # .. 0x11D36 ; Masaram_Gondi + 0x11D37, # .. 0x11D39 ; Unknown + 0x11D3A, # .. 0x11D3A ; Masaram_Gondi + 0x11D3B, # .. 0x11D3B ; Unknown + 0x11D3C, # .. 0x11D3D ; Masaram_Gondi + 0x11D3E, # .. 0x11D3E ; Unknown + 0x11D3F, # .. 0x11D47 ; Masaram_Gondi + 0x11D48, # .. 0x11D4F ; Unknown + 0x11D50, # .. 0x11D59 ; Masaram_Gondi + 0x11D5A, # .. 0x11FFF ; Unknown + 0x12000, # .. 0x12399 ; Cuneiform + 0x1239A, # .. 0x123FF ; Unknown + 0x12400, # .. 0x1246E ; Cuneiform + 0x1246F, # .. 0x1246F ; Unknown + 0x12470, # .. 0x12474 ; Cuneiform + 0x12475, # .. 0x1247F ; Unknown + 0x12480, # .. 0x12543 ; Cuneiform + 0x12544, # .. 0x12FFF ; Unknown + 0x13000, # .. 0x1342E ; Egyptian_Hieroglyphs + 0x1342F, # .. 0x143FF ; Unknown + 0x14400, # .. 0x14646 ; Anatolian_Hieroglyphs + 0x14647, # .. 0x167FF ; Unknown + 0x16800, # .. 0x16A38 ; Bamum + 0x16A39, # .. 0x16A3F ; Unknown + 0x16A40, # .. 0x16A5E ; Mro + 0x16A5F, # .. 0x16A5F ; Unknown + 0x16A60, # .. 0x16A69 ; Mro + 0x16A6A, # .. 0x16A6D ; Unknown + 0x16A6E, # .. 0x16A6F ; Mro + 0x16A70, # .. 0x16ACF ; Unknown + 0x16AD0, # .. 0x16AED ; Bassa_Vah + 0x16AEE, # .. 0x16AEF ; Unknown + 0x16AF0, # .. 0x16AF5 ; Bassa_Vah + 0x16AF6, # .. 0x16AFF ; Unknown + 0x16B00, # .. 0x16B45 ; Pahawh_Hmong + 0x16B46, # .. 0x16B4F ; Unknown + 0x16B50, # .. 0x16B59 ; Pahawh_Hmong + 0x16B5A, # .. 0x16B5A ; Unknown + 0x16B5B, # .. 0x16B61 ; Pahawh_Hmong + 0x16B62, # .. 0x16B62 ; Unknown + 0x16B63, # .. 0x16B77 ; Pahawh_Hmong + 0x16B78, # .. 0x16B7C ; Unknown + 0x16B7D, # .. 0x16B8F ; Pahawh_Hmong + 0x16B90, # .. 0x16EFF ; Unknown + 0x16F00, # .. 0x16F44 ; Miao + 0x16F45, # .. 0x16F4F ; Unknown + 0x16F50, # .. 0x16F7E ; Miao + 0x16F7F, # .. 0x16F8E ; Unknown + 0x16F8F, # .. 0x16F9F ; Miao + 0x16FA0, # .. 0x16FDF ; Unknown + 0x16FE0, # .. 0x16FE0 ; Tangut + 0x16FE1, # .. 0x16FE1 ; Nushu + 0x16FE2, # .. 0x16FFF ; Unknown + 0x17000, # .. 0x187EC ; Tangut + 0x187ED, # .. 0x187FF ; Unknown + 0x18800, # .. 0x18AF2 ; Tangut + 0x18AF3, # .. 0x1AFFF ; Unknown + 0x1B000, # .. 0x1B000 ; Katakana + 0x1B001, # .. 0x1B11E ; Hiragana + 0x1B11F, # .. 0x1B16F ; Unknown + 0x1B170, # .. 0x1B2FB ; Nushu + 0x1B2FC, # .. 0x1BBFF ; Unknown + 0x1BC00, # .. 0x1BC6A ; Duployan + 0x1BC6B, # .. 0x1BC6F ; Unknown + 0x1BC70, # .. 0x1BC7C ; Duployan + 0x1BC7D, # .. 0x1BC7F ; Unknown + 0x1BC80, # .. 0x1BC88 ; Duployan + 0x1BC89, # .. 0x1BC8F ; Unknown + 0x1BC90, # .. 0x1BC99 ; Duployan + 0x1BC9A, # .. 0x1BC9B ; Unknown + 0x1BC9C, # .. 0x1BC9F ; Duployan + 0x1BCA0, # .. 0x1BCA3 ; Common + 0x1BCA4, # .. 0x1CFFF ; Unknown + 0x1D000, # .. 0x1D0F5 ; Common + 0x1D0F6, # .. 0x1D0FF ; Unknown + 0x1D100, # .. 0x1D126 ; Common + 0x1D127, # .. 0x1D128 ; Unknown + 0x1D129, # .. 0x1D166 ; Common + 0x1D167, # .. 0x1D169 ; Inherited + 0x1D16A, # .. 0x1D17A ; Common + 0x1D17B, # .. 0x1D182 ; Inherited + 0x1D183, # .. 0x1D184 ; Common + 0x1D185, # .. 0x1D18B ; Inherited + 0x1D18C, # .. 0x1D1A9 ; Common + 0x1D1AA, # .. 0x1D1AD ; Inherited + 0x1D1AE, # .. 0x1D1E8 ; Common + 0x1D1E9, # .. 0x1D1FF ; Unknown + 0x1D200, # .. 0x1D245 ; Greek + 0x1D246, # .. 0x1D2FF ; Unknown + 0x1D300, # .. 0x1D356 ; Common + 0x1D357, # .. 0x1D35F ; Unknown + 0x1D360, # .. 0x1D371 ; Common + 0x1D372, # .. 0x1D3FF ; Unknown + 0x1D400, # .. 0x1D454 ; Common + 0x1D455, # .. 0x1D455 ; Unknown + 0x1D456, # .. 0x1D49C ; Common + 0x1D49D, # .. 0x1D49D ; Unknown + 0x1D49E, # .. 0x1D49F ; Common + 0x1D4A0, # .. 0x1D4A1 ; Unknown + 0x1D4A2, # .. 0x1D4A2 ; Common + 0x1D4A3, # .. 0x1D4A4 ; Unknown + 0x1D4A5, # .. 0x1D4A6 ; Common + 0x1D4A7, # .. 0x1D4A8 ; Unknown + 0x1D4A9, # .. 0x1D4AC ; Common + 0x1D4AD, # .. 0x1D4AD ; Unknown + 0x1D4AE, # .. 0x1D4B9 ; Common + 0x1D4BA, # .. 0x1D4BA ; Unknown + 0x1D4BB, # .. 0x1D4BB ; Common + 0x1D4BC, # .. 0x1D4BC ; Unknown + 0x1D4BD, # .. 0x1D4C3 ; Common + 0x1D4C4, # .. 0x1D4C4 ; Unknown + 0x1D4C5, # .. 0x1D505 ; Common + 0x1D506, # .. 0x1D506 ; Unknown + 0x1D507, # .. 0x1D50A ; Common + 0x1D50B, # .. 0x1D50C ; Unknown + 0x1D50D, # .. 0x1D514 ; Common + 0x1D515, # .. 0x1D515 ; Unknown + 0x1D516, # .. 0x1D51C ; Common + 0x1D51D, # .. 0x1D51D ; Unknown + 0x1D51E, # .. 0x1D539 ; Common + 0x1D53A, # .. 0x1D53A ; Unknown + 0x1D53B, # .. 0x1D53E ; Common + 0x1D53F, # .. 0x1D53F ; Unknown + 0x1D540, # .. 0x1D544 ; Common + 0x1D545, # .. 0x1D545 ; Unknown + 0x1D546, # .. 0x1D546 ; Common + 0x1D547, # .. 0x1D549 ; Unknown + 0x1D54A, # .. 0x1D550 ; Common + 0x1D551, # .. 0x1D551 ; Unknown + 0x1D552, # .. 0x1D6A5 ; Common + 0x1D6A6, # .. 0x1D6A7 ; Unknown + 0x1D6A8, # .. 0x1D7CB ; Common + 0x1D7CC, # .. 0x1D7CD ; Unknown + 0x1D7CE, # .. 0x1D7FF ; Common + 0x1D800, # .. 0x1DA8B ; SignWriting + 0x1DA8C, # .. 0x1DA9A ; Unknown + 0x1DA9B, # .. 0x1DA9F ; SignWriting + 0x1DAA0, # .. 0x1DAA0 ; Unknown + 0x1DAA1, # .. 0x1DAAF ; SignWriting + 0x1DAB0, # .. 0x1DFFF ; Unknown + 0x1E000, # .. 0x1E006 ; Glagolitic + 0x1E007, # .. 0x1E007 ; Unknown + 0x1E008, # .. 0x1E018 ; Glagolitic + 0x1E019, # .. 0x1E01A ; Unknown + 0x1E01B, # .. 0x1E021 ; Glagolitic + 0x1E022, # .. 0x1E022 ; Unknown + 0x1E023, # .. 0x1E024 ; Glagolitic + 0x1E025, # .. 0x1E025 ; Unknown + 0x1E026, # .. 0x1E02A ; Glagolitic + 0x1E02B, # .. 0x1E7FF ; Unknown + 0x1E800, # .. 0x1E8C4 ; Mende_Kikakui + 0x1E8C5, # .. 0x1E8C6 ; Unknown + 0x1E8C7, # .. 0x1E8D6 ; Mende_Kikakui + 0x1E8D7, # .. 0x1E8FF ; Unknown + 0x1E900, # .. 0x1E94A ; Adlam + 0x1E94B, # .. 0x1E94F ; Unknown + 0x1E950, # .. 0x1E959 ; Adlam + 0x1E95A, # .. 0x1E95D ; Unknown + 0x1E95E, # .. 0x1E95F ; Adlam + 0x1E960, # .. 0x1EDFF ; Unknown + 0x1EE00, # .. 0x1EE03 ; Arabic + 0x1EE04, # .. 0x1EE04 ; Unknown + 0x1EE05, # .. 0x1EE1F ; Arabic + 0x1EE20, # .. 0x1EE20 ; Unknown + 0x1EE21, # .. 0x1EE22 ; Arabic + 0x1EE23, # .. 0x1EE23 ; Unknown + 0x1EE24, # .. 0x1EE24 ; Arabic + 0x1EE25, # .. 0x1EE26 ; Unknown + 0x1EE27, # .. 0x1EE27 ; Arabic + 0x1EE28, # .. 0x1EE28 ; Unknown + 0x1EE29, # .. 0x1EE32 ; Arabic + 0x1EE33, # .. 0x1EE33 ; Unknown + 0x1EE34, # .. 0x1EE37 ; Arabic + 0x1EE38, # .. 0x1EE38 ; Unknown + 0x1EE39, # .. 0x1EE39 ; Arabic + 0x1EE3A, # .. 0x1EE3A ; Unknown + 0x1EE3B, # .. 0x1EE3B ; Arabic + 0x1EE3C, # .. 0x1EE41 ; Unknown + 0x1EE42, # .. 0x1EE42 ; Arabic + 0x1EE43, # .. 0x1EE46 ; Unknown + 0x1EE47, # .. 0x1EE47 ; Arabic + 0x1EE48, # .. 0x1EE48 ; Unknown + 0x1EE49, # .. 0x1EE49 ; Arabic + 0x1EE4A, # .. 0x1EE4A ; Unknown + 0x1EE4B, # .. 0x1EE4B ; Arabic + 0x1EE4C, # .. 0x1EE4C ; Unknown + 0x1EE4D, # .. 0x1EE4F ; Arabic + 0x1EE50, # .. 0x1EE50 ; Unknown + 0x1EE51, # .. 0x1EE52 ; Arabic + 0x1EE53, # .. 0x1EE53 ; Unknown + 0x1EE54, # .. 0x1EE54 ; Arabic + 0x1EE55, # .. 0x1EE56 ; Unknown + 0x1EE57, # .. 0x1EE57 ; Arabic + 0x1EE58, # .. 0x1EE58 ; Unknown + 0x1EE59, # .. 0x1EE59 ; Arabic + 0x1EE5A, # .. 0x1EE5A ; Unknown + 0x1EE5B, # .. 0x1EE5B ; Arabic + 0x1EE5C, # .. 0x1EE5C ; Unknown + 0x1EE5D, # .. 0x1EE5D ; Arabic + 0x1EE5E, # .. 0x1EE5E ; Unknown + 0x1EE5F, # .. 0x1EE5F ; Arabic + 0x1EE60, # .. 0x1EE60 ; Unknown + 0x1EE61, # .. 0x1EE62 ; Arabic + 0x1EE63, # .. 0x1EE63 ; Unknown + 0x1EE64, # .. 0x1EE64 ; Arabic + 0x1EE65, # .. 0x1EE66 ; Unknown + 0x1EE67, # .. 0x1EE6A ; Arabic + 0x1EE6B, # .. 0x1EE6B ; Unknown + 0x1EE6C, # .. 0x1EE72 ; Arabic + 0x1EE73, # .. 0x1EE73 ; Unknown + 0x1EE74, # .. 0x1EE77 ; Arabic + 0x1EE78, # .. 0x1EE78 ; Unknown + 0x1EE79, # .. 0x1EE7C ; Arabic + 0x1EE7D, # .. 0x1EE7D ; Unknown + 0x1EE7E, # .. 0x1EE7E ; Arabic + 0x1EE7F, # .. 0x1EE7F ; Unknown + 0x1EE80, # .. 0x1EE89 ; Arabic + 0x1EE8A, # .. 0x1EE8A ; Unknown + 0x1EE8B, # .. 0x1EE9B ; Arabic + 0x1EE9C, # .. 0x1EEA0 ; Unknown + 0x1EEA1, # .. 0x1EEA3 ; Arabic + 0x1EEA4, # .. 0x1EEA4 ; Unknown + 0x1EEA5, # .. 0x1EEA9 ; Arabic + 0x1EEAA, # .. 0x1EEAA ; Unknown + 0x1EEAB, # .. 0x1EEBB ; Arabic + 0x1EEBC, # .. 0x1EEEF ; Unknown + 0x1EEF0, # .. 0x1EEF1 ; Arabic + 0x1EEF2, # .. 0x1EFFF ; Unknown + 0x1F000, # .. 0x1F02B ; Common + 0x1F02C, # .. 0x1F02F ; Unknown + 0x1F030, # .. 0x1F093 ; Common + 0x1F094, # .. 0x1F09F ; Unknown + 0x1F0A0, # .. 0x1F0AE ; Common + 0x1F0AF, # .. 0x1F0B0 ; Unknown + 0x1F0B1, # .. 0x1F0BF ; Common + 0x1F0C0, # .. 0x1F0C0 ; Unknown + 0x1F0C1, # .. 0x1F0CF ; Common + 0x1F0D0, # .. 0x1F0D0 ; Unknown + 0x1F0D1, # .. 0x1F0F5 ; Common + 0x1F0F6, # .. 0x1F0FF ; Unknown + 0x1F100, # .. 0x1F10C ; Common + 0x1F10D, # .. 0x1F10F ; Unknown + 0x1F110, # .. 0x1F12E ; Common + 0x1F12F, # .. 0x1F12F ; Unknown + 0x1F130, # .. 0x1F16B ; Common + 0x1F16C, # .. 0x1F16F ; Unknown + 0x1F170, # .. 0x1F1AC ; Common + 0x1F1AD, # .. 0x1F1E5 ; Unknown + 0x1F1E6, # .. 0x1F1FF ; Common + 0x1F200, # .. 0x1F200 ; Hiragana + 0x1F201, # .. 0x1F202 ; Common + 0x1F203, # .. 0x1F20F ; Unknown + 0x1F210, # .. 0x1F23B ; Common + 0x1F23C, # .. 0x1F23F ; Unknown + 0x1F240, # .. 0x1F248 ; Common + 0x1F249, # .. 0x1F24F ; Unknown + 0x1F250, # .. 0x1F251 ; Common + 0x1F252, # .. 0x1F25F ; Unknown + 0x1F260, # .. 0x1F265 ; Common + 0x1F266, # .. 0x1F2FF ; Unknown + 0x1F300, # .. 0x1F6D4 ; Common + 0x1F6D5, # .. 0x1F6DF ; Unknown + 0x1F6E0, # .. 0x1F6EC ; Common + 0x1F6ED, # .. 0x1F6EF ; Unknown + 0x1F6F0, # .. 0x1F6F8 ; Common + 0x1F6F9, # .. 0x1F6FF ; Unknown + 0x1F700, # .. 0x1F773 ; Common + 0x1F774, # .. 0x1F77F ; Unknown + 0x1F780, # .. 0x1F7D4 ; Common + 0x1F7D5, # .. 0x1F7FF ; Unknown + 0x1F800, # .. 0x1F80B ; Common + 0x1F80C, # .. 0x1F80F ; Unknown + 0x1F810, # .. 0x1F847 ; Common + 0x1F848, # .. 0x1F84F ; Unknown + 0x1F850, # .. 0x1F859 ; Common + 0x1F85A, # .. 0x1F85F ; Unknown + 0x1F860, # .. 0x1F887 ; Common + 0x1F888, # .. 0x1F88F ; Unknown + 0x1F890, # .. 0x1F8AD ; Common + 0x1F8AE, # .. 0x1F8FF ; Unknown + 0x1F900, # .. 0x1F90B ; Common + 0x1F90C, # .. 0x1F90F ; Unknown + 0x1F910, # .. 0x1F93E ; Common + 0x1F93F, # .. 0x1F93F ; Unknown + 0x1F940, # .. 0x1F94C ; Common + 0x1F94D, # .. 0x1F94F ; Unknown + 0x1F950, # .. 0x1F96B ; Common + 0x1F96C, # .. 0x1F97F ; Unknown + 0x1F980, # .. 0x1F997 ; Common + 0x1F998, # .. 0x1F9BF ; Unknown + 0x1F9C0, # .. 0x1F9C0 ; Common + 0x1F9C1, # .. 0x1F9CF ; Unknown + 0x1F9D0, # .. 0x1F9E6 ; Common + 0x1F9E7, # .. 0x1FFFF ; Unknown + 0x20000, # .. 0x2A6D6 ; Han + 0x2A6D7, # .. 0x2A6FF ; Unknown + 0x2A700, # .. 0x2B734 ; Han + 0x2B735, # .. 0x2B73F ; Unknown + 0x2B740, # .. 0x2B81D ; Han + 0x2B81E, # .. 0x2B81F ; Unknown + 0x2B820, # .. 0x2CEA1 ; Han + 0x2CEA2, # .. 0x2CEAF ; Unknown + 0x2CEB0, # .. 0x2EBE0 ; Han + 0x2EBE1, # .. 0x2F7FF ; Unknown + 0x2F800, # .. 0x2FA1D ; Han + 0x2FA1E, # .. 0xE0000 ; Unknown + 0xE0001, # .. 0xE0001 ; Common + 0xE0002, # .. 0xE001F ; Unknown + 0xE0020, # .. 0xE007F ; Common + 0xE0080, # .. 0xE00FF ; Unknown + 0xE0100, # .. 0xE01EF ; Inherited + 0xE01F0, # .. 0x10FFFF ; Unknown +] + +VALUES = [ + 'Zyyy', # 0000..0040 ; Common + 'Latn', # 0041..005A ; Latin + 'Zyyy', # 005B..0060 ; Common + 'Latn', # 0061..007A ; Latin + 'Zyyy', # 007B..00A9 ; Common + 'Latn', # 00AA..00AA ; Latin + 'Zyyy', # 00AB..00B9 ; Common + 'Latn', # 00BA..00BA ; Latin + 'Zyyy', # 00BB..00BF ; Common + 'Latn', # 00C0..00D6 ; Latin + 'Zyyy', # 00D7..00D7 ; Common + 'Latn', # 00D8..00F6 ; Latin + 'Zyyy', # 00F7..00F7 ; Common + 'Latn', # 00F8..02B8 ; Latin + 'Zyyy', # 02B9..02DF ; Common + 'Latn', # 02E0..02E4 ; Latin + 'Zyyy', # 02E5..02E9 ; Common + 'Bopo', # 02EA..02EB ; Bopomofo + 'Zyyy', # 02EC..02FF ; Common + 'Zinh', # 0300..036F ; Inherited + 'Grek', # 0370..0373 ; Greek + 'Zyyy', # 0374..0374 ; Common + 'Grek', # 0375..0377 ; Greek + 'Zzzz', # 0378..0379 ; Unknown + 'Grek', # 037A..037D ; Greek + 'Zyyy', # 037E..037E ; Common + 'Grek', # 037F..037F ; Greek + 'Zzzz', # 0380..0383 ; Unknown + 'Grek', # 0384..0384 ; Greek + 'Zyyy', # 0385..0385 ; Common + 'Grek', # 0386..0386 ; Greek + 'Zyyy', # 0387..0387 ; Common + 'Grek', # 0388..038A ; Greek + 'Zzzz', # 038B..038B ; Unknown + 'Grek', # 038C..038C ; Greek + 'Zzzz', # 038D..038D ; Unknown + 'Grek', # 038E..03A1 ; Greek + 'Zzzz', # 03A2..03A2 ; Unknown + 'Grek', # 03A3..03E1 ; Greek + 'Copt', # 03E2..03EF ; Coptic + 'Grek', # 03F0..03FF ; Greek + 'Cyrl', # 0400..0484 ; Cyrillic + 'Zinh', # 0485..0486 ; Inherited + 'Cyrl', # 0487..052F ; Cyrillic + 'Zzzz', # 0530..0530 ; Unknown + 'Armn', # 0531..0556 ; Armenian + 'Zzzz', # 0557..0558 ; Unknown + 'Armn', # 0559..055F ; Armenian + 'Zzzz', # 0560..0560 ; Unknown + 'Armn', # 0561..0587 ; Armenian + 'Zzzz', # 0588..0588 ; Unknown + 'Zyyy', # 0589..0589 ; Common + 'Armn', # 058A..058A ; Armenian + 'Zzzz', # 058B..058C ; Unknown + 'Armn', # 058D..058F ; Armenian + 'Zzzz', # 0590..0590 ; Unknown + 'Hebr', # 0591..05C7 ; Hebrew + 'Zzzz', # 05C8..05CF ; Unknown + 'Hebr', # 05D0..05EA ; Hebrew + 'Zzzz', # 05EB..05EF ; Unknown + 'Hebr', # 05F0..05F4 ; Hebrew + 'Zzzz', # 05F5..05FF ; Unknown + 'Arab', # 0600..0604 ; Arabic + 'Zyyy', # 0605..0605 ; Common + 'Arab', # 0606..060B ; Arabic + 'Zyyy', # 060C..060C ; Common + 'Arab', # 060D..061A ; Arabic + 'Zyyy', # 061B..061B ; Common + 'Arab', # 061C..061C ; Arabic + 'Zzzz', # 061D..061D ; Unknown + 'Arab', # 061E..061E ; Arabic + 'Zyyy', # 061F..061F ; Common + 'Arab', # 0620..063F ; Arabic + 'Zyyy', # 0640..0640 ; Common + 'Arab', # 0641..064A ; Arabic + 'Zinh', # 064B..0655 ; Inherited + 'Arab', # 0656..066F ; Arabic + 'Zinh', # 0670..0670 ; Inherited + 'Arab', # 0671..06DC ; Arabic + 'Zyyy', # 06DD..06DD ; Common + 'Arab', # 06DE..06FF ; Arabic + 'Syrc', # 0700..070D ; Syriac + 'Zzzz', # 070E..070E ; Unknown + 'Syrc', # 070F..074A ; Syriac + 'Zzzz', # 074B..074C ; Unknown + 'Syrc', # 074D..074F ; Syriac + 'Arab', # 0750..077F ; Arabic + 'Thaa', # 0780..07B1 ; Thaana + 'Zzzz', # 07B2..07BF ; Unknown + 'Nkoo', # 07C0..07FA ; Nko + 'Zzzz', # 07FB..07FF ; Unknown + 'Samr', # 0800..082D ; Samaritan + 'Zzzz', # 082E..082F ; Unknown + 'Samr', # 0830..083E ; Samaritan + 'Zzzz', # 083F..083F ; Unknown + 'Mand', # 0840..085B ; Mandaic + 'Zzzz', # 085C..085D ; Unknown + 'Mand', # 085E..085E ; Mandaic + 'Zzzz', # 085F..085F ; Unknown + 'Syrc', # 0860..086A ; Syriac + 'Zzzz', # 086B..089F ; Unknown + 'Arab', # 08A0..08B4 ; Arabic + 'Zzzz', # 08B5..08B5 ; Unknown + 'Arab', # 08B6..08BD ; Arabic + 'Zzzz', # 08BE..08D3 ; Unknown + 'Arab', # 08D4..08E1 ; Arabic + 'Zyyy', # 08E2..08E2 ; Common + 'Arab', # 08E3..08FF ; Arabic + 'Deva', # 0900..0950 ; Devanagari + 'Zinh', # 0951..0952 ; Inherited + 'Deva', # 0953..0963 ; Devanagari + 'Zyyy', # 0964..0965 ; Common + 'Deva', # 0966..097F ; Devanagari + 'Beng', # 0980..0983 ; Bengali + 'Zzzz', # 0984..0984 ; Unknown + 'Beng', # 0985..098C ; Bengali + 'Zzzz', # 098D..098E ; Unknown + 'Beng', # 098F..0990 ; Bengali + 'Zzzz', # 0991..0992 ; Unknown + 'Beng', # 0993..09A8 ; Bengali + 'Zzzz', # 09A9..09A9 ; Unknown + 'Beng', # 09AA..09B0 ; Bengali + 'Zzzz', # 09B1..09B1 ; Unknown + 'Beng', # 09B2..09B2 ; Bengali + 'Zzzz', # 09B3..09B5 ; Unknown + 'Beng', # 09B6..09B9 ; Bengali + 'Zzzz', # 09BA..09BB ; Unknown + 'Beng', # 09BC..09C4 ; Bengali + 'Zzzz', # 09C5..09C6 ; Unknown + 'Beng', # 09C7..09C8 ; Bengali + 'Zzzz', # 09C9..09CA ; Unknown + 'Beng', # 09CB..09CE ; Bengali + 'Zzzz', # 09CF..09D6 ; Unknown + 'Beng', # 09D7..09D7 ; Bengali + 'Zzzz', # 09D8..09DB ; Unknown + 'Beng', # 09DC..09DD ; Bengali + 'Zzzz', # 09DE..09DE ; Unknown + 'Beng', # 09DF..09E3 ; Bengali + 'Zzzz', # 09E4..09E5 ; Unknown + 'Beng', # 09E6..09FD ; Bengali + 'Zzzz', # 09FE..0A00 ; Unknown + 'Guru', # 0A01..0A03 ; Gurmukhi + 'Zzzz', # 0A04..0A04 ; Unknown + 'Guru', # 0A05..0A0A ; Gurmukhi + 'Zzzz', # 0A0B..0A0E ; Unknown + 'Guru', # 0A0F..0A10 ; Gurmukhi + 'Zzzz', # 0A11..0A12 ; Unknown + 'Guru', # 0A13..0A28 ; Gurmukhi + 'Zzzz', # 0A29..0A29 ; Unknown + 'Guru', # 0A2A..0A30 ; Gurmukhi + 'Zzzz', # 0A31..0A31 ; Unknown + 'Guru', # 0A32..0A33 ; Gurmukhi + 'Zzzz', # 0A34..0A34 ; Unknown + 'Guru', # 0A35..0A36 ; Gurmukhi + 'Zzzz', # 0A37..0A37 ; Unknown + 'Guru', # 0A38..0A39 ; Gurmukhi + 'Zzzz', # 0A3A..0A3B ; Unknown + 'Guru', # 0A3C..0A3C ; Gurmukhi + 'Zzzz', # 0A3D..0A3D ; Unknown + 'Guru', # 0A3E..0A42 ; Gurmukhi + 'Zzzz', # 0A43..0A46 ; Unknown + 'Guru', # 0A47..0A48 ; Gurmukhi + 'Zzzz', # 0A49..0A4A ; Unknown + 'Guru', # 0A4B..0A4D ; Gurmukhi + 'Zzzz', # 0A4E..0A50 ; Unknown + 'Guru', # 0A51..0A51 ; Gurmukhi + 'Zzzz', # 0A52..0A58 ; Unknown + 'Guru', # 0A59..0A5C ; Gurmukhi + 'Zzzz', # 0A5D..0A5D ; Unknown + 'Guru', # 0A5E..0A5E ; Gurmukhi + 'Zzzz', # 0A5F..0A65 ; Unknown + 'Guru', # 0A66..0A75 ; Gurmukhi + 'Zzzz', # 0A76..0A80 ; Unknown + 'Gujr', # 0A81..0A83 ; Gujarati + 'Zzzz', # 0A84..0A84 ; Unknown + 'Gujr', # 0A85..0A8D ; Gujarati + 'Zzzz', # 0A8E..0A8E ; Unknown + 'Gujr', # 0A8F..0A91 ; Gujarati + 'Zzzz', # 0A92..0A92 ; Unknown + 'Gujr', # 0A93..0AA8 ; Gujarati + 'Zzzz', # 0AA9..0AA9 ; Unknown + 'Gujr', # 0AAA..0AB0 ; Gujarati + 'Zzzz', # 0AB1..0AB1 ; Unknown + 'Gujr', # 0AB2..0AB3 ; Gujarati + 'Zzzz', # 0AB4..0AB4 ; Unknown + 'Gujr', # 0AB5..0AB9 ; Gujarati + 'Zzzz', # 0ABA..0ABB ; Unknown + 'Gujr', # 0ABC..0AC5 ; Gujarati + 'Zzzz', # 0AC6..0AC6 ; Unknown + 'Gujr', # 0AC7..0AC9 ; Gujarati + 'Zzzz', # 0ACA..0ACA ; Unknown + 'Gujr', # 0ACB..0ACD ; Gujarati + 'Zzzz', # 0ACE..0ACF ; Unknown + 'Gujr', # 0AD0..0AD0 ; Gujarati + 'Zzzz', # 0AD1..0ADF ; Unknown + 'Gujr', # 0AE0..0AE3 ; Gujarati + 'Zzzz', # 0AE4..0AE5 ; Unknown + 'Gujr', # 0AE6..0AF1 ; Gujarati + 'Zzzz', # 0AF2..0AF8 ; Unknown + 'Gujr', # 0AF9..0AFF ; Gujarati + 'Zzzz', # 0B00..0B00 ; Unknown + 'Orya', # 0B01..0B03 ; Oriya + 'Zzzz', # 0B04..0B04 ; Unknown + 'Orya', # 0B05..0B0C ; Oriya + 'Zzzz', # 0B0D..0B0E ; Unknown + 'Orya', # 0B0F..0B10 ; Oriya + 'Zzzz', # 0B11..0B12 ; Unknown + 'Orya', # 0B13..0B28 ; Oriya + 'Zzzz', # 0B29..0B29 ; Unknown + 'Orya', # 0B2A..0B30 ; Oriya + 'Zzzz', # 0B31..0B31 ; Unknown + 'Orya', # 0B32..0B33 ; Oriya + 'Zzzz', # 0B34..0B34 ; Unknown + 'Orya', # 0B35..0B39 ; Oriya + 'Zzzz', # 0B3A..0B3B ; Unknown + 'Orya', # 0B3C..0B44 ; Oriya + 'Zzzz', # 0B45..0B46 ; Unknown + 'Orya', # 0B47..0B48 ; Oriya + 'Zzzz', # 0B49..0B4A ; Unknown + 'Orya', # 0B4B..0B4D ; Oriya + 'Zzzz', # 0B4E..0B55 ; Unknown + 'Orya', # 0B56..0B57 ; Oriya + 'Zzzz', # 0B58..0B5B ; Unknown + 'Orya', # 0B5C..0B5D ; Oriya + 'Zzzz', # 0B5E..0B5E ; Unknown + 'Orya', # 0B5F..0B63 ; Oriya + 'Zzzz', # 0B64..0B65 ; Unknown + 'Orya', # 0B66..0B77 ; Oriya + 'Zzzz', # 0B78..0B81 ; Unknown + 'Taml', # 0B82..0B83 ; Tamil + 'Zzzz', # 0B84..0B84 ; Unknown + 'Taml', # 0B85..0B8A ; Tamil + 'Zzzz', # 0B8B..0B8D ; Unknown + 'Taml', # 0B8E..0B90 ; Tamil + 'Zzzz', # 0B91..0B91 ; Unknown + 'Taml', # 0B92..0B95 ; Tamil + 'Zzzz', # 0B96..0B98 ; Unknown + 'Taml', # 0B99..0B9A ; Tamil + 'Zzzz', # 0B9B..0B9B ; Unknown + 'Taml', # 0B9C..0B9C ; Tamil + 'Zzzz', # 0B9D..0B9D ; Unknown + 'Taml', # 0B9E..0B9F ; Tamil + 'Zzzz', # 0BA0..0BA2 ; Unknown + 'Taml', # 0BA3..0BA4 ; Tamil + 'Zzzz', # 0BA5..0BA7 ; Unknown + 'Taml', # 0BA8..0BAA ; Tamil + 'Zzzz', # 0BAB..0BAD ; Unknown + 'Taml', # 0BAE..0BB9 ; Tamil + 'Zzzz', # 0BBA..0BBD ; Unknown + 'Taml', # 0BBE..0BC2 ; Tamil + 'Zzzz', # 0BC3..0BC5 ; Unknown + 'Taml', # 0BC6..0BC8 ; Tamil + 'Zzzz', # 0BC9..0BC9 ; Unknown + 'Taml', # 0BCA..0BCD ; Tamil + 'Zzzz', # 0BCE..0BCF ; Unknown + 'Taml', # 0BD0..0BD0 ; Tamil + 'Zzzz', # 0BD1..0BD6 ; Unknown + 'Taml', # 0BD7..0BD7 ; Tamil + 'Zzzz', # 0BD8..0BE5 ; Unknown + 'Taml', # 0BE6..0BFA ; Tamil + 'Zzzz', # 0BFB..0BFF ; Unknown + 'Telu', # 0C00..0C03 ; Telugu + 'Zzzz', # 0C04..0C04 ; Unknown + 'Telu', # 0C05..0C0C ; Telugu + 'Zzzz', # 0C0D..0C0D ; Unknown + 'Telu', # 0C0E..0C10 ; Telugu + 'Zzzz', # 0C11..0C11 ; Unknown + 'Telu', # 0C12..0C28 ; Telugu + 'Zzzz', # 0C29..0C29 ; Unknown + 'Telu', # 0C2A..0C39 ; Telugu + 'Zzzz', # 0C3A..0C3C ; Unknown + 'Telu', # 0C3D..0C44 ; Telugu + 'Zzzz', # 0C45..0C45 ; Unknown + 'Telu', # 0C46..0C48 ; Telugu + 'Zzzz', # 0C49..0C49 ; Unknown + 'Telu', # 0C4A..0C4D ; Telugu + 'Zzzz', # 0C4E..0C54 ; Unknown + 'Telu', # 0C55..0C56 ; Telugu + 'Zzzz', # 0C57..0C57 ; Unknown + 'Telu', # 0C58..0C5A ; Telugu + 'Zzzz', # 0C5B..0C5F ; Unknown + 'Telu', # 0C60..0C63 ; Telugu + 'Zzzz', # 0C64..0C65 ; Unknown + 'Telu', # 0C66..0C6F ; Telugu + 'Zzzz', # 0C70..0C77 ; Unknown + 'Telu', # 0C78..0C7F ; Telugu + 'Knda', # 0C80..0C83 ; Kannada + 'Zzzz', # 0C84..0C84 ; Unknown + 'Knda', # 0C85..0C8C ; Kannada + 'Zzzz', # 0C8D..0C8D ; Unknown + 'Knda', # 0C8E..0C90 ; Kannada + 'Zzzz', # 0C91..0C91 ; Unknown + 'Knda', # 0C92..0CA8 ; Kannada + 'Zzzz', # 0CA9..0CA9 ; Unknown + 'Knda', # 0CAA..0CB3 ; Kannada + 'Zzzz', # 0CB4..0CB4 ; Unknown + 'Knda', # 0CB5..0CB9 ; Kannada + 'Zzzz', # 0CBA..0CBB ; Unknown + 'Knda', # 0CBC..0CC4 ; Kannada + 'Zzzz', # 0CC5..0CC5 ; Unknown + 'Knda', # 0CC6..0CC8 ; Kannada + 'Zzzz', # 0CC9..0CC9 ; Unknown + 'Knda', # 0CCA..0CCD ; Kannada + 'Zzzz', # 0CCE..0CD4 ; Unknown + 'Knda', # 0CD5..0CD6 ; Kannada + 'Zzzz', # 0CD7..0CDD ; Unknown + 'Knda', # 0CDE..0CDE ; Kannada + 'Zzzz', # 0CDF..0CDF ; Unknown + 'Knda', # 0CE0..0CE3 ; Kannada + 'Zzzz', # 0CE4..0CE5 ; Unknown + 'Knda', # 0CE6..0CEF ; Kannada + 'Zzzz', # 0CF0..0CF0 ; Unknown + 'Knda', # 0CF1..0CF2 ; Kannada + 'Zzzz', # 0CF3..0CFF ; Unknown + 'Mlym', # 0D00..0D03 ; Malayalam + 'Zzzz', # 0D04..0D04 ; Unknown + 'Mlym', # 0D05..0D0C ; Malayalam + 'Zzzz', # 0D0D..0D0D ; Unknown + 'Mlym', # 0D0E..0D10 ; Malayalam + 'Zzzz', # 0D11..0D11 ; Unknown + 'Mlym', # 0D12..0D44 ; Malayalam + 'Zzzz', # 0D45..0D45 ; Unknown + 'Mlym', # 0D46..0D48 ; Malayalam + 'Zzzz', # 0D49..0D49 ; Unknown + 'Mlym', # 0D4A..0D4F ; Malayalam + 'Zzzz', # 0D50..0D53 ; Unknown + 'Mlym', # 0D54..0D63 ; Malayalam + 'Zzzz', # 0D64..0D65 ; Unknown + 'Mlym', # 0D66..0D7F ; Malayalam + 'Zzzz', # 0D80..0D81 ; Unknown + 'Sinh', # 0D82..0D83 ; Sinhala + 'Zzzz', # 0D84..0D84 ; Unknown + 'Sinh', # 0D85..0D96 ; Sinhala + 'Zzzz', # 0D97..0D99 ; Unknown + 'Sinh', # 0D9A..0DB1 ; Sinhala + 'Zzzz', # 0DB2..0DB2 ; Unknown + 'Sinh', # 0DB3..0DBB ; Sinhala + 'Zzzz', # 0DBC..0DBC ; Unknown + 'Sinh', # 0DBD..0DBD ; Sinhala + 'Zzzz', # 0DBE..0DBF ; Unknown + 'Sinh', # 0DC0..0DC6 ; Sinhala + 'Zzzz', # 0DC7..0DC9 ; Unknown + 'Sinh', # 0DCA..0DCA ; Sinhala + 'Zzzz', # 0DCB..0DCE ; Unknown + 'Sinh', # 0DCF..0DD4 ; Sinhala + 'Zzzz', # 0DD5..0DD5 ; Unknown + 'Sinh', # 0DD6..0DD6 ; Sinhala + 'Zzzz', # 0DD7..0DD7 ; Unknown + 'Sinh', # 0DD8..0DDF ; Sinhala + 'Zzzz', # 0DE0..0DE5 ; Unknown + 'Sinh', # 0DE6..0DEF ; Sinhala + 'Zzzz', # 0DF0..0DF1 ; Unknown + 'Sinh', # 0DF2..0DF4 ; Sinhala + 'Zzzz', # 0DF5..0E00 ; Unknown + 'Thai', # 0E01..0E3A ; Thai + 'Zzzz', # 0E3B..0E3E ; Unknown + 'Zyyy', # 0E3F..0E3F ; Common + 'Thai', # 0E40..0E5B ; Thai + 'Zzzz', # 0E5C..0E80 ; Unknown + 'Laoo', # 0E81..0E82 ; Lao + 'Zzzz', # 0E83..0E83 ; Unknown + 'Laoo', # 0E84..0E84 ; Lao + 'Zzzz', # 0E85..0E86 ; Unknown + 'Laoo', # 0E87..0E88 ; Lao + 'Zzzz', # 0E89..0E89 ; Unknown + 'Laoo', # 0E8A..0E8A ; Lao + 'Zzzz', # 0E8B..0E8C ; Unknown + 'Laoo', # 0E8D..0E8D ; Lao + 'Zzzz', # 0E8E..0E93 ; Unknown + 'Laoo', # 0E94..0E97 ; Lao + 'Zzzz', # 0E98..0E98 ; Unknown + 'Laoo', # 0E99..0E9F ; Lao + 'Zzzz', # 0EA0..0EA0 ; Unknown + 'Laoo', # 0EA1..0EA3 ; Lao + 'Zzzz', # 0EA4..0EA4 ; Unknown + 'Laoo', # 0EA5..0EA5 ; Lao + 'Zzzz', # 0EA6..0EA6 ; Unknown + 'Laoo', # 0EA7..0EA7 ; Lao + 'Zzzz', # 0EA8..0EA9 ; Unknown + 'Laoo', # 0EAA..0EAB ; Lao + 'Zzzz', # 0EAC..0EAC ; Unknown + 'Laoo', # 0EAD..0EB9 ; Lao + 'Zzzz', # 0EBA..0EBA ; Unknown + 'Laoo', # 0EBB..0EBD ; Lao + 'Zzzz', # 0EBE..0EBF ; Unknown + 'Laoo', # 0EC0..0EC4 ; Lao + 'Zzzz', # 0EC5..0EC5 ; Unknown + 'Laoo', # 0EC6..0EC6 ; Lao + 'Zzzz', # 0EC7..0EC7 ; Unknown + 'Laoo', # 0EC8..0ECD ; Lao + 'Zzzz', # 0ECE..0ECF ; Unknown + 'Laoo', # 0ED0..0ED9 ; Lao + 'Zzzz', # 0EDA..0EDB ; Unknown + 'Laoo', # 0EDC..0EDF ; Lao + 'Zzzz', # 0EE0..0EFF ; Unknown + 'Tibt', # 0F00..0F47 ; Tibetan + 'Zzzz', # 0F48..0F48 ; Unknown + 'Tibt', # 0F49..0F6C ; Tibetan + 'Zzzz', # 0F6D..0F70 ; Unknown + 'Tibt', # 0F71..0F97 ; Tibetan + 'Zzzz', # 0F98..0F98 ; Unknown + 'Tibt', # 0F99..0FBC ; Tibetan + 'Zzzz', # 0FBD..0FBD ; Unknown + 'Tibt', # 0FBE..0FCC ; Tibetan + 'Zzzz', # 0FCD..0FCD ; Unknown + 'Tibt', # 0FCE..0FD4 ; Tibetan + 'Zyyy', # 0FD5..0FD8 ; Common + 'Tibt', # 0FD9..0FDA ; Tibetan + 'Zzzz', # 0FDB..0FFF ; Unknown + 'Mymr', # 1000..109F ; Myanmar + 'Geor', # 10A0..10C5 ; Georgian + 'Zzzz', # 10C6..10C6 ; Unknown + 'Geor', # 10C7..10C7 ; Georgian + 'Zzzz', # 10C8..10CC ; Unknown + 'Geor', # 10CD..10CD ; Georgian + 'Zzzz', # 10CE..10CF ; Unknown + 'Geor', # 10D0..10FA ; Georgian + 'Zyyy', # 10FB..10FB ; Common + 'Geor', # 10FC..10FF ; Georgian + 'Hang', # 1100..11FF ; Hangul + 'Ethi', # 1200..1248 ; Ethiopic + 'Zzzz', # 1249..1249 ; Unknown + 'Ethi', # 124A..124D ; Ethiopic + 'Zzzz', # 124E..124F ; Unknown + 'Ethi', # 1250..1256 ; Ethiopic + 'Zzzz', # 1257..1257 ; Unknown + 'Ethi', # 1258..1258 ; Ethiopic + 'Zzzz', # 1259..1259 ; Unknown + 'Ethi', # 125A..125D ; Ethiopic + 'Zzzz', # 125E..125F ; Unknown + 'Ethi', # 1260..1288 ; Ethiopic + 'Zzzz', # 1289..1289 ; Unknown + 'Ethi', # 128A..128D ; Ethiopic + 'Zzzz', # 128E..128F ; Unknown + 'Ethi', # 1290..12B0 ; Ethiopic + 'Zzzz', # 12B1..12B1 ; Unknown + 'Ethi', # 12B2..12B5 ; Ethiopic + 'Zzzz', # 12B6..12B7 ; Unknown + 'Ethi', # 12B8..12BE ; Ethiopic + 'Zzzz', # 12BF..12BF ; Unknown + 'Ethi', # 12C0..12C0 ; Ethiopic + 'Zzzz', # 12C1..12C1 ; Unknown + 'Ethi', # 12C2..12C5 ; Ethiopic + 'Zzzz', # 12C6..12C7 ; Unknown + 'Ethi', # 12C8..12D6 ; Ethiopic + 'Zzzz', # 12D7..12D7 ; Unknown + 'Ethi', # 12D8..1310 ; Ethiopic + 'Zzzz', # 1311..1311 ; Unknown + 'Ethi', # 1312..1315 ; Ethiopic + 'Zzzz', # 1316..1317 ; Unknown + 'Ethi', # 1318..135A ; Ethiopic + 'Zzzz', # 135B..135C ; Unknown + 'Ethi', # 135D..137C ; Ethiopic + 'Zzzz', # 137D..137F ; Unknown + 'Ethi', # 1380..1399 ; Ethiopic + 'Zzzz', # 139A..139F ; Unknown + 'Cher', # 13A0..13F5 ; Cherokee + 'Zzzz', # 13F6..13F7 ; Unknown + 'Cher', # 13F8..13FD ; Cherokee + 'Zzzz', # 13FE..13FF ; Unknown + 'Cans', # 1400..167F ; Canadian_Aboriginal + 'Ogam', # 1680..169C ; Ogham + 'Zzzz', # 169D..169F ; Unknown + 'Runr', # 16A0..16EA ; Runic + 'Zyyy', # 16EB..16ED ; Common + 'Runr', # 16EE..16F8 ; Runic + 'Zzzz', # 16F9..16FF ; Unknown + 'Tglg', # 1700..170C ; Tagalog + 'Zzzz', # 170D..170D ; Unknown + 'Tglg', # 170E..1714 ; Tagalog + 'Zzzz', # 1715..171F ; Unknown + 'Hano', # 1720..1734 ; Hanunoo + 'Zyyy', # 1735..1736 ; Common + 'Zzzz', # 1737..173F ; Unknown + 'Buhd', # 1740..1753 ; Buhid + 'Zzzz', # 1754..175F ; Unknown + 'Tagb', # 1760..176C ; Tagbanwa + 'Zzzz', # 176D..176D ; Unknown + 'Tagb', # 176E..1770 ; Tagbanwa + 'Zzzz', # 1771..1771 ; Unknown + 'Tagb', # 1772..1773 ; Tagbanwa + 'Zzzz', # 1774..177F ; Unknown + 'Khmr', # 1780..17DD ; Khmer + 'Zzzz', # 17DE..17DF ; Unknown + 'Khmr', # 17E0..17E9 ; Khmer + 'Zzzz', # 17EA..17EF ; Unknown + 'Khmr', # 17F0..17F9 ; Khmer + 'Zzzz', # 17FA..17FF ; Unknown + 'Mong', # 1800..1801 ; Mongolian + 'Zyyy', # 1802..1803 ; Common + 'Mong', # 1804..1804 ; Mongolian + 'Zyyy', # 1805..1805 ; Common + 'Mong', # 1806..180E ; Mongolian + 'Zzzz', # 180F..180F ; Unknown + 'Mong', # 1810..1819 ; Mongolian + 'Zzzz', # 181A..181F ; Unknown + 'Mong', # 1820..1877 ; Mongolian + 'Zzzz', # 1878..187F ; Unknown + 'Mong', # 1880..18AA ; Mongolian + 'Zzzz', # 18AB..18AF ; Unknown + 'Cans', # 18B0..18F5 ; Canadian_Aboriginal + 'Zzzz', # 18F6..18FF ; Unknown + 'Limb', # 1900..191E ; Limbu + 'Zzzz', # 191F..191F ; Unknown + 'Limb', # 1920..192B ; Limbu + 'Zzzz', # 192C..192F ; Unknown + 'Limb', # 1930..193B ; Limbu + 'Zzzz', # 193C..193F ; Unknown + 'Limb', # 1940..1940 ; Limbu + 'Zzzz', # 1941..1943 ; Unknown + 'Limb', # 1944..194F ; Limbu + 'Tale', # 1950..196D ; Tai_Le + 'Zzzz', # 196E..196F ; Unknown + 'Tale', # 1970..1974 ; Tai_Le + 'Zzzz', # 1975..197F ; Unknown + 'Talu', # 1980..19AB ; New_Tai_Lue + 'Zzzz', # 19AC..19AF ; Unknown + 'Talu', # 19B0..19C9 ; New_Tai_Lue + 'Zzzz', # 19CA..19CF ; Unknown + 'Talu', # 19D0..19DA ; New_Tai_Lue + 'Zzzz', # 19DB..19DD ; Unknown + 'Talu', # 19DE..19DF ; New_Tai_Lue + 'Khmr', # 19E0..19FF ; Khmer + 'Bugi', # 1A00..1A1B ; Buginese + 'Zzzz', # 1A1C..1A1D ; Unknown + 'Bugi', # 1A1E..1A1F ; Buginese + 'Lana', # 1A20..1A5E ; Tai_Tham + 'Zzzz', # 1A5F..1A5F ; Unknown + 'Lana', # 1A60..1A7C ; Tai_Tham + 'Zzzz', # 1A7D..1A7E ; Unknown + 'Lana', # 1A7F..1A89 ; Tai_Tham + 'Zzzz', # 1A8A..1A8F ; Unknown + 'Lana', # 1A90..1A99 ; Tai_Tham + 'Zzzz', # 1A9A..1A9F ; Unknown + 'Lana', # 1AA0..1AAD ; Tai_Tham + 'Zzzz', # 1AAE..1AAF ; Unknown + 'Zinh', # 1AB0..1ABE ; Inherited + 'Zzzz', # 1ABF..1AFF ; Unknown + 'Bali', # 1B00..1B4B ; Balinese + 'Zzzz', # 1B4C..1B4F ; Unknown + 'Bali', # 1B50..1B7C ; Balinese + 'Zzzz', # 1B7D..1B7F ; Unknown + 'Sund', # 1B80..1BBF ; Sundanese + 'Batk', # 1BC0..1BF3 ; Batak + 'Zzzz', # 1BF4..1BFB ; Unknown + 'Batk', # 1BFC..1BFF ; Batak + 'Lepc', # 1C00..1C37 ; Lepcha + 'Zzzz', # 1C38..1C3A ; Unknown + 'Lepc', # 1C3B..1C49 ; Lepcha + 'Zzzz', # 1C4A..1C4C ; Unknown + 'Lepc', # 1C4D..1C4F ; Lepcha + 'Olck', # 1C50..1C7F ; Ol_Chiki + 'Cyrl', # 1C80..1C88 ; Cyrillic + 'Zzzz', # 1C89..1CBF ; Unknown + 'Sund', # 1CC0..1CC7 ; Sundanese + 'Zzzz', # 1CC8..1CCF ; Unknown + 'Zinh', # 1CD0..1CD2 ; Inherited + 'Zyyy', # 1CD3..1CD3 ; Common + 'Zinh', # 1CD4..1CE0 ; Inherited + 'Zyyy', # 1CE1..1CE1 ; Common + 'Zinh', # 1CE2..1CE8 ; Inherited + 'Zyyy', # 1CE9..1CEC ; Common + 'Zinh', # 1CED..1CED ; Inherited + 'Zyyy', # 1CEE..1CF3 ; Common + 'Zinh', # 1CF4..1CF4 ; Inherited + 'Zyyy', # 1CF5..1CF7 ; Common + 'Zinh', # 1CF8..1CF9 ; Inherited + 'Zzzz', # 1CFA..1CFF ; Unknown + 'Latn', # 1D00..1D25 ; Latin + 'Grek', # 1D26..1D2A ; Greek + 'Cyrl', # 1D2B..1D2B ; Cyrillic + 'Latn', # 1D2C..1D5C ; Latin + 'Grek', # 1D5D..1D61 ; Greek + 'Latn', # 1D62..1D65 ; Latin + 'Grek', # 1D66..1D6A ; Greek + 'Latn', # 1D6B..1D77 ; Latin + 'Cyrl', # 1D78..1D78 ; Cyrillic + 'Latn', # 1D79..1DBE ; Latin + 'Grek', # 1DBF..1DBF ; Greek + 'Zinh', # 1DC0..1DF9 ; Inherited + 'Zzzz', # 1DFA..1DFA ; Unknown + 'Zinh', # 1DFB..1DFF ; Inherited + 'Latn', # 1E00..1EFF ; Latin + 'Grek', # 1F00..1F15 ; Greek + 'Zzzz', # 1F16..1F17 ; Unknown + 'Grek', # 1F18..1F1D ; Greek + 'Zzzz', # 1F1E..1F1F ; Unknown + 'Grek', # 1F20..1F45 ; Greek + 'Zzzz', # 1F46..1F47 ; Unknown + 'Grek', # 1F48..1F4D ; Greek + 'Zzzz', # 1F4E..1F4F ; Unknown + 'Grek', # 1F50..1F57 ; Greek + 'Zzzz', # 1F58..1F58 ; Unknown + 'Grek', # 1F59..1F59 ; Greek + 'Zzzz', # 1F5A..1F5A ; Unknown + 'Grek', # 1F5B..1F5B ; Greek + 'Zzzz', # 1F5C..1F5C ; Unknown + 'Grek', # 1F5D..1F5D ; Greek + 'Zzzz', # 1F5E..1F5E ; Unknown + 'Grek', # 1F5F..1F7D ; Greek + 'Zzzz', # 1F7E..1F7F ; Unknown + 'Grek', # 1F80..1FB4 ; Greek + 'Zzzz', # 1FB5..1FB5 ; Unknown + 'Grek', # 1FB6..1FC4 ; Greek + 'Zzzz', # 1FC5..1FC5 ; Unknown + 'Grek', # 1FC6..1FD3 ; Greek + 'Zzzz', # 1FD4..1FD5 ; Unknown + 'Grek', # 1FD6..1FDB ; Greek + 'Zzzz', # 1FDC..1FDC ; Unknown + 'Grek', # 1FDD..1FEF ; Greek + 'Zzzz', # 1FF0..1FF1 ; Unknown + 'Grek', # 1FF2..1FF4 ; Greek + 'Zzzz', # 1FF5..1FF5 ; Unknown + 'Grek', # 1FF6..1FFE ; Greek + 'Zzzz', # 1FFF..1FFF ; Unknown + 'Zyyy', # 2000..200B ; Common + 'Zinh', # 200C..200D ; Inherited + 'Zyyy', # 200E..2064 ; Common + 'Zzzz', # 2065..2065 ; Unknown + 'Zyyy', # 2066..2070 ; Common + 'Latn', # 2071..2071 ; Latin + 'Zzzz', # 2072..2073 ; Unknown + 'Zyyy', # 2074..207E ; Common + 'Latn', # 207F..207F ; Latin + 'Zyyy', # 2080..208E ; Common + 'Zzzz', # 208F..208F ; Unknown + 'Latn', # 2090..209C ; Latin + 'Zzzz', # 209D..209F ; Unknown + 'Zyyy', # 20A0..20BF ; Common + 'Zzzz', # 20C0..20CF ; Unknown + 'Zinh', # 20D0..20F0 ; Inherited + 'Zzzz', # 20F1..20FF ; Unknown + 'Zyyy', # 2100..2125 ; Common + 'Grek', # 2126..2126 ; Greek + 'Zyyy', # 2127..2129 ; Common + 'Latn', # 212A..212B ; Latin + 'Zyyy', # 212C..2131 ; Common + 'Latn', # 2132..2132 ; Latin + 'Zyyy', # 2133..214D ; Common + 'Latn', # 214E..214E ; Latin + 'Zyyy', # 214F..215F ; Common + 'Latn', # 2160..2188 ; Latin + 'Zyyy', # 2189..218B ; Common + 'Zzzz', # 218C..218F ; Unknown + 'Zyyy', # 2190..2426 ; Common + 'Zzzz', # 2427..243F ; Unknown + 'Zyyy', # 2440..244A ; Common + 'Zzzz', # 244B..245F ; Unknown + 'Zyyy', # 2460..27FF ; Common + 'Brai', # 2800..28FF ; Braille + 'Zyyy', # 2900..2B73 ; Common + 'Zzzz', # 2B74..2B75 ; Unknown + 'Zyyy', # 2B76..2B95 ; Common + 'Zzzz', # 2B96..2B97 ; Unknown + 'Zyyy', # 2B98..2BB9 ; Common + 'Zzzz', # 2BBA..2BBC ; Unknown + 'Zyyy', # 2BBD..2BC8 ; Common + 'Zzzz', # 2BC9..2BC9 ; Unknown + 'Zyyy', # 2BCA..2BD2 ; Common + 'Zzzz', # 2BD3..2BEB ; Unknown + 'Zyyy', # 2BEC..2BEF ; Common + 'Zzzz', # 2BF0..2BFF ; Unknown + 'Glag', # 2C00..2C2E ; Glagolitic + 'Zzzz', # 2C2F..2C2F ; Unknown + 'Glag', # 2C30..2C5E ; Glagolitic + 'Zzzz', # 2C5F..2C5F ; Unknown + 'Latn', # 2C60..2C7F ; Latin + 'Copt', # 2C80..2CF3 ; Coptic + 'Zzzz', # 2CF4..2CF8 ; Unknown + 'Copt', # 2CF9..2CFF ; Coptic + 'Geor', # 2D00..2D25 ; Georgian + 'Zzzz', # 2D26..2D26 ; Unknown + 'Geor', # 2D27..2D27 ; Georgian + 'Zzzz', # 2D28..2D2C ; Unknown + 'Geor', # 2D2D..2D2D ; Georgian + 'Zzzz', # 2D2E..2D2F ; Unknown + 'Tfng', # 2D30..2D67 ; Tifinagh + 'Zzzz', # 2D68..2D6E ; Unknown + 'Tfng', # 2D6F..2D70 ; Tifinagh + 'Zzzz', # 2D71..2D7E ; Unknown + 'Tfng', # 2D7F..2D7F ; Tifinagh + 'Ethi', # 2D80..2D96 ; Ethiopic + 'Zzzz', # 2D97..2D9F ; Unknown + 'Ethi', # 2DA0..2DA6 ; Ethiopic + 'Zzzz', # 2DA7..2DA7 ; Unknown + 'Ethi', # 2DA8..2DAE ; Ethiopic + 'Zzzz', # 2DAF..2DAF ; Unknown + 'Ethi', # 2DB0..2DB6 ; Ethiopic + 'Zzzz', # 2DB7..2DB7 ; Unknown + 'Ethi', # 2DB8..2DBE ; Ethiopic + 'Zzzz', # 2DBF..2DBF ; Unknown + 'Ethi', # 2DC0..2DC6 ; Ethiopic + 'Zzzz', # 2DC7..2DC7 ; Unknown + 'Ethi', # 2DC8..2DCE ; Ethiopic + 'Zzzz', # 2DCF..2DCF ; Unknown + 'Ethi', # 2DD0..2DD6 ; Ethiopic + 'Zzzz', # 2DD7..2DD7 ; Unknown + 'Ethi', # 2DD8..2DDE ; Ethiopic + 'Zzzz', # 2DDF..2DDF ; Unknown + 'Cyrl', # 2DE0..2DFF ; Cyrillic + 'Zyyy', # 2E00..2E49 ; Common + 'Zzzz', # 2E4A..2E7F ; Unknown + 'Hani', # 2E80..2E99 ; Han + 'Zzzz', # 2E9A..2E9A ; Unknown + 'Hani', # 2E9B..2EF3 ; Han + 'Zzzz', # 2EF4..2EFF ; Unknown + 'Hani', # 2F00..2FD5 ; Han + 'Zzzz', # 2FD6..2FEF ; Unknown + 'Zyyy', # 2FF0..2FFB ; Common + 'Zzzz', # 2FFC..2FFF ; Unknown + 'Zyyy', # 3000..3004 ; Common + 'Hani', # 3005..3005 ; Han + 'Zyyy', # 3006..3006 ; Common + 'Hani', # 3007..3007 ; Han + 'Zyyy', # 3008..3020 ; Common + 'Hani', # 3021..3029 ; Han + 'Zinh', # 302A..302D ; Inherited + 'Hang', # 302E..302F ; Hangul + 'Zyyy', # 3030..3037 ; Common + 'Hani', # 3038..303B ; Han + 'Zyyy', # 303C..303F ; Common + 'Zzzz', # 3040..3040 ; Unknown + 'Hira', # 3041..3096 ; Hiragana + 'Zzzz', # 3097..3098 ; Unknown + 'Zinh', # 3099..309A ; Inherited + 'Zyyy', # 309B..309C ; Common + 'Hira', # 309D..309F ; Hiragana + 'Zyyy', # 30A0..30A0 ; Common + 'Kana', # 30A1..30FA ; Katakana + 'Zyyy', # 30FB..30FC ; Common + 'Kana', # 30FD..30FF ; Katakana + 'Zzzz', # 3100..3104 ; Unknown + 'Bopo', # 3105..312E ; Bopomofo + 'Zzzz', # 312F..3130 ; Unknown + 'Hang', # 3131..318E ; Hangul + 'Zzzz', # 318F..318F ; Unknown + 'Zyyy', # 3190..319F ; Common + 'Bopo', # 31A0..31BA ; Bopomofo + 'Zzzz', # 31BB..31BF ; Unknown + 'Zyyy', # 31C0..31E3 ; Common + 'Zzzz', # 31E4..31EF ; Unknown + 'Kana', # 31F0..31FF ; Katakana + 'Hang', # 3200..321E ; Hangul + 'Zzzz', # 321F..321F ; Unknown + 'Zyyy', # 3220..325F ; Common + 'Hang', # 3260..327E ; Hangul + 'Zyyy', # 327F..32CF ; Common + 'Kana', # 32D0..32FE ; Katakana + 'Zzzz', # 32FF..32FF ; Unknown + 'Kana', # 3300..3357 ; Katakana + 'Zyyy', # 3358..33FF ; Common + 'Hani', # 3400..4DB5 ; Han + 'Zzzz', # 4DB6..4DBF ; Unknown + 'Zyyy', # 4DC0..4DFF ; Common + 'Hani', # 4E00..9FEA ; Han + 'Zzzz', # 9FEB..9FFF ; Unknown + 'Yiii', # A000..A48C ; Yi + 'Zzzz', # A48D..A48F ; Unknown + 'Yiii', # A490..A4C6 ; Yi + 'Zzzz', # A4C7..A4CF ; Unknown + 'Lisu', # A4D0..A4FF ; Lisu + 'Vaii', # A500..A62B ; Vai + 'Zzzz', # A62C..A63F ; Unknown + 'Cyrl', # A640..A69F ; Cyrillic + 'Bamu', # A6A0..A6F7 ; Bamum + 'Zzzz', # A6F8..A6FF ; Unknown + 'Zyyy', # A700..A721 ; Common + 'Latn', # A722..A787 ; Latin + 'Zyyy', # A788..A78A ; Common + 'Latn', # A78B..A7AE ; Latin + 'Zzzz', # A7AF..A7AF ; Unknown + 'Latn', # A7B0..A7B7 ; Latin + 'Zzzz', # A7B8..A7F6 ; Unknown + 'Latn', # A7F7..A7FF ; Latin + 'Sylo', # A800..A82B ; Syloti_Nagri + 'Zzzz', # A82C..A82F ; Unknown + 'Zyyy', # A830..A839 ; Common + 'Zzzz', # A83A..A83F ; Unknown + 'Phag', # A840..A877 ; Phags_Pa + 'Zzzz', # A878..A87F ; Unknown + 'Saur', # A880..A8C5 ; Saurashtra + 'Zzzz', # A8C6..A8CD ; Unknown + 'Saur', # A8CE..A8D9 ; Saurashtra + 'Zzzz', # A8DA..A8DF ; Unknown + 'Deva', # A8E0..A8FD ; Devanagari + 'Zzzz', # A8FE..A8FF ; Unknown + 'Kali', # A900..A92D ; Kayah_Li + 'Zyyy', # A92E..A92E ; Common + 'Kali', # A92F..A92F ; Kayah_Li + 'Rjng', # A930..A953 ; Rejang + 'Zzzz', # A954..A95E ; Unknown + 'Rjng', # A95F..A95F ; Rejang + 'Hang', # A960..A97C ; Hangul + 'Zzzz', # A97D..A97F ; Unknown + 'Java', # A980..A9CD ; Javanese + 'Zzzz', # A9CE..A9CE ; Unknown + 'Zyyy', # A9CF..A9CF ; Common + 'Java', # A9D0..A9D9 ; Javanese + 'Zzzz', # A9DA..A9DD ; Unknown + 'Java', # A9DE..A9DF ; Javanese + 'Mymr', # A9E0..A9FE ; Myanmar + 'Zzzz', # A9FF..A9FF ; Unknown + 'Cham', # AA00..AA36 ; Cham + 'Zzzz', # AA37..AA3F ; Unknown + 'Cham', # AA40..AA4D ; Cham + 'Zzzz', # AA4E..AA4F ; Unknown + 'Cham', # AA50..AA59 ; Cham + 'Zzzz', # AA5A..AA5B ; Unknown + 'Cham', # AA5C..AA5F ; Cham + 'Mymr', # AA60..AA7F ; Myanmar + 'Tavt', # AA80..AAC2 ; Tai_Viet + 'Zzzz', # AAC3..AADA ; Unknown + 'Tavt', # AADB..AADF ; Tai_Viet + 'Mtei', # AAE0..AAF6 ; Meetei_Mayek + 'Zzzz', # AAF7..AB00 ; Unknown + 'Ethi', # AB01..AB06 ; Ethiopic + 'Zzzz', # AB07..AB08 ; Unknown + 'Ethi', # AB09..AB0E ; Ethiopic + 'Zzzz', # AB0F..AB10 ; Unknown + 'Ethi', # AB11..AB16 ; Ethiopic + 'Zzzz', # AB17..AB1F ; Unknown + 'Ethi', # AB20..AB26 ; Ethiopic + 'Zzzz', # AB27..AB27 ; Unknown + 'Ethi', # AB28..AB2E ; Ethiopic + 'Zzzz', # AB2F..AB2F ; Unknown + 'Latn', # AB30..AB5A ; Latin + 'Zyyy', # AB5B..AB5B ; Common + 'Latn', # AB5C..AB64 ; Latin + 'Grek', # AB65..AB65 ; Greek + 'Zzzz', # AB66..AB6F ; Unknown + 'Cher', # AB70..ABBF ; Cherokee + 'Mtei', # ABC0..ABED ; Meetei_Mayek + 'Zzzz', # ABEE..ABEF ; Unknown + 'Mtei', # ABF0..ABF9 ; Meetei_Mayek + 'Zzzz', # ABFA..ABFF ; Unknown + 'Hang', # AC00..D7A3 ; Hangul + 'Zzzz', # D7A4..D7AF ; Unknown + 'Hang', # D7B0..D7C6 ; Hangul + 'Zzzz', # D7C7..D7CA ; Unknown + 'Hang', # D7CB..D7FB ; Hangul + 'Zzzz', # D7FC..F8FF ; Unknown + 'Hani', # F900..FA6D ; Han + 'Zzzz', # FA6E..FA6F ; Unknown + 'Hani', # FA70..FAD9 ; Han + 'Zzzz', # FADA..FAFF ; Unknown + 'Latn', # FB00..FB06 ; Latin + 'Zzzz', # FB07..FB12 ; Unknown + 'Armn', # FB13..FB17 ; Armenian + 'Zzzz', # FB18..FB1C ; Unknown + 'Hebr', # FB1D..FB36 ; Hebrew + 'Zzzz', # FB37..FB37 ; Unknown + 'Hebr', # FB38..FB3C ; Hebrew + 'Zzzz', # FB3D..FB3D ; Unknown + 'Hebr', # FB3E..FB3E ; Hebrew + 'Zzzz', # FB3F..FB3F ; Unknown + 'Hebr', # FB40..FB41 ; Hebrew + 'Zzzz', # FB42..FB42 ; Unknown + 'Hebr', # FB43..FB44 ; Hebrew + 'Zzzz', # FB45..FB45 ; Unknown + 'Hebr', # FB46..FB4F ; Hebrew + 'Arab', # FB50..FBC1 ; Arabic + 'Zzzz', # FBC2..FBD2 ; Unknown + 'Arab', # FBD3..FD3D ; Arabic + 'Zyyy', # FD3E..FD3F ; Common + 'Zzzz', # FD40..FD4F ; Unknown + 'Arab', # FD50..FD8F ; Arabic + 'Zzzz', # FD90..FD91 ; Unknown + 'Arab', # FD92..FDC7 ; Arabic + 'Zzzz', # FDC8..FDEF ; Unknown + 'Arab', # FDF0..FDFD ; Arabic + 'Zzzz', # FDFE..FDFF ; Unknown + 'Zinh', # FE00..FE0F ; Inherited + 'Zyyy', # FE10..FE19 ; Common + 'Zzzz', # FE1A..FE1F ; Unknown + 'Zinh', # FE20..FE2D ; Inherited + 'Cyrl', # FE2E..FE2F ; Cyrillic + 'Zyyy', # FE30..FE52 ; Common + 'Zzzz', # FE53..FE53 ; Unknown + 'Zyyy', # FE54..FE66 ; Common + 'Zzzz', # FE67..FE67 ; Unknown + 'Zyyy', # FE68..FE6B ; Common + 'Zzzz', # FE6C..FE6F ; Unknown + 'Arab', # FE70..FE74 ; Arabic + 'Zzzz', # FE75..FE75 ; Unknown + 'Arab', # FE76..FEFC ; Arabic + 'Zzzz', # FEFD..FEFE ; Unknown + 'Zyyy', # FEFF..FEFF ; Common + 'Zzzz', # FF00..FF00 ; Unknown + 'Zyyy', # FF01..FF20 ; Common + 'Latn', # FF21..FF3A ; Latin + 'Zyyy', # FF3B..FF40 ; Common + 'Latn', # FF41..FF5A ; Latin + 'Zyyy', # FF5B..FF65 ; Common + 'Kana', # FF66..FF6F ; Katakana + 'Zyyy', # FF70..FF70 ; Common + 'Kana', # FF71..FF9D ; Katakana + 'Zyyy', # FF9E..FF9F ; Common + 'Hang', # FFA0..FFBE ; Hangul + 'Zzzz', # FFBF..FFC1 ; Unknown + 'Hang', # FFC2..FFC7 ; Hangul + 'Zzzz', # FFC8..FFC9 ; Unknown + 'Hang', # FFCA..FFCF ; Hangul + 'Zzzz', # FFD0..FFD1 ; Unknown + 'Hang', # FFD2..FFD7 ; Hangul + 'Zzzz', # FFD8..FFD9 ; Unknown + 'Hang', # FFDA..FFDC ; Hangul + 'Zzzz', # FFDD..FFDF ; Unknown + 'Zyyy', # FFE0..FFE6 ; Common + 'Zzzz', # FFE7..FFE7 ; Unknown + 'Zyyy', # FFE8..FFEE ; Common + 'Zzzz', # FFEF..FFF8 ; Unknown + 'Zyyy', # FFF9..FFFD ; Common + 'Zzzz', # FFFE..FFFF ; Unknown + 'Linb', # 10000..1000B ; Linear_B + 'Zzzz', # 1000C..1000C ; Unknown + 'Linb', # 1000D..10026 ; Linear_B + 'Zzzz', # 10027..10027 ; Unknown + 'Linb', # 10028..1003A ; Linear_B + 'Zzzz', # 1003B..1003B ; Unknown + 'Linb', # 1003C..1003D ; Linear_B + 'Zzzz', # 1003E..1003E ; Unknown + 'Linb', # 1003F..1004D ; Linear_B + 'Zzzz', # 1004E..1004F ; Unknown + 'Linb', # 10050..1005D ; Linear_B + 'Zzzz', # 1005E..1007F ; Unknown + 'Linb', # 10080..100FA ; Linear_B + 'Zzzz', # 100FB..100FF ; Unknown + 'Zyyy', # 10100..10102 ; Common + 'Zzzz', # 10103..10106 ; Unknown + 'Zyyy', # 10107..10133 ; Common + 'Zzzz', # 10134..10136 ; Unknown + 'Zyyy', # 10137..1013F ; Common + 'Grek', # 10140..1018E ; Greek + 'Zzzz', # 1018F..1018F ; Unknown + 'Zyyy', # 10190..1019B ; Common + 'Zzzz', # 1019C..1019F ; Unknown + 'Grek', # 101A0..101A0 ; Greek + 'Zzzz', # 101A1..101CF ; Unknown + 'Zyyy', # 101D0..101FC ; Common + 'Zinh', # 101FD..101FD ; Inherited + 'Zzzz', # 101FE..1027F ; Unknown + 'Lyci', # 10280..1029C ; Lycian + 'Zzzz', # 1029D..1029F ; Unknown + 'Cari', # 102A0..102D0 ; Carian + 'Zzzz', # 102D1..102DF ; Unknown + 'Zinh', # 102E0..102E0 ; Inherited + 'Zyyy', # 102E1..102FB ; Common + 'Zzzz', # 102FC..102FF ; Unknown + 'Ital', # 10300..10323 ; Old_Italic + 'Zzzz', # 10324..1032C ; Unknown + 'Ital', # 1032D..1032F ; Old_Italic + 'Goth', # 10330..1034A ; Gothic + 'Zzzz', # 1034B..1034F ; Unknown + 'Perm', # 10350..1037A ; Old_Permic + 'Zzzz', # 1037B..1037F ; Unknown + 'Ugar', # 10380..1039D ; Ugaritic + 'Zzzz', # 1039E..1039E ; Unknown + 'Ugar', # 1039F..1039F ; Ugaritic + 'Xpeo', # 103A0..103C3 ; Old_Persian + 'Zzzz', # 103C4..103C7 ; Unknown + 'Xpeo', # 103C8..103D5 ; Old_Persian + 'Zzzz', # 103D6..103FF ; Unknown + 'Dsrt', # 10400..1044F ; Deseret + 'Shaw', # 10450..1047F ; Shavian + 'Osma', # 10480..1049D ; Osmanya + 'Zzzz', # 1049E..1049F ; Unknown + 'Osma', # 104A0..104A9 ; Osmanya + 'Zzzz', # 104AA..104AF ; Unknown + 'Osge', # 104B0..104D3 ; Osage + 'Zzzz', # 104D4..104D7 ; Unknown + 'Osge', # 104D8..104FB ; Osage + 'Zzzz', # 104FC..104FF ; Unknown + 'Elba', # 10500..10527 ; Elbasan + 'Zzzz', # 10528..1052F ; Unknown + 'Aghb', # 10530..10563 ; Caucasian_Albanian + 'Zzzz', # 10564..1056E ; Unknown + 'Aghb', # 1056F..1056F ; Caucasian_Albanian + 'Zzzz', # 10570..105FF ; Unknown + 'Lina', # 10600..10736 ; Linear_A + 'Zzzz', # 10737..1073F ; Unknown + 'Lina', # 10740..10755 ; Linear_A + 'Zzzz', # 10756..1075F ; Unknown + 'Lina', # 10760..10767 ; Linear_A + 'Zzzz', # 10768..107FF ; Unknown + 'Cprt', # 10800..10805 ; Cypriot + 'Zzzz', # 10806..10807 ; Unknown + 'Cprt', # 10808..10808 ; Cypriot + 'Zzzz', # 10809..10809 ; Unknown + 'Cprt', # 1080A..10835 ; Cypriot + 'Zzzz', # 10836..10836 ; Unknown + 'Cprt', # 10837..10838 ; Cypriot + 'Zzzz', # 10839..1083B ; Unknown + 'Cprt', # 1083C..1083C ; Cypriot + 'Zzzz', # 1083D..1083E ; Unknown + 'Cprt', # 1083F..1083F ; Cypriot + 'Armi', # 10840..10855 ; Imperial_Aramaic + 'Zzzz', # 10856..10856 ; Unknown + 'Armi', # 10857..1085F ; Imperial_Aramaic + 'Palm', # 10860..1087F ; Palmyrene + 'Nbat', # 10880..1089E ; Nabataean + 'Zzzz', # 1089F..108A6 ; Unknown + 'Nbat', # 108A7..108AF ; Nabataean + 'Zzzz', # 108B0..108DF ; Unknown + 'Hatr', # 108E0..108F2 ; Hatran + 'Zzzz', # 108F3..108F3 ; Unknown + 'Hatr', # 108F4..108F5 ; Hatran + 'Zzzz', # 108F6..108FA ; Unknown + 'Hatr', # 108FB..108FF ; Hatran + 'Phnx', # 10900..1091B ; Phoenician + 'Zzzz', # 1091C..1091E ; Unknown + 'Phnx', # 1091F..1091F ; Phoenician + 'Lydi', # 10920..10939 ; Lydian + 'Zzzz', # 1093A..1093E ; Unknown + 'Lydi', # 1093F..1093F ; Lydian + 'Zzzz', # 10940..1097F ; Unknown + 'Mero', # 10980..1099F ; Meroitic_Hieroglyphs + 'Merc', # 109A0..109B7 ; Meroitic_Cursive + 'Zzzz', # 109B8..109BB ; Unknown + 'Merc', # 109BC..109CF ; Meroitic_Cursive + 'Zzzz', # 109D0..109D1 ; Unknown + 'Merc', # 109D2..109FF ; Meroitic_Cursive + 'Khar', # 10A00..10A03 ; Kharoshthi + 'Zzzz', # 10A04..10A04 ; Unknown + 'Khar', # 10A05..10A06 ; Kharoshthi + 'Zzzz', # 10A07..10A0B ; Unknown + 'Khar', # 10A0C..10A13 ; Kharoshthi + 'Zzzz', # 10A14..10A14 ; Unknown + 'Khar', # 10A15..10A17 ; Kharoshthi + 'Zzzz', # 10A18..10A18 ; Unknown + 'Khar', # 10A19..10A33 ; Kharoshthi + 'Zzzz', # 10A34..10A37 ; Unknown + 'Khar', # 10A38..10A3A ; Kharoshthi + 'Zzzz', # 10A3B..10A3E ; Unknown + 'Khar', # 10A3F..10A47 ; Kharoshthi + 'Zzzz', # 10A48..10A4F ; Unknown + 'Khar', # 10A50..10A58 ; Kharoshthi + 'Zzzz', # 10A59..10A5F ; Unknown + 'Sarb', # 10A60..10A7F ; Old_South_Arabian + 'Narb', # 10A80..10A9F ; Old_North_Arabian + 'Zzzz', # 10AA0..10ABF ; Unknown + 'Mani', # 10AC0..10AE6 ; Manichaean + 'Zzzz', # 10AE7..10AEA ; Unknown + 'Mani', # 10AEB..10AF6 ; Manichaean + 'Zzzz', # 10AF7..10AFF ; Unknown + 'Avst', # 10B00..10B35 ; Avestan + 'Zzzz', # 10B36..10B38 ; Unknown + 'Avst', # 10B39..10B3F ; Avestan + 'Prti', # 10B40..10B55 ; Inscriptional_Parthian + 'Zzzz', # 10B56..10B57 ; Unknown + 'Prti', # 10B58..10B5F ; Inscriptional_Parthian + 'Phli', # 10B60..10B72 ; Inscriptional_Pahlavi + 'Zzzz', # 10B73..10B77 ; Unknown + 'Phli', # 10B78..10B7F ; Inscriptional_Pahlavi + 'Phlp', # 10B80..10B91 ; Psalter_Pahlavi + 'Zzzz', # 10B92..10B98 ; Unknown + 'Phlp', # 10B99..10B9C ; Psalter_Pahlavi + 'Zzzz', # 10B9D..10BA8 ; Unknown + 'Phlp', # 10BA9..10BAF ; Psalter_Pahlavi + 'Zzzz', # 10BB0..10BFF ; Unknown + 'Orkh', # 10C00..10C48 ; Old_Turkic + 'Zzzz', # 10C49..10C7F ; Unknown + 'Hung', # 10C80..10CB2 ; Old_Hungarian + 'Zzzz', # 10CB3..10CBF ; Unknown + 'Hung', # 10CC0..10CF2 ; Old_Hungarian + 'Zzzz', # 10CF3..10CF9 ; Unknown + 'Hung', # 10CFA..10CFF ; Old_Hungarian + 'Zzzz', # 10D00..10E5F ; Unknown + 'Arab', # 10E60..10E7E ; Arabic + 'Zzzz', # 10E7F..10FFF ; Unknown + 'Brah', # 11000..1104D ; Brahmi + 'Zzzz', # 1104E..11051 ; Unknown + 'Brah', # 11052..1106F ; Brahmi + 'Zzzz', # 11070..1107E ; Unknown + 'Brah', # 1107F..1107F ; Brahmi + 'Kthi', # 11080..110C1 ; Kaithi + 'Zzzz', # 110C2..110CF ; Unknown + 'Sora', # 110D0..110E8 ; Sora_Sompeng + 'Zzzz', # 110E9..110EF ; Unknown + 'Sora', # 110F0..110F9 ; Sora_Sompeng + 'Zzzz', # 110FA..110FF ; Unknown + 'Cakm', # 11100..11134 ; Chakma + 'Zzzz', # 11135..11135 ; Unknown + 'Cakm', # 11136..11143 ; Chakma + 'Zzzz', # 11144..1114F ; Unknown + 'Mahj', # 11150..11176 ; Mahajani + 'Zzzz', # 11177..1117F ; Unknown + 'Shrd', # 11180..111CD ; Sharada + 'Zzzz', # 111CE..111CF ; Unknown + 'Shrd', # 111D0..111DF ; Sharada + 'Zzzz', # 111E0..111E0 ; Unknown + 'Sinh', # 111E1..111F4 ; Sinhala + 'Zzzz', # 111F5..111FF ; Unknown + 'Khoj', # 11200..11211 ; Khojki + 'Zzzz', # 11212..11212 ; Unknown + 'Khoj', # 11213..1123E ; Khojki + 'Zzzz', # 1123F..1127F ; Unknown + 'Mult', # 11280..11286 ; Multani + 'Zzzz', # 11287..11287 ; Unknown + 'Mult', # 11288..11288 ; Multani + 'Zzzz', # 11289..11289 ; Unknown + 'Mult', # 1128A..1128D ; Multani + 'Zzzz', # 1128E..1128E ; Unknown + 'Mult', # 1128F..1129D ; Multani + 'Zzzz', # 1129E..1129E ; Unknown + 'Mult', # 1129F..112A9 ; Multani + 'Zzzz', # 112AA..112AF ; Unknown + 'Sind', # 112B0..112EA ; Khudawadi + 'Zzzz', # 112EB..112EF ; Unknown + 'Sind', # 112F0..112F9 ; Khudawadi + 'Zzzz', # 112FA..112FF ; Unknown + 'Gran', # 11300..11303 ; Grantha + 'Zzzz', # 11304..11304 ; Unknown + 'Gran', # 11305..1130C ; Grantha + 'Zzzz', # 1130D..1130E ; Unknown + 'Gran', # 1130F..11310 ; Grantha + 'Zzzz', # 11311..11312 ; Unknown + 'Gran', # 11313..11328 ; Grantha + 'Zzzz', # 11329..11329 ; Unknown + 'Gran', # 1132A..11330 ; Grantha + 'Zzzz', # 11331..11331 ; Unknown + 'Gran', # 11332..11333 ; Grantha + 'Zzzz', # 11334..11334 ; Unknown + 'Gran', # 11335..11339 ; Grantha + 'Zzzz', # 1133A..1133B ; Unknown + 'Gran', # 1133C..11344 ; Grantha + 'Zzzz', # 11345..11346 ; Unknown + 'Gran', # 11347..11348 ; Grantha + 'Zzzz', # 11349..1134A ; Unknown + 'Gran', # 1134B..1134D ; Grantha + 'Zzzz', # 1134E..1134F ; Unknown + 'Gran', # 11350..11350 ; Grantha + 'Zzzz', # 11351..11356 ; Unknown + 'Gran', # 11357..11357 ; Grantha + 'Zzzz', # 11358..1135C ; Unknown + 'Gran', # 1135D..11363 ; Grantha + 'Zzzz', # 11364..11365 ; Unknown + 'Gran', # 11366..1136C ; Grantha + 'Zzzz', # 1136D..1136F ; Unknown + 'Gran', # 11370..11374 ; Grantha + 'Zzzz', # 11375..113FF ; Unknown + 'Newa', # 11400..11459 ; Newa + 'Zzzz', # 1145A..1145A ; Unknown + 'Newa', # 1145B..1145B ; Newa + 'Zzzz', # 1145C..1145C ; Unknown + 'Newa', # 1145D..1145D ; Newa + 'Zzzz', # 1145E..1147F ; Unknown + 'Tirh', # 11480..114C7 ; Tirhuta + 'Zzzz', # 114C8..114CF ; Unknown + 'Tirh', # 114D0..114D9 ; Tirhuta + 'Zzzz', # 114DA..1157F ; Unknown + 'Sidd', # 11580..115B5 ; Siddham + 'Zzzz', # 115B6..115B7 ; Unknown + 'Sidd', # 115B8..115DD ; Siddham + 'Zzzz', # 115DE..115FF ; Unknown + 'Modi', # 11600..11644 ; Modi + 'Zzzz', # 11645..1164F ; Unknown + 'Modi', # 11650..11659 ; Modi + 'Zzzz', # 1165A..1165F ; Unknown + 'Mong', # 11660..1166C ; Mongolian + 'Zzzz', # 1166D..1167F ; Unknown + 'Takr', # 11680..116B7 ; Takri + 'Zzzz', # 116B8..116BF ; Unknown + 'Takr', # 116C0..116C9 ; Takri + 'Zzzz', # 116CA..116FF ; Unknown + 'Ahom', # 11700..11719 ; Ahom + 'Zzzz', # 1171A..1171C ; Unknown + 'Ahom', # 1171D..1172B ; Ahom + 'Zzzz', # 1172C..1172F ; Unknown + 'Ahom', # 11730..1173F ; Ahom + 'Zzzz', # 11740..1189F ; Unknown + 'Wara', # 118A0..118F2 ; Warang_Citi + 'Zzzz', # 118F3..118FE ; Unknown + 'Wara', # 118FF..118FF ; Warang_Citi + 'Zzzz', # 11900..119FF ; Unknown + 'Zanb', # 11A00..11A47 ; Zanabazar_Square + 'Zzzz', # 11A48..11A4F ; Unknown + 'Soyo', # 11A50..11A83 ; Soyombo + 'Zzzz', # 11A84..11A85 ; Unknown + 'Soyo', # 11A86..11A9C ; Soyombo + 'Zzzz', # 11A9D..11A9D ; Unknown + 'Soyo', # 11A9E..11AA2 ; Soyombo + 'Zzzz', # 11AA3..11ABF ; Unknown + 'Pauc', # 11AC0..11AF8 ; Pau_Cin_Hau + 'Zzzz', # 11AF9..11BFF ; Unknown + 'Bhks', # 11C00..11C08 ; Bhaiksuki + 'Zzzz', # 11C09..11C09 ; Unknown + 'Bhks', # 11C0A..11C36 ; Bhaiksuki + 'Zzzz', # 11C37..11C37 ; Unknown + 'Bhks', # 11C38..11C45 ; Bhaiksuki + 'Zzzz', # 11C46..11C4F ; Unknown + 'Bhks', # 11C50..11C6C ; Bhaiksuki + 'Zzzz', # 11C6D..11C6F ; Unknown + 'Marc', # 11C70..11C8F ; Marchen + 'Zzzz', # 11C90..11C91 ; Unknown + 'Marc', # 11C92..11CA7 ; Marchen + 'Zzzz', # 11CA8..11CA8 ; Unknown + 'Marc', # 11CA9..11CB6 ; Marchen + 'Zzzz', # 11CB7..11CFF ; Unknown + 'Gonm', # 11D00..11D06 ; Masaram_Gondi + 'Zzzz', # 11D07..11D07 ; Unknown + 'Gonm', # 11D08..11D09 ; Masaram_Gondi + 'Zzzz', # 11D0A..11D0A ; Unknown + 'Gonm', # 11D0B..11D36 ; Masaram_Gondi + 'Zzzz', # 11D37..11D39 ; Unknown + 'Gonm', # 11D3A..11D3A ; Masaram_Gondi + 'Zzzz', # 11D3B..11D3B ; Unknown + 'Gonm', # 11D3C..11D3D ; Masaram_Gondi + 'Zzzz', # 11D3E..11D3E ; Unknown + 'Gonm', # 11D3F..11D47 ; Masaram_Gondi + 'Zzzz', # 11D48..11D4F ; Unknown + 'Gonm', # 11D50..11D59 ; Masaram_Gondi + 'Zzzz', # 11D5A..11FFF ; Unknown + 'Xsux', # 12000..12399 ; Cuneiform + 'Zzzz', # 1239A..123FF ; Unknown + 'Xsux', # 12400..1246E ; Cuneiform + 'Zzzz', # 1246F..1246F ; Unknown + 'Xsux', # 12470..12474 ; Cuneiform + 'Zzzz', # 12475..1247F ; Unknown + 'Xsux', # 12480..12543 ; Cuneiform + 'Zzzz', # 12544..12FFF ; Unknown + 'Egyp', # 13000..1342E ; Egyptian_Hieroglyphs + 'Zzzz', # 1342F..143FF ; Unknown + 'Hluw', # 14400..14646 ; Anatolian_Hieroglyphs + 'Zzzz', # 14647..167FF ; Unknown + 'Bamu', # 16800..16A38 ; Bamum + 'Zzzz', # 16A39..16A3F ; Unknown + 'Mroo', # 16A40..16A5E ; Mro + 'Zzzz', # 16A5F..16A5F ; Unknown + 'Mroo', # 16A60..16A69 ; Mro + 'Zzzz', # 16A6A..16A6D ; Unknown + 'Mroo', # 16A6E..16A6F ; Mro + 'Zzzz', # 16A70..16ACF ; Unknown + 'Bass', # 16AD0..16AED ; Bassa_Vah + 'Zzzz', # 16AEE..16AEF ; Unknown + 'Bass', # 16AF0..16AF5 ; Bassa_Vah + 'Zzzz', # 16AF6..16AFF ; Unknown + 'Hmng', # 16B00..16B45 ; Pahawh_Hmong + 'Zzzz', # 16B46..16B4F ; Unknown + 'Hmng', # 16B50..16B59 ; Pahawh_Hmong + 'Zzzz', # 16B5A..16B5A ; Unknown + 'Hmng', # 16B5B..16B61 ; Pahawh_Hmong + 'Zzzz', # 16B62..16B62 ; Unknown + 'Hmng', # 16B63..16B77 ; Pahawh_Hmong + 'Zzzz', # 16B78..16B7C ; Unknown + 'Hmng', # 16B7D..16B8F ; Pahawh_Hmong + 'Zzzz', # 16B90..16EFF ; Unknown + 'Plrd', # 16F00..16F44 ; Miao + 'Zzzz', # 16F45..16F4F ; Unknown + 'Plrd', # 16F50..16F7E ; Miao + 'Zzzz', # 16F7F..16F8E ; Unknown + 'Plrd', # 16F8F..16F9F ; Miao + 'Zzzz', # 16FA0..16FDF ; Unknown + 'Tang', # 16FE0..16FE0 ; Tangut + 'Nshu', # 16FE1..16FE1 ; Nushu + 'Zzzz', # 16FE2..16FFF ; Unknown + 'Tang', # 17000..187EC ; Tangut + 'Zzzz', # 187ED..187FF ; Unknown + 'Tang', # 18800..18AF2 ; Tangut + 'Zzzz', # 18AF3..1AFFF ; Unknown + 'Kana', # 1B000..1B000 ; Katakana + 'Hira', # 1B001..1B11E ; Hiragana + 'Zzzz', # 1B11F..1B16F ; Unknown + 'Nshu', # 1B170..1B2FB ; Nushu + 'Zzzz', # 1B2FC..1BBFF ; Unknown + 'Dupl', # 1BC00..1BC6A ; Duployan + 'Zzzz', # 1BC6B..1BC6F ; Unknown + 'Dupl', # 1BC70..1BC7C ; Duployan + 'Zzzz', # 1BC7D..1BC7F ; Unknown + 'Dupl', # 1BC80..1BC88 ; Duployan + 'Zzzz', # 1BC89..1BC8F ; Unknown + 'Dupl', # 1BC90..1BC99 ; Duployan + 'Zzzz', # 1BC9A..1BC9B ; Unknown + 'Dupl', # 1BC9C..1BC9F ; Duployan + 'Zyyy', # 1BCA0..1BCA3 ; Common + 'Zzzz', # 1BCA4..1CFFF ; Unknown + 'Zyyy', # 1D000..1D0F5 ; Common + 'Zzzz', # 1D0F6..1D0FF ; Unknown + 'Zyyy', # 1D100..1D126 ; Common + 'Zzzz', # 1D127..1D128 ; Unknown + 'Zyyy', # 1D129..1D166 ; Common + 'Zinh', # 1D167..1D169 ; Inherited + 'Zyyy', # 1D16A..1D17A ; Common + 'Zinh', # 1D17B..1D182 ; Inherited + 'Zyyy', # 1D183..1D184 ; Common + 'Zinh', # 1D185..1D18B ; Inherited + 'Zyyy', # 1D18C..1D1A9 ; Common + 'Zinh', # 1D1AA..1D1AD ; Inherited + 'Zyyy', # 1D1AE..1D1E8 ; Common + 'Zzzz', # 1D1E9..1D1FF ; Unknown + 'Grek', # 1D200..1D245 ; Greek + 'Zzzz', # 1D246..1D2FF ; Unknown + 'Zyyy', # 1D300..1D356 ; Common + 'Zzzz', # 1D357..1D35F ; Unknown + 'Zyyy', # 1D360..1D371 ; Common + 'Zzzz', # 1D372..1D3FF ; Unknown + 'Zyyy', # 1D400..1D454 ; Common + 'Zzzz', # 1D455..1D455 ; Unknown + 'Zyyy', # 1D456..1D49C ; Common + 'Zzzz', # 1D49D..1D49D ; Unknown + 'Zyyy', # 1D49E..1D49F ; Common + 'Zzzz', # 1D4A0..1D4A1 ; Unknown + 'Zyyy', # 1D4A2..1D4A2 ; Common + 'Zzzz', # 1D4A3..1D4A4 ; Unknown + 'Zyyy', # 1D4A5..1D4A6 ; Common + 'Zzzz', # 1D4A7..1D4A8 ; Unknown + 'Zyyy', # 1D4A9..1D4AC ; Common + 'Zzzz', # 1D4AD..1D4AD ; Unknown + 'Zyyy', # 1D4AE..1D4B9 ; Common + 'Zzzz', # 1D4BA..1D4BA ; Unknown + 'Zyyy', # 1D4BB..1D4BB ; Common + 'Zzzz', # 1D4BC..1D4BC ; Unknown + 'Zyyy', # 1D4BD..1D4C3 ; Common + 'Zzzz', # 1D4C4..1D4C4 ; Unknown + 'Zyyy', # 1D4C5..1D505 ; Common + 'Zzzz', # 1D506..1D506 ; Unknown + 'Zyyy', # 1D507..1D50A ; Common + 'Zzzz', # 1D50B..1D50C ; Unknown + 'Zyyy', # 1D50D..1D514 ; Common + 'Zzzz', # 1D515..1D515 ; Unknown + 'Zyyy', # 1D516..1D51C ; Common + 'Zzzz', # 1D51D..1D51D ; Unknown + 'Zyyy', # 1D51E..1D539 ; Common + 'Zzzz', # 1D53A..1D53A ; Unknown + 'Zyyy', # 1D53B..1D53E ; Common + 'Zzzz', # 1D53F..1D53F ; Unknown + 'Zyyy', # 1D540..1D544 ; Common + 'Zzzz', # 1D545..1D545 ; Unknown + 'Zyyy', # 1D546..1D546 ; Common + 'Zzzz', # 1D547..1D549 ; Unknown + 'Zyyy', # 1D54A..1D550 ; Common + 'Zzzz', # 1D551..1D551 ; Unknown + 'Zyyy', # 1D552..1D6A5 ; Common + 'Zzzz', # 1D6A6..1D6A7 ; Unknown + 'Zyyy', # 1D6A8..1D7CB ; Common + 'Zzzz', # 1D7CC..1D7CD ; Unknown + 'Zyyy', # 1D7CE..1D7FF ; Common + 'Sgnw', # 1D800..1DA8B ; SignWriting + 'Zzzz', # 1DA8C..1DA9A ; Unknown + 'Sgnw', # 1DA9B..1DA9F ; SignWriting + 'Zzzz', # 1DAA0..1DAA0 ; Unknown + 'Sgnw', # 1DAA1..1DAAF ; SignWriting + 'Zzzz', # 1DAB0..1DFFF ; Unknown + 'Glag', # 1E000..1E006 ; Glagolitic + 'Zzzz', # 1E007..1E007 ; Unknown + 'Glag', # 1E008..1E018 ; Glagolitic + 'Zzzz', # 1E019..1E01A ; Unknown + 'Glag', # 1E01B..1E021 ; Glagolitic + 'Zzzz', # 1E022..1E022 ; Unknown + 'Glag', # 1E023..1E024 ; Glagolitic + 'Zzzz', # 1E025..1E025 ; Unknown + 'Glag', # 1E026..1E02A ; Glagolitic + 'Zzzz', # 1E02B..1E7FF ; Unknown + 'Mend', # 1E800..1E8C4 ; Mende_Kikakui + 'Zzzz', # 1E8C5..1E8C6 ; Unknown + 'Mend', # 1E8C7..1E8D6 ; Mende_Kikakui + 'Zzzz', # 1E8D7..1E8FF ; Unknown + 'Adlm', # 1E900..1E94A ; Adlam + 'Zzzz', # 1E94B..1E94F ; Unknown + 'Adlm', # 1E950..1E959 ; Adlam + 'Zzzz', # 1E95A..1E95D ; Unknown + 'Adlm', # 1E95E..1E95F ; Adlam + 'Zzzz', # 1E960..1EDFF ; Unknown + 'Arab', # 1EE00..1EE03 ; Arabic + 'Zzzz', # 1EE04..1EE04 ; Unknown + 'Arab', # 1EE05..1EE1F ; Arabic + 'Zzzz', # 1EE20..1EE20 ; Unknown + 'Arab', # 1EE21..1EE22 ; Arabic + 'Zzzz', # 1EE23..1EE23 ; Unknown + 'Arab', # 1EE24..1EE24 ; Arabic + 'Zzzz', # 1EE25..1EE26 ; Unknown + 'Arab', # 1EE27..1EE27 ; Arabic + 'Zzzz', # 1EE28..1EE28 ; Unknown + 'Arab', # 1EE29..1EE32 ; Arabic + 'Zzzz', # 1EE33..1EE33 ; Unknown + 'Arab', # 1EE34..1EE37 ; Arabic + 'Zzzz', # 1EE38..1EE38 ; Unknown + 'Arab', # 1EE39..1EE39 ; Arabic + 'Zzzz', # 1EE3A..1EE3A ; Unknown + 'Arab', # 1EE3B..1EE3B ; Arabic + 'Zzzz', # 1EE3C..1EE41 ; Unknown + 'Arab', # 1EE42..1EE42 ; Arabic + 'Zzzz', # 1EE43..1EE46 ; Unknown + 'Arab', # 1EE47..1EE47 ; Arabic + 'Zzzz', # 1EE48..1EE48 ; Unknown + 'Arab', # 1EE49..1EE49 ; Arabic + 'Zzzz', # 1EE4A..1EE4A ; Unknown + 'Arab', # 1EE4B..1EE4B ; Arabic + 'Zzzz', # 1EE4C..1EE4C ; Unknown + 'Arab', # 1EE4D..1EE4F ; Arabic + 'Zzzz', # 1EE50..1EE50 ; Unknown + 'Arab', # 1EE51..1EE52 ; Arabic + 'Zzzz', # 1EE53..1EE53 ; Unknown + 'Arab', # 1EE54..1EE54 ; Arabic + 'Zzzz', # 1EE55..1EE56 ; Unknown + 'Arab', # 1EE57..1EE57 ; Arabic + 'Zzzz', # 1EE58..1EE58 ; Unknown + 'Arab', # 1EE59..1EE59 ; Arabic + 'Zzzz', # 1EE5A..1EE5A ; Unknown + 'Arab', # 1EE5B..1EE5B ; Arabic + 'Zzzz', # 1EE5C..1EE5C ; Unknown + 'Arab', # 1EE5D..1EE5D ; Arabic + 'Zzzz', # 1EE5E..1EE5E ; Unknown + 'Arab', # 1EE5F..1EE5F ; Arabic + 'Zzzz', # 1EE60..1EE60 ; Unknown + 'Arab', # 1EE61..1EE62 ; Arabic + 'Zzzz', # 1EE63..1EE63 ; Unknown + 'Arab', # 1EE64..1EE64 ; Arabic + 'Zzzz', # 1EE65..1EE66 ; Unknown + 'Arab', # 1EE67..1EE6A ; Arabic + 'Zzzz', # 1EE6B..1EE6B ; Unknown + 'Arab', # 1EE6C..1EE72 ; Arabic + 'Zzzz', # 1EE73..1EE73 ; Unknown + 'Arab', # 1EE74..1EE77 ; Arabic + 'Zzzz', # 1EE78..1EE78 ; Unknown + 'Arab', # 1EE79..1EE7C ; Arabic + 'Zzzz', # 1EE7D..1EE7D ; Unknown + 'Arab', # 1EE7E..1EE7E ; Arabic + 'Zzzz', # 1EE7F..1EE7F ; Unknown + 'Arab', # 1EE80..1EE89 ; Arabic + 'Zzzz', # 1EE8A..1EE8A ; Unknown + 'Arab', # 1EE8B..1EE9B ; Arabic + 'Zzzz', # 1EE9C..1EEA0 ; Unknown + 'Arab', # 1EEA1..1EEA3 ; Arabic + 'Zzzz', # 1EEA4..1EEA4 ; Unknown + 'Arab', # 1EEA5..1EEA9 ; Arabic + 'Zzzz', # 1EEAA..1EEAA ; Unknown + 'Arab', # 1EEAB..1EEBB ; Arabic + 'Zzzz', # 1EEBC..1EEEF ; Unknown + 'Arab', # 1EEF0..1EEF1 ; Arabic + 'Zzzz', # 1EEF2..1EFFF ; Unknown + 'Zyyy', # 1F000..1F02B ; Common + 'Zzzz', # 1F02C..1F02F ; Unknown + 'Zyyy', # 1F030..1F093 ; Common + 'Zzzz', # 1F094..1F09F ; Unknown + 'Zyyy', # 1F0A0..1F0AE ; Common + 'Zzzz', # 1F0AF..1F0B0 ; Unknown + 'Zyyy', # 1F0B1..1F0BF ; Common + 'Zzzz', # 1F0C0..1F0C0 ; Unknown + 'Zyyy', # 1F0C1..1F0CF ; Common + 'Zzzz', # 1F0D0..1F0D0 ; Unknown + 'Zyyy', # 1F0D1..1F0F5 ; Common + 'Zzzz', # 1F0F6..1F0FF ; Unknown + 'Zyyy', # 1F100..1F10C ; Common + 'Zzzz', # 1F10D..1F10F ; Unknown + 'Zyyy', # 1F110..1F12E ; Common + 'Zzzz', # 1F12F..1F12F ; Unknown + 'Zyyy', # 1F130..1F16B ; Common + 'Zzzz', # 1F16C..1F16F ; Unknown + 'Zyyy', # 1F170..1F1AC ; Common + 'Zzzz', # 1F1AD..1F1E5 ; Unknown + 'Zyyy', # 1F1E6..1F1FF ; Common + 'Hira', # 1F200..1F200 ; Hiragana + 'Zyyy', # 1F201..1F202 ; Common + 'Zzzz', # 1F203..1F20F ; Unknown + 'Zyyy', # 1F210..1F23B ; Common + 'Zzzz', # 1F23C..1F23F ; Unknown + 'Zyyy', # 1F240..1F248 ; Common + 'Zzzz', # 1F249..1F24F ; Unknown + 'Zyyy', # 1F250..1F251 ; Common + 'Zzzz', # 1F252..1F25F ; Unknown + 'Zyyy', # 1F260..1F265 ; Common + 'Zzzz', # 1F266..1F2FF ; Unknown + 'Zyyy', # 1F300..1F6D4 ; Common + 'Zzzz', # 1F6D5..1F6DF ; Unknown + 'Zyyy', # 1F6E0..1F6EC ; Common + 'Zzzz', # 1F6ED..1F6EF ; Unknown + 'Zyyy', # 1F6F0..1F6F8 ; Common + 'Zzzz', # 1F6F9..1F6FF ; Unknown + 'Zyyy', # 1F700..1F773 ; Common + 'Zzzz', # 1F774..1F77F ; Unknown + 'Zyyy', # 1F780..1F7D4 ; Common + 'Zzzz', # 1F7D5..1F7FF ; Unknown + 'Zyyy', # 1F800..1F80B ; Common + 'Zzzz', # 1F80C..1F80F ; Unknown + 'Zyyy', # 1F810..1F847 ; Common + 'Zzzz', # 1F848..1F84F ; Unknown + 'Zyyy', # 1F850..1F859 ; Common + 'Zzzz', # 1F85A..1F85F ; Unknown + 'Zyyy', # 1F860..1F887 ; Common + 'Zzzz', # 1F888..1F88F ; Unknown + 'Zyyy', # 1F890..1F8AD ; Common + 'Zzzz', # 1F8AE..1F8FF ; Unknown + 'Zyyy', # 1F900..1F90B ; Common + 'Zzzz', # 1F90C..1F90F ; Unknown + 'Zyyy', # 1F910..1F93E ; Common + 'Zzzz', # 1F93F..1F93F ; Unknown + 'Zyyy', # 1F940..1F94C ; Common + 'Zzzz', # 1F94D..1F94F ; Unknown + 'Zyyy', # 1F950..1F96B ; Common + 'Zzzz', # 1F96C..1F97F ; Unknown + 'Zyyy', # 1F980..1F997 ; Common + 'Zzzz', # 1F998..1F9BF ; Unknown + 'Zyyy', # 1F9C0..1F9C0 ; Common + 'Zzzz', # 1F9C1..1F9CF ; Unknown + 'Zyyy', # 1F9D0..1F9E6 ; Common + 'Zzzz', # 1F9E7..1FFFF ; Unknown + 'Hani', # 20000..2A6D6 ; Han + 'Zzzz', # 2A6D7..2A6FF ; Unknown + 'Hani', # 2A700..2B734 ; Han + 'Zzzz', # 2B735..2B73F ; Unknown + 'Hani', # 2B740..2B81D ; Han + 'Zzzz', # 2B81E..2B81F ; Unknown + 'Hani', # 2B820..2CEA1 ; Han + 'Zzzz', # 2CEA2..2CEAF ; Unknown + 'Hani', # 2CEB0..2EBE0 ; Han + 'Zzzz', # 2EBE1..2F7FF ; Unknown + 'Hani', # 2F800..2FA1D ; Han + 'Zzzz', # 2FA1E..E0000 ; Unknown + 'Zyyy', # E0001..E0001 ; Common + 'Zzzz', # E0002..E001F ; Unknown + 'Zyyy', # E0020..E007F ; Common + 'Zzzz', # E0080..E00FF ; Unknown + 'Zinh', # E0100..E01EF ; Inherited + 'Zzzz', # E01F0..10FFFF ; Unknown +] + +NAMES = { + 'Adlm': 'Adlam', + 'Aghb': 'Caucasian_Albanian', + 'Ahom': 'Ahom', + 'Arab': 'Arabic', + 'Armi': 'Imperial_Aramaic', + 'Armn': 'Armenian', + 'Avst': 'Avestan', + 'Bali': 'Balinese', + 'Bamu': 'Bamum', + 'Bass': 'Bassa_Vah', + 'Batk': 'Batak', + 'Beng': 'Bengali', + 'Bhks': 'Bhaiksuki', + 'Bopo': 'Bopomofo', + 'Brah': 'Brahmi', + 'Brai': 'Braille', + 'Bugi': 'Buginese', + 'Buhd': 'Buhid', + 'Cakm': 'Chakma', + 'Cans': 'Canadian_Aboriginal', + 'Cari': 'Carian', + 'Cham': 'Cham', + 'Cher': 'Cherokee', + 'Copt': 'Coptic', + 'Cprt': 'Cypriot', + 'Cyrl': 'Cyrillic', + 'Deva': 'Devanagari', + 'Dsrt': 'Deseret', + 'Dupl': 'Duployan', + 'Egyp': 'Egyptian_Hieroglyphs', + 'Elba': 'Elbasan', + 'Ethi': 'Ethiopic', + 'Geor': 'Georgian', + 'Glag': 'Glagolitic', + 'Gonm': 'Masaram_Gondi', + 'Goth': 'Gothic', + 'Gran': 'Grantha', + 'Grek': 'Greek', + 'Gujr': 'Gujarati', + 'Guru': 'Gurmukhi', + 'Hang': 'Hangul', + 'Hani': 'Han', + 'Hano': 'Hanunoo', + 'Hatr': 'Hatran', + 'Hebr': 'Hebrew', + 'Hira': 'Hiragana', + 'Hluw': 'Anatolian_Hieroglyphs', + 'Hmng': 'Pahawh_Hmong', + 'Hrkt': 'Katakana_Or_Hiragana', + 'Hung': 'Old_Hungarian', + 'Ital': 'Old_Italic', + 'Java': 'Javanese', + 'Kali': 'Kayah_Li', + 'Kana': 'Katakana', + 'Khar': 'Kharoshthi', + 'Khmr': 'Khmer', + 'Khoj': 'Khojki', + 'Knda': 'Kannada', + 'Kthi': 'Kaithi', + 'Lana': 'Tai_Tham', + 'Laoo': 'Lao', + 'Latn': 'Latin', + 'Lepc': 'Lepcha', + 'Limb': 'Limbu', + 'Lina': 'Linear_A', + 'Linb': 'Linear_B', + 'Lisu': 'Lisu', + 'Lyci': 'Lycian', + 'Lydi': 'Lydian', + 'Mahj': 'Mahajani', + 'Mand': 'Mandaic', + 'Mani': 'Manichaean', + 'Marc': 'Marchen', + 'Mend': 'Mende_Kikakui', + 'Merc': 'Meroitic_Cursive', + 'Mero': 'Meroitic_Hieroglyphs', + 'Mlym': 'Malayalam', + 'Modi': 'Modi', + 'Mong': 'Mongolian', + 'Mroo': 'Mro', + 'Mtei': 'Meetei_Mayek', + 'Mult': 'Multani', + 'Mymr': 'Myanmar', + 'Narb': 'Old_North_Arabian', + 'Nbat': 'Nabataean', + 'Newa': 'Newa', + 'Nkoo': 'Nko', + 'Nshu': 'Nushu', + 'Ogam': 'Ogham', + 'Olck': 'Ol_Chiki', + 'Orkh': 'Old_Turkic', + 'Orya': 'Oriya', + 'Osge': 'Osage', + 'Osma': 'Osmanya', + 'Palm': 'Palmyrene', + 'Pauc': 'Pau_Cin_Hau', + 'Perm': 'Old_Permic', + 'Phag': 'Phags_Pa', + 'Phli': 'Inscriptional_Pahlavi', + 'Phlp': 'Psalter_Pahlavi', + 'Phnx': 'Phoenician', + 'Plrd': 'Miao', + 'Prti': 'Inscriptional_Parthian', + 'Rjng': 'Rejang', + 'Runr': 'Runic', + 'Samr': 'Samaritan', + 'Sarb': 'Old_South_Arabian', + 'Saur': 'Saurashtra', + 'Sgnw': 'SignWriting', + 'Shaw': 'Shavian', + 'Shrd': 'Sharada', + 'Sidd': 'Siddham', + 'Sind': 'Khudawadi', + 'Sinh': 'Sinhala', + 'Sora': 'Sora_Sompeng', + 'Soyo': 'Soyombo', + 'Sund': 'Sundanese', + 'Sylo': 'Syloti_Nagri', + 'Syrc': 'Syriac', + 'Tagb': 'Tagbanwa', + 'Takr': 'Takri', + 'Tale': 'Tai_Le', + 'Talu': 'New_Tai_Lue', + 'Taml': 'Tamil', + 'Tang': 'Tangut', + 'Tavt': 'Tai_Viet', + 'Telu': 'Telugu', + 'Tfng': 'Tifinagh', + 'Tglg': 'Tagalog', + 'Thaa': 'Thaana', + 'Thai': 'Thai', + 'Tibt': 'Tibetan', + 'Tirh': 'Tirhuta', + 'Ugar': 'Ugaritic', + 'Vaii': 'Vai', + 'Wara': 'Warang_Citi', + 'Xpeo': 'Old_Persian', + 'Xsux': 'Cuneiform', + 'Yiii': 'Yi', + 'Zanb': 'Zanabazar_Square', + 'Zinh': 'Inherited', + 'Zyyy': 'Common', + 'Zzzz': 'Unknown', +} diff --git a/Lib/fontTools/unicodedata/__init__.py b/Lib/fontTools/unicodedata/__init__.py new file mode 100644 index 0000000..0a69dbd --- /dev/null +++ b/Lib/fontTools/unicodedata/__init__.py @@ -0,0 +1,276 @@ +from __future__ import ( + print_function, division, absolute_import, unicode_literals) +from fontTools.misc.py23 import * + +import re +from bisect import bisect_right + +try: + # use unicodedata backport compatible with python2: + # https://github.com/mikekap/unicodedata2 + from unicodedata2 import * +except ImportError: # pragma: no cover + # fall back to built-in unicodedata (possibly outdated) + from unicodedata import * + +from . import Blocks, Scripts, ScriptExtensions, OTTags + + +__all__ = [tostr(s) for s in ( + # names from built-in unicodedata module + "lookup", + "name", + "decimal", + "digit", + "numeric", + "category", + "bidirectional", + "combining", + "east_asian_width", + "mirrored", + "decomposition", + "normalize", + "unidata_version", + "ucd_3_2_0", + # additonal functions + "block", + "script", + "script_extension", + "script_name", + "script_code", + "script_horizontal_direction", + "ot_tags_from_script", + "ot_tag_to_script", +)] + + +def script(char): + """ Return the four-letter script code assigned to the Unicode character + 'char' as string. + + >>> script("a") + 'Latn' + >>> script(",") + 'Zyyy' + >>> script(unichr(0x10FFFF)) + 'Zzzz' + """ + code = byteord(char) + # 'bisect_right(a, x, lo=0, hi=len(a))' returns an insertion point which + # comes after (to the right of) any existing entries of x in a, and it + # partitions array a into two halves so that, for the left side + # all(val <= x for val in a[lo:i]), and for the right side + # all(val > x for val in a[i:hi]). + # Our 'SCRIPT_RANGES' is a sorted list of ranges (only their starting + # breakpoints); we want to use `bisect_right` to look up the range that + # contains the given codepoint: i.e. whose start is less than or equal + # to the codepoint. Thus, we subtract -1 from the index returned. + i = bisect_right(Scripts.RANGES, code) + return Scripts.VALUES[i-1] + + +def script_extension(char): + """ Return the script extension property assigned to the Unicode character + 'char' as a set of string. + + >>> script_extension("a") == {'Latn'} + True + >>> script_extension(unichr(0x060C)) == {'Arab', 'Syrc', 'Thaa'} + True + >>> script_extension(unichr(0x10FFFF)) == {'Zzzz'} + True + """ + code = byteord(char) + i = bisect_right(ScriptExtensions.RANGES, code) + value = ScriptExtensions.VALUES[i-1] + if value is None: + # code points not explicitly listed for Script Extensions + # have as their value the corresponding Script property value + return {script(char)} + return value + + +def script_name(code, default=KeyError): + """ Return the long, human-readable script name given a four-letter + Unicode script code. + + If no matching name is found, a KeyError is raised by default. + + You can use the 'default' argument to return a fallback value (e.g. + 'Unknown' or None) instead of throwing an error. + """ + try: + return str(Scripts.NAMES[code].replace("_", " ")) + except KeyError: + if isinstance(default, type) and issubclass(default, KeyError): + raise + return default + + +_normalize_re = re.compile(r"[-_ ]+") + + +def _normalize_property_name(string): + """Remove case, strip space, '-' and '_' for loose matching.""" + return _normalize_re.sub("", string).lower() + + +_SCRIPT_CODES = {_normalize_property_name(v): k + for k, v in Scripts.NAMES.items()} + + +def script_code(script_name, default=KeyError): + """Returns the four-letter Unicode script code from its long name + + If no matching script code is found, a KeyError is raised by default. + + You can use the 'default' argument to return a fallback string (e.g. + 'Zzzz' or None) instead of throwing an error. + """ + normalized_name = _normalize_property_name(script_name) + try: + return _SCRIPT_CODES[normalized_name] + except KeyError: + if isinstance(default, type) and issubclass(default, KeyError): + raise + return default + + +# The data on script direction is taken from harfbuzz's "hb-common.cc": +# https://goo.gl/X5FDXC +# It matches the CLDR "scriptMetadata.txt as of January 2018: +# http://unicode.org/repos/cldr/trunk/common/properties/scriptMetadata.txt +RTL_SCRIPTS = { + # Unicode-1.1 additions + 'Arab', # Arabic + 'Hebr', # Hebrew + + # Unicode-3.0 additions + 'Syrc', # Syriac + 'Thaa', # Thaana + + # Unicode-4.0 additions + 'Cprt', # Cypriot + + # Unicode-4.1 additions + 'Khar', # Kharoshthi + + # Unicode-5.0 additions + 'Phnx', # Phoenician + 'Nkoo', # Nko + + # Unicode-5.1 additions + 'Lydi', # Lydian + + # Unicode-5.2 additions + 'Avst', # Avestan + 'Armi', # Imperial Aramaic + 'Phli', # Inscriptional Pahlavi + 'Prti', # Inscriptional Parthian + 'Sarb', # Old South Arabian + 'Orkh', # Old Turkic + 'Samr', # Samaritan + + # Unicode-6.0 additions + 'Mand', # Mandaic + + # Unicode-6.1 additions + 'Merc', # Meroitic Cursive + 'Mero', # Meroitic Hieroglyphs + + # Unicode-7.0 additions + 'Mani', # Manichaean + 'Mend', # Mende Kikakui + 'Nbat', # Nabataean + 'Narb', # Old North Arabian + 'Palm', # Palmyrene + 'Phlp', # Psalter Pahlavi + + # Unicode-8.0 additions + 'Hatr', # Hatran + 'Hung', # Old Hungarian + + # Unicode-9.0 additions + 'Adlm', # Adlam +} + +def script_horizontal_direction(script_code, default=KeyError): + """ Return "RTL" for scripts that contain right-to-left characters + according to the Bidi_Class property. Otherwise return "LTR". + """ + if script_code not in Scripts.NAMES: + if isinstance(default, type) and issubclass(default, KeyError): + raise default(script_code) + return default + return str("RTL") if script_code in RTL_SCRIPTS else str("LTR") + + +def block(char): + """ Return the block property assigned to the Unicode character 'char' + as a string. + + >>> block("a") + 'Basic Latin' + >>> block(unichr(0x060C)) + 'Arabic' + >>> block(unichr(0xEFFFF)) + 'No_Block' + """ + code = byteord(char) + i = bisect_right(Blocks.RANGES, code) + return Blocks.VALUES[i-1] + + +def ot_tags_from_script(script_code): + """ Return a list of OpenType script tags associated with a given + Unicode script code. + Return ['DFLT'] script tag for invalid/unknown script codes. + """ + if script_code not in Scripts.NAMES: + return [OTTags.DEFAULT_SCRIPT] + + script_tags = [ + OTTags.SCRIPT_EXCEPTIONS.get( + script_code, + script_code[0].lower() + script_code[1:] + ) + ] + if script_code in OTTags.NEW_SCRIPT_TAGS: + script_tags.extend(OTTags.NEW_SCRIPT_TAGS[script_code]) + script_tags.reverse() # last in, first out + + return script_tags + + +def ot_tag_to_script(tag): + """ Return the Unicode script code for the given OpenType script tag, or + None for "DFLT" tag or if there is no Unicode script associated with it. + Raises ValueError if the tag is invalid. + """ + tag = tostr(tag).strip() + if not tag or " " in tag or len(tag) > 4: + raise ValueError("invalid OpenType tag: %r" % tag) + + while len(tag) != 4: + tag += str(" ") # pad with spaces + + if tag == OTTags.DEFAULT_SCRIPT: + # it's unclear which Unicode script the "DFLT" OpenType tag maps to, + # so here we return None + return None + + if tag in OTTags.NEW_SCRIPT_TAGS_REVERSED: + return OTTags.NEW_SCRIPT_TAGS_REVERSED[tag] + + # This side of the conversion is fully algorithmic + + # Any spaces at the end of the tag are replaced by repeating the last + # letter. Eg 'nko ' -> 'Nkoo'. + # Change first char to uppercase + script_code = tag[0].upper() + tag[1] + for i in range(2, 4): + script_code += (script_code[i-1] if tag[i] == " " else tag[i]) + + if script_code not in Scripts.NAMES: + return None + return script_code diff --git a/Lib/fontTools/varLib/__init__.py b/Lib/fontTools/varLib/__init__.py new file mode 100644 index 0000000..5f0f9bd --- /dev/null +++ b/Lib/fontTools/varLib/__init__.py @@ -0,0 +1,869 @@ +""" +Module for dealing with 'gvar'-style font variations, also known as run-time +interpolation. + +The ideas here are very similar to MutatorMath. There is even code to read +MutatorMath .designspace files in the varLib.designspace module. + +For now, if you run this file on a designspace file, it tries to find +ttf-interpolatable files for the masters and build a variable-font from +them. Such ttf-interpolatable and designspace files can be generated from +a Glyphs source, eg., using noto-source as an example: + + $ fontmake -o ttf-interpolatable -g NotoSansArabic-MM.glyphs + +Then you can make a variable-font this way: + + $ fonttools varLib master_ufo/NotoSansArabic.designspace + +API *will* change in near future. +""" +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import otRound +from fontTools.misc.arrayTools import Vector +from fontTools.ttLib import TTFont, newTable +from fontTools.ttLib.tables._n_a_m_e import NameRecord +from fontTools.ttLib.tables._f_v_a_r import Axis, NamedInstance +from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates +from fontTools.ttLib.tables.ttProgram import Program +from fontTools.ttLib.tables.TupleVariation import TupleVariation +from fontTools.ttLib.tables import otTables as ot +from fontTools.ttLib.tables.otBase import OTTableWriter +from fontTools.varLib import builder, designspace, models, varStore +from fontTools.varLib.merger import VariationMerger, _all_equal +from fontTools.varLib.mvar import MVAR_ENTRIES +from fontTools.varLib.iup import iup_delta_optimize +from collections import OrderedDict +import os.path +import logging +from pprint import pformat + +log = logging.getLogger("fontTools.varLib") + + +class VarLibError(Exception): + pass + +# +# Creation routines +# + +def _add_fvar(font, axes, instances): + """ + Add 'fvar' table to font. + + axes is an ordered dictionary of DesignspaceAxis objects. + + instances is list of dictionary objects with 'location', 'stylename', + and possibly 'postscriptfontname' entries. + """ + + assert axes + assert isinstance(axes, OrderedDict) + + log.info("Generating fvar") + + fvar = newTable('fvar') + nameTable = font['name'] + + for a in axes.values(): + axis = Axis() + axis.axisTag = Tag(a.tag) + # TODO Skip axes that have no variation. + axis.minValue, axis.defaultValue, axis.maxValue = a.minimum, a.default, a.maximum + axis.axisNameID = nameTable.addName(tounicode(a.labelname['en'])) + # TODO: + # Replace previous line with the following when the following issues are resolved: + # https://github.com/fonttools/fonttools/issues/930 + # https://github.com/fonttools/fonttools/issues/931 + # axis.axisNameID = nameTable.addMultilingualName(a.labelname, font) + fvar.axes.append(axis) + + for instance in instances: + coordinates = instance['location'] + name = tounicode(instance['stylename']) + psname = instance.get('postscriptfontname') + + inst = NamedInstance() + inst.subfamilyNameID = nameTable.addName(name) + if psname is not None: + psname = tounicode(psname) + inst.postscriptNameID = nameTable.addName(psname) + inst.coordinates = {axes[k].tag:axes[k].map_backward(v) for k,v in coordinates.items()} + #inst.coordinates = {axes[k].tag:v for k,v in coordinates.items()} + fvar.instances.append(inst) + + assert "fvar" not in font + font['fvar'] = fvar + + return fvar + +def _add_avar(font, axes): + """ + Add 'avar' table to font. + + axes is an ordered dictionary of DesignspaceAxis objects. + """ + + assert axes + assert isinstance(axes, OrderedDict) + + log.info("Generating avar") + + avar = newTable('avar') + + interesting = False + for axis in axes.values(): + # Currently, some rasterizers require that the default value maps + # (-1 to -1, 0 to 0, and 1 to 1) be present for all the segment + # maps, even when the default normalization mapping for the axis + # was not modified. + # https://github.com/googlei18n/fontmake/issues/295 + # https://github.com/fonttools/fonttools/issues/1011 + # TODO(anthrotype) revert this (and 19c4b37) when issue is fixed + curve = avar.segments[axis.tag] = {-1.0: -1.0, 0.0: 0.0, 1.0: 1.0} + if not axis.map: + continue + + items = sorted(axis.map.items()) + keys = [item[0] for item in items] + vals = [item[1] for item in items] + + # Current avar requirements. We don't have to enforce + # these on the designer and can deduce some ourselves, + # but for now just enforce them. + assert axis.minimum == min(keys) + assert axis.maximum == max(keys) + assert axis.default in keys + # No duplicates + assert len(set(keys)) == len(keys) + assert len(set(vals)) == len(vals) + # Ascending values + assert sorted(vals) == vals + + keys_triple = (axis.minimum, axis.default, axis.maximum) + vals_triple = tuple(axis.map_forward(v) for v in keys_triple) + + keys = [models.normalizeValue(v, keys_triple) for v in keys] + vals = [models.normalizeValue(v, vals_triple) for v in vals] + + if all(k == v for k, v in zip(keys, vals)): + continue + interesting = True + + curve.update(zip(keys, vals)) + + assert 0.0 in curve and curve[0.0] == 0.0 + assert -1.0 not in curve or curve[-1.0] == -1.0 + assert +1.0 not in curve or curve[+1.0] == +1.0 + # curve.update({-1.0: -1.0, 0.0: 0.0, 1.0: 1.0}) + + assert "avar" not in font + if not interesting: + log.info("No need for avar") + avar = None + else: + font['avar'] = avar + + return avar + +def _add_stat(font, axes): + # for now we just get the axis tags and nameIDs from the fvar, + # so we can reuse the same nameIDs which were defined in there. + # TODO make use of 'axes' once it adds style attributes info: + # https://github.com/LettError/designSpaceDocument/issues/8 + + if "STAT" in font: + return + + fvarTable = font['fvar'] + + STAT = font["STAT"] = newTable('STAT') + stat = STAT.table = ot.STAT() + stat.Version = 0x00010002 + + axisRecords = [] + for i, a in enumerate(fvarTable.axes): + axis = ot.AxisRecord() + axis.AxisTag = Tag(a.axisTag) + axis.AxisNameID = a.axisNameID + axis.AxisOrdering = i + axisRecords.append(axis) + + axisRecordArray = ot.AxisRecordArray() + axisRecordArray.Axis = axisRecords + # XXX these should not be hard-coded but computed automatically + stat.DesignAxisRecordSize = 8 + stat.DesignAxisCount = len(axisRecords) + stat.DesignAxisRecord = axisRecordArray + + # for the elided fallback name, we default to the base style name. + # TODO make this user-configurable via designspace document + stat.ElidedFallbackNameID = 2 + +# TODO Move to glyf or gvar table proper +def _GetCoordinates(font, glyphName): + """font, glyphName --> glyph coordinates as expected by "gvar" table + + The result includes four "phantom points" for the glyph metrics, + as mandated by the "gvar" spec. + """ + glyf = font["glyf"] + if glyphName not in glyf.glyphs: return None + glyph = glyf[glyphName] + if glyph.isComposite(): + coord = GlyphCoordinates([(getattr(c, 'x', 0),getattr(c, 'y', 0)) for c in glyph.components]) + control = (glyph.numberOfContours,[c.glyphName for c in glyph.components]) + else: + allData = glyph.getCoordinates(glyf) + coord = allData[0] + control = (glyph.numberOfContours,)+allData[1:] + + # Add phantom points for (left, right, top, bottom) positions. + horizontalAdvanceWidth, leftSideBearing = font["hmtx"].metrics[glyphName] + if not hasattr(glyph, 'xMin'): + glyph.recalcBounds(glyf) + leftSideX = glyph.xMin - leftSideBearing + rightSideX = leftSideX + horizontalAdvanceWidth + # XXX these are incorrect. Load vmtx and fix. + topSideY = glyph.yMax + bottomSideY = -glyph.yMin + coord = coord.copy() + coord.extend([(leftSideX, 0), + (rightSideX, 0), + (0, topSideY), + (0, bottomSideY)]) + + return coord, control + +# TODO Move to glyf or gvar table proper +def _SetCoordinates(font, glyphName, coord): + glyf = font["glyf"] + assert glyphName in glyf.glyphs + glyph = glyf[glyphName] + + # Handle phantom points for (left, right, top, bottom) positions. + assert len(coord) >= 4 + if not hasattr(glyph, 'xMin'): + glyph.recalcBounds(glyf) + leftSideX = coord[-4][0] + rightSideX = coord[-3][0] + topSideY = coord[-2][1] + bottomSideY = coord[-1][1] + + for _ in range(4): + del coord[-1] + + if glyph.isComposite(): + assert len(coord) == len(glyph.components) + for p,comp in zip(coord, glyph.components): + if hasattr(comp, 'x'): + comp.x,comp.y = p + elif glyph.numberOfContours is 0: + assert len(coord) == 0 + else: + assert len(coord) == len(glyph.coordinates) + glyph.coordinates = coord + + glyph.recalcBounds(glyf) + + horizontalAdvanceWidth = otRound(rightSideX - leftSideX) + if horizontalAdvanceWidth < 0: + # unlikely, but it can happen, see: + # https://github.com/fonttools/fonttools/pull/1198 + horizontalAdvanceWidth = 0 + leftSideBearing = otRound(glyph.xMin - leftSideX) + # XXX Handle vertical + font["hmtx"].metrics[glyphName] = horizontalAdvanceWidth, leftSideBearing + +def _add_gvar(font, model, master_ttfs, tolerance=0.5, optimize=True): + + assert tolerance >= 0 + + log.info("Generating gvar") + assert "gvar" not in font + gvar = font["gvar"] = newTable('gvar') + gvar.version = 1 + gvar.reserved = 0 + gvar.variations = {} + + for glyph in font.getGlyphOrder(): + + allData = [_GetCoordinates(m, glyph) for m in master_ttfs] + allCoords = [d[0] for d in allData] + allControls = [d[1] for d in allData] + control = allControls[0] + if (any(c != control for c in allControls)): + log.warning("glyph %s has incompatible masters; skipping" % glyph) + continue + del allControls + + # Update gvar + gvar.variations[glyph] = [] + deltas = model.getDeltas(allCoords) + supports = model.supports + assert len(deltas) == len(supports) + + # Prepare for IUP optimization + origCoords = deltas[0] + endPts = control[1] if control[0] >= 1 else list(range(len(control[1]))) + + for i,(delta,support) in enumerate(zip(deltas[1:], supports[1:])): + if all(abs(v) <= tolerance for v in delta.array): + continue + var = TupleVariation(support, delta) + if optimize: + delta_opt = iup_delta_optimize(delta, origCoords, endPts, tolerance=tolerance) + + if None in delta_opt: + # Use "optimized" version only if smaller... + var_opt = TupleVariation(support, delta_opt) + + axis_tags = sorted(support.keys()) # Shouldn't matter that this is different from fvar...? + tupleData, auxData, _ = var.compile(axis_tags, [], None) + unoptimized_len = len(tupleData) + len(auxData) + tupleData, auxData, _ = var_opt.compile(axis_tags, [], None) + optimized_len = len(tupleData) + len(auxData) + + if optimized_len < unoptimized_len: + var = var_opt + + gvar.variations[glyph].append(var) + +def _remove_TTHinting(font): + for tag in ("cvar", "cvt ", "fpgm", "prep"): + if tag in font: + del font[tag] + for attr in ("maxTwilightPoints", "maxStorage", "maxFunctionDefs", "maxInstructionDefs", "maxStackElements", "maxSizeOfInstructions"): + setattr(font["maxp"], attr, 0) + font["maxp"].maxZones = 1 + font["glyf"].removeHinting() + # TODO: Modify gasp table to deactivate gridfitting for all ranges? + +def _merge_TTHinting(font, model, master_ttfs, tolerance=0.5): + + log.info("Merging TT hinting") + assert "cvar" not in font + + # Check that the existing hinting is compatible + + # fpgm and prep table + + for tag in ("fpgm", "prep"): + all_pgms = [m[tag].program for m in master_ttfs if tag in m] + if len(all_pgms) == 0: + continue + if tag in font: + font_pgm = font[tag].program + else: + font_pgm = Program() + if any(pgm != font_pgm for pgm in all_pgms): + log.warning("Masters have incompatible %s tables, hinting is discarded." % tag) + _remove_TTHinting(font) + return + + # glyf table + + for name, glyph in font["glyf"].glyphs.items(): + all_pgms = [ + m["glyf"][name].program + for m in master_ttfs + if hasattr(m["glyf"][name], "program") + ] + if not any(all_pgms): + continue + glyph.expand(font["glyf"]) + if hasattr(glyph, "program"): + font_pgm = glyph.program + else: + font_pgm = Program() + if any(pgm != font_pgm for pgm in all_pgms if pgm): + log.warning("Masters have incompatible glyph programs in glyph '%s', hinting is discarded." % name) + _remove_TTHinting(font) + return + + # cvt table + + all_cvs = [Vector(m["cvt "].values) for m in master_ttfs if "cvt " in m] + + if len(all_cvs) == 0: + # There is no cvt table to make a cvar table from, we're done here. + return + + if len(all_cvs) != len(master_ttfs): + log.warning("Some masters have no cvt table, hinting is discarded.") + _remove_TTHinting(font) + return + + num_cvt0 = len(all_cvs[0]) + if (any(len(c) != num_cvt0 for c in all_cvs)): + log.warning("Masters have incompatible cvt tables, hinting is discarded.") + _remove_TTHinting(font) + return + + # We can build the cvar table now. + + cvar = font["cvar"] = newTable('cvar') + cvar.version = 1 + cvar.variations = [] + + deltas = model.getDeltas(all_cvs) + supports = model.supports + for i,(delta,support) in enumerate(zip(deltas[1:], supports[1:])): + delta = [otRound(d) for d in delta] + if all(abs(v) <= tolerance for v in delta): + continue + var = TupleVariation(support, delta) + cvar.variations.append(var) + +def _add_HVAR(font, model, master_ttfs, axisTags): + + log.info("Generating HVAR") + + hAdvanceDeltas = {} + metricses = [m["hmtx"].metrics for m in master_ttfs] + for glyph in font.getGlyphOrder(): + hAdvances = [metrics[glyph][0] for metrics in metricses] + # TODO move round somewhere else? + hAdvanceDeltas[glyph] = tuple(otRound(d) for d in model.getDeltas(hAdvances)[1:]) + + # Direct mapping + supports = model.supports[1:] + varTupleList = builder.buildVarRegionList(supports, axisTags) + varTupleIndexes = list(range(len(supports))) + n = len(supports) + items = [] + for glyphName in font.getGlyphOrder(): + items.append(hAdvanceDeltas[glyphName]) + + # Build indirect mapping to save on duplicates, compare both sizes + uniq = list(set(items)) + mapper = {v:i for i,v in enumerate(uniq)} + mapping = [mapper[item] for item in items] + advanceMapping = builder.buildVarIdxMap(mapping, font.getGlyphOrder()) + + # Direct + varData = builder.buildVarData(varTupleIndexes, items) + directStore = builder.buildVarStore(varTupleList, [varData]) + + # Indirect + varData = builder.buildVarData(varTupleIndexes, uniq) + indirectStore = builder.buildVarStore(varTupleList, [varData]) + mapping = indirectStore.optimize() + advanceMapping.mapping = {k:mapping[v] for k,v in advanceMapping.mapping.items()} + + # Compile both, see which is more compact + + writer = OTTableWriter() + directStore.compile(writer, font) + directSize = len(writer.getAllData()) + + writer = OTTableWriter() + indirectStore.compile(writer, font) + advanceMapping.compile(writer, font) + indirectSize = len(writer.getAllData()) + + use_direct = directSize < indirectSize + + # Done; put it all together. + assert "HVAR" not in font + HVAR = font["HVAR"] = newTable('HVAR') + hvar = HVAR.table = ot.HVAR() + hvar.Version = 0x00010000 + hvar.LsbMap = hvar.RsbMap = None + if use_direct: + hvar.VarStore = directStore + hvar.AdvWidthMap = None + else: + hvar.VarStore = indirectStore + hvar.AdvWidthMap = advanceMapping + +def _add_MVAR(font, model, master_ttfs, axisTags): + + log.info("Generating MVAR") + + store_builder = varStore.OnlineVarStoreBuilder(axisTags) + store_builder.setModel(model) + + records = [] + lastTableTag = None + fontTable = None + tables = None + for tag, (tableTag, itemName) in sorted(MVAR_ENTRIES.items(), key=lambda kv: kv[1]): + if tableTag != lastTableTag: + tables = fontTable = None + if tableTag in font: + # TODO Check all masters have same table set? + fontTable = font[tableTag] + tables = [master[tableTag] for master in master_ttfs] + lastTableTag = tableTag + if tables is None: + continue + + # TODO support gasp entries + + master_values = [getattr(table, itemName) for table in tables] + if _all_equal(master_values): + base, varIdx = master_values[0], None + else: + base, varIdx = store_builder.storeMasters(master_values) + setattr(fontTable, itemName, base) + + if varIdx is None: + continue + log.info(' %s: %s.%s %s', tag, tableTag, itemName, master_values) + rec = ot.MetricsValueRecord() + rec.ValueTag = tag + rec.VarIdx = varIdx + records.append(rec) + + assert "MVAR" not in font + if records: + store = store_builder.finish() + # Optimize + mapping = store.optimize() + for rec in records: + rec.VarIdx = mapping[rec.VarIdx] + + MVAR = font["MVAR"] = newTable('MVAR') + mvar = MVAR.table = ot.MVAR() + mvar.Version = 0x00010000 + mvar.Reserved = 0 + mvar.VarStore = store + # XXX these should not be hard-coded but computed automatically + mvar.ValueRecordSize = 8 + mvar.ValueRecordCount = len(records) + mvar.ValueRecord = sorted(records, key=lambda r: r.ValueTag) + + +def _merge_OTL(font, model, master_fonts, axisTags): + + log.info("Merging OpenType Layout tables") + merger = VariationMerger(model, axisTags, font) + + merger.mergeTables(font, master_fonts, ['GPOS']) + # TODO Merge GSUB + # TODO Merge GDEF itself! + store = merger.store_builder.finish() + if not store.VarData: + return + try: + GDEF = font['GDEF'].table + assert GDEF.Version <= 0x00010002 + except KeyError: + font['GDEF']= newTable('GDEF') + GDEFTable = font["GDEF"] = newTable('GDEF') + GDEF = GDEFTable.table = ot.GDEF() + GDEF.Version = 0x00010003 + GDEF.VarStore = store + + # Optimize + varidx_map = store.optimize() + GDEF.remap_device_varidxes(varidx_map) + if 'GPOS' in font: + font['GPOS'].table.remap_device_varidxes(varidx_map) + + + +# Pretty much all of this file should be redesigned and moved inot submodules... +# Such a mess right now, but kludging along... +class _DesignspaceAxis(object): + + def __repr__(self): + return repr(self.__dict__) + + @staticmethod + def _map(v, map): + keys = map.keys() + if not keys: + return v + if v in keys: + return map[v] + k = min(keys) + if v < k: + return v + map[k] - k + k = max(keys) + if v > k: + return v + map[k] - k + # Interpolate + a = max(k for k in keys if k < v) + b = min(k for k in keys if k > v) + va = map[a] + vb = map[b] + return va + (vb - va) * (v - a) / (b - a) + + def map_forward(self, v): + if self.map is None: return v + return self._map(v, self.map) + + def map_backward(self, v): + if self.map is None: return v + map = {v:k for k,v in self.map.items()} + return self._map(v, map) + + +def load_designspace(designspace_filename): + + ds = designspace.load(designspace_filename) + axes = ds.get('axes') + masters = ds.get('sources') + if not masters: + raise VarLibError("no sources found in .designspace") + instances = ds.get('instances', []) + + standard_axis_map = OrderedDict([ + ('weight', ('wght', {'en':'Weight'})), + ('width', ('wdth', {'en':'Width'})), + ('slant', ('slnt', {'en':'Slant'})), + ('optical', ('opsz', {'en':'Optical Size'})), + ]) + + + # Setup axes + axis_objects = OrderedDict() + if axes is not None: + for axis_dict in axes: + axis_name = axis_dict.get('name') + if not axis_name: + axis_name = axis_dict['name'] = axis_dict['tag'] + if 'map' not in axis_dict: + axis_dict['map'] = None + else: + axis_dict['map'] = {m['input']:m['output'] for m in axis_dict['map']} + + if axis_name in standard_axis_map: + if 'tag' not in axis_dict: + axis_dict['tag'] = standard_axis_map[axis_name][0] + if 'labelname' not in axis_dict: + axis_dict['labelname'] = standard_axis_map[axis_name][1].copy() + + axis = _DesignspaceAxis() + for item in ['name', 'tag', 'minimum', 'default', 'maximum', 'map']: + assert item in axis_dict, 'Axis does not have "%s"' % item + if 'labelname' not in axis_dict: + axis_dict['labelname'] = {'en': axis_name} + axis.__dict__ = axis_dict + axis_objects[axis_name] = axis + else: + # No <axes> element. Guess things... + base_idx = None + for i,m in enumerate(masters): + if 'info' in m and m['info']['copy']: + assert base_idx is None + base_idx = i + assert base_idx is not None, "Cannot find 'base' master; Either add <axes> element to .designspace document, or add <info> element to one of the sources in the .designspace document." + + master_locs = [o['location'] for o in masters] + base_loc = master_locs[base_idx] + axis_names = set(base_loc.keys()) + assert all(name in standard_axis_map for name in axis_names), "Non-standard axis found and there exist no <axes> element." + + for name,(tag,labelname) in standard_axis_map.items(): + if name not in axis_names: + continue + + axis = _DesignspaceAxis() + axis.name = name + axis.tag = tag + axis.labelname = labelname.copy() + axis.default = base_loc[name] + axis.minimum = min(m[name] for m in master_locs if name in m) + axis.maximum = max(m[name] for m in master_locs if name in m) + axis.map = None + # TODO Fill in weight / width mapping from OS/2 table? Need loading fonts... + axis_objects[name] = axis + del base_idx, base_loc, axis_names, master_locs + axes = axis_objects + del axis_objects + log.info("Axes:\n%s", pformat(axes)) + + + # Check all master and instance locations are valid and fill in defaults + for obj in masters+instances: + obj_name = obj.get('name', obj.get('stylename', '')) + loc = obj['location'] + for axis_name in loc.keys(): + assert axis_name in axes, "Location axis '%s' unknown for '%s'." % (axis_name, obj_name) + for axis_name,axis in axes.items(): + if axis_name not in loc: + loc[axis_name] = axis.default + else: + v = axis.map_backward(loc[axis_name]) + assert axis.minimum <= v <= axis.maximum, "Location for axis '%s' (mapped to %s) out of range for '%s' [%s..%s]" % (axis_name, v, obj_name, axis.minimum, axis.maximum) + + + # Normalize master locations + + internal_master_locs = [o['location'] for o in masters] + log.info("Internal master locations:\n%s", pformat(internal_master_locs)) + + # TODO This mapping should ideally be moved closer to logic in _add_fvar/avar + internal_axis_supports = {} + for axis in axes.values(): + triple = (axis.minimum, axis.default, axis.maximum) + internal_axis_supports[axis.name] = [axis.map_forward(v) for v in triple] + log.info("Internal axis supports:\n%s", pformat(internal_axis_supports)) + + normalized_master_locs = [models.normalizeLocation(m, internal_axis_supports) for m in internal_master_locs] + log.info("Normalized master locations:\n%s", pformat(normalized_master_locs)) + + + # Find base master + base_idx = None + for i,m in enumerate(normalized_master_locs): + if all(v == 0 for v in m.values()): + assert base_idx is None + base_idx = i + assert base_idx is not None, "Base master not found; no master at default location?" + log.info("Index of base master: %s", base_idx) + + return axes, internal_axis_supports, base_idx, normalized_master_locs, masters, instances + + +def build(designspace_filename, master_finder=lambda s:s, exclude=[], optimize=True): + """ + Build variation font from a designspace file. + + If master_finder is set, it should be a callable that takes master + filename as found in designspace file and map it to master font + binary as to be opened (eg. .ttf or .otf). + """ + + axes, internal_axis_supports, base_idx, normalized_master_locs, masters, instances = load_designspace(designspace_filename) + + log.info("Building variable font") + log.info("Loading master fonts") + basedir = os.path.dirname(designspace_filename) + master_ttfs = [master_finder(os.path.join(basedir, m['filename'])) for m in masters] + master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs] + # Reload base font as target font + vf = TTFont(master_ttfs[base_idx]) + + # TODO append masters as named-instances as well; needs .designspace change. + fvar = _add_fvar(vf, axes, instances) + if 'STAT' not in exclude: + _add_stat(vf, axes) + if 'avar' not in exclude: + _add_avar(vf, axes) + del instances + + # Map from axis names to axis tags... + normalized_master_locs = [{axes[k].tag:v for k,v in loc.items()} for loc in normalized_master_locs] + #del axes + # From here on, we use fvar axes only + axisTags = [axis.axisTag for axis in fvar.axes] + + # Assume single-model for now. + model = models.VariationModel(normalized_master_locs, axisOrder=axisTags) + assert 0 == model.mapping[base_idx] + + log.info("Building variations tables") + if 'MVAR' not in exclude: + _add_MVAR(vf, model, master_fonts, axisTags) + if 'HVAR' not in exclude: + _add_HVAR(vf, model, master_fonts, axisTags) + if 'GDEF' not in exclude or 'GPOS' not in exclude: + _merge_OTL(vf, model, master_fonts, axisTags) + if 'gvar' not in exclude and 'glyf' in vf: + _add_gvar(vf, model, master_fonts, optimize=optimize) + if 'cvar' not in exclude and 'glyf' in vf: + _merge_TTHinting(vf, model, master_fonts) + + for tag in exclude: + if tag in vf: + del vf[tag] + + return vf, model, master_ttfs + + +class MasterFinder(object): + + def __init__(self, template): + self.template = template + + def __call__(self, src_path): + fullname = os.path.abspath(src_path) + dirname, basename = os.path.split(fullname) + stem, ext = os.path.splitext(basename) + path = self.template.format( + fullname=fullname, + dirname=dirname, + basename=basename, + stem=stem, + ext=ext, + ) + return os.path.normpath(path) + + +def main(args=None): + from argparse import ArgumentParser + from fontTools import configLogger + + parser = ArgumentParser(prog='varLib') + parser.add_argument('designspace') + parser.add_argument( + '-o', + metavar='OUTPUTFILE', + dest='outfile', + default=None, + help='output file' + ) + parser.add_argument( + '-x', + metavar='TAG', + dest='exclude', + action='append', + default=[], + help='exclude table' + ) + parser.add_argument( + '--disable-iup', + dest='optimize', + action='store_false', + help='do not perform IUP optimization' + ) + parser.add_argument( + '--master-finder', + default='master_ttf_interpolatable/{stem}.ttf', + help=( + 'templated string used for finding binary font ' + 'files given the source file names defined in the ' + 'designspace document. The following special strings ' + 'are defined: {fullname} is the absolute source file ' + 'name; {basename} is the file name without its ' + 'directory; {stem} is the basename without the file ' + 'extension; {ext} is the source file extension; ' + '{dirname} is the directory of the absolute file ' + 'name. The default value is "%(default)s".' + ) + ) + options = parser.parse_args(args) + + # TODO: allow user to configure logging via command-line options + configLogger(level="INFO") + + designspace_filename = options.designspace + finder = MasterFinder(options.master_finder) + outfile = options.outfile + if outfile is None: + outfile = os.path.splitext(designspace_filename)[0] + '-VF.ttf' + + vf, model, master_ttfs = build( + designspace_filename, + finder, + exclude=options.exclude, + optimize=options.optimize + ) + + log.info("Saving variation font %s", outfile) + vf.save(outfile) + + +if __name__ == "__main__": + import sys + if len(sys.argv) > 1: + sys.exit(main()) + import doctest + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/varLib/__main__.py b/Lib/fontTools/varLib/__main__.py new file mode 100644 index 0000000..5cf05e5 --- /dev/null +++ b/Lib/fontTools/varLib/__main__.py @@ -0,0 +1,7 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +import sys +from fontTools.varLib import main + +if __name__ == '__main__': + sys.exit(main()) diff --git a/Lib/fontTools/varLib/builder.py b/Lib/fontTools/varLib/builder.py new file mode 100644 index 0000000..90e3337 --- /dev/null +++ b/Lib/fontTools/varLib/builder.py @@ -0,0 +1,101 @@ +from __future__ import print_function, division, absolute_import +from fontTools import ttLib +from fontTools.ttLib.tables import otTables as ot + +# VariationStore + +def buildVarRegionAxis(axisSupport): + self = ot.VarRegionAxis() + self.StartCoord, self.PeakCoord, self.EndCoord = [float(v) for v in axisSupport] + return self + +def buildVarRegion(support, axisTags): + assert all(tag in axisTags for tag in support.keys()), ("Unknown axis tag found.", support, axisTags) + self = ot.VarRegion() + self.VarRegionAxis = [] + for tag in axisTags: + self.VarRegionAxis.append(buildVarRegionAxis(support.get(tag, (0,0,0)))) + self.VarRegionAxisCount = len(self.VarRegionAxis) + return self + +def buildVarRegionList(supports, axisTags): + self = ot.VarRegionList() + self.RegionAxisCount = len(axisTags) + self.Region = [] + for support in supports: + self.Region.append(buildVarRegion(support, axisTags)) + self.RegionCount = len(self.Region) + return self + + +def _reorderItem(lst, narrows, zeroes): + out = [] + count = len(lst) + for i in range(count): + if i not in narrows: + out.append(lst[i]) + for i in range(count): + if i in narrows and i not in zeroes: + out.append(lst[i]) + return out + +def VarData_CalculateNumShorts(self, optimize=True): + count = self.VarRegionCount + items = self.Item + narrows = set(range(count)) + zeroes = set(range(count)) + for item in items: + wides = [i for i in narrows if not (-128 <= item[i] <= 127)] + narrows.difference_update(wides) + nonzeroes = [i for i in zeroes if item[i]] + zeroes.difference_update(nonzeroes) + if not narrows and not zeroes: + break + if optimize: + # Reorder columns such that all SHORT columns come before UINT8 + self.VarRegionIndex = _reorderItem(self.VarRegionIndex, narrows, zeroes) + self.VarRegionCount = len(self.VarRegionIndex) + for i in range(self.ItemCount): + items[i] = _reorderItem(items[i], narrows, zeroes) + self.NumShorts = count - len(narrows) + else: + wides = set(range(count)) - narrows + self.NumShorts = 1+max(wides) if wides else 0 + return self + +def buildVarData(varRegionIndices, items, optimize=True): + self = ot.VarData() + self.VarRegionIndex = list(varRegionIndices) + regionCount = self.VarRegionCount = len(self.VarRegionIndex) + records = self.Item = [] + if items: + for item in items: + assert len(item) == regionCount + records.append(list(item)) + self.ItemCount = len(self.Item) + VarData_CalculateNumShorts(self, optimize=optimize) + return self + + +def buildVarStore(varRegionList, varDataList): + self = ot.VarStore() + self.Format = 1 + self.VarRegionList = varRegionList + self.VarData = list(varDataList) + self.VarDataCount = len(self.VarData) + return self + + +# Variation helpers + +def buildVarIdxMap(varIdxes, glyphOrder): + self = ot.VarIdxMap() + self.mapping = {g:v for g,v in zip(glyphOrder, varIdxes)} + return self + +def buildVarDevTable(varIdx): + self = ot.Device() + self.DeltaFormat = 0x8000 + self.StartSize = varIdx >> 16 + self.EndSize = varIdx & 0xFFFF + return self diff --git a/Lib/fontTools/varLib/designspace.py b/Lib/fontTools/varLib/designspace.py new file mode 100644 index 0000000..7f235af --- /dev/null +++ b/Lib/fontTools/varLib/designspace.py @@ -0,0 +1,113 @@ +"""Rudimentary support for loading MutatorMath .designspace files.""" +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +try: + import xml.etree.cElementTree as ET +except ImportError: + import xml.etree.ElementTree as ET + +__all__ = ['load', 'loads'] + +namespaces = {'xml': '{http://www.w3.org/XML/1998/namespace}'} + + +def _xml_parse_location(et): + loc = {} + for dim in et.find('location'): + assert dim.tag == 'dimension' + name = dim.attrib['name'] + value = float(dim.attrib['xvalue']) + assert name not in loc + loc[name] = value + return loc + + +def _load_item(et): + item = dict(et.attrib) + for element in et: + if element.tag == 'location': + value = _xml_parse_location(et) + else: + value = {} + if 'copy' in element.attrib: + value['copy'] = bool(int(element.attrib['copy'])) + # TODO load more?! + item[element.tag] = value + return item + + +def _xml_parse_axis_or_map(element): + dic = {} + for name in element.attrib: + if name in ['name', 'tag']: + dic[name] = element.attrib[name] + else: + dic[name] = float(element.attrib[name]) + return dic + + +def _load_axis(et): + item = _xml_parse_axis_or_map(et) + maps = [] + labelnames = {} + for element in et: + assert element.tag in ['labelname', 'map'] + if element.tag == 'labelname': + lang = element.attrib["{0}lang".format(namespaces['xml'])] + labelnames[lang] = element.text + elif element.tag == 'map': + maps.append(_xml_parse_axis_or_map(element)) + if labelnames: + item['labelname'] = labelnames + if maps: + item['map'] = maps + return item + + +def _load(et): + designspace = {} + ds = et.getroot() + + axes_element = ds.find('axes') + if axes_element is not None: + axes = [] + for et in axes_element: + axes.append(_load_axis(et)) + designspace['axes'] = axes + + sources_element = ds.find('sources') + if sources_element is not None: + sources = [] + for et in sources_element: + sources.append(_load_item(et)) + designspace['sources'] = sources + + instances_element = ds.find('instances') + if instances_element is not None: + instances = [] + for et in instances_element: + instances.append(_load_item(et)) + designspace['instances'] = instances + + return designspace + + +def load(filename): + """Load designspace from a file name or object. + Returns a dictionary containing three (optional) items: + - list of "axes" + - list of "sources" (aka masters) + - list of "instances" + """ + return _load(ET.parse(filename)) + + +def loads(string): + """Load designspace from a string.""" + return _load(ET.fromstring(string)) + +if __name__ == '__main__': + import sys + from pprint import pprint + for f in sys.argv[1:]: + pprint(load(f)) diff --git a/Lib/fontTools/varLib/featureVars.py b/Lib/fontTools/varLib/featureVars.py new file mode 100644 index 0000000..6033621 --- /dev/null +++ b/Lib/fontTools/varLib/featureVars.py @@ -0,0 +1,392 @@ +"""Module to build FeatureVariation tables: +https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#featurevariations-table + +NOTE: The API is experimental and subject to change. +""" +from __future__ import print_function, absolute_import, division + +from fontTools.ttLib import newTable +from fontTools.ttLib.tables import otTables as ot +from fontTools.otlLib.builder import buildLookup, buildSingleSubstSubtable +import itertools + + +def addFeatureVariations(font, conditionalSubstitutions): + """Add conditional substitutions to a Variable Font. + + The `conditionalSubstitutions` argument is a list of (Region, Substitutions) + tuples. + + A Region is a list of Spaces. A Space is a dict mapping axisTags to + (minValue, maxValue) tuples. Irrelevant axes may be omitted. + A Space represents a 'rectangular' subset of an N-dimensional design space. + A Region represents a more complex subset of an N-dimensional design space, + ie. the union of all the Spaces in the Region. + For efficiency, Spaces within a Region should ideally not overlap, but + functionality is not compromised if they do. + + The minimum and maximum values are expressed in normalized coordinates. + + A Substitution is a dict mapping source glyph names to substitute glyph names. + """ + + # Example: + # + # >>> f = TTFont(srcPath) + # >>> condSubst = [ + # ... # A list of (Region, Substitution) tuples. + # ... ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}), + # ... ([{"wdth": (0.5, 1.0)}], {"cent": "cent.rvrn"}), + # ... ] + # >>> addFeatureVariations(f, condSubst) + # >>> f.save(dstPath) + + # Since the FeatureVariations table will only ever match one rule at a time, + # we will make new rules for all possible combinations of our input, so we + # can indirectly support overlapping rules. + explodedConditionalSubstitutions = [] + for combination in iterAllCombinations(len(conditionalSubstitutions)): + regions = [] + lookups = [] + for index in combination: + regions.append(conditionalSubstitutions[index][0]) + lookups.append(conditionalSubstitutions[index][1]) + if not regions: + continue + intersection = regions[0] + for region in regions[1:]: + intersection = intersectRegions(intersection, region) + for space in intersection: + # Remove default values, so we don't generate redundant ConditionSets + space = cleanupSpace(space) + if space: + explodedConditionalSubstitutions.append((space, lookups)) + + addFeatureVariationsRaw(font, explodedConditionalSubstitutions) + + +def iterAllCombinations(numRules): + """Given a number of rules, yield all the combinations of indices, sorted + by decreasing length, so we get the most specialized rules first. + + >>> list(iterAllCombinations(0)) + [] + >>> list(iterAllCombinations(1)) + [(0,)] + >>> list(iterAllCombinations(2)) + [(0, 1), (0,), (1,)] + >>> list(iterAllCombinations(3)) + [(0, 1, 2), (0, 1), (0, 2), (1, 2), (0,), (1,), (2,)] + """ + indices = range(numRules) + for length in range(numRules, 0, -1): + for combinations in itertools.combinations(indices, length): + yield combinations + + +# +# Region and Space support +# +# Terminology: +# +# A 'Space' is a dict representing a "rectangular" bit of N-dimensional space. +# The keys in the dict are axis tags, the values are (minValue, maxValue) tuples. +# Missing dimensions (keys) are substituted by the default min and max values +# from the corresponding axes. +# +# A 'Region' is a list of Space dicts, representing the union of the Spaces, +# therefore representing a more complex subset of design space. +# + +def intersectRegions(region1, region2): + """Return the region intersecting `region1` and `region2`. + + >>> intersectRegions([], []) + [] + >>> intersectRegions([{'wdth': (0.0, 1.0)}], []) + [] + >>> expected = [{'wdth': (0.0, 1.0), 'wght': (-1.0, 0.0)}] + >>> expected == intersectRegions([{'wdth': (0.0, 1.0)}], [{'wght': (-1.0, 0.0)}]) + True + >>> expected = [{'wdth': (0.0, 1.0), 'wght': (-0.5, 0.0)}] + >>> expected == intersectRegions([{'wdth': (0.0, 1.0), 'wght': (-0.5, 0.5)}], [{'wght': (-1.0, 0.0)}]) + True + >>> intersectRegions( + ... [{'wdth': (0.0, 1.0), 'wght': (-0.5, 0.5)}], + ... [{'wdth': (-1.0, 0.0), 'wght': (-1.0, 0.0)}]) + [] + + """ + region = [] + for space1 in region1: + for space2 in region2: + space = intersectSpaces(space1, space2) + if space is not None: + region.append(space) + return region + + +def intersectSpaces(space1, space2): + """Return the space intersected by `space1` and `space2`, or None if there + is no intersection. + + >>> intersectSpaces({}, {}) + {} + >>> intersectSpaces({'wdth': (-0.5, 0.5)}, {}) + {'wdth': (-0.5, 0.5)} + >>> intersectSpaces({'wdth': (-0.5, 0.5)}, {'wdth': (0.0, 1.0)}) + {'wdth': (0.0, 0.5)} + >>> expected = {'wdth': (0.0, 0.5), 'wght': (0.25, 0.5)} + >>> expected == intersectSpaces({'wdth': (-0.5, 0.5), 'wght': (0.0, 0.5)}, {'wdth': (0.0, 1.0), 'wght': (0.25, 0.75)}) + True + >>> expected = {'wdth': (-0.5, 0.5), 'wght': (0.0, 1.0)} + >>> expected == intersectSpaces({'wdth': (-0.5, 0.5)}, {'wght': (0.0, 1.0)}) + True + >>> intersectSpaces({'wdth': (-0.5, 0)}, {'wdth': (0.1, 0.5)}) + + """ + space = {} + space.update(space1) + space.update(space2) + for axisTag in set(space1) & set(space2): + min1, max1 = space1[axisTag] + min2, max2 = space2[axisTag] + minimum = max(min1, min2) + maximum = min(max1, max2) + if not minimum < maximum: + return None + space[axisTag] = minimum, maximum + return space + + +def cleanupSpace(space): + """Return a sparse copy of `space`, without redundant (default) values. + + >>> cleanupSpace({}) + {} + >>> cleanupSpace({'wdth': (0.0, 1.0)}) + {'wdth': (0.0, 1.0)} + >>> cleanupSpace({'wdth': (-1.0, 1.0)}) + {} + + """ + return {tag: limit for tag, limit in space.items() if limit != (-1.0, 1.0)} + + +# +# Low level implementation +# + +def addFeatureVariationsRaw(font, conditionalSubstitutions): + """Low level implementation of addFeatureVariations that directly + models the possibilities of the FeatureVariations table.""" + + # + # assert there is no 'rvrn' feature + # make dummy 'rvrn' feature with no lookups + # sort features, get 'rvrn' feature index + # add 'rvrn' feature to all scripts + # make lookups + # add feature variations + # + + if "GSUB" not in font: + font["GSUB"] = buildGSUB() + + gsub = font["GSUB"].table + + if gsub.Version < 0x00010001: + gsub.Version = 0x00010001 # allow gsub.FeatureVariations + + gsub.FeatureVariations = None # delete any existing FeatureVariations + + for feature in gsub.FeatureList.FeatureRecord: + assert feature.FeatureTag != 'rvrn' + + rvrnFeature = buildFeatureRecord('rvrn', []) + gsub.FeatureList.FeatureRecord.append(rvrnFeature) + + sortFeatureList(gsub) + rvrnFeatureIndex = gsub.FeatureList.FeatureRecord.index(rvrnFeature) + + for scriptRecord in gsub.ScriptList.ScriptRecord: + for langSys in [scriptRecord.Script.DefaultLangSys] + scriptRecord.Script.LangSysRecord: + langSys.FeatureIndex.append(rvrnFeatureIndex) + + # setup lookups + + # turn substitution dicts into tuples of tuples, so they are hashable + conditionalSubstitutions, allSubstitutions = makeSubstitutionsHashable(conditionalSubstitutions) + + lookupMap = buildSubstitutionLookups(gsub, allSubstitutions) + + axisIndices = {axis.axisTag: axisIndex for axisIndex, axis in enumerate(font["fvar"].axes)} + + featureVariationRecords = [] + for conditionSet, substitutions in conditionalSubstitutions: + conditionTable = [] + for axisTag, (minValue, maxValue) in sorted(conditionSet.items()): + assert minValue < maxValue + ct = buildConditionTable(axisIndices[axisTag], minValue, maxValue) + conditionTable.append(ct) + + lookupIndices = [lookupMap[subst] for subst in substitutions] + record = buildFeatureTableSubstitutionRecord(rvrnFeatureIndex, lookupIndices) + featureVariationRecords.append(buildFeatureVariationRecord(conditionTable, [record])) + + gsub.FeatureVariations = buildFeatureVariations(featureVariationRecords) + + +# +# Building GSUB/FeatureVariations internals +# + +def buildGSUB(): + """Build a GSUB table from scratch.""" + fontTable = newTable("GSUB") + gsub = fontTable.table = ot.GSUB() + gsub.Version = 0x00010001 # allow gsub.FeatureVariations + + gsub.ScriptList = ot.ScriptList() + gsub.ScriptList.ScriptRecord = [] + gsub.FeatureList = ot.FeatureList() + gsub.FeatureList.FeatureRecord = [] + gsub.LookupList = ot.LookupList() + gsub.LookupList.Lookup = [] + + srec = ot.ScriptRecord() + srec.ScriptTag = 'DFLT' + srec.Script = ot.Script() + srec.Script.DefaultLangSys = None + srec.Script.LangSysRecord = [] + + langrec = ot.LangSysRecord() + langrec.LangSys = ot.LangSys() + langrec.LangSys.ReqFeatureIndex = 0xFFFF + langrec.LangSys.FeatureIndex = [0] + srec.Script.DefaultLangSys = langrec.LangSys + + gsub.ScriptList.ScriptRecord.append(srec) + gsub.FeatureVariations = None + + return fontTable + + +def makeSubstitutionsHashable(conditionalSubstitutions): + """Turn all the substitution dictionaries in sorted tuples of tuples so + they are hashable, to detect duplicates so we don't write out redundant + data.""" + allSubstitutions = set() + condSubst = [] + for conditionSet, substitutionMaps in conditionalSubstitutions: + substitutions = [] + for substitutionMap in substitutionMaps: + subst = tuple(sorted(substitutionMap.items())) + substitutions.append(subst) + allSubstitutions.add(subst) + condSubst.append((conditionSet, substitutions)) + return condSubst, sorted(allSubstitutions) + + +def buildSubstitutionLookups(gsub, allSubstitutions): + """Build the lookups for the glyph substitutions, return a dict mapping + the substitution to lookup indices.""" + firstIndex = len(gsub.LookupList.Lookup) + lookupMap = {} + for i, substitutionMap in enumerate(allSubstitutions): + lookupMap[substitutionMap] = i + firstIndex + + for subst in allSubstitutions: + substMap = dict(subst) + lookup = buildLookup([buildSingleSubstSubtable(substMap)]) + gsub.LookupList.Lookup.append(lookup) + assert gsub.LookupList.Lookup[lookupMap[subst]] is lookup + return lookupMap + + +def buildFeatureVariations(featureVariationRecords): + """Build the FeatureVariations subtable.""" + fv = ot.FeatureVariations() + fv.Version = 0x00010000 + fv.FeatureVariationRecord = featureVariationRecords + return fv + + +def buildFeatureRecord(featureTag, lookupListIndices): + """Build a FeatureRecord.""" + fr = ot.FeatureRecord() + fr.FeatureTag = featureTag + fr.Feature = ot.Feature() + fr.Feature.LookupListIndex = lookupListIndices + return fr + + +def buildFeatureVariationRecord(conditionTable, substitutionRecords): + """Build a FeatureVariationRecord.""" + fvr = ot.FeatureVariationRecord() + fvr.ConditionSet = ot.ConditionSet() + fvr.ConditionSet.ConditionTable = conditionTable + fvr.FeatureTableSubstitution = ot.FeatureTableSubstitution() + fvr.FeatureTableSubstitution.Version = 0x00010001 + fvr.FeatureTableSubstitution.SubstitutionRecord = substitutionRecords + return fvr + + +def buildFeatureTableSubstitutionRecord(featureIndex, lookupListIndices): + """Build a FeatureTableSubstitutionRecord.""" + ftsr = ot.FeatureTableSubstitutionRecord() + ftsr.FeatureIndex = featureIndex + ftsr.Feature = ot.Feature() + ftsr.Feature.LookupListIndex = lookupListIndices + return ftsr + + +def buildConditionTable(axisIndex, filterRangeMinValue, filterRangeMaxValue): + """Build a ConditionTable.""" + ct = ot.ConditionTable() + ct.Format = 1 + ct.AxisIndex = axisIndex + ct.FilterRangeMinValue = filterRangeMinValue + ct.FilterRangeMaxValue = filterRangeMaxValue + return ct + + +def sortFeatureList(table): + """Sort the feature list by feature tag, and remap the feature indices + elsewhere. This is needed after the feature list has been modified. + """ + # decorate, sort, undecorate, because we need to make an index remapping table + tagIndexFea = [(fea.FeatureTag, index, fea) for index, fea in enumerate(table.FeatureList.FeatureRecord)] + tagIndexFea.sort() + table.FeatureList.FeatureRecord = [fea for tag, index, fea in tagIndexFea] + featureRemap = dict(zip([index for tag, index, fea in tagIndexFea], range(len(tagIndexFea)))) + + # Remap the feature indices + remapFeatures(table, featureRemap) + + +def remapFeatures(table, featureRemap): + """Go through the scripts list, and remap feature indices.""" + for scriptIndex, script in enumerate(table.ScriptList.ScriptRecord): + defaultLangSys = script.Script.DefaultLangSys + if defaultLangSys is not None: + _remapLangSys(defaultLangSys, featureRemap) + for langSysRecordIndex, langSysRec in enumerate(script.Script.LangSysRecord): + langSys = langSysRec.LangSys + _remapLangSys(langSys, featureRemap) + + if hasattr(table, "FeatureVariations") and table.FeatureVariations is not None: + for fvr in table.FeatureVariations.FeatureVariationRecord: + for ftsr in fvr.FeatureTableSubstitution.SubstitutionRecord: + ftsr.FeatureIndex = featureRemap[ftsr.FeatureIndex] + + +def _remapLangSys(langSys, featureRemap): + if langSys.ReqFeatureIndex != 0xffff: + langSys.ReqFeatureIndex = featureRemap[langSys.ReqFeatureIndex] + langSys.FeatureIndex = [featureRemap[index] for index in langSys.FeatureIndex] + + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/Lib/fontTools/varLib/interpolatable.py b/Lib/fontTools/varLib/interpolatable.py new file mode 100644 index 0000000..d4feed2 --- /dev/null +++ b/Lib/fontTools/varLib/interpolatable.py @@ -0,0 +1,181 @@ +""" +Tool to find wrong contour order between different masters, and +other interpolatability (or lack thereof) issues. + +Call as: +$ fonttools varLib.interpolatable font1 font2 ... +""" + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * + +from fontTools.pens.basePen import AbstractPen, BasePen +from fontTools.pens.recordingPen import RecordingPen +from fontTools.pens.statisticsPen import StatisticsPen +import itertools + + +class PerContourPen(BasePen): + def __init__(self, Pen, glyphset=None): + BasePen.__init__(self, glyphset) + self._glyphset = glyphset + self._Pen = Pen + self._pen = None + self.value = [] + def _moveTo(self, p0): + self._newItem() + self._pen.moveTo(p0) + def _lineTo(self, p1): + self._pen.lineTo(p1) + def _qCurveToOne(self, p1, p2): + self._pen.qCurveTo(p1, p2) + def _curveToOne(self, p1, p2, p3): + self._pen.curveTo(p1, p2, p3) + def _closePath(self): + self._pen.closePath() + self._pen = None + def _endPath(self): + self._pen.endPath() + self._pen = None + + def _newItem(self): + self._pen = pen = self._Pen() + self.value.append(pen) + +class PerContourOrComponentPen(PerContourPen): + + def addComponent(self, glyphName, transformation): + self._newItem() + self.value[-1].addComponent(glyphName, transformation) + + +def _vdiff(v0, v1): + return tuple(b-a for a,b in zip(v0,v1)) +def _vlen(vec): + v = 0 + for x in vec: + v += x*x + return v + +def _matching_cost(G, matching): + return sum(G[i][j] for i,j in enumerate(matching)) + +def min_cost_perfect_bipartite_matching(G): + n = len(G) + try: + from scipy.optimize import linear_sum_assignment + rows, cols = linear_sum_assignment(G) + assert (rows == list(range(n))).all() + return list(cols), _matching_cost(G, cols) + except ImportError: + pass + + try: + from munkres import Munkres + cols = [None] * n + for row,col in Munkres().compute(G): + cols[row] = col + return cols, _matching_cost(G, cols) + except ImportError: + pass + + if n > 6: + raise Exception("Install Python module 'munkres' or 'scipy >= 0.17.0'") + + # Otherwise just brute-force + permutations = itertools.permutations(range(n)) + best = list(next(permutations)) + best_cost = _matching_cost(G, best) + for p in permutations: + cost = _matching_cost(G, p) + if cost < best_cost: + best, best_cost = list(p), cost + return best, best_cost + + +def test(glyphsets, glyphs=None, names=None): + + if names is None: + names = glyphsets + if glyphs is None: + glyphs = glyphsets[0].keys() + + hist = [] + for glyph_name in glyphs: + #print() + #print(glyph_name) + + try: + allVectors = [] + for glyphset,name in zip(glyphsets, names): + #print('.', end='') + glyph = glyphset[glyph_name] + + perContourPen = PerContourOrComponentPen(RecordingPen, glyphset=glyphset) + glyph.draw(perContourPen) + contourPens = perContourPen.value + del perContourPen + + contourVectors = [] + allVectors.append(contourVectors) + for contour in contourPens: + stats = StatisticsPen(glyphset=glyphset) + contour.replay(stats) + size = abs(stats.area) ** .5 * .5 + vector = ( + int(size), + int(stats.meanX), + int(stats.meanY), + int(stats.stddevX * 2), + int(stats.stddevY * 2), + int(stats.correlation * size), + ) + contourVectors.append(vector) + #print(vector) + + # Check each master against the next one in the list. + for i,(m0,m1) in enumerate(zip(allVectors[:-1],allVectors[1:])): + if len(m0) != len(m1): + print('%s: %s+%s: Glyphs not compatible!!!!!' % (glyph_name, names[i], names[i+1])) + continue + if not m0: + continue + costs = [[_vlen(_vdiff(v0,v1)) for v1 in m1] for v0 in m0] + matching, matching_cost = min_cost_perfect_bipartite_matching(costs) + if matching != list(range(len(m0))): + print('%s: %s+%s: Glyph has wrong contour/component order: %s' % (glyph_name, names[i], names[i+1], matching)) #, m0, m1) + break + upem = 2048 + item_cost = round((matching_cost / len(m0) / len(m0[0])) ** .5 / upem * 100) + hist.append(item_cost) + threshold = 7 + if item_cost >= threshold: + print('%s: %s+%s: Glyph has very high cost: %d%%' % (glyph_name, names[i], names[i+1], item_cost)) + + + except ValueError as e: + print('%s: %s: math error %s; skipping glyph.' % (glyph_name, name, e)) + print(contour.value) + #raise + #for x in hist: + # print(x) + +def main(args): + filenames = args + glyphs = None + #glyphs = ['uni08DB', 'uniFD76'] + #glyphs = ['uni08DE', 'uni0034'] + #glyphs = ['uni08DE', 'uni0034', 'uni0751', 'uni0753', 'uni0754', 'uni08A4', 'uni08A4.fina', 'uni08A5.fina'] + + from os.path import basename + names = [basename(filename).rsplit('.', 1)[0] for filename in filenames] + + from fontTools.ttLib import TTFont + fonts = [TTFont(filename) for filename in filenames] + + glyphsets = [font.getGlyphSet() for font in fonts] + test(glyphsets, glyphs=glyphs, names=names) + +if __name__ == '__main__': + import sys + main(sys.argv[1:]) diff --git a/Lib/fontTools/varLib/interpolate_layout.py b/Lib/fontTools/varLib/interpolate_layout.py new file mode 100644 index 0000000..ca9ccfe --- /dev/null +++ b/Lib/fontTools/varLib/interpolate_layout.py @@ -0,0 +1,91 @@ +""" +Interpolate OpenType Layout tables (GDEF / GPOS / GSUB). +""" +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.ttLib import TTFont +from fontTools.varLib import models, VarLibError, load_designspace +from fontTools.varLib.merger import InstancerMerger +import os.path +import logging +from pprint import pformat + +log = logging.getLogger("fontTools.varLib.interpolate_layout") + + +def interpolate_layout(designspace_filename, loc, master_finder=lambda s:s, mapped=False): + """ + Interpolate GPOS from a designspace file and location. + + If master_finder is set, it should be a callable that takes master + filename as found in designspace file and map it to master font + binary as to be opened (eg. .ttf or .otf). + + If mapped is False (default), then location is mapped using the + map element of the axes in designspace file. If mapped is True, + it is assumed that location is in designspace's internal space and + no mapping is performed. + """ + + axes, internal_axis_supports, base_idx, normalized_master_locs, masters, instances = load_designspace(designspace_filename) + + + log.info("Building interpolated font") + log.info("Loading master fonts") + basedir = os.path.dirname(designspace_filename) + master_ttfs = [master_finder(os.path.join(basedir, m['filename'])) for m in masters] + master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs] + + #font = master_fonts[base_idx] + font = TTFont(master_ttfs[base_idx]) + + log.info("Location: %s", pformat(loc)) + if not mapped: + loc = {name:axes[name].map_forward(v) for name,v in loc.items()} + log.info("Internal location: %s", pformat(loc)) + loc = models.normalizeLocation(loc, internal_axis_supports) + log.info("Normalized location: %s", pformat(loc)) + + # Assume single-model for now. + model = models.VariationModel(normalized_master_locs) + assert 0 == model.mapping[base_idx] + + merger = InstancerMerger(font, model, loc) + + log.info("Building interpolated tables") + merger.mergeTables(font, master_fonts, ['GPOS']) + return font + + +def main(args=None): + from fontTools import configLogger + + import sys + if args is None: + args = sys.argv[1:] + + designspace_filename = args[0] + locargs = args[1:] + outfile = os.path.splitext(designspace_filename)[0] + '-instance.ttf' + + # TODO: allow user to configure logging via command-line options + configLogger(level="INFO") + + finder = lambda s: s.replace('master_ufo', 'master_ttf_interpolatable').replace('.ufo', '.ttf') + + loc = {} + for arg in locargs: + tag,val = arg.split('=') + loc[tag] = float(val) + + font = interpolate_layout(designspace_filename, loc, finder) + log.info("Saving font %s", outfile) + font.save(outfile) + + +if __name__ == "__main__": + import sys + if len(sys.argv) > 1: + sys.exit(main()) + import doctest + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/varLib/iup.py b/Lib/fontTools/varLib/iup.py new file mode 100644 index 0000000..912ff0a --- /dev/null +++ b/Lib/fontTools/varLib/iup.py @@ -0,0 +1,305 @@ +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.misc.py23 import * + + +def iup_segment(coords, rc1, rd1, rc2, rd2): + # rc1 = reference coord 1 + # rd1 = reference delta 1 + out_arrays = [None, None] + for j in 0,1: + out_arrays[j] = out = [] + x1, x2, d1, d2 = rc1[j], rc2[j], rd1[j], rd2[j] + + + if x1 == x2: + n = len(coords) + if d1 == d2: + out.extend([d1]*n) + else: + out.extend([0]*n) + continue + + if x1 > x2: + x1, x2 = x2, x1 + d1, d2 = d2, d1 + + # x1 < x2 + scale = (d2 - d1) / (x2 - x1) + for pair in coords: + x = pair[j] + + if x <= x1: + d = d1 + elif x >= x2: + d = d2 + else: + # Interpolate + d = d1 + (x - x1) * scale + + out.append(d) + + return zip(*out_arrays) + +def iup_contour(delta, coords): + assert len(delta) == len(coords) + if None not in delta: + return delta + + n = len(delta) + # indices of points with explicit deltas + indices = [i for i,v in enumerate(delta) if v is not None] + if not indices: + # All deltas are None. Return 0,0 for all. + return [(0,0)]*n + + out = [] + it = iter(indices) + start = next(it) + if start != 0: + # Initial segment that wraps around + i1, i2, ri1, ri2 = 0, start, start, indices[-1] + out.extend(iup_segment(coords[i1:i2], coords[ri1], delta[ri1], coords[ri2], delta[ri2])) + out.append(delta[start]) + for end in it: + if end - start > 1: + i1, i2, ri1, ri2 = start+1, end, start, end + out.extend(iup_segment(coords[i1:i2], coords[ri1], delta[ri1], coords[ri2], delta[ri2])) + out.append(delta[end]) + start = end + if start != n-1: + # Final segment that wraps around + i1, i2, ri1, ri2 = start+1, n, start, indices[0] + out.extend(iup_segment(coords[i1:i2], coords[ri1], delta[ri1], coords[ri2], delta[ri2])) + + assert len(delta) == len(out), (len(delta), len(out)) + return out + +def iup_delta(delta, coords, ends): + assert sorted(ends) == ends and len(coords) == (ends[-1]+1 if ends else 0) + 4 + n = len(coords) + ends = ends + [n-4, n-3, n-2, n-1] + out = [] + start = 0 + for end in ends: + end += 1 + contour = iup_contour(delta[start:end], coords[start:end]) + out.extend(contour) + start = end + + return out + +# Optimizer + +def can_iup_in_between(deltas, coords, i, j, tolerance): + assert j - i >= 2 + interp = list(iup_segment(coords[i+1:j], coords[i], deltas[i], coords[j], deltas[j])) + deltas = deltas[i+1:j] + + assert len(deltas) == len(interp) + + return all(abs(complex(x-p, y-q)) <= tolerance for (x,y),(p,q) in zip(deltas, interp)) + +def _iup_contour_bound_forced_set(delta, coords, tolerance=0): + """The forced set is a conservative set of points on the contour that must be encoded + explicitly (ie. cannot be interpolated). Calculating this set allows for significantly + speeding up the dynamic-programming, as well as resolve circularity in DP. + + The set is precise; that is, if an index is in the returned set, then there is no way + that IUP can generate delta for that point, given coords and delta. + """ + assert len(delta) == len(coords) + + forced = set() + # Track "last" and "next" points on the contour as we sweep. + nd, nc = delta[0], coords[0] + ld, lc = delta[-1], coords[-1] + for i in range(len(delta)-1, -1, -1): + d, c = ld, lc + ld, lc = delta[i-1], coords[i-1] + + for j in (0,1): # For X and for Y + cj = c[j] + dj = d[j] + lcj = lc[j] + ldj = ld[j] + ncj = nc[j] + ndj = nd[j] + + if lcj <= ncj: + c1, c2 = lcj, ncj + d1, d2 = ldj, ndj + else: + c1, c2 = ncj, lcj + d1, d2 = ndj, ldj + + # If coordinate for current point is between coordinate of adjacent + # points on the two sides, but the delta for current point is NOT + # between delta for those adjacent points (considering tolerance + # allowance), then there is no way that current point can be IUP-ed. + # Mark it forced. + force = False + if c1 <= cj <= c2: + if not (min(d1,d2)-tolerance <= dj <= max(d1,d2)+tolerance): + force = True + else: # cj < c1 or c2 < cj + if c1 == c2: + if d1 == d2: + if abs(dj - d1) > tolerance: + force = True + else: + if abs(dj) > tolerance: + # Disabled the following because the "d1 == d2" does + # check does not take tolerance into consideration... + pass # force = True + elif d1 != d2: + if cj < c1: + if dj != d1 and ((dj-tolerance < d1) != (d1 < d2)): + force = True + else: # c2 < cj + if d2 != dj and ((d2 < dj+tolerance) != (d1 < d2)): + force = True + + if force: + forced.add(i) + break + + nd, nc = d, c + + return forced + +def _iup_contour_optimize_dp(delta, coords, forced={}, tolerance=0, lookback=None): + """Straightforward Dynamic-Programming. For each index i, find least-costly encoding of + points i to n-1 where i is explicitly encoded. We find this by considering all next + explicit points j and check whether interpolation can fill points between i and j. + + Note that solution always encodes last point explicitly. Higher-level is responsible + for removing that restriction. + + As major speedup, we stop looking further whenever we see a "forced" point.""" + + n = len(delta) + if lookback is None: + lookback = n + costs = {-1:0} + chain = {-1:None} + for i in range(0, n): + best_cost = costs[i-1] + 1 + + costs[i] = best_cost + chain[i] = i - 1 + + if i - 1 in forced: + continue + + for j in range(i-2, max(i-lookback, -2), -1): + + cost = costs[j] + 1 + + if cost < best_cost and can_iup_in_between(delta, coords, j, i, tolerance): + costs[i] = best_cost = cost + chain[i] = j + + if j in forced: + break + + return chain, costs + +def _rot_list(l, k): + """Rotate list by k items forward. Ie. item at position 0 will be + at position k in returned list. Negative k is allowed.""" + n = len(l) + k %= n + if not k: return l + return l[n-k:] + l[:n-k] + +def _rot_set(s, k, n): + k %= n + if not k: return s + return {(v + k) % n for v in s} + +def iup_contour_optimize(delta, coords, tolerance=0.): + n = len(delta) + + # Get the easy cases out of the way: + + # If all are within tolerance distance of 0, encode nothing: + if all(abs(complex(*p)) <= tolerance for p in delta): + return [None] * n + + # If there's exactly one point, return it: + if n == 1: + return delta + + # If all deltas are exactly the same, return just one (the first one): + d0 = delta[0] + if all(d0 == d for d in delta): + return [d0] + [None] * (n-1) + + # Else, solve the general problem using Dynamic Programming. + + forced = _iup_contour_bound_forced_set(delta, coords, tolerance) + # The _iup_contour_optimize_dp() routine returns the optimal encoding + # solution given the constraint that the last point is always encoded. + # To remove this constraint, we use two different methods, depending on + # whether forced set is non-empty or not: + + if forced: + # Forced set is non-empty: rotate the contour start point + # such that the last point in the list is a forced point. + k = (n-1) - max(forced) + assert k >= 0 + + delta = _rot_list(delta, k) + coords = _rot_list(coords, k) + forced = _rot_set(forced, k, n) + + chain, costs = _iup_contour_optimize_dp(delta, coords, forced, tolerance) + + # Assemble solution. + solution = set() + i = n - 1 + while i is not None: + solution.add(i) + i = chain[i] + assert forced <= solution, (forced, solution) + delta = [delta[i] if i in solution else None for i in range(n)] + + delta = _rot_list(delta, -k) + else: + # Repeat the contour an extra time, solve the 2*n case, then look for solutions of the + # circular n-length problem in the solution for 2*n linear case. I cannot prove that + # this always produces the optimal solution... + chain, costs = _iup_contour_optimize_dp(delta+delta, coords+coords, forced, tolerance, n) + best_sol, best_cost = None, n+1 + + for start in range(n-1, 2*n-1): + # Assemble solution. + solution = set() + i = start + while i > start - n: + solution.add(i % n) + i = chain[i] + if i == start - n: + cost = costs[start] - costs[start - n] + if cost <= best_cost: + best_sol, best_cost = solution, cost + + delta = [delta[i] if i in best_sol else None for i in range(n)] + + + return delta + +def iup_delta_optimize(delta, coords, ends, tolerance=0.): + assert sorted(ends) == ends and len(coords) == (ends[-1]+1 if ends else 0) + 4 + n = len(coords) + ends = ends + [n-4, n-3, n-2, n-1] + out = [] + start = 0 + for end in ends: + contour = iup_contour_optimize(delta[start:end+1], coords[start:end+1], tolerance) + assert len(contour) == end - start + 1 + out.extend(contour) + start = end+1 + + return out diff --git a/Lib/fontTools/varLib/merger.py b/Lib/fontTools/varLib/merger.py new file mode 100644 index 0000000..0e4095d --- /dev/null +++ b/Lib/fontTools/varLib/merger.py @@ -0,0 +1,830 @@ +""" +Merge OpenType Layout tables (GDEF / GPOS / GSUB). +""" +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import otRound +from fontTools.misc import classifyTools +from fontTools.ttLib.tables import otTables as ot +from fontTools.ttLib.tables import otBase as otBase +from fontTools.ttLib.tables.DefaultTable import DefaultTable +from fontTools.varLib import builder, varStore +from fontTools.varLib.varStore import VarStoreInstancer +from functools import reduce + + +class Merger(object): + + def __init__(self, font=None): + self.font = font + + @classmethod + def merger(celf, clazzes, attrs=(None,)): + assert celf != Merger, 'Subclass Merger instead.' + if 'mergers' not in celf.__dict__: + celf.mergers = {} + if type(clazzes) == type: + clazzes = (clazzes,) + if type(attrs) == str: + attrs = (attrs,) + def wrapper(method): + assert method.__name__ == 'merge' + done = [] + for clazz in clazzes: + if clazz in done: continue # Support multiple names of a clazz + done.append(clazz) + mergers = celf.mergers.setdefault(clazz, {}) + for attr in attrs: + assert attr not in mergers, \ + "Oops, class '%s' has merge function for '%s' defined already." % (clazz.__name__, attr) + mergers[attr] = method + return None + return wrapper + + @classmethod + def mergersFor(celf, thing, _default={}): + typ = type(thing) + + for celf in celf.mro(): + + mergers = getattr(celf, 'mergers', None) + if mergers is None: + break; + + m = celf.mergers.get(typ, None) + if m is not None: + return m + + return _default + + def mergeObjects(self, out, lst, exclude=()): + keys = sorted(vars(out).keys()) + assert all(keys == sorted(vars(v).keys()) for v in lst), \ + (keys, [sorted(vars(v).keys()) for v in lst]) + mergers = self.mergersFor(out) + defaultMerger = mergers.get('*', self.__class__.mergeThings) + try: + for key in keys: + if key in exclude: continue + value = getattr(out, key) + values = [getattr(table, key) for table in lst] + mergerFunc = mergers.get(key, defaultMerger) + mergerFunc(self, value, values) + except Exception as e: + e.args = e.args + ('.'+key,) + raise + + def mergeLists(self, out, lst): + count = len(out) + assert all(count == len(v) for v in lst), (count, [len(v) for v in lst]) + for i,(value,values) in enumerate(zip(out, zip(*lst))): + try: + self.mergeThings(value, values) + except Exception as e: + e.args = e.args + ('[%d]' % i,) + raise + + def mergeThings(self, out, lst): + clazz = type(out) + try: + assert all(type(item) == clazz for item in lst), (out, lst) + mergerFunc = self.mergersFor(out).get(None, None) + if mergerFunc is not None: + mergerFunc(self, out, lst) + elif hasattr(out, '__dict__'): + self.mergeObjects(out, lst) + elif isinstance(out, list): + self.mergeLists(out, lst) + else: + assert all(out == v for v in lst), (out, lst) + except Exception as e: + e.args = e.args + (clazz.__name__,) + raise + + def mergeTables(self, font, master_ttfs, tables): + + for tag in tables: + if tag not in font: continue + self.mergeThings(font[tag], [m[tag] for m in master_ttfs]) + +# +# Aligning merger +# +class AligningMerger(Merger): + pass + +def _SinglePosUpgradeToFormat2(self): + if self.Format == 2: return self + + ret = ot.SinglePos() + ret.Format = 2 + ret.Coverage = self.Coverage + ret.ValueFormat = self.ValueFormat + ret.Value = [self.Value for g in ret.Coverage.glyphs] + ret.ValueCount = len(ret.Value) + + return ret + +def _merge_GlyphOrders(font, lst, values_lst=None, default=None): + """Takes font and list of glyph lists (must be sorted by glyph id), and returns + two things: + - Combined glyph list, + - If values_lst is None, return input glyph lists, but padded with None when a glyph + was missing in a list. Otherwise, return values_lst list-of-list, padded with None + to match combined glyph lists. + """ + if values_lst is None: + dict_sets = [set(l) for l in lst] + else: + dict_sets = [{g:v for g,v in zip(l,vs)} for l,vs in zip(lst,values_lst)] + combined = set() + combined.update(*dict_sets) + + sortKey = font.getReverseGlyphMap().__getitem__ + order = sorted(combined, key=sortKey) + # Make sure all input glyphsets were in proper order + assert all(sorted(vs, key=sortKey) == vs for vs in lst) + del combined + + paddedValues = None + if values_lst is None: + padded = [[glyph if glyph in dict_set else default + for glyph in order] + for dict_set in dict_sets] + else: + assert len(lst) == len(values_lst) + padded = [[dict_set[glyph] if glyph in dict_set else default + for glyph in order] + for dict_set in dict_sets] + return order, padded + +def _Lookup_SinglePos_get_effective_value(subtables, glyph): + for self in subtables: + if self is None or \ + type(self) != ot.SinglePos or \ + self.Coverage is None or \ + glyph not in self.Coverage.glyphs: + continue + if self.Format == 1: + return self.Value + elif self.Format == 2: + return self.Value[self.Coverage.glyphs.index(glyph)] + else: + assert 0 + return None + +def _Lookup_PairPos_get_effective_value_pair(subtables, firstGlyph, secondGlyph): + for self in subtables: + if self is None or \ + type(self) != ot.PairPos or \ + self.Coverage is None or \ + firstGlyph not in self.Coverage.glyphs: + continue + if self.Format == 1: + ps = self.PairSet[self.Coverage.glyphs.index(firstGlyph)] + pvr = ps.PairValueRecord + for rec in pvr: # TODO Speed up + if rec.SecondGlyph == secondGlyph: + return rec + continue + elif self.Format == 2: + klass1 = self.ClassDef1.classDefs.get(firstGlyph, 0) + klass2 = self.ClassDef2.classDefs.get(secondGlyph, 0) + return self.Class1Record[klass1].Class2Record[klass2] + else: + assert 0 + return None + +@AligningMerger.merger(ot.SinglePos) +def merge(merger, self, lst): + self.ValueFormat = valueFormat = reduce(int.__or__, [l.ValueFormat for l in lst], 0) + assert len(lst) == 1 or (valueFormat & ~0xF == 0), valueFormat + + # If all have same coverage table and all are format 1, + if all(v.Format == 1 for v in lst) and all(self.Coverage.glyphs == v.Coverage.glyphs for v in lst): + self.Value = otBase.ValueRecord(valueFormat) + merger.mergeThings(self.Value, [v.Value for v in lst]) + self.ValueFormat = self.Value.getFormat() + return + + # Upgrade everything to Format=2 + self.Format = 2 + lst = [_SinglePosUpgradeToFormat2(v) for v in lst] + + # Align them + glyphs, padded = _merge_GlyphOrders(merger.font, + [v.Coverage.glyphs for v in lst], + [v.Value for v in lst]) + + self.Coverage.glyphs = glyphs + self.Value = [otBase.ValueRecord(valueFormat) for g in glyphs] + self.ValueCount = len(self.Value) + + for i,values in enumerate(padded): + for j,glyph in enumerate(glyphs): + if values[j] is not None: continue + # Fill in value from other subtables + # Note!!! This *might* result in behavior change if ValueFormat2-zeroedness + # is different between used subtable and current subtable! + # TODO(behdad) Check and warn if that happens? + v = _Lookup_SinglePos_get_effective_value(merger.lookup_subtables[i], glyph) + if v is None: + v = otBase.ValueRecord(valueFormat) + values[j] = v + + merger.mergeLists(self.Value, padded) + + # Merge everything else; though, there shouldn't be anything else. :) + merger.mergeObjects(self, lst, + exclude=('Format', 'Coverage', 'Value', 'ValueCount')) + self.ValueFormat = reduce(int.__or__, [v.getFormat() for v in self.Value], 0) + +@AligningMerger.merger(ot.PairSet) +def merge(merger, self, lst): + # Align them + glyphs, padded = _merge_GlyphOrders(merger.font, + [[v.SecondGlyph for v in vs.PairValueRecord] for vs in lst], + [vs.PairValueRecord for vs in lst]) + + self.PairValueRecord = pvrs = [] + for glyph in glyphs: + pvr = ot.PairValueRecord() + pvr.SecondGlyph = glyph + pvr.Value1 = otBase.ValueRecord(merger.valueFormat1) if merger.valueFormat1 else None + pvr.Value2 = otBase.ValueRecord(merger.valueFormat2) if merger.valueFormat2 else None + pvrs.append(pvr) + self.PairValueCount = len(self.PairValueRecord) + + for i,values in enumerate(padded): + for j,glyph in enumerate(glyphs): + # Fill in value from other subtables + v = ot.PairValueRecord() + v.SecondGlyph = glyph + if values[j] is not None: + vpair = values[j] + else: + vpair = _Lookup_PairPos_get_effective_value_pair(merger.lookup_subtables[i], self._firstGlyph, glyph) + if vpair is None: + v1, v2 = None, None + else: + v1, v2 = vpair.Value1, vpair.Value2 + v.Value1 = otBase.ValueRecord(merger.valueFormat1, src=v1) if merger.valueFormat1 else None + v.Value2 = otBase.ValueRecord(merger.valueFormat2, src=v2) if merger.valueFormat2 else None + values[j] = v + del self._firstGlyph + + merger.mergeLists(self.PairValueRecord, padded) + +def _PairPosFormat1_merge(self, lst, merger): + assert _all_equal([l.ValueFormat2 == 0 for l in lst if l.PairSet]), "Report bug against fonttools." + + # Merge everything else; makes sure Format is the same. + merger.mergeObjects(self, lst, + exclude=('Coverage', + 'PairSet', 'PairSetCount', + 'ValueFormat1', 'ValueFormat2')) + + empty = ot.PairSet() + empty.PairValueRecord = [] + empty.PairValueCount = 0 + + # Align them + glyphs, padded = _merge_GlyphOrders(merger.font, + [v.Coverage.glyphs for v in lst], + [v.PairSet for v in lst], + default=empty) + + self.Coverage.glyphs = glyphs + self.PairSet = [ot.PairSet() for g in glyphs] + self.PairSetCount = len(self.PairSet) + for glyph, ps in zip(glyphs, self.PairSet): + ps._firstGlyph = glyph + + merger.mergeLists(self.PairSet, padded) + +def _ClassDef_invert(self, allGlyphs=None): + + if isinstance(self, dict): + classDefs = self + else: + classDefs = self.classDefs if self and self.classDefs else {} + m = max(classDefs.values()) if classDefs else 0 + + ret = [] + for _ in range(m + 1): + ret.append(set()) + + for k,v in classDefs.items(): + ret[v].add(k) + + # Class-0 is special. It's "everything else". + if allGlyphs is None: + ret[0] = None + else: + # Limit all classes to glyphs in allGlyphs. + # Collect anything without a non-zero class into class=zero. + ret[0] = class0 = set(allGlyphs) + for s in ret[1:]: + s.intersection_update(class0) + class0.difference_update(s) + + return ret + +def _ClassDef_merge_classify(lst, allGlyphs=None): + self = ot.ClassDef() + self.classDefs = classDefs = {} + + classifier = classifyTools.Classifier() + for l in lst: + sets = _ClassDef_invert(l, allGlyphs=allGlyphs) + if allGlyphs is None: + sets = sets[1:] + classifier.update(sets) + classes = classifier.getClasses() + + if allGlyphs is None: + classes.insert(0, set()) + + for i,classSet in enumerate(classes): + if i == 0: + continue + for g in classSet: + classDefs[g] = i + + return self, classes + +def _ClassDef_calculate_Format(self, font): + fmt = 2 + ranges = self._getClassRanges(font) + if ranges: + startGlyph = ranges[0][1] + endGlyph = ranges[-1][3] + glyphCount = endGlyph - startGlyph + 1 + if len(ranges) * 3 >= glyphCount + 1: + # Format 1 is more compact + fmt = 1 + self.Format = fmt + +def _PairPosFormat2_align_matrices(self, lst, font, transparent=False): + + matrices = [l.Class1Record for l in lst] + + # Align first classes + self.ClassDef1, classes = _ClassDef_merge_classify([l.ClassDef1 for l in lst], allGlyphs=set(self.Coverage.glyphs)) + _ClassDef_calculate_Format(self.ClassDef1, font) + self.Class1Count = len(classes) + new_matrices = [] + for l,matrix in zip(lst, matrices): + nullRow = None + coverage = set(l.Coverage.glyphs) + classDef1 = l.ClassDef1.classDefs + class1Records = [] + for classSet in classes: + exemplarGlyph = next(iter(classSet)) + if exemplarGlyph not in coverage: + if nullRow is None: + nullRow = ot.Class1Record() + class2records = nullRow.Class2Record = [] + # TODO: When merger becomes selfless, revert e6125b353e1f54a0280ded5434b8e40d042de69f + for _ in range(l.Class2Count): + if transparent: + rec2 = None + else: + rec2 = ot.Class2Record() + rec2.Value1 = otBase.ValueRecord(self.ValueFormat1) if self.ValueFormat1 else None + rec2.Value2 = otBase.ValueRecord(self.ValueFormat2) if self.ValueFormat2 else None + class2records.append(rec2) + rec1 = nullRow + else: + klass = classDef1.get(exemplarGlyph, 0) + rec1 = matrix[klass] # TODO handle out-of-range? + class1Records.append(rec1) + new_matrices.append(class1Records) + matrices = new_matrices + del new_matrices + + # Align second classes + self.ClassDef2, classes = _ClassDef_merge_classify([l.ClassDef2 for l in lst]) + _ClassDef_calculate_Format(self.ClassDef2, font) + self.Class2Count = len(classes) + new_matrices = [] + for l,matrix in zip(lst, matrices): + classDef2 = l.ClassDef2.classDefs + class1Records = [] + for rec1old in matrix: + oldClass2Records = rec1old.Class2Record + rec1new = ot.Class1Record() + class2Records = rec1new.Class2Record = [] + for classSet in classes: + if not classSet: # class=0 + rec2 = oldClass2Records[0] + else: + exemplarGlyph = next(iter(classSet)) + klass = classDef2.get(exemplarGlyph, 0) + rec2 = oldClass2Records[klass] + class2Records.append(rec2) + class1Records.append(rec1new) + new_matrices.append(class1Records) + matrices = new_matrices + del new_matrices + + return matrices + +def _PairPosFormat2_merge(self, lst, merger): + assert _all_equal([l.ValueFormat2 == 0 for l in lst if l.Class1Record]), "Report bug against fonttools." + + merger.mergeObjects(self, lst, + exclude=('Coverage', + 'ClassDef1', 'Class1Count', + 'ClassDef2', 'Class2Count', + 'Class1Record', + 'ValueFormat1', 'ValueFormat2')) + + # Align coverages + glyphs, _ = _merge_GlyphOrders(merger.font, + [v.Coverage.glyphs for v in lst]) + self.Coverage.glyphs = glyphs + + # Currently, if the coverage of PairPosFormat2 subtables are different, + # we do NOT bother walking down the subtable list when filling in new + # rows for alignment. As such, this is only correct if current subtable + # is the last subtable in the lookup. Ensure that. + # + # Note that our canonicalization process merges trailing PairPosFormat2's, + # so in reality this is rare. + for l,subtables in zip(lst,merger.lookup_subtables): + if l.Coverage.glyphs != glyphs: + assert l == subtables[-1] + + matrices = _PairPosFormat2_align_matrices(self, lst, merger.font) + + self.Class1Record = list(matrices[0]) # TODO move merger to be selfless + merger.mergeLists(self.Class1Record, matrices) + +@AligningMerger.merger(ot.PairPos) +def merge(merger, self, lst): + merger.valueFormat1 = self.ValueFormat1 = reduce(int.__or__, [l.ValueFormat1 for l in lst], 0) + merger.valueFormat2 = self.ValueFormat2 = reduce(int.__or__, [l.ValueFormat2 for l in lst], 0) + + if self.Format == 1: + _PairPosFormat1_merge(self, lst, merger) + elif self.Format == 2: + _PairPosFormat2_merge(self, lst, merger) + else: + assert False + + del merger.valueFormat1, merger.valueFormat2 + + # Now examine the list of value records, and update to the union of format values, + # as merge might have created new values. + vf1 = 0 + vf2 = 0 + if self.Format == 1: + for pairSet in self.PairSet: + for pairValueRecord in pairSet.PairValueRecord: + pv1 = pairValueRecord.Value1 + if pv1 is not None: + vf1 |= pv1.getFormat() + pv2 = pairValueRecord.Value2 + if pv2 is not None: + vf2 |= pv2.getFormat() + elif self.Format == 2: + for class1Record in self.Class1Record: + for class2Record in class1Record.Class2Record: + pv1 = class2Record.Value1 + if pv1 is not None: + vf1 |= pv1.getFormat() + pv2 = class2Record.Value2 + if pv2 is not None: + vf2 |= pv2.getFormat() + self.ValueFormat1 = vf1 + self.ValueFormat2 = vf2 + + +def _PairSet_flatten(lst, font): + self = ot.PairSet() + self.Coverage = ot.Coverage() + self.Coverage.Format = 1 + + # Align them + glyphs, padded = _merge_GlyphOrders(font, + [[v.SecondGlyph for v in vs.PairValueRecord] for vs in lst], + [vs.PairValueRecord for vs in lst]) + + self.Coverage.glyphs = glyphs + self.PairValueRecord = pvrs = [] + for values in zip(*padded): + for v in values: + if v is not None: + pvrs.append(v) + break + else: + assert False + self.PairValueCount = len(self.PairValueRecord) + + return self + +def _Lookup_PairPosFormat1_subtables_flatten(lst, font): + assert _all_equal([l.ValueFormat2 == 0 for l in lst if l.PairSet]), "Report bug against fonttools." + + self = ot.PairPos() + self.Format = 1 + self.Coverage = ot.Coverage() + self.Coverage.Format = 1 + self.ValueFormat1 = reduce(int.__or__, [l.ValueFormat1 for l in lst], 0) + self.ValueFormat2 = reduce(int.__or__, [l.ValueFormat2 for l in lst], 0) + + # Align them + glyphs, padded = _merge_GlyphOrders(font, + [v.Coverage.glyphs for v in lst], + [v.PairSet for v in lst]) + + self.Coverage.glyphs = glyphs + self.PairSet = [_PairSet_flatten([v for v in values if v is not None], font) + for values in zip(*padded)] + self.PairSetCount = len(self.PairSet) + return self + +def _Lookup_PairPosFormat2_subtables_flatten(lst, font): + assert _all_equal([l.ValueFormat2 == 0 for l in lst if l.Class1Record]), "Report bug against fonttools." + + self = ot.PairPos() + self.Format = 2 + self.Coverage = ot.Coverage() + self.Coverage.Format = 1 + self.ValueFormat1 = reduce(int.__or__, [l.ValueFormat1 for l in lst], 0) + self.ValueFormat2 = reduce(int.__or__, [l.ValueFormat2 for l in lst], 0) + + # Align them + glyphs, _ = _merge_GlyphOrders(font, + [v.Coverage.glyphs for v in lst]) + self.Coverage.glyphs = glyphs + + matrices = _PairPosFormat2_align_matrices(self, lst, font, transparent=True) + + matrix = self.Class1Record = [] + for rows in zip(*matrices): + row = ot.Class1Record() + matrix.append(row) + row.Class2Record = [] + row = row.Class2Record + for cols in zip(*list(r.Class2Record for r in rows)): + col = next(iter(c for c in cols if c is not None)) + row.append(col) + + return self + +def _Lookup_PairPos_subtables_canonicalize(lst, font): + """Merge multiple Format1 subtables at the beginning of lst, + and merge multiple consecutive Format2 subtables that have the same + Class2 (ie. were split because of offset overflows). Returns new list.""" + lst = list(lst) + + l = len(lst) + i = 0 + while i < l and lst[i].Format == 1: + i += 1 + lst[:i] = [_Lookup_PairPosFormat1_subtables_flatten(lst[:i], font)] + + l = len(lst) + i = l + while i > 0 and lst[i - 1].Format == 2: + i -= 1 + lst[i:] = [_Lookup_PairPosFormat2_subtables_flatten(lst[i:], font)] + + return lst + +@AligningMerger.merger(ot.Lookup) +def merge(merger, self, lst): + subtables = merger.lookup_subtables = [l.SubTable for l in lst] + + # Remove Extension subtables + for l,sts in list(zip(lst,subtables))+[(self,self.SubTable)]: + if not sts: + continue + if sts[0].__class__.__name__.startswith('Extension'): + assert _all_equal([st.__class__ for st in sts]) + assert _all_equal([st.ExtensionLookupType for st in sts]) + l.LookupType = sts[0].ExtensionLookupType + new_sts = [st.ExtSubTable for st in sts] + del sts[:] + sts.extend(new_sts) + + isPairPos = self.SubTable and isinstance(self.SubTable[0], ot.PairPos) + + if isPairPos: + + # AFDKO and feaLib sometimes generate two Format1 subtables instead of one. + # Merge those before continuing. + # https://github.com/fonttools/fonttools/issues/719 + self.SubTable = _Lookup_PairPos_subtables_canonicalize(self.SubTable, merger.font) + subtables = merger.lookup_subtables = [_Lookup_PairPos_subtables_canonicalize(st, merger.font) for st in subtables] + + merger.mergeLists(self.SubTable, subtables) + self.SubTableCount = len(self.SubTable) + + if isPairPos: + # If format-1 subtable created during canonicalization is empty, remove it. + assert len(self.SubTable) >= 1 and self.SubTable[0].Format == 1 + if not self.SubTable[0].Coverage.glyphs: + self.SubTable.pop(0) + self.SubTableCount -= 1 + + # If format-2 subtable created during canonicalization is empty, remove it. + assert len(self.SubTable) >= 1 and self.SubTable[-1].Format == 2 + if not self.SubTable[-1].Coverage.glyphs: + self.SubTable.pop(-1) + self.SubTableCount -= 1 + + merger.mergeObjects(self, lst, exclude=['SubTable', 'SubTableCount']) + + del merger.lookup_subtables + + +# +# InstancerMerger +# + +class InstancerMerger(AligningMerger): + """A merger that takes multiple master fonts, and instantiates + an instance.""" + + def __init__(self, font, model, location): + Merger.__init__(self, font) + self.model = model + self.location = location + self.scalars = model.getScalars(location) + +@InstancerMerger.merger(ot.Anchor) +def merge(merger, self, lst): + XCoords = [a.XCoordinate for a in lst] + YCoords = [a.YCoordinate for a in lst] + model = merger.model + scalars = merger.scalars + self.XCoordinate = otRound(model.interpolateFromMastersAndScalars(XCoords, scalars)) + self.YCoordinate = otRound(model.interpolateFromMastersAndScalars(YCoords, scalars)) + +@InstancerMerger.merger(otBase.ValueRecord) +def merge(merger, self, lst): + model = merger.model + scalars = merger.scalars + # TODO Handle differing valueformats + for name, tableName in [('XAdvance','XAdvDevice'), + ('YAdvance','YAdvDevice'), + ('XPlacement','XPlaDevice'), + ('YPlacement','YPlaDevice')]: + + assert not hasattr(self, tableName) + + if hasattr(self, name): + values = [getattr(a, name, 0) for a in lst] + value = otRound(model.interpolateFromMastersAndScalars(values, scalars)) + setattr(self, name, value) + + +# +# MutatorMerger +# + +class MutatorMerger(AligningMerger): + """A merger that takes a variable font, and instantiates + an instance.""" + + def __init__(self, font, location): + Merger.__init__(self, font) + self.location = location + + store = None + if 'GDEF' in font: + gdef = font['GDEF'].table + if gdef.Version >= 0x00010003: + store = gdef.VarStore + + self.instancer = VarStoreInstancer(store, font['fvar'].axes, location) + + def instantiate(self): + font = self.font + + self.mergeTables(font, [font], ['GPOS']) + + if 'GDEF' in font: + gdef = font['GDEF'].table + if gdef.Version >= 0x00010003: + del gdef.VarStore + gdef.Version = 0x00010002 + if gdef.MarkGlyphSetsDef is None: + del gdef.MarkGlyphSetsDef + gdef.Version = 0x00010000 + if not (gdef.LigCaretList or + gdef.MarkAttachClassDef or + gdef.GlyphClassDef or + gdef.AttachList or + (gdef.Version >= 0x00010002 and gdef.MarkGlyphSetsDef)): + del font['GDEF'] + +@MutatorMerger.merger(ot.Anchor) +def merge(merger, self, lst): + if self.Format != 3: + return + + instancer = merger.instancer + for v in "XY": + tableName = v+'DeviceTable' + if not hasattr(self, tableName): + continue + dev = getattr(self, tableName) + delattr(self, tableName) + if dev is None: + continue + + assert dev.DeltaFormat == 0x8000 + varidx = (dev.StartSize << 16) + dev.EndSize + delta = otRound(instancer[varidx]) + + attr = v+'Coordinate' + setattr(self, attr, getattr(self, attr) + delta) + + self.Format = 1 + +@MutatorMerger.merger(otBase.ValueRecord) +def merge(merger, self, lst): + + # All other structs are merged with self pointing to a copy of base font, + # except for ValueRecords which are sometimes created later and initialized + # to have 0/None members. Hence the copy. + self.__dict__ = lst[0].__dict__.copy() + + instancer = merger.instancer + # TODO Handle differing valueformats + for name, tableName in [('XAdvance','XAdvDevice'), + ('YAdvance','YAdvDevice'), + ('XPlacement','XPlaDevice'), + ('YPlacement','YPlaDevice')]: + + if not hasattr(self, tableName): + continue + dev = getattr(self, tableName) + delattr(self, tableName) + if dev is None: + continue + + assert dev.DeltaFormat == 0x8000 + varidx = (dev.StartSize << 16) + dev.EndSize + delta = otRound(instancer[varidx]) + + setattr(self, name, getattr(self, name) + delta) + + +# +# VariationMerger +# + +class VariationMerger(AligningMerger): + """A merger that takes multiple master fonts, and builds a + variable font.""" + + def __init__(self, model, axisTags, font): + Merger.__init__(self, font) + self.model = model + self.store_builder = varStore.OnlineVarStoreBuilder(axisTags) + self.store_builder.setModel(model) + +def _all_equal(lst): + if not lst: + return True + it = iter(lst) + v0 = next(it) + for v in it: + if v0 != v: + return False + return True + +def buildVarDevTable(store_builder, master_values): + if _all_equal(master_values): + return master_values[0], None + base, varIdx = store_builder.storeMasters(master_values) + return base, builder.buildVarDevTable(varIdx) + +@VariationMerger.merger(ot.Anchor) +def merge(merger, self, lst): + assert self.Format == 1 + self.XCoordinate, XDeviceTable = buildVarDevTable(merger.store_builder, [a.XCoordinate for a in lst]) + self.YCoordinate, YDeviceTable = buildVarDevTable(merger.store_builder, [a.YCoordinate for a in lst]) + if XDeviceTable or YDeviceTable: + self.Format = 3 + self.XDeviceTable = XDeviceTable + self.YDeviceTable = YDeviceTable + +@VariationMerger.merger(otBase.ValueRecord) +def merge(merger, self, lst): + for name, tableName in [('XAdvance','XAdvDevice'), + ('YAdvance','YAdvDevice'), + ('XPlacement','XPlaDevice'), + ('YPlacement','YPlaDevice')]: + + if hasattr(self, name): + value, deviceTable = buildVarDevTable(merger.store_builder, + [getattr(a, name, 0) for a in lst]) + setattr(self, name, value) + if deviceTable: + setattr(self, tableName, deviceTable) diff --git a/Lib/fontTools/varLib/models.py b/Lib/fontTools/varLib/models.py new file mode 100644 index 0000000..8ea1432 --- /dev/null +++ b/Lib/fontTools/varLib/models.py @@ -0,0 +1,384 @@ +"""Variation fonts interpolation models.""" +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * + +__all__ = ['normalizeValue', 'normalizeLocation', 'supportScalar', 'VariationModel'] + + +def normalizeValue(v, triple): + """Normalizes value based on a min/default/max triple. + >>> normalizeValue(400, (100, 400, 900)) + 0.0 + >>> normalizeValue(100, (100, 400, 900)) + -1.0 + >>> normalizeValue(650, (100, 400, 900)) + 0.5 + """ + lower, default, upper = triple + assert lower <= default <= upper, "invalid axis values: %3.3f, %3.3f %3.3f"%(lower, default, upper) + v = max(min(v, upper), lower) + if v == default: + v = 0. + elif v < default: + v = (v - default) / (default - lower) + else: + v = (v - default) / (upper - default) + return v + +def normalizeLocation(location, axes): + """Normalizes location based on axis min/default/max values from axes. + >>> axes = {"wght": (100, 400, 900)} + >>> normalizeLocation({"wght": 400}, axes) + {'wght': 0.0} + >>> normalizeLocation({"wght": 100}, axes) + {'wght': -1.0} + >>> normalizeLocation({"wght": 900}, axes) + {'wght': 1.0} + >>> normalizeLocation({"wght": 650}, axes) + {'wght': 0.5} + >>> normalizeLocation({"wght": 1000}, axes) + {'wght': 1.0} + >>> normalizeLocation({"wght": 0}, axes) + {'wght': -1.0} + >>> axes = {"wght": (0, 0, 1000)} + >>> normalizeLocation({"wght": 0}, axes) + {'wght': 0.0} + >>> normalizeLocation({"wght": -1}, axes) + {'wght': 0.0} + >>> normalizeLocation({"wght": 1000}, axes) + {'wght': 1.0} + >>> normalizeLocation({"wght": 500}, axes) + {'wght': 0.5} + >>> normalizeLocation({"wght": 1001}, axes) + {'wght': 1.0} + >>> axes = {"wght": (0, 1000, 1000)} + >>> normalizeLocation({"wght": 0}, axes) + {'wght': -1.0} + >>> normalizeLocation({"wght": -1}, axes) + {'wght': -1.0} + >>> normalizeLocation({"wght": 500}, axes) + {'wght': -0.5} + >>> normalizeLocation({"wght": 1000}, axes) + {'wght': 0.0} + >>> normalizeLocation({"wght": 1001}, axes) + {'wght': 0.0} + """ + out = {} + for tag,triple in axes.items(): + v = location.get(tag, triple[1]) + out[tag] = normalizeValue(v, triple) + return out + +def supportScalar(location, support, ot=True): + """Returns the scalar multiplier at location, for a master + with support. If ot is True, then a peak value of zero + for support of an axis means "axis does not participate". That + is how OpenType Variation Font technology works. + >>> supportScalar({}, {}) + 1.0 + >>> supportScalar({'wght':.2}, {}) + 1.0 + >>> supportScalar({'wght':.2}, {'wght':(0,2,3)}) + 0.1 + >>> supportScalar({'wght':2.5}, {'wght':(0,2,4)}) + 0.75 + >>> supportScalar({'wght':2.5, 'wdth':0}, {'wght':(0,2,4), 'wdth':(-1,0,+1)}) + 0.75 + >>> supportScalar({'wght':2.5, 'wdth':.5}, {'wght':(0,2,4), 'wdth':(-1,0,+1)}, ot=False) + 0.375 + >>> supportScalar({'wght':2.5, 'wdth':0}, {'wght':(0,2,4), 'wdth':(-1,0,+1)}) + 0.75 + >>> supportScalar({'wght':2.5, 'wdth':.5}, {'wght':(0,2,4), 'wdth':(-1,0,+1)}) + 0.75 + """ + scalar = 1. + for axis,(lower,peak,upper) in support.items(): + if ot: + # OpenType-specific case handling + if peak == 0.: + continue + if lower > peak or peak > upper: + continue + if lower < 0. and upper > 0.: + continue + v = location.get(axis, 0.) + else: + assert axis in location + v = location[axis] + if v == peak: + continue + if v <= lower or upper <= v: + scalar = 0. + break; + if v < peak: + scalar *= (v - lower) / (peak - lower) + else: # v > peak + scalar *= (v - upper) / (peak - upper) + return scalar + + +class VariationModel(object): + + """ + Locations must be in normalized space. Ie. base master + is at origin (0). + >>> from pprint import pprint + >>> locations = [ \ + {'wght':100}, \ + {'wght':-100}, \ + {'wght':-180}, \ + {'wdth':+.3}, \ + {'wght':+120,'wdth':.3}, \ + {'wght':+120,'wdth':.2}, \ + {}, \ + {'wght':+180,'wdth':.3}, \ + {'wght':+180}, \ + ] + >>> model = VariationModel(locations, axisOrder=['wght']) + >>> pprint(model.locations) + [{}, + {'wght': -100}, + {'wght': -180}, + {'wght': 100}, + {'wght': 180}, + {'wdth': 0.3}, + {'wdth': 0.3, 'wght': 180}, + {'wdth': 0.3, 'wght': 120}, + {'wdth': 0.2, 'wght': 120}] + >>> pprint(model.deltaWeights) + [{}, + {0: 1.0}, + {0: 1.0}, + {0: 1.0}, + {0: 1.0}, + {0: 1.0}, + {0: 1.0, 4: 1.0, 5: 1.0}, + {0: 1.0, 3: 0.75, 4: 0.25, 5: 1.0, 6: 0.6666666666666666}, + {0: 1.0, + 3: 0.75, + 4: 0.25, + 5: 0.6666666666666667, + 6: 0.4444444444444445, + 7: 0.6666666666666667}] + """ + + def __init__(self, locations, axisOrder=[]): + locations = [{k:v for k,v in loc.items() if v != 0.} for loc in locations] + keyFunc = self.getMasterLocationsSortKeyFunc(locations, axisOrder=axisOrder) + axisPoints = keyFunc.axisPoints + self.locations = sorted(locations, key=keyFunc) + # TODO Assert that locations are unique. + self.mapping = [self.locations.index(l) for l in locations] # Mapping from user's master order to our master order + self.reverseMapping = [locations.index(l) for l in self.locations] # Reverse of above + + self._computeMasterSupports(axisPoints, axisOrder) + + @staticmethod + def getMasterLocationsSortKeyFunc(locations, axisOrder=[]): + assert {} in locations, "Base master not found." + axisPoints = {} + for loc in locations: + if len(loc) != 1: + continue + axis = next(iter(loc)) + value = loc[axis] + if axis not in axisPoints: + axisPoints[axis] = {0.} + assert value not in axisPoints[axis], ( + 'Value "%s" in axisPoints["%s"] --> %s' % (value, axis, axisPoints) + ) + axisPoints[axis].add(value) + + def getKey(axisPoints, axisOrder): + def sign(v): + return -1 if v < 0 else +1 if v > 0 else 0 + def key(loc): + rank = len(loc) + onPointAxes = [axis for axis,value in loc.items() if value in axisPoints[axis]] + orderedAxes = [axis for axis in axisOrder if axis in loc] + orderedAxes.extend([axis for axis in sorted(loc.keys()) if axis not in axisOrder]) + return ( + rank, # First, order by increasing rank + -len(onPointAxes), # Next, by decreasing number of onPoint axes + tuple(axisOrder.index(axis) if axis in axisOrder else 0x10000 for axis in orderedAxes), # Next, by known axes + tuple(orderedAxes), # Next, by all axes + tuple(sign(loc[axis]) for axis in orderedAxes), # Next, by signs of axis values + tuple(abs(loc[axis]) for axis in orderedAxes), # Next, by absolute value of axis values + ) + return key + + ret = getKey(axisPoints, axisOrder) + ret.axisPoints = axisPoints + return ret + + @staticmethod + def lowerBound(value, lst): + if any(v < value for v in lst): + return max(v for v in lst if v < value) + else: + return value + @staticmethod + def upperBound(value, lst): + if any(v > value for v in lst): + return min(v for v in lst if v > value) + else: + return value + + def _computeMasterSupports(self, axisPoints, axisOrder): + supports = [] + deltaWeights = [] + locations = self.locations + for i,loc in enumerate(locations): + box = {} + + # Account for axisPoints first + # TODO Use axis min/max instead? Isn't that always -1/+1? + for axis,values in axisPoints.items(): + if not axis in loc: + continue + locV = loc[axis] + if locV > 0: + box[axis] = (0, locV, max({locV}|values)) + else: + box[axis] = (min({locV}|values), locV, 0) + + locAxes = set(loc.keys()) + # Walk over previous masters now + for j,m in enumerate(locations[:i]): + # Master with extra axes do not participte + if not set(m.keys()).issubset(locAxes): + continue + # If it's NOT in the current box, it does not participate + relevant = True + for axis, (lower,peak,upper) in box.items(): + if axis not in m or not (m[axis] == peak or lower < m[axis] < upper): + relevant = False + break + if not relevant: + continue + + # Split the box for new master; split in whatever direction + # that has largest range ratio. See commit for details. + orderedAxes = [axis for axis in axisOrder if axis in m.keys()] + orderedAxes.extend([axis for axis in sorted(m.keys()) if axis not in axisOrder]) + bestAxis = None + bestRatio = -1 + for axis in orderedAxes: + val = m[axis] + assert axis in box + lower,locV,upper = box[axis] + newLower, newUpper = lower, upper + if val < locV: + newLower = val + ratio = (val - locV) / (lower - locV) + elif locV < val: + newUpper = val + ratio = (val - locV) / (upper - locV) + else: # val == locV + # Can't split box in this direction. + continue + if ratio > bestRatio: + bestRatio = ratio + bestAxis = axis + bestLower = newLower + bestUpper = newUpper + bestLocV = locV + + if bestAxis: + box[bestAxis] = (bestLower,bestLocV,bestUpper) + supports.append(box) + + deltaWeight = {} + # Walk over previous masters now, populate deltaWeight + for j,m in enumerate(locations[:i]): + scalar = supportScalar(loc, supports[j]) + if scalar: + deltaWeight[j] = scalar + deltaWeights.append(deltaWeight) + + self.supports = supports + self.deltaWeights = deltaWeights + + def getDeltas(self, masterValues): + assert len(masterValues) == len(self.deltaWeights) + mapping = self.reverseMapping + out = [] + for i,weights in enumerate(self.deltaWeights): + delta = masterValues[mapping[i]] + for j,weight in weights.items(): + delta -= out[j] * weight + out.append(delta) + return out + + def getScalars(self, loc): + return [supportScalar(loc, support) for support in self.supports] + + @staticmethod + def interpolateFromDeltasAndScalars(deltas, scalars): + v = None + assert len(deltas) == len(scalars) + for i,(delta,scalar) in enumerate(zip(deltas, scalars)): + if not scalar: continue + contribution = delta * scalar + if v is None: + v = contribution + else: + v += contribution + return v + + def interpolateFromDeltas(self, loc, deltas): + scalars = self.getScalars(loc) + return self.interpolateFromDeltasAndScalars(deltas, scalars) + + def interpolateFromMasters(self, loc, masterValues): + deltas = self.getDeltas(masterValues) + return self.interpolateFromDeltas(loc, deltas) + + def interpolateFromMastersAndScalars(self, masterValues, scalars): + deltas = self.getDeltas(masterValues) + return self.interpolateFromDeltasAndScalars(deltas, scalars) + + +def main(args): + from fontTools import configLogger + + args = args[1:] + + # TODO: allow user to configure logging via command-line options + configLogger(level="INFO") + + if len(args) < 1: + print("usage: fonttools varLib.models source.designspace", file=sys.stderr) + print(" or") + print("usage: fonttools varLib.models location1 location2 ...", file=sys.stderr) + sys.exit(1) + + from pprint import pprint + + if len(args) == 1 and args[0].endswith('.designspace'): + from fontTools.designspaceLib import DesignSpaceDocument + doc = DesignSpaceDocument() + doc.read(args[0]) + locs = [s.location for s in doc.sources] + print("Original locations:") + pprint(locs) + doc.normalize() + print("Normalized locations:") + pprint(locs) + else: + axes = [chr(c) for c in range(ord('A'), ord('Z')+1)] + locs = [dict(zip(axes, (float(v) for v in s.split(',')))) for s in args] + + model = VariationModel(locs) + print("Sorted locations:") + pprint(model.locations) + print("Supports:") + pprint(model.supports) + +if __name__ == "__main__": + import doctest, sys + + if len(sys.argv) > 1: + sys.exit(main(sys.argv)) + + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/varLib/mutator.py b/Lib/fontTools/varLib/mutator.py new file mode 100644 index 0000000..ac771e8 --- /dev/null +++ b/Lib/fontTools/varLib/mutator.py @@ -0,0 +1,212 @@ +""" +Instantiate a variation font. Run, eg: + +$ fonttools varLib.mutator ./NotoSansArabic-VF.ttf wght=140 wdth=85 +""" +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import floatToFixedToFloat, otRound +from fontTools.ttLib import TTFont +from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates +from fontTools.varLib import _GetCoordinates, _SetCoordinates, _DesignspaceAxis +from fontTools.varLib.models import supportScalar, normalizeLocation +from fontTools.varLib.merger import MutatorMerger +from fontTools.varLib.varStore import VarStoreInstancer +from fontTools.varLib.mvar import MVAR_ENTRIES +from fontTools.varLib.iup import iup_delta +import os.path +import logging + + +log = logging.getLogger("fontTools.varlib.mutator") + +# map 'wdth' axis (1..200) to OS/2.usWidthClass (1..9), rounding to closest +OS2_WIDTH_CLASS_VALUES = {} +percents = [50.0, 62.5, 75.0, 87.5, 100.0, 112.5, 125.0, 150.0, 200.0] +for i, (prev, curr) in enumerate(zip(percents[:-1], percents[1:]), start=1): + half = (prev + curr) / 2 + OS2_WIDTH_CLASS_VALUES[half] = i + + +def instantiateVariableFont(varfont, location, inplace=False): + """ Generate a static instance from a variable TTFont and a dictionary + defining the desired location along the variable font's axes. + The location values must be specified as user-space coordinates, e.g.: + + {'wght': 400, 'wdth': 100} + + By default, a new TTFont object is returned. If ``inplace`` is True, the + input varfont is modified and reduced to a static font. + """ + if not inplace: + # make a copy to leave input varfont unmodified + stream = BytesIO() + varfont.save(stream) + stream.seek(0) + varfont = TTFont(stream) + + fvar = varfont['fvar'] + axes = {a.axisTag:(a.minValue,a.defaultValue,a.maxValue) for a in fvar.axes} + loc = normalizeLocation(location, axes) + if 'avar' in varfont: + maps = varfont['avar'].segments + loc = {k:_DesignspaceAxis._map(v, maps[k]) for k,v in loc.items()} + # Quantize to F2Dot14, to avoid surprise interpolations. + loc = {k:floatToFixedToFloat(v, 14) for k,v in loc.items()} + # Location is normalized now + log.info("Normalized location: %s", loc) + + log.info("Mutating glyf/gvar tables") + gvar = varfont['gvar'] + glyf = varfont['glyf'] + # get list of glyph names in gvar sorted by component depth + glyphnames = sorted( + gvar.variations.keys(), + key=lambda name: ( + glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth + if glyf[name].isComposite() else 0, + name)) + for glyphname in glyphnames: + variations = gvar.variations[glyphname] + coordinates,_ = _GetCoordinates(varfont, glyphname) + origCoords, endPts = None, None + for var in variations: + scalar = supportScalar(loc, var.axes) + if not scalar: continue + delta = var.coordinates + if None in delta: + if origCoords is None: + origCoords,control = _GetCoordinates(varfont, glyphname) + endPts = control[1] if control[0] >= 1 else list(range(len(control[1]))) + delta = iup_delta(delta, origCoords, endPts) + coordinates += GlyphCoordinates(delta) * scalar + _SetCoordinates(varfont, glyphname, coordinates) + + if 'cvar' in varfont: + log.info("Mutating cvt/cvar tables") + cvar = varfont['cvar'] + cvt = varfont['cvt '] + deltas = {} + for var in cvar.variations: + scalar = supportScalar(loc, var.axes) + if not scalar: continue + for i, c in enumerate(var.coordinates): + if c is not None: + deltas[i] = deltas.get(i, 0) + scalar * c + for i, delta in deltas.items(): + cvt[i] += otRound(delta) + + if 'MVAR' in varfont: + log.info("Mutating MVAR table") + mvar = varfont['MVAR'].table + varStoreInstancer = VarStoreInstancer(mvar.VarStore, fvar.axes, loc) + records = mvar.ValueRecord + for rec in records: + mvarTag = rec.ValueTag + if mvarTag not in MVAR_ENTRIES: + continue + tableTag, itemName = MVAR_ENTRIES[mvarTag] + delta = otRound(varStoreInstancer[rec.VarIdx]) + if not delta: + continue + setattr(varfont[tableTag], itemName, + getattr(varfont[tableTag], itemName) + delta) + + if 'GDEF' in varfont: + log.info("Mutating GDEF/GPOS/GSUB tables") + merger = MutatorMerger(varfont, loc) + + log.info("Building interpolated tables") + merger.instantiate() + + if 'name' in varfont: + log.info("Pruning name table") + exclude = {a.axisNameID for a in fvar.axes} + for i in fvar.instances: + exclude.add(i.subfamilyNameID) + exclude.add(i.postscriptNameID) + varfont['name'].names[:] = [ + n for n in varfont['name'].names + if n.nameID not in exclude + ] + + if "wght" in location and "OS/2" in varfont: + varfont["OS/2"].usWeightClass = otRound( + max(1, min(location["wght"], 1000)) + ) + if "wdth" in location: + wdth = location["wdth"] + for percent, widthClass in sorted(OS2_WIDTH_CLASS_VALUES.items()): + if wdth < percent: + varfont["OS/2"].usWidthClass = widthClass + break + else: + varfont["OS/2"].usWidthClass = 9 + if "slnt" in location and "post" in varfont: + varfont["post"].italicAngle = max(-90, min(location["slnt"], 90)) + + log.info("Removing variable tables") + for tag in ('avar','cvar','fvar','gvar','HVAR','MVAR','VVAR','STAT'): + if tag in varfont: + del varfont[tag] + + return varfont + + +def main(args=None): + from fontTools import configLogger + import argparse + + parser = argparse.ArgumentParser( + "fonttools varLib.mutator", description="Instantiate a variable font") + parser.add_argument( + "input", metavar="INPUT.ttf", help="Input variable TTF file.") + parser.add_argument( + "locargs", metavar="AXIS=LOC", nargs="*", + help="List of space separated locations. A location consist in " + "the name of a variation axis, followed by '=' and a number. E.g.: " + " wght=700 wdth=80. The default is the location of the base master.") + parser.add_argument( + "-o", "--output", metavar="OUTPUT.ttf", default=None, + help="Output instance TTF file (default: INPUT-instance.ttf).") + logging_group = parser.add_mutually_exclusive_group(required=False) + logging_group.add_argument( + "-v", "--verbose", action="store_true", help="Run more verbosely.") + logging_group.add_argument( + "-q", "--quiet", action="store_true", help="Turn verbosity off.") + options = parser.parse_args(args) + + varfilename = options.input + outfile = ( + os.path.splitext(varfilename)[0] + '-instance.ttf' + if not options.output else options.output) + configLogger(level=( + "DEBUG" if options.verbose else + "ERROR" if options.quiet else + "INFO")) + + loc = {} + for arg in options.locargs: + try: + tag, val = arg.split('=') + assert len(tag) <= 4 + loc[tag.ljust(4)] = float(val) + except (ValueError, AssertionError): + parser.error("invalid location argument format: %r" % arg) + log.info("Location: %s", loc) + + log.info("Loading variable font") + varfont = TTFont(varfilename) + + instantiateVariableFont(varfont, loc, inplace=True) + + log.info("Saving instance font %s", outfile) + varfont.save(outfile) + + +if __name__ == "__main__": + import sys + if len(sys.argv) > 1: + sys.exit(main()) + import doctest + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/varLib/mvar.py b/Lib/fontTools/varLib/mvar.py new file mode 100644 index 0000000..92083dd --- /dev/null +++ b/Lib/fontTools/varLib/mvar.py @@ -0,0 +1,44 @@ +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.misc.py23 import * + +MVAR_ENTRIES = { + 'hasc': ('OS/2', 'sTypoAscender'), # horizontal ascender + 'hdsc': ('OS/2', 'sTypoDescender'), # horizontal descender + 'hlgp': ('OS/2', 'sTypoLineGap'), # horizontal line gap + 'hcla': ('OS/2', 'usWinAscent'), # horizontal clipping ascent + 'hcld': ('OS/2', 'usWinDescent'), # horizontal clipping descent + 'vasc': ('vhea', 'ascent'), # vertical ascender + 'vdsc': ('vhea', 'descent'), # vertical descender + 'vlgp': ('vhea', 'lineGap'), # vertical line gap + 'hcrs': ('hhea', 'caretSlopeRise'), # horizontal caret rise + 'hcrn': ('hhea', 'caretSlopeRun'), # horizontal caret run + 'hcof': ('hhea', 'caretOffset'), # horizontal caret offset + 'vcrs': ('vhea', 'caretSlopeRise'), # vertical caret rise + 'vcrn': ('vhea', 'caretSlopeRun'), # vertical caret run + 'vcof': ('vhea', 'caretOffset'), # vertical caret offset + 'xhgt': ('OS/2', 'sxHeight'), # x height + 'cpht': ('OS/2', 'sCapHeight'), # cap height + 'sbxs': ('OS/2', 'ySubscriptXSize'), # subscript em x size + 'sbys': ('OS/2', 'ySubscriptYSize'), # subscript em y size + 'sbxo': ('OS/2', 'ySubscriptXOffset'), # subscript em x offset + 'sbyo': ('OS/2', 'ySubscriptYOffset'), # subscript em y offset + 'spxs': ('OS/2', 'ySuperscriptXSize'), # superscript em x size + 'spys': ('OS/2', 'ySuperscriptYSize'), # superscript em y size + 'spxo': ('OS/2', 'ySuperscriptXOffset'), # superscript em x offset + 'spyo': ('OS/2', 'ySuperscriptYOffset'), # superscript em y offset + 'strs': ('OS/2', 'yStrikeoutSize'), # strikeout size + 'stro': ('OS/2', 'yStrikeoutPosition'), # strikeout offset + 'unds': ('post', 'underlineThickness'), # underline size + 'undo': ('post', 'underlinePosition'), # underline offset + #'gsp0': ('gasp', 'gaspRange[0].rangeMaxPPEM'), # gaspRange[0] + #'gsp1': ('gasp', 'gaspRange[1].rangeMaxPPEM'), # gaspRange[1] + #'gsp2': ('gasp', 'gaspRange[2].rangeMaxPPEM'), # gaspRange[2] + #'gsp3': ('gasp', 'gaspRange[3].rangeMaxPPEM'), # gaspRange[3] + #'gsp4': ('gasp', 'gaspRange[4].rangeMaxPPEM'), # gaspRange[4] + #'gsp5': ('gasp', 'gaspRange[5].rangeMaxPPEM'), # gaspRange[5] + #'gsp6': ('gasp', 'gaspRange[6].rangeMaxPPEM'), # gaspRange[6] + #'gsp7': ('gasp', 'gaspRange[7].rangeMaxPPEM'), # gaspRange[7] + #'gsp8': ('gasp', 'gaspRange[8].rangeMaxPPEM'), # gaspRange[8] + #'gsp9': ('gasp', 'gaspRange[9].rangeMaxPPEM'), # gaspRange[9] +} diff --git a/Lib/fontTools/varLib/plot.py b/Lib/fontTools/varLib/plot.py new file mode 100644 index 0000000..b03744e --- /dev/null +++ b/Lib/fontTools/varLib/plot.py @@ -0,0 +1,118 @@ +"""Visualize DesignSpaceDocument and resulting VariationModel.""" + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.varLib.models import VariationModel, supportScalar +from fontTools.designspaceLib import DesignSpaceDocument +from mpl_toolkits.mplot3d import axes3d +from matplotlib import pyplot +from itertools import cycle +import math +import logging +import sys + +log = logging.getLogger(__name__) + + +def stops(support, count=10): + a,b,c = support + + return [a + (b - a) * i / count for i in range(count)] + \ + [b + (c - b) * i / count for i in range(count)] + \ + [c] + +def plotLocations(locations, axes, axis3D, **kwargs): + for loc,color in zip(locations, cycle(pyplot.cm.Set1.colors)): + axis3D.plot([loc.get(axes[0], 0)], + [loc.get(axes[1], 0)], + [1.], + 'o', + color=color, + **kwargs) + +def plotLocationsSurfaces(locations, fig, names=None, **kwargs): + + assert len(locations[0].keys()) == 2 + + if names is None: + names = [''] + + n = len(locations) + cols = math.ceil(n**.5) + rows = math.ceil(n / cols) + + model = VariationModel(locations) + names = [names[model.reverseMapping[i]] for i in range(len(names))] + + ax1, ax2 = sorted(locations[0].keys()) + for i, (support,color, name) in enumerate(zip(model.supports, cycle(pyplot.cm.Set1.colors), cycle(names))): + + axis3D = fig.add_subplot(rows, cols, i + 1, projection='3d') + axis3D.set_title(name) + axis3D.set_xlabel(ax1) + axis3D.set_ylabel(ax2) + pyplot.xlim(-1.,+1.) + pyplot.ylim(-1.,+1.) + + Xs = support.get(ax1, (-1.,0.,+1.)) + Ys = support.get(ax2, (-1.,0.,+1.)) + for x in stops(Xs): + X, Y, Z = [], [], [] + for y in Ys: + z = supportScalar({ax1:x, ax2:y}, support) + X.append(x) + Y.append(y) + Z.append(z) + axis3D.plot(X, Y, Z, color=color, **kwargs) + for y in stops(Ys): + X, Y, Z = [], [], [] + for x in Xs: + z = supportScalar({ax1:x, ax2:y}, support) + X.append(x) + Y.append(y) + Z.append(z) + axis3D.plot(X, Y, Z, color=color, **kwargs) + + plotLocations(model.locations, [ax1, ax2], axis3D) + + +def plotDocument(doc, fig, **kwargs): + doc.normalize() + locations = [s.location for s in doc.sources] + names = [s.name for s in doc.sources] + plotLocationsSurfaces(locations, fig, names, **kwargs) + + +def main(args=None): + from fontTools import configLogger + + if args is None: + args = sys.argv[1:] + + # configure the library logger (for >= WARNING) + configLogger() + # comment this out to enable debug messages from logger + # log.setLevel(logging.DEBUG) + + if len(args) < 1: + print("usage: fonttools varLib.plot source.designspace", file=sys.stderr) + print(" or") + print("usage: fonttools varLib.plot location1 location2 ...", file=sys.stderr) + sys.exit(1) + + fig = pyplot.figure() + + if len(args) == 1 and args[0].endswith('.designspace'): + doc = DesignSpaceDocument() + doc.read(args[0]) + plotDocument(doc, fig) + else: + axes = [chr(c) for c in range(ord('A'), ord('Z')+1)] + locs = [dict(zip(axes, (float(v) for v in s.split(',')))) for s in args] + plotLocationsSurfaces(locs, fig) + + pyplot.show() + +if __name__ == '__main__': + import sys + sys.exit(main()) diff --git a/Lib/fontTools/varLib/varStore.py b/Lib/fontTools/varLib/varStore.py new file mode 100644 index 0000000..a1ca7df --- /dev/null +++ b/Lib/fontTools/varLib/varStore.py @@ -0,0 +1,520 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import otRound +from fontTools.ttLib.tables import otTables as ot +from fontTools.varLib.models import supportScalar +from fontTools.varLib.builder import (buildVarRegionList, buildVarStore, + buildVarRegion, buildVarData, + VarData_CalculateNumShorts) +from functools import partial +from collections import defaultdict +from array import array + + +def _getLocationKey(loc): + return tuple(sorted(loc.items(), key=lambda kv: kv[0])) + + +class OnlineVarStoreBuilder(object): + + def __init__(self, axisTags): + self._axisTags = axisTags + self._regionMap = {} + self._regionList = buildVarRegionList([], axisTags) + self._store = buildVarStore(self._regionList, []) + self._data = None + self._model = None + self._cache = {} + + def setModel(self, model): + self._model = model + self._cache = {} # Empty cached items + + def finish(self, optimize=True): + self._regionList.RegionCount = len(self._regionList.Region) + self._store.VarDataCount = len(self._store.VarData) + for data in self._store.VarData: + data.ItemCount = len(data.Item) + VarData_CalculateNumShorts(data, optimize) + return self._store + + def _add_VarData(self): + regionMap = self._regionMap + regionList = self._regionList + + regions = self._model.supports[1:] + regionIndices = [] + for region in regions: + key = _getLocationKey(region) + idx = regionMap.get(key) + if idx is None: + varRegion = buildVarRegion(region, self._axisTags) + idx = regionMap[key] = len(regionList.Region) + regionList.Region.append(varRegion) + regionIndices.append(idx) + + data = self._data = buildVarData(regionIndices, [], optimize=False) + self._outer = len(self._store.VarData) + self._store.VarData.append(data) + + def storeMasters(self, master_values): + deltas = [otRound(d) for d in self._model.getDeltas(master_values)] + base = deltas.pop(0) + deltas = tuple(deltas) + varIdx = self._cache.get(deltas) + if varIdx is not None: + return base, varIdx + + if not self._data: + self._add_VarData() + inner = len(self._data.Item) + if inner == 0xFFFF: + # Full array. Start new one. + self._add_VarData() + return self.storeMasters(master_values) + self._data.Item.append(deltas) + + varIdx = (self._outer << 16) + inner + self._cache[deltas] = varIdx + return base, varIdx + + +def VarRegion_get_support(self, fvar_axes): + return {fvar_axes[i].axisTag: (reg.StartCoord,reg.PeakCoord,reg.EndCoord) + for i,reg in enumerate(self.VarRegionAxis)} + +class VarStoreInstancer(object): + + def __init__(self, varstore, fvar_axes, location={}): + self.fvar_axes = fvar_axes + assert varstore is None or varstore.Format == 1 + self._varData = varstore.VarData if varstore else [] + self._regions = varstore.VarRegionList.Region if varstore else [] + self.setLocation(location) + + def setLocation(self, location): + self.location = dict(location) + self._clearCaches() + + def _clearCaches(self): + self._scalars = {} + + def _getScalar(self, regionIdx): + scalar = self._scalars.get(regionIdx) + if scalar is None: + support = VarRegion_get_support(self._regions[regionIdx], self.fvar_axes) + scalar = supportScalar(self.location, support) + self._scalars[regionIdx] = scalar + return scalar + + def __getitem__(self, varidx): + + major, minor = varidx >> 16, varidx & 0xFFFF + + varData = self._varData + scalars = [self._getScalar(ri) for ri in varData[major].VarRegionIndex] + + deltas = varData[major].Item[minor] + delta = 0. + for d,s in zip(deltas, scalars): + delta += d * s + return delta + + +# +# Optimizations +# + +def VarStore_subset_varidxes(self, varIdxes, optimize=True): + + # Sort out used varIdxes by major/minor. + used = {} + for varIdx in varIdxes: + major = varIdx >> 16 + minor = varIdx & 0xFFFF + d = used.get(major) + if d is None: + d = used[major] = set() + d.add(minor) + del varIdxes + + # + # Subset VarData + # + + varData = self.VarData + newVarData = [] + varDataMap = {} + for major,data in enumerate(varData): + usedMinors = used.get(major) + if usedMinors is None: + continue + newMajor = varDataMap[major] = len(newVarData) + newVarData.append(data) + + items = data.Item + newItems = [] + for minor in sorted(usedMinors): + newMinor = len(newItems) + newItems.append(items[minor]) + varDataMap[(major<<16)+minor] = (newMajor<<16)+newMinor + + data.Item = newItems + data.ItemCount = len(data.Item) + + if optimize: + VarData_CalculateNumShorts(data) + + self.VarData = newVarData + self.VarDataCount = len(self.VarData) + + self.prune_regions() + + return varDataMap + +ot.VarStore.subset_varidxes = VarStore_subset_varidxes + +def VarStore_prune_regions(self): + """Remove unused VarRegions.""" + # + # Subset VarRegionList + # + + # Collect. + usedRegions = set() + for data in self.VarData: + usedRegions.update(data.VarRegionIndex) + # Subset. + regionList = self.VarRegionList + regions = regionList.Region + newRegions = [] + regionMap = {} + for i in sorted(usedRegions): + regionMap[i] = len(newRegions) + newRegions.append(regions[i]) + regionList.Region = newRegions + regionList.RegionCount = len(regionList.Region) + # Map. + for data in self.VarData: + data.VarRegionIndex = [regionMap[i] for i in data.VarRegionIndex] + +ot.VarStore.prune_regions = VarStore_prune_regions + + +def _visit(self, objType, func): + """Recurse down from self, if type of an object is objType, + call func() on it. Only works for otData-style classes.""" + + if type(self) == objType: + func(self) + return # We don't recurse down; don't need to. + + if isinstance(self, list): + for that in self: + _visit(that, objType, func) + + if hasattr(self, 'getConverters'): + for conv in self.getConverters(): + that = getattr(self, conv.name, None) + if that is not None: + _visit(that, objType, func) + + if isinstance(self, ot.ValueRecord): + for that in self.__dict__.values(): + _visit(that, objType, func) + +def _Device_recordVarIdx(self, s): + """Add VarIdx in this Device table (if any) to the set s.""" + if self.DeltaFormat == 0x8000: + s.add((self.StartSize<<16)+self.EndSize) + +def Object_collect_device_varidxes(self, varidxes): + adder = partial(_Device_recordVarIdx, s=varidxes) + _visit(self, ot.Device, adder) + +ot.GDEF.collect_device_varidxes = Object_collect_device_varidxes +ot.GPOS.collect_device_varidxes = Object_collect_device_varidxes + +def _Device_mapVarIdx(self, mapping, done): + """Add VarIdx in this Device table (if any) to the set s.""" + if id(self) in done: + return + done.add(id(self)) + if self.DeltaFormat == 0x8000: + varIdx = mapping[(self.StartSize<<16)+self.EndSize] + self.StartSize = varIdx >> 16 + self.EndSize = varIdx & 0xFFFF + +def Object_remap_device_varidxes(self, varidxes_map): + mapper = partial(_Device_mapVarIdx, mapping=varidxes_map, done=set()) + _visit(self, ot.Device, mapper) + +ot.GDEF.remap_device_varidxes = Object_remap_device_varidxes +ot.GPOS.remap_device_varidxes = Object_remap_device_varidxes + + +class _Encoding(object): + + def __init__(self, chars): + self.chars = chars + self.width = self._popcount(chars) + self.overhead = self._characteristic_overhead(chars) + self.items = set() + + def append(self, row): + self.items.add(row) + + def extend(self, lst): + self.items.update(lst) + + def get_room(self): + """Maximum number of bytes that can be added to characteristic + while still being beneficial to merge it into another one.""" + count = len(self.items) + return max(0, (self.overhead - 1) // count - self.width) + room = property(get_room) + + @property + def gain(self): + """Maximum possible byte gain from merging this into another + characteristic.""" + count = len(self.items) + return max(0, self.overhead - count * (self.width + 1)) + + def sort_key(self): + return self.width, self.chars + + def __len__(self): + return len(self.items) + + def can_encode(self, chars): + return not (chars & ~self.chars) + + def __sub__(self, other): + return self._popcount(self.chars & ~other.chars) + + @staticmethod + def _popcount(n): + # Apparently this is the fastest native way to do it... + # https://stackoverflow.com/a/9831671 + return bin(n).count('1') + + @staticmethod + def _characteristic_overhead(chars): + """Returns overhead in bytes of encoding this characteristic + as a VarData.""" + c = 6 + while chars: + if chars & 3: + c += 2 + chars >>= 2 + return c + + + def _find_yourself_best_new_encoding(self, done_by_width): + self.best_new_encoding = None + for new_width in range(self.width+1, self.width+self.room+1): + for new_encoding in done_by_width[new_width]: + if new_encoding.can_encode(self.chars): + break + else: + new_encoding = None + self.best_new_encoding = new_encoding + + +class _EncodingDict(dict): + + def __missing__(self, chars): + r = self[chars] = _Encoding(chars) + return r + + def add_row(self, row): + chars = self._row_characteristics(row) + self[chars].append(row) + + @staticmethod + def _row_characteristics(row): + """Returns encoding characteristics for a row.""" + chars = 0 + i = 1 + for v in row: + if v: + chars += i + if not (-128 <= v <= 127): + chars += i * 2 + i <<= 2 + return chars + + +def VarStore_optimize(self): + """Optimize storage. Returns mapping from old VarIdxes to new ones.""" + + # TODO + # Check that no two VarRegions are the same; if they are, fold them. + + n = len(self.VarRegionList.Region) # Number of columns + zeroes = array('h', [0]*n) + + front_mapping = {} # Map from old VarIdxes to full row tuples + + encodings = _EncodingDict() + + # Collect all items into a set of full rows (with lots of zeroes.) + for major,data in enumerate(self.VarData): + regionIndices = data.VarRegionIndex + + for minor,item in enumerate(data.Item): + + row = array('h', zeroes) + for regionIdx,v in zip(regionIndices, item): + row[regionIdx] += v + row = tuple(row) + + encodings.add_row(row) + front_mapping[(major<<16)+minor] = row + + # Separate encodings that have no gain (are decided) and those having + # possible gain (possibly to be merged into others.) + encodings = sorted(encodings.values(), key=_Encoding.__len__, reverse=True) + done_by_width = defaultdict(list) + todo = [] + for encoding in encodings: + if not encoding.gain: + done_by_width[encoding.width].append(encoding) + else: + todo.append(encoding) + + # For each encoding that is possibly to be merged, find the best match + # in the decided encodings, and record that. + todo.sort(key=_Encoding.get_room) + for encoding in todo: + encoding._find_yourself_best_new_encoding(done_by_width) + + # Walk through todo encodings, for each, see if merging it with + # another todo encoding gains more than each of them merging with + # their best decided encoding. If yes, merge them and add resulting + # encoding back to todo queue. If not, move the enconding to decided + # list. Repeat till done. + while todo: + encoding = todo.pop() + best_idx = None + best_gain = 0 + for i,other_encoding in enumerate(todo): + combined_chars = other_encoding.chars | encoding.chars + combined_width = _Encoding._popcount(combined_chars) + combined_overhead = _Encoding._characteristic_overhead(combined_chars) + combined_gain = ( + + encoding.overhead + + other_encoding.overhead + - combined_overhead + - (combined_width - encoding.width) * len(encoding) + - (combined_width - other_encoding.width) * len(other_encoding) + ) + this_gain = 0 if encoding.best_new_encoding is None else ( + + encoding.overhead + - (encoding.best_new_encoding.width - encoding.width) * len(encoding) + ) + other_gain = 0 if other_encoding.best_new_encoding is None else ( + + other_encoding.overhead + - (other_encoding.best_new_encoding.width - other_encoding.width) * len(other_encoding) + ) + separate_gain = this_gain + other_gain + + if combined_gain > separate_gain: + best_idx = i + best_gain = combined_gain - separate_gain + + if best_idx is None: + # Encoding is decided as is + done_by_width[encoding.width].append(encoding) + else: + other_encoding = todo[best_idx] + combined_chars = other_encoding.chars | encoding.chars + combined_encoding = _Encoding(combined_chars) + combined_encoding.extend(encoding.items) + combined_encoding.extend(other_encoding.items) + combined_encoding._find_yourself_best_new_encoding(done_by_width) + del todo[best_idx] + todo.append(combined_encoding) + + # Assemble final store. + back_mapping = {} # Mapping from full rows to new VarIdxes + encodings = sum(done_by_width.values(), []) + encodings.sort(key=_Encoding.sort_key) + self.VarData = [] + for major,encoding in enumerate(encodings): + data = ot.VarData() + self.VarData.append(data) + data.VarRegionIndex = range(n) + data.VarRegionCount = len(data.VarRegionIndex) + data.Item = sorted(encoding.items) + for minor,item in enumerate(data.Item): + back_mapping[item] = (major<<16)+minor + + # Compile final mapping. + varidx_map = {} + for k,v in front_mapping.items(): + varidx_map[k] = back_mapping[v] + + # Remove unused regions. + self.prune_regions() + + # Recalculate things and go home. + self.VarRegionList.RegionCount = len(self.VarRegionList.Region) + self.VarDataCount = len(self.VarData) + for data in self.VarData: + data.ItemCount = len(data.Item) + VarData_CalculateNumShorts(data) + + return varidx_map + +ot.VarStore.optimize = VarStore_optimize + + +def main(args=None): + from argparse import ArgumentParser + from fontTools import configLogger + from fontTools.ttLib import TTFont + from fontTools.ttLib.tables.otBase import OTTableWriter + + parser = ArgumentParser(prog='varLib.varStore') + parser.add_argument('fontfile') + parser.add_argument('outfile', nargs='?') + options = parser.parse_args(args) + + # TODO: allow user to configure logging via command-line options + configLogger(level="INFO") + + fontfile = options.fontfile + outfile = options.outfile + + font = TTFont(fontfile) + gdef = font['GDEF'] + store = gdef.table.VarStore + + writer = OTTableWriter() + store.compile(writer, font) + size = len(writer.getAllData()) + print("Before: %7d bytes" % size) + + varidx_map = store.optimize() + + gdef.table.remap_device_varidxes(varidx_map) + if 'GPOS' in font: + font['GPOS'].table.remap_device_varidxes(varidx_map) + + writer = OTTableWriter() + store.compile(writer, font) + size = len(writer.getAllData()) + print("After: %7d bytes" % size) + + if outfile is not None: + font.save(outfile) + + +if __name__ == "__main__": + import sys + if len(sys.argv) > 1: + sys.exit(main()) + import doctest + sys.exit(doctest.testmod().failed) diff --git a/Lib/fontTools/voltLib/__init__.py b/Lib/fontTools/voltLib/__init__.py new file mode 100644 index 0000000..886aa3a --- /dev/null +++ b/Lib/fontTools/voltLib/__init__.py @@ -0,0 +1,5 @@ +"""fontTools.voltLib -- a package for dealing with Visual OpenType Layout Tool +(VOLT) files.""" + +# See +# http://www.microsoft.com/typography/VOLT.mspx diff --git a/Lib/fontTools/voltLib/ast.py b/Lib/fontTools/voltLib/ast.py new file mode 100644 index 0000000..4e78600 --- /dev/null +++ b/Lib/fontTools/voltLib/ast.py @@ -0,0 +1,253 @@ +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.voltLib.error import VoltLibError + + +class Statement(object): + def __init__(self, location=None): + self.location = location + + def build(self, builder): + pass + + +class Expression(object): + def __init__(self, location=None): + self.location = location + + def build(self, builder): + pass + + +class Block(Statement): + def __init__(self, location=None): + Statement.__init__(self, location) + self.statements = [] + + def build(self, builder): + for s in self.statements: + s.build(builder) + + +class VoltFile(Block): + def __init__(self): + Block.__init__(self, location=None) + + +class LookupBlock(Block): + def __init__(self, name, location=None): + Block.__init__(self, location) + self.name = name + + def build(self, builder): + builder.start_lookup_block(self.location, self.name) + Block.build(self, builder) + builder.end_lookup_block() + + +class GlyphDefinition(Statement): + def __init__(self, name, gid, gunicode, gtype, components, location=None): + Statement.__init__(self, location) + self.name = name + self.id = gid + self.unicode = gunicode + self.type = gtype + self.components = components + + +class GroupDefinition(Statement): + def __init__(self, name, enum, location=None): + Statement.__init__(self, location) + self.name = name + self.enum = enum + self.glyphs_ = None + + def glyphSet(self, groups=None): + if groups is not None and self.name in groups: + raise VoltLibError( + 'Group "%s" contains itself.' % (self.name), + self.location) + if self.glyphs_ is None: + if groups is None: + groups = set({self.name}) + else: + groups.add(self.name) + self.glyphs_ = self.enum.glyphSet(groups) + return self.glyphs_ + + +class GlyphName(Expression): + """A single glyph name, such as cedilla.""" + def __init__(self, glyph, location=None): + Expression.__init__(self, location) + self.glyph = glyph + + def glyphSet(self): + return frozenset((self.glyph,)) + + +class Enum(Expression): + """An enum""" + def __init__(self, enum, location=None): + Expression.__init__(self, location) + self.enum = enum + + def __iter__(self): + for e in self.glyphSet(): + yield e + + def glyphSet(self, groups=None): + glyphs = set() + for element in self.enum: + if isinstance(element, (GroupName, Enum)): + glyphs = glyphs.union(element.glyphSet(groups)) + else: + glyphs = glyphs.union(element.glyphSet()) + return frozenset(glyphs) + + +class GroupName(Expression): + """A glyph group""" + def __init__(self, group, parser, location=None): + Expression.__init__(self, location) + self.group = group + self.parser_ = parser + + def glyphSet(self, groups=None): + group = self.parser_.resolve_group(self.group) + if group is not None: + self.glyphs_ = group.glyphSet(groups) + return self.glyphs_ + else: + raise VoltLibError( + 'Group "%s" is used but undefined.' % (self.group), + self.location) + + +class Range(Expression): + """A glyph range""" + def __init__(self, start, end, parser, location=None): + Expression.__init__(self, location) + self.start = start + self.end = end + self.parser = parser + + def glyphSet(self): + glyphs = self.parser.glyph_range(self.start, self.end) + return frozenset(glyphs) + + +class ScriptDefinition(Statement): + def __init__(self, name, tag, langs, location=None): + Statement.__init__(self, location) + self.name = name + self.tag = tag + self.langs = langs + + +class LangSysDefinition(Statement): + def __init__(self, name, tag, features, location=None): + Statement.__init__(self, location) + self.name = name + self.tag = tag + self.features = features + + +class FeatureDefinition(Statement): + def __init__(self, name, tag, lookups, location=None): + Statement.__init__(self, location) + self.name = name + self.tag = tag + self.lookups = lookups + + +class LookupDefinition(Statement): + def __init__(self, name, process_base, process_marks, direction, + reversal, comments, context, sub, pos, location=None): + Statement.__init__(self, location) + self.name = name + self.process_base = process_base + self.process_marks = process_marks + self.direction = direction + self.reversal = reversal + self.comments = comments + self.context = context + self.sub = sub + self.pos = pos + + +class SubstitutionDefinition(Statement): + def __init__(self, mapping, location=None): + Statement.__init__(self, location) + self.mapping = mapping + + +class SubstitutionSingleDefinition(SubstitutionDefinition): + pass + + +class SubstitutionMultipleDefinition(SubstitutionDefinition): + pass + + +class SubstitutionLigatureDefinition(SubstitutionDefinition): + pass + + +class SubstitutionReverseChainingSingleDefinition(SubstitutionDefinition): + pass + + +class PositionAttachDefinition(Statement): + def __init__(self, coverage, coverage_to, location=None): + Statement.__init__(self, location) + self.coverage = coverage + self.coverage_to = coverage_to + + +class PositionAttachCursiveDefinition(Statement): + def __init__(self, coverages_exit, coverages_enter, location=None): + Statement.__init__(self, location) + self.coverages_exit = coverages_exit + self.coverages_enter = coverages_enter + + +class PositionAdjustPairDefinition(Statement): + def __init__(self, coverages_1, coverages_2, adjust_pair, location=None): + Statement.__init__(self, location) + self.coverages_1 = coverages_1 + self.coverages_2 = coverages_2 + self.adjust_pair = adjust_pair + + +class PositionAdjustSingleDefinition(Statement): + def __init__(self, adjust_single, location=None): + Statement.__init__(self, location) + self.adjust_single = adjust_single + + +class ContextDefinition(Statement): + def __init__(self, ex_or_in, left=None, right=None, location=None): + Statement.__init__(self, location) + self.ex_or_in = ex_or_in + self.left = left if left is not None else [] + self.right = right if right is not None else [] + + +class AnchorDefinition(Statement): + def __init__(self, name, gid, glyph_name, component, locked, + pos, location=None): + Statement.__init__(self, location) + self.name = name + self.gid = gid + self.glyph_name = glyph_name + self.component = component + self.locked = locked + self.pos = pos + + +class SettingDefinition(Statement): + def __init__(self, name, value, location=None): + Statement.__init__(self, location) + self.name = name + self.value = value diff --git a/Lib/fontTools/voltLib/error.py b/Lib/fontTools/voltLib/error.py new file mode 100644 index 0000000..f9900ad --- /dev/null +++ b/Lib/fontTools/voltLib/error.py @@ -0,0 +1,16 @@ +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals + + +class VoltLibError(Exception): + def __init__(self, message, location): + Exception.__init__(self, message) + self.location = location + + def __str__(self): + message = Exception.__str__(self) + if self.location: + path, line, column = self.location + return "%s:%d:%d: %s" % (path, line, column, message) + else: + return message diff --git a/Lib/fontTools/voltLib/lexer.py b/Lib/fontTools/voltLib/lexer.py new file mode 100644 index 0000000..271fe3b --- /dev/null +++ b/Lib/fontTools/voltLib/lexer.py @@ -0,0 +1,98 @@ +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.voltLib.error import VoltLibError + +class Lexer(object): + NUMBER = "NUMBER" + STRING = "STRING" + NAME = "NAME" + NEWLINE = "NEWLINE" + + CHAR_WHITESPACE_ = " \t" + CHAR_NEWLINE_ = "\r\n" + CHAR_DIGIT_ = "0123456789" + CHAR_UC_LETTER_ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + CHAR_LC_LETTER_ = "abcdefghijklmnopqrstuvwxyz" + CHAR_UNDERSCORE_ = "_" + CHAR_PERIOD_ = "." + CHAR_NAME_START_ = CHAR_UC_LETTER_ + CHAR_LC_LETTER_ + CHAR_PERIOD_ + \ + CHAR_UNDERSCORE_ + CHAR_NAME_CONTINUATION_ = CHAR_NAME_START_ + CHAR_DIGIT_ + + def __init__(self, text, filename): + self.filename_ = filename + self.line_ = 1 + self.pos_ = 0 + self.line_start_ = 0 + self.text_ = text + self.text_length_ = len(text) + + def __iter__(self): + return self + + def next(self): # Python 2 + return self.__next__() + + def __next__(self): # Python 3 + while True: + token_type, token, location = self.next_() + if token_type not in {Lexer.NEWLINE}: + return (token_type, token, location) + + def next_(self): + self.scan_over_(Lexer.CHAR_WHITESPACE_) + column = self.pos_ - self.line_start_ + 1 + location = (self.filename_, self.line_, column) + start = self.pos_ + text = self.text_ + limit = len(text) + if start >= limit: + raise StopIteration() + cur_char = text[start] + next_char = text[start + 1] if start + 1 < limit else None + + if cur_char == "\n": + self.pos_ += 1 + self.line_ += 1 + self.line_start_ = self.pos_ + return (Lexer.NEWLINE, None, location) + if cur_char == "\r": + self.pos_ += (2 if next_char == "\n" else 1) + self.line_ += 1 + self.line_start_ = self.pos_ + return (Lexer.NEWLINE, None, location) + if cur_char == '"': + self.pos_ += 1 + self.scan_until_('"\r\n') + if self.pos_ < self.text_length_ and self.text_[self.pos_] == '"': + self.pos_ += 1 + return (Lexer.STRING, text[start + 1:self.pos_ - 1], location) + else: + raise VoltLibError("Expected '\"' to terminate string", + location) + if cur_char in Lexer.CHAR_NAME_START_: + self.pos_ += 1 + self.scan_over_(Lexer.CHAR_NAME_CONTINUATION_) + token = text[start:self.pos_] + return (Lexer.NAME, token, location) + if cur_char in Lexer.CHAR_DIGIT_: + self.scan_over_(Lexer.CHAR_DIGIT_) + return (Lexer.NUMBER, int(text[start:self.pos_], 10), location) + if cur_char == "-" and next_char in Lexer.CHAR_DIGIT_: + self.pos_ += 1 + self.scan_over_(Lexer.CHAR_DIGIT_) + return (Lexer.NUMBER, int(text[start:self.pos_], 10), location) + raise VoltLibError("Unexpected character: '%s'" % cur_char, + location) + + def scan_over_(self, valid): + p = self.pos_ + while p < self.text_length_ and self.text_[p] in valid: + p += 1 + self.pos_ = p + + def scan_until_(self, stop_at): + p = self.pos_ + while p < self.text_length_ and self.text_[p] not in stop_at: + p += 1 + self.pos_ = p diff --git a/Lib/fontTools/voltLib/parser.py b/Lib/fontTools/voltLib/parser.py new file mode 100644 index 0000000..db3ccf3 --- /dev/null +++ b/Lib/fontTools/voltLib/parser.py @@ -0,0 +1,651 @@ +from __future__ import ( + print_function, division, absolute_import, unicode_literals) +from collections import OrderedDict +import fontTools.voltLib.ast as ast +from fontTools.voltLib.lexer import Lexer +from fontTools.voltLib.error import VoltLibError +from io import open + +PARSE_FUNCS = { + "DEF_GLYPH": "parse_def_glyph_", + "DEF_GROUP": "parse_def_group_", + "DEF_SCRIPT": "parse_def_script_", + "DEF_LOOKUP": "parse_def_lookup_", + "DEF_ANCHOR": "parse_def_anchor_", + "GRID_PPEM": "parse_ppem_", + "PRESENTATION_PPEM": "parse_ppem_", + "PPOSITIONING_PPEM": "parse_ppem_", + "COMPILER_USEEXTENSIONLOOKUPS": "parse_compiler_flag_", + "COMPILER_USEPAIRPOSFORMAT2": "parse_compiler_flag_", + "CMAP_FORMAT": "parse_cmap_format", +} + + +class Parser(object): + def __init__(self, path): + self.doc_ = ast.VoltFile() + self.glyphs_ = OrderedSymbolTable() + self.groups_ = SymbolTable() + self.anchors_ = {} # dictionary of SymbolTable() keyed by glyph + self.scripts_ = SymbolTable() + self.langs_ = SymbolTable() + self.lookups_ = SymbolTable() + self.next_token_type_, self.next_token_ = (None, None) + self.next_token_location_ = None + with open(path, "r") as f: + self.lexer_ = Lexer(f.read(), path) + self.advance_lexer_() + + def parse(self): + statements = self.doc_.statements + while self.next_token_type_ is not None: + self.advance_lexer_() + if self.cur_token_ in PARSE_FUNCS.keys(): + func = getattr(self, PARSE_FUNCS[self.cur_token_]) + statements.append(func()) + elif self.is_cur_keyword_("END"): + if self.next_token_type_ is not None: + raise VoltLibError("Expected the end of the file", + self.cur_token_location_) + return self.doc_ + else: + raise VoltLibError( + "Expected " + ", ".join(sorted(PARSE_FUNCS.keys())), + self.cur_token_location_) + return self.doc_ + + def parse_def_glyph_(self): + assert self.is_cur_keyword_("DEF_GLYPH") + location = self.cur_token_location_ + name = self.expect_string_() + self.expect_keyword_("ID") + gid = self.expect_number_() + if gid < 0: + raise VoltLibError("Invalid glyph ID", self.cur_token_location_) + gunicode = None + if self.next_token_ == "UNICODE": + self.expect_keyword_("UNICODE") + gunicode = [self.expect_number_()] + if gunicode[0] < 0: + raise VoltLibError("Invalid glyph UNICODE", + self.cur_token_location_) + elif self.next_token_ == "UNICODEVALUES": + self.expect_keyword_("UNICODEVALUES") + gunicode = self.parse_unicode_values_() + gtype = None + if self.next_token_ == "TYPE": + self.expect_keyword_("TYPE") + gtype = self.expect_name_() + assert gtype in ("BASE", "LIGATURE", "MARK") + components = None + if self.next_token_ == "COMPONENTS": + self.expect_keyword_("COMPONENTS") + components = self.expect_number_() + self.expect_keyword_("END_GLYPH") + if self.glyphs_.resolve(name) is not None: + raise VoltLibError( + 'Glyph "%s" (gid %i) already defined' % (name, gid), + location + ) + def_glyph = ast.GlyphDefinition(name, gid, + gunicode, gtype, components, + location=location) + self.glyphs_.define(name, def_glyph) + return def_glyph + + def parse_def_group_(self): + assert self.is_cur_keyword_("DEF_GROUP") + location = self.cur_token_location_ + name = self.expect_string_() + enum = None + if self.next_token_ == "ENUM": + self.expect_keyword_("ENUM") + enum = self.parse_enum_() + self.expect_keyword_("END_GROUP") + if self.groups_.resolve(name) is not None: + raise VoltLibError( + 'Glyph group "%s" already defined, ' + 'group names are case insensitive' % name, + location + ) + def_group = ast.GroupDefinition(name, enum, + location=location) + self.groups_.define(name, def_group) + return def_group + + def parse_def_script_(self): + assert self.is_cur_keyword_("DEF_SCRIPT") + location = self.cur_token_location_ + name = None + if self.next_token_ == "NAME": + self.expect_keyword_("NAME") + name = self.expect_string_() + self.expect_keyword_("TAG") + tag = self.expect_string_() + if self.scripts_.resolve(tag) is not None: + raise VoltLibError( + 'Script "%s" already defined, ' + 'script tags are case insensitive' % tag, + location + ) + self.langs_.enter_scope() + langs = [] + while self.next_token_ != "END_SCRIPT": + self.advance_lexer_() + lang = self.parse_langsys_() + self.expect_keyword_("END_LANGSYS") + if self.langs_.resolve(lang.tag) is not None: + raise VoltLibError( + 'Language "%s" already defined in script "%s", ' + 'language tags are case insensitive' % (lang.tag, tag), + location + ) + self.langs_.define(lang.tag, lang) + langs.append(lang) + self.expect_keyword_("END_SCRIPT") + self.langs_.exit_scope() + def_script = ast.ScriptDefinition(name, tag, langs, location=location) + self.scripts_.define(tag, def_script) + return def_script + + def parse_langsys_(self): + assert self.is_cur_keyword_("DEF_LANGSYS") + location = self.cur_token_location_ + name = None + if self.next_token_ == "NAME": + self.expect_keyword_("NAME") + name = self.expect_string_() + self.expect_keyword_("TAG") + tag = self.expect_string_() + features = [] + while self.next_token_ != "END_LANGSYS": + self.advance_lexer_() + feature = self.parse_feature_() + self.expect_keyword_("END_FEATURE") + features.append(feature) + def_langsys = ast.LangSysDefinition(name, tag, features, + location=location) + return def_langsys + + def parse_feature_(self): + assert self.is_cur_keyword_("DEF_FEATURE") + location = self.cur_token_location_ + self.expect_keyword_("NAME") + name = self.expect_string_() + self.expect_keyword_("TAG") + tag = self.expect_string_() + lookups = [] + while self.next_token_ != "END_FEATURE": + # self.advance_lexer_() + self.expect_keyword_("LOOKUP") + lookup = self.expect_string_() + lookups.append(lookup) + feature = ast.FeatureDefinition(name, tag, lookups, + location=location) + return feature + + def parse_def_lookup_(self): + assert self.is_cur_keyword_("DEF_LOOKUP") + location = self.cur_token_location_ + name = self.expect_string_() + if not name[0].isalpha(): + raise VoltLibError( + 'Lookup name "%s" must start with a letter' % name, + location + ) + if self.lookups_.resolve(name) is not None: + raise VoltLibError( + 'Lookup "%s" already defined, ' + 'lookup names are case insensitive' % name, + location + ) + process_base = True + if self.next_token_ == "PROCESS_BASE": + self.advance_lexer_() + elif self.next_token_ == "SKIP_BASE": + self.advance_lexer_() + process_base = False + process_marks = True + if self.next_token_ == "PROCESS_MARKS": + self.advance_lexer_() + if self.next_token_ == "MARK_GLYPH_SET": + self.advance_lexer_() + process_marks = self.expect_string_() + elif self.next_token_type_ == Lexer.STRING: + process_marks = self.expect_string_() + elif self.next_token_ == "ALL": + self.advance_lexer_() + else: + raise VoltLibError( + "Expected ALL, MARK_GLYPH_SET or an ID. " + "Got %s" % (self.next_token_type_), + location) + elif self.next_token_ == "SKIP_MARKS": + self.advance_lexer_() + process_marks = False + direction = None + if self.next_token_ == "DIRECTION": + self.expect_keyword_("DIRECTION") + direction = self.expect_name_() + assert direction in ("LTR", "RTL") + reversal = None + if self.next_token_ == "REVERSAL": + self.expect_keyword_("REVERSAL") + reversal = True + comments = None + if self.next_token_ == "COMMENTS": + self.expect_keyword_("COMMENTS") + comments = self.expect_string_() + context = [] + while self.next_token_ in ("EXCEPT_CONTEXT", "IN_CONTEXT"): + context = self.parse_context_() + as_pos_or_sub = self.expect_name_() + sub = None + pos = None + if as_pos_or_sub == "AS_SUBSTITUTION": + sub = self.parse_substitution_(reversal) + elif as_pos_or_sub == "AS_POSITION": + pos = self.parse_position_() + else: + raise VoltLibError( + "Expected AS_SUBSTITUTION or AS_POSITION. " + "Got %s" % (as_pos_or_sub), + location) + def_lookup = ast.LookupDefinition( + name, process_base, process_marks, direction, reversal, + comments, context, sub, pos, location=location) + self.lookups_.define(name, def_lookup) + return def_lookup + + def parse_context_(self): + location = self.cur_token_location_ + contexts = [] + while self.next_token_ in ("EXCEPT_CONTEXT", "IN_CONTEXT"): + side = None + coverage = None + ex_or_in = self.expect_name_() + # side_contexts = [] # XXX + if self.next_token_ != "END_CONTEXT": + left = [] + right = [] + while self.next_token_ in ("LEFT", "RIGHT"): + side = self.expect_name_() + coverage = self.parse_coverage_() + if side == "LEFT": + left.append(coverage) + else: + right.append(coverage) + self.expect_keyword_("END_CONTEXT") + context = ast.ContextDefinition(ex_or_in, left, + right, location=location) + contexts.append(context) + else: + self.expect_keyword_("END_CONTEXT") + return contexts + + def parse_substitution_(self, reversal): + assert self.is_cur_keyword_("AS_SUBSTITUTION") + location = self.cur_token_location_ + src = [] + dest = [] + if self.next_token_ != "SUB": + raise VoltLibError("Expected SUB", location) + while self.next_token_ == "SUB": + self.expect_keyword_("SUB") + src.append(self.parse_coverage_()) + self.expect_keyword_("WITH") + dest.append(self.parse_coverage_()) + self.expect_keyword_("END_SUB") + self.expect_keyword_("END_SUBSTITUTION") + max_src = max([len(cov) for cov in src]) + max_dest = max([len(cov) for cov in dest]) + # many to many or mixed is invalid + if ((max_src > 1 and max_dest > 1) or + (reversal and (max_src > 1 or max_dest > 1))): + raise VoltLibError( + "Invalid substitution type", + location) + mapping = OrderedDict(zip(tuple(src), tuple(dest))) + if max_src == 1 and max_dest == 1: + if reversal: + sub = ast.SubstitutionReverseChainingSingleDefinition( + mapping, location=location) + else: + sub = ast.SubstitutionSingleDefinition(mapping, + location=location) + elif max_src == 1 and max_dest > 1: + sub = ast.SubstitutionMultipleDefinition(mapping, + location=location) + elif max_src > 1 and max_dest == 1: + sub = ast.SubstitutionLigatureDefinition(mapping, + location=location) + return sub + + def parse_position_(self): + assert self.is_cur_keyword_("AS_POSITION") + location = self.cur_token_location_ + pos_type = self.expect_name_() + if pos_type not in ( + "ATTACH", "ATTACH_CURSIVE", "ADJUST_PAIR", "ADJUST_SINGLE"): + raise VoltLibError( + "Expected ATTACH, ATTACH_CURSIVE, ADJUST_PAIR, ADJUST_SINGLE", + location) + if pos_type == "ATTACH": + position = self.parse_attach_() + elif pos_type == "ATTACH_CURSIVE": + position = self.parse_attach_cursive_() + elif pos_type == "ADJUST_PAIR": + position = self.parse_adjust_pair_() + elif pos_type == "ADJUST_SINGLE": + position = self.parse_adjust_single_() + self.expect_keyword_("END_POSITION") + return position + + def parse_attach_(self): + assert self.is_cur_keyword_("ATTACH") + location = self.cur_token_location_ + coverage = self.parse_coverage_() + coverage_to = [] + self.expect_keyword_("TO") + while self.next_token_ != "END_ATTACH": + cov = self.parse_coverage_() + self.expect_keyword_("AT") + self.expect_keyword_("ANCHOR") + anchor_name = self.expect_string_() + coverage_to.append((cov, anchor_name)) + self.expect_keyword_("END_ATTACH") + position = ast.PositionAttachDefinition( + coverage, coverage_to, location=location) + return position + + def parse_attach_cursive_(self): + assert self.is_cur_keyword_("ATTACH_CURSIVE") + location = self.cur_token_location_ + coverages_exit = [] + coverages_enter = [] + while self.next_token_ != "ENTER": + self.expect_keyword_("EXIT") + coverages_exit.append(self.parse_coverage_()) + while self.next_token_ != "END_ATTACH": + self.expect_keyword_("ENTER") + coverages_enter.append(self.parse_coverage_()) + self.expect_keyword_("END_ATTACH") + position = ast.PositionAttachCursiveDefinition( + coverages_exit, coverages_enter, location=location) + return position + + def parse_adjust_pair_(self): + assert self.is_cur_keyword_("ADJUST_PAIR") + location = self.cur_token_location_ + coverages_1 = [] + coverages_2 = [] + adjust_pair = {} + while self.next_token_ == "FIRST": + self.advance_lexer_() + coverage_1 = self.parse_coverage_() + coverages_1.append(coverage_1) + while self.next_token_ == "SECOND": + self.advance_lexer_() + coverage_2 = self.parse_coverage_() + coverages_2.append(coverage_2) + while self.next_token_ != "END_ADJUST": + id_1 = self.expect_number_() + id_2 = self.expect_number_() + self.expect_keyword_("BY") + pos_1 = self.parse_pos_() + pos_2 = self.parse_pos_() + adjust_pair[(id_1, id_2)] = (pos_1, pos_2) + self.expect_keyword_("END_ADJUST") + position = ast.PositionAdjustPairDefinition( + coverages_1, coverages_2, adjust_pair, location=location) + return position + + def parse_adjust_single_(self): + assert self.is_cur_keyword_("ADJUST_SINGLE") + location = self.cur_token_location_ + adjust_single = [] + while self.next_token_ != "END_ADJUST": + coverages = self.parse_coverage_() + self.expect_keyword_("BY") + pos = self.parse_pos_() + adjust_single.append((coverages, pos)) + self.expect_keyword_("END_ADJUST") + position = ast.PositionAdjustSingleDefinition( + adjust_single, location=location) + return position + + def parse_def_anchor_(self): + assert self.is_cur_keyword_("DEF_ANCHOR") + location = self.cur_token_location_ + name = self.expect_string_() + self.expect_keyword_("ON") + gid = self.expect_number_() + self.expect_keyword_("GLYPH") + glyph_name = self.expect_name_() + # check for duplicate anchor names on this glyph + if (glyph_name in self.anchors_ + and self.anchors_[glyph_name].resolve(name) is not None): + raise VoltLibError( + 'Anchor "%s" already defined, ' + 'anchor names are case insensitive' % name, + location + ) + self.expect_keyword_("COMPONENT") + component = self.expect_number_() + if self.next_token_ == "LOCKED": + locked = True + self.advance_lexer_() + else: + locked = False + self.expect_keyword_("AT") + pos = self.parse_pos_() + self.expect_keyword_("END_ANCHOR") + anchor = ast.AnchorDefinition(name, gid, glyph_name, + component, locked, pos, + location=location) + if glyph_name not in self.anchors_: + self.anchors_[glyph_name] = SymbolTable() + self.anchors_[glyph_name].define(name, anchor) + return anchor + + def parse_adjust_by_(self): + self.advance_lexer_() + assert self.is_cur_keyword_("ADJUST_BY") + adjustment = self.expect_number_() + self.expect_keyword_("AT") + size = self.expect_number_() + return adjustment, size + + def parse_pos_(self): + # VOLT syntax doesn't seem to take device Y advance + self.advance_lexer_() + location = self.cur_token_location_ + assert self.is_cur_keyword_("POS"), location + adv = None + dx = None + dy = None + adv_adjust_by = {} + dx_adjust_by = {} + dy_adjust_by = {} + if self.next_token_ == "ADV": + self.advance_lexer_() + adv = self.expect_number_() + while self.next_token_ == "ADJUST_BY": + adjustment, size = self.parse_adjust_by_() + adv_adjust_by[size] = adjustment + if self.next_token_ == "DX": + self.advance_lexer_() + dx = self.expect_number_() + while self.next_token_ == "ADJUST_BY": + adjustment, size = self.parse_adjust_by_() + dx_adjust_by[size] = adjustment + if self.next_token_ == "DY": + self.advance_lexer_() + dy = self.expect_number_() + while self.next_token_ == "ADJUST_BY": + adjustment, size = self.parse_adjust_by_() + dy_adjust_by[size] = adjustment + self.expect_keyword_("END_POS") + return (adv, dx, dy, adv_adjust_by, dx_adjust_by, dy_adjust_by) + + def parse_unicode_values_(self): + location = self.cur_token_location_ + try: + unicode_values = self.expect_string_().split(",") + unicode_values = [ + int(uni[2:], 16) + for uni in unicode_values if uni != ""] + except ValueError as err: + raise VoltLibError(str(err), location) + return unicode_values if unicode_values != [] else None + + def parse_enum_(self): + assert self.is_cur_keyword_("ENUM") + enum = self.parse_coverage_() + self.expect_keyword_("END_ENUM") + return enum + + def parse_coverage_(self): + coverage = [] + location = self.cur_token_location_ + while self.next_token_ in ("GLYPH", "GROUP", "RANGE", "ENUM"): + if self.next_token_ == "ENUM": + self.advance_lexer_() + enum = self.parse_enum_() + coverage.append(enum) + elif self.next_token_ == "GLYPH": + self.expect_keyword_("GLYPH") + name = self.expect_string_() + coverage.append(name) + elif self.next_token_ == "GROUP": + self.expect_keyword_("GROUP") + name = self.expect_string_() + # resolved_group = self.groups_.resolve(name) + group = (name,) + coverage.append(group) + # if resolved_group is not None: + # coverage.extend(resolved_group.enum) + # # TODO: check that group exists after all groups are defined + # else: + # group = (name,) + # coverage.append(group) + # # raise VoltLibError( + # # 'Glyph group "%s" is not defined' % name, + # # location) + elif self.next_token_ == "RANGE": + self.expect_keyword_("RANGE") + start = self.expect_string_() + self.expect_keyword_("TO") + end = self.expect_string_() + coverage.append((start, end)) + return tuple(coverage) + + def resolve_group(self, group_name): + return self.groups_.resolve(group_name) + + def glyph_range(self, start, end): + rng = self.glyphs_.range(start, end) + return frozenset(rng) + + def parse_ppem_(self): + location = self.cur_token_location_ + ppem_name = self.cur_token_ + value = self.expect_number_() + setting = ast.SettingDefinition(ppem_name, value, location=location) + return setting + + def parse_compiler_flag_(self): + location = self.cur_token_location_ + flag_name = self.cur_token_ + value = True + setting = ast.SettingDefinition(flag_name, value, location=location) + return setting + + def parse_cmap_format(self): + location = self.cur_token_location_ + name = self.cur_token_ + value = (self.expect_number_(), self.expect_number_(), + self.expect_number_()) + setting = ast.SettingDefinition(name, value, location=location) + return setting + + def is_cur_keyword_(self, k): + return (self.cur_token_type_ is Lexer.NAME) and (self.cur_token_ == k) + + def expect_string_(self): + self.advance_lexer_() + if self.cur_token_type_ is not Lexer.STRING: + raise VoltLibError("Expected a string", self.cur_token_location_) + return self.cur_token_ + + def expect_keyword_(self, keyword): + self.advance_lexer_() + if self.cur_token_type_ is Lexer.NAME and self.cur_token_ == keyword: + return self.cur_token_ + raise VoltLibError("Expected \"%s\"" % keyword, + self.cur_token_location_) + + def expect_name_(self): + self.advance_lexer_() + if self.cur_token_type_ is Lexer.NAME: + return self.cur_token_ + raise VoltLibError("Expected a name", self.cur_token_location_) + + def expect_number_(self): + self.advance_lexer_() + if self.cur_token_type_ is not Lexer.NUMBER: + raise VoltLibError("Expected a number", self.cur_token_location_) + return self.cur_token_ + + def advance_lexer_(self): + self.cur_token_type_, self.cur_token_, self.cur_token_location_ = ( + self.next_token_type_, self.next_token_, self.next_token_location_) + try: + (self.next_token_type_, self.next_token_, + self.next_token_location_) = self.lexer_.next() + except StopIteration: + self.next_token_type_, self.next_token_ = (None, None) + + +class SymbolTable(object): + def __init__(self): + self.scopes_ = [{}] + + def enter_scope(self): + self.scopes_.append({}) + + def exit_scope(self): + self.scopes_.pop() + + def define(self, name, item): + self.scopes_[-1][name] = item + + def resolve(self, name, case_insensitive=True): + for scope in reversed(self.scopes_): + item = scope.get(name) + if item: + return item + if case_insensitive: + for key in scope: + if key.lower() == name.lower(): + return scope[key] + return None + + +class OrderedSymbolTable(SymbolTable): + def __init__(self): + self.scopes_ = [OrderedDict()] + + def enter_scope(self): + self.scopes_.append(OrderedDict()) + + def resolve(self, name, case_insensitive=False): + SymbolTable.resolve(self, name, case_insensitive=case_insensitive) + + def range(self, start, end): + for scope in reversed(self.scopes_): + if start in scope and end in scope: + start_idx = list(scope.keys()).index(start) + end_idx = list(scope.keys()).index(end) + return list(scope.keys())[start_idx:end_idx + 1] + return None diff --git a/Lib/fonttools.egg-info/PKG-INFO b/Lib/fonttools.egg-info/PKG-INFO new file mode 100644 index 0000000..c92fa09 --- /dev/null +++ b/Lib/fonttools.egg-info/PKG-INFO @@ -0,0 +1,1375 @@ +Metadata-Version: 1.2 +Name: fonttools +Version: 3.28.0 +Summary: Tools to manipulate font files +Home-page: http://github.com/fonttools/fonttools +Author: Just van Rossum +Author-email: just@letterror.com +Maintainer: Behdad Esfahbod +Maintainer-email: behdad@behdad.org +License: MIT +Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage Status| + |PyPI| |Gitter Chat| + + What is this? + ~~~~~~~~~~~~~ + + | fontTools is a library for manipulating fonts, written in Python. The + project includes the TTX tool, that can convert TrueType and OpenType + fonts to and from an XML text format, which is also called TTX. It + supports TrueType, OpenType, AFM and to an extent Type 1 and some + Mac-specific formats. The project has a `MIT open-source + licence <LICENSE>`__. + | Among other things this means you can use it free of charge. + + Installation + ~~~~~~~~~~~~ + + FontTools requires `Python <http://www.python.org/download/>`__ 2.7, 3.4 + or later. + + The package is listed in the Python Package Index (PyPI), so you can + install it with `pip <https://pip.pypa.io>`__: + + .. code:: sh + + pip install fonttools + + If you would like to contribute to its development, you can clone the + repository from Github, install the package in 'editable' mode and + modify the source code in place. We recommend creating a virtual + environment, using `virtualenv <https://virtualenv.pypa.io>`__ or + Python 3 `venv <https://docs.python.org/3/library/venv.html>`__ module. + + .. code:: sh + + # download the source code to 'fonttools' folder + git clone https://github.com/fonttools/fonttools.git + cd fonttools + + # create new virtual environment called e.g. 'fonttools-venv', or anything you like + python -m virtualenv fonttools-venv + + # source the `activate` shell script to enter the environment (Un\*x); to exit, just type `deactivate` + . fonttools-venv/bin/activate + + # to activate the virtual environment in Windows `cmd.exe`, do + fonttools-venv\Scripts\activate.bat + + # install in 'editable' mode + pip install -e . + + TTX – From OpenType and TrueType to XML and Back + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Once installed you can use the ``ttx`` command to convert binary font + files (``.otf``, ``.ttf``, etc) to the TTX xml format, edit them, and + convert them back to binary format. TTX files have a .ttx file + extension. + + .. code:: sh + + ttx /path/to/font.otf + ttx /path/to/font.ttx + + The TTX application works can be used in two ways, depending on what + platform you run it on: + + - As a command line tool (Windows/DOS, Unix, MacOSX) + - By dropping files onto the application (Windows, MacOS) + + TTX detects what kind of files it is fed: it will output a ``.ttx`` file + when it sees a ``.ttf`` or ``.otf``, and it will compile a ``.ttf`` or + ``.otf`` when the input file is a ``.ttx`` file. By default, the output + file is created in the same folder as the input file, and will have the + same name as the input file but with a different extension. TTX will + *never* overwrite existing files, but if necessary will append a unique + number to the output filename (before the extension) such as + ``Arial#1.ttf`` + + When using TTX from the command line there are a bunch of extra options, + these are explained in the help text, as displayed when typing + ``ttx -h`` at the command prompt. These additional options include: + + - specifying the folder where the output files are created + - specifying which tables to dump or which tables to exclude + - merging partial ``.ttx`` files with existing ``.ttf`` or ``.otf`` + files + - listing brief table info instead of dumping to ``.ttx`` + - splitting tables to separate ``.ttx`` files + - disabling TrueType instruction disassembly + + The TTX file format + ------------------- + + The following tables are currently supported: + + .. begin table list + .. code:: + + BASE, CBDT, CBLC, CFF, CFF2, COLR, CPAL, DSIG, EBDT, EBLC, FFTM, + Feat, GDEF, GMAP, GPKG, GPOS, GSUB, Glat, Gloc, HVAR, JSTF, LTSH, + MATH, META, MVAR, OS/2, SING, STAT, SVG, Silf, Sill, TSI0, TSI1, + TSI2, TSI3, TSI5, TSIB, TSID, TSIJ, TSIP, TSIS, TSIV, TTFA, VDMX, + VORG, VVAR, ankr, avar, bsln, cidg, cmap, cvar, cvt, feat, fpgm, + fvar, gasp, gcid, glyf, gvar, hdmx, head, hhea, hmtx, kern, lcar, + loca, ltag, maxp, meta, mort, morx, name, opbd, post, prep, prop, + sbix, trak, vhea and vmtx + .. end table list + + Other tables are dumped as hexadecimal data. + + TrueType fonts use glyph indices (GlyphIDs) to refer to glyphs in most + places. While this is fine in binary form, it is really hard to work + with for humans. Therefore we use names instead. + + The glyph names are either extracted from the ``CFF`` table or the + ``post`` table, or are derived from a Unicode ``cmap`` table. In the + latter case the Adobe Glyph List is used to calculate names based on + Unicode values. If all of these methods fail, names are invented based + on GlyphID (eg ``glyph00142``) + + It is possible that different glyphs use the same name. If this happens, + we force the names to be unique by appending ``#n`` to the name (``n`` + being an integer number.) The original names are being kept, so this has + no influence on a "round tripped" font. + + Because the order in which glyphs are stored inside the binary font is + important, we maintain an ordered list of glyph names in the font. + + Other Tools + ~~~~~~~~~~~ + + Commands for inspecting, merging and subsetting fonts are also + available: + + .. code:: sh + + pyftinspect + pyftmerge + pyftsubset + + fontTools Python Module + ~~~~~~~~~~~~~~~~~~~~~~~ + + The fontTools python module provides a convenient way to + programmatically edit font files. + + .. code:: py + + >>> from fontTools.ttLib import TTFont + >>> font = TTFont('/path/to/font.ttf') + >>> font + <fontTools.ttLib.TTFont object at 0x10c34ed50> + >>> + + A selection of sample python programs is in the + `Snippets <https://github.com/fonttools/fonttools/blob/master/Snippets/>`__ + directory. + + Optional Requirements + --------------------- + + The ``fontTools`` package currently has no (required) external dependencies + besides the modules included in the Python Standard Library. + However, a few extra dependencies are required by some of its modules, which + are needed to unlock optional features. + + - ``Lib/fontTools/ttLib/woff2.py`` + + Module to compress/decompress WOFF 2.0 web fonts; it requires: + + - `brotli <https://pypi.python.org/pypi/Brotli>`__: Python bindings of + the Brotli compression library. + + - ``Lib/fontTools/ttLib/sfnt.py`` + + To better compress WOFF 1.0 web fonts, the following module can be used + instead of the built-in ``zlib`` library: + + - `zopfli <https://pypi.python.org/pypi/zopfli>`__: Python bindings of + the Zopfli compression library. + + - ``Lib/fontTools/unicode.py`` + + To display the Unicode character names when dumping the ``cmap`` table + with ``ttx`` we use the ``unicodedata`` module in the Standard Library. + The version included in there varies between different Python versions. + To use the latest available data, you can install: + + - `unicodedata2 <https://pypi.python.org/pypi/unicodedata2>`__: + ``unicodedata`` backport for Python 2.7 and 3.5 updated to the latest + Unicode version 9.0. Note this is not necessary if you use Python 3.6 + as the latter already comes with an up-to-date ``unicodedata``. + + - ``Lib/fontTools/varLib/interpolatable.py`` + + Module for finding wrong contour/component order between different masters. + It requires one of the following packages in order to solve the so-called + "minimum weight perfect matching problem in bipartite graphs", or + the Assignment problem: + + * `scipy <https://pypi.python.org/pypi/scipy>`__: the Scientific Library + for Python, which internally uses `NumPy <https://pypi.python.org/pypi/numpy>`__ + arrays and hence is very fast; + * `munkres <https://pypi.python.org/pypi/munkres>`__: a pure-Python + module that implements the Hungarian or Kuhn-Munkres algorithm. + + - ``Lib/fontTools/misc/symfont.py`` + + Advanced module for symbolic font statistics analysis; it requires: + + * `sympy <https://pypi.python.org/pypi/sympy>`__: the Python library for + symbolic mathematics. + + - ``Lib/fontTools/t1Lib.py`` + + To get the file creator and type of Macintosh PostScript Type 1 fonts + on Python 3 you need to install the following module, as the old ``MacOS`` + module is no longer included in Mac Python: + + * `xattr <https://pypi.python.org/pypi/xattr>`__: Python wrapper for + extended filesystem attributes (macOS platform only). + + - ``Lib/fontTools/pens/cocoaPen.py`` + + Pen for drawing glyphs with Cocoa ``NSBezierPath``, requires: + + * `PyObjC <https://pypi.python.org/pypi/pyobjc>`__: the bridge between + Python and the Objective-C runtime (macOS platform only). + + - ``Lib/fontTools/pens/qtPen.py`` + + Pen for drawing glyphs with Qt's ``QPainterPath``, requires: + + * `PyQt5 <https://pypi.python.org/pypi/PyQt5>`__: Python bindings for + the Qt cross platform UI and application toolkit. + + - ``Lib/fontTools/pens/reportLabPen.py`` + + Pen to drawing glyphs as PNG images, requires: + + * `reportlab <https://pypi.python.org/pypi/reportlab>`__: Python toolkit + for generating PDFs and graphics. + + - ``Lib/fontTools/inspect.py`` + + A GUI font inspector, requires one of the following packages: + + * `PyGTK <https://pypi.python.org/pypi/PyGTK>`__: Python bindings for + GTK  2.x (only works with Python 2). + * `PyGObject <https://wiki.gnome.org/action/show/Projects/PyGObject>`__ : + Python bindings for GTK 3.x and gobject-introspection libraries (also + compatible with Python 3). + + Testing + ~~~~~~~ + + To run the test suite, you can do: + + .. code:: sh + + python setup.py test + + If you have `pytest <http://docs.pytest.org/en/latest/>`__, you can run + the ``pytest`` command directly. The tests will run against the + installed ``fontTools`` package, or the first one found in the + ``PYTHONPATH``. + + You can also use `tox <https://testrun.org/tox/latest/>`__ to + automatically run tests on different Python versions in isolated virtual + environments. + + .. code:: sh + + pip install tox + tox + + Note that when you run ``tox`` without arguments, the tests are executed + for all the environments listed in tox.ini's ``envlist``. In our case, + this includes Python 2.7 and 3.6, so for this to work the ``python2.7`` + and ``python3.6`` executables must be available in your ``PATH``. + + You can specify an alternative environment list via the ``-e`` option, + or the ``TOXENV`` environment variable: + + .. code:: sh + + tox -e py27-nocov + TOXENV="py36-cov,htmlcov" tox + + Development Community + ~~~~~~~~~~~~~~~~~~~~~ + + TTX/FontTools development is ongoing in an active community of + developers, that includes professional developers employed at major + software corporations and type foundries as well as hobbyists. + + Feature requests and bug reports are always welcome at + https://github.com/fonttools/fonttools/issues/ + + The best place for discussions about TTX from an end-user perspective as + well as TTX/FontTools development is the + https://groups.google.com/d/forum/fonttools mailing list. There is also + a development https://groups.google.com/d/forum/fonttools-dev mailing + list for continuous integration notifications. You can also email Behdad + privately at behdad@behdad.org + + History + ~~~~~~~ + + The fontTools project was started by Just van Rossum in 1999, and was + maintained as an open source project at + http://sourceforge.net/projects/fonttools/. In 2008, Paul Wise (pabs3) + began helping Just with stability maintenance. In 2013 Behdad Esfahbod + began a friendly fork, thoroughly reviewing the codebase and making + changes at https://github.com/behdad/fonttools to add new features and + support for new font formats. + + Acknowledgements + ~~~~~~~~~~~~~~~~ + + In alphabetical order: + + Olivier Berten, Samyak Bhuta, Erik van Blokland, Petr van Blokland, + Jelle Bosma, Sascha Brawer, Tom Byrer, Frédéric Coiffier, Vincent + Connare, Dave Crossland, Simon Daniels, Behdad Esfahbod, Behnam + Esfahbod, Hannes Famira, Sam Fishman, Matt Fontaine, Yannis Haralambous, + Greg Hitchcock, Jeremie Hornus, Khaled Hosny, John Hudson, Denis Moyogo + Jacquerye, Jack Jansen, Tom Kacvinsky, Jens Kutilek, Antoine Leca, + Werner Lemberg, Tal Leming, Peter Lofting, Cosimo Lupo, Masaya Nakamura, + Dave Opstad, Laurence Penney, Roozbeh Pournader, Garret Rieger, Read + Roberts, Guido van Rossum, Just van Rossum, Andreas Seidel, Georg + Seifert, Miguel Sousa, Adam Twardoch, Adrien Tétar, Vitaly Volkov, Paul + Wise. + + Copyrights + ~~~~~~~~~~ + + | Copyright (c) 1999-2004 Just van Rossum, LettError + (just@letterror.com) + | See `LICENSE <LICENSE>`__ for the full license. + + Copyright (c) 2000 BeOpen.com. All Rights Reserved. + + Copyright (c) 1995-2001 Corporation for National Research Initiatives. + All Rights Reserved. + + Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam. All + Rights Reserved. + + Have fun! + + .. |Travis Build Status| image:: https://travis-ci.org/fonttools/fonttools.svg + :target: https://travis-ci.org/fonttools/fonttools + .. |Appveyor Build status| image:: https://ci.appveyor.com/api/projects/status/0f7fmee9as744sl7/branch/master?svg=true + :target: https://ci.appveyor.com/project/fonttools/fonttools/branch/master + .. |Health| image:: https://landscape.io/github/behdad/fonttools/master/landscape.svg?style=flat + :target: https://landscape.io/github/behdad/fonttools/master + .. |Coverage Status| image:: https://codecov.io/gh/fonttools/fonttools/branch/master/graph/badge.svg + :target: https://codecov.io/gh/fonttools/fonttools + .. |PyPI| image:: https://img.shields.io/pypi/v/fonttools.svg + :target: https://pypi.org/project/FontTools + .. |Gitter Chat| image:: https://badges.gitter.im/fonttools-dev/Lobby.svg + :alt: Join the chat at https://gitter.im/fonttools-dev/Lobby + :target: https://gitter.im/fonttools-dev/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge + + Changelog + ~~~~~~~~~ + + 3.28.0 (released 2018-06-19) + ---------------------------- + + - [featureVars] Added experimental module to build ``FeatureVariations`` + tables. Still needs to be hooked up to ``varLib.build`` (#1240). + - [fixedTools] Added ``otRound`` to round floats to nearest integer towards + positive Infinity. This is now used where we deal with visual data like X/Y + coordinates, advance widths/heights, variation deltas, and similar (#1274, + #1248). + - [subset] Improved GSUB closure memoize algorithm. + - [varLib.models] Fixed regression in model resolution (180124, #1269). + - [feaLib.ast] Fixed error when converting ``SubtableStatement`` to string + (#1275). + - [varLib.mutator] Set ``OS/2.usWeightClass`` and ``usWidthClass``, and + ``post.italicAngle`` based on the 'wght', 'wdth' and 'slnt' axis values + (#1276, #1264). + - [py23/loggingTools] Don't automatically set ``logging.lastResort`` handler + on py27. Moved ``LastResortLogger`` to the ``loggingTools`` module (#1277). + + 3.27.1 (released 2018-06-11) + ---------------------------- + + - [ttGlyphPen] Issue a warning and skip building non-existing components + (https://github.com/googlei18n/fontmake/issues/411). + - [tests] Fixed issue running ttx_test.py from a tagged commit. + + 3.27.0 (released 2018-06-11) + ---------------------------- + + - [designspaceLib] Added new ``conditionSet`` element to ``rule`` element in + designspace document. Bumped ``format`` attribute to ``4.0`` (previously, + it was formatted as an integer). Removed ``checkDefault``, ``checkAxes`` + methods, and any kind of guessing about the axes when the ``<axes>`` element + is missing. The default master is expected at the intersection of all default + values for each axis (#1254, #1255, #1267). + - [cffLib] Fixed issues when compiling CFF2 or converting from CFF when the + font has an FDArray (#1211, #1271). + - [varLib] Avoid attempting to build ``cvar`` table when ``glyf`` table is not + present, as is the case for CFF2 fonts. + - [subset] Handle None coverages in MarkGlyphSets; revert commit 02616ab that + sets empty Coverage tables in MarkGlyphSets to None, to make OTS happy. + - [ttFont] Allow to build glyph order from ``maxp.numGlyphs`` when ``post`` or + ``cmap`` are missing. + - [ttFont] Added ``__len__`` method to ``_TTGlyphSet``. + - [glyf] Ensure ``GlyphCoordinates`` never overflow signed shorts (#1230). + - [py23] Added alias for ``itertools.izip`` shadowing the built-in ``zip``. + - [loggingTools] Memoize ``log`` property of ``LogMixin`` class (fbab12). + - [ttx] Impoved test coverage (#1261). + - [Snippets] Addded script to append a suffix to all family names in a font. + - [varLib.plot] Make it work with matplotlib >= 2.1 (b38e2b). + + 3.26.0 (released 2018-05-03) + ---------------------------- + + - [designspace] Added a new optional ``layer`` attribute to the source element, + and a corresponding ``layerName`` attribute to the ``SourceDescriptor`` + object (#1253). + Added ``conditionset`` element to the ``rule`` element to the spec, but not + implemented in designspace reader/writer yet (#1254). + - [varLib.models] Refine modeling one last time (0ecf5c5). + - [otBase] Fixed sharing of tables referred to by different offset sizes + (795f2f9). + - [subset] Don't drop a GDEF that only has VarStore (fc819d6). Set to None + empty Coverage tables in MarkGlyphSets (02616ab). + - [varLib]: Added ``--master-finder`` command-line option (#1249). + - [varLib.mutator] Prune fvar nameIDs from instance's name table (#1245). + - [otTables] Allow decompiling bad ClassDef tables with invalid format, with + warning (#1236). + - [varLib] Make STAT v1.2 and reuse nameIDs from fvar table (#1242). + - [varLib.plot] Show master locations. Set axis limits to -1, +1. + - [subset] Handle HVAR direct mapping. Passthrough 'cvar'. + Added ``--font-number`` command-line option for collections. + - [t1Lib] Allow a text encoding to be specified when parsing a Type 1 font + (#1234). Added ``kind`` argument to T1Font constructor (c5c161c). + - [ttLib] Added context manager API to ``TTFont`` class, so it can be used in + ``with`` statements to auto-close the file when exiting the context (#1232). + + 3.25.0 (released 2018-04-03) + ---------------------------- + + - [varLib] Improved support-resolution algorithm. Previously, the on-axis + masters would always cut the space. They don't anymore. That's more + consistent, and fixes the main issue Erik showed at TYPO Labs 2017. + Any varfont built that had an unusual master configuration will change + when rebuilt (42bef17, a523a697, + https://github.com/googlei18n/fontmake/issues/264). + - [varLib.models] Added a ``main()`` entry point, that takes positions and + prints model results. + - [varLib.plot] Added new module to plot a designspace's + VariationModel. Requires ``matplotlib``. + - [varLib.mutator] Added -o option to specify output file path (2ef60fa). + - [otTables] Fixed IndexError while pruning of HVAR pre-write (6b6c34a). + - [varLib.models] Convert delta array to floats if values overflows signed + short integer (0055f94). + + 3.24.2 (released 2018-03-26) + ---------------------------- + + - [otBase] Don't fail during ``ValueRecord`` copy if src has more items. + We drop hinting in the subsetter by simply changing ValueFormat, without + cleaning up the actual ValueRecords. This was causing assertion error if + a variable font was subsetted without hinting and then passed directly to + the mutator for instantiation without first it saving to disk. + + 3.24.1 (released 2018-03-06) + ---------------------------- + + - [varLib] Don't remap the same ``DeviceTable`` twice in VarStore optimizer + (#1206). + - [varLib] Add ``--disable-iup`` option to ``fonttools varLib`` script, + and a ``optimize=True`` keyword argument to ``varLib.build`` function, + to optionally disable IUP optimization while building varfonts. + - [ttCollection] Fixed issue while decompiling ttc with python3 (#1207). + + 3.24.0 (released 2018-03-01) + ---------------------------- + + - [ttGlyphPen] Decompose composite glyphs if any components' transform is too + large to fit a ``F2Dot14`` value, or clamp transform values that are + (almost) equal to +2.0 to make them fit and avoid decomposing (#1200, + #1204, #1205). + - [ttx] Added new ``-g`` option to dump glyphs from the ``glyf`` table + splitted as individual ttx files (#153, #1035, #1132, #1202). + - Copied ``ufoLib.filenames`` module to ``fontTools.misc.filenames``, used + for the ttx split-glyphs option (#1202). + - [feaLib] Added support for ``cvParameters`` blocks in Character Variant + feautures ``cv01-cv99`` (#860, #1169). + - [Snippets] Added ``checksum.py`` script to generate/check SHA1 hash of + ttx files (#1197). + - [varLib.mutator] Fixed issue while instantiating some variable fonts + whereby the horizontal advance width computed from ``gvar`` phantom points + could turn up to be negative (#1198). + - [varLib/subset] Fixed issue with subsetting GPOS variation data not + picking up ``ValueRecord`` ``Device`` objects (54fd71f). + - [feaLib/voltLib] In all AST elements, the ``location`` is no longer a + required positional argument, but an optional kewyord argument (defaults + to ``None``). This will make it easier to construct feature AST from + code (#1201). + + + 3.23.0 (released 2018-02-26) + ---------------------------- + + - [designspaceLib] Added an optional ``lib`` element to the designspace as a + whole, as well as to the instance elements, to store arbitrary data in a + property list dictionary, similar to the UFO's ``lib``. Added an optional + ``font`` attribute to the ``SourceDescriptor``, to allow operating on + in-memory font objects (#1175). + - [cffLib] Fixed issue with lazy-loading of attributes when attempting to + set the CFF TopDict.Encoding (#1177, #1187). + - [ttx] Fixed regression introduced in 3.22.0 that affected the split tables + ``-s`` option (#1188). + - [feaLib] Added ``IncludedFeaNotFound`` custom exception subclass, raised + when an included feature file cannot be found (#1186). + - [otTables] Changed ``VarIdxMap`` to use glyph names internally instead of + glyph indexes. The old ttx dumps of HVAR/VVAR tables that contain indexes + can still be imported (21cbab8, 38a0ffb). + - [varLib] Implemented VarStore optimizer (#1184). + - [subset] Implemented pruning of GDEF VarStore, HVAR and MVAR (#1179). + - [sfnt] Restore backward compatiblity with ``numFonts`` attribute of + ``SFNTReader`` object (#1181). + - [merge] Initial support for merging ``LangSysRecords`` (#1180). + - [ttCollection] don't seek(0) when writing to possibly unseekable strems. + - [subset] Keep all ``--name-IDs`` from 0 to 6 by default (#1170, #605, #114). + - [cffLib] Added ``width`` module to calculate optimal CFF default and + nominal glyph widths. + - [varLib] Don’t fail if STAT already in the master fonts (#1166). + + 3.22.0 (released 2018-02-04) + ---------------------------- + + - [subset] Support subsetting ``endchar`` acting as ``seac``-like components + in ``CFF`` (fixes #1162). + - [feaLib] Allow to build from pre-parsed ``ast.FeatureFile`` object. + Added ``tables`` argument to only build some tables instead of all (#1159, + #1163). + - [textTools] Replaced ``safeEval`` with ``ast.literal_eval`` (#1139). + - [feaLib] Added option to the parser to not resolve ``include`` statements + (#1154). + - [ttLib] Added new ``ttCollection`` module to read/write TrueType and + OpenType Collections. Exports a ``TTCollection`` class with a ``fonts`` + attribute containing a list of ``TTFont`` instances, the methods ``save`` + and ``saveXML``, plus some list-like methods. The ``importXML`` method is + not implemented yet (#17). + - [unicodeadata] Added ``ot_tag_to_script`` function that converts from + OpenType script tag to Unicode script code. + - Added new ``designspaceLib`` subpackage, originally from Erik Van Blokland's + ``designSpaceDocument``: https://github.com/LettError/designSpaceDocument + NOTE: this is not yet used internally by varLib, and the API may be subject + to changes (#911, #1110, LettError/designSpaceDocument#28). + - Added new FontTools icon images (8ee7c32). + - [unicodedata] Added ``script_horizontal_direction`` function that returns + either "LTR" or "RTL" given a unicode script code. + - [otConverters] Don't write descriptive name string as XML comment if the + NameID value is 0 (== NULL) (#1151, #1152). + - [unicodedata] Add ``ot_tags_from_script`` function to get the list of + OpenType script tags associated with unicode script code (#1150). + - [feaLib] Don't error when "enumerated" kern pairs conflict with preceding + single pairs; emit warning and chose the first value (#1147, #1148). + - [loggingTools] In ``CapturingLogHandler.assertRegex`` method, match the + fully formatted log message. + - [sbix] Fixed TypeError when concatenating str and bytes (#1154). + - [bezierTools] Implemented cusp support and removed ``approximate_fallback`` + arg in ``calcQuadraticArcLength``. Added ``calcCubicArcLength`` (#1142). + + 3.21.2 (released 2018-01-08) + ---------------------------- + + - [varLib] Fixed merging PairPos Format1/2 with missing subtables (#1125). + + 3.21.1 (released 2018-01-03) + ---------------------------- + + - [feaLib] Allow mixed single/multiple substitutions (#612) + - Added missing ``*.afm`` test assets to MAINFEST.in (#1137). + - Fixed dumping ``SVG`` tables containing color palettes (#1124). + + 3.21.0 (released 2017-12-18) + ---------------------------- + + - [cmap] when compiling format6 subtable, don't assume gid0 is always called + '.notdef' (1e42224). + - [ot] Allow decompiling fonts with bad Coverage format number (1aafae8). + - Change FontTools licence to MIT (#1127). + - [post] Prune extra names already in standard Mac set (df1e8c7). + - [subset] Delete empty SubrsIndex after subsetting (#994, #1118). + - [varLib] Don't share points in cvar by default, as it currently fails on + some browsers (#1113). + - [afmLib] Make poor old afmLib work on python3. + + 3.20.1 (released 2017-11-22) + ---------------------------- + + - [unicodedata] Fixed issue with ``script`` and ``script_extension`` functions + returning inconsistent short vs long names. They both return the short four- + letter script codes now. Added ``script_name`` and ``script_code`` functions + to look up the long human-readable script name from the script code, and + viceversa (#1109, #1111). + + 3.20.0 (released 2017-11-21) + ---------------------------- + + - [unicodedata] Addded new module ``fontTools.unicodedata`` which exports the + same interface as the built-in ``unicodedata`` module, with the addition of + a few functions that are missing from the latter, such as ``script``, + ``script_extension`` and ``block``. Added a ``MetaTools/buildUCD.py`` script + to download and parse data files from the Unicode Character Database and + generate python modules containing lists of ranges and property values. + - [feaLib] Added ``__str__`` method to all ``ast`` elements (delegates to the + ``asFea`` method). + - [feaLib] ``Parser`` constructor now accepts a ``glyphNames`` iterable + instead of ``glyphMap`` dict. The latter still works but with a pending + deprecation warning (#1104). + - [bezierTools] Added arc length calculation functions originally from + ``pens.perimeterPen`` module (#1101). + - [varLib] Started generating STAT table (8af4309). Right now it just reflects + the axes, and even that with certain limitations: + * AxisOrdering is set to the order axes are defined, + * Name-table entries are not shared with fvar. + - [py23] Added backports for ``redirect_stdout`` and ``redirect_stderr`` + context managers (#1097). + - [Graphite] Fixed some round-trip bugs (#1093). + + 3.19.0 (released 2017-11-06) + ---------------------------- + + - [varLib] Try set of used points instead of all points when testing whether to + share points between tuples (#1090). + - [CFF2] Fixed issue with reading/writing PrivateDict BlueValues to TTX file. + Read the commit message 8b02b5a and issue #1030 for more details. + NOTE: this change invalidates all the TTX files containing CFF2 tables + that where dumped with previous verisons of fonttools. + CFF2 Subr items can have values on the stack after the last operator, thus + a ``CFF2Subr`` class was added to accommodate this (#1091). + - [_k_e_r_n] Fixed compilation of AAT kern version=1.0 tables (#1089, #1094) + - [ttLib] Added getBestCmap() convenience method to TTFont class and cmap table + class that returns a preferred Unicode cmap subtable given a list of options + (#1092). + - [morx] Emit more meaningful subtable flags. Implement InsertionMorphAction + + 3.18.0 (released 2017-10-30) + ---------------------------- + + - [feaLib] Fixed writing back nested glyph classes (#1086). + - [TupleVariation] Reactivated shared points logic, bugfixes (#1009). + - [AAT] Implemented ``morx`` ligature subtables (#1082). + - [reverseContourPen] Keep duplicate lineTo following a moveTo (#1080, + https://github.com/googlei18n/cu2qu/issues/51). + - [varLib.mutator] Suport instantiation of GPOS, GDEF and MVAR (#1079). + - [sstruct] Fixed issue with ``unicode_literals`` and ``struct`` module in + old versions of python 2.7 (#993). + + 3.17.0 (released 2017-10-16) + ---------------------------- + + - [svgPathPen] Added an ``SVGPathPen`` that translates segment pen commands + into SVG path descriptions. Copied from Tal Leming's ``ufo2svg.svgPathPen`` + https://github.com/typesupply/ufo2svg/blob/d69f992/Lib/ufo2svg/svgPathPen.py + - [reverseContourPen] Added ``ReverseContourPen``, a filter pen that draws + contours with the winding direction reversed, while keeping the starting + point (#1071). + - [filterPen] Added ``ContourFilterPen`` to manipulate contours as a whole + rather than segment by segment. + - [arrayTools] Added ``Vector`` class to apply math operations on an array + of numbers, and ``pairwise`` function to loop over pairs of items in an + iterable. + - [varLib] Added support for building and interpolation of ``cvar`` table + (f874cf6, a25a401). + + 3.16.0 (released 2017-10-03) + ---------------------------- + + - [head] Try using ``SOURCE_DATE_EPOCH`` environment variable when setting + the ``head`` modified timestamp to ensure reproducible builds (#1063). + See https://reproducible-builds.org/specs/source-date-epoch/ + - [VTT] Decode VTT's ``TSI*`` tables text as UTF-8 (#1060). + - Added support for Graphite font tables: Feat, Glat, Gloc, Silf and Sill. + Thanks @mhosken! (#1054). + - [varLib] Default to using axis "name" attribute if "labelname" element + is missing (588f524). + - [merge] Added support for merging Script records. Remove unused features + and lookups after merge (d802580, 556508b). + - Added ``fontTools.svgLib`` package. Includes a parser for SVG Paths that + supports the Pen protocol (#1051). Also, added a snippet to convert SVG + outlines to UFO GLIF (#1053). + - [AAT] Added support for ``ankr``, ``bsln``, ``mort``, ``morx``, ``gcid``, + and ``cidg``. + - [subset] Implemented subsetting of ``prop``, ``opbd``, ``bsln``, ``lcar``. + + 3.15.1 (released 2017-08-18) + ---------------------------- + + - [otConverters] Implemented ``__add__`` and ``__radd__`` methods on + ``otConverters._LazyList`` that decompile a lazy list before adding + it to another list or ``_LazyList`` instance. Fixes an ``AttributeError`` + in the ``subset`` module when attempting to sum ``_LazyList`` objects + (6ef48bd2, 1aef1683). + - [AAT] Support the `opbd` table with optical bounds (a47f6588). + - [AAT] Support `prop` table with glyph properties (d05617b4). + + + 3.15.0 (released 2017-08-17) + ---------------------------- + + - [AAT] Added support for AAT lookups. The ``lcar`` table can be decompiled + and recompiled; futher work needed to handle ``morx`` table (#1025). + - [subset] Keep (empty) DefaultLangSys for Script 'DFLT' (6eb807b5). + - [subset] Support GSUB/GPOS.FeatureVariations (fe01d87b). + - [varLib] In ``models.supportScalars``, ignore an axis when its peak value + is 0 (fixes #1020). + - [varLib] Add default mappings to all axes in avar to fix rendering issue + in some rasterizers (19c4b377, 04eacf13). + - [varLib] Flatten multiple tail PairPosFormat2 subtables before merging + (c55ef525). + - [ttLib] Added support for recalculating font bounding box in ``CFF`` and + ``head`` tables, and min/max values in ``hhea`` and ``vhea`` tables (#970). + + 3.14.0 (released 2017-07-31) + ---------------------------- + + - [varLib.merger] Remove Extensions subtables before merging (f7c20cf8). + - [varLib] Initialize the avar segment map with required default entries + (#1014). + - [varLib] Implemented optimal IUP optmiziation (#1019). + - [otData] Add ``AxisValueFormat4`` for STAT table v1.2 from OT v1.8.2 + (#1015). + - [name] Fixed BCP46 language tag for Mac langID=9: 'si' -> 'sl'. + - [subset] Return value from ``_DehintingT2Decompiler.op_hintmask`` + (c0d672ba). + - [cffLib] Allow to get TopDict by index as well as by name (dca96c9c). + - [cffLib] Removed global ``isCFF2`` state; use one set of classes for + both CFF and CFF2, maintaining backward compatibility existing code (#1007). + - [cffLib] Deprecated maxstack operator, per OpenType spec update 1.8.1. + - [cffLib] Added missing default (-100) for UnderlinePosition (#983). + - [feaLib] Enable setting nameIDs greater than 255 (#1003). + - [varLib] Recalculate ValueFormat when merging SinglePos (#996). + - [varLib] Do not emit MVAR if there are no entries in the variation store + (#987). + - [ttx] For ``-x`` option, pad with space if table tag length is < 4. + + 3.13.1 (released 2017-05-30) + ---------------------------- + + - [feaLib.builder] Removed duplicate lookups optimization. The original + lookup order and semantics of the feature file are preserved (#976). + + 3.13.0 (released 2017-05-24) + ---------------------------- + + - [varLib.mutator] Implement IUP optimization (#969). + - [_g_l_y_f.GlyphCoordinates] Changed ``__bool__()`` semantics to match those + of other iterables (e46f949). Removed ``__abs__()`` (3db5be2). + - [varLib.interpolate_layout] Added ``mapped`` keyword argument to + ``interpolate_layout`` to allow disabling avar mapping: if False (default), + the location is mapped using the map element of the axes in designspace file; + if True, it is assumed that location is in designspace's internal space and + no mapping is performed (#950, #975). + - [varLib.interpolate_layout] Import designspace-loading logic from varLib. + - [varLib] Fixed bug with recombining PairPosClass2 subtables (81498e5, #914). + - [cffLib.specializer] When copying iterables, cast to list (462b7f86). + + 3.12.1 (released 2017-05-18) + ---------------------------- + + - [pens.t2CharStringPen] Fixed AttributeError when calling addComponent in + T2CharStringPen (#965). + + 3.12.0 (released 2017-05-17) + ---------------------------- + + - [cffLib.specializer] Added new ``specializer`` module to optimize CFF + charstrings, used by the T2CharStringPen (#948). + - [varLib.mutator] Sort glyphs by component depth before calculating composite + glyphs' bounding boxes to ensure deltas are correctly caclulated (#945). + - [_g_l_y_f] Fixed loss of precision in GlyphCoordinates by using 'd' (double) + instead of 'f' (float) as ``array.array`` typecode (#963, #964). + + 3.11.0 (released 2017-05-03) + ---------------------------- + + - [t2CharStringPen] Initial support for specialized Type2 path operators: + vmoveto, hmoveto, vlineto, hlineto, vvcurveto, hhcurveto, vhcurveto and + hvcurveto. This should produce more compact charstrings (#940, #403). + - [Doc] Added Sphinx sources for the documentation. Thanks @gferreira (#935). + - [fvar] Expose flags in XML (#932) + - [name] Add helper function for building multi-lingual names (#921) + - [varLib] Fixed kern merging when a PairPosFormat2 has ClassDef1 with glyphs + that are NOT present in the Coverage (1b5e1c4, #939). + - [varLib] Fixed non-deterministic ClassDef order with PY3 (f056c12, #927). + - [feLib] Throw an error when the same glyph is defined in multiple mark + classes within the same lookup (3e3ff00, #453). + + 3.10.0 (released 2017-04-14) + ---------------------------- + + - [varLib] Added support for building ``avar`` table, using the designspace + ``<map>`` elements. + - [varLib] Removed unused ``build(..., axisMap)`` argument. Axis map should + be specified in designspace file now. We do not accept nonstandard axes + if ``<axes>`` element is not present. + - [varLib] Removed "custom" axis from the ``standard_axis_map``. This was + added before when glyphsLib was always exporting the (unused) custom axis. + - [varLib] Added partial support for building ``MVAR`` table; does not + implement ``gasp`` table variations yet. + - [pens] Added FilterPen base class, for pens that control another pen; + factored out ``addComponent`` method from BasePen into a separate abstract + DecomposingPen class; added DecomposingRecordingPen, which records + components decomposed as regular contours. + - [TSI1] Fixed computation of the textLength of VTT private tables (#913). + - [loggingTools] Added ``LogMixin`` class providing a ``log`` property to + subclasses, which returns a ``logging.Logger`` named after the latter. + - [loggingTools] Added ``assertRegex`` method to ``CapturingLogHandler``. + - [py23] Added backport for python 3's ``types.SimpleNamespace`` class. + - [EBLC] Fixed issue with python 3 ``zip`` iterator. + + 3.9.2 (released 2017-04-08) + --------------------------- + + - [pens] Added pen to draw glyphs using WxPython ``GraphicsPath`` class: + https://wxpython.org/docs/api/wx.GraphicsPath-class.html + - [varLib.merger] Fixed issue with recombining multiple PairPosFormat2 + subtables (#888) + - [varLib] Do not encode gvar deltas that are all zeroes, or if all values + are smaller than tolerance. + - [ttLib] _TTGlyphSet glyphs now also have ``height`` and ``tsb`` (top + side bearing) attributes from the ``vmtx`` table, if present. + - [glyf] In ``GlyphCoordintes`` class, added ``__bool__`` / ``__nonzero__`` + methods, and ``array`` property to get raw array. + - [ttx] Support reading TTX files with BOM (#896) + - [CFF2] Fixed the reporting of the number of regions in the font. + + 3.9.1 (released 2017-03-20) + --------------------------- + + - [varLib.merger] Fixed issue while recombining multiple PairPosFormat2 + subtables if they were split because of offset overflows (9798c30). + - [varLib.merger] Only merge multiple PairPosFormat1 subtables if there is + at least one of the fonts with a non-empty Format1 subtable (0f5a46b). + - [varLib.merger] Fixed IndexError with empty ClassDef1 in PairPosFormat2 + (aad0d46). + - [varLib.merger] Avoid reusing Class2Record (mutable) objects (e6125b3). + - [varLib.merger] Calculate ClassDef1 and ClassDef2's Format when merging + PairPosFormat2 (23511fd). + - [macUtils] Added missing ttLib import (b05f203). + + 3.9.0 (released 2017-03-13) + --------------------------- + + - [feaLib] Added (partial) support for parsing feature file comments ``# ...`` + appearing in between statements (#879). + - [feaLib] Cleaned up syntax tree for FeatureNames. + - [ttLib] Added support for reading/writing ``CFF2`` table (thanks to + @readroberts at Adobe), and ``TTFA`` (ttfautohint) table. + - [varLib] Fixed regression introduced with 3.8.0 in the calculation of + ``NumShorts``, i.e. the number of deltas in ItemVariationData's delta sets + that use a 16-bit representation (b2825ff). + + 3.8.0 (released 2017-03-05) + --------------------------- + + - New pens: MomentsPen, StatisticsPen, RecordingPen, and TeePen. + - [misc] Added new ``fontTools.misc.symfont`` module, for symbolic font + statistical analysis; requires ``sympy`` (http://www.sympy.org/en/index.html) + - [varLib] Added experimental ``fontTools.varLib.interpolatable`` module for + finding wrong contour order between different masters + - [varLib] designspace.load() now returns a dictionary, instead of a tuple, + and supports <axes> element (#864); the 'masters' item was renamed 'sources', + like the <sources> element in the designspace document + - [ttLib] Fixed issue with recalculating ``head`` modified timestamp when + saving CFF fonts + - [ttLib] In TupleVariation, round deltas before compiling (#861, fixed #592) + - [feaLib] Ignore duplicate glyphs in classes used as MarkFilteringSet and + MarkAttachmentType (#863) + - [merge] Changed the ``gasp`` table merge logic so that only the one from + the first font is retained, similar to other hinting tables (#862) + - [Tests] Added tests for the ``varLib`` package, as well as test fonts + from the "Annotated OpenType Specification" (AOTS) to exercise ``ttLib``'s + table readers/writers (<https://github.com/adobe-type-tools/aots>) + + 3.7.2 (released 2017-02-17) + --------------------------- + + - [subset] Keep advance widths when stripping ".notdef" glyph outline in + CID-keyed CFF fonts (#845) + - [feaLib] Zero values now produce the same results as makeotf (#633, #848) + - [feaLib] More compact encoding for “Contextual positioning with in-line + single positioning rules” (#514) + + 3.7.1 (released 2017-02-15) + --------------------------- + + - [subset] Fixed issue with ``--no-hinting`` option whereby advance widths in + Type 2 charstrings were also being stripped (#709, #343) + - [feaLib] include statements now resolve relative paths like makeotf (#838) + - [feaLib] table ``name`` now handles Unicode codepoints beyond the Basic + Multilingual Plane, also supports old-style MacOS platform encodings (#842) + - [feaLib] correctly escape string literals when emitting feature syntax (#780) + + 3.7.0 (released 2017-02-11) + --------------------------- + + - [ttx, mtiLib] Preserve ordering of glyph alternates in GSUB type 3 (#833). + - [feaLib] Glyph names can have dashes, as per new AFDKO syntax v1.20 (#559). + - [feaLib] feaLib.Parser now needs the font's glyph map for parsing. + - [varLib] Fix regression where GPOS values were stored as 0. + - [varLib] Allow merging of class-based kerning when ClassDefs are different + + 3.6.3 (released 2017-02-06) + --------------------------- + + - [varLib] Fix building variation of PairPosFormat2 (b5c34ce). + - Populate defaults even for otTables that have postRead (e45297b). + - Fix compiling of MultipleSubstFormat1 with zero 'out' glyphs (b887860). + + 3.6.2 (released 2017-01-30) + --------------------------- + + - [varLib.merger] Fixed "TypeError: reduce() of empty sequence with no + initial value" (3717dc6). + + 3.6.1 (released 2017-01-28) + --------------------------- + + - [py23] Fixed unhandled exception occurring at interpreter shutdown in + the "last resort" logging handler (972b3e6). + - [agl] Ensure all glyph names are of native 'str' type; avoid mixing + 'str' and 'unicode' in TTFont.glyphOrder (d8c4058). + - Fixed inconsistent title levels in README.rst that caused PyPI to + incorrectly render the reStructuredText page. + + 3.6.0 (released 2017-01-26) + --------------------------- + + - [varLib] Refactored and improved the variation-font-building process. + - Assembly code in the fpgm, prep, and glyf tables is now indented in + XML output for improved readability. The ``instruction`` element is + written as a simple tag if empty (#819). + - [ttx] Fixed 'I/O operation on closed file' error when dumping + multiple TTXs to standard output with the '-o -' option. + - The unit test modules (``*_test.py``) have been moved outside of the + fontTools package to the Tests folder, thus they are no longer + installed (#811). + + 3.5.0 (released 2017-01-14) + --------------------------- + + - Font tables read from XML can now be written back to XML with no + loss. + - GSUB/GPOS LookupType is written out in XML as an element, not + comment. (#792) + - When parsing cmap table, do not store items mapped to glyph id 0. + (#790) + - [otlLib] Make ClassDef sorting deterministic. Fixes #766 (7d1ddb2) + - [mtiLib] Added unit tests (#787) + - [cvar] Implemented cvar table + - [gvar] Renamed GlyphVariation to TupleVariation to match OpenType + terminology. + - [otTables] Handle gracefully empty VarData.Item array when compiling + XML. (#797) + - [varLib] Re-enabled generation of ``HVAR`` table for fonts with + TrueType outlines; removed ``--build-HVAR`` command-line option. + - [feaLib] The parser can now be extended to support non-standard + statements in FEA code by using a customized Abstract Syntax Tree. + See, for example, ``feaLib.builder_test.test_extensions`` and + baseClass.feax (#794, fixes #773). + - [feaLib] Added ``feaLib`` command to the 'fonttools' command-line + tool; applies a feature file to a font. ``fonttools feaLib -h`` for + help. + - [pens] The ``T2CharStringPen`` now takes an optional + ``roundTolerance`` argument to control the rounding of coordinates + (#804, fixes #769). + - [ci] Measure test coverage on all supported python versions and OSes, + combine coverage data and upload to + https://codecov.io/gh/fonttools/fonttools (#786) + - [ci] Configured Travis and Appveyor for running tests on Python 3.6 + (#785, 55c03bc) + - The manual pages installation directory can be customized through + ``FONTTOOLS_MANPATH`` environment variable (#799, fixes #84). + - [Snippets] Added otf2ttf.py, for converting fonts from CFF to + TrueType using the googlei18n/cu2qu module (#802) + + 3.4.0 (released 2016-12-21) + --------------------------- + + - [feaLib] Added support for generating FEA text from abstract syntax + tree (AST) objects (#776). Thanks @mhosken + - Added ``agl.toUnicode`` function to convert AGL-compliant glyph names + to Unicode strings (#774) + - Implemented MVAR table (b4d5381) + + 3.3.1 (released 2016-12-15) + --------------------------- + + - [setup] We no longer use versioneer.py to compute fonttools version + from git metadata, as this has caused issues for some users (#767). + Now we bump the version strings manually with a custom ``release`` + command of setup.py script. + + 3.3.0 (released 2016-12-06) + --------------------------- + + - [ttLib] Implemented STAT table from OpenType 1.8 (#758) + - [cffLib] Fixed decompilation of CFF fonts containing non-standard + key/value pairs in FontDict (issue #740; PR #744) + - [py23] minor: in ``round3`` function, allow the second argument to be + ``None`` (#757) + - The standalone ``sstruct`` and ``xmlWriter`` modules, deprecated + since vesion 3.2.0, have been removed. They can be imported from the + ``fontTools.misc`` package. + + 3.2.3 (released 2016-12-02) + --------------------------- + + - [py23] optimized performance of round3 function; added backport for + py35 math.isclose() (9d8dacb) + - [subset] fixed issue with 'narrow' (UCS-2) Python 2 builds and + ``--text``/``--text-file`` options containing non-BMP chararcters + (16d0e5e) + - [varLib] fixed issuewhen normalizing location values (8fa2ee1, #749) + - [inspect] Made it compatible with both python2 and python3 (167ee60, + #748). Thanks @pnemade + + 3.2.2 (released 2016-11-24) + --------------------------- + + - [varLib] Do not emit null axes in fvar (1bebcec). Thanks @robmck-ms + - [varLib] Handle fonts without GPOS (7915a45) + - [merge] Ignore LangSys if None (a11bc56) + - [subset] Fix subsetting MathVariants (78d3cbe) + - [OS/2] Fix "Private Use (plane 15)" range (08a0d55). Thanks @mashabow + + 3.2.1 (released 2016-11-03) + --------------------------- + + - [OS/2] fix checking ``fsSelection`` bits matching ``head.macStyle`` + bits + - [varLib] added ``--build-HVAR`` option to generate ``HVAR`` table for + fonts with TrueType outlines. For ``CFF2``, it is enabled by default. + + 3.2.0 (released 2016-11-02) + --------------------------- + + - [varLib] Improve support for OpenType 1.8 Variable Fonts: + - Implement GDEF's VariationStore + - Implement HVAR/VVAR tables + - Partial support for loading MutatorMath .designspace files with + varLib.designspace module + - Add varLib.models with Variation fonts interpolation models + - Implement GSUB/GPOS FeatureVariations + - Initial support for interpolating and merging OpenType Layout tables + (see ``varLib.interpolate_layout`` and ``varLib.merger`` modules) + - [API change] Change version to be an integer instead of a float in + XML output for GSUB, GPOS, GDEF, MATH, BASE, JSTF, HVAR, VVAR, feat, + hhea and vhea tables. Scripts that set the Version for those to 1.0 + or other float values also need fixing. A warning is emitted when + code or XML needs fix. + - several bug fixes to the cffLib module, contributed by Adobe's + @readroberts + - The XML output for CFF table now has a 'major' and 'minor' elements + for specifying whether it's version 1.0 or 2.0 (support for CFF2 is + coming soon) + - [setup.py] remove undocumented/deprecated ``extra_path`` Distutils + argument. This means that we no longer create a "FontTools" subfolder + in site-packages containing the actual fontTools package, as well as + the standalone xmlWriter and sstruct modules. The latter modules are + also deprecated, and scheduled for removal in upcoming releases. + Please change your import statements to point to from fontTools.misc + import xmlWriter and from fontTools.misc import sstruct. + - [scripts] Add a 'fonttools' command-line tool that simply runs + ``fontTools.*`` sub-modules: e.g. ``fonttools ttx``, + ``fonttools subset``, etc. + - [hmtx/vmts] Read advance width/heights as unsigned short (uint16); + automatically round float values to integers. + - [ttLib/xmlWriter] add 'newlinestr=None' keyword argument to + ``TTFont.saveXML`` for overriding os-specific line endings (passed on + to ``XMLWriter`` instances). + - [versioning] Use versioneer instead of ``setuptools_scm`` to + dynamically load version info from a git checkout at import time. + - [feaLib] Support backslash-prefixed glyph names. + + 3.1.2 (released 2016-09-27) + --------------------------- + + - restore Makefile as an alternative way to build/check/install + - README.md: update instructions for installing package from source, + and for running test suite + - NEWS: Change log was out of sync with tagged release + + 3.1.1 (released 2016-09-27) + --------------------------- + + - Fix ``ttLibVersion`` attribute in TTX files still showing '3.0' + instead of '3.1'. + - Use ``setuptools_scm`` to manage package versions. + + 3.1.0 (released 2016-09-26) + --------------------------- + + - [feaLib] New library to parse and compile Adobe FDK OpenType Feature + files. + - [mtiLib] New library to parse and compile Monotype 'FontDame' + OpenType Layout Tables files. + - [voltLib] New library to parse Microsoft VOLT project files. + - [otlLib] New library to work with OpenType Layout tables. + - [varLib] New library to work with OpenType Font Variations. + - [pens] Add ttGlyphPen to draw to TrueType glyphs, and t2CharStringPen + to draw to Type 2 Charstrings (CFF); add areaPen and perimeterPen. + - [ttLib.tables] Implement 'meta' and 'trak' tables. + - [ttx] Add --flavor option for compiling to 'woff' or 'woff2'; add + ``--with-zopfli`` option to use Zopfli to compress WOFF 1.0 fonts. + - [subset] Support subsetting 'COLR'/'CPAL' and 'CBDT'/'CBLC' color + fonts tables, and 'gvar' table for variation fonts. + - [Snippets] Add ``symfont.py``, for symbolic font statistics analysis; + interpolatable.py, a preliminary script for detecting interpolation + errors; ``{merge,dump}_woff_metadata.py``. + - [classifyTools] Helpers to classify things into classes. + - [CI] Run tests on Windows, Linux and macOS using Appveyor and Travis + CI; check unit test coverage with Coverage.py/Coveralls; automatic + deployment to PyPI on tags. + - [loggingTools] Use Python built-in logging module to print messages. + - [py23] Make round() behave like Python 3 built-in round(); define + round2() and round3(). + + 3.0 (released 2015-09-01) + ------------------------- + + - Add Snippet scripts for cmap subtable format conversion, printing + GSUB/GPOS features, building a GX font from two masters + - TTX WOFF2 support and a ``-f`` option to overwrite output file(s) + - Support GX tables: ``avar``, ``gvar``, ``fvar``, ``meta`` + - Support ``feat`` and gzip-compressed SVG tables + - Upgrade Mac East Asian encodings to native implementation if + available + - Add Roman Croatian and Romanian encodings, codecs for mac-extended + East Asian encodings + - Implement optimal GLYF glyph outline packing; disabled by default + + 2.5 (released 2014-09-24) + ------------------------- + + - Add a Qt pen + - Add VDMX table converter + - Load all OpenType sub-structures lazily + - Add support for cmap format 13. + - Add pyftmerge tool + - Update to Unicode 6.3.0d3 + - Add pyftinspect tool + - Add support for Google CBLC/CBDT color bitmaps, standard EBLC/EBDT + embedded bitmaps, and ``SVG`` table (thanks to Read Roberts at Adobe) + - Add support for loading, saving and ttx'ing WOFF file format + - Add support for Microsoft COLR/CPAL layered color glyphs + - Support PyPy + - Support Jython, by replacing numpy with array/lists modules and + removed it, pure-Python StringIO, not cStringIO + - Add pyftsubset and Subsetter object, supporting CFF and TTF + - Add to ttx args for -q for quiet mode, -z to choose a bitmap dump + format + + 2.4 (released 2013-06-22) + ------------------------- + + - Option to write to arbitrary files + - Better dump format for DSIG + - Better detection of OTF XML + - Fix issue with Apple's kern table format + - Fix mangling of TT glyph programs + - Fix issues related to mona.ttf + - Fix Windows Installer instructions + - Fix some modern MacOS issues + - Fix minor issues and typos + + 2.3 (released 2009-11-08) + ------------------------- + + - TrueType Collection (TTC) support + - Python 2.6 support + - Update Unicode data to 5.2.0 + - Couple of bug fixes + + 2.2 (released 2008-05-18) + ------------------------- + + - ClearType support + - cmap format 1 support + - PFA font support + - Switched from Numeric to numpy + - Update Unicode data to 5.1.0 + - Update AGLFN data to 1.6 + - Many bug fixes + + 2.1 (released 2008-01-28) + ------------------------- + + - Many years worth of fixes and features + + 2.0b2 (released 2002-??-??) + --------------------------- + + - Be "forgiving" when interpreting the maxp table version field: + interpret any value as 1.0 if it's not 0.5. Fixes dumping of these + GPL fonts: http://www.freebsd.org/cgi/pds.cgi?ports/chinese/wangttf + - Fixed ttx -l: it turned out this part of the code didn't work with + Python 2.2.1 and earlier. My bad to do most of my testing with a + different version than I shipped TTX with :-( + - Fixed bug in ClassDef format 1 subtable (Andreas Seidel bumped into + this one). + + 2.0b1 (released 2002-09-10) + --------------------------- + + - Fixed embarrassing bug: the master checksum in the head table is now + calculated correctly even on little-endian platforms (such as Intel). + - Made the cmap format 4 compiler smarter: the binary data it creates + is now more or less as compact as possible. TTX now makes more + compact data than in any shipping font I've tested it with. + - Dump glyph names as a separate "GlyphOrder" pseudo table as opposed + to as part of the glyf table (obviously needed for CFF-OTF's). + - Added proper support for the CFF table. + - Don't barf on empty tables (questionable, but "there are font out + there...") + - When writing TT glyf data, align glyphs on 4-byte boundaries. This + seems to be the current recommendation by MS. Also: don't barf on + fonts which are already 4-byte aligned. + - Windows installer contributed bu Adam Twardoch! Yay! + - Changed the command line interface again, now by creating one new + tool replacing the old ones: ttx It dumps and compiles, depending on + input file types. The options have changed somewhat. + - The -d option is back (output dir) + - ttcompile's -i options is now called -m (as in "merge"), to avoid + clash with dump's -i. + - The -s option ("split tables") no longer creates a directory, but + instead outputs a small .ttx file containing references to the + individual table files. This is not a true link, it's a simple file + name, and the referenced file should be in the same directory so + ttcompile can find them. + - compile no longer accepts a directory as input argument. Instead it + can parse the new "mini-ttx" format as output by "ttx -s". + - all arguments are input files + - Renamed the command line programs and moved them to the Tools + subdirectory. They are now installed by the setup.py install script. + - Added OpenType support. BASE, GDEF, GPOS, GSUB and JSTF are (almost) + fully supported. The XML output is not yet final, as I'm still + considering to output certain subtables in a more human-friendly + manner. + - Fixed 'kern' table to correctly accept subtables it doesn't know + about, as well as interpreting Apple's definition of the 'kern' table + headers correctly. + - Fixed bug where glyphnames were not calculated from 'cmap' if it was + (one of the) first tables to be decompiled. More specifically: it + cmap was the first to ask for a glyphID -> glyphName mapping. + - Switched XML parsers: use expat instead of xmlproc. Should be faster. + - Removed my UnicodeString object: I now require Python 2.0 or up, + which has unicode support built in. + - Removed assert in glyf table: redundant data at the end of the table + is now ignored instead of raising an error. Should become a warning. + - Fixed bug in hmtx/vmtx code that only occured if all advances were + equal. + - Fixed subtle bug in TT instruction disassembler. + - Couple of fixes to the 'post' table. + - Updated OS/2 table to latest spec. + + 1.0b1 (released 2001-08-10) + --------------------------- + + - Reorganized the command line interface for ttDump.py and + ttCompile.py, they now behave more like "normal" command line tool, + in that they accept multiple input files for batch processing. + - ttDump.py and ttCompile.py don't silently override files anymore, but + ask before doing so. Can be overridden by -f. + - Added -d option to both ttDump.py and ttCompile.py. + - Installation is now done with distutils. (Needs work for environments + without compilers.) + - Updated installation instructions. + - Added some workarounds so as to handle certain buggy fonts more + gracefully. + - Updated Unicode table to Unicode 3.0 (Thanks Antoine!) + - Included a Python script by Adam Twardoch that adds some useful stuff + to the Windows registry. + - Moved the project to SourceForge. + + 1.0a6 (released 2000-03-15) + --------------------------- + + - Big reorganization: made ttLib a subpackage of the new fontTools + package, changed several module names. Called the entire suite + "FontTools" + - Added several submodules to fontTools, some new, some older. + - Added experimental CFF/GPOS/GSUB support to ttLib, read-only (but XML + dumping of GPOS/GSUB is for now disabled) + - Fixed hdmx endian bug + - Added -b option to ttCompile.py, it disables recalculation of + bounding boxes, as requested by Werner Lemberg. + - Renamed tt2xml.pt to ttDump.py and xml2tt.py to ttCompile.py + - Use ".ttx" as file extension instead of ".xml". + - TTX is now the name of the XML-based *format* for TT fonts, and not + just an application. + + 1.0a5 + ----- + + Never released + + - More tables supported: hdmx, vhea, vmtx + + 1.0a3 & 1.0a4 + ------------- + + Never released + + - fixed most portability issues + - retracted the "Euro_or_currency" change from 1.0a2: it was + nonsense! + + 1.0a2 (released 1999-05-02) + --------------------------- + + - binary release for MacOS + - genenates full FOND resources: including width table, PS font name + info and kern table if applicable. + - added cmap format 4 support. Extra: dumps Unicode char names as XML + comments! + - added cmap format 6 support + - now accepts true type files starting with "true" (instead of just + 0x00010000 and "OTTO") + - 'glyf' table support is now complete: I added support for composite + scale, xy-scale and two-by-two for the 'glyf' table. For now, + component offset scale behaviour defaults to Apple-style. This only + affects the (re)calculation of the glyph bounding box. + - changed "Euro" to "Euro_or_currency" in the Standard Apple Glyph + order list, since we cannot tell from the 'post' table which is + meant. I should probably doublecheck with a Unicode encoding if + available. (This does not affect the output!) + + Fixed bugs: - 'hhea' table is now recalculated correctly - fixed wrong + assumption about sfnt resource names + + 1.0a1 (released 1999-04-27) + --------------------------- + + - initial binary release for MacOS + +Platform: Any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Console +Classifier: Environment :: Other Environment +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: End Users/Desktop +Classifier: License :: OSI Approved :: MIT License +Classifier: Natural Language :: English +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 +Classifier: Topic :: Text Processing :: Fonts +Classifier: Topic :: Multimedia :: Graphics +Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion diff --git a/Lib/fonttools.egg-info/SOURCES.txt b/Lib/fonttools.egg-info/SOURCES.txt new file mode 100644 index 0000000..fa92e3e --- /dev/null +++ b/Lib/fonttools.egg-info/SOURCES.txt @@ -0,0 +1,1576 @@ +.appveyor.yml +.codecov.yml +.coveragerc +.travis.yml +LICENSE +LICENSE.external +MANIFEST.in +Makefile +NEWS.rst +README.rst +dev-requirements.txt +fonttools +requirements.txt +run-tests.sh +setup.cfg +setup.py +tox.ini +.travis/after_success.sh +.travis/before_deploy.sh +.travis/before_install.sh +.travis/install.sh +.travis/run.sh +Doc/Makefile +Doc/make.bat +Doc/man/man1/ttx.1 +Doc/source/afmLib.rst +Doc/source/agl.rst +Doc/source/cffLib.rst +Doc/source/conf.py +Doc/source/encodings.rst +Doc/source/feaLib.rst +Doc/source/index.rst +Doc/source/inspect.rst +Doc/source/merge.rst +Doc/source/subset.rst +Doc/source/t1Lib.rst +Doc/source/ttx.rst +Doc/source/voltLib.rst +Doc/source/designspaceLib/index.rst +Doc/source/designspaceLib/readme.rst +Doc/source/designspaceLib/scripting.rst +Doc/source/misc/arrayTools.rst +Doc/source/misc/bezierTools.rst +Doc/source/misc/classifyTools.rst +Doc/source/misc/eexec.rst +Doc/source/misc/encodingTools.rst +Doc/source/misc/fixedTools.rst +Doc/source/misc/index.rst +Doc/source/misc/loggingTools.rst +Doc/source/misc/psCharStrings.rst +Doc/source/misc/sstruct.rst +Doc/source/misc/testTools.rst +Doc/source/misc/textTools.rst +Doc/source/misc/timeTools.rst +Doc/source/misc/transform.rst +Doc/source/misc/xmlReader.rst +Doc/source/misc/xmlWriter.rst +Doc/source/pens/areaPen.rst +Doc/source/pens/basePen.rst +Doc/source/pens/boundsPen.rst +Doc/source/pens/filterPen.rst +Doc/source/pens/index.rst +Doc/source/pens/perimeterPen.rst +Doc/source/pens/pointInsidePen.rst +Doc/source/pens/recordingPen.rst +Doc/source/pens/statisticsPen.rst +Doc/source/pens/t2CharStringPen.rst +Doc/source/pens/teePen.rst +Doc/source/pens/transformPen.rst +Doc/source/ttLib/index.rst +Doc/source/ttLib/macUtils.rst +Doc/source/ttLib/sfnt.rst +Doc/source/ttLib/tables.rst +Doc/source/ttLib/woff2.rst +Doc/source/varLib/designspace.rst +Doc/source/varLib/index.rst +Doc/source/varLib/interpolatable.rst +Doc/source/varLib/interpolate_layout.rst +Doc/source/varLib/merger.rst +Doc/source/varLib/models.rst +Doc/source/varLib/mutator.rst +Lib/fontTools/__init__.py +Lib/fontTools/__main__.py +Lib/fontTools/afmLib.py +Lib/fontTools/agl.py +Lib/fontTools/inspect.py +Lib/fontTools/merge.py +Lib/fontTools/ttx.py +Lib/fontTools/unicode.py +Lib/fontTools/cffLib/__init__.py +Lib/fontTools/cffLib/specializer.py +Lib/fontTools/cffLib/width.py +Lib/fontTools/designspaceLib/__init__.py +Lib/fontTools/encodings/MacRoman.py +Lib/fontTools/encodings/StandardEncoding.py +Lib/fontTools/encodings/__init__.py +Lib/fontTools/encodings/codecs.py +Lib/fontTools/feaLib/__init__.py +Lib/fontTools/feaLib/__main__.py +Lib/fontTools/feaLib/ast.py +Lib/fontTools/feaLib/builder.py +Lib/fontTools/feaLib/error.py +Lib/fontTools/feaLib/lexer.py +Lib/fontTools/feaLib/parser.py +Lib/fontTools/misc/__init__.py +Lib/fontTools/misc/arrayTools.py +Lib/fontTools/misc/bezierTools.py +Lib/fontTools/misc/classifyTools.py +Lib/fontTools/misc/cliTools.py +Lib/fontTools/misc/eexec.py +Lib/fontTools/misc/encodingTools.py +Lib/fontTools/misc/filenames.py +Lib/fontTools/misc/fixedTools.py +Lib/fontTools/misc/loggingTools.py +Lib/fontTools/misc/macCreatorType.py +Lib/fontTools/misc/macRes.py +Lib/fontTools/misc/psCharStrings.py +Lib/fontTools/misc/psLib.py +Lib/fontTools/misc/psOperators.py +Lib/fontTools/misc/py23.py +Lib/fontTools/misc/sstruct.py +Lib/fontTools/misc/symfont.py +Lib/fontTools/misc/testTools.py +Lib/fontTools/misc/textTools.py +Lib/fontTools/misc/timeTools.py +Lib/fontTools/misc/transform.py +Lib/fontTools/misc/xmlReader.py +Lib/fontTools/misc/xmlWriter.py +Lib/fontTools/mtiLib/__init__.py +Lib/fontTools/mtiLib/__main__.py +Lib/fontTools/otlLib/__init__.py +Lib/fontTools/otlLib/builder.py +Lib/fontTools/pens/__init__.py +Lib/fontTools/pens/areaPen.py +Lib/fontTools/pens/basePen.py +Lib/fontTools/pens/boundsPen.py +Lib/fontTools/pens/cocoaPen.py +Lib/fontTools/pens/filterPen.py +Lib/fontTools/pens/momentsPen.py +Lib/fontTools/pens/perimeterPen.py +Lib/fontTools/pens/pointInsidePen.py +Lib/fontTools/pens/qtPen.py +Lib/fontTools/pens/recordingPen.py +Lib/fontTools/pens/reportLabPen.py +Lib/fontTools/pens/reverseContourPen.py +Lib/fontTools/pens/statisticsPen.py +Lib/fontTools/pens/svgPathPen.py +Lib/fontTools/pens/t2CharStringPen.py +Lib/fontTools/pens/teePen.py +Lib/fontTools/pens/transformPen.py +Lib/fontTools/pens/ttGlyphPen.py +Lib/fontTools/pens/wxPen.py +Lib/fontTools/subset/__init__.py +Lib/fontTools/subset/__main__.py +Lib/fontTools/svgLib/__init__.py +Lib/fontTools/svgLib/path/__init__.py +Lib/fontTools/svgLib/path/parser.py +Lib/fontTools/t1Lib/__init__.py +Lib/fontTools/ttLib/__init__.py +Lib/fontTools/ttLib/macUtils.py +Lib/fontTools/ttLib/sfnt.py +Lib/fontTools/ttLib/standardGlyphOrder.py +Lib/fontTools/ttLib/ttCollection.py +Lib/fontTools/ttLib/ttFont.py +Lib/fontTools/ttLib/woff2.py +Lib/fontTools/ttLib/tables/B_A_S_E_.py +Lib/fontTools/ttLib/tables/BitmapGlyphMetrics.py +Lib/fontTools/ttLib/tables/C_B_D_T_.py +Lib/fontTools/ttLib/tables/C_B_L_C_.py +Lib/fontTools/ttLib/tables/C_F_F_.py +Lib/fontTools/ttLib/tables/C_F_F__2.py +Lib/fontTools/ttLib/tables/C_O_L_R_.py +Lib/fontTools/ttLib/tables/C_P_A_L_.py +Lib/fontTools/ttLib/tables/D_S_I_G_.py +Lib/fontTools/ttLib/tables/DefaultTable.py +Lib/fontTools/ttLib/tables/E_B_D_T_.py +Lib/fontTools/ttLib/tables/E_B_L_C_.py +Lib/fontTools/ttLib/tables/F_F_T_M_.py +Lib/fontTools/ttLib/tables/F__e_a_t.py +Lib/fontTools/ttLib/tables/G_D_E_F_.py +Lib/fontTools/ttLib/tables/G_M_A_P_.py +Lib/fontTools/ttLib/tables/G_P_K_G_.py +Lib/fontTools/ttLib/tables/G_P_O_S_.py +Lib/fontTools/ttLib/tables/G_S_U_B_.py +Lib/fontTools/ttLib/tables/G__l_a_t.py +Lib/fontTools/ttLib/tables/G__l_o_c.py +Lib/fontTools/ttLib/tables/H_V_A_R_.py +Lib/fontTools/ttLib/tables/J_S_T_F_.py +Lib/fontTools/ttLib/tables/L_T_S_H_.py +Lib/fontTools/ttLib/tables/M_A_T_H_.py +Lib/fontTools/ttLib/tables/M_E_T_A_.py +Lib/fontTools/ttLib/tables/M_V_A_R_.py +Lib/fontTools/ttLib/tables/O_S_2f_2.py +Lib/fontTools/ttLib/tables/S_I_N_G_.py +Lib/fontTools/ttLib/tables/S_T_A_T_.py +Lib/fontTools/ttLib/tables/S_V_G_.py +Lib/fontTools/ttLib/tables/S__i_l_f.py +Lib/fontTools/ttLib/tables/S__i_l_l.py +Lib/fontTools/ttLib/tables/T_S_I_B_.py +Lib/fontTools/ttLib/tables/T_S_I_D_.py +Lib/fontTools/ttLib/tables/T_S_I_J_.py +Lib/fontTools/ttLib/tables/T_S_I_P_.py +Lib/fontTools/ttLib/tables/T_S_I_S_.py +Lib/fontTools/ttLib/tables/T_S_I_V_.py +Lib/fontTools/ttLib/tables/T_S_I__0.py +Lib/fontTools/ttLib/tables/T_S_I__1.py +Lib/fontTools/ttLib/tables/T_S_I__2.py +Lib/fontTools/ttLib/tables/T_S_I__3.py +Lib/fontTools/ttLib/tables/T_S_I__5.py +Lib/fontTools/ttLib/tables/T_T_F_A_.py +Lib/fontTools/ttLib/tables/TupleVariation.py +Lib/fontTools/ttLib/tables/V_D_M_X_.py +Lib/fontTools/ttLib/tables/V_O_R_G_.py +Lib/fontTools/ttLib/tables/V_V_A_R_.py +Lib/fontTools/ttLib/tables/__init__.py +Lib/fontTools/ttLib/tables/_a_n_k_r.py +Lib/fontTools/ttLib/tables/_a_v_a_r.py +Lib/fontTools/ttLib/tables/_b_s_l_n.py +Lib/fontTools/ttLib/tables/_c_i_d_g.py +Lib/fontTools/ttLib/tables/_c_m_a_p.py +Lib/fontTools/ttLib/tables/_c_v_a_r.py +Lib/fontTools/ttLib/tables/_c_v_t.py +Lib/fontTools/ttLib/tables/_f_e_a_t.py +Lib/fontTools/ttLib/tables/_f_p_g_m.py +Lib/fontTools/ttLib/tables/_f_v_a_r.py +Lib/fontTools/ttLib/tables/_g_a_s_p.py +Lib/fontTools/ttLib/tables/_g_c_i_d.py +Lib/fontTools/ttLib/tables/_g_l_y_f.py +Lib/fontTools/ttLib/tables/_g_v_a_r.py +Lib/fontTools/ttLib/tables/_h_d_m_x.py +Lib/fontTools/ttLib/tables/_h_e_a_d.py +Lib/fontTools/ttLib/tables/_h_h_e_a.py +Lib/fontTools/ttLib/tables/_h_m_t_x.py +Lib/fontTools/ttLib/tables/_k_e_r_n.py +Lib/fontTools/ttLib/tables/_l_c_a_r.py +Lib/fontTools/ttLib/tables/_l_o_c_a.py +Lib/fontTools/ttLib/tables/_l_t_a_g.py +Lib/fontTools/ttLib/tables/_m_a_x_p.py +Lib/fontTools/ttLib/tables/_m_e_t_a.py +Lib/fontTools/ttLib/tables/_m_o_r_t.py +Lib/fontTools/ttLib/tables/_m_o_r_x.py +Lib/fontTools/ttLib/tables/_n_a_m_e.py +Lib/fontTools/ttLib/tables/_o_p_b_d.py +Lib/fontTools/ttLib/tables/_p_o_s_t.py +Lib/fontTools/ttLib/tables/_p_r_e_p.py +Lib/fontTools/ttLib/tables/_p_r_o_p.py +Lib/fontTools/ttLib/tables/_s_b_i_x.py +Lib/fontTools/ttLib/tables/_t_r_a_k.py +Lib/fontTools/ttLib/tables/_v_h_e_a.py +Lib/fontTools/ttLib/tables/_v_m_t_x.py +Lib/fontTools/ttLib/tables/asciiTable.py +Lib/fontTools/ttLib/tables/grUtils.py +Lib/fontTools/ttLib/tables/otBase.py +Lib/fontTools/ttLib/tables/otConverters.py +Lib/fontTools/ttLib/tables/otData.py +Lib/fontTools/ttLib/tables/otTables.py +Lib/fontTools/ttLib/tables/sbixGlyph.py +Lib/fontTools/ttLib/tables/sbixStrike.py +Lib/fontTools/ttLib/tables/table_API_readme.txt +Lib/fontTools/ttLib/tables/ttProgram.py +Lib/fontTools/unicodedata/Blocks.py +Lib/fontTools/unicodedata/OTTags.py +Lib/fontTools/unicodedata/ScriptExtensions.py +Lib/fontTools/unicodedata/Scripts.py +Lib/fontTools/unicodedata/__init__.py +Lib/fontTools/varLib/__init__.py +Lib/fontTools/varLib/__main__.py +Lib/fontTools/varLib/builder.py +Lib/fontTools/varLib/designspace.py +Lib/fontTools/varLib/featureVars.py +Lib/fontTools/varLib/interpolatable.py +Lib/fontTools/varLib/interpolate_layout.py +Lib/fontTools/varLib/iup.py +Lib/fontTools/varLib/merger.py +Lib/fontTools/varLib/models.py +Lib/fontTools/varLib/mutator.py +Lib/fontTools/varLib/mvar.py +Lib/fontTools/varLib/plot.py +Lib/fontTools/varLib/varStore.py +Lib/fontTools/voltLib/__init__.py +Lib/fontTools/voltLib/ast.py +Lib/fontTools/voltLib/error.py +Lib/fontTools/voltLib/lexer.py +Lib/fontTools/voltLib/parser.py +Lib/fonttools.egg-info/PKG-INFO +Lib/fonttools.egg-info/SOURCES.txt +Lib/fonttools.egg-info/dependency_links.txt +Lib/fonttools.egg-info/entry_points.txt +Lib/fonttools.egg-info/top_level.txt +MetaTools/buildTableList.py +MetaTools/buildUCD.py +MetaTools/roundTrip.py +Snippets/README.md +Snippets/checksum.py +Snippets/cmap-format.py +Snippets/dump_woff_metadata.py +Snippets/edit_raw_table_data.py +Snippets/fix-dflt-langsys.py +Snippets/interpolate.py +Snippets/layout-features.py +Snippets/merge_woff_metadata.py +Snippets/otf2ttf.py +Snippets/rename-fonts.py +Snippets/subset-fpgm.py +Snippets/svg2glif.py +Snippets/woff2_compress.py +Snippets/woff2_decompress.py +Tests/agl_test.py +Tests/merge_test.py +Tests/unicodedata_test.py +Tests/afmLib/afmLib_test.py +Tests/afmLib/data/TestAFM.afm +Tests/cffLib/cffLib_test.py +Tests/cffLib/specializer_test.py +Tests/cffLib/data/TestOTF.otf +Tests/designspaceLib/designspace_test.py +Tests/designspaceLib/data/test.designspace +Tests/encodings/codecs_test.py +Tests/feaLib/__init__.py +Tests/feaLib/builder_test.py +Tests/feaLib/error_test.py +Tests/feaLib/lexer_test.py +Tests/feaLib/parser_test.py +Tests/feaLib/data/Attach.fea +Tests/feaLib/data/Attach.ttx +Tests/feaLib/data/GPOS_1.fea +Tests/feaLib/data/GPOS_1.ttx +Tests/feaLib/data/GPOS_1_zero.fea +Tests/feaLib/data/GPOS_1_zero.ttx +Tests/feaLib/data/GPOS_2.fea +Tests/feaLib/data/GPOS_2.ttx +Tests/feaLib/data/GPOS_2b.fea +Tests/feaLib/data/GPOS_2b.ttx +Tests/feaLib/data/GPOS_3.fea +Tests/feaLib/data/GPOS_3.ttx +Tests/feaLib/data/GPOS_4.fea +Tests/feaLib/data/GPOS_4.ttx +Tests/feaLib/data/GPOS_5.fea +Tests/feaLib/data/GPOS_5.ttx +Tests/feaLib/data/GPOS_6.fea +Tests/feaLib/data/GPOS_6.ttx +Tests/feaLib/data/GPOS_8.fea +Tests/feaLib/data/GPOS_8.ttx +Tests/feaLib/data/GSUB_2.fea +Tests/feaLib/data/GSUB_2.ttx +Tests/feaLib/data/GSUB_3.fea +Tests/feaLib/data/GSUB_3.ttx +Tests/feaLib/data/GSUB_6.fea +Tests/feaLib/data/GSUB_6.ttx +Tests/feaLib/data/GSUB_8.fea +Tests/feaLib/data/GSUB_8.ttx +Tests/feaLib/data/GlyphClassDef.fea +Tests/feaLib/data/GlyphClassDef.ttx +Tests/feaLib/data/LigatureCaretByIndex.fea +Tests/feaLib/data/LigatureCaretByIndex.ttx +Tests/feaLib/data/LigatureCaretByPos.fea +Tests/feaLib/data/LigatureCaretByPos.ttx +Tests/feaLib/data/PairPosSubtable.fea +Tests/feaLib/data/PairPosSubtable.ttx +Tests/feaLib/data/ZeroValue_ChainSinglePos_horizontal.fea +Tests/feaLib/data/ZeroValue_ChainSinglePos_horizontal.ttx +Tests/feaLib/data/ZeroValue_ChainSinglePos_vertical.fea +Tests/feaLib/data/ZeroValue_ChainSinglePos_vertical.ttx +Tests/feaLib/data/ZeroValue_PairPos_horizontal.fea +Tests/feaLib/data/ZeroValue_PairPos_horizontal.ttx +Tests/feaLib/data/ZeroValue_PairPos_vertical.fea +Tests/feaLib/data/ZeroValue_PairPos_vertical.ttx +Tests/feaLib/data/ZeroValue_SinglePos_horizontal.fea +Tests/feaLib/data/ZeroValue_SinglePos_horizontal.ttx +Tests/feaLib/data/ZeroValue_SinglePos_vertical.fea +Tests/feaLib/data/ZeroValue_SinglePos_vertical.ttx +Tests/feaLib/data/baseClass.fea +Tests/feaLib/data/baseClass.feax +Tests/feaLib/data/bug453.fea +Tests/feaLib/data/bug453.ttx +Tests/feaLib/data/bug457.fea +Tests/feaLib/data/bug457.ttx +Tests/feaLib/data/bug463.fea +Tests/feaLib/data/bug463.ttx +Tests/feaLib/data/bug501.fea +Tests/feaLib/data/bug501.ttx +Tests/feaLib/data/bug502.fea +Tests/feaLib/data/bug502.ttx +Tests/feaLib/data/bug504.fea +Tests/feaLib/data/bug504.ttx +Tests/feaLib/data/bug505.fea +Tests/feaLib/data/bug505.ttx +Tests/feaLib/data/bug506.fea +Tests/feaLib/data/bug506.ttx +Tests/feaLib/data/bug509.fea +Tests/feaLib/data/bug509.ttx +Tests/feaLib/data/bug512.fea +Tests/feaLib/data/bug512.ttx +Tests/feaLib/data/bug514.fea +Tests/feaLib/data/bug514.ttx +Tests/feaLib/data/bug568.fea +Tests/feaLib/data/bug568.ttx +Tests/feaLib/data/bug633.fea +Tests/feaLib/data/bug633.ttx +Tests/feaLib/data/enum.fea +Tests/feaLib/data/enum.ttx +Tests/feaLib/data/feature_aalt.fea +Tests/feaLib/data/feature_aalt.ttx +Tests/feaLib/data/ignore_pos.fea +Tests/feaLib/data/ignore_pos.ttx +Tests/feaLib/data/include0.fea +Tests/feaLib/data/language_required.fea +Tests/feaLib/data/language_required.ttx +Tests/feaLib/data/lookup.fea +Tests/feaLib/data/lookup.ttx +Tests/feaLib/data/lookupflag.fea +Tests/feaLib/data/lookupflag.ttx +Tests/feaLib/data/markClass.fea +Tests/feaLib/data/markClass.ttx +Tests/feaLib/data/mini.fea +Tests/feaLib/data/multiple_feature_blocks.fea +Tests/feaLib/data/multiple_feature_blocks.ttx +Tests/feaLib/data/name.fea +Tests/feaLib/data/name.ttx +Tests/feaLib/data/omitted_GlyphClassDef.fea +Tests/feaLib/data/omitted_GlyphClassDef.ttx +Tests/feaLib/data/size.fea +Tests/feaLib/data/size.ttx +Tests/feaLib/data/size2.fea +Tests/feaLib/data/size2.ttx +Tests/feaLib/data/spec10.fea +Tests/feaLib/data/spec10.ttx +Tests/feaLib/data/spec4h1.fea +Tests/feaLib/data/spec4h1.ttx +Tests/feaLib/data/spec4h2.fea +Tests/feaLib/data/spec4h2.ttx +Tests/feaLib/data/spec5d1.fea +Tests/feaLib/data/spec5d1.ttx +Tests/feaLib/data/spec5d2.fea +Tests/feaLib/data/spec5d2.ttx +Tests/feaLib/data/spec5f_ii_1.fea +Tests/feaLib/data/spec5f_ii_1.ttx +Tests/feaLib/data/spec5f_ii_2.fea +Tests/feaLib/data/spec5f_ii_2.ttx +Tests/feaLib/data/spec5f_ii_3.fea +Tests/feaLib/data/spec5f_ii_3.ttx +Tests/feaLib/data/spec5f_ii_4.fea +Tests/feaLib/data/spec5f_ii_4.ttx +Tests/feaLib/data/spec5fi1.fea +Tests/feaLib/data/spec5fi1.ttx +Tests/feaLib/data/spec5fi2.fea +Tests/feaLib/data/spec5fi2.ttx +Tests/feaLib/data/spec5fi3.fea +Tests/feaLib/data/spec5fi3.ttx +Tests/feaLib/data/spec5fi4.fea +Tests/feaLib/data/spec5fi4.ttx +Tests/feaLib/data/spec5h1.fea +Tests/feaLib/data/spec5h1.ttx +Tests/feaLib/data/spec6b_ii.fea +Tests/feaLib/data/spec6b_ii.ttx +Tests/feaLib/data/spec6d2.fea +Tests/feaLib/data/spec6d2.ttx +Tests/feaLib/data/spec6e.fea +Tests/feaLib/data/spec6e.ttx +Tests/feaLib/data/spec6f.fea +Tests/feaLib/data/spec6f.ttx +Tests/feaLib/data/spec6h_ii.fea +Tests/feaLib/data/spec6h_ii.ttx +Tests/feaLib/data/spec6h_iii_1.fea +Tests/feaLib/data/spec6h_iii_1.ttx +Tests/feaLib/data/spec6h_iii_3d.fea +Tests/feaLib/data/spec6h_iii_3d.ttx +Tests/feaLib/data/spec8a.fea +Tests/feaLib/data/spec8a.ttx +Tests/feaLib/data/spec8b.fea +Tests/feaLib/data/spec8b.ttx +Tests/feaLib/data/spec8c.fea +Tests/feaLib/data/spec8c.ttx +Tests/feaLib/data/spec8d.fea +Tests/feaLib/data/spec8d.ttx +Tests/feaLib/data/spec9a.fea +Tests/feaLib/data/spec9a.ttx +Tests/feaLib/data/spec9b.fea +Tests/feaLib/data/spec9b.ttx +Tests/feaLib/data/spec9c1.fea +Tests/feaLib/data/spec9c1.ttx +Tests/feaLib/data/spec9c2.fea +Tests/feaLib/data/spec9c2.ttx +Tests/feaLib/data/spec9c3.fea +Tests/feaLib/data/spec9c3.ttx +Tests/feaLib/data/spec9d.fea +Tests/feaLib/data/spec9d.ttx +Tests/feaLib/data/spec9e.fea +Tests/feaLib/data/spec9e.ttx +Tests/feaLib/data/spec9f.fea +Tests/feaLib/data/spec9f.ttx +Tests/feaLib/data/spec9g.fea +Tests/feaLib/data/spec9g.ttx +Tests/feaLib/data/include/include1.fea +Tests/feaLib/data/include/include3.fea +Tests/feaLib/data/include/include4.fea +Tests/feaLib/data/include/include5.fea +Tests/feaLib/data/include/include6.fea +Tests/feaLib/data/include/includemissingfile.fea +Tests/feaLib/data/include/includeself.fea +Tests/feaLib/data/include/subdir/include2.fea +Tests/misc/arrayTools_test.py +Tests/misc/bezierTools_test.py +Tests/misc/classifyTools_test.py +Tests/misc/eexec_test.py +Tests/misc/encodingTools_test.py +Tests/misc/filenames_test.py +Tests/misc/fixedTools_test.py +Tests/misc/loggingTools_test.py +Tests/misc/macRes_test.py +Tests/misc/psCharStrings_test.py +Tests/misc/py23_test.py +Tests/misc/testTools_test.py +Tests/misc/textTools_test.py +Tests/misc/timeTools_test.py +Tests/misc/transform_test.py +Tests/misc/xmlReader_test.py +Tests/misc/xmlWriter_test.py +Tests/mtiLib/mti_test.py +Tests/mtiLib/data/featurename-backward.ttx.GSUB +Tests/mtiLib/data/featurename-backward.txt +Tests/mtiLib/data/featurename-forward.ttx.GSUB +Tests/mtiLib/data/featurename-forward.txt +Tests/mtiLib/data/lookupnames-backward.ttx.GSUB +Tests/mtiLib/data/lookupnames-backward.txt +Tests/mtiLib/data/lookupnames-forward.ttx.GSUB +Tests/mtiLib/data/lookupnames-forward.txt +Tests/mtiLib/data/mixed-toplevels.ttx.GSUB +Tests/mtiLib/data/mixed-toplevels.txt +Tests/mtiLib/data/mti/README +Tests/mtiLib/data/mti/chained-glyph.ttx.GPOS +Tests/mtiLib/data/mti/chained-glyph.ttx.GSUB +Tests/mtiLib/data/mti/chained-glyph.txt +Tests/mtiLib/data/mti/chainedclass.ttx.GSUB +Tests/mtiLib/data/mti/chainedclass.txt +Tests/mtiLib/data/mti/chainedcoverage.ttx.GSUB +Tests/mtiLib/data/mti/chainedcoverage.txt +Tests/mtiLib/data/mti/cmap.ttx +Tests/mtiLib/data/mti/cmap.ttx.cmap +Tests/mtiLib/data/mti/cmap.txt +Tests/mtiLib/data/mti/context-glyph.txt +Tests/mtiLib/data/mti/contextclass.txt +Tests/mtiLib/data/mti/contextcoverage.txt +Tests/mtiLib/data/mti/featuretable.txt +Tests/mtiLib/data/mti/gdefattach.ttx.GDEF +Tests/mtiLib/data/mti/gdefattach.txt +Tests/mtiLib/data/mti/gdefclasses.ttx.GDEF +Tests/mtiLib/data/mti/gdefclasses.txt +Tests/mtiLib/data/mti/gdefligcaret.ttx.GDEF +Tests/mtiLib/data/mti/gdefligcaret.txt +Tests/mtiLib/data/mti/gdefmarkattach.ttx.GDEF +Tests/mtiLib/data/mti/gdefmarkattach.txt +Tests/mtiLib/data/mti/gdefmarkfilter.ttx.GDEF +Tests/mtiLib/data/mti/gdefmarkfilter.txt +Tests/mtiLib/data/mti/gposcursive.ttx.GPOS +Tests/mtiLib/data/mti/gposcursive.txt +Tests/mtiLib/data/mti/gposkernset.ttx.GPOS +Tests/mtiLib/data/mti/gposkernset.txt +Tests/mtiLib/data/mti/gposmarktobase.ttx.GPOS +Tests/mtiLib/data/mti/gposmarktobase.txt +Tests/mtiLib/data/mti/gpospairclass.ttx.GPOS +Tests/mtiLib/data/mti/gpospairclass.txt +Tests/mtiLib/data/mti/gpospairglyph.ttx.GPOS +Tests/mtiLib/data/mti/gpospairglyph.txt +Tests/mtiLib/data/mti/gpossingle.ttx.GPOS +Tests/mtiLib/data/mti/gpossingle.txt +Tests/mtiLib/data/mti/gsubalternate.ttx.GSUB +Tests/mtiLib/data/mti/gsubalternate.txt +Tests/mtiLib/data/mti/gsubligature.ttx.GSUB +Tests/mtiLib/data/mti/gsubligature.txt +Tests/mtiLib/data/mti/gsubmultiple.ttx.GSUB +Tests/mtiLib/data/mti/gsubmultiple.txt +Tests/mtiLib/data/mti/gsubreversechanined.ttx.GSUB +Tests/mtiLib/data/mti/gsubreversechanined.txt +Tests/mtiLib/data/mti/gsubsingle.ttx.GSUB +Tests/mtiLib/data/mti/gsubsingle.txt +Tests/mtiLib/data/mti/mark-to-ligature.ttx.GPOS +Tests/mtiLib/data/mti/mark-to-ligature.txt +Tests/mtiLib/data/mti/scripttable.ttx.GPOS +Tests/mtiLib/data/mti/scripttable.ttx.GSUB +Tests/mtiLib/data/mti/scripttable.txt +Tests/otlLib/builder_test.py +Tests/pens/areaPen_test.py +Tests/pens/basePen_test.py +Tests/pens/boundsPen_test.py +Tests/pens/perimeterPen_test.py +Tests/pens/pointInsidePen_test.py +Tests/pens/recordingPen_test.py +Tests/pens/reverseContourPen_test.py +Tests/pens/t2CharStringPen_test.py +Tests/pens/ttGlyphPen_test.py +Tests/subset/subset_test.py +Tests/subset/data/Lobster.subset.ttx +Tests/subset/data/NotdefWidthCID-Regular.ttx +Tests/subset/data/TestANKR.ttx +Tests/subset/data/TestBSLN-0.ttx +Tests/subset/data/TestBSLN-1.ttx +Tests/subset/data/TestBSLN-2.ttx +Tests/subset/data/TestBSLN-3.ttx +Tests/subset/data/TestCID-Regular.ttx +Tests/subset/data/TestCLR-Regular.ttx +Tests/subset/data/TestGVAR.ttx +Tests/subset/data/TestLCAR-0.ttx +Tests/subset/data/TestLCAR-1.ttx +Tests/subset/data/TestMATH-Regular.ttx +Tests/subset/data/TestOPBD-0.ttx +Tests/subset/data/TestOPBD-1.ttx +Tests/subset/data/TestOTF-Regular.ttx +Tests/subset/data/TestPROP.ttx +Tests/subset/data/TestTTF-Regular.ttx +Tests/subset/data/TestTTF-Regular_non_BMP_char.ttx +Tests/subset/data/expect_ankr.ttx +Tests/subset/data/expect_bsln_0.ttx +Tests/subset/data/expect_bsln_1.ttx +Tests/subset/data/expect_bsln_2.ttx +Tests/subset/data/expect_bsln_3.ttx +Tests/subset/data/expect_desubroutinize_CFF.ttx +Tests/subset/data/expect_keep_colr.ttx +Tests/subset/data/expect_keep_gvar.ttx +Tests/subset/data/expect_keep_gvar_notdef_outline.ttx +Tests/subset/data/expect_keep_math.ttx +Tests/subset/data/expect_lcar_0.ttx +Tests/subset/data/expect_lcar_1.ttx +Tests/subset/data/expect_no_hinting_CFF.ttx +Tests/subset/data/expect_no_hinting_TTF.ttx +Tests/subset/data/expect_no_hinting_desubroutinize_CFF.ttx +Tests/subset/data/expect_no_notdef_outline_cid.ttx +Tests/subset/data/expect_no_notdef_outline_otf.ttx +Tests/subset/data/expect_no_notdef_outline_ttf.ttx +Tests/subset/data/expect_notdef_width_cid.ttx +Tests/subset/data/expect_opbd_0.ttx +Tests/subset/data/expect_opbd_1.ttx +Tests/subset/data/expect_prop_0.ttx +Tests/subset/data/expect_prop_1.ttx +Tests/subset/data/google_color.ttx +Tests/svgLib/path/__init__.py +Tests/svgLib/path/parser_test.py +Tests/svgLib/path/path_test.py +Tests/t1Lib/t1Lib_test.py +Tests/t1Lib/data/TestT1-Regular.lwfn +Tests/t1Lib/data/TestT1-Regular.pfa +Tests/t1Lib/data/TestT1-Regular.pfb +Tests/ttLib/sfnt_test.py +Tests/ttLib/woff2_test.py +Tests/ttLib/data/TestOTF-Regular.otx +Tests/ttLib/data/TestTTF-Regular.ttx +Tests/ttLib/data/TestTTFComplex-Regular.ttx +Tests/ttLib/data/test_woff2_metadata.xml +Tests/ttLib/tables/C_F_F__2_test.py +Tests/ttLib/tables/C_F_F_test.py +Tests/ttLib/tables/C_P_A_L_test.py +Tests/ttLib/tables/M_V_A_R_test.py +Tests/ttLib/tables/O_S_2f_2_test.py +Tests/ttLib/tables/S_T_A_T_test.py +Tests/ttLib/tables/T_S_I__0_test.py +Tests/ttLib/tables/T_S_I__1_test.py +Tests/ttLib/tables/TupleVariation_test.py +Tests/ttLib/tables/_a_n_k_r_test.py +Tests/ttLib/tables/_a_v_a_r_test.py +Tests/ttLib/tables/_b_s_l_n_test.py +Tests/ttLib/tables/_c_i_d_g_test.py +Tests/ttLib/tables/_c_m_a_p_test.py +Tests/ttLib/tables/_c_v_a_r_test.py +Tests/ttLib/tables/_f_p_g_m_test.py +Tests/ttLib/tables/_f_v_a_r_test.py +Tests/ttLib/tables/_g_c_i_d_test.py +Tests/ttLib/tables/_g_l_y_f_test.py +Tests/ttLib/tables/_g_v_a_r_test.py +Tests/ttLib/tables/_h_h_e_a_test.py +Tests/ttLib/tables/_h_m_t_x_test.py +Tests/ttLib/tables/_k_e_r_n_test.py +Tests/ttLib/tables/_l_c_a_r_test.py +Tests/ttLib/tables/_l_t_a_g_test.py +Tests/ttLib/tables/_m_e_t_a_test.py +Tests/ttLib/tables/_m_o_r_t_test.py +Tests/ttLib/tables/_m_o_r_x_test.py +Tests/ttLib/tables/_n_a_m_e_test.py +Tests/ttLib/tables/_o_p_b_d_test.py +Tests/ttLib/tables/_p_r_o_p_test.py +Tests/ttLib/tables/_t_r_a_k_test.py +Tests/ttLib/tables/_v_h_e_a_test.py +Tests/ttLib/tables/_v_m_t_x_test.py +Tests/ttLib/tables/otBase_test.py +Tests/ttLib/tables/otConverters_test.py +Tests/ttLib/tables/otTables_test.py +Tests/ttLib/tables/tables_test.py +Tests/ttLib/tables/ttProgram_test.py +Tests/ttLib/tables/data/C_F_F_.bin +Tests/ttLib/tables/data/C_F_F_.ttx +Tests/ttLib/tables/data/C_F_F__2.bin +Tests/ttLib/tables/data/C_F_F__2.ttx +Tests/ttLib/tables/data/_h_h_e_a_recalc_OTF.ttx +Tests/ttLib/tables/data/_h_h_e_a_recalc_TTF.ttx +Tests/ttLib/tables/data/_h_h_e_a_recalc_empty.ttx +Tests/ttLib/tables/data/_v_h_e_a_recalc_OTF.ttx +Tests/ttLib/tables/data/_v_h_e_a_recalc_TTF.ttx +Tests/ttLib/tables/data/_v_h_e_a_recalc_empty.ttx +Tests/ttLib/tables/data/ttProgram.ttx +Tests/ttLib/tables/data/aots/README +Tests/ttLib/tables/data/aots/base.otf +Tests/ttLib/tables/data/aots/base.ttx.CFF +Tests/ttLib/tables/data/aots/base.ttx.OS_2 +Tests/ttLib/tables/data/aots/base.ttx.cmap +Tests/ttLib/tables/data/aots/base.ttx.head +Tests/ttLib/tables/data/aots/base.ttx.hhea +Tests/ttLib/tables/data/aots/base.ttx.hmtx +Tests/ttLib/tables/data/aots/base.ttx.maxp +Tests/ttLib/tables/data/aots/base.ttx.name +Tests/ttLib/tables/data/aots/base.ttx.post +Tests/ttLib/tables/data/aots/classdef1_font1.otf +Tests/ttLib/tables/data/aots/classdef1_font1.ttx.GSUB +Tests/ttLib/tables/data/aots/classdef1_font2.otf +Tests/ttLib/tables/data/aots/classdef1_font2.ttx.GSUB +Tests/ttLib/tables/data/aots/classdef1_font3.otf +Tests/ttLib/tables/data/aots/classdef1_font3.ttx.GSUB +Tests/ttLib/tables/data/aots/classdef1_font4.otf +Tests/ttLib/tables/data/aots/classdef1_font4.ttx.GSUB +Tests/ttLib/tables/data/aots/classdef2_font1.otf +Tests/ttLib/tables/data/aots/classdef2_font1.ttx.GSUB +Tests/ttLib/tables/data/aots/classdef2_font2.otf +Tests/ttLib/tables/data/aots/classdef2_font2.ttx.GSUB +Tests/ttLib/tables/data/aots/classdef2_font3.otf +Tests/ttLib/tables/data/aots/classdef2_font3.ttx.GSUB +Tests/ttLib/tables/data/aots/classdef2_font4.otf +Tests/ttLib/tables/data/aots/classdef2_font4.ttx.GSUB +Tests/ttLib/tables/data/aots/cmap0_font1.otf +Tests/ttLib/tables/data/aots/cmap0_font1.ttx.cmap +Tests/ttLib/tables/data/aots/cmap10_font1.otf +Tests/ttLib/tables/data/aots/cmap10_font1.ttx.cmap +Tests/ttLib/tables/data/aots/cmap10_font2.otf +Tests/ttLib/tables/data/aots/cmap10_font2.ttx.cmap +Tests/ttLib/tables/data/aots/cmap12_font1.otf +Tests/ttLib/tables/data/aots/cmap12_font1.ttx.cmap +Tests/ttLib/tables/data/aots/cmap14_font1.otf +Tests/ttLib/tables/data/aots/cmap14_font1.ttx.cmap +Tests/ttLib/tables/data/aots/cmap2_font1.otf +Tests/ttLib/tables/data/aots/cmap2_font1.ttx.cmap +Tests/ttLib/tables/data/aots/cmap4_font1.otf +Tests/ttLib/tables/data/aots/cmap4_font1.ttx.cmap +Tests/ttLib/tables/data/aots/cmap4_font2.otf +Tests/ttLib/tables/data/aots/cmap4_font2.ttx.cmap +Tests/ttLib/tables/data/aots/cmap4_font3.otf +Tests/ttLib/tables/data/aots/cmap4_font3.ttx.cmap +Tests/ttLib/tables/data/aots/cmap4_font4.otf +Tests/ttLib/tables/data/aots/cmap4_font4.ttx.cmap +Tests/ttLib/tables/data/aots/cmap6_font1.otf +Tests/ttLib/tables/data/aots/cmap6_font1.ttx.cmap +Tests/ttLib/tables/data/aots/cmap6_font2.otf +Tests/ttLib/tables/data/aots/cmap6_font2.ttx.cmap +Tests/ttLib/tables/data/aots/cmap8_font1.otf +Tests/ttLib/tables/data/aots/cmap8_font1.ttx.cmap +Tests/ttLib/tables/data/aots/cmap_composition_font1.otf +Tests/ttLib/tables/data/aots/cmap_composition_font1.ttx.cmap +Tests/ttLib/tables/data/aots/cmap_subtableselection_font1.otf +Tests/ttLib/tables/data/aots/cmap_subtableselection_font1.ttx.cmap +Tests/ttLib/tables/data/aots/cmap_subtableselection_font2.otf +Tests/ttLib/tables/data/aots/cmap_subtableselection_font2.ttx.cmap +Tests/ttLib/tables/data/aots/cmap_subtableselection_font3.otf +Tests/ttLib/tables/data/aots/cmap_subtableselection_font3.ttx.cmap +Tests/ttLib/tables/data/aots/cmap_subtableselection_font4.otf +Tests/ttLib/tables/data/aots/cmap_subtableselection_font4.ttx.cmap +Tests/ttLib/tables/data/aots/cmap_subtableselection_font5.otf +Tests/ttLib/tables/data/aots/cmap_subtableselection_font5.ttx.cmap +Tests/ttLib/tables/data/aots/gpos1_1_lookupflag_f1.otf +Tests/ttLib/tables/data/aots/gpos1_1_lookupflag_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos1_1_lookupflag_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos1_1_simple_f1.otf +Tests/ttLib/tables/data/aots/gpos1_1_simple_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos1_1_simple_f2.otf +Tests/ttLib/tables/data/aots/gpos1_1_simple_f2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos1_1_simple_f3.otf +Tests/ttLib/tables/data/aots/gpos1_1_simple_f3.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos1_1_simple_f4.otf +Tests/ttLib/tables/data/aots/gpos1_1_simple_f4.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos1_2_font1.otf +Tests/ttLib/tables/data/aots/gpos1_2_font1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos1_2_font2.otf +Tests/ttLib/tables/data/aots/gpos1_2_font2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos1_2_font2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos2_1_font6.otf +Tests/ttLib/tables/data/aots/gpos2_1_font6.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos2_1_font7.otf +Tests/ttLib/tables/data/aots/gpos2_1_font7.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f1.otf +Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f2.otf +Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos2_1_next_glyph_f1.otf +Tests/ttLib/tables/data/aots/gpos2_1_next_glyph_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos2_1_next_glyph_f2.otf +Tests/ttLib/tables/data/aots/gpos2_1_next_glyph_f2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos2_1_simple_f1.otf +Tests/ttLib/tables/data/aots/gpos2_1_simple_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos2_2_font1.otf +Tests/ttLib/tables/data/aots/gpos2_2_font1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos2_2_font2.otf +Tests/ttLib/tables/data/aots/gpos2_2_font2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos2_2_font2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos2_2_font3.otf +Tests/ttLib/tables/data/aots/gpos2_2_font3.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos2_2_font3.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos2_2_font4.otf +Tests/ttLib/tables/data/aots/gpos2_2_font4.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos2_2_font5.otf +Tests/ttLib/tables/data/aots/gpos2_2_font5.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos3_font1.otf +Tests/ttLib/tables/data/aots/gpos3_font1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos3_font2.otf +Tests/ttLib/tables/data/aots/gpos3_font2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos3_font2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos3_font3.otf +Tests/ttLib/tables/data/aots/gpos3_font3.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos3_font3.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos4_lookupflag_f1.otf +Tests/ttLib/tables/data/aots/gpos4_lookupflag_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos4_lookupflag_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos4_lookupflag_f2.otf +Tests/ttLib/tables/data/aots/gpos4_lookupflag_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos4_lookupflag_f2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos4_multiple_anchors_1.otf +Tests/ttLib/tables/data/aots/gpos4_multiple_anchors_1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos4_multiple_anchors_1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos4_simple_1.otf +Tests/ttLib/tables/data/aots/gpos4_simple_1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos4_simple_1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos5_font1.otf +Tests/ttLib/tables/data/aots/gpos5_font1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos5_font1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos5_font1.ttx.GSUB +Tests/ttLib/tables/data/aots/gpos6_font1.otf +Tests/ttLib/tables/data/aots/gpos6_font1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos6_font1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos7_1_font1.otf +Tests/ttLib/tables/data/aots/gpos7_1_font1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos9_font1.otf +Tests/ttLib/tables/data/aots/gpos9_font1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos9_font2.otf +Tests/ttLib/tables/data/aots/gpos9_font2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f1.otf +Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f2.otf +Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f3.otf +Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f3.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f3.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f4.otf +Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f4.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f4.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining1_lookupflag_f1.otf +Tests/ttLib/tables/data/aots/gpos_chaining1_lookupflag_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining1_lookupflag_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f1.otf +Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f2.otf +Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining1_next_glyph_f1.otf +Tests/ttLib/tables/data/aots/gpos_chaining1_next_glyph_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining1_next_glyph_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f1.otf +Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f2.otf +Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining1_successive_f1.otf +Tests/ttLib/tables/data/aots/gpos_chaining1_successive_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining1_successive_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f1.otf +Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f2.otf +Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f3.otf +Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f3.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f3.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f4.otf +Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f4.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f4.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining2_lookupflag_f1.otf +Tests/ttLib/tables/data/aots/gpos_chaining2_lookupflag_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining2_lookupflag_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining2_multiple_subrules_f1.otf +Tests/ttLib/tables/data/aots/gpos_chaining2_multiple_subrules_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining2_multiple_subrules_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining2_multiple_subrules_f2.otf +Tests/ttLib/tables/data/aots/gpos_chaining2_multiple_subrules_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining2_multiple_subrules_f2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining2_next_glyph_f1.otf +Tests/ttLib/tables/data/aots/gpos_chaining2_next_glyph_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining2_next_glyph_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f1.otf +Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f2.otf +Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining2_successive_f1.otf +Tests/ttLib/tables/data/aots/gpos_chaining2_successive_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining2_successive_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f1.otf +Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f2.otf +Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f3.otf +Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f3.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f3.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f4.otf +Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f4.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f4.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining3_lookupflag_f1.otf +Tests/ttLib/tables/data/aots/gpos_chaining3_lookupflag_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining3_lookupflag_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining3_next_glyph_f1.otf +Tests/ttLib/tables/data/aots/gpos_chaining3_next_glyph_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining3_next_glyph_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f1.otf +Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f2.otf +Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_chaining3_successive_f1.otf +Tests/ttLib/tables/data/aots/gpos_chaining3_successive_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_chaining3_successive_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context1_boundary_f1.otf +Tests/ttLib/tables/data/aots/gpos_context1_boundary_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context1_boundary_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context1_boundary_f2.otf +Tests/ttLib/tables/data/aots/gpos_context1_boundary_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context1_boundary_f2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context1_expansion_f1.otf +Tests/ttLib/tables/data/aots/gpos_context1_expansion_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context1_expansion_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f1.otf +Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f2.otf +Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f1.otf +Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f2.otf +Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context1_next_glyph_f1.otf +Tests/ttLib/tables/data/aots/gpos_context1_next_glyph_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context1_next_glyph_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context1_simple_f1.otf +Tests/ttLib/tables/data/aots/gpos_context1_simple_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context1_simple_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context1_simple_f2.otf +Tests/ttLib/tables/data/aots/gpos_context1_simple_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context1_simple_f2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context1_successive_f1.otf +Tests/ttLib/tables/data/aots/gpos_context1_successive_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context1_successive_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context2_boundary_f1.otf +Tests/ttLib/tables/data/aots/gpos_context2_boundary_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context2_boundary_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context2_boundary_f2.otf +Tests/ttLib/tables/data/aots/gpos_context2_boundary_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context2_boundary_f2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context2_classes_f1.otf +Tests/ttLib/tables/data/aots/gpos_context2_classes_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context2_classes_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context2_classes_f2.otf +Tests/ttLib/tables/data/aots/gpos_context2_classes_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context2_classes_f2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context2_expansion_f1.otf +Tests/ttLib/tables/data/aots/gpos_context2_expansion_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context2_expansion_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f1.otf +Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f2.otf +Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f1.otf +Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f2.otf +Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context2_next_glyph_f1.otf +Tests/ttLib/tables/data/aots/gpos_context2_next_glyph_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context2_next_glyph_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context2_simple_f1.otf +Tests/ttLib/tables/data/aots/gpos_context2_simple_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context2_simple_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context2_simple_f2.otf +Tests/ttLib/tables/data/aots/gpos_context2_simple_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context2_simple_f2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context2_successive_f1.otf +Tests/ttLib/tables/data/aots/gpos_context2_successive_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context2_successive_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context3_boundary_f1.otf +Tests/ttLib/tables/data/aots/gpos_context3_boundary_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context3_boundary_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context3_boundary_f2.otf +Tests/ttLib/tables/data/aots/gpos_context3_boundary_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context3_boundary_f2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f1.otf +Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f2.otf +Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f2.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context3_next_glyph_f1.otf +Tests/ttLib/tables/data/aots/gpos_context3_next_glyph_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context3_next_glyph_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context3_simple_f1.otf +Tests/ttLib/tables/data/aots/gpos_context3_simple_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context3_simple_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gpos_context3_successive_f1.otf +Tests/ttLib/tables/data/aots/gpos_context3_successive_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gpos_context3_successive_f1.ttx.GPOS +Tests/ttLib/tables/data/aots/gsub1_1_lookupflag_f1.otf +Tests/ttLib/tables/data/aots/gsub1_1_lookupflag_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub1_1_lookupflag_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub1_1_modulo_f1.otf +Tests/ttLib/tables/data/aots/gsub1_1_modulo_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub1_1_simple_f1.otf +Tests/ttLib/tables/data/aots/gsub1_1_simple_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub1_2_lookupflag_f1.otf +Tests/ttLib/tables/data/aots/gsub1_2_lookupflag_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub1_2_lookupflag_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub1_2_simple_f1.otf +Tests/ttLib/tables/data/aots/gsub1_2_simple_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub2_1_lookupflag_f1.otf +Tests/ttLib/tables/data/aots/gsub2_1_lookupflag_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub2_1_lookupflag_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub2_1_multiple_sequences_f1.otf +Tests/ttLib/tables/data/aots/gsub2_1_multiple_sequences_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub2_1_simple_f1.otf +Tests/ttLib/tables/data/aots/gsub2_1_simple_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub3_1_lookupflag_f1.otf +Tests/ttLib/tables/data/aots/gsub3_1_lookupflag_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub3_1_lookupflag_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub3_1_multiple_f1.otf +Tests/ttLib/tables/data/aots/gsub3_1_multiple_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub3_1_simple_f1.otf +Tests/ttLib/tables/data/aots/gsub3_1_simple_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub4_1_lookupflag_f1.otf +Tests/ttLib/tables/data/aots/gsub4_1_lookupflag_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub4_1_lookupflag_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub4_1_multiple_ligatures_f1.otf +Tests/ttLib/tables/data/aots/gsub4_1_multiple_ligatures_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub4_1_multiple_ligatures_f2.otf +Tests/ttLib/tables/data/aots/gsub4_1_multiple_ligatures_f2.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub4_1_multiple_ligsets_f1.otf +Tests/ttLib/tables/data/aots/gsub4_1_multiple_ligsets_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub4_1_simple_f1.otf +Tests/ttLib/tables/data/aots/gsub4_1_simple_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub7_font1.otf +Tests/ttLib/tables/data/aots/gsub7_font1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub7_font2.otf +Tests/ttLib/tables/data/aots/gsub7_font2.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f1.otf +Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f2.otf +Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f2.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f3.otf +Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f3.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f3.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f4.otf +Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f4.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f4.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining1_lookupflag_f1.otf +Tests/ttLib/tables/data/aots/gsub_chaining1_lookupflag_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining1_lookupflag_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining1_multiple_subrules_f1.otf +Tests/ttLib/tables/data/aots/gsub_chaining1_multiple_subrules_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining1_multiple_subrules_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining1_multiple_subrules_f2.otf +Tests/ttLib/tables/data/aots/gsub_chaining1_multiple_subrules_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining1_multiple_subrules_f2.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining1_next_glyph_f1.otf +Tests/ttLib/tables/data/aots/gsub_chaining1_next_glyph_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining1_next_glyph_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f1.otf +Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f2.otf +Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f2.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining1_successive_f1.otf +Tests/ttLib/tables/data/aots/gsub_chaining1_successive_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining1_successive_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f1.otf +Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f2.otf +Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f2.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f3.otf +Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f3.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f3.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f4.otf +Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f4.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f4.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining2_lookupflag_f1.otf +Tests/ttLib/tables/data/aots/gsub_chaining2_lookupflag_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining2_lookupflag_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f1.otf +Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f2.otf +Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f2.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining2_next_glyph_f1.otf +Tests/ttLib/tables/data/aots/gsub_chaining2_next_glyph_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining2_next_glyph_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f1.otf +Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f2.otf +Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f2.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining2_successive_f1.otf +Tests/ttLib/tables/data/aots/gsub_chaining2_successive_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining2_successive_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f1.otf +Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f2.otf +Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f2.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f3.otf +Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f3.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f3.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f4.otf +Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f4.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f4.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining3_lookupflag_f1.otf +Tests/ttLib/tables/data/aots/gsub_chaining3_lookupflag_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining3_lookupflag_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining3_next_glyph_f1.otf +Tests/ttLib/tables/data/aots/gsub_chaining3_next_glyph_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining3_next_glyph_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f1.otf +Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f2.otf +Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f2.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_chaining3_successive_f1.otf +Tests/ttLib/tables/data/aots/gsub_chaining3_successive_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_chaining3_successive_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context1_boundary_f1.otf +Tests/ttLib/tables/data/aots/gsub_context1_boundary_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context1_boundary_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context1_boundary_f2.otf +Tests/ttLib/tables/data/aots/gsub_context1_boundary_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context1_boundary_f2.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context1_expansion_f1.otf +Tests/ttLib/tables/data/aots/gsub_context1_expansion_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context1_expansion_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f1.otf +Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f2.otf +Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f2.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f1.otf +Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f2.otf +Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f2.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context1_next_glyph_f1.otf +Tests/ttLib/tables/data/aots/gsub_context1_next_glyph_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context1_next_glyph_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context1_simple_f1.otf +Tests/ttLib/tables/data/aots/gsub_context1_simple_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context1_simple_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context1_simple_f2.otf +Tests/ttLib/tables/data/aots/gsub_context1_simple_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context1_simple_f2.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context1_successive_f1.otf +Tests/ttLib/tables/data/aots/gsub_context1_successive_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context1_successive_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context2_boundary_f1.otf +Tests/ttLib/tables/data/aots/gsub_context2_boundary_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context2_boundary_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context2_boundary_f2.otf +Tests/ttLib/tables/data/aots/gsub_context2_boundary_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context2_boundary_f2.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context2_classes_f1.otf +Tests/ttLib/tables/data/aots/gsub_context2_classes_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context2_classes_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context2_classes_f2.otf +Tests/ttLib/tables/data/aots/gsub_context2_classes_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context2_classes_f2.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context2_expansion_f1.otf +Tests/ttLib/tables/data/aots/gsub_context2_expansion_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context2_expansion_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f1.otf +Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f2.otf +Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f2.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f1.otf +Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f2.otf +Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f2.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context2_next_glyph_f1.otf +Tests/ttLib/tables/data/aots/gsub_context2_next_glyph_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context2_next_glyph_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context2_simple_f1.otf +Tests/ttLib/tables/data/aots/gsub_context2_simple_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context2_simple_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context2_simple_f2.otf +Tests/ttLib/tables/data/aots/gsub_context2_simple_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context2_simple_f2.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context2_successive_f1.otf +Tests/ttLib/tables/data/aots/gsub_context2_successive_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context2_successive_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context3_boundary_f1.otf +Tests/ttLib/tables/data/aots/gsub_context3_boundary_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context3_boundary_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context3_boundary_f2.otf +Tests/ttLib/tables/data/aots/gsub_context3_boundary_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context3_boundary_f2.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context3_lookupflag_f1.otf +Tests/ttLib/tables/data/aots/gsub_context3_lookupflag_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context3_lookupflag_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context3_lookupflag_f2.otf +Tests/ttLib/tables/data/aots/gsub_context3_lookupflag_f2.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context3_lookupflag_f2.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context3_next_glyph_f1.otf +Tests/ttLib/tables/data/aots/gsub_context3_next_glyph_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context3_next_glyph_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context3_simple_f1.otf +Tests/ttLib/tables/data/aots/gsub_context3_simple_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context3_simple_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/gsub_context3_successive_f1.otf +Tests/ttLib/tables/data/aots/gsub_context3_successive_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/gsub_context3_successive_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/lookupflag_ignore_attach_f1.otf +Tests/ttLib/tables/data/aots/lookupflag_ignore_attach_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/lookupflag_ignore_attach_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/lookupflag_ignore_base_f1.otf +Tests/ttLib/tables/data/aots/lookupflag_ignore_base_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/lookupflag_ignore_base_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/lookupflag_ignore_combination_f1.otf +Tests/ttLib/tables/data/aots/lookupflag_ignore_combination_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/lookupflag_ignore_combination_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/lookupflag_ignore_ligatures_f1.otf +Tests/ttLib/tables/data/aots/lookupflag_ignore_ligatures_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/lookupflag_ignore_ligatures_f1.ttx.GSUB +Tests/ttLib/tables/data/aots/lookupflag_ignore_marks_f1.otf +Tests/ttLib/tables/data/aots/lookupflag_ignore_marks_f1.ttx.GDEF +Tests/ttLib/tables/data/aots/lookupflag_ignore_marks_f1.ttx.GSUB +Tests/ttLib/tables/data/graphite/graphite_tests.ttf +Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Feat +Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Glat +Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Glat.setup +Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Silf +Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Silf.setup +Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Sill +Tests/ttx/ttx_test.py +Tests/ttx/data/TestBOM.ttx +Tests/ttx/data/TestDFONT.dfont +Tests/ttx/data/TestNoSFNT.ttx +Tests/ttx/data/TestNoXML.ttx +Tests/ttx/data/TestOTF.otf +Tests/ttx/data/TestOTF.ttx +Tests/ttx/data/TestTTC.ttc +Tests/ttx/data/TestTTF.ttf +Tests/ttx/data/TestTTF.ttx +Tests/ttx/data/TestWOFF.woff +Tests/ttx/data/TestWOFF2.woff2 +Tests/varLib/__init__.py +Tests/varLib/builder_test.py +Tests/varLib/designspace_test.py +Tests/varLib/interpolatable_test.py +Tests/varLib/interpolate_layout_test.py +Tests/varLib/models_test.py +Tests/varLib/mutator_test.py +Tests/varLib/varLib_test.py +Tests/varLib/data/Build.designspace +Tests/varLib/data/BuildAvarEmptyAxis.designspace +Tests/varLib/data/BuildAvarIdentityMaps.designspace +Tests/varLib/data/BuildAvarSingleAxis.designspace +Tests/varLib/data/Designspace.designspace +Tests/varLib/data/Designspace2.designspace +Tests/varLib/data/InterpolateLayout.designspace +Tests/varLib/data/InterpolateLayout2.designspace +Tests/varLib/data/InterpolateLayout3.designspace +Tests/varLib/data/master_ttx_interpolatable_otf/TestFamily2-Master0.ttx +Tests/varLib/data/master_ttx_interpolatable_otf/TestFamily2-Master1.ttx +Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master0.ttx +Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master1.ttx +Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master2.ttx +Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master3.ttx +Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master4.ttx +Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily2-Master0.ttx +Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily2-Master1.ttx +Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Bold.ttx +Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Condensed.ttx +Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-CondensedBold.ttx +Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-CondensedLight.ttx +Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-CondensedSemiBold.ttx +Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Light.ttx +Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Regular.ttx +Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-SemiBold.ttx +Tests/varLib/data/master_ttx_varfont_ttf/Mutator_IUP.ttx +Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/features.fea +Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/fontinfo.plist +Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/lib.plist +Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/metainfo.plist +Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/A_.glif +Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/_notdef.glif +Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/a.glif +Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/contents.plist +Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/dollar.glif +Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/dollar.nostroke.glif +Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/space.glif +Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/features.fea +Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/fontinfo.plist +Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/lib.plist +Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/metainfo.plist +Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/A_.glif +Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/_notdef.glif +Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/a.glif +Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/contents.plist +Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/dollar.glif +Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/dollar.nostroke.glif +Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/space.glif +Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/features.fea +Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/fontinfo.plist +Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/lib.plist +Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/metainfo.plist +Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/A_.glif +Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/_notdef.glif +Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/a.glif +Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/contents.plist +Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/dollar.glif +Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/dollar.nostroke.glif +Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/space.glif +Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/features.fea +Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/fontinfo.plist +Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/lib.plist +Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/metainfo.plist +Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/A_.glif +Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/_notdef.glif +Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/a.glif +Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/contents.plist +Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/dollar.glif +Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/dollar.nostroke.glif +Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/space.glif +Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/features.fea +Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/fontinfo.plist +Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/lib.plist +Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/metainfo.plist +Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/A_.glif +Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/_notdef.glif +Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/a.glif +Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/contents.plist +Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/dollar.glif +Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/dollar.nostroke.glif +Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/space.glif +Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/features.fea +Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/fontinfo.plist +Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/lib.plist +Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/metainfo.plist +Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/A_.glif +Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/A_.sc.glif +Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/_notdef.glif +Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/a.alt.glif +Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/a.glif +Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/ampersand.glif +Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/atilde.glif +Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/circledotted.glif +Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/contents.plist +Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/d.glif +Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/dieresisbelowcmb.glif +Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/dieresiscmb.glif +Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/f.glif +Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/f_t.glif +Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/n.glif +Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/space.glif +Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/t.glif +Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/tildebelowcmb.glif +Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/tildecmb.glif +Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/features.fea +Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/fontinfo.plist +Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/lib.plist +Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/metainfo.plist +Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/A_.glif +Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/A_.sc.glif +Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/_notdef.glif +Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/a.alt.glif +Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/a.glif +Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/ampersand.glif +Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/atilde.glif +Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/circledotted.glif +Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/contents.plist +Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/d.glif +Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/dieresisbelowcmb.glif +Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/dieresiscmb.glif +Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/f.glif +Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/f_t.glif +Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/n.glif +Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/space.glif +Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/t.glif +Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/tildebelowcmb.glif +Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/tildecmb.glif +Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/fontinfo.plist +Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/groups.plist +Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/kerning.plist +Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/layercontents.plist +Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/lib.plist +Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/metainfo.plist +Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/F_.glif +Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/T_.glif +Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/contents.plist +Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/l.glif +Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/layerinfo.plist +Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/n.glif +Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/o.glif +Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/s.glif +Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/t.glif +Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/fontinfo.plist +Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/groups.plist +Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/kerning.plist +Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/layercontents.plist +Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/lib.plist +Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/metainfo.plist +Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/F_.glif +Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/T_.glif +Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/contents.plist +Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/l.glif +Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/layerinfo.plist +Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/n.glif +Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/o.glif +Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/s.glif +Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/t.glif +Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/fontinfo.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/groups.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/kerning.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/layercontents.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/lib.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/metainfo.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/F_.glif +Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/T_.glif +Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/contents.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/l.glif +Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/layerinfo.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/n.glif +Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/o.glif +Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/s.glif +Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/t.glif +Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/fontinfo.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/groups.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/kerning.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/layercontents.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/lib.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/metainfo.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/F_.glif +Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/T_.glif +Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/contents.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/l.glif +Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/layerinfo.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/n.glif +Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/o.glif +Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/s.glif +Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/t.glif +Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/fontinfo.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/groups.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/kerning.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/layercontents.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/lib.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/metainfo.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/F_.glif +Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/T_.glif +Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/contents.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/l.glif +Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/layerinfo.plist +Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/n.glif +Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/o.glif +Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/s.glif +Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/t.glif +Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/fontinfo.plist +Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/groups.plist +Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/kerning.plist +Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/layercontents.plist +Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/lib.plist +Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/metainfo.plist +Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/F_.glif +Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/T_.glif +Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/contents.plist +Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/l.glif +Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/layerinfo.plist +Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/n.glif +Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/o.glif +Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/s.glif +Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/t.glif +Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/fontinfo.plist +Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/groups.plist +Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/kerning.plist +Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/layercontents.plist +Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/lib.plist +Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/metainfo.plist +Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/F_.glif +Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/T_.glif +Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/contents.plist +Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/l.glif +Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/layerinfo.plist +Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/n.glif +Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/o.glif +Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/s.glif +Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/t.glif +Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/fontinfo.plist +Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/groups.plist +Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/kerning.plist +Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/layercontents.plist +Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/lib.plist +Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/metainfo.plist +Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/F_.glif +Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/T_.glif +Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/contents.plist +Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/l.glif +Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/layerinfo.plist +Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/n.glif +Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/o.glif +Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/s.glif +Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/t.glif +Tests/varLib/data/test_results/Build.ttx +Tests/varLib/data/test_results/Build3.ttx +Tests/varLib/data/test_results/BuildAvarEmptyAxis.ttx +Tests/varLib/data/test_results/BuildAvarIdentityMaps.ttx +Tests/varLib/data/test_results/BuildAvarSingleAxis.ttx +Tests/varLib/data/test_results/BuildMain.ttx +Tests/varLib/data/test_results/InterpolateLayout.ttx +Tests/varLib/data/test_results/InterpolateLayout2.ttx +Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_diff.ttx +Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_diff2.ttx +Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_same.ttx +Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_class_diff.ttx +Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_class_diff2.ttx +Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_class_same.ttx +Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_spec_diff.ttx +Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_spec_diff2.ttx +Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_spec_same.ttx +Tests/varLib/data/test_results/InterpolateLayoutGPOS_3_diff.ttx +Tests/varLib/data/test_results/InterpolateLayoutGPOS_3_same.ttx +Tests/varLib/data/test_results/InterpolateLayoutGPOS_4_diff.ttx +Tests/varLib/data/test_results/InterpolateLayoutGPOS_4_same.ttx +Tests/varLib/data/test_results/InterpolateLayoutGPOS_5_diff.ttx +Tests/varLib/data/test_results/InterpolateLayoutGPOS_5_same.ttx +Tests/varLib/data/test_results/InterpolateLayoutGPOS_6_diff.ttx +Tests/varLib/data/test_results/InterpolateLayoutGPOS_6_same.ttx +Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx +Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_same.ttx +Tests/varLib/data/test_results/InterpolateLayoutGPOS_size_feat_same.ttx +Tests/varLib/data/test_results/InterpolateLayoutMain.ttx +Tests/varLib/data/test_results/Mutator.ttx +Tests/varLib/data/test_results/Mutator_IUP-instance.ttx +Tests/voltLib/lexer_test.py +Tests/voltLib/parser_test.py \ No newline at end of file diff --git a/Lib/fonttools.egg-info/dependency_links.txt b/Lib/fonttools.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Lib/fonttools.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/Lib/fonttools.egg-info/entry_points.txt b/Lib/fonttools.egg-info/entry_points.txt new file mode 100644 index 0000000..6d70774 --- /dev/null +++ b/Lib/fonttools.egg-info/entry_points.txt @@ -0,0 +1,7 @@ +[console_scripts] +fonttools = fontTools.__main__:main +pyftinspect = fontTools.inspect:main +pyftmerge = fontTools.merge:main +pyftsubset = fontTools.subset:main +ttx = fontTools.ttx:main + diff --git a/Lib/fonttools.egg-info/top_level.txt b/Lib/fonttools.egg-info/top_level.txt new file mode 100644 index 0000000..9af65ba --- /dev/null +++ b/Lib/fonttools.egg-info/top_level.txt @@ -0,0 +1 @@ +fontTools diff --git a/MANIFEST.in b/MANIFEST.in index db16aa2..1529112 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,13 +1,34 @@ -include LICENSE.txt -include MANIFEST.in -include Doc/ttx.1 -include Doc/ChangeLog -include Doc/*.txt -include Doc/*.html +include README.rst +include LICENSE +include LICENSE.external +include NEWS.rst +include Makefile +include fonttools +include Snippets/*.py +include Snippets/README.md include MetaTools/*.py -include Windows/mcmillan.bat -include Windows/ttx.ico -include Windows/README.TXT -include Windows/fonttools-win-setup.iss -include Windows/fonttools-win-setup.txt include Lib/fontTools/ttLib/tables/table_API_readme.txt + +include *requirements.txt +include tox.ini +include run-tests.sh + +include .appveyor.yml +include .codecov.yml +include .coveragerc +include .travis.yml +recursive-include .travis *.sh + +include Doc/Makefile +include Doc/make.bat +recursive-include Doc/man/man1 *.1 +recursive-include Doc/source *.py *.rst + +recursive-include Tests *.py *.ttx *.otx *.fea *.feax +recursive-include Tests *.ttc *.ttf *.dfont *.woff *.woff2 +recursive-include Tests *.otf *.ttx.* +recursive-include Tests *.glif *.plist +recursive-include Tests *.txt README +recursive-include Tests *.lwfn *.pfa *.pfb +recursive-include Tests *.xml *.designspace *.bin +recursive-include Tests *.afm diff --git a/METADATA b/METADATA new file mode 100644 index 0000000..f5503e3 --- /dev/null +++ b/METADATA @@ -0,0 +1,18 @@ +name: "fonttools" +description: "fontTools is a library for manipulating fonts, written in Python." +third_party { + url { + type: HOMEPAGE + value: "https://github.com/fonttools/fonttools" + } + url { + type: ARCHIVE + value: "https://github.com/fonttools/fonttools/releases/download/3.28.0/fonttools-3.28.0.zip" + } + version: "3.28.0" + last_upgrade_date { + year: 2018 + month: 7 + day: 3 + } +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bd91d7f --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +all: + ./setup.py build + +dist: + ./setup.py sdist bdist_wheel + +install: + pip install --ignore-installed . + +install-user: + pip install --ignore-installed --user . + +uninstall: + pip uninstall --yes fonttools + +check: all + ./run-tests.sh + +clean: + ./setup.py clean --all + +.PHONY: all dist install install-user uninstall check clean diff --git a/MetaTools/buildChangeLog.py b/MetaTools/buildChangeLog.py deleted file mode 100755 index d29e379..0000000 --- a/MetaTools/buildChangeLog.py +++ /dev/null @@ -1,10 +0,0 @@ -#! /usr/bin/env python - -import os, sys - -fontToolsDir = os.path.dirname(os.path.dirname(os.path.normpath( - os.path.join(os.getcwd(), sys.argv[0])))) - -os.chdir(fontToolsDir) -os.system("git2cl > Doc/ChangeLog") -print("done.") diff --git a/MetaTools/buildTableList.py b/MetaTools/buildTableList.py index 1e77492..de8a039 100755 --- a/MetaTools/buildTableList.py +++ b/MetaTools/buildTableList.py @@ -4,13 +4,14 @@ import sys import os import glob from fontTools.ttLib import identifierToTag +import textwrap fontToolsDir = os.path.dirname(os.path.dirname(os.path.join(os.getcwd(), sys.argv[0]))) fontToolsDir= os.path.normpath(fontToolsDir) tablesDir = os.path.join(fontToolsDir, "Lib", "fontTools", "ttLib", "tables") -docFile = os.path.join(fontToolsDir, "Doc", "documentation.html") +docFile = os.path.join(fontToolsDir, "README.rst") names = glob.glob1(tablesDir, "*.py") @@ -31,25 +32,42 @@ tables.sort() file = open(os.path.join(tablesDir, "__init__.py"), "w") -file.write("# DON'T EDIT! This file is generated by MetaTools/buildTableList.py.\n") -file.write("def _moduleFinderHint():\n") -file.write('\t"""Dummy function to let modulefinder know what tables may be\n') -file.write('\tdynamically imported. Generated by MetaTools/buildTableList.py.\n') -file.write('\t"""\n') +file.write(''' +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * + +# DON'T EDIT! This file is generated by MetaTools/buildTableList.py. +def _moduleFinderHint(): + """Dummy function to let modulefinder know what tables may be + dynamically imported. Generated by MetaTools/buildTableList.py. + + >>> _moduleFinderHint() + """ +''') + for module in modules: file.write("\tfrom . import %s\n" % module) +file.write(''' +if __name__ == "__main__": + import doctest, sys + sys.exit(doctest.testmod().failed) +''') + file.close() -begin = "<!-- begin table list -->" -end = "<!-- end table list -->" +begin = ".. begin table list\n.. code::\n" +end = ".. end table list" doc = open(docFile).read() beginPos = doc.find(begin) assert beginPos > 0 beginPos = beginPos + len(begin) + 1 endPos = doc.find(end) -doc = doc[:beginPos] + ", ".join(tables[:-1]) + " and " + tables[-1] + "\n" + doc[endPos:] +lines = textwrap.wrap(", ".join(tables[:-1]) + " and " + tables[-1], 66) +blockquote = "\n".join(" "*4 + line for line in lines) + "\n" + +doc = doc[:beginPos] + blockquote + doc[endPos:] open(docFile, "w").write(doc) diff --git a/MetaTools/buildUCD.py b/MetaTools/buildUCD.py new file mode 100755 index 0000000..12bd58f --- /dev/null +++ b/MetaTools/buildUCD.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python +""" +Tools to parse data files from the Unicode Character Database. +""" + +from __future__ import print_function, absolute_import, division +from __future__ import unicode_literals + +try: + from urllib.request import urlopen +except ImportError: + from urllib2 import urlopen +from contextlib import closing, contextmanager +import re +from codecs import iterdecode +import logging +import os +from io import open +from os.path import abspath, dirname, join as pjoin, pardir, sep + + +try: # pragma: no cover + unicode +except NameError: + unicode = str + + +UNIDATA_URL = "https://unicode.org/Public/UNIDATA/" +UNIDATA_LICENSE_URL = "http://unicode.org/copyright.html#License" + +# by default save output files to ../Lib/fontTools/unicodedata/ +UNIDATA_PATH = pjoin(abspath(dirname(__file__)), pardir, + "Lib", "fontTools", "unicodedata") + sep + +SRC_ENCODING = "# -*- coding: utf-8 -*-\n" + +NOTICE = "# NOTE: This file was auto-generated with MetaTools/buildUCD.py.\n" + +MAX_UNICODE = 0x10FFFF + +log = logging.getLogger() + + +@contextmanager +def open_unidata_file(filename): + """Open a text file from https://unicode.org/Public/UNIDATA/""" + url = UNIDATA_URL + filename + with closing(urlopen(url)) as response: + yield iterdecode(response, encoding="utf-8") + + +def parse_unidata_header(infile): + """Read the top header of data files, until the first line + that does not start with '#'. + """ + header = [] + line = next(infile) + while line.startswith("#"): + header.append(line) + line = next(infile) + return "".join(header) + + +def parse_range_properties(infile, default=None, is_set=False): + """Parse a Unicode data file containing a column with one character or + a range of characters, and another column containing a property value + separated by a semicolon. Comments after '#' are ignored. + + If the ranges defined in the data file are not continuous, assign the + 'default' property to the unassigned codepoints. + + Return a list of (start, end, property_name) tuples. + """ + ranges = [] + line_regex = re.compile( + r"^" + r"([0-9A-F]{4,6})" # first character code + r"(?:\.\.([0-9A-F]{4,6}))?" # optional second character code + r"\s*;\s*" + r"([^#]+)") # everything up to the potential comment + for line in infile: + match = line_regex.match(line) + if not match: + continue + + first, last, data = match.groups() + if last is None: + last = first + + first = int(first, 16) + last = int(last, 16) + data = str(data.rstrip()) + + ranges.append((first, last, data)) + + ranges.sort() + + if isinstance(default, unicode): + default = str(default) + + # fill the gaps between explicitly defined ranges + last_start, last_end = -1, -1 + full_ranges = [] + for start, end, value in ranges: + assert last_end < start + assert start <= end + if start - last_end > 1: + full_ranges.append((last_end+1, start-1, default)) + if is_set: + value = set(value.split()) + full_ranges.append((start, end, value)) + last_start, last_end = start, end + if last_end != MAX_UNICODE: + full_ranges.append((last_end+1, MAX_UNICODE, default)) + + # reduce total number of ranges by combining continuous ones + last_start, last_end, last_value = full_ranges.pop(0) + merged_ranges = [] + for start, end, value in full_ranges: + if value == last_value: + continue + else: + merged_ranges.append((last_start, start-1, last_value)) + last_start, line_end, last_value = start, end, value + merged_ranges.append((last_start, MAX_UNICODE, last_value)) + + # make sure that the ranges cover the full unicode repertoire + assert merged_ranges[0][0] == 0 + for (cs, ce, cv), (ns, ne, nv) in zip(merged_ranges, merged_ranges[1:]): + assert ce+1 == ns + assert merged_ranges[-1][1] == MAX_UNICODE + + return merged_ranges + + +def parse_semicolon_separated_data(infile): + """Parse a Unicode data file where each line contains a lists of values + separated by a semicolon (e.g. "PropertyValueAliases.txt"). + The number of the values on different lines may be different. + + Returns a list of lists each containing the values as strings. + """ + data = [] + for line in infile: + line = line.split('#', 1)[0].strip() # remove the comment + if not line: + continue + fields = [str(field.strip()) for field in line.split(';')] + data.append(fields) + return data + + +def _set_repr(value): + return 'None' if value is None else "{{{}}}".format( + ", ".join(repr(v) for v in sorted(value))) + + +def build_ranges(filename, local_ucd=None, output_path=None, + default=None, is_set=False, aliases=None): + """Fetch 'filename' UCD data file from Unicode official website, parse + the property ranges and values and write them as two Python lists + to 'fontTools.unicodedata.<filename>.py'. + + 'aliases' is an optional mapping of property codes (short names) to long + name aliases (list of strings, with the first item being the preferred + alias). When this is provided, the property values are written using the + short notation, and an additional 'NAMES' dict with the aliases is + written to the output module. + + To load the data file from a local directory, you can use the + 'local_ucd' argument. + """ + modname = os.path.splitext(filename)[0] + ".py" + if not output_path: + output_path = UNIDATA_PATH + modname + + if local_ucd: + log.info("loading '%s' from local directory '%s'", filename, local_ucd) + cm = open(pjoin(local_ucd, filename), "r", encoding="utf-8") + else: + log.info("downloading '%s' from '%s'", filename, UNIDATA_URL) + cm = open_unidata_file(filename) + + with cm as f: + header = parse_unidata_header(f) + ranges = parse_range_properties(f, default=default, is_set=is_set) + + if aliases: + reversed_aliases = {normalize(v[0]): k for k, v in aliases.items()} + max_value_length = 6 # 4-letter tags plus two quotes for repr + else: + max_value_length = min(56, max(len(repr(v)) for _, _, v in ranges)) + + with open(output_path, "w", encoding="utf-8") as f: + f.write(SRC_ENCODING) + f.write("#\n") + f.write(NOTICE) + f.write("# Source: {}{}\n".format(UNIDATA_URL, filename)) + f.write("# License: {}\n".format(UNIDATA_LICENSE_URL)) + f.write("#\n") + f.write(header+"\n\n") + + f.write("RANGES = [\n") + for first, last, value in ranges: + f.write(" 0x{:0>4X}, # .. 0x{:0>4X} ; {}\n".format( + first, last, _set_repr(value) if is_set else value)) + f.write("]\n") + + f.write("\n") + f.write("VALUES = [\n") + for first, last, value in ranges: + comment = "# {:0>4X}..{:0>4X}".format(first, last) + if is_set: + value_repr = "{},".format(_set_repr(value)) + else: + if aliases: + # append long name to comment and use the short code + comment += " ; {}".format(value) + value = reversed_aliases[normalize(value)] + value_repr = "{!r},".format(value) + f.write(" {} {}\n".format( + value_repr.ljust(max_value_length+1), comment)) + f.write("]\n") + + if aliases: + f.write("\n") + f.write("NAMES = {\n") + for value, names in sorted(aliases.items()): + # we only write the first preferred alias + f.write(" {!r}: {!r},\n".format(value, names[0])) + f.write("}\n") + + log.info("saved new file: '%s'", os.path.normpath(output_path)) + + +_normalize_re = re.compile(r"[-_ ]+") + +def normalize(string): + """Remove case, strip space, '-' and '_' for loose matching.""" + return _normalize_re.sub("", string).lower() + + +def parse_property_value_aliases(property_tag, local_ucd=None): + """Fetch the current 'PropertyValueAliases.txt' from the Unicode website, + parse the values for the specified 'property_tag' and return a dictionary + of name aliases (list of strings) keyed by short value codes (strings). + + To load the data file from a local directory, you can use the + 'local_ucd' argument. + """ + filename = "PropertyValueAliases.txt" + if local_ucd: + log.info("loading '%s' from local directory '%s'", filename, local_ucd) + cm = open(pjoin(local_ucd, filename), "r", encoding="utf-8") + else: + log.info("downloading '%s' from '%s'", filename, UNIDATA_URL) + cm = open_unidata_file(filename) + + with cm as f: + header = parse_unidata_header(f) + data = parse_semicolon_separated_data(f) + + aliases = {item[1]: item[2:] for item in data + if item[0] == property_tag} + + return aliases + + +def main(): + import argparse + + parser = argparse.ArgumentParser( + description="Generate fontTools.unicodedata from UCD data files") + parser.add_argument( + '--ucd-path', help="Path to local folder containing UCD data files") + parser.add_argument('-q', '--quiet', action="store_true") + options = parser.parse_args() + + level = "WARNING" if options.quiet else "INFO" + logging.basicConfig(level=level, format="%(message)s") + + build_ranges("Blocks.txt", local_ucd=options.ucd_path, default="No_Block") + + script_aliases = parse_property_value_aliases("sc", options.ucd_path) + build_ranges("Scripts.txt", local_ucd=options.ucd_path, default="Unknown", + aliases=script_aliases) + build_ranges("ScriptExtensions.txt", local_ucd=options.ucd_path, + is_set=True) + + +if __name__ == "__main__": + import sys + sys.exit(main()) diff --git a/NEWS.rst b/NEWS.rst new file mode 100644 index 0000000..5341ca5 --- /dev/null +++ b/NEWS.rst @@ -0,0 +1,980 @@ +3.28.0 (released 2018-06-19) +---------------------------- + +- [featureVars] Added experimental module to build ``FeatureVariations`` + tables. Still needs to be hooked up to ``varLib.build`` (#1240). +- [fixedTools] Added ``otRound`` to round floats to nearest integer towards + positive Infinity. This is now used where we deal with visual data like X/Y + coordinates, advance widths/heights, variation deltas, and similar (#1274, + #1248). +- [subset] Improved GSUB closure memoize algorithm. +- [varLib.models] Fixed regression in model resolution (180124, #1269). +- [feaLib.ast] Fixed error when converting ``SubtableStatement`` to string + (#1275). +- [varLib.mutator] Set ``OS/2.usWeightClass`` and ``usWidthClass``, and + ``post.italicAngle`` based on the 'wght', 'wdth' and 'slnt' axis values + (#1276, #1264). +- [py23/loggingTools] Don't automatically set ``logging.lastResort`` handler + on py27. Moved ``LastResortLogger`` to the ``loggingTools`` module (#1277). + +3.27.1 (released 2018-06-11) +---------------------------- + +- [ttGlyphPen] Issue a warning and skip building non-existing components + (https://github.com/googlei18n/fontmake/issues/411). +- [tests] Fixed issue running ttx_test.py from a tagged commit. + +3.27.0 (released 2018-06-11) +---------------------------- + +- [designspaceLib] Added new ``conditionSet`` element to ``rule`` element in + designspace document. Bumped ``format`` attribute to ``4.0`` (previously, + it was formatted as an integer). Removed ``checkDefault``, ``checkAxes`` + methods, and any kind of guessing about the axes when the ``<axes>`` element + is missing. The default master is expected at the intersection of all default + values for each axis (#1254, #1255, #1267). +- [cffLib] Fixed issues when compiling CFF2 or converting from CFF when the + font has an FDArray (#1211, #1271). +- [varLib] Avoid attempting to build ``cvar`` table when ``glyf`` table is not + present, as is the case for CFF2 fonts. +- [subset] Handle None coverages in MarkGlyphSets; revert commit 02616ab that + sets empty Coverage tables in MarkGlyphSets to None, to make OTS happy. +- [ttFont] Allow to build glyph order from ``maxp.numGlyphs`` when ``post`` or + ``cmap`` are missing. +- [ttFont] Added ``__len__`` method to ``_TTGlyphSet``. +- [glyf] Ensure ``GlyphCoordinates`` never overflow signed shorts (#1230). +- [py23] Added alias for ``itertools.izip`` shadowing the built-in ``zip``. +- [loggingTools] Memoize ``log`` property of ``LogMixin`` class (fbab12). +- [ttx] Impoved test coverage (#1261). +- [Snippets] Addded script to append a suffix to all family names in a font. +- [varLib.plot] Make it work with matplotlib >= 2.1 (b38e2b). + +3.26.0 (released 2018-05-03) +---------------------------- + +- [designspace] Added a new optional ``layer`` attribute to the source element, + and a corresponding ``layerName`` attribute to the ``SourceDescriptor`` + object (#1253). + Added ``conditionset`` element to the ``rule`` element to the spec, but not + implemented in designspace reader/writer yet (#1254). +- [varLib.models] Refine modeling one last time (0ecf5c5). +- [otBase] Fixed sharing of tables referred to by different offset sizes + (795f2f9). +- [subset] Don't drop a GDEF that only has VarStore (fc819d6). Set to None + empty Coverage tables in MarkGlyphSets (02616ab). +- [varLib]: Added ``--master-finder`` command-line option (#1249). +- [varLib.mutator] Prune fvar nameIDs from instance's name table (#1245). +- [otTables] Allow decompiling bad ClassDef tables with invalid format, with + warning (#1236). +- [varLib] Make STAT v1.2 and reuse nameIDs from fvar table (#1242). +- [varLib.plot] Show master locations. Set axis limits to -1, +1. +- [subset] Handle HVAR direct mapping. Passthrough 'cvar'. + Added ``--font-number`` command-line option for collections. +- [t1Lib] Allow a text encoding to be specified when parsing a Type 1 font + (#1234). Added ``kind`` argument to T1Font constructor (c5c161c). +- [ttLib] Added context manager API to ``TTFont`` class, so it can be used in + ``with`` statements to auto-close the file when exiting the context (#1232). + +3.25.0 (released 2018-04-03) +---------------------------- + +- [varLib] Improved support-resolution algorithm. Previously, the on-axis + masters would always cut the space. They don't anymore. That's more + consistent, and fixes the main issue Erik showed at TYPO Labs 2017. + Any varfont built that had an unusual master configuration will change + when rebuilt (42bef17, a523a697, + https://github.com/googlei18n/fontmake/issues/264). +- [varLib.models] Added a ``main()`` entry point, that takes positions and + prints model results. +- [varLib.plot] Added new module to plot a designspace's + VariationModel. Requires ``matplotlib``. +- [varLib.mutator] Added -o option to specify output file path (2ef60fa). +- [otTables] Fixed IndexError while pruning of HVAR pre-write (6b6c34a). +- [varLib.models] Convert delta array to floats if values overflows signed + short integer (0055f94). + +3.24.2 (released 2018-03-26) +---------------------------- + +- [otBase] Don't fail during ``ValueRecord`` copy if src has more items. + We drop hinting in the subsetter by simply changing ValueFormat, without + cleaning up the actual ValueRecords. This was causing assertion error if + a variable font was subsetted without hinting and then passed directly to + the mutator for instantiation without first it saving to disk. + +3.24.1 (released 2018-03-06) +---------------------------- + +- [varLib] Don't remap the same ``DeviceTable`` twice in VarStore optimizer + (#1206). +- [varLib] Add ``--disable-iup`` option to ``fonttools varLib`` script, + and a ``optimize=True`` keyword argument to ``varLib.build`` function, + to optionally disable IUP optimization while building varfonts. +- [ttCollection] Fixed issue while decompiling ttc with python3 (#1207). + +3.24.0 (released 2018-03-01) +---------------------------- + +- [ttGlyphPen] Decompose composite glyphs if any components' transform is too + large to fit a ``F2Dot14`` value, or clamp transform values that are + (almost) equal to +2.0 to make them fit and avoid decomposing (#1200, + #1204, #1205). +- [ttx] Added new ``-g`` option to dump glyphs from the ``glyf`` table + splitted as individual ttx files (#153, #1035, #1132, #1202). +- Copied ``ufoLib.filenames`` module to ``fontTools.misc.filenames``, used + for the ttx split-glyphs option (#1202). +- [feaLib] Added support for ``cvParameters`` blocks in Character Variant + feautures ``cv01-cv99`` (#860, #1169). +- [Snippets] Added ``checksum.py`` script to generate/check SHA1 hash of + ttx files (#1197). +- [varLib.mutator] Fixed issue while instantiating some variable fonts + whereby the horizontal advance width computed from ``gvar`` phantom points + could turn up to be negative (#1198). +- [varLib/subset] Fixed issue with subsetting GPOS variation data not + picking up ``ValueRecord`` ``Device`` objects (54fd71f). +- [feaLib/voltLib] In all AST elements, the ``location`` is no longer a + required positional argument, but an optional kewyord argument (defaults + to ``None``). This will make it easier to construct feature AST from + code (#1201). + + +3.23.0 (released 2018-02-26) +---------------------------- + +- [designspaceLib] Added an optional ``lib`` element to the designspace as a + whole, as well as to the instance elements, to store arbitrary data in a + property list dictionary, similar to the UFO's ``lib``. Added an optional + ``font`` attribute to the ``SourceDescriptor``, to allow operating on + in-memory font objects (#1175). +- [cffLib] Fixed issue with lazy-loading of attributes when attempting to + set the CFF TopDict.Encoding (#1177, #1187). +- [ttx] Fixed regression introduced in 3.22.0 that affected the split tables + ``-s`` option (#1188). +- [feaLib] Added ``IncludedFeaNotFound`` custom exception subclass, raised + when an included feature file cannot be found (#1186). +- [otTables] Changed ``VarIdxMap`` to use glyph names internally instead of + glyph indexes. The old ttx dumps of HVAR/VVAR tables that contain indexes + can still be imported (21cbab8, 38a0ffb). +- [varLib] Implemented VarStore optimizer (#1184). +- [subset] Implemented pruning of GDEF VarStore, HVAR and MVAR (#1179). +- [sfnt] Restore backward compatiblity with ``numFonts`` attribute of + ``SFNTReader`` object (#1181). +- [merge] Initial support for merging ``LangSysRecords`` (#1180). +- [ttCollection] don't seek(0) when writing to possibly unseekable strems. +- [subset] Keep all ``--name-IDs`` from 0 to 6 by default (#1170, #605, #114). +- [cffLib] Added ``width`` module to calculate optimal CFF default and + nominal glyph widths. +- [varLib] Don’t fail if STAT already in the master fonts (#1166). + +3.22.0 (released 2018-02-04) +---------------------------- + +- [subset] Support subsetting ``endchar`` acting as ``seac``-like components + in ``CFF`` (fixes #1162). +- [feaLib] Allow to build from pre-parsed ``ast.FeatureFile`` object. + Added ``tables`` argument to only build some tables instead of all (#1159, + #1163). +- [textTools] Replaced ``safeEval`` with ``ast.literal_eval`` (#1139). +- [feaLib] Added option to the parser to not resolve ``include`` statements + (#1154). +- [ttLib] Added new ``ttCollection`` module to read/write TrueType and + OpenType Collections. Exports a ``TTCollection`` class with a ``fonts`` + attribute containing a list of ``TTFont`` instances, the methods ``save`` + and ``saveXML``, plus some list-like methods. The ``importXML`` method is + not implemented yet (#17). +- [unicodeadata] Added ``ot_tag_to_script`` function that converts from + OpenType script tag to Unicode script code. +- Added new ``designspaceLib`` subpackage, originally from Erik Van Blokland's + ``designSpaceDocument``: https://github.com/LettError/designSpaceDocument + NOTE: this is not yet used internally by varLib, and the API may be subject + to changes (#911, #1110, LettError/designSpaceDocument#28). +- Added new FontTools icon images (8ee7c32). +- [unicodedata] Added ``script_horizontal_direction`` function that returns + either "LTR" or "RTL" given a unicode script code. +- [otConverters] Don't write descriptive name string as XML comment if the + NameID value is 0 (== NULL) (#1151, #1152). +- [unicodedata] Add ``ot_tags_from_script`` function to get the list of + OpenType script tags associated with unicode script code (#1150). +- [feaLib] Don't error when "enumerated" kern pairs conflict with preceding + single pairs; emit warning and chose the first value (#1147, #1148). +- [loggingTools] In ``CapturingLogHandler.assertRegex`` method, match the + fully formatted log message. +- [sbix] Fixed TypeError when concatenating str and bytes (#1154). +- [bezierTools] Implemented cusp support and removed ``approximate_fallback`` + arg in ``calcQuadraticArcLength``. Added ``calcCubicArcLength`` (#1142). + +3.21.2 (released 2018-01-08) +---------------------------- + +- [varLib] Fixed merging PairPos Format1/2 with missing subtables (#1125). + +3.21.1 (released 2018-01-03) +---------------------------- + +- [feaLib] Allow mixed single/multiple substitutions (#612) +- Added missing ``*.afm`` test assets to MAINFEST.in (#1137). +- Fixed dumping ``SVG`` tables containing color palettes (#1124). + +3.21.0 (released 2017-12-18) +---------------------------- + +- [cmap] when compiling format6 subtable, don't assume gid0 is always called + '.notdef' (1e42224). +- [ot] Allow decompiling fonts with bad Coverage format number (1aafae8). +- Change FontTools licence to MIT (#1127). +- [post] Prune extra names already in standard Mac set (df1e8c7). +- [subset] Delete empty SubrsIndex after subsetting (#994, #1118). +- [varLib] Don't share points in cvar by default, as it currently fails on + some browsers (#1113). +- [afmLib] Make poor old afmLib work on python3. + +3.20.1 (released 2017-11-22) +---------------------------- + +- [unicodedata] Fixed issue with ``script`` and ``script_extension`` functions + returning inconsistent short vs long names. They both return the short four- + letter script codes now. Added ``script_name`` and ``script_code`` functions + to look up the long human-readable script name from the script code, and + viceversa (#1109, #1111). + +3.20.0 (released 2017-11-21) +---------------------------- + +- [unicodedata] Addded new module ``fontTools.unicodedata`` which exports the + same interface as the built-in ``unicodedata`` module, with the addition of + a few functions that are missing from the latter, such as ``script``, + ``script_extension`` and ``block``. Added a ``MetaTools/buildUCD.py`` script + to download and parse data files from the Unicode Character Database and + generate python modules containing lists of ranges and property values. +- [feaLib] Added ``__str__`` method to all ``ast`` elements (delegates to the + ``asFea`` method). +- [feaLib] ``Parser`` constructor now accepts a ``glyphNames`` iterable + instead of ``glyphMap`` dict. The latter still works but with a pending + deprecation warning (#1104). +- [bezierTools] Added arc length calculation functions originally from + ``pens.perimeterPen`` module (#1101). +- [varLib] Started generating STAT table (8af4309). Right now it just reflects + the axes, and even that with certain limitations: + * AxisOrdering is set to the order axes are defined, + * Name-table entries are not shared with fvar. +- [py23] Added backports for ``redirect_stdout`` and ``redirect_stderr`` + context managers (#1097). +- [Graphite] Fixed some round-trip bugs (#1093). + +3.19.0 (released 2017-11-06) +---------------------------- + +- [varLib] Try set of used points instead of all points when testing whether to + share points between tuples (#1090). +- [CFF2] Fixed issue with reading/writing PrivateDict BlueValues to TTX file. + Read the commit message 8b02b5a and issue #1030 for more details. + NOTE: this change invalidates all the TTX files containing CFF2 tables + that where dumped with previous verisons of fonttools. + CFF2 Subr items can have values on the stack after the last operator, thus + a ``CFF2Subr`` class was added to accommodate this (#1091). +- [_k_e_r_n] Fixed compilation of AAT kern version=1.0 tables (#1089, #1094) +- [ttLib] Added getBestCmap() convenience method to TTFont class and cmap table + class that returns a preferred Unicode cmap subtable given a list of options + (#1092). +- [morx] Emit more meaningful subtable flags. Implement InsertionMorphAction + +3.18.0 (released 2017-10-30) +---------------------------- + +- [feaLib] Fixed writing back nested glyph classes (#1086). +- [TupleVariation] Reactivated shared points logic, bugfixes (#1009). +- [AAT] Implemented ``morx`` ligature subtables (#1082). +- [reverseContourPen] Keep duplicate lineTo following a moveTo (#1080, + https://github.com/googlei18n/cu2qu/issues/51). +- [varLib.mutator] Suport instantiation of GPOS, GDEF and MVAR (#1079). +- [sstruct] Fixed issue with ``unicode_literals`` and ``struct`` module in + old versions of python 2.7 (#993). + +3.17.0 (released 2017-10-16) +---------------------------- + +- [svgPathPen] Added an ``SVGPathPen`` that translates segment pen commands + into SVG path descriptions. Copied from Tal Leming's ``ufo2svg.svgPathPen`` + https://github.com/typesupply/ufo2svg/blob/d69f992/Lib/ufo2svg/svgPathPen.py +- [reverseContourPen] Added ``ReverseContourPen``, a filter pen that draws + contours with the winding direction reversed, while keeping the starting + point (#1071). +- [filterPen] Added ``ContourFilterPen`` to manipulate contours as a whole + rather than segment by segment. +- [arrayTools] Added ``Vector`` class to apply math operations on an array + of numbers, and ``pairwise`` function to loop over pairs of items in an + iterable. +- [varLib] Added support for building and interpolation of ``cvar`` table + (f874cf6, a25a401). + +3.16.0 (released 2017-10-03) +---------------------------- + +- [head] Try using ``SOURCE_DATE_EPOCH`` environment variable when setting + the ``head`` modified timestamp to ensure reproducible builds (#1063). + See https://reproducible-builds.org/specs/source-date-epoch/ +- [VTT] Decode VTT's ``TSI*`` tables text as UTF-8 (#1060). +- Added support for Graphite font tables: Feat, Glat, Gloc, Silf and Sill. + Thanks @mhosken! (#1054). +- [varLib] Default to using axis "name" attribute if "labelname" element + is missing (588f524). +- [merge] Added support for merging Script records. Remove unused features + and lookups after merge (d802580, 556508b). +- Added ``fontTools.svgLib`` package. Includes a parser for SVG Paths that + supports the Pen protocol (#1051). Also, added a snippet to convert SVG + outlines to UFO GLIF (#1053). +- [AAT] Added support for ``ankr``, ``bsln``, ``mort``, ``morx``, ``gcid``, + and ``cidg``. +- [subset] Implemented subsetting of ``prop``, ``opbd``, ``bsln``, ``lcar``. + +3.15.1 (released 2017-08-18) +---------------------------- + +- [otConverters] Implemented ``__add__`` and ``__radd__`` methods on + ``otConverters._LazyList`` that decompile a lazy list before adding + it to another list or ``_LazyList`` instance. Fixes an ``AttributeError`` + in the ``subset`` module when attempting to sum ``_LazyList`` objects + (6ef48bd2, 1aef1683). +- [AAT] Support the `opbd` table with optical bounds (a47f6588). +- [AAT] Support `prop` table with glyph properties (d05617b4). + + +3.15.0 (released 2017-08-17) +---------------------------- + +- [AAT] Added support for AAT lookups. The ``lcar`` table can be decompiled + and recompiled; futher work needed to handle ``morx`` table (#1025). +- [subset] Keep (empty) DefaultLangSys for Script 'DFLT' (6eb807b5). +- [subset] Support GSUB/GPOS.FeatureVariations (fe01d87b). +- [varLib] In ``models.supportScalars``, ignore an axis when its peak value + is 0 (fixes #1020). +- [varLib] Add default mappings to all axes in avar to fix rendering issue + in some rasterizers (19c4b377, 04eacf13). +- [varLib] Flatten multiple tail PairPosFormat2 subtables before merging + (c55ef525). +- [ttLib] Added support for recalculating font bounding box in ``CFF`` and + ``head`` tables, and min/max values in ``hhea`` and ``vhea`` tables (#970). + +3.14.0 (released 2017-07-31) +---------------------------- + +- [varLib.merger] Remove Extensions subtables before merging (f7c20cf8). +- [varLib] Initialize the avar segment map with required default entries + (#1014). +- [varLib] Implemented optimal IUP optmiziation (#1019). +- [otData] Add ``AxisValueFormat4`` for STAT table v1.2 from OT v1.8.2 + (#1015). +- [name] Fixed BCP46 language tag for Mac langID=9: 'si' -> 'sl'. +- [subset] Return value from ``_DehintingT2Decompiler.op_hintmask`` + (c0d672ba). +- [cffLib] Allow to get TopDict by index as well as by name (dca96c9c). +- [cffLib] Removed global ``isCFF2`` state; use one set of classes for + both CFF and CFF2, maintaining backward compatibility existing code (#1007). +- [cffLib] Deprecated maxstack operator, per OpenType spec update 1.8.1. +- [cffLib] Added missing default (-100) for UnderlinePosition (#983). +- [feaLib] Enable setting nameIDs greater than 255 (#1003). +- [varLib] Recalculate ValueFormat when merging SinglePos (#996). +- [varLib] Do not emit MVAR if there are no entries in the variation store + (#987). +- [ttx] For ``-x`` option, pad with space if table tag length is < 4. + +3.13.1 (released 2017-05-30) +---------------------------- + +- [feaLib.builder] Removed duplicate lookups optimization. The original + lookup order and semantics of the feature file are preserved (#976). + +3.13.0 (released 2017-05-24) +---------------------------- + +- [varLib.mutator] Implement IUP optimization (#969). +- [_g_l_y_f.GlyphCoordinates] Changed ``__bool__()`` semantics to match those + of other iterables (e46f949). Removed ``__abs__()`` (3db5be2). +- [varLib.interpolate_layout] Added ``mapped`` keyword argument to + ``interpolate_layout`` to allow disabling avar mapping: if False (default), + the location is mapped using the map element of the axes in designspace file; + if True, it is assumed that location is in designspace's internal space and + no mapping is performed (#950, #975). +- [varLib.interpolate_layout] Import designspace-loading logic from varLib. +- [varLib] Fixed bug with recombining PairPosClass2 subtables (81498e5, #914). +- [cffLib.specializer] When copying iterables, cast to list (462b7f86). + +3.12.1 (released 2017-05-18) +---------------------------- + +- [pens.t2CharStringPen] Fixed AttributeError when calling addComponent in + T2CharStringPen (#965). + +3.12.0 (released 2017-05-17) +---------------------------- + +- [cffLib.specializer] Added new ``specializer`` module to optimize CFF + charstrings, used by the T2CharStringPen (#948). +- [varLib.mutator] Sort glyphs by component depth before calculating composite + glyphs' bounding boxes to ensure deltas are correctly caclulated (#945). +- [_g_l_y_f] Fixed loss of precision in GlyphCoordinates by using 'd' (double) + instead of 'f' (float) as ``array.array`` typecode (#963, #964). + +3.11.0 (released 2017-05-03) +---------------------------- + +- [t2CharStringPen] Initial support for specialized Type2 path operators: + vmoveto, hmoveto, vlineto, hlineto, vvcurveto, hhcurveto, vhcurveto and + hvcurveto. This should produce more compact charstrings (#940, #403). +- [Doc] Added Sphinx sources for the documentation. Thanks @gferreira (#935). +- [fvar] Expose flags in XML (#932) +- [name] Add helper function for building multi-lingual names (#921) +- [varLib] Fixed kern merging when a PairPosFormat2 has ClassDef1 with glyphs + that are NOT present in the Coverage (1b5e1c4, #939). +- [varLib] Fixed non-deterministic ClassDef order with PY3 (f056c12, #927). +- [feLib] Throw an error when the same glyph is defined in multiple mark + classes within the same lookup (3e3ff00, #453). + +3.10.0 (released 2017-04-14) +---------------------------- + +- [varLib] Added support for building ``avar`` table, using the designspace + ``<map>`` elements. +- [varLib] Removed unused ``build(..., axisMap)`` argument. Axis map should + be specified in designspace file now. We do not accept nonstandard axes + if ``<axes>`` element is not present. +- [varLib] Removed "custom" axis from the ``standard_axis_map``. This was + added before when glyphsLib was always exporting the (unused) custom axis. +- [varLib] Added partial support for building ``MVAR`` table; does not + implement ``gasp`` table variations yet. +- [pens] Added FilterPen base class, for pens that control another pen; + factored out ``addComponent`` method from BasePen into a separate abstract + DecomposingPen class; added DecomposingRecordingPen, which records + components decomposed as regular contours. +- [TSI1] Fixed computation of the textLength of VTT private tables (#913). +- [loggingTools] Added ``LogMixin`` class providing a ``log`` property to + subclasses, which returns a ``logging.Logger`` named after the latter. +- [loggingTools] Added ``assertRegex`` method to ``CapturingLogHandler``. +- [py23] Added backport for python 3's ``types.SimpleNamespace`` class. +- [EBLC] Fixed issue with python 3 ``zip`` iterator. + +3.9.2 (released 2017-04-08) +--------------------------- + +- [pens] Added pen to draw glyphs using WxPython ``GraphicsPath`` class: + https://wxpython.org/docs/api/wx.GraphicsPath-class.html +- [varLib.merger] Fixed issue with recombining multiple PairPosFormat2 + subtables (#888) +- [varLib] Do not encode gvar deltas that are all zeroes, or if all values + are smaller than tolerance. +- [ttLib] _TTGlyphSet glyphs now also have ``height`` and ``tsb`` (top + side bearing) attributes from the ``vmtx`` table, if present. +- [glyf] In ``GlyphCoordintes`` class, added ``__bool__`` / ``__nonzero__`` + methods, and ``array`` property to get raw array. +- [ttx] Support reading TTX files with BOM (#896) +- [CFF2] Fixed the reporting of the number of regions in the font. + +3.9.1 (released 2017-03-20) +--------------------------- + +- [varLib.merger] Fixed issue while recombining multiple PairPosFormat2 + subtables if they were split because of offset overflows (9798c30). +- [varLib.merger] Only merge multiple PairPosFormat1 subtables if there is + at least one of the fonts with a non-empty Format1 subtable (0f5a46b). +- [varLib.merger] Fixed IndexError with empty ClassDef1 in PairPosFormat2 + (aad0d46). +- [varLib.merger] Avoid reusing Class2Record (mutable) objects (e6125b3). +- [varLib.merger] Calculate ClassDef1 and ClassDef2's Format when merging + PairPosFormat2 (23511fd). +- [macUtils] Added missing ttLib import (b05f203). + +3.9.0 (released 2017-03-13) +--------------------------- + +- [feaLib] Added (partial) support for parsing feature file comments ``# ...`` + appearing in between statements (#879). +- [feaLib] Cleaned up syntax tree for FeatureNames. +- [ttLib] Added support for reading/writing ``CFF2`` table (thanks to + @readroberts at Adobe), and ``TTFA`` (ttfautohint) table. +- [varLib] Fixed regression introduced with 3.8.0 in the calculation of + ``NumShorts``, i.e. the number of deltas in ItemVariationData's delta sets + that use a 16-bit representation (b2825ff). + +3.8.0 (released 2017-03-05) +--------------------------- + +- New pens: MomentsPen, StatisticsPen, RecordingPen, and TeePen. +- [misc] Added new ``fontTools.misc.symfont`` module, for symbolic font + statistical analysis; requires ``sympy`` (http://www.sympy.org/en/index.html) +- [varLib] Added experimental ``fontTools.varLib.interpolatable`` module for + finding wrong contour order between different masters +- [varLib] designspace.load() now returns a dictionary, instead of a tuple, + and supports <axes> element (#864); the 'masters' item was renamed 'sources', + like the <sources> element in the designspace document +- [ttLib] Fixed issue with recalculating ``head`` modified timestamp when + saving CFF fonts +- [ttLib] In TupleVariation, round deltas before compiling (#861, fixed #592) +- [feaLib] Ignore duplicate glyphs in classes used as MarkFilteringSet and + MarkAttachmentType (#863) +- [merge] Changed the ``gasp`` table merge logic so that only the one from + the first font is retained, similar to other hinting tables (#862) +- [Tests] Added tests for the ``varLib`` package, as well as test fonts + from the "Annotated OpenType Specification" (AOTS) to exercise ``ttLib``'s + table readers/writers (<https://github.com/adobe-type-tools/aots>) + +3.7.2 (released 2017-02-17) +--------------------------- + +- [subset] Keep advance widths when stripping ".notdef" glyph outline in + CID-keyed CFF fonts (#845) +- [feaLib] Zero values now produce the same results as makeotf (#633, #848) +- [feaLib] More compact encoding for “Contextual positioning with in-line + single positioning rules” (#514) + +3.7.1 (released 2017-02-15) +--------------------------- + +- [subset] Fixed issue with ``--no-hinting`` option whereby advance widths in + Type 2 charstrings were also being stripped (#709, #343) +- [feaLib] include statements now resolve relative paths like makeotf (#838) +- [feaLib] table ``name`` now handles Unicode codepoints beyond the Basic + Multilingual Plane, also supports old-style MacOS platform encodings (#842) +- [feaLib] correctly escape string literals when emitting feature syntax (#780) + +3.7.0 (released 2017-02-11) +--------------------------- + +- [ttx, mtiLib] Preserve ordering of glyph alternates in GSUB type 3 (#833). +- [feaLib] Glyph names can have dashes, as per new AFDKO syntax v1.20 (#559). +- [feaLib] feaLib.Parser now needs the font's glyph map for parsing. +- [varLib] Fix regression where GPOS values were stored as 0. +- [varLib] Allow merging of class-based kerning when ClassDefs are different + +3.6.3 (released 2017-02-06) +--------------------------- + +- [varLib] Fix building variation of PairPosFormat2 (b5c34ce). +- Populate defaults even for otTables that have postRead (e45297b). +- Fix compiling of MultipleSubstFormat1 with zero 'out' glyphs (b887860). + +3.6.2 (released 2017-01-30) +--------------------------- + +- [varLib.merger] Fixed "TypeError: reduce() of empty sequence with no + initial value" (3717dc6). + +3.6.1 (released 2017-01-28) +--------------------------- + +- [py23] Fixed unhandled exception occurring at interpreter shutdown in + the "last resort" logging handler (972b3e6). +- [agl] Ensure all glyph names are of native 'str' type; avoid mixing + 'str' and 'unicode' in TTFont.glyphOrder (d8c4058). +- Fixed inconsistent title levels in README.rst that caused PyPI to + incorrectly render the reStructuredText page. + +3.6.0 (released 2017-01-26) +--------------------------- + +- [varLib] Refactored and improved the variation-font-building process. +- Assembly code in the fpgm, prep, and glyf tables is now indented in + XML output for improved readability. The ``instruction`` element is + written as a simple tag if empty (#819). +- [ttx] Fixed 'I/O operation on closed file' error when dumping + multiple TTXs to standard output with the '-o -' option. +- The unit test modules (``*_test.py``) have been moved outside of the + fontTools package to the Tests folder, thus they are no longer + installed (#811). + +3.5.0 (released 2017-01-14) +--------------------------- + +- Font tables read from XML can now be written back to XML with no + loss. +- GSUB/GPOS LookupType is written out in XML as an element, not + comment. (#792) +- When parsing cmap table, do not store items mapped to glyph id 0. + (#790) +- [otlLib] Make ClassDef sorting deterministic. Fixes #766 (7d1ddb2) +- [mtiLib] Added unit tests (#787) +- [cvar] Implemented cvar table +- [gvar] Renamed GlyphVariation to TupleVariation to match OpenType + terminology. +- [otTables] Handle gracefully empty VarData.Item array when compiling + XML. (#797) +- [varLib] Re-enabled generation of ``HVAR`` table for fonts with + TrueType outlines; removed ``--build-HVAR`` command-line option. +- [feaLib] The parser can now be extended to support non-standard + statements in FEA code by using a customized Abstract Syntax Tree. + See, for example, ``feaLib.builder_test.test_extensions`` and + baseClass.feax (#794, fixes #773). +- [feaLib] Added ``feaLib`` command to the 'fonttools' command-line + tool; applies a feature file to a font. ``fonttools feaLib -h`` for + help. +- [pens] The ``T2CharStringPen`` now takes an optional + ``roundTolerance`` argument to control the rounding of coordinates + (#804, fixes #769). +- [ci] Measure test coverage on all supported python versions and OSes, + combine coverage data and upload to + https://codecov.io/gh/fonttools/fonttools (#786) +- [ci] Configured Travis and Appveyor for running tests on Python 3.6 + (#785, 55c03bc) +- The manual pages installation directory can be customized through + ``FONTTOOLS_MANPATH`` environment variable (#799, fixes #84). +- [Snippets] Added otf2ttf.py, for converting fonts from CFF to + TrueType using the googlei18n/cu2qu module (#802) + +3.4.0 (released 2016-12-21) +--------------------------- + +- [feaLib] Added support for generating FEA text from abstract syntax + tree (AST) objects (#776). Thanks @mhosken +- Added ``agl.toUnicode`` function to convert AGL-compliant glyph names + to Unicode strings (#774) +- Implemented MVAR table (b4d5381) + +3.3.1 (released 2016-12-15) +--------------------------- + +- [setup] We no longer use versioneer.py to compute fonttools version + from git metadata, as this has caused issues for some users (#767). + Now we bump the version strings manually with a custom ``release`` + command of setup.py script. + +3.3.0 (released 2016-12-06) +--------------------------- + +- [ttLib] Implemented STAT table from OpenType 1.8 (#758) +- [cffLib] Fixed decompilation of CFF fonts containing non-standard + key/value pairs in FontDict (issue #740; PR #744) +- [py23] minor: in ``round3`` function, allow the second argument to be + ``None`` (#757) +- The standalone ``sstruct`` and ``xmlWriter`` modules, deprecated + since vesion 3.2.0, have been removed. They can be imported from the + ``fontTools.misc`` package. + +3.2.3 (released 2016-12-02) +--------------------------- + +- [py23] optimized performance of round3 function; added backport for + py35 math.isclose() (9d8dacb) +- [subset] fixed issue with 'narrow' (UCS-2) Python 2 builds and + ``--text``/``--text-file`` options containing non-BMP chararcters + (16d0e5e) +- [varLib] fixed issuewhen normalizing location values (8fa2ee1, #749) +- [inspect] Made it compatible with both python2 and python3 (167ee60, + #748). Thanks @pnemade + +3.2.2 (released 2016-11-24) +--------------------------- + +- [varLib] Do not emit null axes in fvar (1bebcec). Thanks @robmck-ms +- [varLib] Handle fonts without GPOS (7915a45) +- [merge] Ignore LangSys if None (a11bc56) +- [subset] Fix subsetting MathVariants (78d3cbe) +- [OS/2] Fix "Private Use (plane 15)" range (08a0d55). Thanks @mashabow + +3.2.1 (released 2016-11-03) +--------------------------- + +- [OS/2] fix checking ``fsSelection`` bits matching ``head.macStyle`` + bits +- [varLib] added ``--build-HVAR`` option to generate ``HVAR`` table for + fonts with TrueType outlines. For ``CFF2``, it is enabled by default. + +3.2.0 (released 2016-11-02) +--------------------------- + +- [varLib] Improve support for OpenType 1.8 Variable Fonts: +- Implement GDEF's VariationStore +- Implement HVAR/VVAR tables +- Partial support for loading MutatorMath .designspace files with + varLib.designspace module +- Add varLib.models with Variation fonts interpolation models +- Implement GSUB/GPOS FeatureVariations +- Initial support for interpolating and merging OpenType Layout tables + (see ``varLib.interpolate_layout`` and ``varLib.merger`` modules) +- [API change] Change version to be an integer instead of a float in + XML output for GSUB, GPOS, GDEF, MATH, BASE, JSTF, HVAR, VVAR, feat, + hhea and vhea tables. Scripts that set the Version for those to 1.0 + or other float values also need fixing. A warning is emitted when + code or XML needs fix. +- several bug fixes to the cffLib module, contributed by Adobe's + @readroberts +- The XML output for CFF table now has a 'major' and 'minor' elements + for specifying whether it's version 1.0 or 2.0 (support for CFF2 is + coming soon) +- [setup.py] remove undocumented/deprecated ``extra_path`` Distutils + argument. This means that we no longer create a "FontTools" subfolder + in site-packages containing the actual fontTools package, as well as + the standalone xmlWriter and sstruct modules. The latter modules are + also deprecated, and scheduled for removal in upcoming releases. + Please change your import statements to point to from fontTools.misc + import xmlWriter and from fontTools.misc import sstruct. +- [scripts] Add a 'fonttools' command-line tool that simply runs + ``fontTools.*`` sub-modules: e.g. ``fonttools ttx``, + ``fonttools subset``, etc. +- [hmtx/vmts] Read advance width/heights as unsigned short (uint16); + automatically round float values to integers. +- [ttLib/xmlWriter] add 'newlinestr=None' keyword argument to + ``TTFont.saveXML`` for overriding os-specific line endings (passed on + to ``XMLWriter`` instances). +- [versioning] Use versioneer instead of ``setuptools_scm`` to + dynamically load version info from a git checkout at import time. +- [feaLib] Support backslash-prefixed glyph names. + +3.1.2 (released 2016-09-27) +--------------------------- + +- restore Makefile as an alternative way to build/check/install +- README.md: update instructions for installing package from source, + and for running test suite +- NEWS: Change log was out of sync with tagged release + +3.1.1 (released 2016-09-27) +--------------------------- + +- Fix ``ttLibVersion`` attribute in TTX files still showing '3.0' + instead of '3.1'. +- Use ``setuptools_scm`` to manage package versions. + +3.1.0 (released 2016-09-26) +--------------------------- + +- [feaLib] New library to parse and compile Adobe FDK OpenType Feature + files. +- [mtiLib] New library to parse and compile Monotype 'FontDame' + OpenType Layout Tables files. +- [voltLib] New library to parse Microsoft VOLT project files. +- [otlLib] New library to work with OpenType Layout tables. +- [varLib] New library to work with OpenType Font Variations. +- [pens] Add ttGlyphPen to draw to TrueType glyphs, and t2CharStringPen + to draw to Type 2 Charstrings (CFF); add areaPen and perimeterPen. +- [ttLib.tables] Implement 'meta' and 'trak' tables. +- [ttx] Add --flavor option for compiling to 'woff' or 'woff2'; add + ``--with-zopfli`` option to use Zopfli to compress WOFF 1.0 fonts. +- [subset] Support subsetting 'COLR'/'CPAL' and 'CBDT'/'CBLC' color + fonts tables, and 'gvar' table for variation fonts. +- [Snippets] Add ``symfont.py``, for symbolic font statistics analysis; + interpolatable.py, a preliminary script for detecting interpolation + errors; ``{merge,dump}_woff_metadata.py``. +- [classifyTools] Helpers to classify things into classes. +- [CI] Run tests on Windows, Linux and macOS using Appveyor and Travis + CI; check unit test coverage with Coverage.py/Coveralls; automatic + deployment to PyPI on tags. +- [loggingTools] Use Python built-in logging module to print messages. +- [py23] Make round() behave like Python 3 built-in round(); define + round2() and round3(). + +3.0 (released 2015-09-01) +------------------------- + +- Add Snippet scripts for cmap subtable format conversion, printing + GSUB/GPOS features, building a GX font from two masters +- TTX WOFF2 support and a ``-f`` option to overwrite output file(s) +- Support GX tables: ``avar``, ``gvar``, ``fvar``, ``meta`` +- Support ``feat`` and gzip-compressed SVG tables +- Upgrade Mac East Asian encodings to native implementation if + available +- Add Roman Croatian and Romanian encodings, codecs for mac-extended + East Asian encodings +- Implement optimal GLYF glyph outline packing; disabled by default + +2.5 (released 2014-09-24) +------------------------- + +- Add a Qt pen +- Add VDMX table converter +- Load all OpenType sub-structures lazily +- Add support for cmap format 13. +- Add pyftmerge tool +- Update to Unicode 6.3.0d3 +- Add pyftinspect tool +- Add support for Google CBLC/CBDT color bitmaps, standard EBLC/EBDT + embedded bitmaps, and ``SVG`` table (thanks to Read Roberts at Adobe) +- Add support for loading, saving and ttx'ing WOFF file format +- Add support for Microsoft COLR/CPAL layered color glyphs +- Support PyPy +- Support Jython, by replacing numpy with array/lists modules and + removed it, pure-Python StringIO, not cStringIO +- Add pyftsubset and Subsetter object, supporting CFF and TTF +- Add to ttx args for -q for quiet mode, -z to choose a bitmap dump + format + +2.4 (released 2013-06-22) +------------------------- + +- Option to write to arbitrary files +- Better dump format for DSIG +- Better detection of OTF XML +- Fix issue with Apple's kern table format +- Fix mangling of TT glyph programs +- Fix issues related to mona.ttf +- Fix Windows Installer instructions +- Fix some modern MacOS issues +- Fix minor issues and typos + +2.3 (released 2009-11-08) +------------------------- + +- TrueType Collection (TTC) support +- Python 2.6 support +- Update Unicode data to 5.2.0 +- Couple of bug fixes + +2.2 (released 2008-05-18) +------------------------- + +- ClearType support +- cmap format 1 support +- PFA font support +- Switched from Numeric to numpy +- Update Unicode data to 5.1.0 +- Update AGLFN data to 1.6 +- Many bug fixes + +2.1 (released 2008-01-28) +------------------------- + +- Many years worth of fixes and features + +2.0b2 (released 2002-??-??) +--------------------------- + +- Be "forgiving" when interpreting the maxp table version field: + interpret any value as 1.0 if it's not 0.5. Fixes dumping of these + GPL fonts: http://www.freebsd.org/cgi/pds.cgi?ports/chinese/wangttf +- Fixed ttx -l: it turned out this part of the code didn't work with + Python 2.2.1 and earlier. My bad to do most of my testing with a + different version than I shipped TTX with :-( +- Fixed bug in ClassDef format 1 subtable (Andreas Seidel bumped into + this one). + +2.0b1 (released 2002-09-10) +--------------------------- + +- Fixed embarrassing bug: the master checksum in the head table is now + calculated correctly even on little-endian platforms (such as Intel). +- Made the cmap format 4 compiler smarter: the binary data it creates + is now more or less as compact as possible. TTX now makes more + compact data than in any shipping font I've tested it with. +- Dump glyph names as a separate "GlyphOrder" pseudo table as opposed + to as part of the glyf table (obviously needed for CFF-OTF's). +- Added proper support for the CFF table. +- Don't barf on empty tables (questionable, but "there are font out + there...") +- When writing TT glyf data, align glyphs on 4-byte boundaries. This + seems to be the current recommendation by MS. Also: don't barf on + fonts which are already 4-byte aligned. +- Windows installer contributed bu Adam Twardoch! Yay! +- Changed the command line interface again, now by creating one new + tool replacing the old ones: ttx It dumps and compiles, depending on + input file types. The options have changed somewhat. +- The -d option is back (output dir) +- ttcompile's -i options is now called -m (as in "merge"), to avoid + clash with dump's -i. +- The -s option ("split tables") no longer creates a directory, but + instead outputs a small .ttx file containing references to the + individual table files. This is not a true link, it's a simple file + name, and the referenced file should be in the same directory so + ttcompile can find them. +- compile no longer accepts a directory as input argument. Instead it + can parse the new "mini-ttx" format as output by "ttx -s". +- all arguments are input files +- Renamed the command line programs and moved them to the Tools + subdirectory. They are now installed by the setup.py install script. +- Added OpenType support. BASE, GDEF, GPOS, GSUB and JSTF are (almost) + fully supported. The XML output is not yet final, as I'm still + considering to output certain subtables in a more human-friendly + manner. +- Fixed 'kern' table to correctly accept subtables it doesn't know + about, as well as interpreting Apple's definition of the 'kern' table + headers correctly. +- Fixed bug where glyphnames were not calculated from 'cmap' if it was + (one of the) first tables to be decompiled. More specifically: it + cmap was the first to ask for a glyphID -> glyphName mapping. +- Switched XML parsers: use expat instead of xmlproc. Should be faster. +- Removed my UnicodeString object: I now require Python 2.0 or up, + which has unicode support built in. +- Removed assert in glyf table: redundant data at the end of the table + is now ignored instead of raising an error. Should become a warning. +- Fixed bug in hmtx/vmtx code that only occured if all advances were + equal. +- Fixed subtle bug in TT instruction disassembler. +- Couple of fixes to the 'post' table. +- Updated OS/2 table to latest spec. + +1.0b1 (released 2001-08-10) +--------------------------- + +- Reorganized the command line interface for ttDump.py and + ttCompile.py, they now behave more like "normal" command line tool, + in that they accept multiple input files for batch processing. +- ttDump.py and ttCompile.py don't silently override files anymore, but + ask before doing so. Can be overridden by -f. +- Added -d option to both ttDump.py and ttCompile.py. +- Installation is now done with distutils. (Needs work for environments + without compilers.) +- Updated installation instructions. +- Added some workarounds so as to handle certain buggy fonts more + gracefully. +- Updated Unicode table to Unicode 3.0 (Thanks Antoine!) +- Included a Python script by Adam Twardoch that adds some useful stuff + to the Windows registry. +- Moved the project to SourceForge. + +1.0a6 (released 2000-03-15) +--------------------------- + +- Big reorganization: made ttLib a subpackage of the new fontTools + package, changed several module names. Called the entire suite + "FontTools" +- Added several submodules to fontTools, some new, some older. +- Added experimental CFF/GPOS/GSUB support to ttLib, read-only (but XML + dumping of GPOS/GSUB is for now disabled) +- Fixed hdmx endian bug +- Added -b option to ttCompile.py, it disables recalculation of + bounding boxes, as requested by Werner Lemberg. +- Renamed tt2xml.pt to ttDump.py and xml2tt.py to ttCompile.py +- Use ".ttx" as file extension instead of ".xml". +- TTX is now the name of the XML-based *format* for TT fonts, and not + just an application. + +1.0a5 +----- + +Never released + +- More tables supported: hdmx, vhea, vmtx + +1.0a3 & 1.0a4 +------------- + +Never released + +- fixed most portability issues +- retracted the "Euro_or_currency" change from 1.0a2: it was + nonsense! + +1.0a2 (released 1999-05-02) +--------------------------- + +- binary release for MacOS +- genenates full FOND resources: including width table, PS font name + info and kern table if applicable. +- added cmap format 4 support. Extra: dumps Unicode char names as XML + comments! +- added cmap format 6 support +- now accepts true type files starting with "true" (instead of just + 0x00010000 and "OTTO") +- 'glyf' table support is now complete: I added support for composite + scale, xy-scale and two-by-two for the 'glyf' table. For now, + component offset scale behaviour defaults to Apple-style. This only + affects the (re)calculation of the glyph bounding box. +- changed "Euro" to "Euro_or_currency" in the Standard Apple Glyph + order list, since we cannot tell from the 'post' table which is + meant. I should probably doublecheck with a Unicode encoding if + available. (This does not affect the output!) + +Fixed bugs: - 'hhea' table is now recalculated correctly - fixed wrong +assumption about sfnt resource names + +1.0a1 (released 1999-04-27) +--------------------------- + +- initial binary release for MacOS diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..c92fa09 --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,1375 @@ +Metadata-Version: 1.2 +Name: fonttools +Version: 3.28.0 +Summary: Tools to manipulate font files +Home-page: http://github.com/fonttools/fonttools +Author: Just van Rossum +Author-email: just@letterror.com +Maintainer: Behdad Esfahbod +Maintainer-email: behdad@behdad.org +License: MIT +Description: |Travis Build Status| |Appveyor Build status| |Health| |Coverage Status| + |PyPI| |Gitter Chat| + + What is this? + ~~~~~~~~~~~~~ + + | fontTools is a library for manipulating fonts, written in Python. The + project includes the TTX tool, that can convert TrueType and OpenType + fonts to and from an XML text format, which is also called TTX. It + supports TrueType, OpenType, AFM and to an extent Type 1 and some + Mac-specific formats. The project has a `MIT open-source + licence <LICENSE>`__. + | Among other things this means you can use it free of charge. + + Installation + ~~~~~~~~~~~~ + + FontTools requires `Python <http://www.python.org/download/>`__ 2.7, 3.4 + or later. + + The package is listed in the Python Package Index (PyPI), so you can + install it with `pip <https://pip.pypa.io>`__: + + .. code:: sh + + pip install fonttools + + If you would like to contribute to its development, you can clone the + repository from Github, install the package in 'editable' mode and + modify the source code in place. We recommend creating a virtual + environment, using `virtualenv <https://virtualenv.pypa.io>`__ or + Python 3 `venv <https://docs.python.org/3/library/venv.html>`__ module. + + .. code:: sh + + # download the source code to 'fonttools' folder + git clone https://github.com/fonttools/fonttools.git + cd fonttools + + # create new virtual environment called e.g. 'fonttools-venv', or anything you like + python -m virtualenv fonttools-venv + + # source the `activate` shell script to enter the environment (Un\*x); to exit, just type `deactivate` + . fonttools-venv/bin/activate + + # to activate the virtual environment in Windows `cmd.exe`, do + fonttools-venv\Scripts\activate.bat + + # install in 'editable' mode + pip install -e . + + TTX – From OpenType and TrueType to XML and Back + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Once installed you can use the ``ttx`` command to convert binary font + files (``.otf``, ``.ttf``, etc) to the TTX xml format, edit them, and + convert them back to binary format. TTX files have a .ttx file + extension. + + .. code:: sh + + ttx /path/to/font.otf + ttx /path/to/font.ttx + + The TTX application works can be used in two ways, depending on what + platform you run it on: + + - As a command line tool (Windows/DOS, Unix, MacOSX) + - By dropping files onto the application (Windows, MacOS) + + TTX detects what kind of files it is fed: it will output a ``.ttx`` file + when it sees a ``.ttf`` or ``.otf``, and it will compile a ``.ttf`` or + ``.otf`` when the input file is a ``.ttx`` file. By default, the output + file is created in the same folder as the input file, and will have the + same name as the input file but with a different extension. TTX will + *never* overwrite existing files, but if necessary will append a unique + number to the output filename (before the extension) such as + ``Arial#1.ttf`` + + When using TTX from the command line there are a bunch of extra options, + these are explained in the help text, as displayed when typing + ``ttx -h`` at the command prompt. These additional options include: + + - specifying the folder where the output files are created + - specifying which tables to dump or which tables to exclude + - merging partial ``.ttx`` files with existing ``.ttf`` or ``.otf`` + files + - listing brief table info instead of dumping to ``.ttx`` + - splitting tables to separate ``.ttx`` files + - disabling TrueType instruction disassembly + + The TTX file format + ------------------- + + The following tables are currently supported: + + .. begin table list + .. code:: + + BASE, CBDT, CBLC, CFF, CFF2, COLR, CPAL, DSIG, EBDT, EBLC, FFTM, + Feat, GDEF, GMAP, GPKG, GPOS, GSUB, Glat, Gloc, HVAR, JSTF, LTSH, + MATH, META, MVAR, OS/2, SING, STAT, SVG, Silf, Sill, TSI0, TSI1, + TSI2, TSI3, TSI5, TSIB, TSID, TSIJ, TSIP, TSIS, TSIV, TTFA, VDMX, + VORG, VVAR, ankr, avar, bsln, cidg, cmap, cvar, cvt, feat, fpgm, + fvar, gasp, gcid, glyf, gvar, hdmx, head, hhea, hmtx, kern, lcar, + loca, ltag, maxp, meta, mort, morx, name, opbd, post, prep, prop, + sbix, trak, vhea and vmtx + .. end table list + + Other tables are dumped as hexadecimal data. + + TrueType fonts use glyph indices (GlyphIDs) to refer to glyphs in most + places. While this is fine in binary form, it is really hard to work + with for humans. Therefore we use names instead. + + The glyph names are either extracted from the ``CFF`` table or the + ``post`` table, or are derived from a Unicode ``cmap`` table. In the + latter case the Adobe Glyph List is used to calculate names based on + Unicode values. If all of these methods fail, names are invented based + on GlyphID (eg ``glyph00142``) + + It is possible that different glyphs use the same name. If this happens, + we force the names to be unique by appending ``#n`` to the name (``n`` + being an integer number.) The original names are being kept, so this has + no influence on a "round tripped" font. + + Because the order in which glyphs are stored inside the binary font is + important, we maintain an ordered list of glyph names in the font. + + Other Tools + ~~~~~~~~~~~ + + Commands for inspecting, merging and subsetting fonts are also + available: + + .. code:: sh + + pyftinspect + pyftmerge + pyftsubset + + fontTools Python Module + ~~~~~~~~~~~~~~~~~~~~~~~ + + The fontTools python module provides a convenient way to + programmatically edit font files. + + .. code:: py + + >>> from fontTools.ttLib import TTFont + >>> font = TTFont('/path/to/font.ttf') + >>> font + <fontTools.ttLib.TTFont object at 0x10c34ed50> + >>> + + A selection of sample python programs is in the + `Snippets <https://github.com/fonttools/fonttools/blob/master/Snippets/>`__ + directory. + + Optional Requirements + --------------------- + + The ``fontTools`` package currently has no (required) external dependencies + besides the modules included in the Python Standard Library. + However, a few extra dependencies are required by some of its modules, which + are needed to unlock optional features. + + - ``Lib/fontTools/ttLib/woff2.py`` + + Module to compress/decompress WOFF 2.0 web fonts; it requires: + + - `brotli <https://pypi.python.org/pypi/Brotli>`__: Python bindings of + the Brotli compression library. + + - ``Lib/fontTools/ttLib/sfnt.py`` + + To better compress WOFF 1.0 web fonts, the following module can be used + instead of the built-in ``zlib`` library: + + - `zopfli <https://pypi.python.org/pypi/zopfli>`__: Python bindings of + the Zopfli compression library. + + - ``Lib/fontTools/unicode.py`` + + To display the Unicode character names when dumping the ``cmap`` table + with ``ttx`` we use the ``unicodedata`` module in the Standard Library. + The version included in there varies between different Python versions. + To use the latest available data, you can install: + + - `unicodedata2 <https://pypi.python.org/pypi/unicodedata2>`__: + ``unicodedata`` backport for Python 2.7 and 3.5 updated to the latest + Unicode version 9.0. Note this is not necessary if you use Python 3.6 + as the latter already comes with an up-to-date ``unicodedata``. + + - ``Lib/fontTools/varLib/interpolatable.py`` + + Module for finding wrong contour/component order between different masters. + It requires one of the following packages in order to solve the so-called + "minimum weight perfect matching problem in bipartite graphs", or + the Assignment problem: + + * `scipy <https://pypi.python.org/pypi/scipy>`__: the Scientific Library + for Python, which internally uses `NumPy <https://pypi.python.org/pypi/numpy>`__ + arrays and hence is very fast; + * `munkres <https://pypi.python.org/pypi/munkres>`__: a pure-Python + module that implements the Hungarian or Kuhn-Munkres algorithm. + + - ``Lib/fontTools/misc/symfont.py`` + + Advanced module for symbolic font statistics analysis; it requires: + + * `sympy <https://pypi.python.org/pypi/sympy>`__: the Python library for + symbolic mathematics. + + - ``Lib/fontTools/t1Lib.py`` + + To get the file creator and type of Macintosh PostScript Type 1 fonts + on Python 3 you need to install the following module, as the old ``MacOS`` + module is no longer included in Mac Python: + + * `xattr <https://pypi.python.org/pypi/xattr>`__: Python wrapper for + extended filesystem attributes (macOS platform only). + + - ``Lib/fontTools/pens/cocoaPen.py`` + + Pen for drawing glyphs with Cocoa ``NSBezierPath``, requires: + + * `PyObjC <https://pypi.python.org/pypi/pyobjc>`__: the bridge between + Python and the Objective-C runtime (macOS platform only). + + - ``Lib/fontTools/pens/qtPen.py`` + + Pen for drawing glyphs with Qt's ``QPainterPath``, requires: + + * `PyQt5 <https://pypi.python.org/pypi/PyQt5>`__: Python bindings for + the Qt cross platform UI and application toolkit. + + - ``Lib/fontTools/pens/reportLabPen.py`` + + Pen to drawing glyphs as PNG images, requires: + + * `reportlab <https://pypi.python.org/pypi/reportlab>`__: Python toolkit + for generating PDFs and graphics. + + - ``Lib/fontTools/inspect.py`` + + A GUI font inspector, requires one of the following packages: + + * `PyGTK <https://pypi.python.org/pypi/PyGTK>`__: Python bindings for + GTK  2.x (only works with Python 2). + * `PyGObject <https://wiki.gnome.org/action/show/Projects/PyGObject>`__ : + Python bindings for GTK 3.x and gobject-introspection libraries (also + compatible with Python 3). + + Testing + ~~~~~~~ + + To run the test suite, you can do: + + .. code:: sh + + python setup.py test + + If you have `pytest <http://docs.pytest.org/en/latest/>`__, you can run + the ``pytest`` command directly. The tests will run against the + installed ``fontTools`` package, or the first one found in the + ``PYTHONPATH``. + + You can also use `tox <https://testrun.org/tox/latest/>`__ to + automatically run tests on different Python versions in isolated virtual + environments. + + .. code:: sh + + pip install tox + tox + + Note that when you run ``tox`` without arguments, the tests are executed + for all the environments listed in tox.ini's ``envlist``. In our case, + this includes Python 2.7 and 3.6, so for this to work the ``python2.7`` + and ``python3.6`` executables must be available in your ``PATH``. + + You can specify an alternative environment list via the ``-e`` option, + or the ``TOXENV`` environment variable: + + .. code:: sh + + tox -e py27-nocov + TOXENV="py36-cov,htmlcov" tox + + Development Community + ~~~~~~~~~~~~~~~~~~~~~ + + TTX/FontTools development is ongoing in an active community of + developers, that includes professional developers employed at major + software corporations and type foundries as well as hobbyists. + + Feature requests and bug reports are always welcome at + https://github.com/fonttools/fonttools/issues/ + + The best place for discussions about TTX from an end-user perspective as + well as TTX/FontTools development is the + https://groups.google.com/d/forum/fonttools mailing list. There is also + a development https://groups.google.com/d/forum/fonttools-dev mailing + list for continuous integration notifications. You can also email Behdad + privately at behdad@behdad.org + + History + ~~~~~~~ + + The fontTools project was started by Just van Rossum in 1999, and was + maintained as an open source project at + http://sourceforge.net/projects/fonttools/. In 2008, Paul Wise (pabs3) + began helping Just with stability maintenance. In 2013 Behdad Esfahbod + began a friendly fork, thoroughly reviewing the codebase and making + changes at https://github.com/behdad/fonttools to add new features and + support for new font formats. + + Acknowledgements + ~~~~~~~~~~~~~~~~ + + In alphabetical order: + + Olivier Berten, Samyak Bhuta, Erik van Blokland, Petr van Blokland, + Jelle Bosma, Sascha Brawer, Tom Byrer, Frédéric Coiffier, Vincent + Connare, Dave Crossland, Simon Daniels, Behdad Esfahbod, Behnam + Esfahbod, Hannes Famira, Sam Fishman, Matt Fontaine, Yannis Haralambous, + Greg Hitchcock, Jeremie Hornus, Khaled Hosny, John Hudson, Denis Moyogo + Jacquerye, Jack Jansen, Tom Kacvinsky, Jens Kutilek, Antoine Leca, + Werner Lemberg, Tal Leming, Peter Lofting, Cosimo Lupo, Masaya Nakamura, + Dave Opstad, Laurence Penney, Roozbeh Pournader, Garret Rieger, Read + Roberts, Guido van Rossum, Just van Rossum, Andreas Seidel, Georg + Seifert, Miguel Sousa, Adam Twardoch, Adrien Tétar, Vitaly Volkov, Paul + Wise. + + Copyrights + ~~~~~~~~~~ + + | Copyright (c) 1999-2004 Just van Rossum, LettError + (just@letterror.com) + | See `LICENSE <LICENSE>`__ for the full license. + + Copyright (c) 2000 BeOpen.com. All Rights Reserved. + + Copyright (c) 1995-2001 Corporation for National Research Initiatives. + All Rights Reserved. + + Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam. All + Rights Reserved. + + Have fun! + + .. |Travis Build Status| image:: https://travis-ci.org/fonttools/fonttools.svg + :target: https://travis-ci.org/fonttools/fonttools + .. |Appveyor Build status| image:: https://ci.appveyor.com/api/projects/status/0f7fmee9as744sl7/branch/master?svg=true + :target: https://ci.appveyor.com/project/fonttools/fonttools/branch/master + .. |Health| image:: https://landscape.io/github/behdad/fonttools/master/landscape.svg?style=flat + :target: https://landscape.io/github/behdad/fonttools/master + .. |Coverage Status| image:: https://codecov.io/gh/fonttools/fonttools/branch/master/graph/badge.svg + :target: https://codecov.io/gh/fonttools/fonttools + .. |PyPI| image:: https://img.shields.io/pypi/v/fonttools.svg + :target: https://pypi.org/project/FontTools + .. |Gitter Chat| image:: https://badges.gitter.im/fonttools-dev/Lobby.svg + :alt: Join the chat at https://gitter.im/fonttools-dev/Lobby + :target: https://gitter.im/fonttools-dev/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge + + Changelog + ~~~~~~~~~ + + 3.28.0 (released 2018-06-19) + ---------------------------- + + - [featureVars] Added experimental module to build ``FeatureVariations`` + tables. Still needs to be hooked up to ``varLib.build`` (#1240). + - [fixedTools] Added ``otRound`` to round floats to nearest integer towards + positive Infinity. This is now used where we deal with visual data like X/Y + coordinates, advance widths/heights, variation deltas, and similar (#1274, + #1248). + - [subset] Improved GSUB closure memoize algorithm. + - [varLib.models] Fixed regression in model resolution (180124, #1269). + - [feaLib.ast] Fixed error when converting ``SubtableStatement`` to string + (#1275). + - [varLib.mutator] Set ``OS/2.usWeightClass`` and ``usWidthClass``, and + ``post.italicAngle`` based on the 'wght', 'wdth' and 'slnt' axis values + (#1276, #1264). + - [py23/loggingTools] Don't automatically set ``logging.lastResort`` handler + on py27. Moved ``LastResortLogger`` to the ``loggingTools`` module (#1277). + + 3.27.1 (released 2018-06-11) + ---------------------------- + + - [ttGlyphPen] Issue a warning and skip building non-existing components + (https://github.com/googlei18n/fontmake/issues/411). + - [tests] Fixed issue running ttx_test.py from a tagged commit. + + 3.27.0 (released 2018-06-11) + ---------------------------- + + - [designspaceLib] Added new ``conditionSet`` element to ``rule`` element in + designspace document. Bumped ``format`` attribute to ``4.0`` (previously, + it was formatted as an integer). Removed ``checkDefault``, ``checkAxes`` + methods, and any kind of guessing about the axes when the ``<axes>`` element + is missing. The default master is expected at the intersection of all default + values for each axis (#1254, #1255, #1267). + - [cffLib] Fixed issues when compiling CFF2 or converting from CFF when the + font has an FDArray (#1211, #1271). + - [varLib] Avoid attempting to build ``cvar`` table when ``glyf`` table is not + present, as is the case for CFF2 fonts. + - [subset] Handle None coverages in MarkGlyphSets; revert commit 02616ab that + sets empty Coverage tables in MarkGlyphSets to None, to make OTS happy. + - [ttFont] Allow to build glyph order from ``maxp.numGlyphs`` when ``post`` or + ``cmap`` are missing. + - [ttFont] Added ``__len__`` method to ``_TTGlyphSet``. + - [glyf] Ensure ``GlyphCoordinates`` never overflow signed shorts (#1230). + - [py23] Added alias for ``itertools.izip`` shadowing the built-in ``zip``. + - [loggingTools] Memoize ``log`` property of ``LogMixin`` class (fbab12). + - [ttx] Impoved test coverage (#1261). + - [Snippets] Addded script to append a suffix to all family names in a font. + - [varLib.plot] Make it work with matplotlib >= 2.1 (b38e2b). + + 3.26.0 (released 2018-05-03) + ---------------------------- + + - [designspace] Added a new optional ``layer`` attribute to the source element, + and a corresponding ``layerName`` attribute to the ``SourceDescriptor`` + object (#1253). + Added ``conditionset`` element to the ``rule`` element to the spec, but not + implemented in designspace reader/writer yet (#1254). + - [varLib.models] Refine modeling one last time (0ecf5c5). + - [otBase] Fixed sharing of tables referred to by different offset sizes + (795f2f9). + - [subset] Don't drop a GDEF that only has VarStore (fc819d6). Set to None + empty Coverage tables in MarkGlyphSets (02616ab). + - [varLib]: Added ``--master-finder`` command-line option (#1249). + - [varLib.mutator] Prune fvar nameIDs from instance's name table (#1245). + - [otTables] Allow decompiling bad ClassDef tables with invalid format, with + warning (#1236). + - [varLib] Make STAT v1.2 and reuse nameIDs from fvar table (#1242). + - [varLib.plot] Show master locations. Set axis limits to -1, +1. + - [subset] Handle HVAR direct mapping. Passthrough 'cvar'. + Added ``--font-number`` command-line option for collections. + - [t1Lib] Allow a text encoding to be specified when parsing a Type 1 font + (#1234). Added ``kind`` argument to T1Font constructor (c5c161c). + - [ttLib] Added context manager API to ``TTFont`` class, so it can be used in + ``with`` statements to auto-close the file when exiting the context (#1232). + + 3.25.0 (released 2018-04-03) + ---------------------------- + + - [varLib] Improved support-resolution algorithm. Previously, the on-axis + masters would always cut the space. They don't anymore. That's more + consistent, and fixes the main issue Erik showed at TYPO Labs 2017. + Any varfont built that had an unusual master configuration will change + when rebuilt (42bef17, a523a697, + https://github.com/googlei18n/fontmake/issues/264). + - [varLib.models] Added a ``main()`` entry point, that takes positions and + prints model results. + - [varLib.plot] Added new module to plot a designspace's + VariationModel. Requires ``matplotlib``. + - [varLib.mutator] Added -o option to specify output file path (2ef60fa). + - [otTables] Fixed IndexError while pruning of HVAR pre-write (6b6c34a). + - [varLib.models] Convert delta array to floats if values overflows signed + short integer (0055f94). + + 3.24.2 (released 2018-03-26) + ---------------------------- + + - [otBase] Don't fail during ``ValueRecord`` copy if src has more items. + We drop hinting in the subsetter by simply changing ValueFormat, without + cleaning up the actual ValueRecords. This was causing assertion error if + a variable font was subsetted without hinting and then passed directly to + the mutator for instantiation without first it saving to disk. + + 3.24.1 (released 2018-03-06) + ---------------------------- + + - [varLib] Don't remap the same ``DeviceTable`` twice in VarStore optimizer + (#1206). + - [varLib] Add ``--disable-iup`` option to ``fonttools varLib`` script, + and a ``optimize=True`` keyword argument to ``varLib.build`` function, + to optionally disable IUP optimization while building varfonts. + - [ttCollection] Fixed issue while decompiling ttc with python3 (#1207). + + 3.24.0 (released 2018-03-01) + ---------------------------- + + - [ttGlyphPen] Decompose composite glyphs if any components' transform is too + large to fit a ``F2Dot14`` value, or clamp transform values that are + (almost) equal to +2.0 to make them fit and avoid decomposing (#1200, + #1204, #1205). + - [ttx] Added new ``-g`` option to dump glyphs from the ``glyf`` table + splitted as individual ttx files (#153, #1035, #1132, #1202). + - Copied ``ufoLib.filenames`` module to ``fontTools.misc.filenames``, used + for the ttx split-glyphs option (#1202). + - [feaLib] Added support for ``cvParameters`` blocks in Character Variant + feautures ``cv01-cv99`` (#860, #1169). + - [Snippets] Added ``checksum.py`` script to generate/check SHA1 hash of + ttx files (#1197). + - [varLib.mutator] Fixed issue while instantiating some variable fonts + whereby the horizontal advance width computed from ``gvar`` phantom points + could turn up to be negative (#1198). + - [varLib/subset] Fixed issue with subsetting GPOS variation data not + picking up ``ValueRecord`` ``Device`` objects (54fd71f). + - [feaLib/voltLib] In all AST elements, the ``location`` is no longer a + required positional argument, but an optional kewyord argument (defaults + to ``None``). This will make it easier to construct feature AST from + code (#1201). + + + 3.23.0 (released 2018-02-26) + ---------------------------- + + - [designspaceLib] Added an optional ``lib`` element to the designspace as a + whole, as well as to the instance elements, to store arbitrary data in a + property list dictionary, similar to the UFO's ``lib``. Added an optional + ``font`` attribute to the ``SourceDescriptor``, to allow operating on + in-memory font objects (#1175). + - [cffLib] Fixed issue with lazy-loading of attributes when attempting to + set the CFF TopDict.Encoding (#1177, #1187). + - [ttx] Fixed regression introduced in 3.22.0 that affected the split tables + ``-s`` option (#1188). + - [feaLib] Added ``IncludedFeaNotFound`` custom exception subclass, raised + when an included feature file cannot be found (#1186). + - [otTables] Changed ``VarIdxMap`` to use glyph names internally instead of + glyph indexes. The old ttx dumps of HVAR/VVAR tables that contain indexes + can still be imported (21cbab8, 38a0ffb). + - [varLib] Implemented VarStore optimizer (#1184). + - [subset] Implemented pruning of GDEF VarStore, HVAR and MVAR (#1179). + - [sfnt] Restore backward compatiblity with ``numFonts`` attribute of + ``SFNTReader`` object (#1181). + - [merge] Initial support for merging ``LangSysRecords`` (#1180). + - [ttCollection] don't seek(0) when writing to possibly unseekable strems. + - [subset] Keep all ``--name-IDs`` from 0 to 6 by default (#1170, #605, #114). + - [cffLib] Added ``width`` module to calculate optimal CFF default and + nominal glyph widths. + - [varLib] Don’t fail if STAT already in the master fonts (#1166). + + 3.22.0 (released 2018-02-04) + ---------------------------- + + - [subset] Support subsetting ``endchar`` acting as ``seac``-like components + in ``CFF`` (fixes #1162). + - [feaLib] Allow to build from pre-parsed ``ast.FeatureFile`` object. + Added ``tables`` argument to only build some tables instead of all (#1159, + #1163). + - [textTools] Replaced ``safeEval`` with ``ast.literal_eval`` (#1139). + - [feaLib] Added option to the parser to not resolve ``include`` statements + (#1154). + - [ttLib] Added new ``ttCollection`` module to read/write TrueType and + OpenType Collections. Exports a ``TTCollection`` class with a ``fonts`` + attribute containing a list of ``TTFont`` instances, the methods ``save`` + and ``saveXML``, plus some list-like methods. The ``importXML`` method is + not implemented yet (#17). + - [unicodeadata] Added ``ot_tag_to_script`` function that converts from + OpenType script tag to Unicode script code. + - Added new ``designspaceLib`` subpackage, originally from Erik Van Blokland's + ``designSpaceDocument``: https://github.com/LettError/designSpaceDocument + NOTE: this is not yet used internally by varLib, and the API may be subject + to changes (#911, #1110, LettError/designSpaceDocument#28). + - Added new FontTools icon images (8ee7c32). + - [unicodedata] Added ``script_horizontal_direction`` function that returns + either "LTR" or "RTL" given a unicode script code. + - [otConverters] Don't write descriptive name string as XML comment if the + NameID value is 0 (== NULL) (#1151, #1152). + - [unicodedata] Add ``ot_tags_from_script`` function to get the list of + OpenType script tags associated with unicode script code (#1150). + - [feaLib] Don't error when "enumerated" kern pairs conflict with preceding + single pairs; emit warning and chose the first value (#1147, #1148). + - [loggingTools] In ``CapturingLogHandler.assertRegex`` method, match the + fully formatted log message. + - [sbix] Fixed TypeError when concatenating str and bytes (#1154). + - [bezierTools] Implemented cusp support and removed ``approximate_fallback`` + arg in ``calcQuadraticArcLength``. Added ``calcCubicArcLength`` (#1142). + + 3.21.2 (released 2018-01-08) + ---------------------------- + + - [varLib] Fixed merging PairPos Format1/2 with missing subtables (#1125). + + 3.21.1 (released 2018-01-03) + ---------------------------- + + - [feaLib] Allow mixed single/multiple substitutions (#612) + - Added missing ``*.afm`` test assets to MAINFEST.in (#1137). + - Fixed dumping ``SVG`` tables containing color palettes (#1124). + + 3.21.0 (released 2017-12-18) + ---------------------------- + + - [cmap] when compiling format6 subtable, don't assume gid0 is always called + '.notdef' (1e42224). + - [ot] Allow decompiling fonts with bad Coverage format number (1aafae8). + - Change FontTools licence to MIT (#1127). + - [post] Prune extra names already in standard Mac set (df1e8c7). + - [subset] Delete empty SubrsIndex after subsetting (#994, #1118). + - [varLib] Don't share points in cvar by default, as it currently fails on + some browsers (#1113). + - [afmLib] Make poor old afmLib work on python3. + + 3.20.1 (released 2017-11-22) + ---------------------------- + + - [unicodedata] Fixed issue with ``script`` and ``script_extension`` functions + returning inconsistent short vs long names. They both return the short four- + letter script codes now. Added ``script_name`` and ``script_code`` functions + to look up the long human-readable script name from the script code, and + viceversa (#1109, #1111). + + 3.20.0 (released 2017-11-21) + ---------------------------- + + - [unicodedata] Addded new module ``fontTools.unicodedata`` which exports the + same interface as the built-in ``unicodedata`` module, with the addition of + a few functions that are missing from the latter, such as ``script``, + ``script_extension`` and ``block``. Added a ``MetaTools/buildUCD.py`` script + to download and parse data files from the Unicode Character Database and + generate python modules containing lists of ranges and property values. + - [feaLib] Added ``__str__`` method to all ``ast`` elements (delegates to the + ``asFea`` method). + - [feaLib] ``Parser`` constructor now accepts a ``glyphNames`` iterable + instead of ``glyphMap`` dict. The latter still works but with a pending + deprecation warning (#1104). + - [bezierTools] Added arc length calculation functions originally from + ``pens.perimeterPen`` module (#1101). + - [varLib] Started generating STAT table (8af4309). Right now it just reflects + the axes, and even that with certain limitations: + * AxisOrdering is set to the order axes are defined, + * Name-table entries are not shared with fvar. + - [py23] Added backports for ``redirect_stdout`` and ``redirect_stderr`` + context managers (#1097). + - [Graphite] Fixed some round-trip bugs (#1093). + + 3.19.0 (released 2017-11-06) + ---------------------------- + + - [varLib] Try set of used points instead of all points when testing whether to + share points between tuples (#1090). + - [CFF2] Fixed issue with reading/writing PrivateDict BlueValues to TTX file. + Read the commit message 8b02b5a and issue #1030 for more details. + NOTE: this change invalidates all the TTX files containing CFF2 tables + that where dumped with previous verisons of fonttools. + CFF2 Subr items can have values on the stack after the last operator, thus + a ``CFF2Subr`` class was added to accommodate this (#1091). + - [_k_e_r_n] Fixed compilation of AAT kern version=1.0 tables (#1089, #1094) + - [ttLib] Added getBestCmap() convenience method to TTFont class and cmap table + class that returns a preferred Unicode cmap subtable given a list of options + (#1092). + - [morx] Emit more meaningful subtable flags. Implement InsertionMorphAction + + 3.18.0 (released 2017-10-30) + ---------------------------- + + - [feaLib] Fixed writing back nested glyph classes (#1086). + - [TupleVariation] Reactivated shared points logic, bugfixes (#1009). + - [AAT] Implemented ``morx`` ligature subtables (#1082). + - [reverseContourPen] Keep duplicate lineTo following a moveTo (#1080, + https://github.com/googlei18n/cu2qu/issues/51). + - [varLib.mutator] Suport instantiation of GPOS, GDEF and MVAR (#1079). + - [sstruct] Fixed issue with ``unicode_literals`` and ``struct`` module in + old versions of python 2.7 (#993). + + 3.17.0 (released 2017-10-16) + ---------------------------- + + - [svgPathPen] Added an ``SVGPathPen`` that translates segment pen commands + into SVG path descriptions. Copied from Tal Leming's ``ufo2svg.svgPathPen`` + https://github.com/typesupply/ufo2svg/blob/d69f992/Lib/ufo2svg/svgPathPen.py + - [reverseContourPen] Added ``ReverseContourPen``, a filter pen that draws + contours with the winding direction reversed, while keeping the starting + point (#1071). + - [filterPen] Added ``ContourFilterPen`` to manipulate contours as a whole + rather than segment by segment. + - [arrayTools] Added ``Vector`` class to apply math operations on an array + of numbers, and ``pairwise`` function to loop over pairs of items in an + iterable. + - [varLib] Added support for building and interpolation of ``cvar`` table + (f874cf6, a25a401). + + 3.16.0 (released 2017-10-03) + ---------------------------- + + - [head] Try using ``SOURCE_DATE_EPOCH`` environment variable when setting + the ``head`` modified timestamp to ensure reproducible builds (#1063). + See https://reproducible-builds.org/specs/source-date-epoch/ + - [VTT] Decode VTT's ``TSI*`` tables text as UTF-8 (#1060). + - Added support for Graphite font tables: Feat, Glat, Gloc, Silf and Sill. + Thanks @mhosken! (#1054). + - [varLib] Default to using axis "name" attribute if "labelname" element + is missing (588f524). + - [merge] Added support for merging Script records. Remove unused features + and lookups after merge (d802580, 556508b). + - Added ``fontTools.svgLib`` package. Includes a parser for SVG Paths that + supports the Pen protocol (#1051). Also, added a snippet to convert SVG + outlines to UFO GLIF (#1053). + - [AAT] Added support for ``ankr``, ``bsln``, ``mort``, ``morx``, ``gcid``, + and ``cidg``. + - [subset] Implemented subsetting of ``prop``, ``opbd``, ``bsln``, ``lcar``. + + 3.15.1 (released 2017-08-18) + ---------------------------- + + - [otConverters] Implemented ``__add__`` and ``__radd__`` methods on + ``otConverters._LazyList`` that decompile a lazy list before adding + it to another list or ``_LazyList`` instance. Fixes an ``AttributeError`` + in the ``subset`` module when attempting to sum ``_LazyList`` objects + (6ef48bd2, 1aef1683). + - [AAT] Support the `opbd` table with optical bounds (a47f6588). + - [AAT] Support `prop` table with glyph properties (d05617b4). + + + 3.15.0 (released 2017-08-17) + ---------------------------- + + - [AAT] Added support for AAT lookups. The ``lcar`` table can be decompiled + and recompiled; futher work needed to handle ``morx`` table (#1025). + - [subset] Keep (empty) DefaultLangSys for Script 'DFLT' (6eb807b5). + - [subset] Support GSUB/GPOS.FeatureVariations (fe01d87b). + - [varLib] In ``models.supportScalars``, ignore an axis when its peak value + is 0 (fixes #1020). + - [varLib] Add default mappings to all axes in avar to fix rendering issue + in some rasterizers (19c4b377, 04eacf13). + - [varLib] Flatten multiple tail PairPosFormat2 subtables before merging + (c55ef525). + - [ttLib] Added support for recalculating font bounding box in ``CFF`` and + ``head`` tables, and min/max values in ``hhea`` and ``vhea`` tables (#970). + + 3.14.0 (released 2017-07-31) + ---------------------------- + + - [varLib.merger] Remove Extensions subtables before merging (f7c20cf8). + - [varLib] Initialize the avar segment map with required default entries + (#1014). + - [varLib] Implemented optimal IUP optmiziation (#1019). + - [otData] Add ``AxisValueFormat4`` for STAT table v1.2 from OT v1.8.2 + (#1015). + - [name] Fixed BCP46 language tag for Mac langID=9: 'si' -> 'sl'. + - [subset] Return value from ``_DehintingT2Decompiler.op_hintmask`` + (c0d672ba). + - [cffLib] Allow to get TopDict by index as well as by name (dca96c9c). + - [cffLib] Removed global ``isCFF2`` state; use one set of classes for + both CFF and CFF2, maintaining backward compatibility existing code (#1007). + - [cffLib] Deprecated maxstack operator, per OpenType spec update 1.8.1. + - [cffLib] Added missing default (-100) for UnderlinePosition (#983). + - [feaLib] Enable setting nameIDs greater than 255 (#1003). + - [varLib] Recalculate ValueFormat when merging SinglePos (#996). + - [varLib] Do not emit MVAR if there are no entries in the variation store + (#987). + - [ttx] For ``-x`` option, pad with space if table tag length is < 4. + + 3.13.1 (released 2017-05-30) + ---------------------------- + + - [feaLib.builder] Removed duplicate lookups optimization. The original + lookup order and semantics of the feature file are preserved (#976). + + 3.13.0 (released 2017-05-24) + ---------------------------- + + - [varLib.mutator] Implement IUP optimization (#969). + - [_g_l_y_f.GlyphCoordinates] Changed ``__bool__()`` semantics to match those + of other iterables (e46f949). Removed ``__abs__()`` (3db5be2). + - [varLib.interpolate_layout] Added ``mapped`` keyword argument to + ``interpolate_layout`` to allow disabling avar mapping: if False (default), + the location is mapped using the map element of the axes in designspace file; + if True, it is assumed that location is in designspace's internal space and + no mapping is performed (#950, #975). + - [varLib.interpolate_layout] Import designspace-loading logic from varLib. + - [varLib] Fixed bug with recombining PairPosClass2 subtables (81498e5, #914). + - [cffLib.specializer] When copying iterables, cast to list (462b7f86). + + 3.12.1 (released 2017-05-18) + ---------------------------- + + - [pens.t2CharStringPen] Fixed AttributeError when calling addComponent in + T2CharStringPen (#965). + + 3.12.0 (released 2017-05-17) + ---------------------------- + + - [cffLib.specializer] Added new ``specializer`` module to optimize CFF + charstrings, used by the T2CharStringPen (#948). + - [varLib.mutator] Sort glyphs by component depth before calculating composite + glyphs' bounding boxes to ensure deltas are correctly caclulated (#945). + - [_g_l_y_f] Fixed loss of precision in GlyphCoordinates by using 'd' (double) + instead of 'f' (float) as ``array.array`` typecode (#963, #964). + + 3.11.0 (released 2017-05-03) + ---------------------------- + + - [t2CharStringPen] Initial support for specialized Type2 path operators: + vmoveto, hmoveto, vlineto, hlineto, vvcurveto, hhcurveto, vhcurveto and + hvcurveto. This should produce more compact charstrings (#940, #403). + - [Doc] Added Sphinx sources for the documentation. Thanks @gferreira (#935). + - [fvar] Expose flags in XML (#932) + - [name] Add helper function for building multi-lingual names (#921) + - [varLib] Fixed kern merging when a PairPosFormat2 has ClassDef1 with glyphs + that are NOT present in the Coverage (1b5e1c4, #939). + - [varLib] Fixed non-deterministic ClassDef order with PY3 (f056c12, #927). + - [feLib] Throw an error when the same glyph is defined in multiple mark + classes within the same lookup (3e3ff00, #453). + + 3.10.0 (released 2017-04-14) + ---------------------------- + + - [varLib] Added support for building ``avar`` table, using the designspace + ``<map>`` elements. + - [varLib] Removed unused ``build(..., axisMap)`` argument. Axis map should + be specified in designspace file now. We do not accept nonstandard axes + if ``<axes>`` element is not present. + - [varLib] Removed "custom" axis from the ``standard_axis_map``. This was + added before when glyphsLib was always exporting the (unused) custom axis. + - [varLib] Added partial support for building ``MVAR`` table; does not + implement ``gasp`` table variations yet. + - [pens] Added FilterPen base class, for pens that control another pen; + factored out ``addComponent`` method from BasePen into a separate abstract + DecomposingPen class; added DecomposingRecordingPen, which records + components decomposed as regular contours. + - [TSI1] Fixed computation of the textLength of VTT private tables (#913). + - [loggingTools] Added ``LogMixin`` class providing a ``log`` property to + subclasses, which returns a ``logging.Logger`` named after the latter. + - [loggingTools] Added ``assertRegex`` method to ``CapturingLogHandler``. + - [py23] Added backport for python 3's ``types.SimpleNamespace`` class. + - [EBLC] Fixed issue with python 3 ``zip`` iterator. + + 3.9.2 (released 2017-04-08) + --------------------------- + + - [pens] Added pen to draw glyphs using WxPython ``GraphicsPath`` class: + https://wxpython.org/docs/api/wx.GraphicsPath-class.html + - [varLib.merger] Fixed issue with recombining multiple PairPosFormat2 + subtables (#888) + - [varLib] Do not encode gvar deltas that are all zeroes, or if all values + are smaller than tolerance. + - [ttLib] _TTGlyphSet glyphs now also have ``height`` and ``tsb`` (top + side bearing) attributes from the ``vmtx`` table, if present. + - [glyf] In ``GlyphCoordintes`` class, added ``__bool__`` / ``__nonzero__`` + methods, and ``array`` property to get raw array. + - [ttx] Support reading TTX files with BOM (#896) + - [CFF2] Fixed the reporting of the number of regions in the font. + + 3.9.1 (released 2017-03-20) + --------------------------- + + - [varLib.merger] Fixed issue while recombining multiple PairPosFormat2 + subtables if they were split because of offset overflows (9798c30). + - [varLib.merger] Only merge multiple PairPosFormat1 subtables if there is + at least one of the fonts with a non-empty Format1 subtable (0f5a46b). + - [varLib.merger] Fixed IndexError with empty ClassDef1 in PairPosFormat2 + (aad0d46). + - [varLib.merger] Avoid reusing Class2Record (mutable) objects (e6125b3). + - [varLib.merger] Calculate ClassDef1 and ClassDef2's Format when merging + PairPosFormat2 (23511fd). + - [macUtils] Added missing ttLib import (b05f203). + + 3.9.0 (released 2017-03-13) + --------------------------- + + - [feaLib] Added (partial) support for parsing feature file comments ``# ...`` + appearing in between statements (#879). + - [feaLib] Cleaned up syntax tree for FeatureNames. + - [ttLib] Added support for reading/writing ``CFF2`` table (thanks to + @readroberts at Adobe), and ``TTFA`` (ttfautohint) table. + - [varLib] Fixed regression introduced with 3.8.0 in the calculation of + ``NumShorts``, i.e. the number of deltas in ItemVariationData's delta sets + that use a 16-bit representation (b2825ff). + + 3.8.0 (released 2017-03-05) + --------------------------- + + - New pens: MomentsPen, StatisticsPen, RecordingPen, and TeePen. + - [misc] Added new ``fontTools.misc.symfont`` module, for symbolic font + statistical analysis; requires ``sympy`` (http://www.sympy.org/en/index.html) + - [varLib] Added experimental ``fontTools.varLib.interpolatable`` module for + finding wrong contour order between different masters + - [varLib] designspace.load() now returns a dictionary, instead of a tuple, + and supports <axes> element (#864); the 'masters' item was renamed 'sources', + like the <sources> element in the designspace document + - [ttLib] Fixed issue with recalculating ``head`` modified timestamp when + saving CFF fonts + - [ttLib] In TupleVariation, round deltas before compiling (#861, fixed #592) + - [feaLib] Ignore duplicate glyphs in classes used as MarkFilteringSet and + MarkAttachmentType (#863) + - [merge] Changed the ``gasp`` table merge logic so that only the one from + the first font is retained, similar to other hinting tables (#862) + - [Tests] Added tests for the ``varLib`` package, as well as test fonts + from the "Annotated OpenType Specification" (AOTS) to exercise ``ttLib``'s + table readers/writers (<https://github.com/adobe-type-tools/aots>) + + 3.7.2 (released 2017-02-17) + --------------------------- + + - [subset] Keep advance widths when stripping ".notdef" glyph outline in + CID-keyed CFF fonts (#845) + - [feaLib] Zero values now produce the same results as makeotf (#633, #848) + - [feaLib] More compact encoding for “Contextual positioning with in-line + single positioning rules” (#514) + + 3.7.1 (released 2017-02-15) + --------------------------- + + - [subset] Fixed issue with ``--no-hinting`` option whereby advance widths in + Type 2 charstrings were also being stripped (#709, #343) + - [feaLib] include statements now resolve relative paths like makeotf (#838) + - [feaLib] table ``name`` now handles Unicode codepoints beyond the Basic + Multilingual Plane, also supports old-style MacOS platform encodings (#842) + - [feaLib] correctly escape string literals when emitting feature syntax (#780) + + 3.7.0 (released 2017-02-11) + --------------------------- + + - [ttx, mtiLib] Preserve ordering of glyph alternates in GSUB type 3 (#833). + - [feaLib] Glyph names can have dashes, as per new AFDKO syntax v1.20 (#559). + - [feaLib] feaLib.Parser now needs the font's glyph map for parsing. + - [varLib] Fix regression where GPOS values were stored as 0. + - [varLib] Allow merging of class-based kerning when ClassDefs are different + + 3.6.3 (released 2017-02-06) + --------------------------- + + - [varLib] Fix building variation of PairPosFormat2 (b5c34ce). + - Populate defaults even for otTables that have postRead (e45297b). + - Fix compiling of MultipleSubstFormat1 with zero 'out' glyphs (b887860). + + 3.6.2 (released 2017-01-30) + --------------------------- + + - [varLib.merger] Fixed "TypeError: reduce() of empty sequence with no + initial value" (3717dc6). + + 3.6.1 (released 2017-01-28) + --------------------------- + + - [py23] Fixed unhandled exception occurring at interpreter shutdown in + the "last resort" logging handler (972b3e6). + - [agl] Ensure all glyph names are of native 'str' type; avoid mixing + 'str' and 'unicode' in TTFont.glyphOrder (d8c4058). + - Fixed inconsistent title levels in README.rst that caused PyPI to + incorrectly render the reStructuredText page. + + 3.6.0 (released 2017-01-26) + --------------------------- + + - [varLib] Refactored and improved the variation-font-building process. + - Assembly code in the fpgm, prep, and glyf tables is now indented in + XML output for improved readability. The ``instruction`` element is + written as a simple tag if empty (#819). + - [ttx] Fixed 'I/O operation on closed file' error when dumping + multiple TTXs to standard output with the '-o -' option. + - The unit test modules (``*_test.py``) have been moved outside of the + fontTools package to the Tests folder, thus they are no longer + installed (#811). + + 3.5.0 (released 2017-01-14) + --------------------------- + + - Font tables read from XML can now be written back to XML with no + loss. + - GSUB/GPOS LookupType is written out in XML as an element, not + comment. (#792) + - When parsing cmap table, do not store items mapped to glyph id 0. + (#790) + - [otlLib] Make ClassDef sorting deterministic. Fixes #766 (7d1ddb2) + - [mtiLib] Added unit tests (#787) + - [cvar] Implemented cvar table + - [gvar] Renamed GlyphVariation to TupleVariation to match OpenType + terminology. + - [otTables] Handle gracefully empty VarData.Item array when compiling + XML. (#797) + - [varLib] Re-enabled generation of ``HVAR`` table for fonts with + TrueType outlines; removed ``--build-HVAR`` command-line option. + - [feaLib] The parser can now be extended to support non-standard + statements in FEA code by using a customized Abstract Syntax Tree. + See, for example, ``feaLib.builder_test.test_extensions`` and + baseClass.feax (#794, fixes #773). + - [feaLib] Added ``feaLib`` command to the 'fonttools' command-line + tool; applies a feature file to a font. ``fonttools feaLib -h`` for + help. + - [pens] The ``T2CharStringPen`` now takes an optional + ``roundTolerance`` argument to control the rounding of coordinates + (#804, fixes #769). + - [ci] Measure test coverage on all supported python versions and OSes, + combine coverage data and upload to + https://codecov.io/gh/fonttools/fonttools (#786) + - [ci] Configured Travis and Appveyor for running tests on Python 3.6 + (#785, 55c03bc) + - The manual pages installation directory can be customized through + ``FONTTOOLS_MANPATH`` environment variable (#799, fixes #84). + - [Snippets] Added otf2ttf.py, for converting fonts from CFF to + TrueType using the googlei18n/cu2qu module (#802) + + 3.4.0 (released 2016-12-21) + --------------------------- + + - [feaLib] Added support for generating FEA text from abstract syntax + tree (AST) objects (#776). Thanks @mhosken + - Added ``agl.toUnicode`` function to convert AGL-compliant glyph names + to Unicode strings (#774) + - Implemented MVAR table (b4d5381) + + 3.3.1 (released 2016-12-15) + --------------------------- + + - [setup] We no longer use versioneer.py to compute fonttools version + from git metadata, as this has caused issues for some users (#767). + Now we bump the version strings manually with a custom ``release`` + command of setup.py script. + + 3.3.0 (released 2016-12-06) + --------------------------- + + - [ttLib] Implemented STAT table from OpenType 1.8 (#758) + - [cffLib] Fixed decompilation of CFF fonts containing non-standard + key/value pairs in FontDict (issue #740; PR #744) + - [py23] minor: in ``round3`` function, allow the second argument to be + ``None`` (#757) + - The standalone ``sstruct`` and ``xmlWriter`` modules, deprecated + since vesion 3.2.0, have been removed. They can be imported from the + ``fontTools.misc`` package. + + 3.2.3 (released 2016-12-02) + --------------------------- + + - [py23] optimized performance of round3 function; added backport for + py35 math.isclose() (9d8dacb) + - [subset] fixed issue with 'narrow' (UCS-2) Python 2 builds and + ``--text``/``--text-file`` options containing non-BMP chararcters + (16d0e5e) + - [varLib] fixed issuewhen normalizing location values (8fa2ee1, #749) + - [inspect] Made it compatible with both python2 and python3 (167ee60, + #748). Thanks @pnemade + + 3.2.2 (released 2016-11-24) + --------------------------- + + - [varLib] Do not emit null axes in fvar (1bebcec). Thanks @robmck-ms + - [varLib] Handle fonts without GPOS (7915a45) + - [merge] Ignore LangSys if None (a11bc56) + - [subset] Fix subsetting MathVariants (78d3cbe) + - [OS/2] Fix "Private Use (plane 15)" range (08a0d55). Thanks @mashabow + + 3.2.1 (released 2016-11-03) + --------------------------- + + - [OS/2] fix checking ``fsSelection`` bits matching ``head.macStyle`` + bits + - [varLib] added ``--build-HVAR`` option to generate ``HVAR`` table for + fonts with TrueType outlines. For ``CFF2``, it is enabled by default. + + 3.2.0 (released 2016-11-02) + --------------------------- + + - [varLib] Improve support for OpenType 1.8 Variable Fonts: + - Implement GDEF's VariationStore + - Implement HVAR/VVAR tables + - Partial support for loading MutatorMath .designspace files with + varLib.designspace module + - Add varLib.models with Variation fonts interpolation models + - Implement GSUB/GPOS FeatureVariations + - Initial support for interpolating and merging OpenType Layout tables + (see ``varLib.interpolate_layout`` and ``varLib.merger`` modules) + - [API change] Change version to be an integer instead of a float in + XML output for GSUB, GPOS, GDEF, MATH, BASE, JSTF, HVAR, VVAR, feat, + hhea and vhea tables. Scripts that set the Version for those to 1.0 + or other float values also need fixing. A warning is emitted when + code or XML needs fix. + - several bug fixes to the cffLib module, contributed by Adobe's + @readroberts + - The XML output for CFF table now has a 'major' and 'minor' elements + for specifying whether it's version 1.0 or 2.0 (support for CFF2 is + coming soon) + - [setup.py] remove undocumented/deprecated ``extra_path`` Distutils + argument. This means that we no longer create a "FontTools" subfolder + in site-packages containing the actual fontTools package, as well as + the standalone xmlWriter and sstruct modules. The latter modules are + also deprecated, and scheduled for removal in upcoming releases. + Please change your import statements to point to from fontTools.misc + import xmlWriter and from fontTools.misc import sstruct. + - [scripts] Add a 'fonttools' command-line tool that simply runs + ``fontTools.*`` sub-modules: e.g. ``fonttools ttx``, + ``fonttools subset``, etc. + - [hmtx/vmts] Read advance width/heights as unsigned short (uint16); + automatically round float values to integers. + - [ttLib/xmlWriter] add 'newlinestr=None' keyword argument to + ``TTFont.saveXML`` for overriding os-specific line endings (passed on + to ``XMLWriter`` instances). + - [versioning] Use versioneer instead of ``setuptools_scm`` to + dynamically load version info from a git checkout at import time. + - [feaLib] Support backslash-prefixed glyph names. + + 3.1.2 (released 2016-09-27) + --------------------------- + + - restore Makefile as an alternative way to build/check/install + - README.md: update instructions for installing package from source, + and for running test suite + - NEWS: Change log was out of sync with tagged release + + 3.1.1 (released 2016-09-27) + --------------------------- + + - Fix ``ttLibVersion`` attribute in TTX files still showing '3.0' + instead of '3.1'. + - Use ``setuptools_scm`` to manage package versions. + + 3.1.0 (released 2016-09-26) + --------------------------- + + - [feaLib] New library to parse and compile Adobe FDK OpenType Feature + files. + - [mtiLib] New library to parse and compile Monotype 'FontDame' + OpenType Layout Tables files. + - [voltLib] New library to parse Microsoft VOLT project files. + - [otlLib] New library to work with OpenType Layout tables. + - [varLib] New library to work with OpenType Font Variations. + - [pens] Add ttGlyphPen to draw to TrueType glyphs, and t2CharStringPen + to draw to Type 2 Charstrings (CFF); add areaPen and perimeterPen. + - [ttLib.tables] Implement 'meta' and 'trak' tables. + - [ttx] Add --flavor option for compiling to 'woff' or 'woff2'; add + ``--with-zopfli`` option to use Zopfli to compress WOFF 1.0 fonts. + - [subset] Support subsetting 'COLR'/'CPAL' and 'CBDT'/'CBLC' color + fonts tables, and 'gvar' table for variation fonts. + - [Snippets] Add ``symfont.py``, for symbolic font statistics analysis; + interpolatable.py, a preliminary script for detecting interpolation + errors; ``{merge,dump}_woff_metadata.py``. + - [classifyTools] Helpers to classify things into classes. + - [CI] Run tests on Windows, Linux and macOS using Appveyor and Travis + CI; check unit test coverage with Coverage.py/Coveralls; automatic + deployment to PyPI on tags. + - [loggingTools] Use Python built-in logging module to print messages. + - [py23] Make round() behave like Python 3 built-in round(); define + round2() and round3(). + + 3.0 (released 2015-09-01) + ------------------------- + + - Add Snippet scripts for cmap subtable format conversion, printing + GSUB/GPOS features, building a GX font from two masters + - TTX WOFF2 support and a ``-f`` option to overwrite output file(s) + - Support GX tables: ``avar``, ``gvar``, ``fvar``, ``meta`` + - Support ``feat`` and gzip-compressed SVG tables + - Upgrade Mac East Asian encodings to native implementation if + available + - Add Roman Croatian and Romanian encodings, codecs for mac-extended + East Asian encodings + - Implement optimal GLYF glyph outline packing; disabled by default + + 2.5 (released 2014-09-24) + ------------------------- + + - Add a Qt pen + - Add VDMX table converter + - Load all OpenType sub-structures lazily + - Add support for cmap format 13. + - Add pyftmerge tool + - Update to Unicode 6.3.0d3 + - Add pyftinspect tool + - Add support for Google CBLC/CBDT color bitmaps, standard EBLC/EBDT + embedded bitmaps, and ``SVG`` table (thanks to Read Roberts at Adobe) + - Add support for loading, saving and ttx'ing WOFF file format + - Add support for Microsoft COLR/CPAL layered color glyphs + - Support PyPy + - Support Jython, by replacing numpy with array/lists modules and + removed it, pure-Python StringIO, not cStringIO + - Add pyftsubset and Subsetter object, supporting CFF and TTF + - Add to ttx args for -q for quiet mode, -z to choose a bitmap dump + format + + 2.4 (released 2013-06-22) + ------------------------- + + - Option to write to arbitrary files + - Better dump format for DSIG + - Better detection of OTF XML + - Fix issue with Apple's kern table format + - Fix mangling of TT glyph programs + - Fix issues related to mona.ttf + - Fix Windows Installer instructions + - Fix some modern MacOS issues + - Fix minor issues and typos + + 2.3 (released 2009-11-08) + ------------------------- + + - TrueType Collection (TTC) support + - Python 2.6 support + - Update Unicode data to 5.2.0 + - Couple of bug fixes + + 2.2 (released 2008-05-18) + ------------------------- + + - ClearType support + - cmap format 1 support + - PFA font support + - Switched from Numeric to numpy + - Update Unicode data to 5.1.0 + - Update AGLFN data to 1.6 + - Many bug fixes + + 2.1 (released 2008-01-28) + ------------------------- + + - Many years worth of fixes and features + + 2.0b2 (released 2002-??-??) + --------------------------- + + - Be "forgiving" when interpreting the maxp table version field: + interpret any value as 1.0 if it's not 0.5. Fixes dumping of these + GPL fonts: http://www.freebsd.org/cgi/pds.cgi?ports/chinese/wangttf + - Fixed ttx -l: it turned out this part of the code didn't work with + Python 2.2.1 and earlier. My bad to do most of my testing with a + different version than I shipped TTX with :-( + - Fixed bug in ClassDef format 1 subtable (Andreas Seidel bumped into + this one). + + 2.0b1 (released 2002-09-10) + --------------------------- + + - Fixed embarrassing bug: the master checksum in the head table is now + calculated correctly even on little-endian platforms (such as Intel). + - Made the cmap format 4 compiler smarter: the binary data it creates + is now more or less as compact as possible. TTX now makes more + compact data than in any shipping font I've tested it with. + - Dump glyph names as a separate "GlyphOrder" pseudo table as opposed + to as part of the glyf table (obviously needed for CFF-OTF's). + - Added proper support for the CFF table. + - Don't barf on empty tables (questionable, but "there are font out + there...") + - When writing TT glyf data, align glyphs on 4-byte boundaries. This + seems to be the current recommendation by MS. Also: don't barf on + fonts which are already 4-byte aligned. + - Windows installer contributed bu Adam Twardoch! Yay! + - Changed the command line interface again, now by creating one new + tool replacing the old ones: ttx It dumps and compiles, depending on + input file types. The options have changed somewhat. + - The -d option is back (output dir) + - ttcompile's -i options is now called -m (as in "merge"), to avoid + clash with dump's -i. + - The -s option ("split tables") no longer creates a directory, but + instead outputs a small .ttx file containing references to the + individual table files. This is not a true link, it's a simple file + name, and the referenced file should be in the same directory so + ttcompile can find them. + - compile no longer accepts a directory as input argument. Instead it + can parse the new "mini-ttx" format as output by "ttx -s". + - all arguments are input files + - Renamed the command line programs and moved them to the Tools + subdirectory. They are now installed by the setup.py install script. + - Added OpenType support. BASE, GDEF, GPOS, GSUB and JSTF are (almost) + fully supported. The XML output is not yet final, as I'm still + considering to output certain subtables in a more human-friendly + manner. + - Fixed 'kern' table to correctly accept subtables it doesn't know + about, as well as interpreting Apple's definition of the 'kern' table + headers correctly. + - Fixed bug where glyphnames were not calculated from 'cmap' if it was + (one of the) first tables to be decompiled. More specifically: it + cmap was the first to ask for a glyphID -> glyphName mapping. + - Switched XML parsers: use expat instead of xmlproc. Should be faster. + - Removed my UnicodeString object: I now require Python 2.0 or up, + which has unicode support built in. + - Removed assert in glyf table: redundant data at the end of the table + is now ignored instead of raising an error. Should become a warning. + - Fixed bug in hmtx/vmtx code that only occured if all advances were + equal. + - Fixed subtle bug in TT instruction disassembler. + - Couple of fixes to the 'post' table. + - Updated OS/2 table to latest spec. + + 1.0b1 (released 2001-08-10) + --------------------------- + + - Reorganized the command line interface for ttDump.py and + ttCompile.py, they now behave more like "normal" command line tool, + in that they accept multiple input files for batch processing. + - ttDump.py and ttCompile.py don't silently override files anymore, but + ask before doing so. Can be overridden by -f. + - Added -d option to both ttDump.py and ttCompile.py. + - Installation is now done with distutils. (Needs work for environments + without compilers.) + - Updated installation instructions. + - Added some workarounds so as to handle certain buggy fonts more + gracefully. + - Updated Unicode table to Unicode 3.0 (Thanks Antoine!) + - Included a Python script by Adam Twardoch that adds some useful stuff + to the Windows registry. + - Moved the project to SourceForge. + + 1.0a6 (released 2000-03-15) + --------------------------- + + - Big reorganization: made ttLib a subpackage of the new fontTools + package, changed several module names. Called the entire suite + "FontTools" + - Added several submodules to fontTools, some new, some older. + - Added experimental CFF/GPOS/GSUB support to ttLib, read-only (but XML + dumping of GPOS/GSUB is for now disabled) + - Fixed hdmx endian bug + - Added -b option to ttCompile.py, it disables recalculation of + bounding boxes, as requested by Werner Lemberg. + - Renamed tt2xml.pt to ttDump.py and xml2tt.py to ttCompile.py + - Use ".ttx" as file extension instead of ".xml". + - TTX is now the name of the XML-based *format* for TT fonts, and not + just an application. + + 1.0a5 + ----- + + Never released + + - More tables supported: hdmx, vhea, vmtx + + 1.0a3 & 1.0a4 + ------------- + + Never released + + - fixed most portability issues + - retracted the "Euro_or_currency" change from 1.0a2: it was + nonsense! + + 1.0a2 (released 1999-05-02) + --------------------------- + + - binary release for MacOS + - genenates full FOND resources: including width table, PS font name + info and kern table if applicable. + - added cmap format 4 support. Extra: dumps Unicode char names as XML + comments! + - added cmap format 6 support + - now accepts true type files starting with "true" (instead of just + 0x00010000 and "OTTO") + - 'glyf' table support is now complete: I added support for composite + scale, xy-scale and two-by-two for the 'glyf' table. For now, + component offset scale behaviour defaults to Apple-style. This only + affects the (re)calculation of the glyph bounding box. + - changed "Euro" to "Euro_or_currency" in the Standard Apple Glyph + order list, since we cannot tell from the 'post' table which is + meant. I should probably doublecheck with a Unicode encoding if + available. (This does not affect the output!) + + Fixed bugs: - 'hhea' table is now recalculated correctly - fixed wrong + assumption about sfnt resource names + + 1.0a1 (released 1999-04-27) + --------------------------- + + - initial binary release for MacOS + +Platform: Any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Console +Classifier: Environment :: Other Environment +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: End Users/Desktop +Classifier: License :: OSI Approved :: MIT License +Classifier: Natural Language :: English +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 +Classifier: Topic :: Text Processing :: Fonts +Classifier: Topic :: Multimedia :: Graphics +Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion diff --git a/README.md b/README.md deleted file mode 100644 index c2f7d0c..0000000 --- a/README.md +++ /dev/null @@ -1,34 +0,0 @@ -### What it is ? - -Quoting from [TTX/FontTools Sourceforge Project](http://sourceforge.net/projects/fonttools/) -> a tool to convert OpenType and TrueType fonts to and from XML. FontTools is a library for manipulating fonts, written in Python. It supports TrueType, OpenType, AFM and to an extent Type 1 and some Mac-specific formats. - -### Quick start - -```python setup.py install``` - -From your command line type the above command to get fontools installed on your system. - -### Installation - -See [install.txt](https://github.com/behdad/fonttools/blob/master/Doc/install.txt) in the 'Doc' subdirectory for instructions on how to build and install TTX/FontTools from the sources. - - -### Documentation - -#### What is TTX ? - -See [documentation.html](https://github.com/behdad/fonttools/blob/master/Doc/documentation.html) in the "Doc" subdirectory for TTX usage instructions and information about the TTX file format. - -### Community -* https://groups.google.com/d/forum/fonttools - -### License - -See "LICENSE.txt" for licensing information. - - - -Have fun! - -Just van Rossum <just@letterror.com> diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..664ad8f --- /dev/null +++ b/README.rst @@ -0,0 +1,365 @@ +|Travis Build Status| |Appveyor Build status| |Health| |Coverage Status| +|PyPI| |Gitter Chat| + +What is this? +~~~~~~~~~~~~~ + +| fontTools is a library for manipulating fonts, written in Python. The + project includes the TTX tool, that can convert TrueType and OpenType + fonts to and from an XML text format, which is also called TTX. It + supports TrueType, OpenType, AFM and to an extent Type 1 and some + Mac-specific formats. The project has a `MIT open-source + licence <LICENSE>`__. +| Among other things this means you can use it free of charge. + +Installation +~~~~~~~~~~~~ + +FontTools requires `Python <http://www.python.org/download/>`__ 2.7, 3.4 +or later. + +The package is listed in the Python Package Index (PyPI), so you can +install it with `pip <https://pip.pypa.io>`__: + +.. code:: sh + + pip install fonttools + +If you would like to contribute to its development, you can clone the +repository from Github, install the package in 'editable' mode and +modify the source code in place. We recommend creating a virtual +environment, using `virtualenv <https://virtualenv.pypa.io>`__ or +Python 3 `venv <https://docs.python.org/3/library/venv.html>`__ module. + +.. code:: sh + + # download the source code to 'fonttools' folder + git clone https://github.com/fonttools/fonttools.git + cd fonttools + + # create new virtual environment called e.g. 'fonttools-venv', or anything you like + python -m virtualenv fonttools-venv + + # source the `activate` shell script to enter the environment (Un\*x); to exit, just type `deactivate` + . fonttools-venv/bin/activate + + # to activate the virtual environment in Windows `cmd.exe`, do + fonttools-venv\Scripts\activate.bat + + # install in 'editable' mode + pip install -e . + +TTX – From OpenType and TrueType to XML and Back +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Once installed you can use the ``ttx`` command to convert binary font +files (``.otf``, ``.ttf``, etc) to the TTX xml format, edit them, and +convert them back to binary format. TTX files have a .ttx file +extension. + +.. code:: sh + + ttx /path/to/font.otf + ttx /path/to/font.ttx + +The TTX application works can be used in two ways, depending on what +platform you run it on: + +- As a command line tool (Windows/DOS, Unix, MacOSX) +- By dropping files onto the application (Windows, MacOS) + +TTX detects what kind of files it is fed: it will output a ``.ttx`` file +when it sees a ``.ttf`` or ``.otf``, and it will compile a ``.ttf`` or +``.otf`` when the input file is a ``.ttx`` file. By default, the output +file is created in the same folder as the input file, and will have the +same name as the input file but with a different extension. TTX will +*never* overwrite existing files, but if necessary will append a unique +number to the output filename (before the extension) such as +``Arial#1.ttf`` + +When using TTX from the command line there are a bunch of extra options, +these are explained in the help text, as displayed when typing +``ttx -h`` at the command prompt. These additional options include: + +- specifying the folder where the output files are created +- specifying which tables to dump or which tables to exclude +- merging partial ``.ttx`` files with existing ``.ttf`` or ``.otf`` + files +- listing brief table info instead of dumping to ``.ttx`` +- splitting tables to separate ``.ttx`` files +- disabling TrueType instruction disassembly + +The TTX file format +------------------- + +The following tables are currently supported: + +.. begin table list +.. code:: + + BASE, CBDT, CBLC, CFF, CFF2, COLR, CPAL, DSIG, EBDT, EBLC, FFTM, + Feat, GDEF, GMAP, GPKG, GPOS, GSUB, Glat, Gloc, HVAR, JSTF, LTSH, + MATH, META, MVAR, OS/2, SING, STAT, SVG, Silf, Sill, TSI0, TSI1, + TSI2, TSI3, TSI5, TSIB, TSID, TSIJ, TSIP, TSIS, TSIV, TTFA, VDMX, + VORG, VVAR, ankr, avar, bsln, cidg, cmap, cvar, cvt, feat, fpgm, + fvar, gasp, gcid, glyf, gvar, hdmx, head, hhea, hmtx, kern, lcar, + loca, ltag, maxp, meta, mort, morx, name, opbd, post, prep, prop, + sbix, trak, vhea and vmtx +.. end table list + +Other tables are dumped as hexadecimal data. + +TrueType fonts use glyph indices (GlyphIDs) to refer to glyphs in most +places. While this is fine in binary form, it is really hard to work +with for humans. Therefore we use names instead. + +The glyph names are either extracted from the ``CFF`` table or the +``post`` table, or are derived from a Unicode ``cmap`` table. In the +latter case the Adobe Glyph List is used to calculate names based on +Unicode values. If all of these methods fail, names are invented based +on GlyphID (eg ``glyph00142``) + +It is possible that different glyphs use the same name. If this happens, +we force the names to be unique by appending ``#n`` to the name (``n`` +being an integer number.) The original names are being kept, so this has +no influence on a "round tripped" font. + +Because the order in which glyphs are stored inside the binary font is +important, we maintain an ordered list of glyph names in the font. + +Other Tools +~~~~~~~~~~~ + +Commands for inspecting, merging and subsetting fonts are also +available: + +.. code:: sh + + pyftinspect + pyftmerge + pyftsubset + +fontTools Python Module +~~~~~~~~~~~~~~~~~~~~~~~ + +The fontTools python module provides a convenient way to +programmatically edit font files. + +.. code:: py + + >>> from fontTools.ttLib import TTFont + >>> font = TTFont('/path/to/font.ttf') + >>> font + <fontTools.ttLib.TTFont object at 0x10c34ed50> + >>> + +A selection of sample python programs is in the +`Snippets <https://github.com/fonttools/fonttools/blob/master/Snippets/>`__ +directory. + +Optional Requirements +--------------------- + +The ``fontTools`` package currently has no (required) external dependencies +besides the modules included in the Python Standard Library. +However, a few extra dependencies are required by some of its modules, which +are needed to unlock optional features. + +- ``Lib/fontTools/ttLib/woff2.py`` + + Module to compress/decompress WOFF 2.0 web fonts; it requires: + + - `brotli <https://pypi.python.org/pypi/Brotli>`__: Python bindings of + the Brotli compression library. + +- ``Lib/fontTools/ttLib/sfnt.py`` + + To better compress WOFF 1.0 web fonts, the following module can be used + instead of the built-in ``zlib`` library: + + - `zopfli <https://pypi.python.org/pypi/zopfli>`__: Python bindings of + the Zopfli compression library. + +- ``Lib/fontTools/unicode.py`` + + To display the Unicode character names when dumping the ``cmap`` table + with ``ttx`` we use the ``unicodedata`` module in the Standard Library. + The version included in there varies between different Python versions. + To use the latest available data, you can install: + + - `unicodedata2 <https://pypi.python.org/pypi/unicodedata2>`__: + ``unicodedata`` backport for Python 2.7 and 3.5 updated to the latest + Unicode version 9.0. Note this is not necessary if you use Python 3.6 + as the latter already comes with an up-to-date ``unicodedata``. + +- ``Lib/fontTools/varLib/interpolatable.py`` + + Module for finding wrong contour/component order between different masters. + It requires one of the following packages in order to solve the so-called + "minimum weight perfect matching problem in bipartite graphs", or + the Assignment problem: + + * `scipy <https://pypi.python.org/pypi/scipy>`__: the Scientific Library + for Python, which internally uses `NumPy <https://pypi.python.org/pypi/numpy>`__ + arrays and hence is very fast; + * `munkres <https://pypi.python.org/pypi/munkres>`__: a pure-Python + module that implements the Hungarian or Kuhn-Munkres algorithm. + +- ``Lib/fontTools/misc/symfont.py`` + + Advanced module for symbolic font statistics analysis; it requires: + + * `sympy <https://pypi.python.org/pypi/sympy>`__: the Python library for + symbolic mathematics. + +- ``Lib/fontTools/t1Lib.py`` + + To get the file creator and type of Macintosh PostScript Type 1 fonts + on Python 3 you need to install the following module, as the old ``MacOS`` + module is no longer included in Mac Python: + + * `xattr <https://pypi.python.org/pypi/xattr>`__: Python wrapper for + extended filesystem attributes (macOS platform only). + +- ``Lib/fontTools/pens/cocoaPen.py`` + + Pen for drawing glyphs with Cocoa ``NSBezierPath``, requires: + + * `PyObjC <https://pypi.python.org/pypi/pyobjc>`__: the bridge between + Python and the Objective-C runtime (macOS platform only). + +- ``Lib/fontTools/pens/qtPen.py`` + + Pen for drawing glyphs with Qt's ``QPainterPath``, requires: + + * `PyQt5 <https://pypi.python.org/pypi/PyQt5>`__: Python bindings for + the Qt cross platform UI and application toolkit. + +- ``Lib/fontTools/pens/reportLabPen.py`` + + Pen to drawing glyphs as PNG images, requires: + + * `reportlab <https://pypi.python.org/pypi/reportlab>`__: Python toolkit + for generating PDFs and graphics. + +- ``Lib/fontTools/inspect.py`` + + A GUI font inspector, requires one of the following packages: + + * `PyGTK <https://pypi.python.org/pypi/PyGTK>`__: Python bindings for + GTK  2.x (only works with Python 2). + * `PyGObject <https://wiki.gnome.org/action/show/Projects/PyGObject>`__ : + Python bindings for GTK 3.x and gobject-introspection libraries (also + compatible with Python 3). + +Testing +~~~~~~~ + +To run the test suite, you can do: + +.. code:: sh + + python setup.py test + +If you have `pytest <http://docs.pytest.org/en/latest/>`__, you can run +the ``pytest`` command directly. The tests will run against the +installed ``fontTools`` package, or the first one found in the +``PYTHONPATH``. + +You can also use `tox <https://testrun.org/tox/latest/>`__ to +automatically run tests on different Python versions in isolated virtual +environments. + +.. code:: sh + + pip install tox + tox + +Note that when you run ``tox`` without arguments, the tests are executed +for all the environments listed in tox.ini's ``envlist``. In our case, +this includes Python 2.7 and 3.6, so for this to work the ``python2.7`` +and ``python3.6`` executables must be available in your ``PATH``. + +You can specify an alternative environment list via the ``-e`` option, +or the ``TOXENV`` environment variable: + +.. code:: sh + + tox -e py27-nocov + TOXENV="py36-cov,htmlcov" tox + +Development Community +~~~~~~~~~~~~~~~~~~~~~ + +TTX/FontTools development is ongoing in an active community of +developers, that includes professional developers employed at major +software corporations and type foundries as well as hobbyists. + +Feature requests and bug reports are always welcome at +https://github.com/fonttools/fonttools/issues/ + +The best place for discussions about TTX from an end-user perspective as +well as TTX/FontTools development is the +https://groups.google.com/d/forum/fonttools mailing list. There is also +a development https://groups.google.com/d/forum/fonttools-dev mailing +list for continuous integration notifications. You can also email Behdad +privately at behdad@behdad.org + +History +~~~~~~~ + +The fontTools project was started by Just van Rossum in 1999, and was +maintained as an open source project at +http://sourceforge.net/projects/fonttools/. In 2008, Paul Wise (pabs3) +began helping Just with stability maintenance. In 2013 Behdad Esfahbod +began a friendly fork, thoroughly reviewing the codebase and making +changes at https://github.com/behdad/fonttools to add new features and +support for new font formats. + +Acknowledgements +~~~~~~~~~~~~~~~~ + +In alphabetical order: + +Olivier Berten, Samyak Bhuta, Erik van Blokland, Petr van Blokland, +Jelle Bosma, Sascha Brawer, Tom Byrer, Frédéric Coiffier, Vincent +Connare, Dave Crossland, Simon Daniels, Behdad Esfahbod, Behnam +Esfahbod, Hannes Famira, Sam Fishman, Matt Fontaine, Yannis Haralambous, +Greg Hitchcock, Jeremie Hornus, Khaled Hosny, John Hudson, Denis Moyogo +Jacquerye, Jack Jansen, Tom Kacvinsky, Jens Kutilek, Antoine Leca, +Werner Lemberg, Tal Leming, Peter Lofting, Cosimo Lupo, Masaya Nakamura, +Dave Opstad, Laurence Penney, Roozbeh Pournader, Garret Rieger, Read +Roberts, Guido van Rossum, Just van Rossum, Andreas Seidel, Georg +Seifert, Miguel Sousa, Adam Twardoch, Adrien Tétar, Vitaly Volkov, Paul +Wise. + +Copyrights +~~~~~~~~~~ + +| Copyright (c) 1999-2004 Just van Rossum, LettError + (just@letterror.com) +| See `LICENSE <LICENSE>`__ for the full license. + +Copyright (c) 2000 BeOpen.com. All Rights Reserved. + +Copyright (c) 1995-2001 Corporation for National Research Initiatives. +All Rights Reserved. + +Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam. All +Rights Reserved. + +Have fun! + +.. |Travis Build Status| image:: https://travis-ci.org/fonttools/fonttools.svg + :target: https://travis-ci.org/fonttools/fonttools +.. |Appveyor Build status| image:: https://ci.appveyor.com/api/projects/status/0f7fmee9as744sl7/branch/master?svg=true + :target: https://ci.appveyor.com/project/fonttools/fonttools/branch/master +.. |Health| image:: https://landscape.io/github/behdad/fonttools/master/landscape.svg?style=flat + :target: https://landscape.io/github/behdad/fonttools/master +.. |Coverage Status| image:: https://codecov.io/gh/fonttools/fonttools/branch/master/graph/badge.svg + :target: https://codecov.io/gh/fonttools/fonttools +.. |PyPI| image:: https://img.shields.io/pypi/v/fonttools.svg + :target: https://pypi.org/project/FontTools +.. |Gitter Chat| image:: https://badges.gitter.im/fonttools-dev/Lobby.svg + :alt: Join the chat at https://gitter.im/fonttools-dev/Lobby + :target: https://gitter.im/fonttools-dev/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge diff --git a/README.version b/README.version deleted file mode 100644 index 850e5ea..0000000 --- a/README.version +++ /dev/null @@ -1,3 +0,0 @@ -URL: https://pypi.python.org/packages/source/F/FontTools/FontTools-2.4.tar.gz#md5=304b20e6109787c0ff4467d2b9f7f2c5 -Version: 2.4 -BugComponent: 75970 diff --git a/Snippets/README.md b/Snippets/README.md new file mode 100644 index 0000000..915b1c3 --- /dev/null +++ b/Snippets/README.md @@ -0,0 +1,11 @@ +This directory includes snippets that people might useful to get ideas from. +The contents will come and go, don't rely on them being there or having a certain API. +If you need it, copy it and modify it. + +If you do and think your work is useful for others, please add a link to it here: + +* https://github.com/twardoch/fonttools-utils +* https://github.com/twardoch/ttfdiet +* https://github.com/googlei18n/nototools +* https://github.com/googlefonts/fontbakery +* https://github.com/Typefounding/setUseTypoMetricsFalse diff --git a/Snippets/checksum.py b/Snippets/checksum.py new file mode 100644 index 0000000..53a5318 --- /dev/null +++ b/Snippets/checksum.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import argparse +import hashlib +import os +import sys + +from os.path import basename + +from fontTools.ttLib import TTFont + + +def write_checksum(filepaths, stdout_write=False, use_ttx=False, include_tables=None, exclude_tables=None, do_not_cleanup=False): + checksum_dict = {} + for path in filepaths: + if not os.path.exists(path): + sys.stderr.write("[checksum.py] ERROR: " + path + " is not a valid file path" + os.linesep) + sys.exit(1) + + if use_ttx: + # append a .ttx extension to existing extension to maintain data about the binary that + # was used to generate the .ttx XML dump. This creates unique checksum path values for + # paths that would otherwise not be unique with a file extension replacement with .ttx + # An example is woff and woff2 web font files that share the same base file name: + # + # coolfont-regular.woff ==> coolfont-regular.ttx + # coolfont-regular.woff2 ==> coolfont-regular.ttx (KAPOW! checksum data lost as this would overwrite dict value) + temp_ttx_path = path + ".ttx" + + tt = TTFont(path) + # important to keep the newlinestr value defined here as hash values will change across platforms + # if platform specific newline values are assumed + tt.saveXML(temp_ttx_path, newlinestr="\n", skipTables=exclude_tables, tables=include_tables) + checksum_path = temp_ttx_path + else: + if include_tables is not None: + sys.stderr.write("[checksum.py] -i and --include are not supported for font binary filepaths. \ + Use these flags for checksums with the --ttx flag.") + sys.exit(1) + if exclude_tables is not None: + sys.stderr.write("[checksum.py] -e and --exclude are not supported for font binary filepaths. \ + Use these flags for checksums with the --ttx flag.") + sys.exit(1) + checksum_path = path + + file_contents = _read_binary(checksum_path) + + # store SHA1 hash data and associated file path basename in the checksum_dict dictionary + checksum_dict[basename(checksum_path)] = hashlib.sha1(file_contents).hexdigest() + + # remove temp ttx files when present + if use_ttx and do_not_cleanup is False: + os.remove(temp_ttx_path) + + # generate the checksum list string for writes + checksum_out_data = "" + for key in checksum_dict.keys(): + checksum_out_data += checksum_dict[key] + " " + key + "\n" + + # write to stdout stream or file based upon user request (default = file write) + if stdout_write: + sys.stdout.write(checksum_out_data) + else: + checksum_report_filepath = "checksum.txt" + with open(checksum_report_filepath, "w") as file: + file.write(checksum_out_data) + + +def check_checksum(filepaths): + check_failed = False + for path in filepaths: + if not os.path.exists(path): + sys.stderr.write("[checksum.py] ERROR: " + path + " is not a valid filepath" + os.linesep) + sys.exit(1) + + with open(path, mode='r') as file: + for line in file.readlines(): + cleaned_line = line.rstrip() + line_list = cleaned_line.split(" ") + # eliminate empty strings parsed from > 1 space characters + line_list = list(filter(None, line_list)) + if len(line_list) == 2: + expected_sha1 = line_list[0] + test_path = line_list[1] + else: + sys.stderr.write("[checksum.py] ERROR: failed to parse checksum file values" + os.linesep) + sys.exit(1) + + if not os.path.exists(test_path): + print(test_path + ": Filepath is not valid, ignored") + else: + file_contents = _read_binary(test_path) + observed_sha1 = hashlib.sha1(file_contents).hexdigest() + if observed_sha1 == expected_sha1: + print(test_path + ": OK") + else: + print("-" * 80) + print(test_path + ": === FAIL ===") + print("Expected vs. Observed:") + print(expected_sha1) + print(observed_sha1) + print("-" * 80) + check_failed = True + + # exit with status code 1 if any fails detected across all tests in the check + if check_failed is True: + sys.exit(1) + + +def _read_binary(filepath): + with open(filepath, mode='rb') as file: + return file.read() + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(prog="checksum.py", description="A SHA1 hash checksum list generator and checksum testing script") + parser.add_argument("-t", "--ttx", help="Calculate from ttx file", action="store_true") + parser.add_argument("-s", "--stdout", help="Write output to stdout stream", action="store_true") + parser.add_argument("-n", "--noclean", help="Do not discard *.ttx files used to calculate SHA1 hashes", action="store_true") + parser.add_argument("-c", "--check", help="Verify checksum values vs. files", action="store_true") + parser.add_argument("filepaths", nargs="+", help="One or more file paths. Use checksum file path for -c/--check. Use paths\ + to font files for all other commands.") + + parser.add_argument("-i", "--include", action="append", help="Included OpenType tables for ttx data dump") + parser.add_argument("-e", "--exclude", action="append", help="Excluded OpenType tables for ttx data dump") + + args = parser.parse_args(sys.argv[1:]) + + if args.check is True: + check_checksum(args.filepaths) + else: + write_checksum(args.filepaths, stdout_write=args.stdout, use_ttx=args.ttx, do_not_cleanup=args.noclean, include_tables=args.include, exclude_tables=args.exclude) diff --git a/Snippets/cmap-format.py b/Snippets/cmap-format.py new file mode 100755 index 0000000..0cee39c --- /dev/null +++ b/Snippets/cmap-format.py @@ -0,0 +1,40 @@ +#! /usr/bin/env python + +# Sample script to convert legacy cmap subtables to format-4 +# subtables. Note that this is rarely what one needs. You +# probably need to just drop the legacy subtables if the font +# already has a format-4 subtable. +# +# Other times, you would need to convert a non-Unicode cmap +# legacy subtable to a Unicode one. In those cases, use the +# getEncoding() of subtable and use that encoding to map the +# characters to Unicode... TODO: Extend this script to do that. + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.ttLib import TTFont +from fontTools.ttLib.tables._c_m_a_p import CmapSubtable +import sys + +if len(sys.argv) != 3: + print("usage: cmap-format.py fontfile.ttf outfile.ttf") + sys.exit(1) +fontfile = sys.argv[1] +outfile = sys.argv[2] +font = TTFont(fontfile) + +cmap = font['cmap'] +outtables = [] +for table in cmap.tables: + if table.format in [4, 12, 13, 14]: + outtables.append(table) + # Convert ot format4 + newtable = CmapSubtable.newSubtable(4) + newtable.platformID = table.platformID + newtable.platEncID = table.platEncID + newtable.language = table.language + newtable.cmap = table.cmap + outtables.append(newtable) +cmap.tables = outtables + +font.save(outfile) diff --git a/Snippets/dump_woff_metadata.py b/Snippets/dump_woff_metadata.py new file mode 100644 index 0000000..0023a10 --- /dev/null +++ b/Snippets/dump_woff_metadata.py @@ -0,0 +1,33 @@ +from __future__ import print_function +import sys +from fontTools.ttx import makeOutputFileName +from fontTools.ttLib import TTFont + + +def main(args=None): + if args is None: + args = sys.argv[1:] + + if len(args) < 1: + print("usage: dump_woff_metadata.py " + "INPUT.woff [OUTPUT.xml]", file=sys.stderr) + return 1 + + infile = args[0] + if len(args) > 1: + outfile = args[1] + else: + outfile = makeOutputFileName(infile, None, ".xml") + + font = TTFont(infile) + + if not font.flavorData or not font.flavorData.metaData: + print("No WOFF metadata") + return 1 + + with open(outfile, "wb") as f: + f.write(font.flavorData.metaData) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Snippets/edit_raw_table_data.py b/Snippets/edit_raw_table_data.py new file mode 100644 index 0000000..89326fe --- /dev/null +++ b/Snippets/edit_raw_table_data.py @@ -0,0 +1,31 @@ +from fontTools.ttLib import TTFont +from fontTools.ttLib.tables.DefaultTable import DefaultTable + +font_path = "myfont.ttf" +output_path = "myfont_patched.ttf" + +table_tag = "DSIG" + + +# Get raw table data from the source font + +font = TTFont(font_path) +raw_data = font.getTableData(table_tag) + + +# Do something with the raw table data +# This example just sets an empty DSIG table. + +raw_data = b"\0\0\0\1\0\0\0\0" + + +# Write the data back to the font + +# We could re-use the existing table when the source and target font are +# identical, but let's make a new empty table to be more universal. +table = DefaultTable(table_tag) +table.data = raw_data + +# Add the new table back into the source font and save under a new name. +font[table_tag] = table +font.save(output_path) diff --git a/Snippets/fix-dflt-langsys.py b/Snippets/fix-dflt-langsys.py new file mode 100644 index 0000000..d8eccb4 --- /dev/null +++ b/Snippets/fix-dflt-langsys.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python + +import argparse +import logging +import os +import sys + +from fontTools.ttLib import TTFont + + +def ProcessTable(table): + found = set() + + for rec in table.ScriptList.ScriptRecord: + if rec.ScriptTag == "DFLT" and rec.Script.LangSysCount != 0: + tags = [r.LangSysTag for r in rec.Script.LangSysRecord] + logging.info("Removing %d extraneous LangSys records: %s", + rec.Script.LangSysCount, " ".join(tags)) + rec.Script.LangSysRecord = [] + rec.Script.LangSysCount = 0 + found.update(tags) + + if not found: + logging.info("All fine") + return False + else: + for rec in table.ScriptList.ScriptRecord: + tags = set([r.LangSysTag for r in rec.Script.LangSysRecord]) + found -= tags + + if found: + logging.warning("Records are missing from non-DFLT scripts: %s", + " ".join(found)) + return True + + +def ProcessFont(font): + found = False + for tag in ("GSUB", "GPOS"): + if tag in font: + logging.info("Processing %s table", tag) + if ProcessTable(font[tag].table): + found = True + else: + # Unmark the table as loaded so that it is read from disk when + # writing the font, to avoid any unnecessary changes caused by + # decompiling then recompiling again. + del font.tables[tag] + + return found + + +def ProcessFiles(filenames): + for filename in filenames: + logging.info("Processing %s", filename) + font = TTFont(filename) + name, ext = os.path.splitext(filename) + fixedname = name + ".fixed" + ext + if ProcessFont(font): + logging.info("Saving fixed font to %s\n", fixedname) + font.save(fixedname) + else: + logging.info("Font file is fine, nothing to fix\n") + + +def main(): + parser = argparse.ArgumentParser( + description="Fix LangSys records for DFLT script") + parser.add_argument("files", metavar="FILE", type=str, nargs="+", + help="input font to process") + parser.add_argument("-s", "--silent", action='store_true', + help="suppress normal messages") + + args = parser.parse_args() + + logformat = "%(levelname)s: %(message)s" + if args.silent: + logging.basicConfig(format=logformat, level=logging.DEBUG) + else: + logging.basicConfig(format=logformat, level=logging.INFO) + + ProcessFiles(args.files) + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Snippets/interpolate.py b/Snippets/interpolate.py new file mode 100755 index 0000000..7ed822d --- /dev/null +++ b/Snippets/interpolate.py @@ -0,0 +1,144 @@ +#! /usr/bin/env python + +# Illustrates how a fonttools script can construct variable fonts. +# +# This script reads Roboto-Thin.ttf, Roboto-Regular.ttf, and +# Roboto-Black.ttf from /tmp/Roboto, and writes a Multiple Master GX +# font named "Roboto.ttf" into the current working directory. +# This output font supports interpolation along the Weight axis, +# and it contains named instances for "Thin", "Light", "Regular", +# "Bold", and "Black". +# +# All input fonts must contain the same set of glyphs, and these glyphs +# need to have the same control points in the same order. Note that this +# is *not* the case for the normal Roboto fonts that can be downloaded +# from Google. This demo script prints a warning for any problematic +# glyphs; in the resulting font, these glyphs will not be interpolated +# and get rendered in the "Regular" weight. +# +# Usage: +# $ mkdir /tmp/Roboto && cp Roboto-*.ttf /tmp/Roboto +# $ ./interpolate.py && open Roboto.ttf + + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.ttLib import TTFont +from fontTools.ttLib.tables._n_a_m_e import NameRecord +from fontTools.ttLib.tables._f_v_a_r import table__f_v_a_r, Axis, NamedInstance +from fontTools.ttLib.tables._g_v_a_r import table__g_v_a_r, TupleVariation +import logging + + +def AddFontVariations(font): + assert "fvar" not in font + fvar = font["fvar"] = table__f_v_a_r() + + weight = Axis() + weight.axisTag = "wght" + weight.nameID = AddName(font, "Weight").nameID + weight.minValue, weight.defaultValue, weight.maxValue = (100, 400, 900) + fvar.axes.append(weight) + + # https://www.microsoft.com/typography/otspec/os2.htm#wtc + for name, wght in ( + ("Thin", 100), + ("Light", 300), + ("Regular", 400), + ("Bold", 700), + ("Black", 900)): + inst = NamedInstance() + inst.nameID = AddName(font, name).nameID + inst.coordinates = {"wght": wght} + fvar.instances.append(inst) + + +def AddName(font, name): + """(font, "Bold") --> NameRecord""" + nameTable = font.get("name") + namerec = NameRecord() + namerec.nameID = 1 + max([n.nameID for n in nameTable.names] + [256]) + namerec.string = name.encode("mac_roman") + namerec.platformID, namerec.platEncID, namerec.langID = (1, 0, 0) + nameTable.names.append(namerec) + return namerec + + +def AddGlyphVariations(font, thin, regular, black): + assert "gvar" not in font + gvar = font["gvar"] = table__g_v_a_r() + gvar.version = 1 + gvar.reserved = 0 + gvar.variations = {} + for glyphName in regular.getGlyphOrder(): + regularCoord = GetCoordinates(regular, glyphName) + thinCoord = GetCoordinates(thin, glyphName) + blackCoord = GetCoordinates(black, glyphName) + if not regularCoord or not blackCoord or not thinCoord: + logging.warning("glyph %s not present in all input fonts", + glyphName) + continue + if (len(regularCoord) != len(blackCoord) or + len(regularCoord) != len(thinCoord)): + logging.warning("glyph %s has not the same number of " + "control points in all input fonts", glyphName) + continue + thinDelta = [] + blackDelta = [] + for ((regX, regY), (blackX, blackY), (thinX, thinY)) in \ + zip(regularCoord, blackCoord, thinCoord): + thinDelta.append(((thinX - regX, thinY - regY))) + blackDelta.append((blackX - regX, blackY - regY)) + thinVar = TupleVariation({"wght": (-1.0, -1.0, 0.0)}, thinDelta) + blackVar = TupleVariation({"wght": (0.0, 1.0, 1.0)}, blackDelta) + gvar.variations[glyphName] = [thinVar, blackVar] + + +def GetCoordinates(font, glyphName): + """font, glyphName --> glyph coordinates as expected by "gvar" table + + The result includes four "phantom points" for the glyph metrics, + as mandated by the "gvar" spec. + """ + glyphTable = font["glyf"] + glyph = glyphTable.glyphs.get(glyphName) + if glyph is None: + return None + glyph.expand(glyphTable) + glyph.recalcBounds(glyphTable) + if glyph.isComposite(): + coord = [c.getComponentInfo()[1][-2:] for c in glyph.components] + else: + coord = [c for c in glyph.getCoordinates(glyphTable)[0]] + # Add phantom points for (left, right, top, bottom) positions. + horizontalAdvanceWidth, leftSideBearing = font["hmtx"].metrics[glyphName] + + + leftSideX = glyph.xMin - leftSideBearing + rightSideX = leftSideX + horizontalAdvanceWidth + + # XXX these are incorrect. Load vmtx and fix. + topSideY = glyph.yMax + bottomSideY = -glyph.yMin + + coord.extend([(leftSideX, 0), + (rightSideX, 0), + (0, topSideY), + (0, bottomSideY)]) + return coord + + +def main(): + logging.basicConfig(format="%(levelname)s: %(message)s") + thin = TTFont("/tmp/Roboto/Roboto-Thin.ttf") + regular = TTFont("/tmp/Roboto/Roboto-Regular.ttf") + black = TTFont("/tmp/Roboto/Roboto-Black.ttf") + out = regular + AddFontVariations(out) + AddGlyphVariations(out, thin, regular, black) + out.save("./Roboto.ttf") + + +if __name__ == "__main__": + import sys + sys.exit(main()) diff --git a/Snippets/layout-features.py b/Snippets/layout-features.py new file mode 100755 index 0000000..25522cd --- /dev/null +++ b/Snippets/layout-features.py @@ -0,0 +1,51 @@ +#! /usr/bin/env python + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.ttLib import TTFont +from fontTools.ttLib.tables import otTables +import sys + +if len(sys.argv) != 2: + print("usage: layout-features.py fontfile.ttf") + sys.exit(1) +fontfile = sys.argv[1] +if fontfile.rsplit(".", 1)[-1] == "ttx": + font = TTFont() + font.importXML(fontfile) +else: + font = TTFont(fontfile) + +for tag in ('GSUB', 'GPOS'): + if not tag in font: continue + print("Table:", tag) + table = font[tag].table + if not table.ScriptList or not table.FeatureList: continue + featureRecords = table.FeatureList.FeatureRecord + for script in table.ScriptList.ScriptRecord: + print(" Script:", script.ScriptTag) + if not script.Script: + print (" Null script.") + continue + languages = list(script.Script.LangSysRecord) + if script.Script.DefaultLangSys: + defaultlangsys = otTables.LangSysRecord() + defaultlangsys.LangSysTag = "default" + defaultlangsys.LangSys = script.Script.DefaultLangSys + languages.insert(0, defaultlangsys) + for langsys in languages: + print(" Language:", langsys.LangSysTag) + if not langsys.LangSys: + print (" Null language.") + continue + features = [featureRecords[index] for index in langsys.LangSys.FeatureIndex] + if langsys.LangSys.ReqFeatureIndex != 0xFFFF: + record = featureRecords[langsys.LangSys.ReqFeatureIndex] + requiredfeature = otTables.FeatureRecord() + requiredfeature.FeatureTag = 'required(%s)' % record.FeatureTag + requiredfeature.Feature = record.Feature + features.insert(0, requiredfeature) + for feature in features: + print(" Feature:", feature.FeatureTag) + lookups = feature.Feature.LookupListIndex + print(" Lookups:", ','.join(str(l) for l in lookups)) diff --git a/Snippets/merge_woff_metadata.py b/Snippets/merge_woff_metadata.py new file mode 100644 index 0000000..669de46 --- /dev/null +++ b/Snippets/merge_woff_metadata.py @@ -0,0 +1,43 @@ +from __future__ import print_function +import sys +import os +from fontTools.ttx import makeOutputFileName +from fontTools.ttLib import TTFont + + +def main(args=None): + if args is None: + args = sys.argv[1:] + + if len(args) < 2: + print("usage: merge_woff_metadata.py METADATA.xml " + "INPUT.woff [OUTPUT.woff]", file=sys.stderr) + return 1 + + metadata_file = args[0] + with open(metadata_file, 'rb') as f: + metadata = f.read() + + infile = args[1] + if len(args) > 2: + outfile = args[2] + else: + filename, ext = os.path.splitext(infile) + outfile = makeOutputFileName(filename, None, ext) + + font = TTFont(infile) + + if font.flavor not in ("woff", "woff2"): + print("Input file is not a WOFF or WOFF2 font", file=sys.stderr) + return 1 + + data = font.flavorData + + # this sets the new WOFF metadata + data.metaData = metadata + + font.save(outfile) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Snippets/otf2ttf.py b/Snippets/otf2ttf.py new file mode 100755 index 0000000..60acd54 --- /dev/null +++ b/Snippets/otf2ttf.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +from __future__ import print_function, division, absolute_import +import sys +from fontTools.ttLib import TTFont, newTable +from cu2qu.pens import Cu2QuPen +from fontTools.pens.ttGlyphPen import TTGlyphPen +from fontTools.ttx import makeOutputFileName +import argparse + + +# default approximation error, measured in UPEM +MAX_ERR = 1.0 + +# default 'post' table format +POST_FORMAT = 2.0 + +# assuming the input contours' direction is correctly set (counter-clockwise), +# we just flip it to clockwise +REVERSE_DIRECTION = True + + +def glyphs_to_quadratic( + glyphs, max_err=MAX_ERR, reverse_direction=REVERSE_DIRECTION): + quadGlyphs = {} + for gname in glyphs.keys(): + glyph = glyphs[gname] + ttPen = TTGlyphPen(glyphs) + cu2quPen = Cu2QuPen(ttPen, max_err, + reverse_direction=reverse_direction) + glyph.draw(cu2quPen) + quadGlyphs[gname] = ttPen.glyph() + return quadGlyphs + + +def otf_to_ttf(ttFont, post_format=POST_FORMAT, **kwargs): + assert ttFont.sfntVersion == "OTTO" + assert "CFF " in ttFont + + glyphOrder = ttFont.getGlyphOrder() + + ttFont["loca"] = newTable("loca") + ttFont["glyf"] = glyf = newTable("glyf") + glyf.glyphOrder = glyphOrder + glyf.glyphs = glyphs_to_quadratic(ttFont.getGlyphSet(), **kwargs) + del ttFont["CFF "] + + ttFont["maxp"] = maxp = newTable("maxp") + maxp.tableVersion = 0x00010000 + maxp.maxZones = 1 + maxp.maxTwilightPoints = 0 + maxp.maxStorage = 0 + maxp.maxFunctionDefs = 0 + maxp.maxInstructionDefs = 0 + maxp.maxStackElements = 0 + maxp.maxSizeOfInstructions = 0 + maxp.maxComponentElements = max( + len(g.components if hasattr(g, 'components') else []) + for g in glyf.glyphs.values()) + + post = ttFont["post"] + post.formatType = post_format + post.extraNames = [] + post.mapping = {} + post.glyphOrder = glyphOrder + + ttFont.sfntVersion = "\000\001\000\000" + + +def main(args=None): + parser = argparse.ArgumentParser() + parser.add_argument("input", metavar="INPUT") + parser.add_argument("-o", "--output") + parser.add_argument("-e", "--max-error", type=float, default=MAX_ERR) + parser.add_argument("--post-format", type=float, default=POST_FORMAT) + parser.add_argument( + "--keep-direction", dest='reverse_direction', action='store_false') + options = parser.parse_args(args) + + output = options.output or makeOutputFileName(options.input, + outputDir=None, + extension='.ttf') + font = TTFont(options.input) + otf_to_ttf(font, + post_format=options.post_format, + max_err=options.max_error, + reverse_direction=options.reverse_direction) + font.save(output) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Snippets/rename-fonts.py b/Snippets/rename-fonts.py new file mode 100755 index 0000000..ddfce10 --- /dev/null +++ b/Snippets/rename-fonts.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python +"""Script to add a suffix to all family names in the input font's `name` table, +and to optionally rename the output files with the given suffix. + +The current family name substring is searched in the nameIDs 1, 3, 4, 6, 16, +and 21, and if found the suffix is inserted after it; or else the suffix is +appended at the end. +""" +from __future__ import print_function, absolute_import, unicode_literals +import os +import argparse +import logging +from fontTools.ttLib import TTFont +from fontTools.misc.cliTools import makeOutputFileName + + +logger = logging.getLogger() + +WINDOWS_ENGLISH_IDS = 3, 1, 0x409 +MAC_ROMAN_IDS = 1, 0, 0 + +FAMILY_RELATED_IDS = dict( + LEGACY_FAMILY=1, + TRUETYPE_UNIQUE_ID=3, + FULL_NAME=4, + POSTSCRIPT_NAME=6, + PREFERRED_FAMILY=16, + WWS_FAMILY=21, +) + + +def get_current_family_name(table): + family_name_rec = None + for plat_id, enc_id, lang_id in (WINDOWS_ENGLISH_IDS, MAC_ROMAN_IDS): + for name_id in ( + FAMILY_RELATED_IDS["PREFERRED_FAMILY"], + FAMILY_RELATED_IDS["LEGACY_FAMILY"], + ): + family_name_rec = table.getName( + nameID=name_id, + platformID=plat_id, + platEncID=enc_id, + langID=lang_id, + ) + if family_name_rec is not None: + break + if family_name_rec is not None: + break + if not family_name_rec: + raise ValueError("family name not found; can't add suffix") + return family_name_rec.toUnicode() + + +def insert_suffix(string, family_name, suffix): + # check whether family_name is a substring + start = string.find(family_name) + if start != -1: + # insert suffix after the family_name substring + end = start + len(family_name) + new_string = string[:end] + suffix + string[end:] + else: + # it's not, we just append the suffix at the end + new_string = string + suffix + return new_string + + +def rename_record(name_record, family_name, suffix): + string = name_record.toUnicode() + new_string = insert_suffix(string, family_name, suffix) + name_record.string = new_string + return string, new_string + + +def rename_file(filename, family_name, suffix): + filename, ext = os.path.splitext(filename) + ps_name = family_name.replace(" ", "") + if ps_name in filename: + ps_suffix = suffix.replace(" ", "") + return insert_suffix(filename, ps_name, ps_suffix) + ext + else: + return insert_suffix(filename, family_name, suffix) + ext + + +def add_family_suffix(font, suffix): + table = font["name"] + + family_name = get_current_family_name(table) + logger.info(" Current family name: '%s'", family_name) + + # postcript name can't contain spaces + ps_family_name = family_name.replace(" ", "") + ps_suffix = suffix.replace(" ", "") + for rec in table.names: + name_id = rec.nameID + if name_id not in FAMILY_RELATED_IDS.values(): + continue + if name_id == FAMILY_RELATED_IDS["POSTSCRIPT_NAME"]: + old, new = rename_record(rec, ps_family_name, ps_suffix) + elif name_id == FAMILY_RELATED_IDS["TRUETYPE_UNIQUE_ID"]: + # The Truetype Unique ID rec may contain either the PostScript + # Name or the Full Name string, so we try both + if ps_family_name in rec.toUnicode(): + old, new = rename_record(rec, ps_family_name, ps_suffix) + else: + old, new = rename_record(rec, family_name, suffix) + else: + old, new = rename_record(rec, family_name, suffix) + logger.info(" %r: '%s' -> '%s'", rec, old, new) + + return family_name + + +def main(args=None): + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("-s", "--suffix", required=True) + parser.add_argument("input_fonts", metavar="FONTFILE", nargs="+") + output_group = parser.add_mutually_exclusive_group() + output_group.add_argument("-i", "--inplace", action="store_true") + output_group.add_argument("-d", "--output-dir") + output_group.add_argument("-o", "--output-file") + parser.add_argument("-R", "--rename-files", action="store_true") + parser.add_argument("-v", "--verbose", action="count", default=0) + options = parser.parse_args(args) + + if not options.verbose: + level = "WARNING" + elif options.verbose == 1: + level = "INFO" + else: + level = "DEBUG" + logging.basicConfig(level=level, format="%(message)s") + + if options.output_file and len(options.input_fonts) > 1: + parser.error( + "argument -o/--output-file can't be used with multiple inputs" + ) + if options.rename_files and (options.inplace or options.output_file): + parser.error("argument -R not allowed with arguments -i or -o") + + for input_name in options.input_fonts: + logger.info("Renaming font: '%s'", input_name) + + font = TTFont(input_name) + family_name = add_family_suffix(font, options.suffix) + + if options.inplace: + output_name = input_name + elif options.output_file: + output_name = options.output_file + else: + if options.rename_files: + input_name = rename_file( + input_name, family_name, options.suffix + ) + output_name = makeOutputFileName(input_name, options.output_dir) + + font.save(output_name) + logger.info("Saved font: '%s'", output_name) + + font.close() + del font + + logger.info("Done!") + + +if __name__ == "__main__": + main() diff --git a/Snippets/subset-fpgm.py b/Snippets/subset-fpgm.py new file mode 100755 index 0000000..c20c05f --- /dev/null +++ b/Snippets/subset-fpgm.py @@ -0,0 +1,60 @@ +#! /usr/bin/env python + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.ttLib import TTFont +import sys + +if len(sys.argv) < 2: + print("usage: subset-fpgm.py fontfile.ttf func-number...") + sys.exit(1) +fontfile = sys.argv[1] +func_nums = [int(x) for x in sys.argv[2:]] + +font = TTFont(fontfile) +fpgm = font['fpgm'] + +# Parse fpgm +asm = fpgm.program.getAssembly() +funcs = {} +stack = [] +tokens = iter(asm) +for token in tokens: + if token.startswith("PUSH") or token.startswith("NPUSH"): + for token in tokens: + try: + num = int(token) + stack.append(num) + except ValueError: + break + if token.startswith("FDEF"): + num = stack.pop() + body = [] + for token in tokens: + if token.startswith("ENDF"): + break + body.append(token) + funcs[num] = body + continue + assert 0, "Unexpected token in fpgm: %s" % token + +# Subset! +funcs = {i:funcs[i] for i in func_nums} + +# Put it back together: +asm = [] +if funcs: + asm.append("PUSH[ ]") +nums = sorted(funcs.keys()) +asm.extend(str(i) for i in nums) +for i in nums: + asm.append("FDEF[ ]") + asm.extend(funcs[i]) + asm.append("ENDF[ ]") + +import pprint +pprint.pprint(asm) + +fpgm.program.fromAssembly(asm) +# Make sure it compiles +fpgm.program.getBytecode() diff --git a/Snippets/svg2glif.py b/Snippets/svg2glif.py new file mode 100755 index 0000000..2dd6402 --- /dev/null +++ b/Snippets/svg2glif.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python +""" Convert SVG paths to UFO glyphs. """ + +from __future__ import print_function, absolute_import + +__requires__ = ["FontTools", "ufoLib"] + +from fontTools.misc.py23 import SimpleNamespace +from fontTools.svgLib import SVGPath + +from ufoLib.pointPen import SegmentToPointPen +from ufoLib.glifLib import writeGlyphToString + + +__all__ = ["svg2glif"] + + +def svg2glif(svg, name, width=0, height=0, unicodes=None, transform=None, + version=2): + """ Convert an SVG outline to a UFO glyph with given 'name', advance + 'width' and 'height' (int), and 'unicodes' (list of int). + Return the resulting string in GLIF format (default: version 2). + If 'transform' is provided, apply a transformation matrix before the + conversion (must be tuple of 6 floats, or a FontTools Transform object). + """ + glyph = SimpleNamespace(width=width, height=height, unicodes=unicodes) + outline = SVGPath.fromstring(svg, transform=transform) + + # writeGlyphToString takes a callable (usually a glyph's drawPoints + # method) that accepts a PointPen, however SVGPath currently only has + # a draw method that accepts a segment pen. We need to wrap the call + # with a converter pen. + def drawPoints(pointPen): + pen = SegmentToPointPen(pointPen) + outline.draw(pen) + + return writeGlyphToString(name, + glyphObject=glyph, + drawPointsFunc=drawPoints, + formatVersion=version) + + +def parse_args(args): + import argparse + + def split(arg): + return arg.replace(",", " ").split() + + def unicode_hex_list(arg): + try: + return [int(unihex, 16) for unihex in split(arg)] + except ValueError: + msg = "Invalid unicode hexadecimal value: %r" % arg + raise argparse.ArgumentTypeError(msg) + + def transform_list(arg): + try: + return [float(n) for n in split(arg)] + except ValueError: + msg = "Invalid transformation matrix: %r" % arg + raise argparse.ArgumentTypeError(msg) + + parser = argparse.ArgumentParser( + description="Convert SVG outlines to UFO glyphs (.glif)") + parser.add_argument( + "infile", metavar="INPUT.svg", help="Input SVG file containing " + '<path> elements with "d" attributes.') + parser.add_argument( + "outfile", metavar="OUTPUT.glif", help="Output GLIF file (default: " + "print to stdout)", nargs='?') + parser.add_argument( + "-n", "--name", help="The glyph name (default: input SVG file " + "basename, without the .svg extension)") + parser.add_argument( + "-w", "--width", help="The glyph advance width (default: 0)", + type=int, default=0) + parser.add_argument( + "-H", "--height", help="The glyph vertical advance (optional if " + '"width" is defined)', type=int, default=0) + parser.add_argument( + "-u", "--unicodes", help="List of Unicode code points as hexadecimal " + 'numbers (e.g. -u "0041 0042")', + type=unicode_hex_list) + parser.add_argument( + "-t", "--transform", help="Transformation matrix as a list of six " + 'float values (e.g. -t "0.1 0 0 -0.1 -50 200")', type=transform_list) + parser.add_argument( + "-f", "--format", help="UFO GLIF format version (default: 2)", + type=int, choices=(1, 2), default=2) + + return parser.parse_args(args) + + +def main(args=None): + from io import open + + options = parse_args(args) + + svg_file = options.infile + + if options.name: + name = options.name + else: + import os + name = os.path.splitext(os.path.basename(svg_file))[0] + + with open(svg_file, "r", encoding="utf-8") as f: + svg = f.read() + + glif = svg2glif(svg, name, + width=options.width, + height=options.height, + unicodes=options.unicodes, + transform=options.transform, + version=options.format) + + if options.outfile is None: + print(glif) + else: + with open(options.outfile, 'w', encoding='utf-8') as f: + f.write(glif) + + +if __name__ == "__main__": + import sys + sys.exit(main()) diff --git a/Snippets/woff2_compress.py b/Snippets/woff2_compress.py new file mode 100755 index 0000000..689ebdc --- /dev/null +++ b/Snippets/woff2_compress.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.ttLib import TTFont +from fontTools.ttx import makeOutputFileName +import sys +import os + + +def main(args=None): + if args is None: + args = sys.argv[1:] + if len(args) < 1: + print("One argument, the input filename, must be provided.", file=sys.stderr) + return 1 + + filename = args[0] + outfilename = makeOutputFileName(filename, outputDir=None, extension='.woff2') + + print("Processing %s => %s" % (filename, outfilename)) + + font = TTFont(filename, recalcBBoxes=False, recalcTimestamp=False) + font.flavor = "woff2" + font.save(outfilename, reorderTables=False) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/Snippets/woff2_decompress.py b/Snippets/woff2_decompress.py new file mode 100755 index 0000000..e7c1bea --- /dev/null +++ b/Snippets/woff2_decompress.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python + +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.ttLib import TTFont +from fontTools.ttx import makeOutputFileName +import sys +import os + + +def make_output_name(filename): + with open(filename, "rb") as f: + f.seek(4) + sfntVersion = f.read(4) + assert len(sfntVersion) == 4, "not enough data" + ext = '.ttf' if sfntVersion == b"\x00\x01\x00\x00" else ".otf" + outfilename = makeOutputFileName(filename, outputDir=None, extension=ext) + return outfilename + + +def main(args=None): + if args is None: + args = sys.argv[1:] + if len(args) < 1: + print("One argument, the input filename, must be provided.", file=sys.stderr) + return 1 + + filename = args[0] + outfilename = make_output_name(filename) + + print("Processing %s => %s" % (filename, outfilename)) + + font = TTFont(filename, recalcBBoxes=False, recalcTimestamp=False) + font.flavor = None + font.save(outfilename, reorderTables=True) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/Tests/afmLib/afmLib_test.py b/Tests/afmLib/afmLib_test.py new file mode 100644 index 0000000..97fcf8d --- /dev/null +++ b/Tests/afmLib/afmLib_test.py @@ -0,0 +1,55 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +import unittest +import os +from fontTools import afmLib + + +CWD = os.path.abspath(os.path.dirname(__file__)) +DATADIR = os.path.join(CWD, 'data') +AFM = os.path.join(DATADIR, 'TestAFM.afm') + + +class AFMTest(unittest.TestCase): + + def test_read_afm(self): + afm = afmLib.AFM(AFM) + self.assertEqual(sorted(afm.kernpairs()), + sorted([('V', 'A'), ('T', 'comma'), ('V', 'd'), ('T', 'c'), ('T', 'period')])) + self.assertEqual(afm['V', 'A'], -60) + self.assertEqual(afm['V', 'd'], 30) + self.assertEqual(afm['A'], (65, 668, (8, -25, 660, 666))) + + def test_write_afm(self): + afm = afmLib.AFM(AFM) + newAfm, afmData = self.write(afm) + self.assertEqual(afm.kernpairs(), newAfm.kernpairs()) + self.assertEqual(afm.chars(), newAfm.chars()) + self.assertEqual(afm.comments(), newAfm.comments()[1:]) # skip the "generated by afmLib" comment + for pair in afm.kernpairs(): + self.assertEqual(afm[pair], newAfm[pair]) + for char in afm.chars(): + self.assertEqual(afm[char], newAfm[char]) + with open(AFM, 'r') as f: + originalLines = f.read().splitlines() + newLines = afmData.splitlines() + del newLines[1] # remove the "generated by afmLib" comment + self.assertEqual(originalLines, newLines) + + @staticmethod + def write(afm, sep='\r'): + temp = os.path.join(DATADIR, 'temp.afm') + try: + afm.write(temp, sep) + with open(temp, 'r') as f: + afmData = f.read() + afm = afmLib.AFM(temp) + finally: + if os.path.exists(temp): + os.remove(temp) + return afm, afmData + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/afmLib/data/TestAFM.afm b/Tests/afmLib/data/TestAFM.afm new file mode 100644 index 0000000..634c66a --- /dev/null +++ b/Tests/afmLib/data/TestAFM.afm @@ -0,0 +1,37 @@ +StartFontMetrics 2.0 +Comment UniqueID 2123703 +Comment Panose 2 0 6 3 3 0 0 2 0 4 +FontName TestFont-Regular +FullName TestFont-Regular +FamilyName TestFont +Weight Regular +ItalicAngle 0.00 +IsFixedPitch false +FontBBox -94 -317 1316 1009 +UnderlinePosition -296 +UnderlineThickness 111 +Version 001.000 +Notice [c] Copyright 2017. All Rights Reserved. +EncodingScheme FontSpecific +CapHeight 700 +XHeight 500 +Ascender 750 +Descender -250 +StdHW 181 +StdVW 194 +StartCharMetrics 4 +C 32 ; WX 200 ; N space ; B 0 0 0 0 ; +C 65 ; WX 668 ; N A ; B 8 -25 660 666 ; +C 66 ; WX 543 ; N B ; B 36 0 522 666 ; +C 67 ; WX 582 ; N C ; B 24 -21 564 687 ; +EndCharMetrics +StartKernData +StartKernPairs 5 +KPX T c 30 +KPX T comma -100 +KPX T period -100 +KPX V A -60 +KPX V d 30 +EndKernPairs +EndKernData +EndFontMetrics diff --git a/Tests/agl_test.py b/Tests/agl_test.py new file mode 100644 index 0000000..8754e6a --- /dev/null +++ b/Tests/agl_test.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +from __future__ import (print_function, division, absolute_import, + unicode_literals) +from fontTools.misc.py23 import * +from fontTools import agl +import unittest + + +class AglToUnicodeTest(unittest.TestCase): + def test_spec_examples(self): + # https://github.com/adobe-type-tools/agl-specification#3-examples + # + # TODO: Currently, we only handle AGLFN instead of legacy AGL names. + # Therefore, the test cases below use Iogonek instead of Lcommaaccent. + # Change Iogonek to Lcommaaccent as soon as the implementation has + # been fixed to also support legacy AGL names. + # https://github.com/fonttools/fonttools/issues/775 + self.assertEqual(agl.toUnicode("Iogonek"), "Į") + self.assertEqual(agl.toUnicode("uni20AC0308"), "\u20AC\u0308") + self.assertEqual(agl.toUnicode("u1040C"), "\U0001040C") + self.assertEqual(agl.toUnicode("uniD801DC0C"), "") + self.assertEqual(agl.toUnicode("uni20ac"), "") + self.assertEqual( + agl.toUnicode("Iogonek_uni20AC0308_u1040C.alternate"), + "\u012E\u20AC\u0308\U0001040C") + self.assertEqual(agl.toUnicode("Iogonek_uni012E_u012E"), "ĮĮĮ") + self.assertEqual(agl.toUnicode("foo"), "") + self.assertEqual(agl.toUnicode(".notdef"), "") + + def test_aglfn(self): + self.assertEqual(agl.toUnicode("longs_t"), "ſt") + self.assertEqual(agl.toUnicode("f_f_i.alt123"), "ffi") + + def test_uniABCD(self): + self.assertEqual(agl.toUnicode("uni0041"), "A") + self.assertEqual(agl.toUnicode("uni0041_uni0042_uni0043"), "ABC") + self.assertEqual(agl.toUnicode("uni004100420043"), "ABC") + self.assertEqual(agl.toUnicode("uni"), "") + self.assertEqual(agl.toUnicode("uni41"), "") + self.assertEqual(agl.toUnicode("uni004101"), "") + self.assertEqual(agl.toUnicode("uniDC00"), "") + + def test_uABCD(self): + self.assertEqual(agl.toUnicode("u0041"), "A") + self.assertEqual(agl.toUnicode("u00041"), "A") + self.assertEqual(agl.toUnicode("u000041"), "A") + self.assertEqual(agl.toUnicode("u0000041"), "") + self.assertEqual(agl.toUnicode("u0041_uni0041_A.alt"), "AAA") + + def test_union(self): + # Interesting test case because "uni" is a prefix of "union". + self.assertEqual(agl.toUnicode("union"), "∪") + # U+222A U+FE00 is a Standardized Variant for UNION WITH SERIFS. + self.assertEqual(agl.toUnicode("union_uniFE00"), "\u222A\uFE00") + + def test_dingbats(self): + self.assertEqual(agl.toUnicode("a20", isZapfDingbats=True), "✔") + self.assertEqual(agl.toUnicode("a20.alt", isZapfDingbats=True), "✔") + self.assertEqual(agl.toUnicode("a206", isZapfDingbats=True), "❰") + self.assertEqual(agl.toUnicode("a20", isZapfDingbats=False), "") + self.assertEqual(agl.toUnicode("a0", isZapfDingbats=True), "") + self.assertEqual(agl.toUnicode("a207", isZapfDingbats=True), "") + self.assertEqual(agl.toUnicode("abcdef", isZapfDingbats=True), "") + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/cffLib/cffLib_test.py b/Tests/cffLib/cffLib_test.py new file mode 100644 index 0000000..1d169c9 --- /dev/null +++ b/Tests/cffLib/cffLib_test.py @@ -0,0 +1,64 @@ +from __future__ import print_function, division, absolute_import +from fontTools.cffLib import TopDict, PrivateDict, CharStrings +from fontTools.misc.testTools import parseXML, DataFilesHandler +from fontTools.ttLib import TTFont +import sys +import unittest + + +class CffLibTest(DataFilesHandler): + + def test_topDict_recalcFontBBox(self): + topDict = TopDict() + topDict.CharStrings = CharStrings(None, None, None, PrivateDict(), None, None) + topDict.CharStrings.fromXML(None, None, parseXML(""" + <CharString name=".notdef"> + endchar + </CharString> + <CharString name="foo"><!-- [100, -100, 300, 100] --> + 100 -100 rmoveto 200 hlineto 200 vlineto -200 hlineto endchar + </CharString> + <CharString name="bar"><!-- [0, 0, 200, 200] --> + 0 0 rmoveto 200 hlineto 200 vlineto -200 hlineto endchar + </CharString> + <CharString name="baz"><!-- [-55.1, -55.1, 55.1, 55.1] --> + -55.1 -55.1 rmoveto 110.2 hlineto 110.2 vlineto -110.2 hlineto endchar + </CharString> + """)) + + topDict.recalcFontBBox() + self.assertEqual(topDict.FontBBox, [-56, -100, 300, 200]) + + def test_topDict_recalcFontBBox_empty(self): + topDict = TopDict() + topDict.CharStrings = CharStrings(None, None, None, PrivateDict(), None, None) + topDict.CharStrings.fromXML(None, None, parseXML(""" + <CharString name=".notdef"> + endchar + </CharString> + <CharString name="space"> + 123 endchar + </CharString> + """)) + + topDict.recalcFontBBox() + self.assertEqual(topDict.FontBBox, [0, 0, 0, 0]) + + def test_topDict_set_Encoding(self): + file_name = 'TestOTF.otf' + font_path = self.getpath(file_name) + temp_path = self.temp_font(font_path, file_name) + save_path = temp_path[:-4] + '2.otf' + font = TTFont(temp_path) + topDict = font["CFF "].cff.topDictIndex[0] + encoding = [".notdef"] * 256 + encoding[0x20] = "space" + topDict.Encoding = encoding + font.save(save_path) + font2 = TTFont(save_path) + topDict2 = font2["CFF "].cff.topDictIndex[0] + self.assertEqual(topDict2.Encoding[32], "space") + + +if __name__ == "__main__": + sys.exit(unittest.main()) diff --git a/Tests/cffLib/data/TestOTF.otf b/Tests/cffLib/data/TestOTF.otf new file mode 100644 index 0000000000000000000000000000000000000000..cd0bdf67d24e99164a89a4bdd920496f346b5bff GIT binary patch literal 2308 zcmeYd3Grv(VrXDsW>9c;b5l_Jb4Y-Jf&B*q1H)^VU{7}j1_nk3297@r3=ACp!TLt& z%KSeV7#IZ@7#I?ga}x^~4V7*)FtFZXU|{@}k(!voQQ;BKz`(GEfq}s+BO^7DErjs_ z0|Ubo1_lO|jNFn6_B9L(7#KJ}e2Lt|iUJ0<0FV_N3=9lxd5O8HEj=|a7#J7}7#LWx z3i69f{_kKgVqjpu!N9<v0CEWk`^AU%$Mf5KWngB0!N9=4aB-TTIf%aU_j3qHHPalX zcMJ>+oD57LQIPQr49qS6t}rk#U1l(1Fk+emG6gCJq1YH00vH(B7#J8>7&{mzFt9Q( zFzsRLVqjqCfzXV18H^ZyaWFA)v$3!+v2!r8G=M_}q8&u~`TMyt9AIEjP-A-cpN-)J za|@#m#BK%##xG2Jz&-#elxFmT+iwC>!NADC!C(RvXJTMyuz|9f86+8;p==f;HLMK0 z3^7n~HU@r%OemY3L6e~Z%I09;WLOAgb0V?18I&1TLB)BH*nAAu3}>O@LJS;?91P41 zj4YfC3=A2J0#G(1g8*X%l+DDz$Jha7Gcy=5PJ^;pkkqg;NHLy(inB4uF+PH_*%^YE z7@%wp1|g;(D4P?B&CTG(lmZp!L1ObUlrgPx&M&Ae%1qBFQP4=%R4_6yG*w8dRB+4B zD+$TZ&nebZ@XJ>K%NHvYr52|am8GWWg`^gjDENoC1*N8!<|G!u6<aC5WURm@g{2l1 zXXfWA80r}q7=WbwL)>(snm}BT848)j3VHb@3W*9OMTse?xrs&D3i)YB4uG4UQBqP+ zY^ATCo>`JnnxvPUpR1pgnvs&2qMw$ZS5g9UEfGrelXCKt^m7x7OHzyUeLS6A{eoTf zN-9dg?g<HTV{m53XDDE(WGG_DWJqVoU?^cwV9;PlX3%6%U@&4ZU@&AbWl&&9VyI+L zU~pr|XUJnHVF+Q!XUJ#BVJK$MV^Co5W5{PvK+<0f=A|+eGo&&UF_bZ+GNdr*F@!LH zL_ntdGlVd>F$6KBGNdz<GUPBMG8ADk*@{5{Lk~y{=B6;P>BS70U^gi+7&7QF7=R(n zCWt+{Xg0xQ6&N7$3JjSHAQ$H`<b(a5$e_Sb!cfGJ$dJO2%8(0oT{eRPLq0<qju5~M z6;LRaFcdHpGgvX`Gw3s<Gh{N9Fk~>4G9)qRF(fnOGvqSpGbAyjGGs8MfNj!eNCW$$ zgrNi;w$w2NWKRx5K0^|NKG-+K3?<-@(`WEu@MLgiaAojg2xf3)&|@fJs9-1o6*UaZ z44^#5z#s$7)iI#l!~o*6FgP$UFz_&NFfcK&FfcIiGAJ<UDX9Jb57vj^G5-Jh|MCB8 z_wPeZ084{PYsPtC<|#CBuppFxmA_Cf4hrN(98zGH;gTB_#}xvs&{$_+VEDfkR3l<4 zWMp7rWME`u6aW<;NTnM{O!0>R!w*@;A974T<d}a*u<Y&rY4Ed&^}7Op@Am?s@7jV4 zER2k79CmJcd8Iiy&OwNhI7TBmhSahcR0Fm%b}%q9@H130urV@9NXaUw+x+PG5hnJt z>5t0aCicC1MSseDKk!$Mov)kkdmk9J9uiaF;`^BnqQ1BO<`Y%mf>M0F!XRI8ei#4F g_gkEen}P8QGz@+*Fff4dM+S4I%M74410yJ806r{tod5s; literal 0 HcmV?d00001 diff --git a/Tests/cffLib/specializer_test.py b/Tests/cffLib/specializer_test.py new file mode 100644 index 0000000..aa50611 --- /dev/null +++ b/Tests/cffLib/specializer_test.py @@ -0,0 +1,918 @@ +from __future__ import print_function, division, absolute_import +from fontTools.cffLib.specializer import (programToString, stringToProgram, + generalizeProgram, specializeProgram) +import unittest + +# TODO +# https://github.com/fonttools/fonttools/pull/959#commitcomment-22059841 +# Maybe we should make these data driven. Each entry will have an input string, +# and a generalized and specialized. For the latter two, if they are None, they +# are considered equal to the input. Then we can do roundtripping tests as well... +# There are a few other places (aosp tests for example) where we generate tests +# from data. + + +def get_generalized_charstr(charstr, **kwargs): + return programToString(generalizeProgram(stringToProgram(charstr), **kwargs)) + + +def get_specialized_charstr(charstr, **kwargs): + return programToString(specializeProgram(stringToProgram(charstr), **kwargs)) + + +class CFFGeneralizeProgramTest(unittest.TestCase): + + def __init__(self, methodName): + unittest.TestCase.__init__(self, methodName) + # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, + # and fires deprecation warnings if a program uses the old name. + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + +# no arguments/operands + def test_rmoveto_none(self): + test_charstr = 'rmoveto' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_generalized_charstr(test_charstr) + + def test_hmoveto_none(self): + test_charstr = 'hmoveto' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_generalized_charstr(test_charstr) + + def test_vmoveto_none(self): + test_charstr = 'vmoveto' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_generalized_charstr(test_charstr) + + def test_rlineto_none(self): + test_charstr = 'rlineto' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_generalized_charstr(test_charstr) + + def test_hlineto_none(self): + test_charstr = 'hlineto' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_generalized_charstr(test_charstr) + + def test_vlineto_none(self): + test_charstr = 'vlineto' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_generalized_charstr(test_charstr) + + def test_rrcurveto_none(self): + test_charstr = 'rrcurveto' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_generalized_charstr(test_charstr) + + def test_hhcurveto_none(self): + test_charstr = 'hhcurveto' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_generalized_charstr(test_charstr) + + def test_vvcurveto_none(self): + test_charstr = 'vvcurveto' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_generalized_charstr(test_charstr) + + def test_hvcurveto_none(self): + test_charstr = 'hvcurveto' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_generalized_charstr(test_charstr) + + def test_vhcurveto_none(self): + test_charstr = 'vhcurveto' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_generalized_charstr(test_charstr) + + def test_rcurveline_none(self): + test_charstr = 'rcurveline' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_generalized_charstr(test_charstr) + + def test_rlinecurve_none(self): + test_charstr = 'rlinecurve' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_generalized_charstr(test_charstr) + +# rmoveto + def test_rmoveto_zero(self): + test_charstr = '0 0 rmoveto' + xpct_charstr = test_charstr + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_rmoveto_zero_width(self): + test_charstr = '100 0 0 rmoveto' + xpct_charstr = test_charstr + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_rmoveto(self): + test_charstr = '.55 -.8 rmoveto' + xpct_charstr = '0.55 -0.8 rmoveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_rmoveto_width(self): + test_charstr = '100.5 50 -5.8 rmoveto' + xpct_charstr = test_charstr + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + +# hmoveto + def test_hmoveto_zero(self): + test_charstr = '0 hmoveto' + xpct_charstr = '0 0 rmoveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_hmoveto_zero_width(self): + test_charstr = '100 0 hmoveto' + xpct_charstr = '100 0 0 rmoveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_hmoveto(self): + test_charstr = '.67 hmoveto' + xpct_charstr = '0.67 0 rmoveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_hmoveto_width(self): + test_charstr = '100 -70 hmoveto' + xpct_charstr = '100 -70 0 rmoveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + +# vmoveto + def test_vmoveto_zero(self): + test_charstr = '0 vmoveto' + xpct_charstr = '0 0 rmoveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_vmoveto_zero_width(self): + test_charstr = '100 0 vmoveto' + xpct_charstr = '100 0 0 rmoveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_vmoveto(self): + test_charstr = '-.24 vmoveto' + xpct_charstr = '0 -0.24 rmoveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_vmoveto_width(self): + test_charstr = '100 44 vmoveto' + xpct_charstr = '100 0 44 rmoveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + +# rlineto + def test_rlineto_zero(self): + test_charstr = '0 0 rlineto' + xpct_charstr = test_charstr + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_rlineto_zero_mult(self): + test_charstr = '0 0 0 0 0 0 rlineto' + xpct_charstr = ('0 0 rlineto '*3).rstrip() + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_rlineto(self): + test_charstr = '.55 -.8 rlineto' + xpct_charstr = '0.55 -0.8 rlineto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_rlineto_mult(self): + test_charstr = '.55 -.8 .55 -.8 .55 -.8 rlineto' + xpct_charstr = ('0.55 -0.8 rlineto '*3).rstrip() + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + +# hlineto + def test_hlineto_zero(self): + test_charstr = '0 hlineto' + xpct_charstr = '0 0 rlineto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_hlineto_zero_mult(self): + test_charstr = '0 0 0 0 hlineto' + xpct_charstr = ('0 0 rlineto '*4).rstrip() + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_hlineto(self): + test_charstr = '.67 hlineto' + xpct_charstr = '0.67 0 rlineto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_hlineto_mult(self): + test_charstr = '.67 -6.0 .67 hlineto' + xpct_charstr = '0.67 0 rlineto 0 -6.0 rlineto 0.67 0 rlineto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + +# vlineto + def test_vlineto_zero(self): + test_charstr = '0 vlineto' + xpct_charstr = '0 0 rlineto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_vlineto_zero_mult(self): + test_charstr = '0 0 0 vlineto' + xpct_charstr = ('0 0 rlineto '*3).rstrip() + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_vlineto(self): + test_charstr = '-.24 vlineto' + xpct_charstr = '0 -0.24 rlineto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_vlineto_mult(self): + test_charstr = '-.24 +50 30 -4 vlineto' + xpct_charstr = '0 -0.24 rlineto 50 0 rlineto 0 30 rlineto -4 0 rlineto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + +# rrcurveto + def test_rrcurveto(self): + test_charstr = '-1 56 -2 57 -1 57 rrcurveto' + xpct_charstr = test_charstr + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_rrcurveto_mult(self): + test_charstr = '-30 8 -36 15 -37 22 44 54 31 61 22 68 rrcurveto' + xpct_charstr = '-30 8 -36 15 -37 22 rrcurveto 44 54 31 61 22 68 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_rrcurveto_d3947b8(self): + test_charstr = '1 2 3 4 5 0 rrcurveto' + xpct_charstr = test_charstr + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_rrcurveto_v0_0h_h0(self): + test_charstr = '0 10 1 2 0 0 0 0 1 2 0 1 0 1 3 4 0 0 rrcurveto' + xpct_charstr = '0 10 1 2 0 0 rrcurveto 0 0 1 2 0 1 rrcurveto 0 1 3 4 0 0 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_rrcurveto_h0_0h_h0(self): + test_charstr = '10 0 1 2 0 0 0 0 1 2 0 1 0 1 3 4 0 0 rrcurveto' + xpct_charstr = '10 0 1 2 0 0 rrcurveto 0 0 1 2 0 1 rrcurveto 0 1 3 4 0 0 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_rrcurveto_00_0h_h0(self): + test_charstr = '0 0 1 2 0 0 0 0 1 2 0 1 0 1 3 4 0 0 rrcurveto' + xpct_charstr = '0 0 1 2 0 0 rrcurveto 0 0 1 2 0 1 rrcurveto 0 1 3 4 0 0 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_rrcurveto_r0_0h_h0(self): + test_charstr = '10 10 1 2 0 0 0 0 1 2 0 1 0 1 3 4 0 0 rrcurveto' + xpct_charstr = '10 10 1 2 0 0 rrcurveto 0 0 1 2 0 1 rrcurveto 0 1 3 4 0 0 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_rrcurveto_v0_0v_v0(self): + test_charstr = '0 10 1 2 0 0 0 0 1 2 1 0 1 0 3 4 0 0 rrcurveto' + xpct_charstr = '0 10 1 2 0 0 rrcurveto 0 0 1 2 1 0 rrcurveto 1 0 3 4 0 0 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_rrcurveto_h0_0v_v0(self): + test_charstr = '10 0 1 2 0 0 0 0 1 2 1 0 1 0 3 4 0 0 rrcurveto' + xpct_charstr = '10 0 1 2 0 0 rrcurveto 0 0 1 2 1 0 rrcurveto 1 0 3 4 0 0 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_rrcurveto_00_0v_v0(self): + test_charstr = '0 0 1 2 0 0 0 0 1 2 1 0 1 0 3 4 0 0 rrcurveto' + xpct_charstr = '0 0 1 2 0 0 rrcurveto 0 0 1 2 1 0 rrcurveto 1 0 3 4 0 0 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_rrcurveto_r0_0v_v0(self): + test_charstr = '10 10 1 2 0 0 0 0 1 2 1 0 1 0 3 4 0 0 rrcurveto' + xpct_charstr = '10 10 1 2 0 0 rrcurveto 0 0 1 2 1 0 rrcurveto 1 0 3 4 0 0 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + +# hhcurveto + def test_hhcurveto_4(self): + test_charstr = '10 30 0 10 hhcurveto' + xpct_charstr = '10 0 30 0 10 0 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_hhcurveto_5(self): + test_charstr = '40 -38 -60 41 -91 hhcurveto' + xpct_charstr = '-38 40 -60 41 -91 0 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_hhcurveto_mult_4_4(self): + test_charstr = '43 23 25 18 29 56 42 -84 hhcurveto' + xpct_charstr = '43 0 23 25 18 0 rrcurveto 29 0 56 42 -84 0 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_hhcurveto_mult_5_4(self): + test_charstr = '43 23 25 18 29 56 42 -84 79 hhcurveto' + xpct_charstr = '23 43 25 18 29 0 rrcurveto 56 0 42 -84 79 0 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_hhcurveto_mult_4_4_4(self): + test_charstr = '1 2 3 4 5 6 7 8 9 10 11 12 hhcurveto' + xpct_charstr = '1 0 2 3 4 0 rrcurveto 5 0 6 7 8 0 rrcurveto 9 0 10 11 12 0 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_hhcurveto_mult_5_4_4(self): + test_charstr = '1 2 3 4 5 6 7 8 9 10 11 12 13 hhcurveto' + xpct_charstr = '2 1 3 4 5 0 rrcurveto 6 0 7 8 9 0 rrcurveto 10 0 11 12 13 0 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + +# vvcurveto + def test_vvcurveto_4(self): + test_charstr = '61 6 52 68 vvcurveto' + xpct_charstr = '0 61 6 52 0 68 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_vvcurveto_5(self): + test_charstr = '61 38 35 56 72 vvcurveto' + xpct_charstr = '61 38 35 56 0 72 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_vvcurveto_mult_4_4(self): + test_charstr = '-84 -88 -30 -90 -13 19 23 -11 vvcurveto' + xpct_charstr = '0 -84 -88 -30 0 -90 rrcurveto 0 -13 19 23 0 -11 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_vvcurveto_mult_5_4(self): + test_charstr = '43 12 17 32 65 68 -6 52 61 vvcurveto' + xpct_charstr = '43 12 17 32 0 65 rrcurveto 0 68 -6 52 0 61 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_vvcurveto_mult_4_4_4(self): + test_charstr = '1 2 3 4 5 6 7 8 9 10 11 12 vvcurveto' + xpct_charstr = '0 1 2 3 0 4 rrcurveto 0 5 6 7 0 8 rrcurveto 0 9 10 11 0 12 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_vvcurveto_mult_5_4_4(self): + test_charstr = '1 2 3 4 5 6 7 8 9 10 11 12 13 vvcurveto' + xpct_charstr = '1 2 3 4 0 5 rrcurveto 0 6 7 8 0 9 rrcurveto 0 10 11 12 0 13 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + +# hvcurveto + def test_hvcurveto_4(self): + test_charstr = '1 2 3 4 hvcurveto' + xpct_charstr = '1 0 2 3 0 4 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_hvcurveto_5(self): + test_charstr = '57 44 22 40 34 hvcurveto' + xpct_charstr = '57 0 44 22 34 40 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_hvcurveto_4_4(self): + test_charstr = '65 33 -19 -45 -45 -29 -25 -71 hvcurveto' + xpct_charstr = '65 0 33 -19 0 -45 rrcurveto 0 -45 -29 -25 -71 0 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_hvcurveto_4_5(self): + test_charstr = '97 69 41 86 58 -36 34 -64 11 hvcurveto' + xpct_charstr = '97 0 69 41 0 86 rrcurveto 0 58 -36 34 -64 11 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_hvcurveto_4_4_4(self): + test_charstr = '1 2 3 4 5 6 7 8 9 10 11 12 hvcurveto' + xpct_charstr = '1 0 2 3 0 4 rrcurveto 0 5 6 7 8 0 rrcurveto 9 0 10 11 0 12 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_hvcurveto_4_4_5(self): + test_charstr = '-124 -79 104 165 163 82 102 124 56 43 -25 -37 35 hvcurveto' + xpct_charstr = '-124 0 -79 104 0 165 rrcurveto 0 163 82 102 124 0 rrcurveto 56 0 43 -25 35 -37 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_hvcurveto_4_4_4_4(self): + test_charstr = '32 25 22 32 31 -25 22 -32 -32 -25 -22 -31 -32 25 -22 32 hvcurveto' + xpct_charstr = '32 0 25 22 0 32 rrcurveto 0 31 -25 22 -32 0 rrcurveto -32 0 -25 -22 0 -31 rrcurveto 0 -32 25 -22 32 0 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_hvcurveto_4_4_4_4_5(self): + test_charstr = '-170 -128 111 195 234 172 151 178 182 95 -118 -161 -130 -71 -77 -63 -55 -19 38 79 20 hvcurveto' + xpct_charstr = '-170 0 -128 111 0 195 rrcurveto 0 234 172 151 178 0 rrcurveto 182 0 95 -118 0 -161 rrcurveto 0 -130 -71 -77 -63 0 rrcurveto -55 0 -19 38 20 79 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + +# vhcurveto + def test_vhcurveto_4(self): + test_charstr = '-57 43 -30 53 vhcurveto' + xpct_charstr = '0 -57 43 -30 53 0 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_vhcurveto_5(self): + test_charstr = '41 -27 19 -46 11 vhcurveto' + xpct_charstr = '0 41 -27 19 -46 11 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_vhcurveto_4_4(self): + test_charstr = '1 2 3 4 5 6 7 8 vhcurveto' + xpct_charstr = '0 1 2 3 4 0 rrcurveto 5 0 6 7 0 8 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_vhcurveto_4_5(self): + test_charstr = '-64 -23 -25 -45 -30 -24 14 33 -19 vhcurveto' + xpct_charstr = '0 -64 -23 -25 -45 0 rrcurveto -30 0 -24 14 -19 33 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_vhcurveto_4_4_4(self): + test_charstr = '1 2 3 4 5 6 7 8 9 10 11 12 vhcurveto' + xpct_charstr = '0 1 2 3 4 0 rrcurveto 5 0 6 7 0 8 rrcurveto 0 9 10 11 12 0 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_vhcurveto_4_4_5(self): + test_charstr = '108 59 81 98 99 59 -81 -108 -100 -46 -66 -63 -47 vhcurveto' + xpct_charstr = '0 108 59 81 98 0 rrcurveto 99 0 59 -81 0 -108 rrcurveto 0 -100 -46 -66 -63 -47 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_vhcurveto_4_4_4_5(self): + test_charstr = '60 -26 37 -43 -33 -28 -22 -36 -37 27 -20 32 3 4 0 1 3 vhcurveto' + xpct_charstr = '0 60 -26 37 -43 0 rrcurveto -33 0 -28 -22 0 -36 rrcurveto 0 -37 27 -20 32 0 rrcurveto 3 0 4 0 3 1 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + +# rcurveline + def test_rcurveline_6_2(self): + test_charstr = '21 -76 21 -72 24 -73 31 -100 rcurveline' + xpct_charstr = '21 -76 21 -72 24 -73 rrcurveto 31 -100 rlineto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_rcurveline_6_6_2(self): + test_charstr = '-73 80 -80 121 -49 96 60 65 55 41 54 17 -8 78 rcurveline' + xpct_charstr = '-73 80 -80 121 -49 96 rrcurveto 60 65 55 41 54 17 rrcurveto -8 78 rlineto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_rcurveline_6_6_6_2(self): + test_charstr = '1 64 10 51 29 39 15 21 15 20 15 18 47 -89 63 -98 52 -59 91 8 rcurveline' + xpct_charstr = '1 64 10 51 29 39 rrcurveto 15 21 15 20 15 18 rrcurveto 47 -89 63 -98 52 -59 rrcurveto 91 8 rlineto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_rcurveline_6_6_6_6_2(self): + test_charstr = '1 64 10 51 29 39 15 21 15 20 15 18 46 -88 63 -97 52 -59 -38 -57 -49 -62 -52 -54 96 -8 rcurveline' + xpct_charstr = '1 64 10 51 29 39 rrcurveto 15 21 15 20 15 18 rrcurveto 46 -88 63 -97 52 -59 rrcurveto -38 -57 -49 -62 -52 -54 rrcurveto 96 -8 rlineto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + +# rlinecurve + def test_rlinecurve_2_6(self): + test_charstr = '21 -76 21 -72 24 -73 31 -100 rlinecurve' + xpct_charstr = '21 -76 rlineto 21 -72 24 -73 31 -100 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_rlinecurve_2_2_6(self): + test_charstr = '-73 80 -80 121 -49 96 60 65 55 41 rlinecurve' + xpct_charstr = '-73 80 rlineto -80 121 rlineto -49 96 60 65 55 41 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_rlinecurve_2_2_2_6(self): + test_charstr = '1 64 10 51 29 39 15 21 15 20 15 18 rlinecurve' + xpct_charstr = '1 64 rlineto 10 51 rlineto 29 39 rlineto 15 21 15 20 15 18 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + def test_rlinecurve_2_2_2_2_6(self): + test_charstr = '1 64 10 51 29 39 15 21 15 20 15 18 46 -88 rlinecurve' + xpct_charstr = '1 64 rlineto 10 51 rlineto 29 39 rlineto 15 21 rlineto 15 20 15 18 46 -88 rrcurveto' + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + +# hstem/vstem + def test_hstem_vstem(self): + test_charstr = '95 0 58 542 60 hstem 89 65 344 67 vstem 89 45 rmoveto' + xpct_charstr = test_charstr + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + +# hstemhm/vstemhm + def test_hstemhm_vstemhm(self): + test_charstr = '-16 577 60 24 60 hstemhm 98 55 236 55 vstemhm 343 577 rmoveto' + xpct_charstr = test_charstr + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + +# hintmask/cntrmask + def test_hintmask_cntrmask(self): + test_charstr = '52 80 153 61 4 83 -71.5 71.5 hintmask 11011100 94 119 216 119 216 119 cntrmask 1110000 154 -12 rmoveto' + xpct_charstr = test_charstr + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + +# endchar + def test_endchar(self): + test_charstr = '-255 319 rmoveto 266 57 rlineto endchar' + xpct_charstr = test_charstr + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + +# xtra + def test_xtra(self): + test_charstr = '-255 319 rmoveto 266 57 rlineto xtra 90 34' + xpct_charstr = test_charstr + self.assertEqual(get_generalized_charstr(test_charstr), xpct_charstr) + + +class CFFSpecializeProgramTest(unittest.TestCase): + + def __init__(self, methodName): + unittest.TestCase.__init__(self, methodName) + # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, + # and fires deprecation warnings if a program uses the old name. + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + +# no arguments/operands + def test_rmoveto_none(self): + test_charstr = 'rmoveto' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_specialized_charstr(test_charstr) + + def test_hmoveto_none(self): + test_charstr = 'hmoveto' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_specialized_charstr(test_charstr) + + def test_vmoveto_none(self): + test_charstr = 'vmoveto' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_specialized_charstr(test_charstr) + + def test_rlineto_none(self): + test_charstr = 'rlineto' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_specialized_charstr(test_charstr) + + def test_hlineto_none(self): + test_charstr = 'hlineto' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_specialized_charstr(test_charstr) + + def test_vlineto_none(self): + test_charstr = 'vlineto' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_specialized_charstr(test_charstr) + + def test_rrcurveto_none(self): + test_charstr = 'rrcurveto' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_specialized_charstr(test_charstr) + + def test_hhcurveto_none(self): + test_charstr = 'hhcurveto' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_specialized_charstr(test_charstr) + + def test_vvcurveto_none(self): + test_charstr = 'vvcurveto' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_specialized_charstr(test_charstr) + + def test_hvcurveto_none(self): + test_charstr = 'hvcurveto' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_specialized_charstr(test_charstr) + + def test_vhcurveto_none(self): + test_charstr = 'vhcurveto' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_specialized_charstr(test_charstr) + + def test_rcurveline_none(self): + test_charstr = 'rcurveline' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_specialized_charstr(test_charstr) + + def test_rlinecurve_none(self): + test_charstr = 'rlinecurve' + with self.assertRaisesRegex(ValueError, r'\[\]'): + get_specialized_charstr(test_charstr) + +# rmoveto + def test_rmoveto_zero(self): + test_charstr = '0 0 rmoveto' + xpct_charstr = '0 hmoveto' + self.assertEqual(get_specialized_charstr(test_charstr, + generalizeFirst=False), xpct_charstr) + + def test_rmoveto_zero_mult(self): + test_charstr = '0 0 rmoveto '*3 + xpct_charstr = '0 hmoveto' + self.assertEqual(get_specialized_charstr(test_charstr, + generalizeFirst=False), xpct_charstr) + + def test_rmoveto_zero_width(self): + test_charstr = '100 0 0 rmoveto' + xpct_charstr = '100 0 hmoveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_rmoveto(self): + test_charstr = '.55 -.8 rmoveto' + xpct_charstr = '0.55 -0.8 rmoveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_rmoveto_mult(self): + test_charstr = '55 -8 rmoveto '*3 + xpct_charstr = '165 -24 rmoveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_rmoveto_width(self): + test_charstr = '100.5 50 -5.8 rmoveto' + xpct_charstr = test_charstr + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + +# rlineto + def test_rlineto_zero(self): + test_charstr = '0 0 rlineto' + xpct_charstr = '' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_rlineto_zero_mult(self): + test_charstr = '0 0 rlineto '*3 + xpct_charstr = '' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_rlineto(self): + test_charstr = '.55 -.8 rlineto' + xpct_charstr = '0.55 -0.8 rlineto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_rlineto_mult(self): + test_charstr = '.55 -.8 rlineto '*3 + xpct_charstr = '0.55 -0.8 0.55 -0.8 0.55 -0.8 rlineto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_hlineto(self): + test_charstr = '.67 0 rlineto' + xpct_charstr = '0.67 hlineto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_hlineto_zero_mult(self): + test_charstr = '62 0 rlineto '*3 + xpct_charstr = '186 hlineto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_hlineto_mult(self): + test_charstr = '.67 0 rlineto 0 -6.0 rlineto .67 0 rlineto' + xpct_charstr = '0.67 -6.0 0.67 hlineto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_vlineto(self): + test_charstr = '0 -.24 rlineto' + xpct_charstr = '-0.24 vlineto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_vlineto_zero_mult(self): + test_charstr = '0 -24 rlineto '*3 + xpct_charstr = '-72 vlineto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_vlineto_mult(self): + test_charstr = '0 -.24 rlineto +50 0 rlineto 0 30 rlineto -4 0 rlineto' + xpct_charstr = '-0.24 50 30 -4 vlineto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_0lineto_peephole(self): + test_charstr = '1 2 0 0 3 4 rlineto' + xpct_charstr = '1 2 3 4 rlineto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_hlineto_peephole(self): + test_charstr = '1 2 5 0 3 4 rlineto' + xpct_charstr = test_charstr + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_vlineto_peephole(self): + test_charstr = '1 2 0 5 3 4 rlineto' + xpct_charstr = test_charstr + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + +# rrcurveto + def test_rrcurveto(self): + test_charstr = '-1 56 -2 57 -1 57 rrcurveto' + xpct_charstr = test_charstr + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_rrcurveto_mult(self): + test_charstr = '-30 8 -36 15 -37 22 rrcurveto 44 54 31 61 22 68 rrcurveto' + xpct_charstr = '-30 8 -36 15 -37 22 44 54 31 61 22 68 rrcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_rrcurveto_d3947b8(self): + test_charstr = '1 2 3 4 5 0 rrcurveto' + xpct_charstr = '2 1 3 4 5 hhcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_hhcurveto_4(self): + test_charstr = '10 0 30 0 10 0 rrcurveto' + xpct_charstr = '10 30 0 10 hhcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_hhcurveto_5(self): + test_charstr = '-38 40 -60 41 -91 0 rrcurveto' + xpct_charstr = '40 -38 -60 41 -91 hhcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_hhcurveto_mult_4_4(self): + test_charstr = '43 0 23 25 18 0 rrcurveto 29 0 56 42 -84 0 rrcurveto' + xpct_charstr = '43 23 25 18 29 56 42 -84 hhcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_hhcurveto_mult_5_4(self): + test_charstr = '23 43 25 18 29 0 rrcurveto 56 0 42 -84 79 0 rrcurveto' + xpct_charstr = '43 23 25 18 29 56 42 -84 79 hhcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_hhcurveto_mult_4_4_4(self): + test_charstr = '1 0 2 3 4 0 rrcurveto 5 0 6 7 8 0 rrcurveto 9 0 10 11 12 0 rrcurveto' + xpct_charstr = '1 2 3 4 5 6 7 8 9 10 11 12 hhcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_hhcurveto_mult_5_4_4(self): + test_charstr = '2 1 3 4 5 0 rrcurveto 6 0 7 8 9 0 rrcurveto 10 0 11 12 13 0 rrcurveto' + xpct_charstr = '1 2 3 4 5 6 7 8 9 10 11 12 13 hhcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_vvcurveto_4(self): + test_charstr = '0 61 6 52 0 68 rrcurveto' + xpct_charstr = '61 6 52 68 vvcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_vvcurveto_5(self): + test_charstr = '61 38 35 56 0 72 rrcurveto' + xpct_charstr = '61 38 35 56 72 vvcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_vvcurveto_mult_4_4(self): + test_charstr = '0 -84 -88 -30 0 -90 rrcurveto 0 -13 19 23 0 -11 rrcurveto' + xpct_charstr = '-84 -88 -30 -90 -13 19 23 -11 vvcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_vvcurveto_mult_5_4(self): + test_charstr = '43 12 17 32 0 65 rrcurveto 0 68 -6 52 0 61 rrcurveto' + xpct_charstr = '43 12 17 32 65 68 -6 52 61 vvcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_vvcurveto_mult_4_4_4(self): + test_charstr = '0 1 2 3 0 4 rrcurveto 0 5 6 7 0 8 rrcurveto 0 9 10 11 0 12 rrcurveto' + xpct_charstr = '1 2 3 4 5 6 7 8 9 10 11 12 vvcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_vvcurveto_mult_5_4_4(self): + test_charstr = '1 2 3 4 0 5 rrcurveto 0 6 7 8 0 9 rrcurveto 0 10 11 12 0 13 rrcurveto' + xpct_charstr = '1 2 3 4 5 6 7 8 9 10 11 12 13 vvcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_hvcurveto_4(self): + test_charstr = '1 0 2 3 0 4 rrcurveto' + xpct_charstr = '1 2 3 4 hvcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_hvcurveto_5(self): + test_charstr = '57 0 44 22 34 40 rrcurveto' + xpct_charstr = '57 44 22 40 34 hvcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_hvcurveto_4_4(self): + test_charstr = '65 0 33 -19 0 -45 rrcurveto 0 -45 -29 -25 -71 0 rrcurveto' + xpct_charstr = '65 33 -19 -45 -45 -29 -25 -71 hvcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_hvcurveto_4_5(self): + test_charstr = '97 0 69 41 0 86 rrcurveto 0 58 -36 34 -64 11 rrcurveto' + xpct_charstr = '97 69 41 86 58 -36 34 -64 11 hvcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_hvcurveto_4_4_4(self): + test_charstr = '1 0 2 3 0 4 rrcurveto 0 5 6 7 8 0 rrcurveto 9 0 10 11 0 12 rrcurveto' + xpct_charstr = '1 2 3 4 5 6 7 8 9 10 11 12 hvcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_hvcurveto_4_4_5(self): + test_charstr = '-124 0 -79 104 0 165 rrcurveto 0 163 82 102 124 0 rrcurveto 56 0 43 -25 35 -37 rrcurveto' + xpct_charstr = '-124 -79 104 165 163 82 102 124 56 43 -25 -37 35 hvcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_hvcurveto_4_4_4_4(self): + test_charstr = '32 0 25 22 0 32 rrcurveto 0 31 -25 22 -32 0 rrcurveto -32 0 -25 -22 0 -31 rrcurveto 0 -32 25 -22 32 0 rrcurveto' + xpct_charstr = '32 25 22 32 31 -25 22 -32 -32 -25 -22 -31 -32 25 -22 32 hvcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_hvcurveto_4_4_4_4_5(self): + test_charstr = '-170 0 -128 111 0 195 rrcurveto 0 234 172 151 178 0 rrcurveto 182 0 95 -118 0 -161 rrcurveto 0 -130 -71 -77 -63 0 rrcurveto -55 0 -19 38 20 79 rrcurveto' + xpct_charstr = '-170 -128 111 195 234 172 151 178 182 95 -118 -161 -130 -71 -77 -63 -55 -19 38 79 20 hvcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_vhcurveto_4(self): + test_charstr = '0 -57 43 -30 53 0 rrcurveto' + xpct_charstr = '-57 43 -30 53 vhcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_vhcurveto_5(self): + test_charstr = '0 41 -27 19 -46 11 rrcurveto' + xpct_charstr = '41 -27 19 -46 11 vhcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_vhcurveto_4_4(self): + test_charstr = '0 1 2 3 4 0 rrcurveto 5 0 6 7 0 8 rrcurveto' + xpct_charstr = '1 2 3 4 5 6 7 8 vhcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_vhcurveto_4_5(self): + test_charstr = '0 -64 -23 -25 -45 0 rrcurveto -30 0 -24 14 -19 33 rrcurveto' + xpct_charstr = '-64 -23 -25 -45 -30 -24 14 33 -19 vhcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_vhcurveto_4_4_4(self): + test_charstr = '0 1 2 3 4 0 rrcurveto 5 0 6 7 0 8 rrcurveto 0 9 10 11 12 0 rrcurveto' + xpct_charstr = '1 2 3 4 5 6 7 8 9 10 11 12 vhcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_vhcurveto_4_4_5(self): + test_charstr = '0 108 59 81 98 0 rrcurveto 99 0 59 -81 0 -108 rrcurveto 0 -100 -46 -66 -63 -47 rrcurveto' + xpct_charstr = '108 59 81 98 99 59 -81 -108 -100 -46 -66 -63 -47 vhcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_vhcurveto_4_4_4_5(self): + test_charstr = '0 60 -26 37 -43 0 rrcurveto -33 0 -28 -22 0 -36 rrcurveto 0 -37 27 -20 32 0 rrcurveto 3 0 4 0 3 1 rrcurveto' + xpct_charstr = '60 -26 37 -43 -33 -28 -22 -36 -37 27 -20 32 3 4 0 1 3 vhcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_rrcurveto_v0_0h_h0(self): + test_charstr = '0 10 1 2 0 0 0 0 1 2 0 1 0 1 3 4 0 0 rrcurveto' + xpct_charstr = '10 1 2 0 0 1 2 1 1 3 4 0 vhcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_rrcurveto_h0_0h_h0(self): + test_charstr = '10 0 1 2 0 0 0 0 1 2 0 1 0 1 3 4 0 0 rrcurveto' + xpct_charstr = '10 1 2 0 hhcurveto 0 1 2 1 1 3 4 0 hvcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_rrcurveto_00_0h_h0(self): + test_charstr = '0 0 1 2 0 0 0 0 1 2 0 1 0 1 3 4 0 0 rrcurveto' + xpct_charstr = '1 2 rlineto 0 1 2 1 1 3 4 0 hvcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_rrcurveto_r0_0h_h0(self): + test_charstr = '10 10 1 2 0 0 0 0 1 2 0 1 0 1 3 4 0 0 rrcurveto' + xpct_charstr = '10 10 1 2 0 0 1 2 1 1 3 4 0 vvcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_rrcurveto_v0_0v_v0(self): + test_charstr = '0 10 1 2 0 0 0 0 1 2 1 0 1 0 3 4 0 0 rrcurveto' + xpct_charstr = '10 1 2 0 vhcurveto 0 1 2 1 1 3 4 0 hhcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_rrcurveto_h0_0v_v0(self): + test_charstr = '10 0 1 2 0 0 0 0 1 2 1 0 1 0 3 4 0 0 rrcurveto' + xpct_charstr = '10 1 2 0 0 1 2 1 1 3 4 0 hhcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_rrcurveto_00_0v_v0(self): + test_charstr = '0 0 1 2 0 0 0 0 1 2 1 0 1 0 3 4 0 0 rrcurveto' + xpct_charstr = '1 2 rlineto 0 1 2 1 1 3 4 0 hhcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_rrcurveto_r0_0v_v0(self): + test_charstr = '10 10 1 2 0 0 0 0 1 2 1 0 1 0 3 4 0 0 rrcurveto' + xpct_charstr = '10 10 1 2 0 0 1 2 1 1 3 4 0 hhcurveto' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_hhcurveto_peephole(self): + test_charstr = '1 2 3 4 5 6 1 2 3 4 5 0 1 2 3 4 5 6 rrcurveto' + xpct_charstr = test_charstr + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_vvcurveto_peephole(self): + test_charstr = '1 2 3 4 5 6 1 2 3 4 0 6 1 2 3 4 5 6 rrcurveto' + xpct_charstr = test_charstr + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_hvcurveto_peephole(self): + test_charstr = '1 2 3 4 5 6 1 0 3 4 5 6 1 2 3 4 5 6 rrcurveto' + xpct_charstr = test_charstr + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_vhcurveto_peephole(self): + test_charstr = '1 2 3 4 5 6 0 2 3 4 5 6 1 2 3 4 5 6 rrcurveto' + xpct_charstr = test_charstr + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_rcurveline_6_2(self): + test_charstr = '21 -76 21 -72 24 -73 rrcurveto 31 -100 rlineto' + xpct_charstr = '21 -76 21 -72 24 -73 31 -100 rcurveline' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_rcurveline_6_6_2(self): + test_charstr = '-73 80 -80 121 -49 96 rrcurveto 60 65 55 41 54 17 rrcurveto -8 78 rlineto' + xpct_charstr = '-73 80 -80 121 -49 96 60 65 55 41 54 17 -8 78 rcurveline' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_rcurveline_6_6_6_2(self): + test_charstr = '1 64 10 51 29 39 rrcurveto 15 21 15 20 15 18 rrcurveto 47 -89 63 -98 52 -59 rrcurveto 91 8 rlineto' + xpct_charstr = '1 64 10 51 29 39 15 21 15 20 15 18 47 -89 63 -98 52 -59 91 8 rcurveline' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_rlinecurve_2_6(self): + test_charstr = '21 -76 rlineto 21 -72 24 -73 31 -100 rrcurveto' + xpct_charstr = '21 -76 21 -72 24 -73 31 -100 rlinecurve' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_rlinecurve_2_2_6(self): + test_charstr = '-73 80 rlineto -80 121 rlineto -49 96 60 65 55 41 rrcurveto' + xpct_charstr = '-73 80 -80 121 -49 96 60 65 55 41 rlinecurve' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + def test_rlinecurve_2_2_2_6(self): + test_charstr = '1 64 rlineto 10 51 rlineto 29 39 rlineto 15 21 15 20 15 18 rrcurveto' + xpct_charstr = '1 64 10 51 29 39 15 21 15 20 15 18 rlinecurve' + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + +# maxstack CFF=48 + def test_maxstack(self): + operands = '1 2 3 4 5 6 ' + operator = 'rrcurveto ' + test_charstr = (operands + operator)*9 + xpct_charstr = (operands + operator + operands*8 + operator).rstrip() + self.assertEqual(get_specialized_charstr(test_charstr), xpct_charstr) + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/designspaceLib/data/test.designspace b/Tests/designspaceLib/data/test.designspace new file mode 100644 index 0000000..cf7056b --- /dev/null +++ b/Tests/designspaceLib/data/test.designspace @@ -0,0 +1,107 @@ +<?xml version='1.0' encoding='utf-8'?> +<designspace format="4.0"> + <axes> + <axis default="0" maximum="1000" minimum="0" name="weight" tag="wght"> + <labelname xml:lang="en">Wéíght</labelname> + <labelname xml:lang="fa-IR">قطر</labelname> + </axis> + <axis default="20" hidden="1" maximum="1000" minimum="0" name="width" tag="wdth"> + <labelname xml:lang="fr">Chasse</labelname> + <map input="0" output="10" /> + <map input="401" output="66" /> + <map input="1000" output="990" /> + </axis> + </axes> + <rules> + <rule name="named.rule.1"> + <conditionset> + <condition maximum="1" minimum="0" name="axisName_a" /> + <condition maximum="3" minimum="2" name="axisName_b" /> + </conditionset> + <sub name="a" with="a.alt" /> + </rule> + </rules> + <sources> + <source familyname="MasterFamilyName" filename="masters/masterTest1.ufo" name="master.ufo1" stylename="MasterStyleNameOne"> + <lib copy="1" /> + <features copy="1" /> + <info copy="1" /> + <glyph mute="1" name="A" /> + <glyph mute="1" name="Z" /> + <location> + <dimension name="weight" xvalue="0" /> + <dimension name="width" xvalue="20" /> + </location> + </source> + <source familyname="MasterFamilyName" filename="masters/masterTest2.ufo" name="master.ufo2" stylename="MasterStyleNameTwo"> + <kerning mute="1" /> + <location> + <dimension name="weight" xvalue="1000" /> + <dimension name="width" xvalue="20" /> + </location> + </source> + <source familyname="MasterFamilyName" filename="masters/masterTest2.ufo" layer="supports" name="master.ufo2" stylename="Supports"> + <location> + <dimension name="weight" xvalue="1000" /> + <dimension name="width" xvalue="20" /> + </location> + </source> + </sources> + <instances> + <instance familyname="InstanceFamilyName" filename="instances/instanceTest1.ufo" name="instance.ufo1" postscriptfontname="InstancePostscriptName" stylemapfamilyname="InstanceStyleMapFamilyName" stylemapstylename="InstanceStyleMapStyleName" stylename="InstanceStyleName"> + <location> + <dimension name="weight" xvalue="500" /> + <dimension name="width" xvalue="20" /> + </location> + <glyphs> + <glyph mute="1" name="arrow" unicode="0x123 0x124 0x125" /> + </glyphs> + <kerning /> + <info /> + <lib> + <dict> + <key>com.coolDesignspaceApp.specimenText</key> + <string>Hamburgerwhatever</string> + </dict> + </lib> + </instance> + <instance familyname="InstanceFamilyName" filename="instances/instanceTest2.ufo" name="instance.ufo2" postscriptfontname="InstancePostscriptName" stylemapfamilyname="InstanceStyleMapFamilyName" stylemapstylename="InstanceStyleMapStyleName" stylename="InstanceStyleName"> + <location> + <dimension name="weight" xvalue="500" /> + <dimension name="width" xvalue="400" yvalue="300" /> + </location> + <glyphs> + <glyph name="arrow" unicode="0x65 0xc9 0x12d"> + <location> + <dimension name="weight" xvalue="120" /> + <dimension name="width" xvalue="100" /> + </location> + <note>A note about this glyph</note> + <masters> + <master glyphname="BB" source="master.ufo1"> + <location> + <dimension name="weight" xvalue="20" /> + <dimension name="width" xvalue="20" /> + </location> + </master> + <master glyphname="CC" source="master.ufo2"> + <location> + <dimension name="weight" xvalue="900" /> + <dimension name="width" xvalue="900" /> + </location> + </master> + </masters> + </glyph> + <glyph name="arrow2" /> + </glyphs> + <kerning /> + <info /> + </instance> + </instances> + <lib> + <dict> + <key>com.coolDesignspaceApp.previewSize</key> + <integer>30</integer> + </dict> + </lib> +</designspace> diff --git a/Tests/designspaceLib/designspace_test.py b/Tests/designspaceLib/designspace_test.py new file mode 100644 index 0000000..1d9b841 --- /dev/null +++ b/Tests/designspaceLib/designspace_test.py @@ -0,0 +1,791 @@ +# coding=utf-8 + +from __future__ import (print_function, division, absolute_import, + unicode_literals) + +import os +import pytest +import warnings + +from fontTools.misc.py23 import open +from fontTools.designspaceLib import ( + DesignSpaceDocument, SourceDescriptor, AxisDescriptor, RuleDescriptor, + InstanceDescriptor, evaluateRule, processRules, posix, DesignSpaceDocumentError) + +def _axesAsDict(axes): + """ + Make the axis data we have available in + """ + axesDict = {} + for axisDescriptor in axes: + d = { + 'name': axisDescriptor.name, + 'tag': axisDescriptor.tag, + 'minimum': axisDescriptor.minimum, + 'maximum': axisDescriptor.maximum, + 'default': axisDescriptor.default, + 'map': axisDescriptor.map, + } + axesDict[axisDescriptor.name] = d + return axesDict + + +def assert_equals_test_file(path, test_filename): + with open(path) as fp: + actual = fp.read() + + test_path = os.path.join(os.path.dirname(__file__), test_filename) + with open(test_path) as fp: + expected = fp.read() + + assert actual == expected + + +def test_fill_document(tmpdir): + tmpdir = str(tmpdir) + testDocPath = os.path.join(tmpdir, "test.designspace") + masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") + masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") + instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") + instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") + doc = DesignSpaceDocument() + + # write some axes + a1 = AxisDescriptor() + a1.minimum = 0 + a1.maximum = 1000 + a1.default = 0 + a1.name = "weight" + a1.tag = "wght" + # note: just to test the element language, not an actual label name recommendations. + a1.labelNames[u'fa-IR'] = u"قطر" + a1.labelNames[u'en'] = u"Wéíght" + doc.addAxis(a1) + a2 = AxisDescriptor() + a2.minimum = 0 + a2.maximum = 1000 + a2.default = 20 + a2.name = "width" + a2.tag = "wdth" + a2.map = [(0.0, 10.0), (401.0, 66.0), (1000.0, 990.0)] + a2.hidden = True + a2.labelNames[u'fr'] = u"Chasse" + doc.addAxis(a2) + + # add master 1 + s1 = SourceDescriptor() + s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath)) + assert s1.font is None + s1.name = "master.ufo1" + s1.copyLib = True + s1.copyInfo = True + s1.copyFeatures = True + s1.location = dict(weight=0) + s1.familyName = "MasterFamilyName" + s1.styleName = "MasterStyleNameOne" + s1.mutedGlyphNames.append("A") + s1.mutedGlyphNames.append("Z") + doc.addSource(s1) + # add master 2 + s2 = SourceDescriptor() + s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) + s2.name = "master.ufo2" + s2.copyLib = False + s2.copyInfo = False + s2.copyFeatures = False + s2.muteKerning = True + s2.location = dict(weight=1000) + s2.familyName = "MasterFamilyName" + s2.styleName = "MasterStyleNameTwo" + doc.addSource(s2) + # add master 3 from a different layer + s3 = SourceDescriptor() + s3.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) + s3.name = "master.ufo2" + s3.copyLib = False + s3.copyInfo = False + s3.copyFeatures = False + s3.muteKerning = False + s3.layerName = "supports" + s3.location = dict(weight=1000) + s3.familyName = "MasterFamilyName" + s3.styleName = "Supports" + doc.addSource(s3) + # add instance 1 + i1 = InstanceDescriptor() + i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath)) + i1.familyName = "InstanceFamilyName" + i1.styleName = "InstanceStyleName" + i1.name = "instance.ufo1" + i1.location = dict(weight=500, spooky=666) # this adds a dimension that is not defined. + i1.postScriptFontName = "InstancePostscriptName" + i1.styleMapFamilyName = "InstanceStyleMapFamilyName" + i1.styleMapStyleName = "InstanceStyleMapStyleName" + glyphData = dict(name="arrow", mute=True, unicodes=[0x123, 0x124, 0x125]) + i1.glyphs['arrow'] = glyphData + i1.lib['com.coolDesignspaceApp.specimenText'] = "Hamburgerwhatever" + doc.addInstance(i1) + # add instance 2 + i2 = InstanceDescriptor() + i2.filename = os.path.relpath(instancePath2, os.path.dirname(testDocPath)) + i2.familyName = "InstanceFamilyName" + i2.styleName = "InstanceStyleName" + i2.name = "instance.ufo2" + # anisotropic location + i2.location = dict(weight=500, width=(400,300)) + i2.postScriptFontName = "InstancePostscriptName" + i2.styleMapFamilyName = "InstanceStyleMapFamilyName" + i2.styleMapStyleName = "InstanceStyleMapStyleName" + glyphMasters = [dict(font="master.ufo1", glyphName="BB", location=dict(width=20,weight=20)), dict(font="master.ufo2", glyphName="CC", location=dict(width=900,weight=900))] + glyphData = dict(name="arrow", unicodes=[101, 201, 301]) + glyphData['masters'] = glyphMasters + glyphData['note'] = "A note about this glyph" + glyphData['instanceLocation'] = dict(width=100, weight=120) + i2.glyphs['arrow'] = glyphData + i2.glyphs['arrow2'] = dict(mute=False) + doc.addInstance(i2) + + doc.filename = "suggestedFileName.designspace" + doc.lib['com.coolDesignspaceApp.previewSize'] = 30 + + # write some rules + r1 = RuleDescriptor() + r1.name = "named.rule.1" + r1.conditionSets.append([ + dict(name='axisName_a', minimum=0, maximum=1), + dict(name='axisName_b', minimum=2, maximum=3) + ]) + r1.subs.append(("a", "a.alt")) + doc.addRule(r1) + # write the document + doc.write(testDocPath) + assert os.path.exists(testDocPath) + assert_equals_test_file(testDocPath, 'data/test.designspace') + # import it again + new = DesignSpaceDocument() + new.read(testDocPath) + + assert new.default.location == {'width': 20.0, 'weight': 0.0} + assert new.filename == 'test.designspace' + assert new.lib == doc.lib + assert new.instances[0].lib == doc.instances[0].lib + + # test roundtrip for the axis attributes and data + axes = {} + for axis in doc.axes: + if axis.tag not in axes: + axes[axis.tag] = [] + axes[axis.tag].append(axis.serialize()) + for axis in new.axes: + if axis.tag[0] == "_": + continue + if axis.tag not in axes: + axes[axis.tag] = [] + axes[axis.tag].append(axis.serialize()) + for v in axes.values(): + a, b = v + assert a == b + + +def test_unicodes(tmpdir): + tmpdir = str(tmpdir) + testDocPath = os.path.join(tmpdir, "testUnicodes.designspace") + testDocPath2 = os.path.join(tmpdir, "testUnicodes_roundtrip.designspace") + masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") + masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") + instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") + instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") + doc = DesignSpaceDocument() + # add master 1 + s1 = SourceDescriptor() + s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath)) + s1.name = "master.ufo1" + s1.copyInfo = True + s1.location = dict(weight=0) + doc.addSource(s1) + # add master 2 + s2 = SourceDescriptor() + s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) + s2.name = "master.ufo2" + s2.location = dict(weight=1000) + doc.addSource(s2) + # add instance 1 + i1 = InstanceDescriptor() + i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath)) + i1.name = "instance.ufo1" + i1.location = dict(weight=500) + glyphData = dict(name="arrow", mute=True, unicodes=[100, 200, 300]) + i1.glyphs['arrow'] = glyphData + doc.addInstance(i1) + # now we have sources and instances, but no axes yet. + doc.axes = [] # clear the axes + # write some axes + a1 = AxisDescriptor() + a1.minimum = 0 + a1.maximum = 1000 + a1.default = 0 + a1.name = "weight" + a1.tag = "wght" + doc.addAxis(a1) + # write the document + doc.write(testDocPath) + assert os.path.exists(testDocPath) + # import it again + new = DesignSpaceDocument() + new.read(testDocPath) + new.write(testDocPath2) + # compare the file contents + f1 = open(testDocPath, 'r', encoding='utf-8') + t1 = f1.read() + f1.close() + f2 = open(testDocPath2, 'r', encoding='utf-8') + t2 = f2.read() + f2.close() + assert t1 == t2 + # check the unicode values read from the document + assert new.instances[0].glyphs['arrow']['unicodes'] == [100,200,300] + + +def test_localisedNames(tmpdir): + tmpdir = str(tmpdir) + testDocPath = os.path.join(tmpdir, "testLocalisedNames.designspace") + testDocPath2 = os.path.join(tmpdir, "testLocalisedNames_roundtrip.designspace") + masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") + masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") + instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") + instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") + doc = DesignSpaceDocument() + # add master 1 + s1 = SourceDescriptor() + s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath)) + s1.name = "master.ufo1" + s1.copyInfo = True + s1.location = dict(weight=0) + doc.addSource(s1) + # add master 2 + s2 = SourceDescriptor() + s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) + s2.name = "master.ufo2" + s2.location = dict(weight=1000) + doc.addSource(s2) + # add instance 1 + i1 = InstanceDescriptor() + i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath)) + i1.familyName = "Montserrat" + i1.styleName = "SemiBold" + i1.styleMapFamilyName = "Montserrat SemiBold" + i1.styleMapStyleName = "Regular" + i1.setFamilyName("Montserrat", "fr") + i1.setFamilyName(u"モンセラート", "ja") + i1.setStyleName("Demigras", "fr") + i1.setStyleName(u"半ば", "ja") + i1.setStyleMapStyleName(u"Standard", "de") + i1.setStyleMapFamilyName("Montserrat Halbfett", "de") + i1.setStyleMapFamilyName(u"モンセラート SemiBold", "ja") + i1.name = "instance.ufo1" + i1.location = dict(weight=500, spooky=666) # this adds a dimension that is not defined. + i1.postScriptFontName = "InstancePostscriptName" + glyphData = dict(name="arrow", mute=True, unicodes=[0x123]) + i1.glyphs['arrow'] = glyphData + doc.addInstance(i1) + # now we have sources and instances, but no axes yet. + doc.axes = [] # clear the axes + # write some axes + a1 = AxisDescriptor() + a1.minimum = 0 + a1.maximum = 1000 + a1.default = 0 + a1.name = "weight" + a1.tag = "wght" + # note: just to test the element language, not an actual label name recommendations. + a1.labelNames[u'fa-IR'] = u"قطر" + a1.labelNames[u'en'] = u"Wéíght" + doc.addAxis(a1) + a2 = AxisDescriptor() + a2.minimum = 0 + a2.maximum = 1000 + a2.default = 0 + a2.name = "width" + a2.tag = "wdth" + a2.map = [(0.0, 10.0), (401.0, 66.0), (1000.0, 990.0)] + a2.labelNames[u'fr'] = u"Poids" + doc.addAxis(a2) + # add an axis that is not part of any location to see if that works + a3 = AxisDescriptor() + a3.minimum = 333 + a3.maximum = 666 + a3.default = 444 + a3.name = "spooky" + a3.tag = "spok" + a3.map = [(0.0, 10.0), (401.0, 66.0), (1000.0, 990.0)] + #doc.addAxis(a3) # uncomment this line to test the effects of default axes values + # write some rules + r1 = RuleDescriptor() + r1.name = "named.rule.1" + r1.conditionSets.append([ + dict(name='weight', minimum=200, maximum=500), + dict(name='width', minimum=0, maximum=150) + ]) + r1.subs.append(("a", "a.alt")) + doc.addRule(r1) + # write the document + doc.write(testDocPath) + assert os.path.exists(testDocPath) + # import it again + new = DesignSpaceDocument() + new.read(testDocPath) + new.write(testDocPath2) + f1 = open(testDocPath, 'r', encoding='utf-8') + t1 = f1.read() + f1.close() + f2 = open(testDocPath2, 'r', encoding='utf-8') + t2 = f2.read() + f2.close() + assert t1 == t2 + + +def test_handleNoAxes(tmpdir): + tmpdir = str(tmpdir) + # test what happens if the designspacedocument has no axes element. + testDocPath = os.path.join(tmpdir, "testNoAxes_source.designspace") + testDocPath2 = os.path.join(tmpdir, "testNoAxes_recontructed.designspace") + masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") + masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") + instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") + instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") + + # Case 1: No axes element in the document, but there are sources and instances + doc = DesignSpaceDocument() + + for name, value in [('One', 1),('Two', 2),('Three', 3)]: + a = AxisDescriptor() + a.minimum = 0 + a.maximum = 1000 + a.default = 0 + a.name = "axisName%s" % (name) + a.tag = "ax_%d" % (value) + doc.addAxis(a) + + # add master 1 + s1 = SourceDescriptor() + s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath)) + s1.name = "master.ufo1" + s1.copyLib = True + s1.copyInfo = True + s1.copyFeatures = True + s1.location = dict(axisNameOne=-1000, axisNameTwo=0, axisNameThree=1000) + s1.familyName = "MasterFamilyName" + s1.styleName = "MasterStyleNameOne" + doc.addSource(s1) + + # add master 2 + s2 = SourceDescriptor() + s2.filename = os.path.relpath(masterPath2, os.path.dirname(testDocPath)) + s2.name = "master.ufo1" + s2.copyLib = False + s2.copyInfo = False + s2.copyFeatures = False + s2.location = dict(axisNameOne=1000, axisNameTwo=1000, axisNameThree=0) + s2.familyName = "MasterFamilyName" + s2.styleName = "MasterStyleNameTwo" + doc.addSource(s2) + + # add instance 1 + i1 = InstanceDescriptor() + i1.filename = os.path.relpath(instancePath1, os.path.dirname(testDocPath)) + i1.familyName = "InstanceFamilyName" + i1.styleName = "InstanceStyleName" + i1.name = "instance.ufo1" + i1.location = dict(axisNameOne=(-1000,500), axisNameTwo=100) + i1.postScriptFontName = "InstancePostscriptName" + i1.styleMapFamilyName = "InstanceStyleMapFamilyName" + i1.styleMapStyleName = "InstanceStyleMapStyleName" + doc.addInstance(i1) + + doc.write(testDocPath) + verify = DesignSpaceDocument() + verify.read(testDocPath) + verify.write(testDocPath2) + +def test_pathNameResolve(tmpdir): + tmpdir = str(tmpdir) + # test how descriptor.path and descriptor.filename are resolved + testDocPath1 = os.path.join(tmpdir, "testPathName_case1.designspace") + testDocPath2 = os.path.join(tmpdir, "testPathName_case2.designspace") + testDocPath3 = os.path.join(tmpdir, "testPathName_case3.designspace") + testDocPath4 = os.path.join(tmpdir, "testPathName_case4.designspace") + testDocPath5 = os.path.join(tmpdir, "testPathName_case5.designspace") + testDocPath6 = os.path.join(tmpdir, "testPathName_case6.designspace") + masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo") + masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo") + instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo") + instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo") + + a1 = AxisDescriptor() + a1.tag = "TAGA" + a1.name = "axisName_a" + a1.minimum = 0 + a1.maximum = 1000 + a1.default = 0 + + # Case 1: filename and path are both empty. Nothing to calculate, nothing to put in the file. + doc = DesignSpaceDocument() + doc.addAxis(a1) + s = SourceDescriptor() + s.filename = None + s.path = None + s.copyInfo = True + s.location = dict(weight=0) + s.familyName = "MasterFamilyName" + s.styleName = "MasterStyleNameOne" + doc.addSource(s) + doc.write(testDocPath1) + verify = DesignSpaceDocument() + verify.read(testDocPath1) + assert verify.sources[0].filename == None + assert verify.sources[0].path == None + + # Case 2: filename is empty, path points somewhere: calculate a new filename. + doc = DesignSpaceDocument() + doc.addAxis(a1) + s = SourceDescriptor() + s.filename = None + s.path = masterPath1 + s.copyInfo = True + s.location = dict(weight=0) + s.familyName = "MasterFamilyName" + s.styleName = "MasterStyleNameOne" + doc.addSource(s) + doc.write(testDocPath2) + verify = DesignSpaceDocument() + verify.read(testDocPath2) + assert verify.sources[0].filename == "masters/masterTest1.ufo" + assert verify.sources[0].path == posix(masterPath1) + + # Case 3: the filename is set, the path is None. + doc = DesignSpaceDocument() + doc.addAxis(a1) + s = SourceDescriptor() + s.filename = "../somewhere/over/the/rainbow.ufo" + s.path = None + s.copyInfo = True + s.location = dict(weight=0) + s.familyName = "MasterFamilyName" + s.styleName = "MasterStyleNameOne" + doc.addSource(s) + doc.write(testDocPath3) + verify = DesignSpaceDocument() + verify.read(testDocPath3) + assert verify.sources[0].filename == "../somewhere/over/the/rainbow.ufo" + # make the absolute path for filename so we can see if it matches the path + p = os.path.abspath(os.path.join(os.path.dirname(testDocPath3), verify.sources[0].filename)) + assert verify.sources[0].path == posix(p) + + # Case 4: the filename points to one file, the path points to another. The path takes precedence. + doc = DesignSpaceDocument() + doc.addAxis(a1) + s = SourceDescriptor() + s.filename = "../somewhere/over/the/rainbow.ufo" + s.path = masterPath1 + s.copyInfo = True + s.location = dict(weight=0) + s.familyName = "MasterFamilyName" + s.styleName = "MasterStyleNameOne" + doc.addSource(s) + doc.write(testDocPath4) + verify = DesignSpaceDocument() + verify.read(testDocPath4) + assert verify.sources[0].filename == "masters/masterTest1.ufo" + + # Case 5: the filename is None, path has a value, update the filename + doc = DesignSpaceDocument() + doc.addAxis(a1) + s = SourceDescriptor() + s.filename = None + s.path = masterPath1 + s.copyInfo = True + s.location = dict(weight=0) + s.familyName = "MasterFamilyName" + s.styleName = "MasterStyleNameOne" + doc.addSource(s) + doc.write(testDocPath5) # so that the document has a path + doc.updateFilenameFromPath() + assert doc.sources[0].filename == "masters/masterTest1.ufo" + + # Case 6: the filename has a value, path has a value, update the filenames with force + doc = DesignSpaceDocument() + doc.addAxis(a1) + s = SourceDescriptor() + s.filename = "../somewhere/over/the/rainbow.ufo" + s.path = masterPath1 + s.copyInfo = True + s.location = dict(weight=0) + s.familyName = "MasterFamilyName" + s.styleName = "MasterStyleNameOne" + doc.write(testDocPath5) # so that the document has a path + doc.addSource(s) + assert doc.sources[0].filename == "../somewhere/over/the/rainbow.ufo" + doc.updateFilenameFromPath(force=True) + assert doc.sources[0].filename == "masters/masterTest1.ufo" + + +def test_normalise1(): + # normalisation of anisotropic locations, clipping + doc = DesignSpaceDocument() + # write some axes + a1 = AxisDescriptor() + a1.minimum = -1000 + a1.maximum = 1000 + a1.default = 0 + a1.name = "axisName_a" + a1.tag = "TAGA" + doc.addAxis(a1) + assert doc.normalizeLocation(dict(axisName_a=0)) == {'axisName_a': 0.0} + assert doc.normalizeLocation(dict(axisName_a=1000)) == {'axisName_a': 1.0} + # clipping beyond max values: + assert doc.normalizeLocation(dict(axisName_a=1001)) == {'axisName_a': 1.0} + assert doc.normalizeLocation(dict(axisName_a=500)) == {'axisName_a': 0.5} + assert doc.normalizeLocation(dict(axisName_a=-1000)) == {'axisName_a': -1.0} + assert doc.normalizeLocation(dict(axisName_a=-1001)) == {'axisName_a': -1.0} + # anisotropic coordinates normalise to isotropic + assert doc.normalizeLocation(dict(axisName_a=(1000, -1000))) == {'axisName_a': 1.0} + doc.normalize() + r = [] + for axis in doc.axes: + r.append((axis.name, axis.minimum, axis.default, axis.maximum)) + r.sort() + assert r == [('axisName_a', -1.0, 0.0, 1.0)] + +def test_normalise2(): + # normalisation with minimum > 0 + doc = DesignSpaceDocument() + # write some axes + a2 = AxisDescriptor() + a2.minimum = 100 + a2.maximum = 1000 + a2.default = 100 + a2.name = "axisName_b" + doc.addAxis(a2) + assert doc.normalizeLocation(dict(axisName_b=0)) == {'axisName_b': 0.0} + assert doc.normalizeLocation(dict(axisName_b=1000)) == {'axisName_b': 1.0} + # clipping beyond max values: + assert doc.normalizeLocation(dict(axisName_b=1001)) == {'axisName_b': 1.0} + assert doc.normalizeLocation(dict(axisName_b=500)) == {'axisName_b': 0.4444444444444444} + assert doc.normalizeLocation(dict(axisName_b=-1000)) == {'axisName_b': 0.0} + assert doc.normalizeLocation(dict(axisName_b=-1001)) == {'axisName_b': 0.0} + # anisotropic coordinates normalise to isotropic + assert doc.normalizeLocation(dict(axisName_b=(1000,-1000))) == {'axisName_b': 1.0} + assert doc.normalizeLocation(dict(axisName_b=1001)) == {'axisName_b': 1.0} + doc.normalize() + r = [] + for axis in doc.axes: + r.append((axis.name, axis.minimum, axis.default, axis.maximum)) + r.sort() + assert r == [('axisName_b', 0.0, 0.0, 1.0)] + +def test_normalise3(): + # normalisation of negative values, with default == maximum + doc = DesignSpaceDocument() + # write some axes + a3 = AxisDescriptor() + a3.minimum = -1000 + a3.maximum = 0 + a3.default = 0 + a3.name = "ccc" + doc.addAxis(a3) + assert doc.normalizeLocation(dict(ccc=0)) == {'ccc': 0.0} + assert doc.normalizeLocation(dict(ccc=1)) == {'ccc': 0.0} + assert doc.normalizeLocation(dict(ccc=-1000)) == {'ccc': -1.0} + assert doc.normalizeLocation(dict(ccc=-1001)) == {'ccc': -1.0} + doc.normalize() + r = [] + for axis in doc.axes: + r.append((axis.name, axis.minimum, axis.default, axis.maximum)) + r.sort() + assert r == [('ccc', -1.0, 0.0, 0.0)] + +def test_normalise4(): + # normalisation with a map + doc = DesignSpaceDocument() + # write some axes + a4 = AxisDescriptor() + a4.minimum = 0 + a4.maximum = 1000 + a4.default = 0 + a4.name = "ddd" + a4.map = [(0,100), (300, 500), (600, 500), (1000,900)] + doc.addAxis(a4) + doc.normalize() + r = [] + for axis in doc.axes: + r.append((axis.name, axis.map)) + r.sort() + assert r == [('ddd', [(0, 0.1), (300, 0.5), (600, 0.5), (1000, 0.9)])] + +def test_axisMapping(): + # note: because designspance lib does not do any actual + # processing of the mapping data, we can only check if there data is there. + doc = DesignSpaceDocument() + # write some axes + a4 = AxisDescriptor() + a4.minimum = 0 + a4.maximum = 1000 + a4.default = 0 + a4.name = "ddd" + a4.map = [(0,100), (300, 500), (600, 500), (1000,900)] + doc.addAxis(a4) + doc.normalize() + r = [] + for axis in doc.axes: + r.append((axis.name, axis.map)) + r.sort() + assert r == [('ddd', [(0, 0.1), (300, 0.5), (600, 0.5), (1000, 0.9)])] + +def test_rulesConditions(tmpdir): + # tests of rules, conditionsets and conditions + r1 = RuleDescriptor() + r1.name = "named.rule.1" + r1.conditionSets.append([ + dict(name='axisName_a', minimum=0, maximum=1000), + dict(name='axisName_b', minimum=0, maximum=3000) + ]) + r1.subs.append(("a", "a.alt")) + + assert evaluateRule(r1, dict(axisName_a = 500, axisName_b = 0)) == True + assert evaluateRule(r1, dict(axisName_a = 0, axisName_b = 0)) == True + assert evaluateRule(r1, dict(axisName_a = 1000, axisName_b = 0)) == True + assert evaluateRule(r1, dict(axisName_a = 1000, axisName_b = -100)) == False + assert evaluateRule(r1, dict(axisName_a = 1000.0001, axisName_b = 0)) == False + assert evaluateRule(r1, dict(axisName_a = -0.0001, axisName_b = 0)) == False + assert evaluateRule(r1, dict(axisName_a = -100, axisName_b = 0)) == False + assert processRules([r1], dict(axisName_a = 500, axisName_b = 0), ["a", "b", "c"]) == ['a.alt', 'b', 'c'] + assert processRules([r1], dict(axisName_a = 500, axisName_b = 0), ["a.alt", "b", "c"]) == ['a.alt', 'b', 'c'] + assert processRules([r1], dict(axisName_a = 2000, axisName_b = 0), ["a", "b", "c"]) == ['a', 'b', 'c'] + + # rule with only a maximum + r2 = RuleDescriptor() + r2.name = "named.rule.2" + r2.conditionSets.append([dict(name='axisName_a', maximum=500)]) + r2.subs.append(("b", "b.alt")) + + assert evaluateRule(r2, dict(axisName_a = 0)) == True + assert evaluateRule(r2, dict(axisName_a = -500)) == True + assert evaluateRule(r2, dict(axisName_a = 1000)) == False + + # rule with only a minimum + r3 = RuleDescriptor() + r3.name = "named.rule.3" + r3.conditionSets.append([dict(name='axisName_a', minimum=500)]) + r3.subs.append(("c", "c.alt")) + + assert evaluateRule(r3, dict(axisName_a = 0)) == False + assert evaluateRule(r3, dict(axisName_a = 1000)) == True + assert evaluateRule(r3, dict(axisName_a = 1000)) == True + + # rule with only a minimum, maximum in separate conditions + r4 = RuleDescriptor() + r4.name = "named.rule.4" + r4.conditionSets.append([ + dict(name='axisName_a', minimum=500), + dict(name='axisName_b', maximum=500) + ]) + r4.subs.append(("c", "c.alt")) + + assert evaluateRule(r4, dict(axisName_a = 1000, axisName_b = 0)) == True + assert evaluateRule(r4, dict(axisName_a = 0, axisName_b = 0)) == False + assert evaluateRule(r4, dict(axisName_a = 1000, axisName_b = 1000)) == False + +def test_rulesDocument(tmpdir): + # tests of rules in a document, roundtripping. + tmpdir = str(tmpdir) + testDocPath = os.path.join(tmpdir, "testRules.designspace") + testDocPath2 = os.path.join(tmpdir, "testRules_roundtrip.designspace") + doc = DesignSpaceDocument() + a1 = AxisDescriptor() + a1.minimum = 0 + a1.maximum = 1000 + a1.default = 0 + a1.name = "axisName_a" + a1.tag = "TAGA" + b1 = AxisDescriptor() + b1.minimum = 2000 + b1.maximum = 3000 + b1.default = 2000 + b1.name = "axisName_b" + b1.tag = "TAGB" + doc.addAxis(a1) + doc.addAxis(b1) + r1 = RuleDescriptor() + r1.name = "named.rule.1" + r1.conditionSets.append([ + dict(name='axisName_a', minimum=0, maximum=1000), + dict(name='axisName_b', minimum=0, maximum=3000) + ]) + r1.subs.append(("a", "a.alt")) + # rule with minium and maximum + doc.addRule(r1) + assert len(doc.rules) == 1 + assert len(doc.rules[0].conditionSets) == 1 + assert len(doc.rules[0].conditionSets[0]) == 2 + assert _axesAsDict(doc.axes) == {'axisName_a': {'map': [], 'name': 'axisName_a', 'default': 0, 'minimum': 0, 'maximum': 1000, 'tag': 'TAGA'}, 'axisName_b': {'map': [], 'name': 'axisName_b', 'default': 2000, 'minimum': 2000, 'maximum': 3000, 'tag': 'TAGB'}} + assert doc.rules[0].conditionSets == [[ + {'minimum': 0, 'maximum': 1000, 'name': 'axisName_a'}, + {'minimum': 0, 'maximum': 3000, 'name': 'axisName_b'}]] + assert doc.rules[0].subs == [('a', 'a.alt')] + doc.normalize() + assert doc.rules[0].name == 'named.rule.1' + assert doc.rules[0].conditionSets == [[ + {'minimum': 0.0, 'maximum': 1.0, 'name': 'axisName_a'}, + {'minimum': 0.0, 'maximum': 1.0, 'name': 'axisName_b'}]] + # still one conditionset + assert len(doc.rules[0].conditionSets) == 1 + doc.write(testDocPath) + # add a stray conditionset + _addUnwrappedCondition(testDocPath) + doc2 = DesignSpaceDocument() + doc2.read(testDocPath) + assert len(doc2.axes) == 2 + assert len(doc2.rules) == 1 + assert len(doc2.rules[0].conditionSets) == 2 + doc2.write(testDocPath2) + # verify these results + # make sure the stray condition is now neatly wrapped in a conditionset. + doc3 = DesignSpaceDocument() + doc3.read(testDocPath2) + assert len(doc3.rules) == 1 + assert len(doc3.rules[0].conditionSets) == 2 + +def _addUnwrappedCondition(path): + # only for testing, so we can make an invalid designspace file + # older designspace files may have conditions that are not wrapped in a conditionset + # These can be read into a new conditionset. + f = open(path, 'r', encoding='utf-8') + d = f.read() + print(d) + f.close() + d = d.replace('<rule name="named.rule.1">', '<rule name="named.rule.1">\n\t<condition maximum="22" minimum="33" name="axisName_a" />') + f = open(path, 'w', encoding='utf-8') + f.write(d) + f.close() + +def test_documentLib(tmpdir): + # roundtrip test of the document lib with some nested data + tmpdir = str(tmpdir) + testDocPath1 = os.path.join(tmpdir, "testDocumentLibTest.designspace") + doc = DesignSpaceDocument() + a1 = AxisDescriptor() + a1.tag = "TAGA" + a1.name = "axisName_a" + a1.minimum = 0 + a1.maximum = 1000 + a1.default = 0 + doc.addAxis(a1) + dummyData = dict(a=123, b=u"äbc", c=[1,2,3], d={'a':123}) + dummyKey = "org.fontTools.designspaceLib" + doc.lib = {dummyKey: dummyData} + doc.write(testDocPath1) + new = DesignSpaceDocument() + new.read(testDocPath1) + assert dummyKey in new.lib + assert new.lib[dummyKey] == dummyData + diff --git a/Tests/encodings/codecs_test.py b/Tests/encodings/codecs_test.py new file mode 100644 index 0000000..29e3a0d --- /dev/null +++ b/Tests/encodings/codecs_test.py @@ -0,0 +1,26 @@ +from __future__ import print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +import unittest +import fontTools.encodings.codecs # Not to be confused with "import codecs" + +class ExtendedCodecsTest(unittest.TestCase): + + def test_decode_mac_japanese(self): + self.assertEqual(b'x\xfe\xfdy'.decode("x_mac_japanese_ttx"), + unichr(0x78)+unichr(0x2122)+unichr(0x00A9)+unichr(0x79)) + + def test_encode_mac_japanese(self): + self.assertEqual(b'x\xfe\xfdy', + (unichr(0x78)+unichr(0x2122)+unichr(0x00A9)+unichr(0x79)).encode("x_mac_japanese_ttx")) + + def test_decode_mac_trad_chinese(self): + self.assertEqual(b'\x80'.decode("x_mac_trad_chinese_ttx"), + unichr(0x5C)) + + def test_decode_mac_romanian(self): + self.assertEqual(b'x\xfb'.decode("mac_romanian"), + unichr(0x78)+unichr(0x02DA)) + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/feaLib/__init__.py b/Tests/feaLib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py new file mode 100644 index 0000000..e831f97 --- /dev/null +++ b/Tests/feaLib/builder_test.py @@ -0,0 +1,528 @@ +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc.loggingTools import CapturingLogHandler +from fontTools.feaLib.builder import Builder, addOpenTypeFeatures, \ + addOpenTypeFeaturesFromString +from fontTools.feaLib.error import FeatureLibError +from fontTools.ttLib import TTFont +from fontTools.feaLib.parser import Parser +from fontTools.feaLib import ast +from fontTools.feaLib.lexer import Lexer +import difflib +import os +import shutil +import sys +import tempfile +import logging +import unittest + + +def makeTTFont(): + glyphs = """ + .notdef space slash fraction semicolon period comma ampersand + quotedblleft quotedblright quoteleft quoteright + zero one two three four five six seven eight nine + zero.oldstyle one.oldstyle two.oldstyle three.oldstyle + four.oldstyle five.oldstyle six.oldstyle seven.oldstyle + eight.oldstyle nine.oldstyle onequarter onehalf threequarters + onesuperior twosuperior threesuperior ordfeminine ordmasculine + A B C D E F G H I J K L M N O P Q R S T U V W X Y Z + a b c d e f g h i j k l m n o p q r s t u v w x y z + A.sc B.sc C.sc D.sc E.sc F.sc G.sc H.sc I.sc J.sc K.sc L.sc M.sc + N.sc O.sc P.sc Q.sc R.sc S.sc T.sc U.sc V.sc W.sc X.sc Y.sc Z.sc + A.alt1 A.alt2 A.alt3 B.alt1 B.alt2 B.alt3 C.alt1 C.alt2 C.alt3 + a.alt1 a.alt2 a.alt3 a.end b.alt c.mid d.alt d.mid + e.begin e.mid e.end m.begin n.end s.end z.end + Eng Eng.alt1 Eng.alt2 Eng.alt3 + A.swash B.swash C.swash D.swash E.swash F.swash G.swash H.swash + I.swash J.swash K.swash L.swash M.swash N.swash O.swash P.swash + Q.swash R.swash S.swash T.swash U.swash V.swash W.swash X.swash + Y.swash Z.swash + f_l c_h c_k c_s c_t f_f f_f_i f_f_l f_i o_f_f_i s_t f_i.begin + a_n_d T_h T_h.swash germandbls ydieresis yacute breve + grave acute dieresis macron circumflex cedilla umlaut ogonek caron + damma hamza sukun kasratan lam_meem_jeem noon.final noon.initial + by feature lookup sub table + """.split() + font = TTFont() + font.setGlyphOrder(glyphs) + return font + + +class BuilderTest(unittest.TestCase): + # Feature files in data/*.fea; output gets compared to data/*.ttx. + TEST_FEATURE_FILES = """ + Attach enum markClass language_required + GlyphClassDef LigatureCaretByIndex LigatureCaretByPos + lookup lookupflag feature_aalt ignore_pos + GPOS_1 GPOS_1_zero GPOS_2 GPOS_2b GPOS_3 GPOS_4 GPOS_5 GPOS_6 GPOS_8 + GSUB_2 GSUB_3 GSUB_6 GSUB_8 + spec4h1 spec4h2 spec5d1 spec5d2 spec5fi1 spec5fi2 spec5fi3 spec5fi4 + spec5f_ii_1 spec5f_ii_2 spec5f_ii_3 spec5f_ii_4 + spec5h1 spec6b_ii spec6d2 spec6e spec6f + spec6h_ii spec6h_iii_1 spec6h_iii_3d spec8a spec8b spec8c spec8d + spec9a spec9b spec9c1 spec9c2 spec9c3 spec9d spec9e spec9f spec9g + spec10 + bug453 bug457 bug463 bug501 bug502 bug504 bug505 bug506 bug509 + bug512 bug514 bug568 bug633 + name size size2 multiple_feature_blocks omitted_GlyphClassDef + ZeroValue_SinglePos_horizontal ZeroValue_SinglePos_vertical + ZeroValue_PairPos_horizontal ZeroValue_PairPos_vertical + ZeroValue_ChainSinglePos_horizontal ZeroValue_ChainSinglePos_vertical + PairPosSubtable + """.split() + + def __init__(self, methodName): + unittest.TestCase.__init__(self, methodName) + # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, + # and fires deprecation warnings if a program uses the old name. + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + + def setUp(self): + self.tempdir = None + self.num_tempfiles = 0 + + def tearDown(self): + if self.tempdir: + shutil.rmtree(self.tempdir) + + @staticmethod + def getpath(testfile): + path, _ = os.path.split(__file__) + return os.path.join(path, "data", testfile) + + def temp_path(self, suffix): + if not self.tempdir: + self.tempdir = tempfile.mkdtemp() + self.num_tempfiles += 1 + return os.path.join(self.tempdir, + "tmp%d%s" % (self.num_tempfiles, suffix)) + + def read_ttx(self, path): + lines = [] + with open(path, "r", encoding="utf-8") as ttx: + for line in ttx.readlines(): + # Elide ttFont attributes because ttLibVersion may change, + # and use os-native line separators so we can run difflib. + if line.startswith("<ttFont "): + lines.append("<ttFont>" + os.linesep) + else: + lines.append(line.rstrip() + os.linesep) + return lines + + def expect_ttx(self, font, expected_ttx): + path = self.temp_path(suffix=".ttx") + font.saveXML(path, tables=['head', 'name', 'BASE', 'GDEF', 'GSUB', + 'GPOS', 'OS/2', 'hhea', 'vhea']) + actual = self.read_ttx(path) + expected = self.read_ttx(expected_ttx) + if actual != expected: + for line in difflib.unified_diff( + expected, actual, fromfile=expected_ttx, tofile=path): + sys.stderr.write(line) + self.fail("TTX output is different from expected") + + def build(self, featureFile, tables=None): + font = makeTTFont() + addOpenTypeFeaturesFromString(font, featureFile, tables=tables) + return font + + def check_feature_file(self, name): + font = makeTTFont() + addOpenTypeFeatures(font, self.getpath("%s.fea" % name)) + self.expect_ttx(font, self.getpath("%s.ttx" % name)) + # Make sure we can produce binary OpenType tables, not just XML. + for tag in ('GDEF', 'GSUB', 'GPOS'): + if tag in font: + font[tag].compile(font) + + def check_fea2fea_file(self, name, base=None, parser=Parser): + font = makeTTFont() + fname = (name + ".fea") if '.' not in name else name + p = parser(self.getpath(fname), glyphNames=font.getGlyphOrder()) + doc = p.parse() + actual = self.normal_fea(doc.asFea().split("\n")) + with open(self.getpath(base or fname), "r", encoding="utf-8") as ofile: + expected = self.normal_fea(ofile.readlines()) + + if expected != actual: + fname = name.rsplit(".", 1)[0] + ".fea" + for line in difflib.unified_diff( + expected, actual, + fromfile=fname + " (expected)", + tofile=fname + " (actual)"): + sys.stderr.write(line+"\n") + self.fail("Fea2Fea output is different from expected. " + "Generated:\n{}\n".format("\n".join(actual))) + + def normal_fea(self, lines): + output = [] + skip = 0 + for l in lines: + l = l.strip() + if l.startswith("#test-fea2fea:"): + if len(l) > 15: + output.append(l[15:].strip()) + skip = 1 + x = l.find("#") + if x >= 0: + l = l[:x].strip() + if not len(l): + continue + if skip > 0: + skip = skip - 1 + continue + output.append(l) + return output + + def test_alternateSubst_multipleSubstitutionsForSameGlyph(self): + self.assertRaisesRegex( + FeatureLibError, + "Already defined alternates for glyph \"A\"", + self.build, + "feature test {" + " sub A from [A.alt1 A.alt2];" + " sub B from [B.alt1 B.alt2 B.alt3];" + " sub A from [A.alt1 A.alt2];" + "} test;") + + def test_multipleSubst_multipleSubstitutionsForSameGlyph(self): + self.assertRaisesRegex( + FeatureLibError, + "Already defined substitution for glyph \"f_f_i\"", + self.build, + "feature test {" + " sub f_f_i by f f i;" + " sub c_t by c t;" + " sub f_f_i by f f i;" + "} test;") + + def test_pairPos_redefinition_warning(self): + # https://github.com/fonttools/fonttools/issues/1147 + logger = logging.getLogger("fontTools.feaLib.builder") + with CapturingLogHandler(logger, "WARNING") as captor: + # the pair "yacute semicolon" is redefined in the enum pos + font = self.build( + "@Y_LC = [y yacute ydieresis];" + "@SMALL_PUNC = [comma semicolon period];" + "feature kern {" + " pos yacute semicolon -70;" + " enum pos @Y_LC semicolon -80;" + " pos @Y_LC @SMALL_PUNC -100;" + "} kern;") + + captor.assertRegex("Already defined position for pair yacute semicolon") + + # the first definition prevails: yacute semicolon -70 + st = font["GPOS"].table.LookupList.Lookup[0].SubTable[0] + self.assertEqual(st.Coverage.glyphs[2], "yacute") + self.assertEqual(st.PairSet[2].PairValueRecord[0].SecondGlyph, + "semicolon") + self.assertEqual(vars(st.PairSet[2].PairValueRecord[0].Value1), + {"XAdvance": -70}) + + def test_singleSubst_multipleSubstitutionsForSameGlyph(self): + self.assertRaisesRegex( + FeatureLibError, + 'Already defined rule for replacing glyph "e" by "E.sc"', + self.build, + "feature test {" + " sub [a-z] by [A.sc-Z.sc];" + " sub e by e.fina;" + "} test;") + + def test_singlePos_redefinition(self): + self.assertRaisesRegex( + FeatureLibError, + "Already defined different position for glyph \"A\"", + self.build, "feature test { pos A 123; pos A 456; } test;") + + def test_feature_outside_aalt(self): + self.assertRaisesRegex( + FeatureLibError, + 'Feature references are only allowed inside "feature aalt"', + self.build, "feature test { feature test; } test;") + + def test_feature_undefinedReference(self): + self.assertRaisesRegex( + FeatureLibError, 'Feature none has not been defined', + self.build, "feature aalt { feature none; } aalt;") + + def test_GlyphClassDef_conflictingClasses(self): + self.assertRaisesRegex( + FeatureLibError, "Glyph X was assigned to a different class", + self.build, + "table GDEF {" + " GlyphClassDef [a b], [X], , ;" + " GlyphClassDef [a b X], , , ;" + "} GDEF;") + + def test_languagesystem(self): + builder = Builder(makeTTFont(), (None, None)) + builder.add_language_system(None, 'latn', 'FRA') + builder.add_language_system(None, 'cyrl', 'RUS') + builder.start_feature(location=None, name='test') + self.assertEqual(builder.language_systems, + {('latn', 'FRA'), ('cyrl', 'RUS')}) + + def test_languagesystem_duplicate(self): + self.assertRaisesRegex( + FeatureLibError, + '"languagesystem cyrl RUS" has already been specified', + self.build, "languagesystem cyrl RUS; languagesystem cyrl RUS;") + + def test_languagesystem_none_specified(self): + builder = Builder(makeTTFont(), (None, None)) + builder.start_feature(location=None, name='test') + self.assertEqual(builder.language_systems, {('DFLT', 'dflt')}) + + def test_languagesystem_DFLT_dflt_not_first(self): + self.assertRaisesRegex( + FeatureLibError, + "If \"languagesystem DFLT dflt\" is present, " + "it must be the first of the languagesystem statements", + self.build, "languagesystem latn TRK; languagesystem DFLT dflt;") + + def test_script(self): + builder = Builder(makeTTFont(), (None, None)) + builder.start_feature(location=None, name='test') + builder.set_script(location=None, script='cyrl') + self.assertEqual(builder.language_systems, {('cyrl', 'dflt')}) + + def test_script_in_aalt_feature(self): + self.assertRaisesRegex( + FeatureLibError, + "Script statements are not allowed within \"feature aalt\"", + self.build, "feature aalt { script latn; } aalt;") + + def test_script_in_size_feature(self): + self.assertRaisesRegex( + FeatureLibError, + "Script statements are not allowed within \"feature size\"", + self.build, "feature size { script latn; } size;") + + def test_language(self): + builder = Builder(makeTTFont(), (None, None)) + builder.add_language_system(None, 'latn', 'FRA ') + builder.start_feature(location=None, name='test') + builder.set_script(location=None, script='cyrl') + builder.set_language(location=None, language='RUS ', + include_default=False, required=False) + self.assertEqual(builder.language_systems, {('cyrl', 'RUS ')}) + builder.set_language(location=None, language='BGR ', + include_default=True, required=False) + self.assertEqual(builder.language_systems, + {('cyrl', 'BGR ')}) + builder.start_feature(location=None, name='test2') + self.assertRaisesRegex( + FeatureLibError, + "Need non-DFLT script when using non-dflt language " + "\(was: \"FRA \"\)", + builder.set_language, None, 'FRA ', True, False) + + def test_language_in_aalt_feature(self): + self.assertRaisesRegex( + FeatureLibError, + "Language statements are not allowed within \"feature aalt\"", + self.build, "feature aalt { language FRA; } aalt;") + + def test_language_in_size_feature(self): + self.assertRaisesRegex( + FeatureLibError, + "Language statements are not allowed within \"feature size\"", + self.build, "feature size { language FRA; } size;") + + def test_language_required_duplicate(self): + self.assertRaisesRegex( + FeatureLibError, + r"Language FRA \(script latn\) has already specified " + "feature scmp as its required feature", + self.build, + "feature scmp {" + " script latn;" + " language FRA required;" + " language DEU required;" + " substitute [a-z] by [A.sc-Z.sc];" + "} scmp;" + "feature test {" + " script latn;" + " language FRA required;" + " substitute [a-z] by [A.sc-Z.sc];" + "} test;") + + def test_lookup_already_defined(self): + self.assertRaisesRegex( + FeatureLibError, + "Lookup \"foo\" has already been defined", + self.build, "lookup foo {} foo; lookup foo {} foo;") + + def test_lookup_multiple_flags(self): + self.assertRaisesRegex( + FeatureLibError, + "Within a named lookup block, all rules must be " + "of the same lookup type and flag", + self.build, + "lookup foo {" + " lookupflag 1;" + " sub f i by f_i;" + " lookupflag 2;" + " sub f f i by f_f_i;" + "} foo;") + + def test_lookup_multiple_types(self): + self.assertRaisesRegex( + FeatureLibError, + "Within a named lookup block, all rules must be " + "of the same lookup type and flag", + self.build, + "lookup foo {" + " sub f f i by f_f_i;" + " sub A from [A.alt1 A.alt2];" + "} foo;") + + def test_lookup_inside_feature_aalt(self): + self.assertRaisesRegex( + FeatureLibError, + "Lookup blocks cannot be placed inside 'aalt' features", + self.build, "feature aalt {lookup L {} L;} aalt;") + + def test_extensions(self): + class ast_BaseClass(ast.MarkClass): + def asFea(self, indent=""): + return "" + + class ast_BaseClassDefinition(ast.MarkClassDefinition): + def asFea(self, indent=""): + return "" + + class ast_MarkBasePosStatement(ast.MarkBasePosStatement): + def asFea(self, indent=""): + if isinstance(self.base, ast.MarkClassName): + res = "" + for bcd in self.base.markClass.definitions: + if res != "": + res += "\n{}".format(indent) + res += "pos base {} {}".format(bcd.glyphs.asFea(), bcd.anchor.asFea()) + for m in self.marks: + res += " mark @{}".format(m.name) + res += ";" + else: + res = "pos base {}".format(self.base.asFea()) + for a, m in self.marks: + res += " {} mark @{}".format(a.asFea(), m.name) + res += ";" + return res + + class testAst(object): + MarkBasePosStatement = ast_MarkBasePosStatement + def __getattr__(self, name): + return getattr(ast, name) + + class testParser(Parser): + def parse_position_base_(self, enumerated, vertical): + location = self.cur_token_location_ + self.expect_keyword_("base") + if enumerated: + raise FeatureLibError( + '"enumerate" is not allowed with ' + 'mark-to-base attachment positioning', + location) + base = self.parse_glyphclass_(accept_glyphname=True) + if self.next_token_ == "<": + marks = self.parse_anchor_marks_() + else: + marks = [] + while self.next_token_ == "mark": + self.expect_keyword_("mark") + m = self.expect_markClass_reference_() + marks.append(m) + self.expect_symbol_(";") + return self.ast.MarkBasePosStatement(base, marks, + location=location) + + def parseBaseClass(self): + if not hasattr(self.doc_, 'baseClasses'): + self.doc_.baseClasses = {} + location = self.cur_token_location_ + glyphs = self.parse_glyphclass_(accept_glyphname=True) + anchor = self.parse_anchor_() + name = self.expect_class_name_() + self.expect_symbol_(";") + baseClass = self.doc_.baseClasses.get(name) + if baseClass is None: + baseClass = ast_BaseClass(name) + self.doc_.baseClasses[name] = baseClass + self.glyphclasses_.define(name, baseClass) + bcdef = ast_BaseClassDefinition(baseClass, anchor, glyphs, + location=location) + baseClass.addDefinition(bcdef) + return bcdef + + extensions = { + 'baseClass' : lambda s : s.parseBaseClass() + } + ast = testAst() + + self.check_fea2fea_file( + "baseClass.feax", base="baseClass.fea", parser=testParser) + + def test_markClass_same_glyph_redefined(self): + self.assertRaisesRegex( + FeatureLibError, + "Glyph acute already defined", + self.build, + "markClass [acute] <anchor 350 0> @TOP_MARKS;"*2) + + def test_markClass_same_glyph_multiple_classes(self): + self.assertRaisesRegex( + FeatureLibError, + 'Glyph uni0327 cannot be in both @ogonek and @cedilla', + self.build, + "feature mark {" + " markClass [uni0327 uni0328] <anchor 0 0> @ogonek;" + " pos base [a] <anchor 399 0> mark @ogonek;" + " markClass [uni0327] <anchor 0 0> @cedilla;" + " pos base [a] <anchor 244 0> mark @cedilla;" + "} mark;") + + def test_build_specific_tables(self): + features = "feature liga {sub f i by f_i;} liga;" + font = self.build(features) + assert "GSUB" in font + + font2 = self.build(features, tables=set()) + assert "GSUB" not in font2 + + def test_build_unsupported_tables(self): + self.assertRaises(AssertionError, self.build, "", tables={"FOO"}) + + def test_build_pre_parsed_ast_featurefile(self): + f = UnicodeIO("feature liga {sub f i by f_i;} liga;") + tree = Parser(f).parse() + font = makeTTFont() + addOpenTypeFeatures(font, tree) + assert "GSUB" in font + + +def generate_feature_file_test(name): + return lambda self: self.check_feature_file(name) + + +for name in BuilderTest.TEST_FEATURE_FILES: + setattr(BuilderTest, "test_FeatureFile_%s" % name, + generate_feature_file_test(name)) + + +def generate_fea2fea_file_test(name): + return lambda self: self.check_fea2fea_file(name) + + +for name in BuilderTest.TEST_FEATURE_FILES: + setattr(BuilderTest, "test_Fea2feaFile_{}".format(name), + generate_fea2fea_file_test(name)) + + +if __name__ == "__main__": + sys.exit(unittest.main()) diff --git a/Tests/feaLib/data/Attach.fea b/Tests/feaLib/data/Attach.fea new file mode 100644 index 0000000..86266ee --- /dev/null +++ b/Tests/feaLib/data/Attach.fea @@ -0,0 +1,5 @@ +table GDEF { + Attach [a e] 7; + Attach a 23; + Attach a 23; +} GDEF; diff --git a/Tests/feaLib/data/Attach.ttx b/Tests/feaLib/data/Attach.ttx new file mode 100644 index 0000000..f72d6d8 --- /dev/null +++ b/Tests/feaLib/data/Attach.ttx @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GDEF> + <Version value="0x00010000"/> + <AttachList> + <Coverage> + <Glyph value="a"/> + <Glyph value="e"/> + </Coverage> + <!-- GlyphCount=2 --> + <AttachPoint index="0"> + <!-- PointCount=2 --> + <PointIndex index="0" value="7"/> + <PointIndex index="1" value="23"/> + </AttachPoint> + <AttachPoint index="1"> + <!-- PointCount=1 --> + <PointIndex index="0" value="7"/> + </AttachPoint> + </AttachList> + </GDEF> + +</ttFont> diff --git a/Tests/feaLib/data/GPOS_1.fea b/Tests/feaLib/data/GPOS_1.fea new file mode 100644 index 0000000..a9bccf2 --- /dev/null +++ b/Tests/feaLib/data/GPOS_1.fea @@ -0,0 +1,42 @@ +languagesystem DFLT dflt; + +@sevenEightNine = [seven eight nine]; + +feature kern { + pos zero 0; + + pos [one two three] <-80 0 -160 0>; + pos A <1 2 3 4 <device 11 111, 12 112> <device 13 113, 14 114> <device 16 116> <device NULL>>; + pos B <1 2 3 4 <device 11 111, 12 112> <device 13 113, 14 114> <device 16 116> <device NULL>>; + pos C <1 2 3 4 <device 11 -2, 14 1> <device 13 -3, 15 1> <device 11 -8, 14 7> <device 13 8, 15 1>>; + pos four 400; + pos four.oldstyle 401; + pos five <-80 0 -160 0>; + pos six -200; + pos @sevenEightNine -100; + + pos P <1 0 800 0>; + pos Q <1 0 801 0>; + pos R <1 0 802 0>; + pos S <1 1 803 0>; + pos T <1 1 804 0>; + pos U <1 1 805 0>; + + # The AFDKO makeotf tool accepts re-definitions of previously defined + # single adjustment positionings, provided the re-definition is using + # the same value. We replicate this behavior. + pos four 400; + pos four <0 0 400 0>; + pos nine -100; +} kern; + +# According to the OpenType Feature File specification section 2.e.iv, +# the following should be interpreted as vertical advance adjustment +# because -100 (a value record format A) appears within a ‘vkrn’ feature. +# However, the AFDKO makeotf tool v2.0.90 (built on Nov 19, 2015) still +# makes it a horizontal advance adjustment. In our implementation, +# we follow the specification, so we produce different output than makeotf. +# https://github.com/adobe-type-tools/afdko/issues/85 +feature vkrn { + pos A -100; +} vkrn; diff --git a/Tests/feaLib/data/GPOS_1.ttx b/Tests/feaLib/data/GPOS_1.ttx new file mode 100644 index 0000000..bd32013 --- /dev/null +++ b/Tests/feaLib/data/GPOS_1.ttx @@ -0,0 +1,180 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=2 --> + <FeatureIndex index="0" value="0"/> + <FeatureIndex index="1" value="1"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=2 --> + <FeatureRecord index="0"> + <FeatureTag value="kern"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="1"> + <FeatureTag value="vkrn"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="1"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=8 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="one"/> + <Glyph value="two"/> + <Glyph value="three"/> + <Glyph value="five"/> + </Coverage> + <ValueFormat value="5"/> + <Value XPlacement="-80" XAdvance="-160"/> + </SinglePos> + <SinglePos index="1" Format="2"> + <Coverage> + <Glyph value="four"/> + <Glyph value="six"/> + <Glyph value="four.oldstyle"/> + </Coverage> + <ValueFormat value="4"/> + <!-- ValueCount=3 --> + <Value index="0" XAdvance="400"/> + <Value index="1" XAdvance="-200"/> + <Value index="2" XAdvance="401"/> + </SinglePos> + <SinglePos index="2" Format="1"> + <Coverage> + <Glyph value="seven"/> + <Glyph value="eight"/> + <Glyph value="nine"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="-100"/> + </SinglePos> + <SinglePos index="3" Format="2"> + <Coverage> + <Glyph value="P"/> + <Glyph value="Q"/> + <Glyph value="R"/> + </Coverage> + <ValueFormat value="5"/> + <!-- ValueCount=3 --> + <Value index="0" XPlacement="1" XAdvance="800"/> + <Value index="1" XPlacement="1" XAdvance="801"/> + <Value index="2" XPlacement="1" XAdvance="802"/> + </SinglePos> + <SinglePos index="4" Format="2"> + <Coverage> + <Glyph value="S"/> + <Glyph value="T"/> + <Glyph value="U"/> + </Coverage> + <ValueFormat value="7"/> + <!-- ValueCount=3 --> + <Value index="0" XPlacement="1" YPlacement="1" XAdvance="803"/> + <Value index="1" XPlacement="1" YPlacement="1" XAdvance="804"/> + <Value index="2" XPlacement="1" YPlacement="1" XAdvance="805"/> + </SinglePos> + <SinglePos index="5" Format="1"> + <Coverage> + <Glyph value="A"/> + <Glyph value="B"/> + </Coverage> + <ValueFormat value="127"/> + <Value XPlacement="1" YPlacement="2" XAdvance="3" YAdvance="4"> + <XPlaDevice> + <StartSize value="11"/> + <EndSize value="12"/> + <DeltaFormat value="3"/> + <DeltaValue value="[111, 112]"/> + </XPlaDevice> + <YPlaDevice> + <StartSize value="13"/> + <EndSize value="14"/> + <DeltaFormat value="3"/> + <DeltaValue value="[113, 114]"/> + </YPlaDevice> + <XAdvDevice> + <StartSize value="16"/> + <EndSize value="16"/> + <DeltaFormat value="3"/> + <DeltaValue value="[116]"/> + </XAdvDevice> + </Value> + </SinglePos> + <SinglePos index="6" Format="1"> + <Coverage> + <Glyph value="zero"/> + </Coverage> + <ValueFormat value="0"/> + </SinglePos> + <SinglePos index="7" Format="1"> + <Coverage> + <Glyph value="C"/> + </Coverage> + <ValueFormat value="255"/> + <Value XPlacement="1" YPlacement="2" XAdvance="3" YAdvance="4"> + <XPlaDevice> + <StartSize value="11"/> + <EndSize value="14"/> + <DeltaFormat value="1"/> + <DeltaValue value="[-2, 0, 0, 1]"/> + </XPlaDevice> + <YPlaDevice> + <StartSize value="13"/> + <EndSize value="15"/> + <DeltaFormat value="2"/> + <DeltaValue value="[-3, 0, 1]"/> + </YPlaDevice> + <XAdvDevice> + <StartSize value="11"/> + <EndSize value="14"/> + <DeltaFormat value="2"/> + <DeltaValue value="[-8, 0, 0, 7]"/> + </XAdvDevice> + <YAdvDevice> + <StartSize value="13"/> + <EndSize value="15"/> + <DeltaFormat value="3"/> + <DeltaValue value="[8, 0, 1]"/> + </YAdvDevice> + </Value> + </SinglePos> + </Lookup> + <Lookup index="1"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="A"/> + </Coverage> + <ValueFormat value="8"/> + <Value YAdvance="-100"/> + </SinglePos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/GPOS_1_zero.fea b/Tests/feaLib/data/GPOS_1_zero.fea new file mode 100644 index 0000000..bfb51db --- /dev/null +++ b/Tests/feaLib/data/GPOS_1_zero.fea @@ -0,0 +1,5 @@ +# https://github.com/behdad/fonttools/issues/471 +feature test { + pos zero 0; + pos four 500; +} test; \ No newline at end of file diff --git a/Tests/feaLib/data/GPOS_1_zero.ttx b/Tests/feaLib/data/GPOS_1_zero.ttx new file mode 100644 index 0000000..b02db67 --- /dev/null +++ b/Tests/feaLib/data/GPOS_1_zero.ttx @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=2 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="zero"/> + </Coverage> + <ValueFormat value="0"/> + </SinglePos> + <SinglePos index="1" Format="1"> + <Coverage> + <Glyph value="four"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="500"/> + </SinglePos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/GPOS_2.fea b/Tests/feaLib/data/GPOS_2.fea new file mode 100644 index 0000000..b1f26d1 --- /dev/null +++ b/Tests/feaLib/data/GPOS_2.fea @@ -0,0 +1,30 @@ +languagesystem DFLT dflt; + +# Mixes kerning between single glyphs, and class-based kerning. +# https://github.com/behdad/fonttools/issues/456 +lookup MixedKerning { + pos v v 14; + pos [D O Q] [T V W] -26; +} MixedKerning; + +lookup GlyphKerning { + pos T one 100; + pos T two 200; + pos T two.oldstyle 200; + pos T three 300; + pos T four 400; + pos X a 100; + pos X b 200; + pos Y a 100; + pos Y b 200; + pos Y c <3 3 3 3>; +} GlyphKerning; + +feature kern { + lookup GlyphKerning; + lookup MixedKerning; +} kern; + +feature vkrn { + pos T one 100; +} vkrn; diff --git a/Tests/feaLib/data/GPOS_2.ttx b/Tests/feaLib/data/GPOS_2.ttx new file mode 100644 index 0000000..84dc819 --- /dev/null +++ b/Tests/feaLib/data/GPOS_2.ttx @@ -0,0 +1,184 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=2 --> + <FeatureIndex index="0" value="0"/> + <FeatureIndex index="1" value="1"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=2 --> + <FeatureRecord index="0"> + <FeatureTag value="kern"/> + <Feature> + <!-- LookupCount=2 --> + <LookupListIndex index="0" value="1"/> + <LookupListIndex index="1" value="0"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="1"> + <FeatureTag value="vkrn"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="2"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=3 --> + <Lookup index="0"> + <LookupType value="2"/> + <LookupFlag value="0"/> + <!-- SubTableCount=2 --> + <PairPos index="0" Format="1"> + <Coverage> + <Glyph value="v"/> + </Coverage> + <ValueFormat1 value="4"/> + <ValueFormat2 value="0"/> + <!-- PairSetCount=1 --> + <PairSet index="0"> + <!-- PairValueCount=1 --> + <PairValueRecord index="0"> + <SecondGlyph value="v"/> + <Value1 XAdvance="14"/> + </PairValueRecord> + </PairSet> + </PairPos> + <PairPos index="1" Format="2"> + <Coverage> + <Glyph value="D"/> + <Glyph value="O"/> + <Glyph value="Q"/> + </Coverage> + <ValueFormat1 value="4"/> + <ValueFormat2 value="0"/> + <ClassDef1> + </ClassDef1> + <ClassDef2> + <ClassDef glyph="T" class="1"/> + <ClassDef glyph="V" class="1"/> + <ClassDef glyph="W" class="1"/> + </ClassDef2> + <!-- Class1Count=1 --> + <!-- Class2Count=2 --> + <Class1Record index="0"> + <Class2Record index="0"> + </Class2Record> + <Class2Record index="1"> + <Value1 XAdvance="-26"/> + </Class2Record> + </Class1Record> + </PairPos> + </Lookup> + <Lookup index="1"> + <LookupType value="2"/> + <LookupFlag value="0"/> + <!-- SubTableCount=2 --> + <PairPos index="0" Format="1"> + <Coverage> + <Glyph value="T"/> + <Glyph value="X"/> + <Glyph value="Y"/> + </Coverage> + <ValueFormat1 value="4"/> + <ValueFormat2 value="0"/> + <!-- PairSetCount=3 --> + <PairSet index="0"> + <!-- PairValueCount=5 --> + <PairValueRecord index="0"> + <SecondGlyph value="one"/> + <Value1 XAdvance="100"/> + </PairValueRecord> + <PairValueRecord index="1"> + <SecondGlyph value="two"/> + <Value1 XAdvance="200"/> + </PairValueRecord> + <PairValueRecord index="2"> + <SecondGlyph value="three"/> + <Value1 XAdvance="300"/> + </PairValueRecord> + <PairValueRecord index="3"> + <SecondGlyph value="four"/> + <Value1 XAdvance="400"/> + </PairValueRecord> + <PairValueRecord index="4"> + <SecondGlyph value="two.oldstyle"/> + <Value1 XAdvance="200"/> + </PairValueRecord> + </PairSet> + <PairSet index="1"> + <!-- PairValueCount=2 --> + <PairValueRecord index="0"> + <SecondGlyph value="a"/> + <Value1 XAdvance="100"/> + </PairValueRecord> + <PairValueRecord index="1"> + <SecondGlyph value="b"/> + <Value1 XAdvance="200"/> + </PairValueRecord> + </PairSet> + <PairSet index="2"> + <!-- PairValueCount=2 --> + <PairValueRecord index="0"> + <SecondGlyph value="a"/> + <Value1 XAdvance="100"/> + </PairValueRecord> + <PairValueRecord index="1"> + <SecondGlyph value="b"/> + <Value1 XAdvance="200"/> + </PairValueRecord> + </PairSet> + </PairPos> + <PairPos index="1" Format="1"> + <Coverage> + <Glyph value="Y"/> + </Coverage> + <ValueFormat1 value="15"/> + <ValueFormat2 value="0"/> + <!-- PairSetCount=1 --> + <PairSet index="0"> + <!-- PairValueCount=1 --> + <PairValueRecord index="0"> + <SecondGlyph value="c"/> + <Value1 XPlacement="3" YPlacement="3" XAdvance="3" YAdvance="3"/> + </PairValueRecord> + </PairSet> + </PairPos> + </Lookup> + <Lookup index="2"> + <LookupType value="2"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <PairPos index="0" Format="1"> + <Coverage> + <Glyph value="T"/> + </Coverage> + <ValueFormat1 value="8"/> + <ValueFormat2 value="0"/> + <!-- PairSetCount=1 --> + <PairSet index="0"> + <!-- PairValueCount=1 --> + <PairValueRecord index="0"> + <SecondGlyph value="one"/> + <Value1 YAdvance="100"/> + </PairValueRecord> + </PairSet> + </PairPos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/GPOS_2b.fea b/Tests/feaLib/data/GPOS_2b.fea new file mode 100644 index 0000000..da9fd84 --- /dev/null +++ b/Tests/feaLib/data/GPOS_2b.fea @@ -0,0 +1,9 @@ +@PUNC = [comma semicolon period]; + +feature kern { + pos [A] @PUNC 1; + pos [B C] [comma] 2; + pos [D E F] [comma] 3; + pos [D E F] [semicolon period] 4; + pos [G] @PUNC <5 5 5 5>; +} kern; diff --git a/Tests/feaLib/data/GPOS_2b.ttx b/Tests/feaLib/data/GPOS_2b.ttx new file mode 100644 index 0000000..40f458f --- /dev/null +++ b/Tests/feaLib/data/GPOS_2b.ttx @@ -0,0 +1,127 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="kern"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="2"/> + <LookupFlag value="0"/> + <!-- SubTableCount=3 --> + <PairPos index="0" Format="2"> + <Coverage> + <Glyph value="A"/> + </Coverage> + <ValueFormat1 value="4"/> + <ValueFormat2 value="0"/> + <ClassDef1> + </ClassDef1> + <ClassDef2> + <ClassDef glyph="comma" class="1"/> + <ClassDef glyph="period" class="1"/> + <ClassDef glyph="semicolon" class="1"/> + </ClassDef2> + <!-- Class1Count=1 --> + <!-- Class2Count=2 --> + <Class1Record index="0"> + <Class2Record index="0"> + </Class2Record> + <Class2Record index="1"> + <Value1 XAdvance="1"/> + </Class2Record> + </Class1Record> + </PairPos> + <PairPos index="1" Format="2"> + <Coverage> + <Glyph value="B"/> + <Glyph value="C"/> + <Glyph value="D"/> + <Glyph value="E"/> + <Glyph value="F"/> + </Coverage> + <ValueFormat1 value="4"/> + <ValueFormat2 value="0"/> + <ClassDef1> + <ClassDef glyph="B" class="1"/> + <ClassDef glyph="C" class="1"/> + </ClassDef1> + <ClassDef2> + <ClassDef glyph="comma" class="2"/> + <ClassDef glyph="period" class="1"/> + <ClassDef glyph="semicolon" class="1"/> + </ClassDef2> + <!-- Class1Count=2 --> + <!-- Class2Count=3 --> + <Class1Record index="0"> + <Class2Record index="0"> + </Class2Record> + <Class2Record index="1"> + <Value1 XAdvance="4"/> + </Class2Record> + <Class2Record index="2"> + <Value1 XAdvance="3"/> + </Class2Record> + </Class1Record> + <Class1Record index="1"> + <Class2Record index="0"> + </Class2Record> + <Class2Record index="1"> + </Class2Record> + <Class2Record index="2"> + <Value1 XAdvance="2"/> + </Class2Record> + </Class1Record> + </PairPos> + <PairPos index="2" Format="2"> + <Coverage> + <Glyph value="G"/> + </Coverage> + <ValueFormat1 value="15"/> + <ValueFormat2 value="0"/> + <ClassDef1> + </ClassDef1> + <ClassDef2> + <ClassDef glyph="comma" class="1"/> + <ClassDef glyph="period" class="1"/> + <ClassDef glyph="semicolon" class="1"/> + </ClassDef2> + <!-- Class1Count=1 --> + <!-- Class2Count=2 --> + <Class1Record index="0"> + <Class2Record index="0"> + </Class2Record> + <Class2Record index="1"> + <Value1 XPlacement="5" YPlacement="5" XAdvance="5" YAdvance="5"/> + </Class2Record> + </Class1Record> + </PairPos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/GPOS_3.fea b/Tests/feaLib/data/GPOS_3.fea new file mode 100644 index 0000000..114ee16 --- /dev/null +++ b/Tests/feaLib/data/GPOS_3.fea @@ -0,0 +1,12 @@ +languagesystem DFLT dflt; + +anchorDef 3 4 contourpoint 2 ANCH342; + +feature kern { + pos cursive zero <anchor NULL> <anchor NULL>; + pos cursive one <anchor 121 -1> <anchor ANCH342>; + pos cursive two <anchor 122 -2> <anchor 3 4>; + pos cursive three <anchor 123 -3> <anchor NULL>; + pos cursive four <anchor 124 -4 contourpoint 7> <anchor 3 4>; + pos cursive five <anchor 124 -4 <device 8 1, 9 2> <device 7 3>> <anchor ANCH342>; +} kern; diff --git a/Tests/feaLib/data/GPOS_3.ttx b/Tests/feaLib/data/GPOS_3.ttx new file mode 100644 index 0000000..a29f30e --- /dev/null +++ b/Tests/feaLib/data/GPOS_3.ttx @@ -0,0 +1,114 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="kern"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="3"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <CursivePos index="0" Format="1"> + <Coverage> + <Glyph value="zero"/> + <Glyph value="one"/> + <Glyph value="two"/> + <Glyph value="three"/> + <Glyph value="four"/> + <Glyph value="five"/> + </Coverage> + <!-- EntryExitCount=6 --> + <EntryExitRecord index="0"> + </EntryExitRecord> + <EntryExitRecord index="1"> + <EntryAnchor Format="1"> + <XCoordinate value="121"/> + <YCoordinate value="-1"/> + </EntryAnchor> + <ExitAnchor Format="2"> + <XCoordinate value="3"/> + <YCoordinate value="4"/> + <AnchorPoint value="2"/> + </ExitAnchor> + </EntryExitRecord> + <EntryExitRecord index="2"> + <EntryAnchor Format="1"> + <XCoordinate value="122"/> + <YCoordinate value="-2"/> + </EntryAnchor> + <ExitAnchor Format="1"> + <XCoordinate value="3"/> + <YCoordinate value="4"/> + </ExitAnchor> + </EntryExitRecord> + <EntryExitRecord index="3"> + <EntryAnchor Format="1"> + <XCoordinate value="123"/> + <YCoordinate value="-3"/> + </EntryAnchor> + </EntryExitRecord> + <EntryExitRecord index="4"> + <EntryAnchor Format="2"> + <XCoordinate value="124"/> + <YCoordinate value="-4"/> + <AnchorPoint value="7"/> + </EntryAnchor> + <ExitAnchor Format="1"> + <XCoordinate value="3"/> + <YCoordinate value="4"/> + </ExitAnchor> + </EntryExitRecord> + <EntryExitRecord index="5"> + <EntryAnchor Format="3"> + <XCoordinate value="124"/> + <YCoordinate value="-4"/> + <XDeviceTable> + <StartSize value="8"/> + <EndSize value="9"/> + <DeltaFormat value="2"/> + <DeltaValue value="[1, 2]"/> + </XDeviceTable> + <YDeviceTable> + <StartSize value="7"/> + <EndSize value="7"/> + <DeltaFormat value="2"/> + <DeltaValue value="[3]"/> + </YDeviceTable> + </EntryAnchor> + <ExitAnchor Format="2"> + <XCoordinate value="3"/> + <YCoordinate value="4"/> + <AnchorPoint value="2"/> + </ExitAnchor> + </EntryExitRecord> + </CursivePos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/GPOS_4.fea b/Tests/feaLib/data/GPOS_4.fea new file mode 100644 index 0000000..cfd2d75 --- /dev/null +++ b/Tests/feaLib/data/GPOS_4.fea @@ -0,0 +1,12 @@ +languagesystem DFLT dflt; + +markClass [acute grave] <anchor 111 -11> @TOP_MARKS; +markClass macron <anchor 112 -12> @TOP_MARKS; +markClass [cedilla] <anchor 222 22> @BOTTOM_MARKS; +markClass [ogonek] <anchor 333 33> @SIDE_MARKS; + +feature test { + pos base a <anchor 11 1> mark @TOP_MARKS <anchor 12 -1> mark @BOTTOM_MARKS; + pos base [b c] <anchor 22 -2> mark @BOTTOM_MARKS; + pos base d <anchor 33 3> mark @SIDE_MARKS; +} test; diff --git a/Tests/feaLib/data/GPOS_4.ttx b/Tests/feaLib/data/GPOS_4.ttx new file mode 100644 index 0000000..65cf498 --- /dev/null +++ b/Tests/feaLib/data/GPOS_4.ttx @@ -0,0 +1,147 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GDEF> + <Version value="0x00010000"/> + <GlyphClassDef> + <ClassDef glyph="a" class="1"/> + <ClassDef glyph="acute" class="3"/> + <ClassDef glyph="b" class="1"/> + <ClassDef glyph="c" class="1"/> + <ClassDef glyph="cedilla" class="3"/> + <ClassDef glyph="d" class="1"/> + <ClassDef glyph="grave" class="3"/> + <ClassDef glyph="macron" class="3"/> + <ClassDef glyph="ogonek" class="3"/> + </GlyphClassDef> + </GDEF> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <MarkBasePos index="0" Format="1"> + <MarkCoverage> + <Glyph value="grave"/> + <Glyph value="acute"/> + <Glyph value="macron"/> + <Glyph value="cedilla"/> + <Glyph value="ogonek"/> + </MarkCoverage> + <BaseCoverage> + <Glyph value="a"/> + <Glyph value="b"/> + <Glyph value="c"/> + <Glyph value="d"/> + </BaseCoverage> + <!-- ClassCount=3 --> + <MarkArray> + <!-- MarkCount=5 --> + <MarkRecord index="0"> + <Class value="0"/> + <MarkAnchor Format="1"> + <XCoordinate value="111"/> + <YCoordinate value="-11"/> + </MarkAnchor> + </MarkRecord> + <MarkRecord index="1"> + <Class value="0"/> + <MarkAnchor Format="1"> + <XCoordinate value="111"/> + <YCoordinate value="-11"/> + </MarkAnchor> + </MarkRecord> + <MarkRecord index="2"> + <Class value="0"/> + <MarkAnchor Format="1"> + <XCoordinate value="112"/> + <YCoordinate value="-12"/> + </MarkAnchor> + </MarkRecord> + <MarkRecord index="3"> + <Class value="1"/> + <MarkAnchor Format="1"> + <XCoordinate value="222"/> + <YCoordinate value="22"/> + </MarkAnchor> + </MarkRecord> + <MarkRecord index="4"> + <Class value="2"/> + <MarkAnchor Format="1"> + <XCoordinate value="333"/> + <YCoordinate value="33"/> + </MarkAnchor> + </MarkRecord> + </MarkArray> + <BaseArray> + <!-- BaseCount=4 --> + <BaseRecord index="0"> + <BaseAnchor index="0" Format="1"> + <XCoordinate value="11"/> + <YCoordinate value="1"/> + </BaseAnchor> + <BaseAnchor index="1" Format="1"> + <XCoordinate value="12"/> + <YCoordinate value="-1"/> + </BaseAnchor> + <BaseAnchor index="2" empty="1"/> + </BaseRecord> + <BaseRecord index="1"> + <BaseAnchor index="0" empty="1"/> + <BaseAnchor index="1" Format="1"> + <XCoordinate value="22"/> + <YCoordinate value="-2"/> + </BaseAnchor> + <BaseAnchor index="2" empty="1"/> + </BaseRecord> + <BaseRecord index="2"> + <BaseAnchor index="0" empty="1"/> + <BaseAnchor index="1" Format="1"> + <XCoordinate value="22"/> + <YCoordinate value="-2"/> + </BaseAnchor> + <BaseAnchor index="2" empty="1"/> + </BaseRecord> + <BaseRecord index="3"> + <BaseAnchor index="0" empty="1"/> + <BaseAnchor index="1" empty="1"/> + <BaseAnchor index="2" Format="1"> + <XCoordinate value="33"/> + <YCoordinate value="3"/> + </BaseAnchor> + </BaseRecord> + </BaseArray> + </MarkBasePos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/GPOS_5.fea b/Tests/feaLib/data/GPOS_5.fea new file mode 100644 index 0000000..b116539 --- /dev/null +++ b/Tests/feaLib/data/GPOS_5.fea @@ -0,0 +1,18 @@ +markClass [acute grave] <anchor 500 700> @TOP_MARKS; +markClass macron <anchor 500 750> @TOP_MARKS; +markClass [cedilla] <anchor 500 -10> @BOTTOM_MARKS; +markClass [ogonek] <anchor 800 -10> @OGONEK; + +feature test { + + pos ligature [c_t s_t] <anchor 500 800> mark @TOP_MARKS <anchor 500 -200> mark @BOTTOM_MARKS + ligComponent <anchor 1500 800> mark @TOP_MARKS <anchor 1500 -200> mark @BOTTOM_MARKS <anchor 1550 0> mark @OGONEK; + + pos ligature f_l <anchor 300 800> mark @TOP_MARKS <anchor 300 -200> mark @BOTTOM_MARKS + ligComponent <anchor 600 800> mark @TOP_MARKS <anchor 600 -200> mark @BOTTOM_MARKS; + + pos ligature [f_f_l] <anchor 300 800> mark @TOP_MARKS <anchor 300 -200> mark @BOTTOM_MARKS + ligComponent <anchor 600 800> mark @TOP_MARKS <anchor 600 -200> mark @BOTTOM_MARKS + ligComponent <anchor 900 800> mark @TOP_MARKS <anchor 900 -200> mark @BOTTOM_MARKS; + +} test; diff --git a/Tests/feaLib/data/GPOS_5.ttx b/Tests/feaLib/data/GPOS_5.ttx new file mode 100644 index 0000000..f63ce30 --- /dev/null +++ b/Tests/feaLib/data/GPOS_5.ttx @@ -0,0 +1,229 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GDEF> + <Version value="0x00010000"/> + <GlyphClassDef> + <ClassDef glyph="acute" class="3"/> + <ClassDef glyph="c_t" class="2"/> + <ClassDef glyph="cedilla" class="3"/> + <ClassDef glyph="f_f_l" class="2"/> + <ClassDef glyph="f_l" class="2"/> + <ClassDef glyph="grave" class="3"/> + <ClassDef glyph="macron" class="3"/> + <ClassDef glyph="ogonek" class="3"/> + <ClassDef glyph="s_t" class="2"/> + </GlyphClassDef> + </GDEF> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="5"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <MarkLigPos index="0" Format="1"> + <MarkCoverage> + <Glyph value="grave"/> + <Glyph value="acute"/> + <Glyph value="macron"/> + <Glyph value="cedilla"/> + <Glyph value="ogonek"/> + </MarkCoverage> + <LigatureCoverage> + <Glyph value="f_l"/> + <Glyph value="c_t"/> + <Glyph value="f_f_l"/> + <Glyph value="s_t"/> + </LigatureCoverage> + <!-- ClassCount=3 --> + <MarkArray> + <!-- MarkCount=5 --> + <MarkRecord index="0"> + <Class value="0"/> + <MarkAnchor Format="1"> + <XCoordinate value="500"/> + <YCoordinate value="700"/> + </MarkAnchor> + </MarkRecord> + <MarkRecord index="1"> + <Class value="0"/> + <MarkAnchor Format="1"> + <XCoordinate value="500"/> + <YCoordinate value="700"/> + </MarkAnchor> + </MarkRecord> + <MarkRecord index="2"> + <Class value="0"/> + <MarkAnchor Format="1"> + <XCoordinate value="500"/> + <YCoordinate value="750"/> + </MarkAnchor> + </MarkRecord> + <MarkRecord index="3"> + <Class value="1"/> + <MarkAnchor Format="1"> + <XCoordinate value="500"/> + <YCoordinate value="-10"/> + </MarkAnchor> + </MarkRecord> + <MarkRecord index="4"> + <Class value="2"/> + <MarkAnchor Format="1"> + <XCoordinate value="800"/> + <YCoordinate value="-10"/> + </MarkAnchor> + </MarkRecord> + </MarkArray> + <LigatureArray> + <!-- LigatureCount=4 --> + <LigatureAttach index="0"> + <!-- ComponentCount=2 --> + <ComponentRecord index="0"> + <LigatureAnchor index="0" Format="1"> + <XCoordinate value="300"/> + <YCoordinate value="800"/> + </LigatureAnchor> + <LigatureAnchor index="1" Format="1"> + <XCoordinate value="300"/> + <YCoordinate value="-200"/> + </LigatureAnchor> + <LigatureAnchor index="2" empty="1"/> + </ComponentRecord> + <ComponentRecord index="1"> + <LigatureAnchor index="0" Format="1"> + <XCoordinate value="600"/> + <YCoordinate value="800"/> + </LigatureAnchor> + <LigatureAnchor index="1" Format="1"> + <XCoordinate value="600"/> + <YCoordinate value="-200"/> + </LigatureAnchor> + <LigatureAnchor index="2" empty="1"/> + </ComponentRecord> + </LigatureAttach> + <LigatureAttach index="1"> + <!-- ComponentCount=2 --> + <ComponentRecord index="0"> + <LigatureAnchor index="0" Format="1"> + <XCoordinate value="500"/> + <YCoordinate value="800"/> + </LigatureAnchor> + <LigatureAnchor index="1" Format="1"> + <XCoordinate value="500"/> + <YCoordinate value="-200"/> + </LigatureAnchor> + <LigatureAnchor index="2" empty="1"/> + </ComponentRecord> + <ComponentRecord index="1"> + <LigatureAnchor index="0" Format="1"> + <XCoordinate value="1500"/> + <YCoordinate value="800"/> + </LigatureAnchor> + <LigatureAnchor index="1" Format="1"> + <XCoordinate value="1500"/> + <YCoordinate value="-200"/> + </LigatureAnchor> + <LigatureAnchor index="2" Format="1"> + <XCoordinate value="1550"/> + <YCoordinate value="0"/> + </LigatureAnchor> + </ComponentRecord> + </LigatureAttach> + <LigatureAttach index="2"> + <!-- ComponentCount=3 --> + <ComponentRecord index="0"> + <LigatureAnchor index="0" Format="1"> + <XCoordinate value="300"/> + <YCoordinate value="800"/> + </LigatureAnchor> + <LigatureAnchor index="1" Format="1"> + <XCoordinate value="300"/> + <YCoordinate value="-200"/> + </LigatureAnchor> + <LigatureAnchor index="2" empty="1"/> + </ComponentRecord> + <ComponentRecord index="1"> + <LigatureAnchor index="0" Format="1"> + <XCoordinate value="600"/> + <YCoordinate value="800"/> + </LigatureAnchor> + <LigatureAnchor index="1" Format="1"> + <XCoordinate value="600"/> + <YCoordinate value="-200"/> + </LigatureAnchor> + <LigatureAnchor index="2" empty="1"/> + </ComponentRecord> + <ComponentRecord index="2"> + <LigatureAnchor index="0" Format="1"> + <XCoordinate value="900"/> + <YCoordinate value="800"/> + </LigatureAnchor> + <LigatureAnchor index="1" Format="1"> + <XCoordinate value="900"/> + <YCoordinate value="-200"/> + </LigatureAnchor> + <LigatureAnchor index="2" empty="1"/> + </ComponentRecord> + </LigatureAttach> + <LigatureAttach index="3"> + <!-- ComponentCount=2 --> + <ComponentRecord index="0"> + <LigatureAnchor index="0" Format="1"> + <XCoordinate value="500"/> + <YCoordinate value="800"/> + </LigatureAnchor> + <LigatureAnchor index="1" Format="1"> + <XCoordinate value="500"/> + <YCoordinate value="-200"/> + </LigatureAnchor> + <LigatureAnchor index="2" empty="1"/> + </ComponentRecord> + <ComponentRecord index="1"> + <LigatureAnchor index="0" Format="1"> + <XCoordinate value="1500"/> + <YCoordinate value="800"/> + </LigatureAnchor> + <LigatureAnchor index="1" Format="1"> + <XCoordinate value="1500"/> + <YCoordinate value="-200"/> + </LigatureAnchor> + <LigatureAnchor index="2" Format="1"> + <XCoordinate value="1550"/> + <YCoordinate value="0"/> + </LigatureAnchor> + </ComponentRecord> + </LigatureAttach> + </LigatureArray> + </MarkLigPos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/GPOS_6.fea b/Tests/feaLib/data/GPOS_6.fea new file mode 100644 index 0000000..37b2936 --- /dev/null +++ b/Tests/feaLib/data/GPOS_6.fea @@ -0,0 +1,10 @@ +languagesystem DFLT dflt; + +markClass [acute grave] <anchor 1 1 contourpoint 11> @TOP_MARKS; +markClass macron <anchor 2 2 contourpoint 22> @TOP_MARKS; +markClass [cedilla] <anchor 3 3 contourpoint 33> @BOTTOM_MARKS; + +feature test { + pos mark [acute grave macron ogonek] <anchor 500 200> mark @TOP_MARKS <anchor 500 -80> mark @BOTTOM_MARKS; + pos mark [dieresis caron] <anchor 500 200> mark @TOP_MARKS; +} test; diff --git a/Tests/feaLib/data/GPOS_6.ttx b/Tests/feaLib/data/GPOS_6.ttx new file mode 100644 index 0000000..c09fdf3 --- /dev/null +++ b/Tests/feaLib/data/GPOS_6.ttx @@ -0,0 +1,162 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GDEF> + <Version value="0x00010000"/> + <GlyphClassDef> + <ClassDef glyph="acute" class="3"/> + <ClassDef glyph="caron" class="3"/> + <ClassDef glyph="cedilla" class="3"/> + <ClassDef glyph="dieresis" class="3"/> + <ClassDef glyph="grave" class="3"/> + <ClassDef glyph="macron" class="3"/> + <ClassDef glyph="ogonek" class="3"/> + </GlyphClassDef> + </GDEF> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="6"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <MarkMarkPos index="0" Format="1"> + <Mark1Coverage> + <Glyph value="grave"/> + <Glyph value="acute"/> + <Glyph value="macron"/> + <Glyph value="cedilla"/> + </Mark1Coverage> + <Mark2Coverage> + <Glyph value="grave"/> + <Glyph value="acute"/> + <Glyph value="dieresis"/> + <Glyph value="macron"/> + <Glyph value="ogonek"/> + <Glyph value="caron"/> + </Mark2Coverage> + <!-- ClassCount=2 --> + <Mark1Array> + <!-- MarkCount=4 --> + <MarkRecord index="0"> + <Class value="0"/> + <MarkAnchor Format="2"> + <XCoordinate value="1"/> + <YCoordinate value="1"/> + <AnchorPoint value="11"/> + </MarkAnchor> + </MarkRecord> + <MarkRecord index="1"> + <Class value="0"/> + <MarkAnchor Format="2"> + <XCoordinate value="1"/> + <YCoordinate value="1"/> + <AnchorPoint value="11"/> + </MarkAnchor> + </MarkRecord> + <MarkRecord index="2"> + <Class value="0"/> + <MarkAnchor Format="2"> + <XCoordinate value="2"/> + <YCoordinate value="2"/> + <AnchorPoint value="22"/> + </MarkAnchor> + </MarkRecord> + <MarkRecord index="3"> + <Class value="1"/> + <MarkAnchor Format="2"> + <XCoordinate value="3"/> + <YCoordinate value="3"/> + <AnchorPoint value="33"/> + </MarkAnchor> + </MarkRecord> + </Mark1Array> + <Mark2Array> + <!-- Mark2Count=6 --> + <Mark2Record index="0"> + <Mark2Anchor index="0" Format="1"> + <XCoordinate value="500"/> + <YCoordinate value="200"/> + </Mark2Anchor> + <Mark2Anchor index="1" Format="1"> + <XCoordinate value="500"/> + <YCoordinate value="-80"/> + </Mark2Anchor> + </Mark2Record> + <Mark2Record index="1"> + <Mark2Anchor index="0" Format="1"> + <XCoordinate value="500"/> + <YCoordinate value="200"/> + </Mark2Anchor> + <Mark2Anchor index="1" Format="1"> + <XCoordinate value="500"/> + <YCoordinate value="-80"/> + </Mark2Anchor> + </Mark2Record> + <Mark2Record index="2"> + <Mark2Anchor index="0" Format="1"> + <XCoordinate value="500"/> + <YCoordinate value="200"/> + </Mark2Anchor> + <Mark2Anchor index="1" empty="1"/> + </Mark2Record> + <Mark2Record index="3"> + <Mark2Anchor index="0" Format="1"> + <XCoordinate value="500"/> + <YCoordinate value="200"/> + </Mark2Anchor> + <Mark2Anchor index="1" Format="1"> + <XCoordinate value="500"/> + <YCoordinate value="-80"/> + </Mark2Anchor> + </Mark2Record> + <Mark2Record index="4"> + <Mark2Anchor index="0" Format="1"> + <XCoordinate value="500"/> + <YCoordinate value="200"/> + </Mark2Anchor> + <Mark2Anchor index="1" Format="1"> + <XCoordinate value="500"/> + <YCoordinate value="-80"/> + </Mark2Anchor> + </Mark2Record> + <Mark2Record index="5"> + <Mark2Anchor index="0" Format="1"> + <XCoordinate value="500"/> + <YCoordinate value="200"/> + </Mark2Anchor> + <Mark2Anchor index="1" empty="1"/> + </Mark2Record> + </Mark2Array> + </MarkMarkPos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/GPOS_8.fea b/Tests/feaLib/data/GPOS_8.fea new file mode 100644 index 0000000..2351ee2 --- /dev/null +++ b/Tests/feaLib/data/GPOS_8.fea @@ -0,0 +1,22 @@ +languagesystem DFLT dflt; + +lookup ChainedSinglePos { + pos A one' 1 two' 2 one' -1 two' -2; +} ChainedSinglePos; + +lookup L1 { + pos one 100; +} L1; + +lookup L2 { + pos two 200; +} L2; + +lookup ChainedContextualPos { + pos [A a] [B b] I' lookup L1 N' lookup L2 P' [Y y] [Z z]; +} ChainedContextualPos; + +feature test { + lookup ChainedSinglePos; + lookup ChainedContextualPos; +} test; diff --git a/Tests/feaLib/data/GPOS_8.ttx b/Tests/feaLib/data/GPOS_8.ttx new file mode 100644 index 0000000..86f9756 --- /dev/null +++ b/Tests/feaLib/data/GPOS_8.ttx @@ -0,0 +1,176 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=2 --> + <LookupListIndex index="0" value="0"/> + <LookupListIndex index="1" value="5"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=6 --> + <Lookup index="0"> + <LookupType value="8"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <ChainContextPos index="0" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="A"/> + </BacktrackCoverage> + <!-- InputGlyphCount=4 --> + <InputCoverage index="0"> + <Glyph value="one"/> + </InputCoverage> + <InputCoverage index="1"> + <Glyph value="two"/> + </InputCoverage> + <InputCoverage index="2"> + <Glyph value="one"/> + </InputCoverage> + <InputCoverage index="3"> + <Glyph value="two"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- PosCount=4 --> + <PosLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </PosLookupRecord> + <PosLookupRecord index="1"> + <SequenceIndex value="1"/> + <LookupListIndex value="1"/> + </PosLookupRecord> + <PosLookupRecord index="2"> + <SequenceIndex value="2"/> + <LookupListIndex value="2"/> + </PosLookupRecord> + <PosLookupRecord index="3"> + <SequenceIndex value="3"/> + <LookupListIndex value="2"/> + </PosLookupRecord> + </ChainContextPos> + </Lookup> + <Lookup index="1"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="2"> + <Coverage> + <Glyph value="one"/> + <Glyph value="two"/> + </Coverage> + <ValueFormat value="4"/> + <!-- ValueCount=2 --> + <Value index="0" XAdvance="1"/> + <Value index="1" XAdvance="2"/> + </SinglePos> + </Lookup> + <Lookup index="2"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="2"> + <Coverage> + <Glyph value="one"/> + <Glyph value="two"/> + </Coverage> + <ValueFormat value="4"/> + <!-- ValueCount=2 --> + <Value index="0" XAdvance="-1"/> + <Value index="1" XAdvance="-2"/> + </SinglePos> + </Lookup> + <Lookup index="3"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="one"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="100"/> + </SinglePos> + </Lookup> + <Lookup index="4"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="two"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="200"/> + </SinglePos> + </Lookup> + <Lookup index="5"> + <LookupType value="8"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <ChainContextPos index="0" Format="3"> + <!-- BacktrackGlyphCount=2 --> + <BacktrackCoverage index="0"> + <Glyph value="B"/> + <Glyph value="b"/> + </BacktrackCoverage> + <BacktrackCoverage index="1"> + <Glyph value="A"/> + <Glyph value="a"/> + </BacktrackCoverage> + <!-- InputGlyphCount=3 --> + <InputCoverage index="0"> + <Glyph value="I"/> + </InputCoverage> + <InputCoverage index="1"> + <Glyph value="N"/> + </InputCoverage> + <InputCoverage index="2"> + <Glyph value="P"/> + </InputCoverage> + <!-- LookAheadGlyphCount=2 --> + <LookAheadCoverage index="0"> + <Glyph value="Y"/> + <Glyph value="y"/> + </LookAheadCoverage> + <LookAheadCoverage index="1"> + <Glyph value="Z"/> + <Glyph value="z"/> + </LookAheadCoverage> + <!-- PosCount=2 --> + <PosLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="3"/> + </PosLookupRecord> + <PosLookupRecord index="1"> + <SequenceIndex value="1"/> + <LookupListIndex value="4"/> + </PosLookupRecord> + </ChainContextPos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/GSUB_2.fea b/Tests/feaLib/data/GSUB_2.fea new file mode 100644 index 0000000..d2a3cb1 --- /dev/null +++ b/Tests/feaLib/data/GSUB_2.fea @@ -0,0 +1,14 @@ +feature f1 { + sub c_t by c t; + sub f_i by f i; + sub f_f_i by f f i; +} f1; + + +# Even if it has exactly the same content as feature f1, +# the lookup should not be shared. +feature f2 { + sub c_t by c t; + sub f_i by f i; + sub f_f_i by f f i; +} f2; diff --git a/Tests/feaLib/data/GSUB_2.ttx b/Tests/feaLib/data/GSUB_2.ttx new file mode 100644 index 0000000..b91c20f --- /dev/null +++ b/Tests/feaLib/data/GSUB_2.ttx @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=2 --> + <FeatureIndex index="0" value="0"/> + <FeatureIndex index="1" value="1"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=2 --> + <FeatureRecord index="0"> + <FeatureTag value="f1 "/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="1"> + <FeatureTag value="f2 "/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="1"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="2"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <MultipleSubst index="0"> + <Substitution in="c_t" out="c,t"/> + <Substitution in="f_f_i" out="f,f,i"/> + <Substitution in="f_i" out="f,i"/> + </MultipleSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="2"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <MultipleSubst index="0"> + <Substitution in="c_t" out="c,t"/> + <Substitution in="f_f_i" out="f,f,i"/> + <Substitution in="f_i" out="f,i"/> + </MultipleSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/GSUB_3.fea b/Tests/feaLib/data/GSUB_3.fea new file mode 100644 index 0000000..e128305 --- /dev/null +++ b/Tests/feaLib/data/GSUB_3.fea @@ -0,0 +1,14 @@ +feature f1 { + sub A from [A.alt1 A.alt2]; + sub B from [B.alt1 B.alt2 B.alt3]; + sub C from [C.alt1]; +} f1; + + +# Even if it has exactly the same content as feature f1, +# the lookup should not be shared. +feature f2 { + sub A from [A.alt1 A.alt2]; + sub B from [B.alt1 B.alt2 B.alt3]; + sub C from [C.alt1]; +} f2; diff --git a/Tests/feaLib/data/GSUB_3.ttx b/Tests/feaLib/data/GSUB_3.ttx new file mode 100644 index 0000000..77cb4c6 --- /dev/null +++ b/Tests/feaLib/data/GSUB_3.ttx @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=2 --> + <FeatureIndex index="0" value="0"/> + <FeatureIndex index="1" value="1"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=2 --> + <FeatureRecord index="0"> + <FeatureTag value="f1 "/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="1"> + <FeatureTag value="f2 "/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="1"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="3"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <AlternateSubst index="0"> + <AlternateSet glyph="A"> + <Alternate glyph="A.alt1"/> + <Alternate glyph="A.alt2"/> + </AlternateSet> + <AlternateSet glyph="B"> + <Alternate glyph="B.alt1"/> + <Alternate glyph="B.alt2"/> + <Alternate glyph="B.alt3"/> + </AlternateSet> + <AlternateSet glyph="C"> + <Alternate glyph="C.alt1"/> + </AlternateSet> + </AlternateSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="3"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <AlternateSubst index="0"> + <AlternateSet glyph="A"> + <Alternate glyph="A.alt1"/> + <Alternate glyph="A.alt2"/> + </AlternateSet> + <AlternateSet glyph="B"> + <Alternate glyph="B.alt1"/> + <Alternate glyph="B.alt2"/> + <Alternate glyph="B.alt3"/> + </AlternateSet> + <AlternateSet glyph="C"> + <Alternate glyph="C.alt1"/> + </AlternateSet> + </AlternateSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/GSUB_6.fea b/Tests/feaLib/data/GSUB_6.fea new file mode 100644 index 0000000..82fdac2 --- /dev/null +++ b/Tests/feaLib/data/GSUB_6.fea @@ -0,0 +1,28 @@ +lookup ChainedSingleSubst { + sub [one two] three A' by A.sc; + sub [B-D]' seven [eight nine] by [B.sc-D.sc]; +} ChainedSingleSubst; + +lookup ChainedMultipleSubst { + sub [A-C a-c] [D d] E c_t' V [W w] [X-Z x-z] by c t; +} ChainedMultipleSubst; + +lookup ChainedAlternateSubst { + sub [space comma semicolon] e' from [e e.begin]; +} ChainedAlternateSubst; + +lookup ChainedLigatureSubst { + sub A [C c]' [T t]' Z by c_t; +} ChainedLigatureSubst; + +lookup ChainedContextualSubst { + sub A D E c_t' lookup ChainedMultipleSubst V W X; +} ChainedContextualSubst; + +feature test { + lookup ChainedSingleSubst; + lookup ChainedMultipleSubst; + lookup ChainedAlternateSubst; + lookup ChainedLigatureSubst; + lookup ChainedContextualSubst; +} test; diff --git a/Tests/feaLib/data/GSUB_6.ttx b/Tests/feaLib/data/GSUB_6.ttx new file mode 100644 index 0000000..f32e47d --- /dev/null +++ b/Tests/feaLib/data/GSUB_6.ttx @@ -0,0 +1,267 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=5 --> + <LookupListIndex index="0" value="0"/> + <LookupListIndex index="1" value="2"/> + <LookupListIndex index="2" value="4"/> + <LookupListIndex index="3" value="6"/> + <LookupListIndex index="4" value="8"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=9 --> + <Lookup index="0"> + <LookupType value="6"/> + <LookupFlag value="0"/> + <!-- SubTableCount=2 --> + <ChainContextSubst index="0" Format="3"> + <!-- BacktrackGlyphCount=2 --> + <BacktrackCoverage index="0"> + <Glyph value="three"/> + </BacktrackCoverage> + <BacktrackCoverage index="1"> + <Glyph value="one"/> + <Glyph value="two"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="A"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </SubstLookupRecord> + </ChainContextSubst> + <ChainContextSubst index="1" Format="3"> + <!-- BacktrackGlyphCount=0 --> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="B"/> + <Glyph value="C"/> + <Glyph value="D"/> + </InputCoverage> + <!-- LookAheadGlyphCount=2 --> + <LookAheadCoverage index="0"> + <Glyph value="seven"/> + </LookAheadCoverage> + <LookAheadCoverage index="1"> + <Glyph value="eight"/> + <Glyph value="nine"/> + </LookAheadCoverage> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </SubstLookupRecord> + </ChainContextSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="A" out="A.sc"/> + <Substitution in="B" out="B.sc"/> + <Substitution in="C" out="C.sc"/> + <Substitution in="D" out="D.sc"/> + </SingleSubst> + </Lookup> + <Lookup index="2"> + <LookupType value="6"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <ChainContextSubst index="0" Format="3"> + <!-- BacktrackGlyphCount=3 --> + <BacktrackCoverage index="0"> + <Glyph value="E"/> + </BacktrackCoverage> + <BacktrackCoverage index="1"> + <Glyph value="D"/> + <Glyph value="d"/> + </BacktrackCoverage> + <BacktrackCoverage index="2"> + <Glyph value="A"/> + <Glyph value="B"/> + <Glyph value="C"/> + <Glyph value="a"/> + <Glyph value="b"/> + <Glyph value="c"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="c_t"/> + </InputCoverage> + <!-- LookAheadGlyphCount=3 --> + <LookAheadCoverage index="0"> + <Glyph value="V"/> + </LookAheadCoverage> + <LookAheadCoverage index="1"> + <Glyph value="W"/> + <Glyph value="w"/> + </LookAheadCoverage> + <LookAheadCoverage index="2"> + <Glyph value="X"/> + <Glyph value="Y"/> + <Glyph value="Z"/> + <Glyph value="x"/> + <Glyph value="y"/> + <Glyph value="z"/> + </LookAheadCoverage> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="3"/> + </SubstLookupRecord> + </ChainContextSubst> + </Lookup> + <Lookup index="3"> + <LookupType value="2"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <MultipleSubst index="0"> + <Substitution in="c_t" out="c,t"/> + </MultipleSubst> + </Lookup> + <Lookup index="4"> + <LookupType value="6"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <ChainContextSubst index="0" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="space"/> + <Glyph value="semicolon"/> + <Glyph value="comma"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="e"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="5"/> + </SubstLookupRecord> + </ChainContextSubst> + </Lookup> + <Lookup index="5"> + <LookupType value="3"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <AlternateSubst index="0"> + <AlternateSet glyph="e"> + <Alternate glyph="e"/> + <Alternate glyph="e.begin"/> + </AlternateSet> + </AlternateSubst> + </Lookup> + <Lookup index="6"> + <LookupType value="6"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <ChainContextSubst index="0" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="A"/> + </BacktrackCoverage> + <!-- InputGlyphCount=2 --> + <InputCoverage index="0"> + <Glyph value="C"/> + <Glyph value="c"/> + </InputCoverage> + <InputCoverage index="1"> + <Glyph value="T"/> + <Glyph value="t"/> + </InputCoverage> + <!-- LookAheadGlyphCount=1 --> + <LookAheadCoverage index="0"> + <Glyph value="Z"/> + </LookAheadCoverage> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="7"/> + </SubstLookupRecord> + </ChainContextSubst> + </Lookup> + <Lookup index="7"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="C"> + <Ligature components="T" glyph="c_t"/> + <Ligature components="t" glyph="c_t"/> + </LigatureSet> + <LigatureSet glyph="c"> + <Ligature components="T" glyph="c_t"/> + <Ligature components="t" glyph="c_t"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + <Lookup index="8"> + <LookupType value="6"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <ChainContextSubst index="0" Format="3"> + <!-- BacktrackGlyphCount=3 --> + <BacktrackCoverage index="0"> + <Glyph value="E"/> + </BacktrackCoverage> + <BacktrackCoverage index="1"> + <Glyph value="D"/> + </BacktrackCoverage> + <BacktrackCoverage index="2"> + <Glyph value="A"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="c_t"/> + </InputCoverage> + <!-- LookAheadGlyphCount=3 --> + <LookAheadCoverage index="0"> + <Glyph value="V"/> + </LookAheadCoverage> + <LookAheadCoverage index="1"> + <Glyph value="W"/> + </LookAheadCoverage> + <LookAheadCoverage index="2"> + <Glyph value="X"/> + </LookAheadCoverage> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="2"/> + </SubstLookupRecord> + </ChainContextSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/GSUB_8.fea b/Tests/feaLib/data/GSUB_8.fea new file mode 100644 index 0000000..62c7bee --- /dev/null +++ b/Tests/feaLib/data/GSUB_8.fea @@ -0,0 +1,11 @@ +languagesystem DFLT dflt; + +feature test { + rsub [a A] [b B] [c C] q' [d D] [e E] [f F] by Q; + rsub [a A] [b B] [c C] [s-z]' [d D] [e E] [f F] by [S-Z]; + + # Having no context for a reverse chaining substitution rule + # is a little degenerate (we define a chain without linking it + # to anything else), but makeotf accepts this. + rsub p by P; +} test; diff --git a/Tests/feaLib/data/GSUB_8.ttx b/Tests/feaLib/data/GSUB_8.ttx new file mode 100644 index 0000000..1b10d25 --- /dev/null +++ b/Tests/feaLib/data/GSUB_8.ttx @@ -0,0 +1,129 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="8"/> + <LookupFlag value="0"/> + <!-- SubTableCount=3 --> + <ReverseChainSingleSubst index="0" Format="1"> + <Coverage> + <Glyph value="q"/> + </Coverage> + <!-- BacktrackGlyphCount=3 --> + <BacktrackCoverage index="0"> + <Glyph value="C"/> + <Glyph value="c"/> + </BacktrackCoverage> + <BacktrackCoverage index="1"> + <Glyph value="B"/> + <Glyph value="b"/> + </BacktrackCoverage> + <BacktrackCoverage index="2"> + <Glyph value="A"/> + <Glyph value="a"/> + </BacktrackCoverage> + <!-- LookAheadGlyphCount=3 --> + <LookAheadCoverage index="0"> + <Glyph value="D"/> + <Glyph value="d"/> + </LookAheadCoverage> + <LookAheadCoverage index="1"> + <Glyph value="E"/> + <Glyph value="e"/> + </LookAheadCoverage> + <LookAheadCoverage index="2"> + <Glyph value="F"/> + <Glyph value="f"/> + </LookAheadCoverage> + <!-- GlyphCount=1 --> + <Substitute index="0" value="Q"/> + </ReverseChainSingleSubst> + <ReverseChainSingleSubst index="1" Format="1"> + <Coverage> + <Glyph value="s"/> + <Glyph value="t"/> + <Glyph value="u"/> + <Glyph value="v"/> + <Glyph value="w"/> + <Glyph value="x"/> + <Glyph value="y"/> + <Glyph value="z"/> + </Coverage> + <!-- BacktrackGlyphCount=3 --> + <BacktrackCoverage index="0"> + <Glyph value="C"/> + <Glyph value="c"/> + </BacktrackCoverage> + <BacktrackCoverage index="1"> + <Glyph value="B"/> + <Glyph value="b"/> + </BacktrackCoverage> + <BacktrackCoverage index="2"> + <Glyph value="A"/> + <Glyph value="a"/> + </BacktrackCoverage> + <!-- LookAheadGlyphCount=3 --> + <LookAheadCoverage index="0"> + <Glyph value="D"/> + <Glyph value="d"/> + </LookAheadCoverage> + <LookAheadCoverage index="1"> + <Glyph value="E"/> + <Glyph value="e"/> + </LookAheadCoverage> + <LookAheadCoverage index="2"> + <Glyph value="F"/> + <Glyph value="f"/> + </LookAheadCoverage> + <!-- GlyphCount=8 --> + <Substitute index="0" value="S"/> + <Substitute index="1" value="T"/> + <Substitute index="2" value="U"/> + <Substitute index="3" value="V"/> + <Substitute index="4" value="W"/> + <Substitute index="5" value="X"/> + <Substitute index="6" value="Y"/> + <Substitute index="7" value="Z"/> + </ReverseChainSingleSubst> + <ReverseChainSingleSubst index="2" Format="1"> + <Coverage> + <Glyph value="p"/> + </Coverage> + <!-- BacktrackGlyphCount=0 --> + <!-- LookAheadGlyphCount=0 --> + <!-- GlyphCount=1 --> + <Substitute index="0" value="P"/> + </ReverseChainSingleSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/GlyphClassDef.fea b/Tests/feaLib/data/GlyphClassDef.fea new file mode 100644 index 0000000..8e53536 --- /dev/null +++ b/Tests/feaLib/data/GlyphClassDef.fea @@ -0,0 +1,5 @@ +table GDEF { + GlyphClassDef [a], [b], [c], [d]; + GlyphClassDef [e], [f], [g], [h]; + GlyphClassDef [i], [j], [k], [l]; +} GDEF; diff --git a/Tests/feaLib/data/GlyphClassDef.ttx b/Tests/feaLib/data/GlyphClassDef.ttx new file mode 100644 index 0000000..d7d8b90 --- /dev/null +++ b/Tests/feaLib/data/GlyphClassDef.ttx @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GDEF> + <Version value="0x00010000"/> + <GlyphClassDef> + <ClassDef glyph="a" class="1"/> + <ClassDef glyph="b" class="2"/> + <ClassDef glyph="c" class="3"/> + <ClassDef glyph="d" class="4"/> + <ClassDef glyph="e" class="1"/> + <ClassDef glyph="f" class="2"/> + <ClassDef glyph="g" class="3"/> + <ClassDef glyph="h" class="4"/> + <ClassDef glyph="i" class="1"/> + <ClassDef glyph="j" class="2"/> + <ClassDef glyph="k" class="3"/> + <ClassDef glyph="l" class="4"/> + </GlyphClassDef> + </GDEF> + +</ttFont> diff --git a/Tests/feaLib/data/LigatureCaretByIndex.fea b/Tests/feaLib/data/LigatureCaretByIndex.fea new file mode 100644 index 0000000..1968172 --- /dev/null +++ b/Tests/feaLib/data/LigatureCaretByIndex.fea @@ -0,0 +1,10 @@ +table GDEF { + LigatureCaretByIndex [c_t s_t] 11; + + # The OpenType Feature File specification does not define what should + # happen when there are multiple LigatureCaretByIndex statements for + # the same glyph. Our behavior matches that of Adobe makeotf v2.0.90. + # https://github.com/adobe-type-tools/afdko/issues/95 + LigatureCaretByIndex o_f_f_i 66 33; + LigatureCaretByIndex o_f_f_i 55; +} GDEF; diff --git a/Tests/feaLib/data/LigatureCaretByIndex.ttx b/Tests/feaLib/data/LigatureCaretByIndex.ttx new file mode 100644 index 0000000..a758077 --- /dev/null +++ b/Tests/feaLib/data/LigatureCaretByIndex.ttx @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GDEF> + <Version value="0x00010000"/> + <LigCaretList> + <Coverage> + <Glyph value="c_t"/> + <Glyph value="o_f_f_i"/> + <Glyph value="s_t"/> + </Coverage> + <!-- LigGlyphCount=3 --> + <LigGlyph index="0"> + <!-- CaretCount=1 --> + <CaretValue index="0" Format="2"> + <CaretValuePoint value="11"/> + </CaretValue> + </LigGlyph> + <LigGlyph index="1"> + <!-- CaretCount=3 --> + <CaretValue index="0" Format="2"> + <CaretValuePoint value="33"/> + </CaretValue> + <CaretValue index="1" Format="2"> + <CaretValuePoint value="55"/> + </CaretValue> + <CaretValue index="2" Format="2"> + <CaretValuePoint value="66"/> + </CaretValue> + </LigGlyph> + <LigGlyph index="2"> + <!-- CaretCount=1 --> + <CaretValue index="0" Format="2"> + <CaretValuePoint value="11"/> + </CaretValue> + </LigGlyph> + </LigCaretList> + </GDEF> + +</ttFont> diff --git a/Tests/feaLib/data/LigatureCaretByPos.fea b/Tests/feaLib/data/LigatureCaretByPos.fea new file mode 100644 index 0000000..a8ccf6f --- /dev/null +++ b/Tests/feaLib/data/LigatureCaretByPos.fea @@ -0,0 +1,10 @@ +table GDEF { + LigatureCaretByPos [c_h c_k] 500; + + # The OpenType Feature File specification does not define what should + # happen when there are multiple LigatureCaretByPos statements for + # the same glyph. Our behavior matches that of Adobe makeotf v2.0.90. + # https://github.com/adobe-type-tools/afdko/issues/95 + LigatureCaretByPos o_f_f_i 700 300; + LigatureCaretByPos o_f_f_i 900; +} GDEF; diff --git a/Tests/feaLib/data/LigatureCaretByPos.ttx b/Tests/feaLib/data/LigatureCaretByPos.ttx new file mode 100644 index 0000000..911db6b --- /dev/null +++ b/Tests/feaLib/data/LigatureCaretByPos.ttx @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GDEF> + <Version value="0x00010000"/> + <LigCaretList> + <Coverage> + <Glyph value="c_h"/> + <Glyph value="c_k"/> + <Glyph value="o_f_f_i"/> + </Coverage> + <!-- LigGlyphCount=3 --> + <LigGlyph index="0"> + <!-- CaretCount=1 --> + <CaretValue index="0" Format="1"> + <Coordinate value="500"/> + </CaretValue> + </LigGlyph> + <LigGlyph index="1"> + <!-- CaretCount=1 --> + <CaretValue index="0" Format="1"> + <Coordinate value="500"/> + </CaretValue> + </LigGlyph> + <LigGlyph index="2"> + <!-- CaretCount=3 --> + <CaretValue index="0" Format="1"> + <Coordinate value="300"/> + </CaretValue> + <CaretValue index="1" Format="1"> + <Coordinate value="700"/> + </CaretValue> + <CaretValue index="2" Format="1"> + <Coordinate value="900"/> + </CaretValue> + </LigGlyph> + </LigCaretList> + </GDEF> + +</ttFont> diff --git a/Tests/feaLib/data/PairPosSubtable.fea b/Tests/feaLib/data/PairPosSubtable.fea new file mode 100644 index 0000000..021f3cc --- /dev/null +++ b/Tests/feaLib/data/PairPosSubtable.fea @@ -0,0 +1,20 @@ +languagesystem DFLT dflt; +languagesystem latn dflt; + +lookup kernlookup { + pos A V -34; + subtable; + @group1 = [b o]; + @group2 = [c d]; + pos @group1 @group2 -12; +} kernlookup; + +feature kern { + script DFLT; + language dflt; + lookup kernlookup; + + script latn; + language dflt; + lookup kernlookup; +} kern; diff --git a/Tests/feaLib/data/PairPosSubtable.ttx b/Tests/feaLib/data/PairPosSubtable.ttx new file mode 100644 index 0000000..13a77aa --- /dev/null +++ b/Tests/feaLib/data/PairPosSubtable.ttx @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=2 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + <ScriptRecord index="1"> + <ScriptTag value="latn"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="1"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=2 --> + <FeatureRecord index="0"> + <FeatureTag value="kern"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="1"> + <FeatureTag value="kern"/> + <Feature> + <!-- LookupCount=2 --> + <LookupListIndex index="0" value="0"/> + <LookupListIndex index="1" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="2"/> + <LookupFlag value="0"/> + <!-- SubTableCount=2 --> + <PairPos index="0" Format="1"> + <Coverage> + <Glyph value="A"/> + </Coverage> + <ValueFormat1 value="4"/> + <ValueFormat2 value="0"/> + <!-- PairSetCount=1 --> + <PairSet index="0"> + <!-- PairValueCount=1 --> + <PairValueRecord index="0"> + <SecondGlyph value="V"/> + <Value1 XAdvance="-34"/> + </PairValueRecord> + </PairSet> + </PairPos> + <PairPos index="1" Format="2"> + <Coverage> + <Glyph value="b"/> + <Glyph value="o"/> + </Coverage> + <ValueFormat1 value="4"/> + <ValueFormat2 value="0"/> + <ClassDef1> + </ClassDef1> + <ClassDef2> + <ClassDef glyph="c" class="1"/> + <ClassDef glyph="d" class="1"/> + </ClassDef2> + <!-- Class1Count=1 --> + <!-- Class2Count=2 --> + <Class1Record index="0"> + <Class2Record index="0"> + </Class2Record> + <Class2Record index="1"> + <Value1 XAdvance="-12"/> + </Class2Record> + </Class1Record> + </PairPos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/ZeroValue_ChainSinglePos_horizontal.fea b/Tests/feaLib/data/ZeroValue_ChainSinglePos_horizontal.fea new file mode 100644 index 0000000..480fc76 --- /dev/null +++ b/Tests/feaLib/data/ZeroValue_ChainSinglePos_horizontal.fea @@ -0,0 +1,9 @@ +# For contextual positioning statements with in-line single positioning rules, +# zeroes should get compiled to ValueRecord format 0. +# https://github.com/fonttools/fonttools/issues/633 + +# Zero value in a horizontal context. +feature kern { + pos A G' 0 A; # value format A + pos B G' <0 0 0 0> B; # value format B +} kern; diff --git a/Tests/feaLib/data/ZeroValue_ChainSinglePos_horizontal.ttx b/Tests/feaLib/data/ZeroValue_ChainSinglePos_horizontal.ttx new file mode 100644 index 0000000..ce76370 --- /dev/null +++ b/Tests/feaLib/data/ZeroValue_ChainSinglePos_horizontal.ttx @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.7"> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="kern"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="8"/> + <LookupFlag value="0"/> + <!-- SubTableCount=2 --> + <ChainContextPos index="0" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="A"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="G"/> + </InputCoverage> + <!-- LookAheadGlyphCount=1 --> + <LookAheadCoverage index="0"> + <Glyph value="A"/> + </LookAheadCoverage> + <!-- PosCount=1 --> + <PosLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </PosLookupRecord> + </ChainContextPos> + <ChainContextPos index="1" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="B"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="G"/> + </InputCoverage> + <!-- LookAheadGlyphCount=1 --> + <LookAheadCoverage index="0"> + <Glyph value="B"/> + </LookAheadCoverage> + <!-- PosCount=1 --> + <PosLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </PosLookupRecord> + </ChainContextPos> + </Lookup> + <Lookup index="1"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="G"/> + </Coverage> + <ValueFormat value="0"/> + </SinglePos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/ZeroValue_ChainSinglePos_vertical.fea b/Tests/feaLib/data/ZeroValue_ChainSinglePos_vertical.fea new file mode 100644 index 0000000..a9fb7d1 --- /dev/null +++ b/Tests/feaLib/data/ZeroValue_ChainSinglePos_vertical.fea @@ -0,0 +1,9 @@ +# For contextual positioning statements with in-line single positioning rules, +# zeroes should get compiled to ValueRecord format 0. +# https://github.com/fonttools/fonttools/issues/633 + +# Zero value in a vertical context. +feature vkrn { + pos A G' 0 A; # value format A + pos B G' <0 0 0 0> B; # value format B +} vkrn; diff --git a/Tests/feaLib/data/ZeroValue_ChainSinglePos_vertical.ttx b/Tests/feaLib/data/ZeroValue_ChainSinglePos_vertical.ttx new file mode 100644 index 0000000..e1cb5d6 --- /dev/null +++ b/Tests/feaLib/data/ZeroValue_ChainSinglePos_vertical.ttx @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.7"> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="vkrn"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="8"/> + <LookupFlag value="0"/> + <!-- SubTableCount=2 --> + <ChainContextPos index="0" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="A"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="G"/> + </InputCoverage> + <!-- LookAheadGlyphCount=1 --> + <LookAheadCoverage index="0"> + <Glyph value="A"/> + </LookAheadCoverage> + <!-- PosCount=1 --> + <PosLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </PosLookupRecord> + </ChainContextPos> + <ChainContextPos index="1" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="B"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="G"/> + </InputCoverage> + <!-- LookAheadGlyphCount=1 --> + <LookAheadCoverage index="0"> + <Glyph value="B"/> + </LookAheadCoverage> + <!-- PosCount=1 --> + <PosLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </PosLookupRecord> + </ChainContextPos> + </Lookup> + <Lookup index="1"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="G"/> + </Coverage> + <ValueFormat value="0"/> + </SinglePos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/ZeroValue_PairPos_horizontal.fea b/Tests/feaLib/data/ZeroValue_PairPos_horizontal.fea new file mode 100644 index 0000000..b5e48f4 --- /dev/null +++ b/Tests/feaLib/data/ZeroValue_PairPos_horizontal.fea @@ -0,0 +1,9 @@ +# For PairPos statements in horizontal compilation contexts, +# zero values should get compiled to ValueRecord format 4. +# https://github.com/fonttools/fonttools/issues/633 +feature kern { + pos A 0 A 0; + pos A 0 B <0 0 0 0>; + pos B <0 0 0 0> A 0; + pos B <0 0 0 0> B <0 0 0 0>; +} kern; diff --git a/Tests/feaLib/data/ZeroValue_PairPos_horizontal.ttx b/Tests/feaLib/data/ZeroValue_PairPos_horizontal.ttx new file mode 100644 index 0000000..6537852 --- /dev/null +++ b/Tests/feaLib/data/ZeroValue_PairPos_horizontal.ttx @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="kern"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="2"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <PairPos index="0" Format="1"> + <Coverage> + <Glyph value="A"/> + <Glyph value="B"/> + </Coverage> + <ValueFormat1 value="4"/> + <ValueFormat2 value="4"/> + <!-- PairSetCount=2 --> + <PairSet index="0"> + <!-- PairValueCount=2 --> + <PairValueRecord index="0"> + <SecondGlyph value="A"/> + <Value1 XAdvance="0"/> + <Value2 XAdvance="0"/> + </PairValueRecord> + <PairValueRecord index="1"> + <SecondGlyph value="B"/> + <Value1 XAdvance="0"/> + <Value2 XAdvance="0"/> + </PairValueRecord> + </PairSet> + <PairSet index="1"> + <!-- PairValueCount=2 --> + <PairValueRecord index="0"> + <SecondGlyph value="A"/> + <Value1 XAdvance="0"/> + <Value2 XAdvance="0"/> + </PairValueRecord> + <PairValueRecord index="1"> + <SecondGlyph value="B"/> + <Value1 XAdvance="0"/> + <Value2 XAdvance="0"/> + </PairValueRecord> + </PairSet> + </PairPos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/ZeroValue_PairPos_vertical.fea b/Tests/feaLib/data/ZeroValue_PairPos_vertical.fea new file mode 100644 index 0000000..1ab33a9 --- /dev/null +++ b/Tests/feaLib/data/ZeroValue_PairPos_vertical.fea @@ -0,0 +1,9 @@ +# For PairPos statements in vertical compilation contexts, +# zero values should get compiled to ValueRecord format 8. +# https://github.com/fonttools/fonttools/issues/633 +feature vkrn { + pos A 0 A 0; + pos A 0 B <0 0 0 0>; + pos B <0 0 0 0> A 0; + pos B <0 0 0 0> B <0 0 0 0>; +} vkrn; diff --git a/Tests/feaLib/data/ZeroValue_PairPos_vertical.ttx b/Tests/feaLib/data/ZeroValue_PairPos_vertical.ttx new file mode 100644 index 0000000..774bae0 --- /dev/null +++ b/Tests/feaLib/data/ZeroValue_PairPos_vertical.ttx @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="vkrn"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="2"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <PairPos index="0" Format="1"> + <Coverage> + <Glyph value="A"/> + <Glyph value="B"/> + </Coverage> + <ValueFormat1 value="8"/> + <ValueFormat2 value="8"/> + <!-- PairSetCount=2 --> + <PairSet index="0"> + <!-- PairValueCount=2 --> + <PairValueRecord index="0"> + <SecondGlyph value="A"/> + <Value1 YAdvance="0"/> + <Value2 YAdvance="0"/> + </PairValueRecord> + <PairValueRecord index="1"> + <SecondGlyph value="B"/> + <Value1 YAdvance="0"/> + <Value2 YAdvance="0"/> + </PairValueRecord> + </PairSet> + <PairSet index="1"> + <!-- PairValueCount=2 --> + <PairValueRecord index="0"> + <SecondGlyph value="A"/> + <Value1 YAdvance="0"/> + <Value2 YAdvance="0"/> + </PairValueRecord> + <PairValueRecord index="1"> + <SecondGlyph value="B"/> + <Value1 YAdvance="0"/> + <Value2 YAdvance="0"/> + </PairValueRecord> + </PairSet> + </PairPos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/ZeroValue_SinglePos_horizontal.fea b/Tests/feaLib/data/ZeroValue_SinglePos_horizontal.fea new file mode 100644 index 0000000..44fe683 --- /dev/null +++ b/Tests/feaLib/data/ZeroValue_SinglePos_horizontal.fea @@ -0,0 +1,8 @@ +# For SinglePos statements, zeroes should get compiled to ValueRecord format 0. +# https://github.com/fonttools/fonttools/issues/633 + +# Zero value in a horizontal context. +feature kern { + pos A 0; # format A + pos B <0 0 0 0>; # format B +} kern; diff --git a/Tests/feaLib/data/ZeroValue_SinglePos_horizontal.ttx b/Tests/feaLib/data/ZeroValue_SinglePos_horizontal.ttx new file mode 100644 index 0000000..d5fe639 --- /dev/null +++ b/Tests/feaLib/data/ZeroValue_SinglePos_horizontal.ttx @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="kern"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="A"/> + <Glyph value="B"/> + </Coverage> + <ValueFormat value="0"/> + </SinglePos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/ZeroValue_SinglePos_vertical.fea b/Tests/feaLib/data/ZeroValue_SinglePos_vertical.fea new file mode 100644 index 0000000..3ee9a1b --- /dev/null +++ b/Tests/feaLib/data/ZeroValue_SinglePos_vertical.fea @@ -0,0 +1,8 @@ +# For SinglePos statements, zeroes should get compiled to ValueRecord format 0. +# https://github.com/fonttools/fonttools/issues/633 + +# Zero value in a vertical context. +feature vkrn { + pos A 0; # format A + pos B <0 0 0 0>; # format B +} vkrn; diff --git a/Tests/feaLib/data/ZeroValue_SinglePos_vertical.ttx b/Tests/feaLib/data/ZeroValue_SinglePos_vertical.ttx new file mode 100644 index 0000000..3879c36 --- /dev/null +++ b/Tests/feaLib/data/ZeroValue_SinglePos_vertical.ttx @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="vkrn"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="A"/> + <Glyph value="B"/> + </Coverage> + <ValueFormat value="0"/> + </SinglePos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/baseClass.fea b/Tests/feaLib/data/baseClass.fea new file mode 100644 index 0000000..2a370c1 --- /dev/null +++ b/Tests/feaLib/data/baseClass.fea @@ -0,0 +1,10 @@ +languagesystem DFLT dflt; + +markClass [acute] <anchor 350 0> @TOP_MARKS; + +feature test { + pos base [a] <anchor 500 500> mark @TOP_MARKS; + pos base b <anchor 500 750> mark @TOP_MARKS; +} test; + + diff --git a/Tests/feaLib/data/baseClass.feax b/Tests/feaLib/data/baseClass.feax new file mode 100644 index 0000000..2950e30 --- /dev/null +++ b/Tests/feaLib/data/baseClass.feax @@ -0,0 +1,10 @@ +languagesystem DFLT dflt; + +markClass [acute] <anchor 350 0> @TOP_MARKS; +baseClass [a] <anchor 500 500> @BASE_TOPS; +baseClass b <anchor 500 750> @BASE_TOPS; + +feature test { + pos base @BASE_TOPS mark @TOP_MARKS; +} test; + diff --git a/Tests/feaLib/data/bug453.fea b/Tests/feaLib/data/bug453.fea new file mode 100644 index 0000000..fc5072e --- /dev/null +++ b/Tests/feaLib/data/bug453.fea @@ -0,0 +1,11 @@ +# https://github.com/behdad/fonttools/issues/453 +feature mark { + lookup mark1 { + markClass [acute] <anchor 150 -10> @TOP_MARKS; + pos base [e] <anchor 250 450> mark @TOP_MARKS; + } mark1; + lookup mark2 { + markClass [acute] <anchor 150 -20> @TOP_MARKS_2; + pos base [e] <anchor 250 450> mark @TOP_MARKS_2; + } mark2; +} mark; diff --git a/Tests/feaLib/data/bug453.ttx b/Tests/feaLib/data/bug453.ttx new file mode 100644 index 0000000..748e13d --- /dev/null +++ b/Tests/feaLib/data/bug453.ttx @@ -0,0 +1,110 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GDEF> + <Version value="0x00010000"/> + <GlyphClassDef> + <ClassDef glyph="acute" class="3"/> + <ClassDef glyph="e" class="1"/> + </GlyphClassDef> + </GDEF> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="mark"/> + <Feature> + <!-- LookupCount=2 --> + <LookupListIndex index="0" value="0"/> + <LookupListIndex index="1" value="1"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <MarkBasePos index="0" Format="1"> + <MarkCoverage> + <Glyph value="acute"/> + </MarkCoverage> + <BaseCoverage> + <Glyph value="e"/> + </BaseCoverage> + <!-- ClassCount=1 --> + <MarkArray> + <!-- MarkCount=1 --> + <MarkRecord index="0"> + <Class value="0"/> + <MarkAnchor Format="1"> + <XCoordinate value="150"/> + <YCoordinate value="-10"/> + </MarkAnchor> + </MarkRecord> + </MarkArray> + <BaseArray> + <!-- BaseCount=1 --> + <BaseRecord index="0"> + <BaseAnchor index="0" Format="1"> + <XCoordinate value="250"/> + <YCoordinate value="450"/> + </BaseAnchor> + </BaseRecord> + </BaseArray> + </MarkBasePos> + </Lookup> + <Lookup index="1"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <MarkBasePos index="0" Format="1"> + <MarkCoverage> + <Glyph value="acute"/> + </MarkCoverage> + <BaseCoverage> + <Glyph value="e"/> + </BaseCoverage> + <!-- ClassCount=1 --> + <MarkArray> + <!-- MarkCount=1 --> + <MarkRecord index="0"> + <Class value="0"/> + <MarkAnchor Format="1"> + <XCoordinate value="150"/> + <YCoordinate value="-20"/> + </MarkAnchor> + </MarkRecord> + </MarkArray> + <BaseArray> + <!-- BaseCount=1 --> + <BaseRecord index="0"> + <BaseAnchor index="0" Format="1"> + <XCoordinate value="250"/> + <YCoordinate value="450"/> + </BaseAnchor> + </BaseRecord> + </BaseArray> + </MarkBasePos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/bug457.fea b/Tests/feaLib/data/bug457.fea new file mode 100644 index 0000000..9ab083b --- /dev/null +++ b/Tests/feaLib/data/bug457.fea @@ -0,0 +1,5 @@ +@group = [A \sub \lookup \feature \by \table]; + +feature liga { + sub @group by G; +} liga; diff --git a/Tests/feaLib/data/bug457.ttx b/Tests/feaLib/data/bug457.ttx new file mode 100644 index 0000000..1967f1e --- /dev/null +++ b/Tests/feaLib/data/bug457.ttx @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="liga"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="A" out="G"/> + <Substitution in="by" out="G"/> + <Substitution in="feature" out="G"/> + <Substitution in="lookup" out="G"/> + <Substitution in="sub" out="G"/> + <Substitution in="table" out="G"/> + </SingleSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/bug463.fea b/Tests/feaLib/data/bug463.fea new file mode 100644 index 0000000..e7e21af --- /dev/null +++ b/Tests/feaLib/data/bug463.fea @@ -0,0 +1,6 @@ +# https://github.com/behdad/fonttools/issues/463 +feature ordn { + @DIGIT = [zero one two three four five six seven eight nine]; + sub @DIGIT [A a]' by ordfeminine; + sub @DIGIT [O o]' by ordmasculine; +} ordn; diff --git a/Tests/feaLib/data/bug463.ttx b/Tests/feaLib/data/bug463.ttx new file mode 100644 index 0000000..eba1d14 --- /dev/null +++ b/Tests/feaLib/data/bug463.ttx @@ -0,0 +1,103 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="ordn"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="6"/> + <LookupFlag value="0"/> + <!-- SubTableCount=2 --> + <ChainContextSubst index="0" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="zero"/> + <Glyph value="one"/> + <Glyph value="two"/> + <Glyph value="three"/> + <Glyph value="four"/> + <Glyph value="five"/> + <Glyph value="six"/> + <Glyph value="seven"/> + <Glyph value="eight"/> + <Glyph value="nine"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="A"/> + <Glyph value="a"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </SubstLookupRecord> + </ChainContextSubst> + <ChainContextSubst index="1" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="zero"/> + <Glyph value="one"/> + <Glyph value="two"/> + <Glyph value="three"/> + <Glyph value="four"/> + <Glyph value="five"/> + <Glyph value="six"/> + <Glyph value="seven"/> + <Glyph value="eight"/> + <Glyph value="nine"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="O"/> + <Glyph value="o"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </SubstLookupRecord> + </ChainContextSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="A" out="ordfeminine"/> + <Substitution in="O" out="ordmasculine"/> + <Substitution in="a" out="ordfeminine"/> + <Substitution in="o" out="ordmasculine"/> + </SingleSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/bug501.fea b/Tests/feaLib/data/bug501.fea new file mode 100644 index 0000000..58a4081 --- /dev/null +++ b/Tests/feaLib/data/bug501.fea @@ -0,0 +1,8 @@ +# https://github.com/behdad/fonttools/issues/501 +languagesystem DFLT dflt; +feature test { + lookup L { + script grek; + pos T 100; + } L; +} test; diff --git a/Tests/feaLib/data/bug501.ttx b/Tests/feaLib/data/bug501.ttx new file mode 100644 index 0000000..032d21b --- /dev/null +++ b/Tests/feaLib/data/bug501.ttx @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="grek"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="T"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="100"/> + </SinglePos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/bug502.fea b/Tests/feaLib/data/bug502.fea new file mode 100644 index 0000000..b8130c4 --- /dev/null +++ b/Tests/feaLib/data/bug502.fea @@ -0,0 +1,7 @@ +# https://github.com/behdad/fonttools/issues/502 +feature aalt { + sub A by A.alt1; + sub Eng by Eng.alt1; + sub Eng by Eng.alt2; + sub Eng by Eng.alt3; +} aalt; diff --git a/Tests/feaLib/data/bug502.ttx b/Tests/feaLib/data/bug502.ttx new file mode 100644 index 0000000..c952fba --- /dev/null +++ b/Tests/feaLib/data/bug502.ttx @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="aalt"/> + <Feature> + <!-- LookupCount=2 --> + <LookupListIndex index="0" value="0"/> + <LookupListIndex index="1" value="1"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="A" out="A.alt1"/> + </SingleSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="3"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <AlternateSubst index="0"> + <AlternateSet glyph="Eng"> + <Alternate glyph="Eng.alt1"/> + <Alternate glyph="Eng.alt2"/> + <Alternate glyph="Eng.alt3"/> + </AlternateSet> + </AlternateSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/bug504.fea b/Tests/feaLib/data/bug504.fea new file mode 100644 index 0000000..64d05ba --- /dev/null +++ b/Tests/feaLib/data/bug504.fea @@ -0,0 +1,5 @@ +# https://github.com/behdad/fonttools/issues/504 + +feature test { + sub [a b c d] by [T E S T]; +} test; diff --git a/Tests/feaLib/data/bug504.ttx b/Tests/feaLib/data/bug504.ttx new file mode 100644 index 0000000..4d678b7 --- /dev/null +++ b/Tests/feaLib/data/bug504.ttx @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="a" out="T"/> + <Substitution in="b" out="E"/> + <Substitution in="c" out="S"/> + <Substitution in="d" out="T"/> + </SingleSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/bug505.fea b/Tests/feaLib/data/bug505.fea new file mode 100644 index 0000000..016ed1f --- /dev/null +++ b/Tests/feaLib/data/bug505.fea @@ -0,0 +1,12 @@ +# https://github.com/behdad/fonttools/issues/505 + +languagesystem armn dflt; +languagesystem avst dflt; +languagesystem bali dflt; +languagesystem bamu dflt; + +feature test { + script linb; + script vai; + sub T by t; +} test; diff --git a/Tests/feaLib/data/bug505.ttx b/Tests/feaLib/data/bug505.ttx new file mode 100644 index 0000000..e0a556f --- /dev/null +++ b/Tests/feaLib/data/bug505.ttx @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="vai "/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="T" out="t"/> + </SingleSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/bug506.fea b/Tests/feaLib/data/bug506.fea new file mode 100644 index 0000000..62e4bbb --- /dev/null +++ b/Tests/feaLib/data/bug506.fea @@ -0,0 +1,4 @@ +# https://github.com/behdad/fonttools/issues/506 +feature test { + sub f' i' by f_i; +} test; diff --git a/Tests/feaLib/data/bug506.ttx b/Tests/feaLib/data/bug506.ttx new file mode 100644 index 0000000..9aff913 --- /dev/null +++ b/Tests/feaLib/data/bug506.ttx @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="6"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <ChainContextSubst index="0" Format="3"> + <!-- BacktrackGlyphCount=0 --> + <!-- InputGlyphCount=2 --> + <InputCoverage index="0"> + <Glyph value="f"/> + </InputCoverage> + <InputCoverage index="1"> + <Glyph value="i"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </SubstLookupRecord> + </ChainContextSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="f"> + <Ligature components="i" glyph="f_i"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/bug509.fea b/Tests/feaLib/data/bug509.fea new file mode 100644 index 0000000..b7af056 --- /dev/null +++ b/Tests/feaLib/data/bug509.fea @@ -0,0 +1,5 @@ +@LETTER_A = [A A.sc A.alt1]; +feature test { + ignore sub A; + sub @LETTER_A' by a; +} test; diff --git a/Tests/feaLib/data/bug509.ttx b/Tests/feaLib/data/bug509.ttx new file mode 100644 index 0000000..e5a36af --- /dev/null +++ b/Tests/feaLib/data/bug509.ttx @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="6"/> + <LookupFlag value="0"/> + <!-- SubTableCount=2 --> + <ChainContextSubst index="0" Format="3"> + <!-- BacktrackGlyphCount=0 --> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="A"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=0 --> + </ChainContextSubst> + <ChainContextSubst index="1" Format="3"> + <!-- BacktrackGlyphCount=0 --> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="A"/> + <Glyph value="A.sc"/> + <Glyph value="A.alt1"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </SubstLookupRecord> + </ChainContextSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="A" out="a"/> + <Substitution in="A.alt1" out="a"/> + <Substitution in="A.sc" out="a"/> + </SingleSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/bug512.fea b/Tests/feaLib/data/bug512.fea new file mode 100644 index 0000000..310b294 --- /dev/null +++ b/Tests/feaLib/data/bug512.fea @@ -0,0 +1,6 @@ +feature test { + sub G' by G.swash; + sub H' by H.swash; + sub G' by g; + sub H' by H.swash; +} test; diff --git a/Tests/feaLib/data/bug512.ttx b/Tests/feaLib/data/bug512.ttx new file mode 100644 index 0000000..1dfe63f --- /dev/null +++ b/Tests/feaLib/data/bug512.ttx @@ -0,0 +1,110 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=3 --> + <Lookup index="0"> + <LookupType value="6"/> + <LookupFlag value="0"/> + <!-- SubTableCount=4 --> + <ChainContextSubst index="0" Format="3"> + <!-- BacktrackGlyphCount=0 --> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="G"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </SubstLookupRecord> + </ChainContextSubst> + <ChainContextSubst index="1" Format="3"> + <!-- BacktrackGlyphCount=0 --> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="H"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </SubstLookupRecord> + </ChainContextSubst> + <ChainContextSubst index="2" Format="3"> + <!-- BacktrackGlyphCount=0 --> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="G"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="2"/> + </SubstLookupRecord> + </ChainContextSubst> + <ChainContextSubst index="3" Format="3"> + <!-- BacktrackGlyphCount=0 --> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="H"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="2"/> + </SubstLookupRecord> + </ChainContextSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="G" out="G.swash"/> + <Substitution in="H" out="H.swash"/> + </SingleSubst> + </Lookup> + <Lookup index="2"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="G" out="g"/> + <Substitution in="H" out="H.swash"/> + </SingleSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/bug514.fea b/Tests/feaLib/data/bug514.fea new file mode 100644 index 0000000..26da865 --- /dev/null +++ b/Tests/feaLib/data/bug514.fea @@ -0,0 +1,11 @@ +# The many chain targets for this feature should get combined into +# two separate SinglePos lookups: {A:-40, B:-40, C:-40} and {A:-111}. +# https://github.com/fonttools/fonttools/issues/514 +# +# makeotf produces {A:-40, B:-40, C:-40} and {A:-111, B:-40} which +# is redundant. https://github.com/adobe-type-tools/afdko/issues/169 +feature test { + pos X [A-B]' -40 B' -40 A' -40 Y; + pos X A' -111 Y; + pos X B' -40 A' -111 [A-C]' -40 Y; +} test; diff --git a/Tests/feaLib/data/bug514.ttx b/Tests/feaLib/data/bug514.ttx new file mode 100644 index 0000000..f6b14f6 --- /dev/null +++ b/Tests/feaLib/data/bug514.ttx @@ -0,0 +1,154 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.7"> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=3 --> + <Lookup index="0"> + <LookupType value="8"/> + <LookupFlag value="0"/> + <!-- SubTableCount=3 --> + <ChainContextPos index="0" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="X"/> + </BacktrackCoverage> + <!-- InputGlyphCount=3 --> + <InputCoverage index="0"> + <Glyph value="A"/> + <Glyph value="B"/> + </InputCoverage> + <InputCoverage index="1"> + <Glyph value="B"/> + </InputCoverage> + <InputCoverage index="2"> + <Glyph value="A"/> + </InputCoverage> + <!-- LookAheadGlyphCount=1 --> + <LookAheadCoverage index="0"> + <Glyph value="Y"/> + </LookAheadCoverage> + <!-- PosCount=3 --> + <PosLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </PosLookupRecord> + <PosLookupRecord index="1"> + <SequenceIndex value="1"/> + <LookupListIndex value="1"/> + </PosLookupRecord> + <PosLookupRecord index="2"> + <SequenceIndex value="2"/> + <LookupListIndex value="1"/> + </PosLookupRecord> + </ChainContextPos> + <ChainContextPos index="1" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="X"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="A"/> + </InputCoverage> + <!-- LookAheadGlyphCount=1 --> + <LookAheadCoverage index="0"> + <Glyph value="Y"/> + </LookAheadCoverage> + <!-- PosCount=1 --> + <PosLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="2"/> + </PosLookupRecord> + </ChainContextPos> + <ChainContextPos index="2" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="X"/> + </BacktrackCoverage> + <!-- InputGlyphCount=3 --> + <InputCoverage index="0"> + <Glyph value="B"/> + </InputCoverage> + <InputCoverage index="1"> + <Glyph value="A"/> + </InputCoverage> + <InputCoverage index="2"> + <Glyph value="A"/> + <Glyph value="B"/> + <Glyph value="C"/> + </InputCoverage> + <!-- LookAheadGlyphCount=1 --> + <LookAheadCoverage index="0"> + <Glyph value="Y"/> + </LookAheadCoverage> + <!-- PosCount=3 --> + <PosLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </PosLookupRecord> + <PosLookupRecord index="1"> + <SequenceIndex value="1"/> + <LookupListIndex value="2"/> + </PosLookupRecord> + <PosLookupRecord index="2"> + <SequenceIndex value="2"/> + <LookupListIndex value="1"/> + </PosLookupRecord> + </ChainContextPos> + </Lookup> + <Lookup index="1"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="A"/> + <Glyph value="B"/> + <Glyph value="C"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="-40"/> + </SinglePos> + </Lookup> + <Lookup index="2"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="A"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="-111"/> + </SinglePos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/bug568.fea b/Tests/feaLib/data/bug568.fea new file mode 100644 index 0000000..3e879b1 --- /dev/null +++ b/Tests/feaLib/data/bug568.fea @@ -0,0 +1,11 @@ +# https://github.com/behdad/fonttools/issues/568 + +feature tst1 { + script latn; + pos T -20; +} tst1; + +feature tst2 { + script cyrl; + pos T -80; +} tst2; diff --git a/Tests/feaLib/data/bug568.ttx b/Tests/feaLib/data/bug568.ttx new file mode 100644 index 0000000..b47e14a --- /dev/null +++ b/Tests/feaLib/data/bug568.ttx @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=2 --> + <ScriptRecord index="0"> + <ScriptTag value="cyrl"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="1"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + <ScriptRecord index="1"> + <ScriptTag value="latn"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=2 --> + <FeatureRecord index="0"> + <FeatureTag value="tst1"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="1"> + <FeatureTag value="tst2"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="1"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="T"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="-20"/> + </SinglePos> + </Lookup> + <Lookup index="1"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="T"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="-80"/> + </SinglePos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/bug633.fea b/Tests/feaLib/data/bug633.fea new file mode 100644 index 0000000..4f47168 --- /dev/null +++ b/Tests/feaLib/data/bug633.fea @@ -0,0 +1,10 @@ +# https://github.com/fonttools/fonttools/issues/633 + +@public.kern1.K = [K X]; +@public.kern2.O = [C O]; +@public.kern2.V = [V W]; + +feature kern { + pos @public.kern1.K @public.kern2.O -20; + pos @public.kern1.K @public.kern2.V 0; +} kern; diff --git a/Tests/feaLib/data/bug633.ttx b/Tests/feaLib/data/bug633.ttx new file mode 100644 index 0000000..b119ebb --- /dev/null +++ b/Tests/feaLib/data/bug633.ttx @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="kern"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="2"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <PairPos index="0" Format="2"> + <Coverage> + <Glyph value="K"/> + <Glyph value="X"/> + </Coverage> + <ValueFormat1 value="4"/> + <ValueFormat2 value="0"/> + <ClassDef1> + </ClassDef1> + <ClassDef2> + <ClassDef glyph="C" class="2"/> + <ClassDef glyph="O" class="2"/> + <ClassDef glyph="V" class="1"/> + <ClassDef glyph="W" class="1"/> + </ClassDef2> + <!-- Class1Count=1 --> + <!-- Class2Count=3 --> + <Class1Record index="0"> + <Class2Record index="0"> + </Class2Record> + <Class2Record index="1"> + <Value1 XAdvance="0"/> + </Class2Record> + <Class2Record index="2"> + <Value1 XAdvance="-20"/> + </Class2Record> + </Class1Record> + </PairPos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/enum.fea b/Tests/feaLib/data/enum.fea new file mode 100644 index 0000000..124fb62 --- /dev/null +++ b/Tests/feaLib/data/enum.fea @@ -0,0 +1,7 @@ +languagesystem DFLT dflt; + +feature kern { + @Y_LC = [y yacute ydieresis]; + @SMALL_PUNC = [comma semicolon period]; + enum pos @Y_LC @SMALL_PUNC -100; +} kern; diff --git a/Tests/feaLib/data/enum.ttx b/Tests/feaLib/data/enum.ttx new file mode 100644 index 0000000..7852aec --- /dev/null +++ b/Tests/feaLib/data/enum.ttx @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="kern"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="2"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <PairPos index="0" Format="1"> + <Coverage> + <Glyph value="y"/> + <Glyph value="ydieresis"/> + <Glyph value="yacute"/> + </Coverage> + <ValueFormat1 value="4"/> + <ValueFormat2 value="0"/> + <!-- PairSetCount=3 --> + <PairSet index="0"> + <!-- PairValueCount=3 --> + <PairValueRecord index="0"> + <SecondGlyph value="semicolon"/> + <Value1 XAdvance="-100"/> + </PairValueRecord> + <PairValueRecord index="1"> + <SecondGlyph value="period"/> + <Value1 XAdvance="-100"/> + </PairValueRecord> + <PairValueRecord index="2"> + <SecondGlyph value="comma"/> + <Value1 XAdvance="-100"/> + </PairValueRecord> + </PairSet> + <PairSet index="1"> + <!-- PairValueCount=3 --> + <PairValueRecord index="0"> + <SecondGlyph value="semicolon"/> + <Value1 XAdvance="-100"/> + </PairValueRecord> + <PairValueRecord index="1"> + <SecondGlyph value="period"/> + <Value1 XAdvance="-100"/> + </PairValueRecord> + <PairValueRecord index="2"> + <SecondGlyph value="comma"/> + <Value1 XAdvance="-100"/> + </PairValueRecord> + </PairSet> + <PairSet index="2"> + <!-- PairValueCount=3 --> + <PairValueRecord index="0"> + <SecondGlyph value="semicolon"/> + <Value1 XAdvance="-100"/> + </PairValueRecord> + <PairValueRecord index="1"> + <SecondGlyph value="period"/> + <Value1 XAdvance="-100"/> + </PairValueRecord> + <PairValueRecord index="2"> + <SecondGlyph value="comma"/> + <Value1 XAdvance="-100"/> + </PairValueRecord> + </PairSet> + </PairPos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/feature_aalt.fea b/Tests/feaLib/data/feature_aalt.fea new file mode 100644 index 0000000..1905ca9 --- /dev/null +++ b/Tests/feaLib/data/feature_aalt.fea @@ -0,0 +1,29 @@ +languagesystem DFLT dflt; + +feature aalt { + feature sups; + feature frac; + feature ordn; +} aalt; + +feature sups { + sub one by onesuperior; + sub two by twosuperior; + sub three by threesuperior; +} sups; + +feature frac { + sub one slash four by onequarter; + sub one slash two by onehalf; + sub three slash four by threequarters; +} frac; + +feature ordn { + sub [zero one two three four five six seven eight nine] [A a]' by ordfeminine; + sub [zero one two three four five six seven eight nine] [O o]' by ordmasculine; +} ordn; + +feature liga { + sub f i by f_i; + sub f l by f_l; +} liga; diff --git a/Tests/feaLib/data/feature_aalt.ttx b/Tests/feaLib/data/feature_aalt.ttx new file mode 100644 index 0000000..eac2c3f --- /dev/null +++ b/Tests/feaLib/data/feature_aalt.ttx @@ -0,0 +1,184 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=5 --> + <FeatureIndex index="0" value="0"/> + <FeatureIndex index="1" value="1"/> + <FeatureIndex index="2" value="2"/> + <FeatureIndex index="3" value="3"/> + <FeatureIndex index="4" value="4"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=5 --> + <FeatureRecord index="0"> + <FeatureTag value="aalt"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="1"> + <FeatureTag value="frac"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="2"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="2"> + <FeatureTag value="liga"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="5"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="3"> + <FeatureTag value="ordn"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="3"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="4"> + <FeatureTag value="sups"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="1"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=6 --> + <Lookup index="0"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="A" out="ordfeminine"/> + <Substitution in="O" out="ordmasculine"/> + <Substitution in="a" out="ordfeminine"/> + <Substitution in="o" out="ordmasculine"/> + <Substitution in="one" out="onesuperior"/> + <Substitution in="three" out="threesuperior"/> + <Substitution in="two" out="twosuperior"/> + </SingleSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="one" out="onesuperior"/> + <Substitution in="three" out="threesuperior"/> + <Substitution in="two" out="twosuperior"/> + </SingleSubst> + </Lookup> + <Lookup index="2"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="one"> + <Ligature components="slash,four" glyph="onequarter"/> + <Ligature components="slash,two" glyph="onehalf"/> + </LigatureSet> + <LigatureSet glyph="three"> + <Ligature components="slash,four" glyph="threequarters"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + <Lookup index="3"> + <LookupType value="6"/> + <LookupFlag value="0"/> + <!-- SubTableCount=2 --> + <ChainContextSubst index="0" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="zero"/> + <Glyph value="one"/> + <Glyph value="two"/> + <Glyph value="three"/> + <Glyph value="four"/> + <Glyph value="five"/> + <Glyph value="six"/> + <Glyph value="seven"/> + <Glyph value="eight"/> + <Glyph value="nine"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="A"/> + <Glyph value="a"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="4"/> + </SubstLookupRecord> + </ChainContextSubst> + <ChainContextSubst index="1" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="zero"/> + <Glyph value="one"/> + <Glyph value="two"/> + <Glyph value="three"/> + <Glyph value="four"/> + <Glyph value="five"/> + <Glyph value="six"/> + <Glyph value="seven"/> + <Glyph value="eight"/> + <Glyph value="nine"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="O"/> + <Glyph value="o"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="4"/> + </SubstLookupRecord> + </ChainContextSubst> + </Lookup> + <Lookup index="4"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="A" out="ordfeminine"/> + <Substitution in="O" out="ordmasculine"/> + <Substitution in="a" out="ordfeminine"/> + <Substitution in="o" out="ordmasculine"/> + </SingleSubst> + </Lookup> + <Lookup index="5"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="f"> + <Ligature components="i" glyph="f_i"/> + <Ligature components="l" glyph="f_l"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/ignore_pos.fea b/Tests/feaLib/data/ignore_pos.fea new file mode 100644 index 0000000..58e217e --- /dev/null +++ b/Tests/feaLib/data/ignore_pos.fea @@ -0,0 +1,5 @@ +feature test { + ignore pos f [a e] d'; + ignore pos a d' d; + pos [a e n] d' -20; +} test; diff --git a/Tests/feaLib/data/ignore_pos.ttx b/Tests/feaLib/data/ignore_pos.ttx new file mode 100644 index 0000000..6b03b59 --- /dev/null +++ b/Tests/feaLib/data/ignore_pos.ttx @@ -0,0 +1,101 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="8"/> + <LookupFlag value="0"/> + <!-- SubTableCount=3 --> + <ChainContextPos index="0" Format="3"> + <!-- BacktrackGlyphCount=2 --> + <BacktrackCoverage index="0"> + <Glyph value="a"/> + <Glyph value="e"/> + </BacktrackCoverage> + <BacktrackCoverage index="1"> + <Glyph value="f"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="d"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- PosCount=0 --> + </ChainContextPos> + <ChainContextPos index="1" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="a"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="d"/> + </InputCoverage> + <!-- LookAheadGlyphCount=1 --> + <LookAheadCoverage index="0"> + <Glyph value="d"/> + </LookAheadCoverage> + <!-- PosCount=0 --> + </ChainContextPos> + <ChainContextPos index="2" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="a"/> + <Glyph value="e"/> + <Glyph value="n"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="d"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- PosCount=1 --> + <PosLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </PosLookupRecord> + </ChainContextPos> + </Lookup> + <Lookup index="1"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="d"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="-20"/> + </SinglePos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/include/include1.fea b/Tests/feaLib/data/include/include1.fea new file mode 100644 index 0000000..b88d1f5 --- /dev/null +++ b/Tests/feaLib/data/include/include1.fea @@ -0,0 +1,3 @@ +I1a +include(../include0.fea) +I1b diff --git a/Tests/feaLib/data/include/include3.fea b/Tests/feaLib/data/include/include3.fea new file mode 100644 index 0000000..09cbbbe --- /dev/null +++ b/Tests/feaLib/data/include/include3.fea @@ -0,0 +1,4 @@ +I3a +include(subdir/include2.fea); +I3b + diff --git a/Tests/feaLib/data/include/include4.fea b/Tests/feaLib/data/include/include4.fea new file mode 100644 index 0000000..677b460 --- /dev/null +++ b/Tests/feaLib/data/include/include4.fea @@ -0,0 +1,4 @@ +I4a +include(include3.fea); +I4b + diff --git a/Tests/feaLib/data/include/include5.fea b/Tests/feaLib/data/include/include5.fea new file mode 100644 index 0000000..553967b --- /dev/null +++ b/Tests/feaLib/data/include/include5.fea @@ -0,0 +1,3 @@ +I5a +include(include4.fea); +I5b diff --git a/Tests/feaLib/data/include/include6.fea b/Tests/feaLib/data/include/include6.fea new file mode 100644 index 0000000..75a64be --- /dev/null +++ b/Tests/feaLib/data/include/include6.fea @@ -0,0 +1,3 @@ +I6a +include(include5.fea); +I6b diff --git a/Tests/feaLib/data/include/includemissingfile.fea b/Tests/feaLib/data/include/includemissingfile.fea new file mode 100644 index 0000000..524c293 --- /dev/null +++ b/Tests/feaLib/data/include/includemissingfile.fea @@ -0,0 +1 @@ +include(missingfile.fea); diff --git a/Tests/feaLib/data/include/includeself.fea b/Tests/feaLib/data/include/includeself.fea new file mode 100644 index 0000000..6caad09 --- /dev/null +++ b/Tests/feaLib/data/include/includeself.fea @@ -0,0 +1 @@ +include(includeself.fea); diff --git a/Tests/feaLib/data/include/subdir/include2.fea b/Tests/feaLib/data/include/subdir/include2.fea new file mode 100644 index 0000000..52c078a --- /dev/null +++ b/Tests/feaLib/data/include/subdir/include2.fea @@ -0,0 +1,3 @@ +I2a +include(include1.fea); +I2b diff --git a/Tests/feaLib/data/include0.fea b/Tests/feaLib/data/include0.fea new file mode 100644 index 0000000..8f177e1 --- /dev/null +++ b/Tests/feaLib/data/include0.fea @@ -0,0 +1 @@ +I0 diff --git a/Tests/feaLib/data/language_required.fea b/Tests/feaLib/data/language_required.fea new file mode 100644 index 0000000..4005a78 --- /dev/null +++ b/Tests/feaLib/data/language_required.fea @@ -0,0 +1,22 @@ +languagesystem latn DEU; +languagesystem latn FRA; +languagesystem latn ITA; + +feature hlig { + script latn; + language DEU exclude_dflt required; + sub D E U by D.sc; + + language FRA exclude_dflt; + sub F R A by F.sc; +} hlig; + +feature liga { + script latn; + language ITA exclude_dflt required; + sub I T A by I.sc; +} liga; + +feature scmp { + sub [a-z] by [A.sc-Z.sc]; +} scmp; diff --git a/Tests/feaLib/data/language_required.ttx b/Tests/feaLib/data/language_required.ttx new file mode 100644 index 0000000..5481439 --- /dev/null +++ b/Tests/feaLib/data/language_required.ttx @@ -0,0 +1,139 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="latn"/> + <Script> + <!-- LangSysCount=3 --> + <LangSysRecord index="0"> + <LangSysTag value="DEU "/> + <LangSys> + <ReqFeatureIndex value="0"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="3"/> + </LangSys> + </LangSysRecord> + <LangSysRecord index="1"> + <LangSysTag value="FRA "/> + <LangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=2 --> + <FeatureIndex index="0" value="1"/> + <FeatureIndex index="1" value="3"/> + </LangSys> + </LangSysRecord> + <LangSysRecord index="2"> + <LangSysTag value="ITA "/> + <LangSys> + <ReqFeatureIndex value="2"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="3"/> + </LangSys> + </LangSysRecord> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=4 --> + <FeatureRecord index="0"> + <FeatureTag value="hlig"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="1"> + <FeatureTag value="hlig"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="1"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="2"> + <FeatureTag value="liga"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="2"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="3"> + <FeatureTag value="scmp"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="3"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=4 --> + <Lookup index="0"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="D"> + <Ligature components="E,U" glyph="D.sc"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="F"> + <Ligature components="R,A" glyph="F.sc"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + <Lookup index="2"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="I"> + <Ligature components="T,A" glyph="I.sc"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + <Lookup index="3"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="a" out="A.sc"/> + <Substitution in="b" out="B.sc"/> + <Substitution in="c" out="C.sc"/> + <Substitution in="d" out="D.sc"/> + <Substitution in="e" out="E.sc"/> + <Substitution in="f" out="F.sc"/> + <Substitution in="g" out="G.sc"/> + <Substitution in="h" out="H.sc"/> + <Substitution in="i" out="I.sc"/> + <Substitution in="j" out="J.sc"/> + <Substitution in="k" out="K.sc"/> + <Substitution in="l" out="L.sc"/> + <Substitution in="m" out="M.sc"/> + <Substitution in="n" out="N.sc"/> + <Substitution in="o" out="O.sc"/> + <Substitution in="p" out="P.sc"/> + <Substitution in="q" out="Q.sc"/> + <Substitution in="r" out="R.sc"/> + <Substitution in="s" out="S.sc"/> + <Substitution in="t" out="T.sc"/> + <Substitution in="u" out="U.sc"/> + <Substitution in="v" out="V.sc"/> + <Substitution in="w" out="W.sc"/> + <Substitution in="x" out="X.sc"/> + <Substitution in="y" out="Y.sc"/> + <Substitution in="z" out="Z.sc"/> + </SingleSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/lookup.fea b/Tests/feaLib/data/lookup.fea new file mode 100644 index 0000000..ad1fb7b --- /dev/null +++ b/Tests/feaLib/data/lookup.fea @@ -0,0 +1,24 @@ +# Three features. In the output, they should all point to the same lookup. + +lookup SomeLookup { + sub f f i by f_f_i; + sub f i by f_i; +} SomeLookup; + +feature tst1 { + lookup SomeLookup; +} tst1; + +feature tst2 { + lookup SomeLookup; +} tst2; + +feature tst3 { + lookup EmbeddedLookup { + sub A by A.sc; + } EmbeddedLookup; +} tst3; + +feature tst4 { + lookup EmbeddedLookup; +} tst4; diff --git a/Tests/feaLib/data/lookup.ttx b/Tests/feaLib/data/lookup.ttx new file mode 100644 index 0000000..35ab04c --- /dev/null +++ b/Tests/feaLib/data/lookup.ttx @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=4 --> + <FeatureIndex index="0" value="0"/> + <FeatureIndex index="1" value="1"/> + <FeatureIndex index="2" value="2"/> + <FeatureIndex index="3" value="3"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=4 --> + <FeatureRecord index="0"> + <FeatureTag value="tst1"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="1"> + <FeatureTag value="tst2"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="2"> + <FeatureTag value="tst3"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="1"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="3"> + <FeatureTag value="tst4"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="1"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="f"> + <Ligature components="f,i" glyph="f_f_i"/> + <Ligature components="i" glyph="f_i"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="A" out="A.sc"/> + </SingleSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/lookupflag.fea b/Tests/feaLib/data/lookupflag.fea new file mode 100644 index 0000000..651dcd0 --- /dev/null +++ b/Tests/feaLib/data/lookupflag.fea @@ -0,0 +1,97 @@ +languagesystem DFLT dflt; + +@TOP_MARKS = [acute grave macron]; +markClass [cedilla ogonek] <anchor 350 -20> @BOTTOM_MARKS; +@FRENCH_MARKS = [acute grave cedilla dieresis circumflex]; +@MARKS_WITH_DUPLICATES = [breve caron umlaut breve caron umlaut]; + +lookup A { + lookupflag RightToLeft; + pos one 1; +} A; + +lookup B { + lookupflag IgnoreBaseGlyphs; + pos two 2; +} B; + +lookup C { + lookupflag IgnoreLigatures; + pos four 4; +} C; + +lookup D { +#test-fea2fea: lookupflag RightToLeft IgnoreBaseGlyphs IgnoreLigatures; + lookupflag 7; + pos seven 7; +} D; + +lookup E { + lookupflag IgnoreMarks; + pos eight 8; +} E; + +lookup F { + lookupflag MarkAttachmentType @TOP_MARKS; + pos F 1; +} F; + +lookup G { + lookupflag MarkAttachmentType @BOTTOM_MARKS; + pos G 1; +} G; + +lookup H { + lookupflag IgnoreLigatures MarkAttachmentType @TOP_MARKS; + pos H 1; +} H; + +lookup I { + lookupflag UseMarkFilteringSet @TOP_MARKS; + pos I 1; +} I; + +lookup J { + # @FRENCH_MARKS overlaps with @TOP_MARKS. + # Other than with MarkAttachmentType, the same glyph may appear + # in multiple glyphsets for UseMarkFilteringSet. Make sure that + # our implementation can handle this. + lookupflag UseMarkFilteringSet @FRENCH_MARKS; + pos J 1; +} J; + +lookup K { + lookupflag IgnoreLigatures UseMarkFilteringSet @TOP_MARKS; + pos K 1; +} K; + +lookup L { + pos L 1; +} L; + +lookup M { + lookupflag UseMarkFilteringSet @MARKS_WITH_DUPLICATES; + pos M 1; +} M; + +lookup N { + lookupflag MarkAttachmentType @MARKS_WITH_DUPLICATES; + pos N 1; +} N; + +feature test { + lookup A; + lookup B; + lookup C; + lookup D; + lookup E; + lookup F; + lookup G; + lookup H; + lookup I; + lookup J; + lookup K; + lookup L; + lookup M; + lookup N; +} test; diff --git a/Tests/feaLib/data/lookupflag.ttx b/Tests/feaLib/data/lookupflag.ttx new file mode 100644 index 0000000..bb05b9a --- /dev/null +++ b/Tests/feaLib/data/lookupflag.ttx @@ -0,0 +1,259 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GDEF> + <Version value="0x00010002"/> + <GlyphClassDef> + <ClassDef glyph="cedilla" class="3"/> + <ClassDef glyph="ogonek" class="3"/> + </GlyphClassDef> + <MarkAttachClassDef> + <ClassDef glyph="acute" class="1"/> + <ClassDef glyph="breve" class="3"/> + <ClassDef glyph="caron" class="3"/> + <ClassDef glyph="cedilla" class="2"/> + <ClassDef glyph="grave" class="1"/> + <ClassDef glyph="macron" class="1"/> + <ClassDef glyph="ogonek" class="2"/> + <ClassDef glyph="umlaut" class="3"/> + </MarkAttachClassDef> + <MarkGlyphSetsDef> + <MarkSetTableFormat value="1"/> + <!-- MarkSetCount=3 --> + <Coverage index="0"> + <Glyph value="grave"/> + <Glyph value="acute"/> + <Glyph value="macron"/> + </Coverage> + <Coverage index="1"> + <Glyph value="grave"/> + <Glyph value="acute"/> + <Glyph value="dieresis"/> + <Glyph value="circumflex"/> + <Glyph value="cedilla"/> + </Coverage> + <Coverage index="2"> + <Glyph value="breve"/> + <Glyph value="umlaut"/> + <Glyph value="caron"/> + </Coverage> + </MarkGlyphSetsDef> + </GDEF> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=14 --> + <LookupListIndex index="0" value="0"/> + <LookupListIndex index="1" value="1"/> + <LookupListIndex index="2" value="2"/> + <LookupListIndex index="3" value="3"/> + <LookupListIndex index="4" value="4"/> + <LookupListIndex index="5" value="5"/> + <LookupListIndex index="6" value="6"/> + <LookupListIndex index="7" value="7"/> + <LookupListIndex index="8" value="8"/> + <LookupListIndex index="9" value="9"/> + <LookupListIndex index="10" value="10"/> + <LookupListIndex index="11" value="11"/> + <LookupListIndex index="12" value="12"/> + <LookupListIndex index="13" value="13"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=14 --> + <Lookup index="0"> + <LookupType value="1"/> + <LookupFlag value="1"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="one"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="1"/> + </SinglePos> + </Lookup> + <Lookup index="1"> + <LookupType value="1"/> + <LookupFlag value="2"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="two"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="2"/> + </SinglePos> + </Lookup> + <Lookup index="2"> + <LookupType value="1"/> + <LookupFlag value="4"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="four"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="4"/> + </SinglePos> + </Lookup> + <Lookup index="3"> + <LookupType value="1"/> + <LookupFlag value="7"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="seven"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="7"/> + </SinglePos> + </Lookup> + <Lookup index="4"> + <LookupType value="1"/> + <LookupFlag value="8"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="eight"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="8"/> + </SinglePos> + </Lookup> + <Lookup index="5"> + <LookupType value="1"/> + <LookupFlag value="256"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="F"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="1"/> + </SinglePos> + </Lookup> + <Lookup index="6"> + <LookupType value="1"/> + <LookupFlag value="512"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="G"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="1"/> + </SinglePos> + </Lookup> + <Lookup index="7"> + <LookupType value="1"/> + <LookupFlag value="260"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="H"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="1"/> + </SinglePos> + </Lookup> + <Lookup index="8"> + <LookupType value="1"/> + <LookupFlag value="16"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="I"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="1"/> + </SinglePos> + <MarkFilteringSet value="0"/> + </Lookup> + <Lookup index="9"> + <LookupType value="1"/> + <LookupFlag value="16"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="J"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="1"/> + </SinglePos> + <MarkFilteringSet value="1"/> + </Lookup> + <Lookup index="10"> + <LookupType value="1"/> + <LookupFlag value="20"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="K"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="1"/> + </SinglePos> + <MarkFilteringSet value="0"/> + </Lookup> + <Lookup index="11"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="L"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="1"/> + </SinglePos> + </Lookup> + <Lookup index="12"> + <LookupType value="1"/> + <LookupFlag value="16"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="M"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="1"/> + </SinglePos> + <MarkFilteringSet value="2"/> + </Lookup> + <Lookup index="13"> + <LookupType value="1"/> + <LookupFlag value="768"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="N"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="1"/> + </SinglePos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/markClass.fea b/Tests/feaLib/data/markClass.fea new file mode 100644 index 0000000..855de6f --- /dev/null +++ b/Tests/feaLib/data/markClass.fea @@ -0,0 +1,12 @@ +languagesystem DFLT dflt; + +markClass [acute] <anchor 350 0> @TOP_MARKS; + +feature foo { + markClass [grave] <anchor 350 0> @TOP_MARKS; + markClass cedilla <anchor 300 0> @BOTTOM_MARKS; +} foo; + +feature bar { + markClass [dieresis breve] <anchor 400 0> @TOP_MARKS; +} bar; diff --git a/Tests/feaLib/data/markClass.ttx b/Tests/feaLib/data/markClass.ttx new file mode 100644 index 0000000..88ed5f9 --- /dev/null +++ b/Tests/feaLib/data/markClass.ttx @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GDEF> + <Version value="0x00010000"/> + <GlyphClassDef> + <ClassDef glyph="acute" class="3"/> + <ClassDef glyph="breve" class="3"/> + <ClassDef glyph="cedilla" class="3"/> + <ClassDef glyph="dieresis" class="3"/> + <ClassDef glyph="grave" class="3"/> + </GlyphClassDef> + </GDEF> + +</ttFont> diff --git a/Tests/feaLib/data/mini.fea b/Tests/feaLib/data/mini.fea new file mode 100644 index 0000000..9cf4b2f --- /dev/null +++ b/Tests/feaLib/data/mini.fea @@ -0,0 +1,19 @@ +# Example file from OpenType Feature File specification, section 1. +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html + +# Script and language coverage +languagesystem DFLT dflt; +languagesystem latn dflt; + +# Ligature formation +feature liga { + substitute f i by f_i; + substitute f l by f_l; +} liga; + +# Kerning +feature kern { + position A Y -100; + position a y -80; + position s f' <0 0 10 0> t; +} kern; diff --git a/Tests/feaLib/data/multiple_feature_blocks.fea b/Tests/feaLib/data/multiple_feature_blocks.fea new file mode 100644 index 0000000..2f9902a --- /dev/null +++ b/Tests/feaLib/data/multiple_feature_blocks.fea @@ -0,0 +1,18 @@ +languagesystem DFLT dflt; +languagesystem latn dflt; +languagesystem latn TRK; + +feature liga { + lookup liga_fl { + sub f l by f_l; + } liga_fl; +} liga; + +feature liga { + lookup liga_fi { + sub f i by f_i; + } liga_fi; + + script latn; + language TRK exclude_dflt; +} liga; diff --git a/Tests/feaLib/data/multiple_feature_blocks.ttx b/Tests/feaLib/data/multiple_feature_blocks.ttx new file mode 100644 index 0000000..4f9a389 --- /dev/null +++ b/Tests/feaLib/data/multiple_feature_blocks.ttx @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.0"> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=2 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="1"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + <ScriptRecord index="1"> + <ScriptTag value="latn"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="1"/> + </DefaultLangSys> + <!-- LangSysCount=1 --> + <LangSysRecord index="0"> + <LangSysTag value="TRK "/> + <LangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </LangSys> + </LangSysRecord> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=2 --> + <FeatureRecord index="0"> + <FeatureTag value="liga"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="1"> + <FeatureTag value="liga"/> + <Feature> + <!-- LookupCount=2 --> + <LookupListIndex index="0" value="0"/> + <LookupListIndex index="1" value="1"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="f"> + <Ligature components="l" glyph="f_l"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="f"> + <Ligature components="i" glyph="f_i"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/name.fea b/Tests/feaLib/data/name.fea new file mode 100644 index 0000000..17727ed --- /dev/null +++ b/Tests/feaLib/data/name.fea @@ -0,0 +1,23 @@ +table name { +#test-fea2fea: + nameid 1 "Ignored-1"; +#test-fea2fea: + nameid 2 "Ignored-2"; +#test-fea2fea: + nameid 3 "Ignored-3"; +#test-fea2fea: + nameid 4 "Ignored-4"; +#test-fea2fea: + nameid 5 "Ignored-5"; +#test-fea2fea: + nameid 6 "Ignored-6"; +#test-fea2fea: nameid 7 "Test7"; + nameid 7 3 "Test7"; + nameid 8 1 "Test8"; +#test-fea2fea: nameid 9 "Test9"; + nameid 9 3 1 0x0409 "Test9"; + nameid 10 1 "Test10"; +#test-fea2fea: nameid 11 1 "Test11"; + nameid 11 1 0 0 "Test11"; +} name; + diff --git a/Tests/feaLib/data/name.ttx b/Tests/feaLib/data/name.ttx new file mode 100644 index 0000000..cdb6038 --- /dev/null +++ b/Tests/feaLib/data/name.ttx @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <name> + <namerecord nameID="7" platformID="3" platEncID="1" langID="0x409"> + Test7 + </namerecord> + <namerecord nameID="8" platformID="1" platEncID="0" langID="0x0" unicode="True"> + Test8 + </namerecord> + <namerecord nameID="9" platformID="3" platEncID="1" langID="0x409"> + Test9 + </namerecord> + <namerecord nameID="10" platformID="1" platEncID="0" langID="0x0" unicode="True"> + Test10 + </namerecord> + <namerecord nameID="11" platformID="1" platEncID="0" langID="0x0" unicode="True"> + Test11 + </namerecord> + </name> + +</ttFont> diff --git a/Tests/feaLib/data/omitted_GlyphClassDef.fea b/Tests/feaLib/data/omitted_GlyphClassDef.fea new file mode 100644 index 0000000..d580e22 --- /dev/null +++ b/Tests/feaLib/data/omitted_GlyphClassDef.fea @@ -0,0 +1,6 @@ +@BASE = [f i]; +@MARKS = [acute grave]; + +table GDEF { + GlyphClassDef @BASE, , @MARKS, ; +} GDEF; diff --git a/Tests/feaLib/data/omitted_GlyphClassDef.ttx b/Tests/feaLib/data/omitted_GlyphClassDef.ttx new file mode 100644 index 0000000..5ad040b --- /dev/null +++ b/Tests/feaLib/data/omitted_GlyphClassDef.ttx @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GDEF> + <Version value="0x00010000"/> + <GlyphClassDef> + <ClassDef glyph="acute" class="3"/> + <ClassDef glyph="f" class="1"/> + <ClassDef glyph="grave" class="3"/> + <ClassDef glyph="i" class="1"/> + </GlyphClassDef> + </GDEF> + +</ttFont> diff --git a/Tests/feaLib/data/size.fea b/Tests/feaLib/data/size.fea new file mode 100644 index 0000000..6a30063 --- /dev/null +++ b/Tests/feaLib/data/size.fea @@ -0,0 +1,4 @@ +feature size { +#test-fea2fea: parameters 10.0 0; + parameters 10.0 0 0 0; +} size; diff --git a/Tests/feaLib/data/size.ttx b/Tests/feaLib/data/size.ttx new file mode 100644 index 0000000..1a12ddf --- /dev/null +++ b/Tests/feaLib/data/size.ttx @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="size"/> + <Feature> + <FeatureParamsSize> + <DesignSize value="10.0"/> + <SubfamilyID value="0"/> + <SubfamilyNameID value="0"/> + <RangeStart value="0.0"/> + <RangeEnd value="0.0"/> + </FeatureParamsSize> + <!-- LookupCount=0 --> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=0 --> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/size2.fea b/Tests/feaLib/data/size2.fea new file mode 100644 index 0000000..a5a08a5 --- /dev/null +++ b/Tests/feaLib/data/size2.fea @@ -0,0 +1,3 @@ +feature size { + parameters 10.0 0; +} size; diff --git a/Tests/feaLib/data/size2.ttx b/Tests/feaLib/data/size2.ttx new file mode 100644 index 0000000..a822af3 --- /dev/null +++ b/Tests/feaLib/data/size2.ttx @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="size"/> + <Feature> + <FeatureParamsSize> + <DesignSize value="10.0"/> + <SubfamilyID value="0"/> + <SubfamilyNameID value="0"/> + <RangeStart value="0"/> + <RangeEnd value="0"/> + </FeatureParamsSize> + <!-- LookupCount=0 --> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=0 --> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/spec10.fea b/Tests/feaLib/data/spec10.fea new file mode 100644 index 0000000..9d1be02 --- /dev/null +++ b/Tests/feaLib/data/spec10.fea @@ -0,0 +1,12 @@ +# OpenType Feature File specification, section 10. +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html + +anon sbit { + /* sbit table specifications */ + 72 % dpi + sizes { + 10, 12, 14 source { + all "Generic/JGeneric" + } + } +} sbit; diff --git a/Tests/feaLib/data/spec10.ttx b/Tests/feaLib/data/spec10.ttx new file mode 100644 index 0000000..5db8ac2 --- /dev/null +++ b/Tests/feaLib/data/spec10.ttx @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + +</ttFont> diff --git a/Tests/feaLib/data/spec4h1.fea b/Tests/feaLib/data/spec4h1.fea new file mode 100644 index 0000000..b43e13b --- /dev/null +++ b/Tests/feaLib/data/spec4h1.fea @@ -0,0 +1,64 @@ +# OpenType Feature File specification, section 4.h, example 1. +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html + +languagesystem DFLT dflt; +languagesystem latn dflt; +languagesystem latn DEU; +languagesystem latn TRK; +languagesystem cyrl dflt; + +feature smcp { + sub [a-z] by [A.sc-Z.sc]; + + # Since all the rules in this feature are of the same type, they + # will be grouped in a single lookup. Since no script or language + # keyword has been specified yet, the lookup will be registered + # for this feature under all the language systems. +} smcp; + +feature liga { + sub f f by f_f; + sub f i by f_i; + sub f l by f_l; + + # Since all the rules in this feature are of the same type, they + # will be grouped in a single lookup. Since no script or language + # keyword has been specified yet, the lookup will be registered + # for this feature under all the language systems. + + script latn; + language dflt; + # lookupflag 0; (implicit) + sub c t by c_t; + sub c s by c_s; + + # The rules above will be placed in a lookup that is registered + # for all the specified languages for the script latn, but not any + # other scripts. + + language DEU; + # script latn; (stays the same) + # lookupflag 0; (stays the same) + sub c h by c_h; + sub c k by c_k; + + # The rules above will be placed in a lookup that is registered + # only under the script latn, language DEU. + + language TRK; + + # This will inherit both the top level default rules - the rules + # defined before the first 'script' statement, and the + # script-level default rules for 'latn': all the lookups of this + # feature defined after the 'script latn' statement, and before + # the language DEU statement. If TRK were not named here, it + # would not inherit the default rules for the script latn. +} liga; + +# TODO(sascha): Uncomment once we support 'pos' statements. +# feature kern { +# pos a y -150; +# # [more pos statements] +# # All the rules in this feature will be grouped in a single lookup +# # that is is registered under all the language systems. +# } kern; diff --git a/Tests/feaLib/data/spec4h1.ttx b/Tests/feaLib/data/spec4h1.ttx new file mode 100644 index 0000000..0e42fc5 --- /dev/null +++ b/Tests/feaLib/data/spec4h1.ttx @@ -0,0 +1,169 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=3 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=2 --> + <FeatureIndex index="0" value="2"/> + <FeatureIndex index="1" value="3"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + <ScriptRecord index="1"> + <ScriptTag value="cyrl"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=2 --> + <FeatureIndex index="0" value="2"/> + <FeatureIndex index="1" value="3"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + <ScriptRecord index="2"> + <ScriptTag value="latn"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=2 --> + <FeatureIndex index="0" value="1"/> + <FeatureIndex index="1" value="3"/> + </DefaultLangSys> + <!-- LangSysCount=2 --> + <LangSysRecord index="0"> + <LangSysTag value="DEU "/> + <LangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=2 --> + <FeatureIndex index="0" value="0"/> + <FeatureIndex index="1" value="3"/> + </LangSys> + </LangSysRecord> + <LangSysRecord index="1"> + <LangSysTag value="TRK "/> + <LangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=2 --> + <FeatureIndex index="0" value="1"/> + <FeatureIndex index="1" value="3"/> + </LangSys> + </LangSysRecord> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=4 --> + <FeatureRecord index="0"> + <FeatureTag value="liga"/> + <Feature> + <!-- LookupCount=3 --> + <LookupListIndex index="0" value="1"/> + <LookupListIndex index="1" value="2"/> + <LookupListIndex index="2" value="3"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="1"> + <FeatureTag value="liga"/> + <Feature> + <!-- LookupCount=2 --> + <LookupListIndex index="0" value="1"/> + <LookupListIndex index="1" value="2"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="2"> + <FeatureTag value="liga"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="1"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="3"> + <FeatureTag value="smcp"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=4 --> + <Lookup index="0"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="a" out="A.sc"/> + <Substitution in="b" out="B.sc"/> + <Substitution in="c" out="C.sc"/> + <Substitution in="d" out="D.sc"/> + <Substitution in="e" out="E.sc"/> + <Substitution in="f" out="F.sc"/> + <Substitution in="g" out="G.sc"/> + <Substitution in="h" out="H.sc"/> + <Substitution in="i" out="I.sc"/> + <Substitution in="j" out="J.sc"/> + <Substitution in="k" out="K.sc"/> + <Substitution in="l" out="L.sc"/> + <Substitution in="m" out="M.sc"/> + <Substitution in="n" out="N.sc"/> + <Substitution in="o" out="O.sc"/> + <Substitution in="p" out="P.sc"/> + <Substitution in="q" out="Q.sc"/> + <Substitution in="r" out="R.sc"/> + <Substitution in="s" out="S.sc"/> + <Substitution in="t" out="T.sc"/> + <Substitution in="u" out="U.sc"/> + <Substitution in="v" out="V.sc"/> + <Substitution in="w" out="W.sc"/> + <Substitution in="x" out="X.sc"/> + <Substitution in="y" out="Y.sc"/> + <Substitution in="z" out="Z.sc"/> + </SingleSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="f"> + <Ligature components="f" glyph="f_f"/> + <Ligature components="i" glyph="f_i"/> + <Ligature components="l" glyph="f_l"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + <Lookup index="2"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="c"> + <Ligature components="s" glyph="c_s"/> + <Ligature components="t" glyph="c_t"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + <Lookup index="3"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="c"> + <Ligature components="h" glyph="c_h"/> + <Ligature components="k" glyph="c_k"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/spec4h2.fea b/Tests/feaLib/data/spec4h2.fea new file mode 100644 index 0000000..0384804 --- /dev/null +++ b/Tests/feaLib/data/spec4h2.fea @@ -0,0 +1,40 @@ +# OpenType Feature File specification, section 4.h, example 2. +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html + +languagesystem DFLT dflt; +languagesystem latn dflt; +languagesystem latn DEU; +languagesystem cyrl dflt; +languagesystem cyrl SRB; +languagesystem grek dflt; + +feature liga { + # start of default rules that are applied under all language systems. + lookup HAS_I { + sub f f i by f_f_i; + sub f i by f_i; + } HAS_I; + + lookup NO_I { + sub f f l by f_f_l; + sub f f by f_f; + } NO_I; + + # end of default rules that are applied under all language systems. + + script latn; + language dflt; + # default lookup for latn included under all languages for the latn script + sub f l by f_l; + + language DEU; + # default lookups included under the DEU language + sub s s by germandbls; # This is also included. + + language TRK exclude_dflt; # default lookups are excluded. + lookup NO_I; # Only this lookup is included under the TRK language + + script cyrl; + language SRB; + sub c t by c_t; # this rule will apply only under script cyrl language SRB. +} liga; diff --git a/Tests/feaLib/data/spec4h2.ttx b/Tests/feaLib/data/spec4h2.ttx new file mode 100644 index 0000000..266c487 --- /dev/null +++ b/Tests/feaLib/data/spec4h2.ttx @@ -0,0 +1,180 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.0"> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=4 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="3"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + <ScriptRecord index="1"> + <ScriptTag value="cyrl"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="3"/> + </DefaultLangSys> + <!-- LangSysCount=1 --> + <LangSysRecord index="0"> + <LangSysTag value="SRB "/> + <LangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="1"/> + </LangSys> + </LangSysRecord> + </Script> + </ScriptRecord> + <ScriptRecord index="2"> + <ScriptTag value="grek"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="3"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + <ScriptRecord index="3"> + <ScriptTag value="latn"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="4"/> + </DefaultLangSys> + <!-- LangSysCount=2 --> + <LangSysRecord index="0"> + <LangSysTag value="DEU "/> + <LangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </LangSys> + </LangSysRecord> + <LangSysRecord index="1"> + <LangSysTag value="TRK "/> + <LangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="2"/> + </LangSys> + </LangSysRecord> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=5 --> + <FeatureRecord index="0"> + <FeatureTag value="liga"/> + <Feature> + <!-- LookupCount=4 --> + <LookupListIndex index="0" value="0"/> + <LookupListIndex index="1" value="1"/> + <LookupListIndex index="2" value="2"/> + <LookupListIndex index="3" value="3"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="1"> + <FeatureTag value="liga"/> + <Feature> + <!-- LookupCount=3 --> + <LookupListIndex index="0" value="0"/> + <LookupListIndex index="1" value="1"/> + <LookupListIndex index="2" value="4"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="2"> + <FeatureTag value="liga"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="1"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="3"> + <FeatureTag value="liga"/> + <Feature> + <!-- LookupCount=2 --> + <LookupListIndex index="0" value="0"/> + <LookupListIndex index="1" value="1"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="4"> + <FeatureTag value="liga"/> + <Feature> + <!-- LookupCount=3 --> + <LookupListIndex index="0" value="0"/> + <LookupListIndex index="1" value="1"/> + <LookupListIndex index="2" value="2"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=5 --> + <Lookup index="0"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="f"> + <Ligature components="f,i" glyph="f_f_i"/> + <Ligature components="i" glyph="f_i"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="f"> + <Ligature components="f,l" glyph="f_f_l"/> + <Ligature components="f" glyph="f_f"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + <Lookup index="2"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="f"> + <Ligature components="l" glyph="f_l"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + <Lookup index="3"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="s"> + <Ligature components="s" glyph="germandbls"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + <Lookup index="4"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="c"> + <Ligature components="t" glyph="c_t"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/spec5d1.fea b/Tests/feaLib/data/spec5d1.fea new file mode 100644 index 0000000..cf7d76f --- /dev/null +++ b/Tests/feaLib/data/spec5d1.fea @@ -0,0 +1,23 @@ +# OpenType Feature File specification, section 5.d, example 1. +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html + +feature F1 { + sub [one one.oldstyle] [slash fraction] [two two.oldstyle] by onehalf; +} F1; + +# Since the OpenType specification does not allow ligature substitutions +# to be specified on target sequences that contain glyph classes, the +# implementation software will enumerate all specific glyph sequences +# if glyph classes are detected in <glyph sequence>. Thus, the above +# example produces an identical representation in the font as if all +# the sequences were manually enumerated by the font editor: +feature F2 { + sub one slash two by onehalf; + sub one.oldstyle slash two by onehalf; + sub one fraction two by onehalf; + sub one.oldstyle fraction two by onehalf; + sub one slash two.oldstyle by onehalf; + sub one.oldstyle slash two.oldstyle by onehalf; + sub one fraction two.oldstyle by onehalf; + sub one.oldstyle fraction two.oldstyle by onehalf; +} F2; diff --git a/Tests/feaLib/data/spec5d1.ttx b/Tests/feaLib/data/spec5d1.ttx new file mode 100644 index 0000000..77dfc93 --- /dev/null +++ b/Tests/feaLib/data/spec5d1.ttx @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=2 --> + <FeatureIndex index="0" value="0"/> + <FeatureIndex index="1" value="1"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=2 --> + <FeatureRecord index="0"> + <FeatureTag value="F1 "/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="1"> + <FeatureTag value="F2 "/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="1"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="one"> + <Ligature components="fraction,two" glyph="onehalf"/> + <Ligature components="fraction,two.oldstyle" glyph="onehalf"/> + <Ligature components="slash,two" glyph="onehalf"/> + <Ligature components="slash,two.oldstyle" glyph="onehalf"/> + </LigatureSet> + <LigatureSet glyph="one.oldstyle"> + <Ligature components="fraction,two" glyph="onehalf"/> + <Ligature components="fraction,two.oldstyle" glyph="onehalf"/> + <Ligature components="slash,two" glyph="onehalf"/> + <Ligature components="slash,two.oldstyle" glyph="onehalf"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="one"> + <Ligature components="fraction,two" glyph="onehalf"/> + <Ligature components="fraction,two.oldstyle" glyph="onehalf"/> + <Ligature components="slash,two" glyph="onehalf"/> + <Ligature components="slash,two.oldstyle" glyph="onehalf"/> + </LigatureSet> + <LigatureSet glyph="one.oldstyle"> + <Ligature components="fraction,two" glyph="onehalf"/> + <Ligature components="fraction,two.oldstyle" glyph="onehalf"/> + <Ligature components="slash,two" glyph="onehalf"/> + <Ligature components="slash,two.oldstyle" glyph="onehalf"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/spec5d2.fea b/Tests/feaLib/data/spec5d2.fea new file mode 100644 index 0000000..7fd7eab --- /dev/null +++ b/Tests/feaLib/data/spec5d2.fea @@ -0,0 +1,22 @@ +# OpenType Feature File specification, section 5.d, example 2. +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html + +# A contiguous set of ligature rules does not need to be ordered in +# any particular way by the font editor; the implementation software +# must do the appropriate sorting. + +# So: +feature F1 { + sub f f by f_f; + sub f i by f_i; + sub f f i by f_f_i; + sub o f f i by o_f_f_i; +} F1; + +# will produce an identical representation in the font as: +feature F2 { + sub o f f i by o_f_f_i; + sub f f i by f_f_i; + sub f f by f_f; + sub f i by f_i; +} F2; diff --git a/Tests/feaLib/data/spec5d2.ttx b/Tests/feaLib/data/spec5d2.ttx new file mode 100644 index 0000000..7bcd4da --- /dev/null +++ b/Tests/feaLib/data/spec5d2.ttx @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=2 --> + <FeatureIndex index="0" value="0"/> + <FeatureIndex index="1" value="1"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=2 --> + <FeatureRecord index="0"> + <FeatureTag value="F1 "/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="1"> + <FeatureTag value="F2 "/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="1"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="f"> + <Ligature components="f,i" glyph="f_f_i"/> + <Ligature components="f" glyph="f_f"/> + <Ligature components="i" glyph="f_i"/> + </LigatureSet> + <LigatureSet glyph="o"> + <Ligature components="f,f,i" glyph="o_f_f_i"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="f"> + <Ligature components="f,i" glyph="f_f_i"/> + <Ligature components="f" glyph="f_f"/> + <Ligature components="i" glyph="f_i"/> + </LigatureSet> + <LigatureSet glyph="o"> + <Ligature components="f,f,i" glyph="o_f_f_i"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/spec5f_ii_1.fea b/Tests/feaLib/data/spec5f_ii_1.fea new file mode 100644 index 0000000..2d5c0df --- /dev/null +++ b/Tests/feaLib/data/spec5f_ii_1.fea @@ -0,0 +1,9 @@ +# OpenType Feature File specification, section 5.f.ii, example 1 +# "Specifying exceptions to the Chain Sub rule" +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html + +feature test { + ignore sub f [a e] d'; + ignore sub a d' d; + sub [a e n] d' by d.alt; +} test; diff --git a/Tests/feaLib/data/spec5f_ii_1.ttx b/Tests/feaLib/data/spec5f_ii_1.ttx new file mode 100644 index 0000000..8f7f94b --- /dev/null +++ b/Tests/feaLib/data/spec5f_ii_1.ttx @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="6"/> + <LookupFlag value="0"/> + <!-- SubTableCount=3 --> + <ChainContextSubst index="0" Format="3"> + <!-- BacktrackGlyphCount=2 --> + <BacktrackCoverage index="0"> + <Glyph value="a"/> + <Glyph value="e"/> + </BacktrackCoverage> + <BacktrackCoverage index="1"> + <Glyph value="f"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="d"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=0 --> + </ChainContextSubst> + <ChainContextSubst index="1" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="a"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="d"/> + </InputCoverage> + <!-- LookAheadGlyphCount=1 --> + <LookAheadCoverage index="0"> + <Glyph value="d"/> + </LookAheadCoverage> + <!-- SubstCount=0 --> + </ChainContextSubst> + <ChainContextSubst index="2" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="a"/> + <Glyph value="e"/> + <Glyph value="n"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="d"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </SubstLookupRecord> + </ChainContextSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="d" out="d.alt"/> + </SingleSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/spec5f_ii_2.fea b/Tests/feaLib/data/spec5f_ii_2.fea new file mode 100644 index 0000000..b20a74c --- /dev/null +++ b/Tests/feaLib/data/spec5f_ii_2.fea @@ -0,0 +1,9 @@ +# OpenType Feature File specification, section 5.f.ii, example 2 +# "Specifying exceptions to the Chain Sub rule" +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html + +feature test { + @LETTER = [a-z]; + ignore sub @LETTER f' i'; + sub f' i' by f_i.begin; +} test; diff --git a/Tests/feaLib/data/spec5f_ii_2.ttx b/Tests/feaLib/data/spec5f_ii_2.ttx new file mode 100644 index 0000000..0a1511d --- /dev/null +++ b/Tests/feaLib/data/spec5f_ii_2.ttx @@ -0,0 +1,106 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="6"/> + <LookupFlag value="0"/> + <!-- SubTableCount=2 --> + <ChainContextSubst index="0" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="a"/> + <Glyph value="b"/> + <Glyph value="c"/> + <Glyph value="d"/> + <Glyph value="e"/> + <Glyph value="f"/> + <Glyph value="g"/> + <Glyph value="h"/> + <Glyph value="i"/> + <Glyph value="j"/> + <Glyph value="k"/> + <Glyph value="l"/> + <Glyph value="m"/> + <Glyph value="n"/> + <Glyph value="o"/> + <Glyph value="p"/> + <Glyph value="q"/> + <Glyph value="r"/> + <Glyph value="s"/> + <Glyph value="t"/> + <Glyph value="u"/> + <Glyph value="v"/> + <Glyph value="w"/> + <Glyph value="x"/> + <Glyph value="y"/> + <Glyph value="z"/> + </BacktrackCoverage> + <!-- InputGlyphCount=2 --> + <InputCoverage index="0"> + <Glyph value="f"/> + </InputCoverage> + <InputCoverage index="1"> + <Glyph value="i"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=0 --> + </ChainContextSubst> + <ChainContextSubst index="1" Format="3"> + <!-- BacktrackGlyphCount=0 --> + <!-- InputGlyphCount=2 --> + <InputCoverage index="0"> + <Glyph value="f"/> + </InputCoverage> + <InputCoverage index="1"> + <Glyph value="i"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </SubstLookupRecord> + </ChainContextSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="f"> + <Ligature components="i" glyph="f_i.begin"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/spec5f_ii_3.fea b/Tests/feaLib/data/spec5f_ii_3.fea new file mode 100644 index 0000000..5fd1991 --- /dev/null +++ b/Tests/feaLib/data/spec5f_ii_3.fea @@ -0,0 +1,9 @@ +# OpenType Feature File specification, section 5.f.ii, example 3 +# "Specifying exceptions to the Chain Sub rule" +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html + +feature test { + @LETTER = [a-z]; + ignore sub @LETTER a' n' d', a' n' d' @LETTER; + sub a' n' d' by a_n_d; +} test; diff --git a/Tests/feaLib/data/spec5f_ii_3.ttx b/Tests/feaLib/data/spec5f_ii_3.ttx new file mode 100644 index 0000000..a550f0b --- /dev/null +++ b/Tests/feaLib/data/spec5f_ii_3.ttx @@ -0,0 +1,155 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="6"/> + <LookupFlag value="0"/> + <!-- SubTableCount=3 --> + <ChainContextSubst index="0" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="a"/> + <Glyph value="b"/> + <Glyph value="c"/> + <Glyph value="d"/> + <Glyph value="e"/> + <Glyph value="f"/> + <Glyph value="g"/> + <Glyph value="h"/> + <Glyph value="i"/> + <Glyph value="j"/> + <Glyph value="k"/> + <Glyph value="l"/> + <Glyph value="m"/> + <Glyph value="n"/> + <Glyph value="o"/> + <Glyph value="p"/> + <Glyph value="q"/> + <Glyph value="r"/> + <Glyph value="s"/> + <Glyph value="t"/> + <Glyph value="u"/> + <Glyph value="v"/> + <Glyph value="w"/> + <Glyph value="x"/> + <Glyph value="y"/> + <Glyph value="z"/> + </BacktrackCoverage> + <!-- InputGlyphCount=3 --> + <InputCoverage index="0"> + <Glyph value="a"/> + </InputCoverage> + <InputCoverage index="1"> + <Glyph value="n"/> + </InputCoverage> + <InputCoverage index="2"> + <Glyph value="d"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=0 --> + </ChainContextSubst> + <ChainContextSubst index="1" Format="3"> + <!-- BacktrackGlyphCount=0 --> + <!-- InputGlyphCount=3 --> + <InputCoverage index="0"> + <Glyph value="a"/> + </InputCoverage> + <InputCoverage index="1"> + <Glyph value="n"/> + </InputCoverage> + <InputCoverage index="2"> + <Glyph value="d"/> + </InputCoverage> + <!-- LookAheadGlyphCount=1 --> + <LookAheadCoverage index="0"> + <Glyph value="a"/> + <Glyph value="b"/> + <Glyph value="c"/> + <Glyph value="d"/> + <Glyph value="e"/> + <Glyph value="f"/> + <Glyph value="g"/> + <Glyph value="h"/> + <Glyph value="i"/> + <Glyph value="j"/> + <Glyph value="k"/> + <Glyph value="l"/> + <Glyph value="m"/> + <Glyph value="n"/> + <Glyph value="o"/> + <Glyph value="p"/> + <Glyph value="q"/> + <Glyph value="r"/> + <Glyph value="s"/> + <Glyph value="t"/> + <Glyph value="u"/> + <Glyph value="v"/> + <Glyph value="w"/> + <Glyph value="x"/> + <Glyph value="y"/> + <Glyph value="z"/> + </LookAheadCoverage> + <!-- SubstCount=0 --> + </ChainContextSubst> + <ChainContextSubst index="2" Format="3"> + <!-- BacktrackGlyphCount=0 --> + <!-- InputGlyphCount=3 --> + <InputCoverage index="0"> + <Glyph value="a"/> + </InputCoverage> + <InputCoverage index="1"> + <Glyph value="n"/> + </InputCoverage> + <InputCoverage index="2"> + <Glyph value="d"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </SubstLookupRecord> + </ChainContextSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="a"> + <Ligature components="n,d" glyph="a_n_d"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/spec5f_ii_4.fea b/Tests/feaLib/data/spec5f_ii_4.fea new file mode 100644 index 0000000..731a1f6 --- /dev/null +++ b/Tests/feaLib/data/spec5f_ii_4.fea @@ -0,0 +1,23 @@ +# OpenType Feature File specification, section 5.f.ii, example 4 +# "Specifying exceptions to the Chain Sub rule" +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html + +@LETTER = [A-Z a-z]; + +feature cswh { + + # --- Glyph classes used in this feature: + @BEGINNINGS = [A-N P-Z T_h m]; + @BEGINNINGS_SWASH = [A.swash-N.swash P.swash-Z.swash T_h.swash m.begin]; + @ENDINGS = [a e z]; + @ENDINGS_SWASH = [a.end e.end z.end]; + + # --- Beginning-of-word swashes: + ignore sub @LETTER @BEGINNINGS'; + sub @BEGINNINGS' by @BEGINNINGS_SWASH; + + # --- End-of-word swashes: + ignore sub @ENDINGS' @LETTER; + sub @ENDINGS' by @ENDINGS_SWASH; + +} cswh; diff --git a/Tests/feaLib/data/spec5f_ii_4.ttx b/Tests/feaLib/data/spec5f_ii_4.ttx new file mode 100644 index 0000000..f358161 --- /dev/null +++ b/Tests/feaLib/data/spec5f_ii_4.ttx @@ -0,0 +1,285 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="cswh"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="6"/> + <LookupFlag value="0"/> + <!-- SubTableCount=4 --> + <ChainContextSubst index="0" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="A"/> + <Glyph value="B"/> + <Glyph value="C"/> + <Glyph value="D"/> + <Glyph value="E"/> + <Glyph value="F"/> + <Glyph value="G"/> + <Glyph value="H"/> + <Glyph value="I"/> + <Glyph value="J"/> + <Glyph value="K"/> + <Glyph value="L"/> + <Glyph value="M"/> + <Glyph value="N"/> + <Glyph value="O"/> + <Glyph value="P"/> + <Glyph value="Q"/> + <Glyph value="R"/> + <Glyph value="S"/> + <Glyph value="T"/> + <Glyph value="U"/> + <Glyph value="V"/> + <Glyph value="W"/> + <Glyph value="X"/> + <Glyph value="Y"/> + <Glyph value="Z"/> + <Glyph value="a"/> + <Glyph value="b"/> + <Glyph value="c"/> + <Glyph value="d"/> + <Glyph value="e"/> + <Glyph value="f"/> + <Glyph value="g"/> + <Glyph value="h"/> + <Glyph value="i"/> + <Glyph value="j"/> + <Glyph value="k"/> + <Glyph value="l"/> + <Glyph value="m"/> + <Glyph value="n"/> + <Glyph value="o"/> + <Glyph value="p"/> + <Glyph value="q"/> + <Glyph value="r"/> + <Glyph value="s"/> + <Glyph value="t"/> + <Glyph value="u"/> + <Glyph value="v"/> + <Glyph value="w"/> + <Glyph value="x"/> + <Glyph value="y"/> + <Glyph value="z"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="A"/> + <Glyph value="B"/> + <Glyph value="C"/> + <Glyph value="D"/> + <Glyph value="E"/> + <Glyph value="F"/> + <Glyph value="G"/> + <Glyph value="H"/> + <Glyph value="I"/> + <Glyph value="J"/> + <Glyph value="K"/> + <Glyph value="L"/> + <Glyph value="M"/> + <Glyph value="N"/> + <Glyph value="P"/> + <Glyph value="Q"/> + <Glyph value="R"/> + <Glyph value="S"/> + <Glyph value="T"/> + <Glyph value="U"/> + <Glyph value="V"/> + <Glyph value="W"/> + <Glyph value="X"/> + <Glyph value="Y"/> + <Glyph value="Z"/> + <Glyph value="m"/> + <Glyph value="T_h"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=0 --> + </ChainContextSubst> + <ChainContextSubst index="1" Format="3"> + <!-- BacktrackGlyphCount=0 --> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="A"/> + <Glyph value="B"/> + <Glyph value="C"/> + <Glyph value="D"/> + <Glyph value="E"/> + <Glyph value="F"/> + <Glyph value="G"/> + <Glyph value="H"/> + <Glyph value="I"/> + <Glyph value="J"/> + <Glyph value="K"/> + <Glyph value="L"/> + <Glyph value="M"/> + <Glyph value="N"/> + <Glyph value="P"/> + <Glyph value="Q"/> + <Glyph value="R"/> + <Glyph value="S"/> + <Glyph value="T"/> + <Glyph value="U"/> + <Glyph value="V"/> + <Glyph value="W"/> + <Glyph value="X"/> + <Glyph value="Y"/> + <Glyph value="Z"/> + <Glyph value="m"/> + <Glyph value="T_h"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </SubstLookupRecord> + </ChainContextSubst> + <ChainContextSubst index="2" Format="3"> + <!-- BacktrackGlyphCount=0 --> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="a"/> + <Glyph value="e"/> + <Glyph value="z"/> + </InputCoverage> + <!-- LookAheadGlyphCount=1 --> + <LookAheadCoverage index="0"> + <Glyph value="A"/> + <Glyph value="B"/> + <Glyph value="C"/> + <Glyph value="D"/> + <Glyph value="E"/> + <Glyph value="F"/> + <Glyph value="G"/> + <Glyph value="H"/> + <Glyph value="I"/> + <Glyph value="J"/> + <Glyph value="K"/> + <Glyph value="L"/> + <Glyph value="M"/> + <Glyph value="N"/> + <Glyph value="O"/> + <Glyph value="P"/> + <Glyph value="Q"/> + <Glyph value="R"/> + <Glyph value="S"/> + <Glyph value="T"/> + <Glyph value="U"/> + <Glyph value="V"/> + <Glyph value="W"/> + <Glyph value="X"/> + <Glyph value="Y"/> + <Glyph value="Z"/> + <Glyph value="a"/> + <Glyph value="b"/> + <Glyph value="c"/> + <Glyph value="d"/> + <Glyph value="e"/> + <Glyph value="f"/> + <Glyph value="g"/> + <Glyph value="h"/> + <Glyph value="i"/> + <Glyph value="j"/> + <Glyph value="k"/> + <Glyph value="l"/> + <Glyph value="m"/> + <Glyph value="n"/> + <Glyph value="o"/> + <Glyph value="p"/> + <Glyph value="q"/> + <Glyph value="r"/> + <Glyph value="s"/> + <Glyph value="t"/> + <Glyph value="u"/> + <Glyph value="v"/> + <Glyph value="w"/> + <Glyph value="x"/> + <Glyph value="y"/> + <Glyph value="z"/> + </LookAheadCoverage> + <!-- SubstCount=0 --> + </ChainContextSubst> + <ChainContextSubst index="3" Format="3"> + <!-- BacktrackGlyphCount=0 --> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="a"/> + <Glyph value="e"/> + <Glyph value="z"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </SubstLookupRecord> + </ChainContextSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="A" out="A.swash"/> + <Substitution in="B" out="B.swash"/> + <Substitution in="C" out="C.swash"/> + <Substitution in="D" out="D.swash"/> + <Substitution in="E" out="E.swash"/> + <Substitution in="F" out="F.swash"/> + <Substitution in="G" out="G.swash"/> + <Substitution in="H" out="H.swash"/> + <Substitution in="I" out="I.swash"/> + <Substitution in="J" out="J.swash"/> + <Substitution in="K" out="K.swash"/> + <Substitution in="L" out="L.swash"/> + <Substitution in="M" out="M.swash"/> + <Substitution in="N" out="N.swash"/> + <Substitution in="P" out="P.swash"/> + <Substitution in="Q" out="Q.swash"/> + <Substitution in="R" out="R.swash"/> + <Substitution in="S" out="S.swash"/> + <Substitution in="T" out="T.swash"/> + <Substitution in="T_h" out="T_h.swash"/> + <Substitution in="U" out="U.swash"/> + <Substitution in="V" out="V.swash"/> + <Substitution in="W" out="W.swash"/> + <Substitution in="X" out="X.swash"/> + <Substitution in="Y" out="Y.swash"/> + <Substitution in="Z" out="Z.swash"/> + <Substitution in="a" out="a.end"/> + <Substitution in="e" out="e.end"/> + <Substitution in="m" out="m.begin"/> + <Substitution in="z" out="z.end"/> + </SingleSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/spec5fi1.fea b/Tests/feaLib/data/spec5fi1.fea new file mode 100644 index 0000000..f5de2dc --- /dev/null +++ b/Tests/feaLib/data/spec5fi1.fea @@ -0,0 +1,20 @@ +# OpenType Feature File specification, section 5.f.i, example 1 +# "Specifying a Chain Sub rule and marking sub-runs" +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html + +languagesystem latn dflt; + +lookup CNTXT_LIGS { + sub f i by f_i; + sub c t by c_t; +} CNTXT_LIGS; + +lookup CNTXT_SUB { + sub n by n.end; + sub s by s.end; +} CNTXT_SUB; + +feature test { + sub [a e i o u] f' lookup CNTXT_LIGS i' n' lookup CNTXT_SUB; + sub [a e i o u] c' lookup CNTXT_LIGS t' s' lookup CNTXT_SUB; +} test; diff --git a/Tests/feaLib/data/spec5fi1.ttx b/Tests/feaLib/data/spec5fi1.ttx new file mode 100644 index 0000000..356ddc7 --- /dev/null +++ b/Tests/feaLib/data/spec5fi1.ttx @@ -0,0 +1,122 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="latn"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="2"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=3 --> + <Lookup index="0"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="c"> + <Ligature components="t" glyph="c_t"/> + </LigatureSet> + <LigatureSet glyph="f"> + <Ligature components="i" glyph="f_i"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="n" out="n.end"/> + <Substitution in="s" out="s.end"/> + </SingleSubst> + </Lookup> + <Lookup index="2"> + <LookupType value="6"/> + <LookupFlag value="0"/> + <!-- SubTableCount=2 --> + <ChainContextSubst index="0" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="a"/> + <Glyph value="e"/> + <Glyph value="i"/> + <Glyph value="o"/> + <Glyph value="u"/> + </BacktrackCoverage> + <!-- InputGlyphCount=3 --> + <InputCoverage index="0"> + <Glyph value="f"/> + </InputCoverage> + <InputCoverage index="1"> + <Glyph value="i"/> + </InputCoverage> + <InputCoverage index="2"> + <Glyph value="n"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=2 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="0"/> + </SubstLookupRecord> + <SubstLookupRecord index="1"> + <SequenceIndex value="2"/> + <LookupListIndex value="1"/> + </SubstLookupRecord> + </ChainContextSubst> + <ChainContextSubst index="1" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="a"/> + <Glyph value="e"/> + <Glyph value="i"/> + <Glyph value="o"/> + <Glyph value="u"/> + </BacktrackCoverage> + <!-- InputGlyphCount=3 --> + <InputCoverage index="0"> + <Glyph value="c"/> + </InputCoverage> + <InputCoverage index="1"> + <Glyph value="t"/> + </InputCoverage> + <InputCoverage index="2"> + <Glyph value="s"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=2 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="0"/> + </SubstLookupRecord> + <SubstLookupRecord index="1"> + <SequenceIndex value="2"/> + <LookupListIndex value="1"/> + </SubstLookupRecord> + </ChainContextSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/spec5fi2.fea b/Tests/feaLib/data/spec5fi2.fea new file mode 100644 index 0000000..87dbf2d --- /dev/null +++ b/Tests/feaLib/data/spec5fi2.fea @@ -0,0 +1,11 @@ +# OpenType Feature File specification, section 5.f.i, example 2 +# "Specifying a Chain Sub rule and marking sub-runs" +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html + +languagesystem latn dflt; + +feature test { +#test-fea2fea: lookupflag RightToLeft IgnoreBaseGlyphs IgnoreLigatures; + lookupflag 7; + sub [a e n] d' by d.alt; +} test; diff --git a/Tests/feaLib/data/spec5fi2.ttx b/Tests/feaLib/data/spec5fi2.ttx new file mode 100644 index 0000000..0842cf9 --- /dev/null +++ b/Tests/feaLib/data/spec5fi2.ttx @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="latn"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="6"/> + <LookupFlag value="7"/> + <!-- SubTableCount=1 --> + <ChainContextSubst index="0" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="a"/> + <Glyph value="e"/> + <Glyph value="n"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="d"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </SubstLookupRecord> + </ChainContextSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="1"/> + <LookupFlag value="7"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="d" out="d.alt"/> + </SingleSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/spec5fi3.fea b/Tests/feaLib/data/spec5fi3.fea new file mode 100644 index 0000000..e47a6f8 --- /dev/null +++ b/Tests/feaLib/data/spec5fi3.fea @@ -0,0 +1,9 @@ +# OpenType Feature File specification, section 5.f.i, example 3 +# "Specifying a Chain Sub rule and marking sub-runs" +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html + +languagesystem latn dflt; + +feature test { + sub [A-Z] [A.sc-Z.sc]' by [a-z]; +} test; diff --git a/Tests/feaLib/data/spec5fi3.ttx b/Tests/feaLib/data/spec5fi3.ttx new file mode 100644 index 0000000..1b3cd26 --- /dev/null +++ b/Tests/feaLib/data/spec5fi3.ttx @@ -0,0 +1,139 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="latn"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="6"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <ChainContextSubst index="0" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="A"/> + <Glyph value="B"/> + <Glyph value="C"/> + <Glyph value="D"/> + <Glyph value="E"/> + <Glyph value="F"/> + <Glyph value="G"/> + <Glyph value="H"/> + <Glyph value="I"/> + <Glyph value="J"/> + <Glyph value="K"/> + <Glyph value="L"/> + <Glyph value="M"/> + <Glyph value="N"/> + <Glyph value="O"/> + <Glyph value="P"/> + <Glyph value="Q"/> + <Glyph value="R"/> + <Glyph value="S"/> + <Glyph value="T"/> + <Glyph value="U"/> + <Glyph value="V"/> + <Glyph value="W"/> + <Glyph value="X"/> + <Glyph value="Y"/> + <Glyph value="Z"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="A.sc"/> + <Glyph value="B.sc"/> + <Glyph value="C.sc"/> + <Glyph value="D.sc"/> + <Glyph value="E.sc"/> + <Glyph value="F.sc"/> + <Glyph value="G.sc"/> + <Glyph value="H.sc"/> + <Glyph value="I.sc"/> + <Glyph value="J.sc"/> + <Glyph value="K.sc"/> + <Glyph value="L.sc"/> + <Glyph value="M.sc"/> + <Glyph value="N.sc"/> + <Glyph value="O.sc"/> + <Glyph value="P.sc"/> + <Glyph value="Q.sc"/> + <Glyph value="R.sc"/> + <Glyph value="S.sc"/> + <Glyph value="T.sc"/> + <Glyph value="U.sc"/> + <Glyph value="V.sc"/> + <Glyph value="W.sc"/> + <Glyph value="X.sc"/> + <Glyph value="Y.sc"/> + <Glyph value="Z.sc"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </SubstLookupRecord> + </ChainContextSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="A.sc" out="a"/> + <Substitution in="B.sc" out="b"/> + <Substitution in="C.sc" out="c"/> + <Substitution in="D.sc" out="d"/> + <Substitution in="E.sc" out="e"/> + <Substitution in="F.sc" out="f"/> + <Substitution in="G.sc" out="g"/> + <Substitution in="H.sc" out="h"/> + <Substitution in="I.sc" out="i"/> + <Substitution in="J.sc" out="j"/> + <Substitution in="K.sc" out="k"/> + <Substitution in="L.sc" out="l"/> + <Substitution in="M.sc" out="m"/> + <Substitution in="N.sc" out="n"/> + <Substitution in="O.sc" out="o"/> + <Substitution in="P.sc" out="p"/> + <Substitution in="Q.sc" out="q"/> + <Substitution in="R.sc" out="r"/> + <Substitution in="S.sc" out="s"/> + <Substitution in="T.sc" out="t"/> + <Substitution in="U.sc" out="u"/> + <Substitution in="V.sc" out="v"/> + <Substitution in="W.sc" out="w"/> + <Substitution in="X.sc" out="x"/> + <Substitution in="Y.sc" out="y"/> + <Substitution in="Z.sc" out="z"/> + </SingleSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/spec5fi4.fea b/Tests/feaLib/data/spec5fi4.fea new file mode 100644 index 0000000..845c560 --- /dev/null +++ b/Tests/feaLib/data/spec5fi4.fea @@ -0,0 +1,9 @@ +# OpenType Feature File specification, section 5.f.i, example 4 +# "Specifying a Chain Sub rule and marking sub-runs" +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html + +languagesystem latn dflt; + +feature test { + sub [e e.begin]' t' c by ampersand; +} test; diff --git a/Tests/feaLib/data/spec5fi4.ttx b/Tests/feaLib/data/spec5fi4.ttx new file mode 100644 index 0000000..a0701db --- /dev/null +++ b/Tests/feaLib/data/spec5fi4.ttx @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="latn"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="6"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <ChainContextSubst index="0" Format="3"> + <!-- BacktrackGlyphCount=0 --> + <!-- InputGlyphCount=2 --> + <InputCoverage index="0"> + <Glyph value="e"/> + <Glyph value="e.begin"/> + </InputCoverage> + <InputCoverage index="1"> + <Glyph value="t"/> + </InputCoverage> + <!-- LookAheadGlyphCount=1 --> + <LookAheadCoverage index="0"> + <Glyph value="c"/> + </LookAheadCoverage> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </SubstLookupRecord> + </ChainContextSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="e"> + <Ligature components="t" glyph="ampersand"/> + </LigatureSet> + <LigatureSet glyph="e.begin"> + <Ligature components="t" glyph="ampersand"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/spec5h1.fea b/Tests/feaLib/data/spec5h1.fea new file mode 100644 index 0000000..8d2f516 --- /dev/null +++ b/Tests/feaLib/data/spec5h1.fea @@ -0,0 +1,9 @@ +# OpenType Feature File specification, section 5.h, example 1. +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html + +languagesystem DFLT dflt; + +feature test { +#test-fea2fea: rsub [a e n] d' by d.alt; + reversesub [a e n] d' by d.alt; +} test; diff --git a/Tests/feaLib/data/spec5h1.ttx b/Tests/feaLib/data/spec5h1.ttx new file mode 100644 index 0000000..20278f5 --- /dev/null +++ b/Tests/feaLib/data/spec5h1.ttx @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="8"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <ReverseChainSingleSubst index="0" Format="1"> + <Coverage> + <Glyph value="d"/> + </Coverage> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="a"/> + <Glyph value="e"/> + <Glyph value="n"/> + </BacktrackCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- GlyphCount=1 --> + <Substitute index="0" value="d.alt"/> + </ReverseChainSingleSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/spec6b_ii.fea b/Tests/feaLib/data/spec6b_ii.fea new file mode 100644 index 0000000..59fc7d5 --- /dev/null +++ b/Tests/feaLib/data/spec6b_ii.fea @@ -0,0 +1,12 @@ +# OpenType Feature File specification, section 6.b.ii: +# [GPOS LookupType 2] Enumerating pairs +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html + +@Y_LC = [y yacute ydieresis]; +@SMALL_PUNC = [comma semicolon period]; + +feature kern { + enum pos @Y_LC semicolon -80; # specific pairs + pos f quoteright 30; # specific pair + pos @Y_LC @SMALL_PUNC -100; # class pair +} kern; diff --git a/Tests/feaLib/data/spec6b_ii.ttx b/Tests/feaLib/data/spec6b_ii.ttx new file mode 100644 index 0000000..a7131de --- /dev/null +++ b/Tests/feaLib/data/spec6b_ii.ttx @@ -0,0 +1,104 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="kern"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="2"/> + <LookupFlag value="0"/> + <!-- SubTableCount=2 --> + <PairPos index="0" Format="1"> + <Coverage> + <Glyph value="f"/> + <Glyph value="y"/> + <Glyph value="ydieresis"/> + <Glyph value="yacute"/> + </Coverage> + <ValueFormat1 value="4"/> + <ValueFormat2 value="0"/> + <!-- PairSetCount=4 --> + <PairSet index="0"> + <!-- PairValueCount=1 --> + <PairValueRecord index="0"> + <SecondGlyph value="quoteright"/> + <Value1 XAdvance="30"/> + </PairValueRecord> + </PairSet> + <PairSet index="1"> + <!-- PairValueCount=1 --> + <PairValueRecord index="0"> + <SecondGlyph value="semicolon"/> + <Value1 XAdvance="-80"/> + </PairValueRecord> + </PairSet> + <PairSet index="2"> + <!-- PairValueCount=1 --> + <PairValueRecord index="0"> + <SecondGlyph value="semicolon"/> + <Value1 XAdvance="-80"/> + </PairValueRecord> + </PairSet> + <PairSet index="3"> + <!-- PairValueCount=1 --> + <PairValueRecord index="0"> + <SecondGlyph value="semicolon"/> + <Value1 XAdvance="-80"/> + </PairValueRecord> + </PairSet> + </PairPos> + <PairPos index="1" Format="2"> + <Coverage> + <Glyph value="y"/> + <Glyph value="ydieresis"/> + <Glyph value="yacute"/> + </Coverage> + <ValueFormat1 value="4"/> + <ValueFormat2 value="0"/> + <ClassDef1> + </ClassDef1> + <ClassDef2> + <ClassDef glyph="comma" class="1"/> + <ClassDef glyph="period" class="1"/> + <ClassDef glyph="semicolon" class="1"/> + </ClassDef2> + <!-- Class1Count=1 --> + <!-- Class2Count=2 --> + <Class1Record index="0"> + <Class2Record index="0"> + </Class2Record> + <Class2Record index="1"> + <Value1 XAdvance="-100"/> + </Class2Record> + </Class1Record> + </PairPos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/spec6d2.fea b/Tests/feaLib/data/spec6d2.fea new file mode 100644 index 0000000..ead224f --- /dev/null +++ b/Tests/feaLib/data/spec6d2.fea @@ -0,0 +1,15 @@ +# OpenType Feature File specification, section 6.d, example 1: +# [GPOS LookupType 4] Mark-to-Base attachment positioning +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html + +languagesystem DFLT dflt; + +markClass [acute grave] <anchor 150 -10> @TOP_MARKS; +markClass [dieresis umlaut] <anchor 300 -10> @TOP_MARKS; +markClass [cedilla] <anchor 300 600> @BOTTOM_MARKS; + +feature test { + pos base [e o] <anchor 250 450> mark @TOP_MARKS <anchor 250 -12> mark @BOTTOM_MARKS; +#test-fea2fea: pos base [a u] <anchor 265 450> mark @TOP_MARKS <anchor 250 -10> mark @BOTTOM_MARKS; + position base [a u] <anchor 265 450> mark @TOP_MARKS <anchor 250-10> mark @BOTTOM_MARKS; +} test; diff --git a/Tests/feaLib/data/spec6d2.ttx b/Tests/feaLib/data/spec6d2.ttx new file mode 100644 index 0000000..11a09f2 --- /dev/null +++ b/Tests/feaLib/data/spec6d2.ttx @@ -0,0 +1,152 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GDEF> + <Version value="0x00010000"/> + <GlyphClassDef> + <ClassDef glyph="a" class="1"/> + <ClassDef glyph="acute" class="3"/> + <ClassDef glyph="cedilla" class="3"/> + <ClassDef glyph="dieresis" class="3"/> + <ClassDef glyph="e" class="1"/> + <ClassDef glyph="grave" class="3"/> + <ClassDef glyph="o" class="1"/> + <ClassDef glyph="u" class="1"/> + <ClassDef glyph="umlaut" class="3"/> + </GlyphClassDef> + </GDEF> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <MarkBasePos index="0" Format="1"> + <MarkCoverage> + <Glyph value="grave"/> + <Glyph value="acute"/> + <Glyph value="dieresis"/> + <Glyph value="cedilla"/> + <Glyph value="umlaut"/> + </MarkCoverage> + <BaseCoverage> + <Glyph value="a"/> + <Glyph value="e"/> + <Glyph value="o"/> + <Glyph value="u"/> + </BaseCoverage> + <!-- ClassCount=2 --> + <MarkArray> + <!-- MarkCount=5 --> + <MarkRecord index="0"> + <Class value="0"/> + <MarkAnchor Format="1"> + <XCoordinate value="150"/> + <YCoordinate value="-10"/> + </MarkAnchor> + </MarkRecord> + <MarkRecord index="1"> + <Class value="0"/> + <MarkAnchor Format="1"> + <XCoordinate value="150"/> + <YCoordinate value="-10"/> + </MarkAnchor> + </MarkRecord> + <MarkRecord index="2"> + <Class value="0"/> + <MarkAnchor Format="1"> + <XCoordinate value="300"/> + <YCoordinate value="-10"/> + </MarkAnchor> + </MarkRecord> + <MarkRecord index="3"> + <Class value="1"/> + <MarkAnchor Format="1"> + <XCoordinate value="300"/> + <YCoordinate value="600"/> + </MarkAnchor> + </MarkRecord> + <MarkRecord index="4"> + <Class value="0"/> + <MarkAnchor Format="1"> + <XCoordinate value="300"/> + <YCoordinate value="-10"/> + </MarkAnchor> + </MarkRecord> + </MarkArray> + <BaseArray> + <!-- BaseCount=4 --> + <BaseRecord index="0"> + <BaseAnchor index="0" Format="1"> + <XCoordinate value="265"/> + <YCoordinate value="450"/> + </BaseAnchor> + <BaseAnchor index="1" Format="1"> + <XCoordinate value="250"/> + <YCoordinate value="-10"/> + </BaseAnchor> + </BaseRecord> + <BaseRecord index="1"> + <BaseAnchor index="0" Format="1"> + <XCoordinate value="250"/> + <YCoordinate value="450"/> + </BaseAnchor> + <BaseAnchor index="1" Format="1"> + <XCoordinate value="250"/> + <YCoordinate value="-12"/> + </BaseAnchor> + </BaseRecord> + <BaseRecord index="2"> + <BaseAnchor index="0" Format="1"> + <XCoordinate value="250"/> + <YCoordinate value="450"/> + </BaseAnchor> + <BaseAnchor index="1" Format="1"> + <XCoordinate value="250"/> + <YCoordinate value="-12"/> + </BaseAnchor> + </BaseRecord> + <BaseRecord index="3"> + <BaseAnchor index="0" Format="1"> + <XCoordinate value="265"/> + <YCoordinate value="450"/> + </BaseAnchor> + <BaseAnchor index="1" Format="1"> + <XCoordinate value="250"/> + <YCoordinate value="-10"/> + </BaseAnchor> + </BaseRecord> + </BaseArray> + </MarkBasePos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/spec6e.fea b/Tests/feaLib/data/spec6e.fea new file mode 100644 index 0000000..ed956c8 --- /dev/null +++ b/Tests/feaLib/data/spec6e.fea @@ -0,0 +1,10 @@ +languagesystem DFLT dflt; + +markClass sukun <anchor 261 488> @TOP_MARKS; +markClass kasratan <anchor 346 -98> @BOTTOM_MARKS; + +feature test { + pos ligature lam_meem_jeem <anchor 625 1800> mark @TOP_MARKS # mark above lam + ligComponent <anchor 376 -368> mark @BOTTOM_MARKS # mark below meem + ligComponent <anchor NULL>; # jeem has no marks +} test; diff --git a/Tests/feaLib/data/spec6e.ttx b/Tests/feaLib/data/spec6e.ttx new file mode 100644 index 0000000..9631910 --- /dev/null +++ b/Tests/feaLib/data/spec6e.ttx @@ -0,0 +1,100 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GDEF> + <Version value="0x00010000"/> + <GlyphClassDef> + <ClassDef glyph="kasratan" class="3"/> + <ClassDef glyph="lam_meem_jeem" class="2"/> + <ClassDef glyph="sukun" class="3"/> + </GlyphClassDef> + </GDEF> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="5"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <MarkLigPos index="0" Format="1"> + <MarkCoverage> + <Glyph value="sukun"/> + <Glyph value="kasratan"/> + </MarkCoverage> + <LigatureCoverage> + <Glyph value="lam_meem_jeem"/> + </LigatureCoverage> + <!-- ClassCount=2 --> + <MarkArray> + <!-- MarkCount=2 --> + <MarkRecord index="0"> + <Class value="0"/> + <MarkAnchor Format="1"> + <XCoordinate value="261"/> + <YCoordinate value="488"/> + </MarkAnchor> + </MarkRecord> + <MarkRecord index="1"> + <Class value="1"/> + <MarkAnchor Format="1"> + <XCoordinate value="346"/> + <YCoordinate value="-98"/> + </MarkAnchor> + </MarkRecord> + </MarkArray> + <LigatureArray> + <!-- LigatureCount=1 --> + <LigatureAttach index="0"> + <!-- ComponentCount=3 --> + <ComponentRecord index="0"> + <LigatureAnchor index="0" Format="1"> + <XCoordinate value="625"/> + <YCoordinate value="1800"/> + </LigatureAnchor> + <LigatureAnchor index="1" empty="1"/> + </ComponentRecord> + <ComponentRecord index="1"> + <LigatureAnchor index="0" empty="1"/> + <LigatureAnchor index="1" Format="1"> + <XCoordinate value="376"/> + <YCoordinate value="-368"/> + </LigatureAnchor> + </ComponentRecord> + <ComponentRecord index="2"> + <LigatureAnchor index="0" empty="1"/> + <LigatureAnchor index="1" empty="1"/> + </ComponentRecord> + </LigatureAttach> + </LigatureArray> + </MarkLigPos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/spec6f.fea b/Tests/feaLib/data/spec6f.fea new file mode 100644 index 0000000..8d32008 --- /dev/null +++ b/Tests/feaLib/data/spec6f.fea @@ -0,0 +1,6 @@ +languagesystem DFLT dflt; + +feature test { + markClass damma <anchor 189 -103> @MARK_CLASS_1; + pos mark hamza <anchor 221 301> mark @MARK_CLASS_1; +} test; diff --git a/Tests/feaLib/data/spec6f.ttx b/Tests/feaLib/data/spec6f.ttx new file mode 100644 index 0000000..18a956b --- /dev/null +++ b/Tests/feaLib/data/spec6f.ttx @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GDEF> + <Version value="0x00010000"/> + <GlyphClassDef> + <ClassDef glyph="damma" class="3"/> + <ClassDef glyph="hamza" class="3"/> + </GlyphClassDef> + </GDEF> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="6"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <MarkMarkPos index="0" Format="1"> + <Mark1Coverage> + <Glyph value="damma"/> + </Mark1Coverage> + <Mark2Coverage> + <Glyph value="hamza"/> + </Mark2Coverage> + <!-- ClassCount=1 --> + <Mark1Array> + <!-- MarkCount=1 --> + <MarkRecord index="0"> + <Class value="0"/> + <MarkAnchor Format="1"> + <XCoordinate value="189"/> + <YCoordinate value="-103"/> + </MarkAnchor> + </MarkRecord> + </Mark1Array> + <Mark2Array> + <!-- Mark2Count=1 --> + <Mark2Record index="0"> + <Mark2Anchor index="0" Format="1"> + <XCoordinate value="221"/> + <YCoordinate value="301"/> + </Mark2Anchor> + </Mark2Record> + </Mark2Array> + </MarkMarkPos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/spec6h_ii.fea b/Tests/feaLib/data/spec6h_ii.fea new file mode 100644 index 0000000..36a1f03 --- /dev/null +++ b/Tests/feaLib/data/spec6h_ii.fea @@ -0,0 +1,21 @@ +# OpenType Feature File specification, section 6.h.ii: +# Specifying Contextual Positioning with explicit lookup references +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html + +languagesystem DFLT dflt; + +markClass [acute grave] <anchor 150 -10> @ALL_MARKS; + +lookup CNTXT_PAIR_POS { + pos T o -10; + pos T c -12; +} CNTXT_PAIR_POS; + +lookup CNTXT_MARK_TO_BASE { + pos base o <anchor 250 450> mark @ALL_MARKS; + pos base c <anchor 250 450> mark @ALL_MARKS; +} CNTXT_MARK_TO_BASE; + +feature test { + pos T' lookup CNTXT_PAIR_POS [o c]' @ALL_MARKS' lookup CNTXT_MARK_TO_BASE; +} test; diff --git a/Tests/feaLib/data/spec6h_ii.ttx b/Tests/feaLib/data/spec6h_ii.ttx new file mode 100644 index 0000000..e8ec85f --- /dev/null +++ b/Tests/feaLib/data/spec6h_ii.ttx @@ -0,0 +1,147 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GDEF> + <Version value="0x00010000"/> + <GlyphClassDef> + <ClassDef glyph="acute" class="3"/> + <ClassDef glyph="c" class="1"/> + <ClassDef glyph="grave" class="3"/> + <ClassDef glyph="o" class="1"/> + </GlyphClassDef> + </GDEF> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="2"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=3 --> + <Lookup index="0"> + <LookupType value="2"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <PairPos index="0" Format="1"> + <Coverage> + <Glyph value="T"/> + </Coverage> + <ValueFormat1 value="4"/> + <ValueFormat2 value="0"/> + <!-- PairSetCount=1 --> + <PairSet index="0"> + <!-- PairValueCount=2 --> + <PairValueRecord index="0"> + <SecondGlyph value="c"/> + <Value1 XAdvance="-12"/> + </PairValueRecord> + <PairValueRecord index="1"> + <SecondGlyph value="o"/> + <Value1 XAdvance="-10"/> + </PairValueRecord> + </PairSet> + </PairPos> + </Lookup> + <Lookup index="1"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <MarkBasePos index="0" Format="1"> + <MarkCoverage> + <Glyph value="grave"/> + <Glyph value="acute"/> + </MarkCoverage> + <BaseCoverage> + <Glyph value="c"/> + <Glyph value="o"/> + </BaseCoverage> + <!-- ClassCount=1 --> + <MarkArray> + <!-- MarkCount=2 --> + <MarkRecord index="0"> + <Class value="0"/> + <MarkAnchor Format="1"> + <XCoordinate value="150"/> + <YCoordinate value="-10"/> + </MarkAnchor> + </MarkRecord> + <MarkRecord index="1"> + <Class value="0"/> + <MarkAnchor Format="1"> + <XCoordinate value="150"/> + <YCoordinate value="-10"/> + </MarkAnchor> + </MarkRecord> + </MarkArray> + <BaseArray> + <!-- BaseCount=2 --> + <BaseRecord index="0"> + <BaseAnchor index="0" Format="1"> + <XCoordinate value="250"/> + <YCoordinate value="450"/> + </BaseAnchor> + </BaseRecord> + <BaseRecord index="1"> + <BaseAnchor index="0" Format="1"> + <XCoordinate value="250"/> + <YCoordinate value="450"/> + </BaseAnchor> + </BaseRecord> + </BaseArray> + </MarkBasePos> + </Lookup> + <Lookup index="2"> + <LookupType value="8"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <ChainContextPos index="0" Format="3"> + <!-- BacktrackGlyphCount=0 --> + <!-- InputGlyphCount=3 --> + <InputCoverage index="0"> + <Glyph value="T"/> + </InputCoverage> + <InputCoverage index="1"> + <Glyph value="c"/> + <Glyph value="o"/> + </InputCoverage> + <InputCoverage index="2"> + <Glyph value="grave"/> + <Glyph value="acute"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- PosCount=2 --> + <PosLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="0"/> + </PosLookupRecord> + <PosLookupRecord index="1"> + <SequenceIndex value="2"/> + <LookupListIndex value="1"/> + </PosLookupRecord> + </ChainContextPos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/spec6h_iii_1.fea b/Tests/feaLib/data/spec6h_iii_1.fea new file mode 100644 index 0000000..276fa2f --- /dev/null +++ b/Tests/feaLib/data/spec6h_iii_1.fea @@ -0,0 +1,9 @@ +# OpenType Feature File specification, section 6.h.iii, example 1: +# Specifying Contextual Positioning with in-line single positioning rules +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html + +languagesystem DFLT dflt; + +feature test { + pos [quoteleft quotedblleft] [Y T]' <0 0 20 0> [quoteright quotedblright]; +} test; diff --git a/Tests/feaLib/data/spec6h_iii_1.ttx b/Tests/feaLib/data/spec6h_iii_1.ttx new file mode 100644 index 0000000..a12b0f5 --- /dev/null +++ b/Tests/feaLib/data/spec6h_iii_1.ttx @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="8"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <ChainContextPos index="0" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="quotedblleft"/> + <Glyph value="quoteleft"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="T"/> + <Glyph value="Y"/> + </InputCoverage> + <!-- LookAheadGlyphCount=1 --> + <LookAheadCoverage index="0"> + <Glyph value="quotedblright"/> + <Glyph value="quoteright"/> + </LookAheadCoverage> + <!-- PosCount=1 --> + <PosLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="1"/> + </PosLookupRecord> + </ChainContextPos> + </Lookup> + <Lookup index="1"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="T"/> + <Glyph value="Y"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="20"/> + </SinglePos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/spec6h_iii_3d.fea b/Tests/feaLib/data/spec6h_iii_3d.fea new file mode 100644 index 0000000..7b7d8e9 --- /dev/null +++ b/Tests/feaLib/data/spec6h_iii_3d.fea @@ -0,0 +1,7 @@ +# OpenType Feature File specification, section 6.h.iii, example 3d: +# Specifying Contextual Positioning with in-line single positioning rules +# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html + +feature test { + pos L' quoteright' -150; +} test; diff --git a/Tests/feaLib/data/spec6h_iii_3d.ttx b/Tests/feaLib/data/spec6h_iii_3d.ttx new file mode 100644 index 0000000..a608f0e --- /dev/null +++ b/Tests/feaLib/data/spec6h_iii_3d.ttx @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="test"/> + <Feature> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=2 --> + <Lookup index="0"> + <LookupType value="8"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <ChainContextPos index="0" Format="3"> + <!-- BacktrackGlyphCount=0 --> + <!-- InputGlyphCount=2 --> + <InputCoverage index="0"> + <Glyph value="L"/> + </InputCoverage> + <InputCoverage index="1"> + <Glyph value="quoteright"/> + </InputCoverage> + <!-- LookAheadGlyphCount=0 --> + <!-- PosCount=1 --> + <PosLookupRecord index="0"> + <SequenceIndex value="1"/> + <LookupListIndex value="1"/> + </PosLookupRecord> + </ChainContextPos> + </Lookup> + <Lookup index="1"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SinglePos index="0" Format="1"> + <Coverage> + <Glyph value="quoteright"/> + </Coverage> + <ValueFormat value="4"/> + <Value XAdvance="-150"/> + </SinglePos> + </Lookup> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/spec8a.fea b/Tests/feaLib/data/spec8a.fea new file mode 100644 index 0000000..b4d7dd2 --- /dev/null +++ b/Tests/feaLib/data/spec8a.fea @@ -0,0 +1,21 @@ +languagesystem DFLT dflt; +languagesystem latn dflt; +languagesystem latn TRK; +languagesystem cyrl dflt; + +feature aalt { + feature salt; + feature smcp; + sub d by d.alt; +} aalt; + +feature smcp { + sub [a-c] by [A.sc-C.sc]; + sub f i by f_i; # not considered for aalt +} smcp; + +feature salt { + sub a from [a.alt1 a.alt2 a.alt3]; + sub e [c d e]' f by [c.mid d.mid e.mid]; + sub b by b.alt; +} salt; diff --git a/Tests/feaLib/data/spec8a.ttx b/Tests/feaLib/data/spec8a.ttx new file mode 100644 index 0000000..787ecfa --- /dev/null +++ b/Tests/feaLib/data/spec8a.ttx @@ -0,0 +1,200 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=3 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=3 --> + <FeatureIndex index="0" value="0"/> + <FeatureIndex index="1" value="1"/> + <FeatureIndex index="2" value="2"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + <ScriptRecord index="1"> + <ScriptTag value="cyrl"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=3 --> + <FeatureIndex index="0" value="0"/> + <FeatureIndex index="1" value="1"/> + <FeatureIndex index="2" value="2"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + <ScriptRecord index="2"> + <ScriptTag value="latn"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=3 --> + <FeatureIndex index="0" value="0"/> + <FeatureIndex index="1" value="1"/> + <FeatureIndex index="2" value="2"/> + </DefaultLangSys> + <!-- LangSysCount=1 --> + <LangSysRecord index="0"> + <LangSysTag value="TRK "/> + <LangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=3 --> + <FeatureIndex index="0" value="0"/> + <FeatureIndex index="1" value="1"/> + <FeatureIndex index="2" value="2"/> + </LangSys> + </LangSysRecord> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=3 --> + <FeatureRecord index="0"> + <FeatureTag value="aalt"/> + <Feature> + <!-- LookupCount=2 --> + <LookupListIndex index="0" value="0"/> + <LookupListIndex index="1" value="1"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="1"> + <FeatureTag value="salt"/> + <Feature> + <!-- LookupCount=3 --> + <LookupListIndex index="0" value="4"/> + <LookupListIndex index="1" value="5"/> + <LookupListIndex index="2" value="7"/> + </Feature> + </FeatureRecord> + <FeatureRecord index="2"> + <FeatureTag value="smcp"/> + <Feature> + <!-- LookupCount=2 --> + <LookupListIndex index="0" value="2"/> + <LookupListIndex index="1" value="3"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=8 --> + <Lookup index="0"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="e" out="e.mid"/> + </SingleSubst> + </Lookup> + <Lookup index="1"> + <LookupType value="3"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <AlternateSubst index="0"> + <AlternateSet glyph="a"> + <Alternate glyph="A.sc"/> + <Alternate glyph="a.alt1"/> + <Alternate glyph="a.alt2"/> + <Alternate glyph="a.alt3"/> + </AlternateSet> + <AlternateSet glyph="b"> + <Alternate glyph="B.sc"/> + <Alternate glyph="b.alt"/> + </AlternateSet> + <AlternateSet glyph="c"> + <Alternate glyph="C.sc"/> + <Alternate glyph="c.mid"/> + </AlternateSet> + <AlternateSet glyph="d"> + <Alternate glyph="d.alt"/> + <Alternate glyph="d.mid"/> + </AlternateSet> + </AlternateSubst> + </Lookup> + <Lookup index="2"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="a" out="A.sc"/> + <Substitution in="b" out="B.sc"/> + <Substitution in="c" out="C.sc"/> + </SingleSubst> + </Lookup> + <Lookup index="3"> + <LookupType value="4"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <LigatureSubst index="0"> + <LigatureSet glyph="f"> + <Ligature components="i" glyph="f_i"/> + </LigatureSet> + </LigatureSubst> + </Lookup> + <Lookup index="4"> + <LookupType value="3"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <AlternateSubst index="0"> + <AlternateSet glyph="a"> + <Alternate glyph="a.alt1"/> + <Alternate glyph="a.alt2"/> + <Alternate glyph="a.alt3"/> + </AlternateSet> + </AlternateSubst> + </Lookup> + <Lookup index="5"> + <LookupType value="6"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <ChainContextSubst index="0" Format="3"> + <!-- BacktrackGlyphCount=1 --> + <BacktrackCoverage index="0"> + <Glyph value="e"/> + </BacktrackCoverage> + <!-- InputGlyphCount=1 --> + <InputCoverage index="0"> + <Glyph value="c"/> + <Glyph value="d"/> + <Glyph value="e"/> + </InputCoverage> + <!-- LookAheadGlyphCount=1 --> + <LookAheadCoverage index="0"> + <Glyph value="f"/> + </LookAheadCoverage> + <!-- SubstCount=1 --> + <SubstLookupRecord index="0"> + <SequenceIndex value="0"/> + <LookupListIndex value="6"/> + </SubstLookupRecord> + </ChainContextSubst> + </Lookup> + <Lookup index="6"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="c" out="c.mid"/> + <Substitution in="d" out="d.mid"/> + <Substitution in="e" out="e.mid"/> + </SingleSubst> + </Lookup> + <Lookup index="7"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="b" out="b.alt"/> + </SingleSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/spec8b.fea b/Tests/feaLib/data/spec8b.fea new file mode 100644 index 0000000..538ac39 --- /dev/null +++ b/Tests/feaLib/data/spec8b.fea @@ -0,0 +1,12 @@ +feature size { + parameters 10.0 3 80 139; +# 10.0 - design size, 3 - subfamily identifier, 80 - range start (exclusive, decipoints) +# 139 - range end (inclusive, decipoints) + sizemenuname "Win MinionPro Size Name"; + sizemenuname 1 "Mac MinionPro Size Name"; + # The specification says: sizemenuname 1 21 0 "Mac MinionPro Size Name"; + # which means Macintosh platform, MacOS Thai encoding, English language. + # Since fonttools currently does not support the MacOS Thai encoding, + # we use instead MacOS Roman encoding (0), Swedish language (5) for our test. + sizemenuname 1 0 5 "Mac MinionPro Size Name"; +} size; diff --git a/Tests/feaLib/data/spec8b.ttx b/Tests/feaLib/data/spec8b.ttx new file mode 100644 index 0000000..6e66c16 --- /dev/null +++ b/Tests/feaLib/data/spec8b.ttx @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <name> + <namerecord nameID="256" platformID="3" platEncID="1" langID="0x409"> + Win MinionPro Size Name + </namerecord> + <namerecord nameID="256" platformID="1" platEncID="0" langID="0x0" unicode="True"> + Mac MinionPro Size Name + </namerecord> + <namerecord nameID="256" platformID="1" platEncID="0" langID="0x5" unicode="True"> + Mac MinionPro Size Name + </namerecord> + </name> + + <GPOS> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="size"/> + <Feature> + <FeatureParamsSize> + <DesignSize value="10.0"/> + <SubfamilyID value="3"/> + <SubfamilyNameID value="256"/> <!-- Win MinionPro Size Name --> + <RangeStart value="8.0"/> + <RangeEnd value="13.9"/> + </FeatureParamsSize> + <!-- LookupCount=0 --> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=0 --> + </LookupList> + </GPOS> + +</ttFont> diff --git a/Tests/feaLib/data/spec8c.fea b/Tests/feaLib/data/spec8c.fea new file mode 100644 index 0000000..be3d31b --- /dev/null +++ b/Tests/feaLib/data/spec8c.fea @@ -0,0 +1,13 @@ +feature ss01 { + featureNames { + name "Feature description for MS Platform, script Unicode, language English"; +# With no platform ID, script ID, or language ID specified, the implementation assumes (3,1,0x409). +#test-fea2fea: name 3 1 1041 "Feature description for MS Platform, script Unicode, language Japanese"; + name 3 1 0x411 "Feature description for MS Platform, script Unicode, language Japanese"; + name 1 "Feature description for Apple Platform, script Roman, language unspecified"; +# With only the platform ID specified, the implementation assumes script and language = Latin. For Apple this is (1,0,0). + name 1 1 12 "Feature description for Apple Platform, script Japanese, language Japanese"; + }; +# --- rules for this feature --- + sub A by B; +} ss01; diff --git a/Tests/feaLib/data/spec8c.ttx b/Tests/feaLib/data/spec8c.ttx new file mode 100644 index 0000000..a5b5517 --- /dev/null +++ b/Tests/feaLib/data/spec8c.ttx @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <name> + <namerecord nameID="256" platformID="3" platEncID="1" langID="0x409"> + Feature description for MS Platform, script Unicode, language English + </namerecord> + <namerecord nameID="256" platformID="3" platEncID="1" langID="0x411"> + Feature description for MS Platform, script Unicode, language Japanese + </namerecord> + <namerecord nameID="256" platformID="1" platEncID="0" langID="0x0" unicode="True"> + Feature description for Apple Platform, script Roman, language unspecified + </namerecord> + <namerecord nameID="256" platformID="1" platEncID="1" langID="0xc" unicode="True"> + Feature description for Apple Platform, script Japanese, language Japanese + </namerecord> + </name> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="ss01"/> + <Feature> + <FeatureParamsStylisticSet> + <Version value="0"/> + <UINameID value="256"/> <!-- Feature description for MS Platform, script Unicode, language English --> + </FeatureParamsStylisticSet> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="A" out="B"/> + </SingleSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/spec8d.fea b/Tests/feaLib/data/spec8d.fea new file mode 100644 index 0000000..2b68b03 --- /dev/null +++ b/Tests/feaLib/data/spec8d.fea @@ -0,0 +1,57 @@ +# The cvParameters block must precede any of the rules in the feature. +# The ParamUILabelNameID entry may be omitted or repeated as often as needed. +# The other NameID types may be omitted, or defined only once. +# The NameID entries must be specified in the order listed below. + +# Following the set of NameID entries, a series of 24-bit Unicode values may be specified. +# These provide Unicode values for the base glyphs referenced by the feature. +# The developer may specify none, some, or all of the Unicode values for the base glyphs. +# The Unicode value may be written with either decimal or hexadecimal notation. +# The value must be preceded by '0x' if it is a hexadecimal value. + +# NOTE: The ParamUILabelNameID entries are used when one base glyph is mapped to more than +# one variant; the font designer may then specify one ParamUILabelNameID for each variant, in +# order to uniquely describe that variant. If any ParamUILabelNameID entries are specified, +# the number of ParamUILabelNameID entries must match the number of variants for each base +# glyph. If the Character Variant feature specifies more than one base glyph, then the set +# of NameID entries in the parameter block will be used for each base glyph and its variants. +feature cv01 { + cvParameters { + FeatUILabelNameID { +#test-fea2fea: name "uilabel simple a"; + name 3 1 0x0409 "uilabel simple a"; # English US +#test-fea2fea: name 1 "uilabel simple a"; + name 1 0 0 "uilabel simple a"; # Mac English + }; + FeatUITooltipTextNameID { +#test-fea2fea: name "tool tip simple a"; + name 3 1 0x0409 "tool tip simple a"; # English US +#test-fea2fea: name 1 "tool tip simple a"; + name 1 0 0 "tool tip simple a"; # Mac English + }; + SampleTextNameID { +#test-fea2fea: name "sample text simple a"; + name 3 1 0x0409 "sample text simple a"; # English US +#test-fea2fea: name 1 "sample text simple a"; + name 1 0 0 "sample text simple a"; # Mac English + }; + ParamUILabelNameID { +#test-fea2fea: name "param1 text simple a"; + name 3 1 0x0409 "param1 text simple a"; # English US +#test-fea2fea: name 1 "param1 text simple a"; + name 1 0 0 "param1 text simple a"; # Mac English + }; + ParamUILabelNameID { +#test-fea2fea: name "param2 text simple a"; + name 3 1 0x0409 "param2 text simple a"; # English US +#test-fea2fea: name 1 "param2 text simple a"; + name 1 0 0 "param2 text simple a"; # Mac English + }; +#test-fea2fea: Character 0xa; + Character 10; +#test-fea2fea: Character 0x5dde; + Character 0x5DDE; + }; +# --- rules for this feature --- + sub A by B; +} cv01; diff --git a/Tests/feaLib/data/spec8d.ttx b/Tests/feaLib/data/spec8d.ttx new file mode 100644 index 0000000..9848a69 --- /dev/null +++ b/Tests/feaLib/data/spec8d.ttx @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <name> + <namerecord nameID="256" platformID="3" platEncID="1" langID="0x409"> + uilabel simple a + </namerecord> + <namerecord nameID="256" platformID="1" platEncID="0" langID="0x0" unicode="True"> + uilabel simple a + </namerecord> + <namerecord nameID="257" platformID="3" platEncID="1" langID="0x409"> + tool tip simple a + </namerecord> + <namerecord nameID="257" platformID="1" platEncID="0" langID="0x0" unicode="True"> + tool tip simple a + </namerecord> + <namerecord nameID="258" platformID="3" platEncID="1" langID="0x409"> + sample text simple a + </namerecord> + <namerecord nameID="258" platformID="1" platEncID="0" langID="0x0" unicode="True"> + sample text simple a + </namerecord> + <namerecord nameID="259" platformID="3" platEncID="1" langID="0x409"> + param1 text simple a + </namerecord> + <namerecord nameID="259" platformID="1" platEncID="0" langID="0x0" unicode="True"> + param1 text simple a + </namerecord> + <namerecord nameID="260" platformID="3" platEncID="1" langID="0x409"> + param2 text simple a + </namerecord> + <namerecord nameID="260" platformID="1" platEncID="0" langID="0x0" unicode="True"> + param2 text simple a + </namerecord> + </name> + + <GSUB> + <Version value="0x00010000"/> + <ScriptList> + <!-- ScriptCount=1 --> + <ScriptRecord index="0"> + <ScriptTag value="DFLT"/> + <Script> + <DefaultLangSys> + <ReqFeatureIndex value="65535"/> + <!-- FeatureCount=1 --> + <FeatureIndex index="0" value="0"/> + </DefaultLangSys> + <!-- LangSysCount=0 --> + </Script> + </ScriptRecord> + </ScriptList> + <FeatureList> + <!-- FeatureCount=1 --> + <FeatureRecord index="0"> + <FeatureTag value="cv01"/> + <Feature> + <FeatureParamsCharacterVariants Format="0"> + <Format value="0"/> + <FeatUILabelNameID value="256"/> <!-- uilabel simple a --> + <FeatUITooltipTextNameID value="257"/> <!-- tool tip simple a --> + <SampleTextNameID value="258"/> <!-- sample text simple a --> + <NumNamedParameters value="2"/> + <FirstParamUILabelNameID value="259"/> <!-- param1 text simple a --> + <!-- CharCount=2 --> + <Character index="0" value="10"/> + <Character index="1" value="24030"/> + </FeatureParamsCharacterVariants> + <!-- LookupCount=1 --> + <LookupListIndex index="0" value="0"/> + </Feature> + </FeatureRecord> + </FeatureList> + <LookupList> + <!-- LookupCount=1 --> + <Lookup index="0"> + <LookupType value="1"/> + <LookupFlag value="0"/> + <!-- SubTableCount=1 --> + <SingleSubst index="0"> + <Substitution in="A" out="B"/> + </SingleSubst> + </Lookup> + </LookupList> + </GSUB> + +</ttFont> diff --git a/Tests/feaLib/data/spec9a.fea b/Tests/feaLib/data/spec9a.fea new file mode 100644 index 0000000..84cca57 --- /dev/null +++ b/Tests/feaLib/data/spec9a.fea @@ -0,0 +1,4 @@ +table BASE { + HorizAxis.BaseTagList ideo romn; + HorizAxis.BaseScriptList latn romn -120 0, cyrl romn -120 0, grek romn -120 0, hani ideo -120 0, kana ideo -120 0, hang ideo -120 0; +} BASE; diff --git a/Tests/feaLib/data/spec9a.ttx b/Tests/feaLib/data/spec9a.ttx new file mode 100644 index 0000000..c6bb896 --- /dev/null +++ b/Tests/feaLib/data/spec9a.ttx @@ -0,0 +1,114 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <BASE> + <Version value="0x00010000"/> + <HorizAxis> + <BaseTagList> + <!-- BaseTagCount=2 --> + <BaselineTag index="0" value="ideo"/> + <BaselineTag index="1" value="romn"/> + </BaseTagList> + <BaseScriptList> + <!-- BaseScriptCount=6 --> + <BaseScriptRecord index="0"> + <BaseScriptTag value="cyrl"/> + <BaseScript> + <BaseValues> + <DefaultIndex value="1"/> + <!-- BaseCoordCount=2 --> + <BaseCoord index="0" Format="1"> + <Coordinate value="-120"/> + </BaseCoord> + <BaseCoord index="1" Format="1"> + <Coordinate value="0"/> + </BaseCoord> + </BaseValues> + <!-- BaseLangSysCount=0 --> + </BaseScript> + </BaseScriptRecord> + <BaseScriptRecord index="1"> + <BaseScriptTag value="grek"/> + <BaseScript> + <BaseValues> + <DefaultIndex value="1"/> + <!-- BaseCoordCount=2 --> + <BaseCoord index="0" Format="1"> + <Coordinate value="-120"/> + </BaseCoord> + <BaseCoord index="1" Format="1"> + <Coordinate value="0"/> + </BaseCoord> + </BaseValues> + <!-- BaseLangSysCount=0 --> + </BaseScript> + </BaseScriptRecord> + <BaseScriptRecord index="2"> + <BaseScriptTag value="hang"/> + <BaseScript> + <BaseValues> + <DefaultIndex value="0"/> + <!-- BaseCoordCount=2 --> + <BaseCoord index="0" Format="1"> + <Coordinate value="-120"/> + </BaseCoord> + <BaseCoord index="1" Format="1"> + <Coordinate value="0"/> + </BaseCoord> + </BaseValues> + <!-- BaseLangSysCount=0 --> + </BaseScript> + </BaseScriptRecord> + <BaseScriptRecord index="3"> + <BaseScriptTag value="hani"/> + <BaseScript> + <BaseValues> + <DefaultIndex value="0"/> + <!-- BaseCoordCount=2 --> + <BaseCoord index="0" Format="1"> + <Coordinate value="-120"/> + </BaseCoord> + <BaseCoord index="1" Format="1"> + <Coordinate value="0"/> + </BaseCoord> + </BaseValues> + <!-- BaseLangSysCount=0 --> + </BaseScript> + </BaseScriptRecord> + <BaseScriptRecord index="4"> + <BaseScriptTag value="kana"/> + <BaseScript> + <BaseValues> + <DefaultIndex value="0"/> + <!-- BaseCoordCount=2 --> + <BaseCoord index="0" Format="1"> + <Coordinate value="-120"/> + </BaseCoord> + <BaseCoord index="1" Format="1"> + <Coordinate value="0"/> + </BaseCoord> + </BaseValues> + <!-- BaseLangSysCount=0 --> + </BaseScript> + </BaseScriptRecord> + <BaseScriptRecord index="5"> + <BaseScriptTag value="latn"/> + <BaseScript> + <BaseValues> + <DefaultIndex value="1"/> + <!-- BaseCoordCount=2 --> + <BaseCoord index="0" Format="1"> + <Coordinate value="-120"/> + </BaseCoord> + <BaseCoord index="1" Format="1"> + <Coordinate value="0"/> + </BaseCoord> + </BaseValues> + <!-- BaseLangSysCount=0 --> + </BaseScript> + </BaseScriptRecord> + </BaseScriptList> + </HorizAxis> + </BASE> + +</ttFont> diff --git a/Tests/feaLib/data/spec9b.fea b/Tests/feaLib/data/spec9b.fea new file mode 100644 index 0000000..c72d3f0 --- /dev/null +++ b/Tests/feaLib/data/spec9b.fea @@ -0,0 +1,13 @@ +@BASE = [f i]; +@LIGATURES = [c_s c_t f_i f_f_i s_t]; +@MARKS = [acute grave]; +@COMPONENT = [noon.final noon.initial]; + +table GDEF { + GlyphClassDef @BASE, @LIGATURES, @MARKS, @COMPONENT; + Attach noon.final 5; + Attach noon.initial 4; + LigatureCaretByPos f_i 400 380; + LigatureCaretByPos [c_t s_t] 500; + LigatureCaretByIndex f_f_i 23 46; +} GDEF; diff --git a/Tests/feaLib/data/spec9b.ttx b/Tests/feaLib/data/spec9b.ttx new file mode 100644 index 0000000..eb77bbb --- /dev/null +++ b/Tests/feaLib/data/spec9b.ttx @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="true" ttLibVersion="3.0"> + + <GDEF> + <Version value="0x00010000"/> + <GlyphClassDef> + <ClassDef glyph="acute" class="3"/> + <ClassDef glyph="c_s" class="2"/> + <ClassDef glyph="c_t" class="2"/> + <ClassDef glyph="f" class="1"/> + <ClassDef glyph="f_f_i" class="2"/> + <ClassDef glyph="f_i" class="2"/> + <ClassDef glyph="grave" class="3"/> + <ClassDef glyph="i" class="1"/> + <ClassDef glyph="noon.final" class="4"/> + <ClassDef glyph="noon.initial" class="4"/> + <ClassDef glyph="s_t" class="2"/> + </GlyphClassDef> + <AttachList> + <Coverage> + <Glyph value="noon.final"/> + <Glyph value="noon.initial"/> + </Coverage> + <!-- GlyphCount=2 --> + <AttachPoint index="0"> + <!-- PointCount=1 --> + <PointIndex index="0" value="5"/> + </AttachPoint> + <AttachPoint index="1"> + <!-- PointCount=1 --> + <PointIndex index="0" value="4"/> + </AttachPoint> + </AttachList> + <LigCaretList> + <Coverage> + <Glyph value="c_t"/> + <Glyph value="f_f_i"/> + <Glyph value="f_i"/> + <Glyph value="s_t"/> + </Coverage> + <!-- LigGlyphCount=4 --> + <LigGlyph index="0"> + <!-- CaretCount=1 --> + <CaretValue index="0" Format="1"> + <Coordinate value="500"/> + </CaretValue> + </LigGlyph> + <LigGlyph index="1"> + <!-- CaretCount=2 --> + <CaretValue index="0" Format="2"> + <CaretValuePoint value="23"/> + </CaretValue> + <CaretValue index="1" Format="2"> + <CaretValuePoint value="46"/> + </CaretValue> + </LigGlyph> + <LigGlyph index="2"> + <!-- CaretCount=2 --> + <CaretValue index="0" Format="1"> + <Coordinate value="380"/> + </CaretValue> + <CaretValue index="1" Format="1"> + <Coordinate value="400"/> + </CaretValue> + </LigGlyph> + <LigGlyph index="3"> + <!-- CaretCount=1 --> + <CaretValue index="0" Format="1"> + <Coordinate value="500"/> + </CaretValue> + </LigGlyph> + </LigCaretList> + </GDEF> + +</ttFont> diff --git a/Tests/feaLib/data/spec9c1.fea b/Tests/feaLib/data/spec9c1.fea new file mode 100644 index 0000000..57ed119 --- /dev/null +++ b/Tests/feaLib/data/spec9c1.fea @@ -0,0 +1,4 @@ +table head { +#test-fea2fea: FontRevision 1.100; + FontRevision 1.1; +} head; diff --git a/Tests/feaLib/data/spec9c1.ttx b/Tests/feaLib/data/spec9c1.ttx new file mode 100644 index 0000000..b60e0f5 --- /dev/null +++ b/Tests/feaLib/data/spec9c1.ttx @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <head> + <!-- Most of this table will be recalculated by the compiler --> + <tableVersion value="1.0"/> + <fontRevision value="1.1"/> + <checkSumAdjustment value="0x0"/> + <magicNumber value="0x0"/> + <flags value="00000000 00000000"/> + <unitsPerEm value="0"/> + <created value="Tue Dec 13 11:22:33 2011"/> + <modified value="Tue Dec 13 11:22:33 2011"/> + <xMin value="0"/> + <yMin value="0"/> + <xMax value="0"/> + <yMax value="0"/> + <macStyle value="00000000 00000000"/> + <lowestRecPPEM value="0"/> + <fontDirectionHint value="0"/> + <indexToLocFormat value="0"/> + <glyphDataFormat value="0"/> + </head> + +</ttFont> diff --git a/Tests/feaLib/data/spec9c2.fea b/Tests/feaLib/data/spec9c2.fea new file mode 100644 index 0000000..333fa16 --- /dev/null +++ b/Tests/feaLib/data/spec9c2.fea @@ -0,0 +1,3 @@ +table head { + FontRevision 1.001; +} head; diff --git a/Tests/feaLib/data/spec9c2.ttx b/Tests/feaLib/data/spec9c2.ttx new file mode 100644 index 0000000..ed3d740 --- /dev/null +++ b/Tests/feaLib/data/spec9c2.ttx @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <head> + <!-- Most of this table will be recalculated by the compiler --> + <tableVersion value="1.0"/> + <fontRevision value="1.001"/> + <checkSumAdjustment value="0x0"/> + <magicNumber value="0x0"/> + <flags value="00000000 00000000"/> + <unitsPerEm value="0"/> + <created value="Tue Dec 13 11:22:33 2011"/> + <modified value="Tue Dec 13 11:22:33 2011"/> + <xMin value="0"/> + <yMin value="0"/> + <xMax value="0"/> + <yMax value="0"/> + <macStyle value="00000000 00000000"/> + <lowestRecPPEM value="0"/> + <fontDirectionHint value="0"/> + <indexToLocFormat value="0"/> + <glyphDataFormat value="0"/> + </head> + +</ttFont> diff --git a/Tests/feaLib/data/spec9c3.fea b/Tests/feaLib/data/spec9c3.fea new file mode 100644 index 0000000..e9433a8 --- /dev/null +++ b/Tests/feaLib/data/spec9c3.fea @@ -0,0 +1,3 @@ +table head { + FontRevision 1.500; +} head; diff --git a/Tests/feaLib/data/spec9c3.ttx b/Tests/feaLib/data/spec9c3.ttx new file mode 100644 index 0000000..e20186c --- /dev/null +++ b/Tests/feaLib/data/spec9c3.ttx @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <head> + <!-- Most of this table will be recalculated by the compiler --> + <tableVersion value="1.0"/> + <fontRevision value="1.5"/> + <checkSumAdjustment value="0x0"/> + <magicNumber value="0x0"/> + <flags value="00000000 00000000"/> + <unitsPerEm value="0"/> + <created value="Tue Dec 13 11:22:33 2011"/> + <modified value="Tue Dec 13 11:22:33 2011"/> + <xMin value="0"/> + <yMin value="0"/> + <xMax value="0"/> + <yMax value="0"/> + <macStyle value="00000000 00000000"/> + <lowestRecPPEM value="0"/> + <fontDirectionHint value="0"/> + <indexToLocFormat value="0"/> + <glyphDataFormat value="0"/> + </head> + +</ttFont> diff --git a/Tests/feaLib/data/spec9d.fea b/Tests/feaLib/data/spec9d.fea new file mode 100644 index 0000000..231e7bd --- /dev/null +++ b/Tests/feaLib/data/spec9d.fea @@ -0,0 +1,6 @@ +table hhea { + CaretOffset -50; + Ascender 800; + Descender 200; + LineGap 200; +} hhea; diff --git a/Tests/feaLib/data/spec9d.ttx b/Tests/feaLib/data/spec9d.ttx new file mode 100644 index 0000000..76863d8 --- /dev/null +++ b/Tests/feaLib/data/spec9d.ttx @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <hhea> + <tableVersion value="0x00010000"/> + <ascent value="800"/> + <descent value="200"/> + <lineGap value="200"/> + <advanceWidthMax value="0"/> + <minLeftSideBearing value="0"/> + <minRightSideBearing value="0"/> + <xMaxExtent value="0"/> + <caretSlopeRise value="0"/> + <caretSlopeRun value="0"/> + <caretOffset value="-50"/> + <reserved0 value="0"/> + <reserved1 value="0"/> + <reserved2 value="0"/> + <reserved3 value="0"/> + <metricDataFormat value="0"/> + <numberOfHMetrics value="0"/> + </hhea> + +</ttFont> diff --git a/Tests/feaLib/data/spec9e.fea b/Tests/feaLib/data/spec9e.fea new file mode 100644 index 0000000..58f59e9 --- /dev/null +++ b/Tests/feaLib/data/spec9e.fea @@ -0,0 +1,4 @@ +table name { + nameid 9 "Joachim M\00fcller-Lanc\00e9"; # Windows (Unicode) + nameid 9 1 "Joachim M\9fller-Lanc\8e"; # Macintosh (Mac Roman) +} name; diff --git a/Tests/feaLib/data/spec9e.ttx b/Tests/feaLib/data/spec9e.ttx new file mode 100644 index 0000000..5119a5f --- /dev/null +++ b/Tests/feaLib/data/spec9e.ttx @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <name> + <namerecord nameID="9" platformID="3" platEncID="1" langID="0x409"> + Joachim Müller-Lancé + </namerecord> + <namerecord nameID="9" platformID="1" platEncID="0" langID="0x0" unicode="True"> + Joachim Müller-Lancé + </namerecord> + </name> + +</ttFont> diff --git a/Tests/feaLib/data/spec9f.fea b/Tests/feaLib/data/spec9f.fea new file mode 100644 index 0000000..90cf6d1 --- /dev/null +++ b/Tests/feaLib/data/spec9f.fea @@ -0,0 +1,19 @@ +table OS/2 { + FSType 4; + Panose 2 15 0 0 2 2 8 2 9 4; + TypoAscender 800; + TypoDescender -200; # Note that TypoDescender is negative for descent below the baseline. + winAscent 832; + winDescent 321; # Note that winDescent is positive for descent below the baseline. + UnicodeRange 0 1 9 55 59 60; +# 0 - Basic Latin, 1 - Latin-1 Supplement +# 9 - Cyrillic, 55 - CJK Compatibility +# 59 - CJK Unified Ideographs, 60 - Private Use Area + CodePageRange 1252 1251 932; +# 1252 - Latin 1, 1251 - Cyrllic, 932 - JIS/Japan + XHeight 400; + CapHeight 600; + WeightClass 800; + WidthClass 3; + Vendor "ADBE"; +} OS/2; diff --git a/Tests/feaLib/data/spec9f.ttx b/Tests/feaLib/data/spec9f.ttx new file mode 100644 index 0000000..9e147d1 --- /dev/null +++ b/Tests/feaLib/data/spec9f.ttx @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont sfntVersion="OTTO" ttLibVersion="3.0"> + + <OS_2> + <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex' + will be recalculated by the compiler --> + <version value="2"/> + <xAvgCharWidth value="0"/> + <usWeightClass value="800"/> + <usWidthClass value="3"/> + <fsType value="00000000 00000100"/> + <ySubscriptXSize value="0"/> + <ySubscriptYSize value="0"/> + <ySubscriptXOffset value="0"/> + <ySubscriptYOffset value="0"/> + <ySuperscriptXSize value="0"/> + <ySuperscriptYSize value="0"/> + <ySuperscriptXOffset value="0"/> + <ySuperscriptYOffset value="0"/> + <yStrikeoutSize value="0"/> + <yStrikeoutPosition value="0"/> + <sFamilyClass value="0"/> + <panose> + <bFamilyType value="2"/> + <bSerifStyle value="15"/> + <bWeight value="0"/> + <bProportion value="0"/> + <bContrast value="2"/> + <bStrokeVariation value="2"/> + <bArmStyle value="8"/> + <bLetterForm value="2"/> + <bMidline value="9"/> + <bXHeight value="4"/> + </panose> + <ulUnicodeRange1 value="00000000 00000000 00000010 00000011"/> + <ulUnicodeRange2 value="00011000 10000000 00000000 00000000"/> + <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/> + <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/> + <achVendID value="ADBE"/> + <fsSelection value="00000000 00000000"/> + <usFirstCharIndex value="0"/> + <usLastCharIndex value="0"/> + <sTypoAscender value="800"/> + <sTypoDescender value="-200"/> + <sTypoLineGap value="0"/> + <usWinAscent value="832"/> + <usWinDescent value="321"/> + <ulCodePageRange1 value="00000000 00000010 00000000 00000101"/> + <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/> + <sxHeight value="400"/> + <sCapHeight value="600"/> + <usDefaultChar value="0"/> + <usBreakChar value="0"/> + <usMaxContext value="0"/> + </OS_2> + +</ttFont> diff --git a/Tests/feaLib/data/spec9g.fea b/Tests/feaLib/data/spec9g.fea new file mode 100644 index 0000000..3b3e0c1 --- /dev/null +++ b/Tests/feaLib/data/spec9g.fea @@ -0,0 +1,5 @@ +table vhea { + VertTypoAscender 500; + VertTypoDescender -500; + VertTypoLineGap 1000; +} vhea; diff --git a/Tests/feaLib/data/spec9g.ttx b/Tests/feaLib/data/spec9g.ttx new file mode 100644 index 0000000..e5541f0 --- /dev/null +++ b/Tests/feaLib/data/spec9g.ttx @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + + <vhea> + <tableVersion value="0x00011000"/> + <ascent value="500"/> + <descent value="-500"/> + <lineGap value="1000"/> + <advanceHeightMax value="0"/> + <minTopSideBearing value="0"/> + <minBottomSideBearing value="0"/> + <yMaxExtent value="0"/> + <caretSlopeRise value="0"/> + <caretSlopeRun value="0"/> + <caretOffset value="0"/> + <reserved1 value="0"/> + <reserved2 value="0"/> + <reserved3 value="0"/> + <reserved4 value="0"/> + <metricDataFormat value="0"/> + <numberOfVMetrics value="0"/> + </vhea> + +</ttFont> diff --git a/Tests/feaLib/error_test.py b/Tests/feaLib/error_test.py new file mode 100644 index 0000000..87cbecb --- /dev/null +++ b/Tests/feaLib/error_test.py @@ -0,0 +1,19 @@ +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.feaLib.error import FeatureLibError +import unittest + + +class FeatureLibErrorTest(unittest.TestCase): + def test_str(self): + err = FeatureLibError("Squeak!", ("foo.fea", 23, 42)) + self.assertEqual(str(err), "foo.fea:23:42: Squeak!") + + def test_str_nolocation(self): + err = FeatureLibError("Squeak!", None) + self.assertEqual(str(err), "Squeak!") + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/feaLib/lexer_test.py b/Tests/feaLib/lexer_test.py new file mode 100644 index 0000000..27e2da6 --- /dev/null +++ b/Tests/feaLib/lexer_test.py @@ -0,0 +1,237 @@ +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.misc.py23 import * +from fontTools.feaLib.error import FeatureLibError, IncludedFeaNotFound +from fontTools.feaLib.lexer import IncludingLexer, Lexer +import os +import shutil +import tempfile +import unittest + + +def lex(s): + return [(typ, tok) for (typ, tok, _) in Lexer(s, "test.fea")] + + +class LexerTest(unittest.TestCase): + def __init__(self, methodName): + unittest.TestCase.__init__(self, methodName) + # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, + # and fires deprecation warnings if a program uses the old name. + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + + def test_empty(self): + self.assertEqual(lex(""), []) + self.assertEqual(lex(" \t "), []) + + def test_name(self): + self.assertEqual(lex("a17"), [(Lexer.NAME, "a17")]) + self.assertEqual(lex(".notdef"), [(Lexer.NAME, ".notdef")]) + self.assertEqual(lex("two.oldstyle"), [(Lexer.NAME, "two.oldstyle")]) + self.assertEqual(lex("_"), [(Lexer.NAME, "_")]) + self.assertEqual(lex("\\table"), [(Lexer.NAME, "\\table")]) + self.assertEqual(lex("a+*:^~!"), [(Lexer.NAME, "a+*:^~!")]) + self.assertEqual(lex("with-dash"), [(Lexer.NAME, "with-dash")]) + + def test_cid(self): + self.assertEqual(lex("\\0 \\987"), [(Lexer.CID, 0), (Lexer.CID, 987)]) + + def test_glyphclass(self): + self.assertEqual(lex("@Vowel.sc"), [(Lexer.GLYPHCLASS, "Vowel.sc")]) + self.assertRaisesRegex(FeatureLibError, + "Expected glyph class", lex, "@(a)") + self.assertRaisesRegex(FeatureLibError, + "Expected glyph class", lex, "@ A") + self.assertRaisesRegex(FeatureLibError, + "not be longer than 63 characters", + lex, "@" + ("A" * 64)) + self.assertRaisesRegex(FeatureLibError, + "Glyph class names must consist of", + lex, "@Ab:c") + + def test_include(self): + self.assertEqual(lex("include (~/foo/bar baz.fea);"), [ + (Lexer.NAME, "include"), + (Lexer.FILENAME, "~/foo/bar baz.fea"), + (Lexer.SYMBOL, ";") + ]) + self.assertEqual(lex("include # Comment\n (foo) \n;"), [ + (Lexer.NAME, "include"), + (Lexer.COMMENT, "# Comment"), + (Lexer.FILENAME, "foo"), + (Lexer.SYMBOL, ";") + ]) + self.assertRaises(FeatureLibError, lex, "include blah") + self.assertRaises(FeatureLibError, lex, "include (blah") + + def test_number(self): + self.assertEqual(lex("123 -456"), + [(Lexer.NUMBER, 123), (Lexer.NUMBER, -456)]) + self.assertEqual(lex("0xCAFED00D"), [(Lexer.NUMBER, 0xCAFED00D)]) + self.assertEqual(lex("0xcafed00d"), [(Lexer.NUMBER, 0xCAFED00D)]) + + def test_float(self): + self.assertEqual(lex("1.23 -4.5"), + [(Lexer.FLOAT, 1.23), (Lexer.FLOAT, -4.5)]) + + def test_symbol(self): + self.assertEqual(lex("a'"), [(Lexer.NAME, "a"), (Lexer.SYMBOL, "'")]) + self.assertEqual(lex("-A-B"), + [(Lexer.SYMBOL, "-"), (Lexer.NAME, "A-B")]) + self.assertEqual( + lex("foo - -2"), + [(Lexer.NAME, "foo"), (Lexer.SYMBOL, "-"), (Lexer.NUMBER, -2)]) + + def test_comment(self): + self.assertEqual(lex("# Comment\n#"), + [(Lexer.COMMENT, "# Comment"), (Lexer.COMMENT, "#")]) + + def test_string(self): + self.assertEqual(lex('"foo" "bar"'), + [(Lexer.STRING, "foo"), (Lexer.STRING, "bar")]) + self.assertEqual(lex('"foo \nbar\r baz \r\nqux\n\n "'), + [(Lexer.STRING, "foo bar baz qux ")]) + # The lexer should preserve escape sequences because they have + # different interpretations depending on context. For better + # or for worse, that is how the OpenType Feature File Syntax + # has been specified; see section 9.e (name table) for examples. + self.assertEqual(lex(r'"M\00fcller-Lanc\00e9"'), # 'nameid 9' + [(Lexer.STRING, r"M\00fcller-Lanc\00e9")]) + self.assertEqual(lex(r'"M\9fller-Lanc\8e"'), # 'nameid 9 1' + [(Lexer.STRING, r"M\9fller-Lanc\8e")]) + self.assertRaises(FeatureLibError, lex, '"foo\n bar') + + def test_bad_character(self): + self.assertRaises(FeatureLibError, lambda: lex("123 \u0001")) + + def test_newline(self): + def lines(s): + return [loc[1] for (_, _, loc) in Lexer(s, "test.fea")] + self.assertEqual(lines("FOO\n\nBAR\nBAZ"), [1, 3, 4]) # Unix + self.assertEqual(lines("FOO\r\rBAR\rBAZ"), [1, 3, 4]) # Macintosh + self.assertEqual(lines("FOO\r\n\r\n BAR\r\nBAZ"), [1, 3, 4]) # Windows + self.assertEqual(lines("FOO\n\rBAR\r\nBAZ"), [1, 3, 4]) # mixed + + def test_location(self): + def locs(s): + return ["%s:%d:%d" % loc for (_, _, loc) in Lexer(s, "test.fea")] + self.assertEqual(locs("a b # Comment\n12 @x"), [ + "test.fea:1:1", "test.fea:1:3", "test.fea:1:5", "test.fea:2:1", + "test.fea:2:4" + ]) + + def test_scan_over_(self): + lexer = Lexer("abbacabba12", "test.fea") + self.assertEqual(lexer.pos_, 0) + lexer.scan_over_("xyz") + self.assertEqual(lexer.pos_, 0) + lexer.scan_over_("abc") + self.assertEqual(lexer.pos_, 9) + lexer.scan_over_("abc") + self.assertEqual(lexer.pos_, 9) + lexer.scan_over_("0123456789") + self.assertEqual(lexer.pos_, 11) + + def test_scan_until_(self): + lexer = Lexer("foo'bar", "test.fea") + self.assertEqual(lexer.pos_, 0) + lexer.scan_until_("'") + self.assertEqual(lexer.pos_, 3) + lexer.scan_until_("'") + self.assertEqual(lexer.pos_, 3) + + +class IncludingLexerTest(unittest.TestCase): + @staticmethod + def getpath(filename): + path, _ = os.path.split(__file__) + return os.path.join(path, "data", filename) + + def test_include(self): + lexer = IncludingLexer(self.getpath("include/include4.fea")) + result = ['%s %s:%d' % (token, os.path.split(loc[0])[1], loc[1]) + for _, token, loc in lexer] + self.assertEqual(result, [ + "I4a include4.fea:1", + "I3a include3.fea:1", + "I2a include2.fea:1", + "I1a include1.fea:1", + "I0 include0.fea:1", + "I1b include1.fea:3", + "; include2.fea:2", + "I2b include2.fea:3", + "; include3.fea:2", + "I3b include3.fea:3", + "; include4.fea:2", + "I4b include4.fea:3" + ]) + + def test_include_limit(self): + lexer = IncludingLexer(self.getpath("include/include6.fea")) + self.assertRaises(FeatureLibError, lambda: list(lexer)) + + def test_include_self(self): + lexer = IncludingLexer(self.getpath("include/includeself.fea")) + self.assertRaises(FeatureLibError, lambda: list(lexer)) + + def test_include_missing_file(self): + lexer = IncludingLexer(self.getpath("include/includemissingfile.fea")) + self.assertRaisesRegex(IncludedFeaNotFound, + "includemissingfile.fea:1:8: missingfile.fea", + lambda: list(lexer)) + + def test_featurefilepath_None(self): + lexer = IncludingLexer(UnicodeIO("# foobar")) + self.assertIsNone(lexer.featurefilepath) + files = set(loc[0] for _, _, loc in lexer) + self.assertIn("<features>", files) + + def test_include_absolute_path(self): + with tempfile.NamedTemporaryFile(delete=False) as included: + included.write(tobytes(""" + feature kern { + pos A B -40; + } kern; + """, encoding="utf-8")) + including = UnicodeIO("include(%s);" % included.name) + try: + lexer = IncludingLexer(including) + files = set(loc[0] for _, _, loc in lexer) + self.assertIn(included.name, files) + finally: + os.remove(included.name) + + def test_include_relative_to_cwd(self): + # save current working directory, to be restored later + cwd = os.getcwd() + tmpdir = tempfile.mkdtemp() + try: + # create new feature file in a temporary directory + with open(os.path.join(tmpdir, "included.fea"), "w", + encoding="utf-8") as included: + included.write(""" + feature kern { + pos A B -40; + } kern; + """) + # change current folder to the temporary dir + os.chdir(tmpdir) + # instantiate a new lexer that includes the above file + # using a relative path; the IncludingLexer does not + # itself have a path, because it was initialized from + # an in-memory stream, so it will use the current working + # directory to resolve relative include statements + lexer = IncludingLexer(UnicodeIO("include(included.fea);")) + files = set(loc[0] for _, _, loc in lexer) + expected = os.path.realpath(included.name) + self.assertIn(expected, files) + finally: + # remove temporary folder and restore previous working directory + os.chdir(cwd) + shutil.rmtree(tmpdir) + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/feaLib/parser_test.py b/Tests/feaLib/parser_test.py new file mode 100644 index 0000000..91744f0 --- /dev/null +++ b/Tests/feaLib/parser_test.py @@ -0,0 +1,1623 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.feaLib.error import FeatureLibError +from fontTools.feaLib.parser import Parser, SymbolTable +from fontTools.misc.py23 import * +import warnings +import fontTools.feaLib.ast as ast +import os +import unittest + + +def glyphstr(glyphs): + def f(x): + if len(x) == 1: + return list(x)[0] + else: + return '[%s]' % ' '.join(sorted(list(x))) + return ' '.join(f(g.glyphSet()) for g in glyphs) + + +def mapping(s): + b = [] + for a in s.glyphs: + b.extend(a.glyphSet()) + c = [] + for a in s.replacements: + c.extend(a.glyphSet()) + if len(c) == 1: + c = c * len(b) + return dict(zip(b, c)) + + +GLYPHNAMES = (""" + .notdef space A B C D E F G H I J K L M N O P Q R S T U V W X Y Z + A.sc B.sc C.sc D.sc E.sc F.sc G.sc H.sc I.sc J.sc K.sc L.sc M.sc + N.sc O.sc P.sc Q.sc R.sc S.sc T.sc U.sc V.sc W.sc X.sc Y.sc Z.sc + A.swash B.swash X.swash Y.swash Z.swash + a b c d e f g h i j k l m n o p q r s t u v w x y z + a.sc b.sc c.sc d.sc e.sc f.sc g.sc h.sc i.sc j.sc k.sc l.sc m.sc + n.sc o.sc p.sc q.sc r.sc s.sc t.sc u.sc v.sc w.sc x.sc y.sc z.sc + a.swash b.swash x.swash y.swash z.swash + foobar foo.09 foo.1234 foo.9876 +""").split() + ["foo.%d" % i for i in range(1, 200)] + + +class ParserTest(unittest.TestCase): + def __init__(self, methodName): + unittest.TestCase.__init__(self, methodName) + # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, + # and fires deprecation warnings if a program uses the old name. + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + + def test_glyphMap_deprecated(self): + glyphMap = {'a': 0, 'b': 1, 'c': 2} + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + parser = Parser(UnicodeIO(), glyphMap=glyphMap) + + self.assertEqual(len(w), 1) + self.assertEqual(w[-1].category, UserWarning) + self.assertIn("deprecated", str(w[-1].message)) + self.assertEqual(parser.glyphNames_, {'a', 'b', 'c'}) + + self.assertRaisesRegex( + TypeError, "mutually exclusive", + Parser, UnicodeIO(), ("a",), glyphMap={"a": 0}) + + self.assertRaisesRegex( + TypeError, "unsupported keyword argument", + Parser, UnicodeIO(), foo="bar") + + def test_comments(self): + doc = self.parse( + """ # Initial + feature test { + sub A by B; # simple + } test;""") + c1 = doc.statements[0] + c2 = doc.statements[1].statements[1] + self.assertEqual(type(c1), ast.Comment) + self.assertEqual(c1.text, "# Initial") + self.assertEqual(str(c1), "# Initial") + self.assertEqual(type(c2), ast.Comment) + self.assertEqual(c2.text, "# simple") + self.assertEqual(doc.statements[1].name, "test") + + def test_only_comments(self): + doc = self.parse("""\ + # Initial + """) + c1 = doc.statements[0] + self.assertEqual(type(c1), ast.Comment) + self.assertEqual(c1.text, "# Initial") + self.assertEqual(str(c1), "# Initial") + + def test_anchor_format_a(self): + doc = self.parse( + "feature test {" + " pos cursive A <anchor 120 -20> <anchor NULL>;" + "} test;") + anchor = doc.statements[0].statements[0].entryAnchor + self.assertEqual(type(anchor), ast.Anchor) + self.assertEqual(anchor.x, 120) + self.assertEqual(anchor.y, -20) + self.assertIsNone(anchor.contourpoint) + self.assertIsNone(anchor.xDeviceTable) + self.assertIsNone(anchor.yDeviceTable) + + def test_anchor_format_b(self): + doc = self.parse( + "feature test {" + " pos cursive A <anchor 120 -20 contourpoint 5> <anchor NULL>;" + "} test;") + anchor = doc.statements[0].statements[0].entryAnchor + self.assertEqual(type(anchor), ast.Anchor) + self.assertEqual(anchor.x, 120) + self.assertEqual(anchor.y, -20) + self.assertEqual(anchor.contourpoint, 5) + self.assertIsNone(anchor.xDeviceTable) + self.assertIsNone(anchor.yDeviceTable) + + def test_anchor_format_c(self): + doc = self.parse( + "feature test {" + " pos cursive A " + " <anchor 120 -20 <device 11 111, 12 112> <device NULL>>" + " <anchor NULL>;" + "} test;") + anchor = doc.statements[0].statements[0].entryAnchor + self.assertEqual(type(anchor), ast.Anchor) + self.assertEqual(anchor.x, 120) + self.assertEqual(anchor.y, -20) + self.assertIsNone(anchor.contourpoint) + self.assertEqual(anchor.xDeviceTable, ((11, 111), (12, 112))) + self.assertIsNone(anchor.yDeviceTable) + + def test_anchor_format_d(self): + doc = self.parse( + "feature test {" + " pos cursive A <anchor 120 -20> <anchor NULL>;" + "} test;") + anchor = doc.statements[0].statements[0].exitAnchor + self.assertIsNone(anchor) + + def test_anchor_format_e(self): + doc = self.parse( + "feature test {" + " anchorDef 120 -20 contourpoint 7 Foo;" + " pos cursive A <anchor Foo> <anchor NULL>;" + "} test;") + anchor = doc.statements[0].statements[1].entryAnchor + self.assertEqual(type(anchor), ast.Anchor) + self.assertEqual(anchor.x, 120) + self.assertEqual(anchor.y, -20) + self.assertEqual(anchor.contourpoint, 7) + self.assertIsNone(anchor.xDeviceTable) + self.assertIsNone(anchor.yDeviceTable) + + def test_anchor_format_e_undefined(self): + self.assertRaisesRegex( + FeatureLibError, 'Unknown anchor "UnknownName"', self.parse, + "feature test {" + " position cursive A <anchor UnknownName> <anchor NULL>;" + "} test;") + + def test_anchordef(self): + [foo] = self.parse("anchorDef 123 456 foo;").statements + self.assertEqual(type(foo), ast.AnchorDefinition) + self.assertEqual(foo.name, "foo") + self.assertEqual(foo.x, 123) + self.assertEqual(foo.y, 456) + self.assertEqual(foo.contourpoint, None) + + def test_anchordef_contourpoint(self): + [foo] = self.parse("anchorDef 123 456 contourpoint 5 foo;").statements + self.assertEqual(type(foo), ast.AnchorDefinition) + self.assertEqual(foo.name, "foo") + self.assertEqual(foo.x, 123) + self.assertEqual(foo.y, 456) + self.assertEqual(foo.contourpoint, 5) + + def test_anon(self): + anon = self.parse("anon TEST { # a\nfoo\n } TEST; # qux").statements[0] + self.assertIsInstance(anon, ast.AnonymousBlock) + self.assertEqual(anon.tag, "TEST") + self.assertEqual(anon.content, "foo\n ") + + def test_anonymous(self): + anon = self.parse("anonymous TEST {\nbar\n} TEST;").statements[0] + self.assertIsInstance(anon, ast.AnonymousBlock) + self.assertEqual(anon.tag, "TEST") + # feature file spec requires passing the final end-of-line + self.assertEqual(anon.content, "bar\n") + + def test_anon_missingBrace(self): + self.assertRaisesRegex( + FeatureLibError, "Expected '} TEST;' to terminate anonymous block", + self.parse, "anon TEST { \n no end in sight") + + def test_attach(self): + doc = self.parse("table GDEF {Attach [a e] 2;} GDEF;") + s = doc.statements[0].statements[0] + self.assertIsInstance(s, ast.AttachStatement) + self.assertEqual(glyphstr([s.glyphs]), "[a e]") + self.assertEqual(s.contourPoints, {2}) + + def test_feature_block(self): + [liga] = self.parse("feature liga {} liga;").statements + self.assertEqual(liga.name, "liga") + self.assertFalse(liga.use_extension) + + def test_feature_block_useExtension(self): + [liga] = self.parse("feature liga useExtension {} liga;").statements + self.assertEqual(liga.name, "liga") + self.assertTrue(liga.use_extension) + + def test_feature_comment(self): + [liga] = self.parse("feature liga { # Comment\n } liga;").statements + [comment] = liga.statements + self.assertIsInstance(comment, ast.Comment) + self.assertEqual(comment.text, "# Comment") + + def test_feature_reference(self): + doc = self.parse("feature aalt { feature salt; } aalt;") + ref = doc.statements[0].statements[0] + self.assertIsInstance(ref, ast.FeatureReferenceStatement) + self.assertEqual(ref.featureName, "salt") + + def test_FeatureNames_bad(self): + self.assertRaisesRegex( + FeatureLibError, 'Expected "name"', + self.parse, "feature ss01 { featureNames { feature test; } ss01;") + + def test_FeatureNames_comment(self): + [feature] = self.parse( + "feature ss01 { featureNames { # Comment\n }; } ss01;").statements + [featureNames] = feature.statements + self.assertIsInstance(featureNames, ast.NestedBlock) + [comment] = featureNames.statements + self.assertIsInstance(comment, ast.Comment) + self.assertEqual(comment.text, "# Comment") + + def test_FeatureNames_emptyStatements(self): + [feature] = self.parse( + "feature ss01 { featureNames { ;;; }; } ss01;").statements + [featureNames] = feature.statements + self.assertIsInstance(featureNames, ast.NestedBlock) + self.assertEqual(featureNames.statements, []) + + def test_FontRevision(self): + doc = self.parse("table head {FontRevision 2.5;} head;") + s = doc.statements[0].statements[0] + self.assertIsInstance(s, ast.FontRevisionStatement) + self.assertEqual(s.revision, 2.5) + + def test_FontRevision_negative(self): + self.assertRaisesRegex( + FeatureLibError, "Font revision numbers must be positive", + self.parse, "table head {FontRevision -17.2;} head;") + + def test_glyphclass(self): + [gc] = self.parse("@dash = [endash emdash figuredash];").statements + self.assertEqual(gc.name, "dash") + self.assertEqual(gc.glyphSet(), ("endash", "emdash", "figuredash")) + + def test_glyphclass_glyphNameTooLong(self): + self.assertRaisesRegex( + FeatureLibError, "must not be longer than 63 characters", + self.parse, "@GlyphClass = [%s];" % ("G" * 64)) + + def test_glyphclass_bad(self): + self.assertRaisesRegex( + FeatureLibError, + "Expected glyph name, glyph range, or glyph class reference", + self.parse, "@bad = [a 123];") + + def test_glyphclass_duplicate(self): + # makeotf accepts this, so we should too + ab, xy = self.parse("@dup = [a b]; @dup = [x y];").statements + self.assertEqual(glyphstr([ab]), "[a b]") + self.assertEqual(glyphstr([xy]), "[x y]") + + def test_glyphclass_empty(self): + [gc] = self.parse("@empty_set = [];").statements + self.assertEqual(gc.name, "empty_set") + self.assertEqual(gc.glyphSet(), tuple()) + + def test_glyphclass_equality(self): + [foo, bar] = self.parse("@foo = [a b]; @bar = @foo;").statements + self.assertEqual(foo.glyphSet(), ("a", "b")) + self.assertEqual(bar.glyphSet(), ("a", "b")) + + def test_glyphclass_from_markClass(self): + doc = self.parse( + "markClass [acute grave] <anchor 500 800> @TOP_MARKS;" + "markClass cedilla <anchor 500 -100> @BOTTOM_MARKS;" + "@MARKS = [@TOP_MARKS @BOTTOM_MARKS ogonek];" + "@ALL = @MARKS;") + self.assertEqual(doc.statements[-1].glyphSet(), + ("acute", "grave", "cedilla", "ogonek")) + + def test_glyphclass_range_cid(self): + [gc] = self.parse(r"@GlyphClass = [\999-\1001];").statements + self.assertEqual(gc.name, "GlyphClass") + self.assertEqual(gc.glyphSet(), ("cid00999", "cid01000", "cid01001")) + + def test_glyphclass_range_cid_bad(self): + self.assertRaisesRegex( + FeatureLibError, + "Bad range: start should be less than limit", + self.parse, r"@bad = [\998-\995];") + + def test_glyphclass_range_uppercase(self): + [gc] = self.parse("@swashes = [X.swash-Z.swash];").statements + self.assertEqual(gc.name, "swashes") + self.assertEqual(gc.glyphSet(), ("X.swash", "Y.swash", "Z.swash")) + + def test_glyphclass_range_lowercase(self): + [gc] = self.parse("@defg.sc = [d.sc-g.sc];").statements + self.assertEqual(gc.name, "defg.sc") + self.assertEqual(gc.glyphSet(), ("d.sc", "e.sc", "f.sc", "g.sc")) + + def test_glyphclass_range_dash(self): + glyphNames = "A-foo.sc B-foo.sc C-foo.sc".split() + [gc] = self.parse("@range = [A-foo.sc-C-foo.sc];", glyphNames).statements + self.assertEqual(gc.glyphSet(), ("A-foo.sc", "B-foo.sc", "C-foo.sc")) + + def test_glyphclass_range_dash_with_space(self): + gn = "A-foo.sc B-foo.sc C-foo.sc".split() + [gc] = self.parse("@range = [A-foo.sc - C-foo.sc];", gn).statements + self.assertEqual(gc.glyphSet(), ("A-foo.sc", "B-foo.sc", "C-foo.sc")) + + def test_glyphclass_glyph_name_should_win_over_range(self): + # The OpenType Feature File Specification v1.20 makes it clear + # that if a dashed name could be interpreted either as a glyph name + # or as a range, then the semantics should be the single dashed name. + glyphNames = ( + "A-foo.sc-C-foo.sc A-foo.sc B-foo.sc C-foo.sc".split()) + [gc] = self.parse("@range = [A-foo.sc-C-foo.sc];", glyphNames).statements + self.assertEqual(gc.glyphSet(), ("A-foo.sc-C-foo.sc",)) + + def test_glyphclass_range_dash_ambiguous(self): + glyphNames = "A B C A-B B-C".split() + self.assertRaisesRegex( + FeatureLibError, + 'Ambiguous glyph range "A-B-C"; ' + 'please use "A - B-C" or "A-B - C" to clarify what you mean', + self.parse, r"@bad = [A-B-C];", glyphNames) + + def test_glyphclass_range_digit1(self): + [gc] = self.parse("@range = [foo.2-foo.5];").statements + self.assertEqual(gc.glyphSet(), ("foo.2", "foo.3", "foo.4", "foo.5")) + + def test_glyphclass_range_digit2(self): + [gc] = self.parse("@range = [foo.09-foo.11];").statements + self.assertEqual(gc.glyphSet(), ("foo.09", "foo.10", "foo.11")) + + def test_glyphclass_range_digit3(self): + [gc] = self.parse("@range = [foo.123-foo.125];").statements + self.assertEqual(gc.glyphSet(), ("foo.123", "foo.124", "foo.125")) + + def test_glyphclass_range_bad(self): + self.assertRaisesRegex( + FeatureLibError, + "Bad range: \"a\" and \"foobar\" should have the same length", + self.parse, "@bad = [a-foobar];") + self.assertRaisesRegex( + FeatureLibError, "Bad range: \"A.swash-z.swash\"", + self.parse, "@bad = [A.swash-z.swash];") + self.assertRaisesRegex( + FeatureLibError, "Start of range must be smaller than its end", + self.parse, "@bad = [B.swash-A.swash];") + self.assertRaisesRegex( + FeatureLibError, "Bad range: \"foo.1234-foo.9876\"", + self.parse, "@bad = [foo.1234-foo.9876];") + + def test_glyphclass_range_mixed(self): + [gc] = self.parse("@range = [a foo.09-foo.11 X.sc-Z.sc];").statements + self.assertEqual(gc.glyphSet(), ( + "a", "foo.09", "foo.10", "foo.11", "X.sc", "Y.sc", "Z.sc" + )) + + def test_glyphclass_reference(self): + [vowels_lc, vowels_uc, vowels] = self.parse( + "@Vowels.lc = [a e i o u]; @Vowels.uc = [A E I O U];" + "@Vowels = [@Vowels.lc @Vowels.uc y Y];").statements + self.assertEqual(vowels_lc.glyphSet(), tuple("aeiou")) + self.assertEqual(vowels_uc.glyphSet(), tuple("AEIOU")) + self.assertEqual(vowels.glyphSet(), tuple("aeiouAEIOUyY")) + self.assertEqual(vowels.asFea(), + "@Vowels = [@Vowels.lc @Vowels.uc y Y];") + self.assertRaisesRegex( + FeatureLibError, "Unknown glyph class @unknown", + self.parse, "@bad = [@unknown];") + + def test_glyphclass_scoping(self): + [foo, liga, smcp] = self.parse( + "@foo = [a b];" + "feature liga { @bar = [@foo l]; } liga;" + "feature smcp { @bar = [@foo s]; } smcp;" + ).statements + self.assertEqual(foo.glyphSet(), ("a", "b")) + self.assertEqual(liga.statements[0].glyphSet(), ("a", "b", "l")) + self.assertEqual(smcp.statements[0].glyphSet(), ("a", "b", "s")) + + def test_glyphclass_scoping_bug496(self): + # https://github.com/behdad/fonttools/issues/496 + f1, f2 = self.parse( + "feature F1 { lookup L { @GLYPHCLASS = [A B C];} L; } F1;" + "feature F2 { sub @GLYPHCLASS by D; } F2;" + ).statements + self.assertEqual(list(f2.statements[0].glyphs[0].glyphSet()), + ["A", "B", "C"]) + + def test_GlyphClassDef(self): + doc = self.parse("table GDEF {GlyphClassDef [b],[l],[m],[C c];} GDEF;") + s = doc.statements[0].statements[0] + self.assertIsInstance(s, ast.GlyphClassDefStatement) + self.assertEqual(glyphstr([s.baseGlyphs]), "b") + self.assertEqual(glyphstr([s.ligatureGlyphs]), "l") + self.assertEqual(glyphstr([s.markGlyphs]), "m") + self.assertEqual(glyphstr([s.componentGlyphs]), "[C c]") + + def test_GlyphClassDef_noCLassesSpecified(self): + doc = self.parse("table GDEF {GlyphClassDef ,,,;} GDEF;") + s = doc.statements[0].statements[0] + self.assertIsNone(s.baseGlyphs) + self.assertIsNone(s.ligatureGlyphs) + self.assertIsNone(s.markGlyphs) + self.assertIsNone(s.componentGlyphs) + + def test_ignore_pos(self): + doc = self.parse("feature test {ignore pos e t' c, q u' u' x;} test;") + sub = doc.statements[0].statements[0] + self.assertIsInstance(sub, ast.IgnorePosStatement) + [(pref1, glyphs1, suff1), (pref2, glyphs2, suff2)] = sub.chainContexts + self.assertEqual(glyphstr(pref1), "e") + self.assertEqual(glyphstr(glyphs1), "t") + self.assertEqual(glyphstr(suff1), "c") + self.assertEqual(glyphstr(pref2), "q") + self.assertEqual(glyphstr(glyphs2), "u u") + self.assertEqual(glyphstr(suff2), "x") + + def test_ignore_position(self): + doc = self.parse( + "feature test {" + " ignore position f [a e] d' [a u]' [e y];" + "} test;") + sub = doc.statements[0].statements[0] + self.assertIsInstance(sub, ast.IgnorePosStatement) + [(prefix, glyphs, suffix)] = sub.chainContexts + self.assertEqual(glyphstr(prefix), "f [a e]") + self.assertEqual(glyphstr(glyphs), "d [a u]") + self.assertEqual(glyphstr(suffix), "[e y]") + + def test_ignore_position_with_lookup(self): + self.assertRaisesRegex( + FeatureLibError, + 'No lookups can be specified for "ignore pos"', + self.parse, + "lookup L { pos [A A.sc] -100; } L;" + "feature test { ignore pos f' i', A' lookup L; } test;") + + def test_ignore_sub(self): + doc = self.parse("feature test {ignore sub e t' c, q u' u' x;} test;") + sub = doc.statements[0].statements[0] + self.assertIsInstance(sub, ast.IgnoreSubstStatement) + [(pref1, glyphs1, suff1), (pref2, glyphs2, suff2)] = sub.chainContexts + self.assertEqual(glyphstr(pref1), "e") + self.assertEqual(glyphstr(glyphs1), "t") + self.assertEqual(glyphstr(suff1), "c") + self.assertEqual(glyphstr(pref2), "q") + self.assertEqual(glyphstr(glyphs2), "u u") + self.assertEqual(glyphstr(suff2), "x") + + def test_ignore_substitute(self): + doc = self.parse( + "feature test {" + " ignore substitute f [a e] d' [a u]' [e y];" + "} test;") + sub = doc.statements[0].statements[0] + self.assertIsInstance(sub, ast.IgnoreSubstStatement) + [(prefix, glyphs, suffix)] = sub.chainContexts + self.assertEqual(glyphstr(prefix), "f [a e]") + self.assertEqual(glyphstr(glyphs), "d [a u]") + self.assertEqual(glyphstr(suffix), "[e y]") + + def test_ignore_substitute_with_lookup(self): + self.assertRaisesRegex( + FeatureLibError, + 'No lookups can be specified for "ignore sub"', + self.parse, + "lookup L { sub [A A.sc] by a; } L;" + "feature test { ignore sub f' i', A' lookup L; } test;") + + def test_include_statement(self): + doc = self.parse("""\ + include(../family.fea); + include # Comment + (foo) + ; + """, followIncludes=False) + s1, s2, s3 = doc.statements + self.assertEqual(type(s1), ast.IncludeStatement) + self.assertEqual(s1.filename, "../family.fea") + self.assertEqual(s1.asFea(), "include(../family.fea);") + self.assertEqual(type(s2), ast.IncludeStatement) + self.assertEqual(s2.filename, "foo") + self.assertEqual(s2.asFea(), "include(foo);") + self.assertEqual(type(s3), ast.Comment) + self.assertEqual(s3.text, "# Comment") + + def test_include_statement_no_semicolon(self): + doc = self.parse("""\ + include(../family.fea) + """, followIncludes=False) + s1 = doc.statements[0] + self.assertEqual(type(s1), ast.IncludeStatement) + self.assertEqual(s1.filename, "../family.fea") + self.assertEqual(s1.asFea(), "include(../family.fea);") + + def test_language(self): + doc = self.parse("feature test {language DEU;} test;") + s = doc.statements[0].statements[0] + self.assertEqual(type(s), ast.LanguageStatement) + self.assertEqual(s.language, "DEU ") + self.assertTrue(s.include_default) + self.assertFalse(s.required) + + def test_language_exclude_dflt(self): + doc = self.parse("feature test {language DEU exclude_dflt;} test;") + s = doc.statements[0].statements[0] + self.assertEqual(type(s), ast.LanguageStatement) + self.assertEqual(s.language, "DEU ") + self.assertFalse(s.include_default) + self.assertFalse(s.required) + + def test_language_exclude_dflt_required(self): + doc = self.parse("feature test {" + " language DEU exclude_dflt required;" + "} test;") + s = doc.statements[0].statements[0] + self.assertEqual(type(s), ast.LanguageStatement) + self.assertEqual(s.language, "DEU ") + self.assertFalse(s.include_default) + self.assertTrue(s.required) + + def test_language_include_dflt(self): + doc = self.parse("feature test {language DEU include_dflt;} test;") + s = doc.statements[0].statements[0] + self.assertEqual(type(s), ast.LanguageStatement) + self.assertEqual(s.language, "DEU ") + self.assertTrue(s.include_default) + self.assertFalse(s.required) + + def test_language_include_dflt_required(self): + doc = self.parse("feature test {" + " language DEU include_dflt required;" + "} test;") + s = doc.statements[0].statements[0] + self.assertEqual(type(s), ast.LanguageStatement) + self.assertEqual(s.language, "DEU ") + self.assertTrue(s.include_default) + self.assertTrue(s.required) + + def test_language_DFLT(self): + self.assertRaisesRegex( + FeatureLibError, + '"DFLT" is not a valid language tag; use "dflt" instead', + self.parse, "feature test { language DFLT; } test;") + + def test_ligatureCaretByIndex_glyphClass(self): + doc = self.parse("table GDEF{LigatureCaretByIndex [c_t f_i] 2;}GDEF;") + s = doc.statements[0].statements[0] + self.assertIsInstance(s, ast.LigatureCaretByIndexStatement) + self.assertEqual(glyphstr([s.glyphs]), "[c_t f_i]") + self.assertEqual(s.carets, [2]) + + def test_ligatureCaretByIndex_singleGlyph(self): + doc = self.parse("table GDEF{LigatureCaretByIndex f_f_i 3 7;}GDEF;") + s = doc.statements[0].statements[0] + self.assertIsInstance(s, ast.LigatureCaretByIndexStatement) + self.assertEqual(glyphstr([s.glyphs]), "f_f_i") + self.assertEqual(s.carets, [3, 7]) + + def test_ligatureCaretByPos_glyphClass(self): + doc = self.parse("table GDEF {LigatureCaretByPos [c_t f_i] 7;} GDEF;") + s = doc.statements[0].statements[0] + self.assertIsInstance(s, ast.LigatureCaretByPosStatement) + self.assertEqual(glyphstr([s.glyphs]), "[c_t f_i]") + self.assertEqual(s.carets, [7]) + + def test_ligatureCaretByPos_singleGlyph(self): + doc = self.parse("table GDEF {LigatureCaretByPos f_i 400 380;} GDEF;") + s = doc.statements[0].statements[0] + self.assertIsInstance(s, ast.LigatureCaretByPosStatement) + self.assertEqual(glyphstr([s.glyphs]), "f_i") + self.assertEqual(s.carets, [400, 380]) + + def test_lookup_block(self): + [lookup] = self.parse("lookup Ligatures {} Ligatures;").statements + self.assertEqual(lookup.name, "Ligatures") + self.assertFalse(lookup.use_extension) + + def test_lookup_block_useExtension(self): + [lookup] = self.parse("lookup Foo useExtension {} Foo;").statements + self.assertEqual(lookup.name, "Foo") + self.assertTrue(lookup.use_extension) + + def test_lookup_block_name_mismatch(self): + self.assertRaisesRegex( + FeatureLibError, 'Expected "Foo"', + self.parse, "lookup Foo {} Bar;") + + def test_lookup_block_with_horizontal_valueRecordDef(self): + doc = self.parse("feature liga {" + " lookup look {" + " valueRecordDef 123 foo;" + " } look;" + "} liga;") + [liga] = doc.statements + [look] = liga.statements + [foo] = look.statements + self.assertEqual(foo.value.xAdvance, 123) + self.assertIsNone(foo.value.yAdvance) + + def test_lookup_block_with_vertical_valueRecordDef(self): + doc = self.parse("feature vkrn {" + " lookup look {" + " valueRecordDef 123 foo;" + " } look;" + "} vkrn;") + [vkrn] = doc.statements + [look] = vkrn.statements + [foo] = look.statements + self.assertIsNone(foo.value.xAdvance) + self.assertEqual(foo.value.yAdvance, 123) + + def test_lookup_comment(self): + [lookup] = self.parse("lookup L { # Comment\n } L;").statements + [comment] = lookup.statements + self.assertIsInstance(comment, ast.Comment) + self.assertEqual(comment.text, "# Comment") + + def test_lookup_reference(self): + [foo, bar] = self.parse("lookup Foo {} Foo;" + "feature Bar {lookup Foo;} Bar;").statements + [ref] = bar.statements + self.assertEqual(type(ref), ast.LookupReferenceStatement) + self.assertEqual(ref.lookup, foo) + + def test_lookup_reference_to_lookup_inside_feature(self): + [qux, bar] = self.parse("feature Qux {lookup Foo {} Foo;} Qux;" + "feature Bar {lookup Foo;} Bar;").statements + [foo] = qux.statements + [ref] = bar.statements + self.assertIsInstance(ref, ast.LookupReferenceStatement) + self.assertEqual(ref.lookup, foo) + + def test_lookup_reference_unknown(self): + self.assertRaisesRegex( + FeatureLibError, 'Unknown lookup "Huh"', + self.parse, "feature liga {lookup Huh;} liga;") + + def parse_lookupflag_(self, s): + return self.parse("lookup L {%s} L;" % s).statements[0].statements[-1] + + def test_lookupflag_format_A(self): + flag = self.parse_lookupflag_("lookupflag RightToLeft IgnoreMarks;") + self.assertIsInstance(flag, ast.LookupFlagStatement) + self.assertEqual(flag.value, 9) + self.assertIsNone(flag.markAttachment) + self.assertIsNone(flag.markFilteringSet) + + def test_lookupflag_format_A_MarkAttachmentType(self): + flag = self.parse_lookupflag_( + "@TOP_MARKS = [acute grave macron];" + "lookupflag RightToLeft MarkAttachmentType @TOP_MARKS;") + self.assertIsInstance(flag, ast.LookupFlagStatement) + self.assertEqual(flag.value, 1) + self.assertIsInstance(flag.markAttachment, ast.GlyphClassName) + self.assertEqual(flag.markAttachment.glyphSet(), + ("acute", "grave", "macron")) + self.assertIsNone(flag.markFilteringSet) + + def test_lookupflag_format_A_UseMarkFilteringSet(self): + flag = self.parse_lookupflag_( + "@BOTTOM_MARKS = [cedilla ogonek];" + "lookupflag UseMarkFilteringSet @BOTTOM_MARKS IgnoreLigatures;") + self.assertIsInstance(flag, ast.LookupFlagStatement) + self.assertEqual(flag.value, 4) + self.assertIsNone(flag.markAttachment) + self.assertIsInstance(flag.markFilteringSet, ast.GlyphClassName) + self.assertEqual(flag.markFilteringSet.glyphSet(), + ("cedilla", "ogonek")) + + def test_lookupflag_format_B(self): + flag = self.parse_lookupflag_("lookupflag 7;") + self.assertIsInstance(flag, ast.LookupFlagStatement) + self.assertEqual(flag.value, 7) + self.assertIsNone(flag.markAttachment) + self.assertIsNone(flag.markFilteringSet) + + def test_lookupflag_repeated(self): + self.assertRaisesRegex( + FeatureLibError, + 'RightToLeft can be specified only once', + self.parse, + "feature test {lookupflag RightToLeft RightToLeft;} test;") + + def test_lookupflag_unrecognized(self): + self.assertRaisesRegex( + FeatureLibError, + '"IgnoreCookies" is not a recognized lookupflag', + self.parse, "feature test {lookupflag IgnoreCookies;} test;") + + def test_gpos_type_1_glyph(self): + doc = self.parse("feature kern {pos one <1 2 3 4>;} kern;") + pos = doc.statements[0].statements[0] + self.assertIsInstance(pos, ast.SinglePosStatement) + [(glyphs, value)] = pos.pos + self.assertEqual(glyphstr([glyphs]), "one") + self.assertEqual(value.makeString(vertical=False), "<1 2 3 4>") + + def test_gpos_type_1_glyphclass_horizontal(self): + doc = self.parse("feature kern {pos [one two] -300;} kern;") + pos = doc.statements[0].statements[0] + self.assertIsInstance(pos, ast.SinglePosStatement) + [(glyphs, value)] = pos.pos + self.assertEqual(glyphstr([glyphs]), "[one two]") + self.assertEqual(value.makeString(vertical=False), "-300") + + def test_gpos_type_1_glyphclass_vertical(self): + doc = self.parse("feature vkrn {pos [one two] -300;} vkrn;") + pos = doc.statements[0].statements[0] + self.assertIsInstance(pos, ast.SinglePosStatement) + [(glyphs, value)] = pos.pos + self.assertEqual(glyphstr([glyphs]), "[one two]") + self.assertEqual(value.makeString(vertical=True), "-300") + + def test_gpos_type_1_multiple(self): + doc = self.parse("feature f {pos one'1 two'2 [five six]'56;} f;") + pos = doc.statements[0].statements[0] + self.assertIsInstance(pos, ast.SinglePosStatement) + [(glyphs1, val1), (glyphs2, val2), (glyphs3, val3)] = pos.pos + self.assertEqual(glyphstr([glyphs1]), "one") + self.assertEqual(val1.makeString(vertical=False), "1") + self.assertEqual(glyphstr([glyphs2]), "two") + self.assertEqual(val2.makeString(vertical=False), "2") + self.assertEqual(glyphstr([glyphs3]), "[five six]") + self.assertEqual(val3.makeString(vertical=False), "56") + self.assertEqual(pos.prefix, []) + self.assertEqual(pos.suffix, []) + + def test_gpos_type_1_enumerated(self): + self.assertRaisesRegex( + FeatureLibError, + '"enumerate" is only allowed with pair positionings', + self.parse, "feature test {enum pos T 100;} test;") + self.assertRaisesRegex( + FeatureLibError, + '"enumerate" is only allowed with pair positionings', + self.parse, "feature test {enumerate pos T 100;} test;") + + def test_gpos_type_1_chained(self): + doc = self.parse("feature kern {pos [A B] [T Y]' 20 comma;} kern;") + pos = doc.statements[0].statements[0] + self.assertIsInstance(pos, ast.SinglePosStatement) + [(glyphs, value)] = pos.pos + self.assertEqual(glyphstr([glyphs]), "[T Y]") + self.assertEqual(value.makeString(vertical=False), "20") + self.assertEqual(glyphstr(pos.prefix), "[A B]") + self.assertEqual(glyphstr(pos.suffix), "comma") + + def test_gpos_type_2_format_a(self): + doc = self.parse("feature kern {" + " pos [T V] -60 [a b c] <1 2 3 4>;" + "} kern;") + pos = doc.statements[0].statements[0] + self.assertEqual(type(pos), ast.PairPosStatement) + self.assertFalse(pos.enumerated) + self.assertEqual(glyphstr([pos.glyphs1]), "[T V]") + self.assertEqual(pos.valuerecord1.makeString(vertical=False), "-60") + self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]") + self.assertEqual(pos.valuerecord2.makeString(vertical=False), + "<1 2 3 4>") + + def test_gpos_type_2_format_a_enumerated(self): + doc = self.parse("feature kern {" + " enum pos [T V] -60 [a b c] <1 2 3 4>;" + "} kern;") + pos = doc.statements[0].statements[0] + self.assertEqual(type(pos), ast.PairPosStatement) + self.assertTrue(pos.enumerated) + self.assertEqual(glyphstr([pos.glyphs1]), "[T V]") + self.assertEqual(pos.valuerecord1.makeString(vertical=False), "-60") + self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]") + self.assertEqual(pos.valuerecord2.makeString(vertical=False), + "<1 2 3 4>") + + def test_gpos_type_2_format_a_with_null(self): + doc = self.parse("feature kern {" + " pos [T V] <1 2 3 4> [a b c] <NULL>;" + "} kern;") + pos = doc.statements[0].statements[0] + self.assertEqual(type(pos), ast.PairPosStatement) + self.assertFalse(pos.enumerated) + self.assertEqual(glyphstr([pos.glyphs1]), "[T V]") + self.assertEqual(pos.valuerecord1.makeString(vertical=False), + "<1 2 3 4>") + self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]") + self.assertIsNone(pos.valuerecord2) + + def test_gpos_type_2_format_b(self): + doc = self.parse("feature kern {" + " pos [T V] [a b c] <1 2 3 4>;" + "} kern;") + pos = doc.statements[0].statements[0] + self.assertEqual(type(pos), ast.PairPosStatement) + self.assertFalse(pos.enumerated) + self.assertEqual(glyphstr([pos.glyphs1]), "[T V]") + self.assertEqual(pos.valuerecord1.makeString(vertical=False), + "<1 2 3 4>") + self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]") + self.assertIsNone(pos.valuerecord2) + + def test_gpos_type_2_format_b_enumerated(self): + doc = self.parse("feature kern {" + " enumerate position [T V] [a b c] <1 2 3 4>;" + "} kern;") + pos = doc.statements[0].statements[0] + self.assertEqual(type(pos), ast.PairPosStatement) + self.assertTrue(pos.enumerated) + self.assertEqual(glyphstr([pos.glyphs1]), "[T V]") + self.assertEqual(pos.valuerecord1.makeString(vertical=False), + "<1 2 3 4>") + self.assertEqual(glyphstr([pos.glyphs2]), "[a b c]") + self.assertIsNone(pos.valuerecord2) + + def test_gpos_type_3(self): + doc = self.parse("feature kern {" + " position cursive A <anchor 12 -2> <anchor 2 3>;" + "} kern;") + pos = doc.statements[0].statements[0] + self.assertEqual(type(pos), ast.CursivePosStatement) + self.assertEqual(pos.glyphclass.glyphSet(), ("A",)) + self.assertEqual((pos.entryAnchor.x, pos.entryAnchor.y), (12, -2)) + self.assertEqual((pos.exitAnchor.x, pos.exitAnchor.y), (2, 3)) + + def test_gpos_type_3_enumerated(self): + self.assertRaisesRegex( + FeatureLibError, + '"enumerate" is not allowed with cursive attachment positioning', + self.parse, + "feature kern {" + " enumerate position cursive A <anchor 12 -2> <anchor 2 3>;" + "} kern;") + + def test_gpos_type_4(self): + doc = self.parse( + "markClass [acute grave] <anchor 150 -10> @TOP_MARKS;" + "markClass [dieresis umlaut] <anchor 300 -10> @TOP_MARKS;" + "markClass [cedilla] <anchor 300 600> @BOTTOM_MARKS;" + "feature test {" + " position base [a e o u] " + " <anchor 250 450> mark @TOP_MARKS " + " <anchor 210 -10> mark @BOTTOM_MARKS;" + "} test;") + pos = doc.statements[-1].statements[0] + self.assertEqual(type(pos), ast.MarkBasePosStatement) + self.assertEqual(pos.base.glyphSet(), ("a", "e", "o", "u")) + (a1, m1), (a2, m2) = pos.marks + self.assertEqual((a1.x, a1.y, m1.name), (250, 450, "TOP_MARKS")) + self.assertEqual((a2.x, a2.y, m2.name), (210, -10, "BOTTOM_MARKS")) + + def test_gpos_type_4_enumerated(self): + self.assertRaisesRegex( + FeatureLibError, + '"enumerate" is not allowed with ' + 'mark-to-base attachment positioning', + self.parse, + "feature kern {" + " markClass cedilla <anchor 300 600> @BOTTOM_MARKS;" + " enumerate position base A <anchor 12 -2> mark @BOTTOM_MARKS;" + "} kern;") + + def test_gpos_type_4_not_markClass(self): + self.assertRaisesRegex( + FeatureLibError, "@MARKS is not a markClass", self.parse, + "@MARKS = [acute grave];" + "feature test {" + " position base [a e o u] <anchor 250 450> mark @MARKS;" + "} test;") + + def test_gpos_type_5(self): + doc = self.parse( + "markClass [grave acute] <anchor 150 500> @TOP_MARKS;" + "markClass [cedilla] <anchor 300 -100> @BOTTOM_MARKS;" + "feature test {" + " position " + " ligature [a_f_f_i o_f_f_i] " + " <anchor 50 600> mark @TOP_MARKS " + " <anchor 50 -10> mark @BOTTOM_MARKS " + " ligComponent " + " <anchor 30 800> mark @TOP_MARKS " + " ligComponent " + " <anchor NULL> " + " ligComponent " + " <anchor 30 -10> mark @BOTTOM_MARKS;" + "} test;") + pos = doc.statements[-1].statements[0] + self.assertEqual(type(pos), ast.MarkLigPosStatement) + self.assertEqual(pos.ligatures.glyphSet(), ("a_f_f_i", "o_f_f_i")) + [(a11, m11), (a12, m12)], [(a2, m2)], [], [(a4, m4)] = pos.marks + self.assertEqual((a11.x, a11.y, m11.name), (50, 600, "TOP_MARKS")) + self.assertEqual((a12.x, a12.y, m12.name), (50, -10, "BOTTOM_MARKS")) + self.assertEqual((a2.x, a2.y, m2.name), (30, 800, "TOP_MARKS")) + self.assertEqual((a4.x, a4.y, m4.name), (30, -10, "BOTTOM_MARKS")) + + def test_gpos_type_5_enumerated(self): + self.assertRaisesRegex( + FeatureLibError, + '"enumerate" is not allowed with ' + 'mark-to-ligature attachment positioning', + self.parse, + "feature test {" + " markClass cedilla <anchor 300 600> @MARKS;" + " enumerate position " + " ligature f_i <anchor 100 0> mark @MARKS" + " ligComponent <anchor NULL>;" + "} test;") + + def test_gpos_type_5_not_markClass(self): + self.assertRaisesRegex( + FeatureLibError, "@MARKS is not a markClass", self.parse, + "@MARKS = [acute grave];" + "feature test {" + " position ligature f_i <anchor 250 450> mark @MARKS;" + "} test;") + + def test_gpos_type_6(self): + doc = self.parse( + "markClass damma <anchor 189 -103> @MARK_CLASS_1;" + "feature test {" + " position mark hamza <anchor 221 301> mark @MARK_CLASS_1;" + "} test;") + pos = doc.statements[-1].statements[0] + self.assertEqual(type(pos), ast.MarkMarkPosStatement) + self.assertEqual(pos.baseMarks.glyphSet(), ("hamza",)) + [(a1, m1)] = pos.marks + self.assertEqual((a1.x, a1.y, m1.name), (221, 301, "MARK_CLASS_1")) + + def test_gpos_type_6_enumerated(self): + self.assertRaisesRegex( + FeatureLibError, + '"enumerate" is not allowed with ' + 'mark-to-mark attachment positioning', + self.parse, + "markClass damma <anchor 189 -103> @MARK_CLASS_1;" + "feature test {" + " enum pos mark hamza <anchor 221 301> mark @MARK_CLASS_1;" + "} test;") + + def test_gpos_type_6_not_markClass(self): + self.assertRaisesRegex( + FeatureLibError, "@MARKS is not a markClass", self.parse, + "@MARKS = [acute grave];" + "feature test {" + " position mark cedilla <anchor 250 450> mark @MARKS;" + "} test;") + + def test_gpos_type_8(self): + doc = self.parse( + "lookup L1 {pos one 100;} L1; lookup L2 {pos two 200;} L2;" + "feature test {" + " pos [A a] [B b] I' lookup L1 [N n]' lookup L2 P' [Y y] [Z z];" + "} test;") + lookup1, lookup2 = doc.statements[0:2] + pos = doc.statements[-1].statements[0] + self.assertEqual(type(pos), ast.ChainContextPosStatement) + self.assertEqual(glyphstr(pos.prefix), "[A a] [B b]") + self.assertEqual(glyphstr(pos.glyphs), "I [N n] P") + self.assertEqual(glyphstr(pos.suffix), "[Y y] [Z z]") + self.assertEqual(pos.lookups, [lookup1, lookup2, None]) + + def test_gpos_type_8_lookup_with_values(self): + self.assertRaisesRegex( + FeatureLibError, + 'If "lookup" is present, no values must be specified', + self.parse, + "lookup L1 {pos one 100;} L1;" + "feature test {" + " pos A' lookup L1 B' 20;" + "} test;") + + def test_markClass(self): + doc = self.parse("markClass [acute grave] <anchor 350 3> @MARKS;") + mc = doc.statements[0] + self.assertIsInstance(mc, ast.MarkClassDefinition) + self.assertEqual(mc.markClass.name, "MARKS") + self.assertEqual(mc.glyphSet(), ("acute", "grave")) + self.assertEqual((mc.anchor.x, mc.anchor.y), (350, 3)) + + def test_nameid_windows_utf16(self): + doc = self.parse( + r'table name { nameid 9 "M\00fcller-Lanc\00e9"; } name;') + name = doc.statements[0].statements[0] + self.assertIsInstance(name, ast.NameRecord) + self.assertEqual(name.nameID, 9) + self.assertEqual(name.platformID, 3) + self.assertEqual(name.platEncID, 1) + self.assertEqual(name.langID, 0x0409) + self.assertEqual(name.string, "Müller-Lancé") + self.assertEqual(name.asFea(), r'nameid 9 "M\00fcller-Lanc\00e9";') + + def test_nameid_windows_utf16_backslash(self): + doc = self.parse(r'table name { nameid 9 "Back\005cslash"; } name;') + name = doc.statements[0].statements[0] + self.assertEqual(name.string, r"Back\slash") + self.assertEqual(name.asFea(), r'nameid 9 "Back\005cslash";') + + def test_nameid_windows_utf16_quotation_mark(self): + doc = self.parse( + r'table name { nameid 9 "Quotation \0022Mark\0022"; } name;') + name = doc.statements[0].statements[0] + self.assertEqual(name.string, 'Quotation "Mark"') + self.assertEqual(name.asFea(), r'nameid 9 "Quotation \0022Mark\0022";') + + def test_nameid_windows_utf16_surroates(self): + doc = self.parse(r'table name { nameid 9 "Carrot \D83E\DD55"; } name;') + name = doc.statements[0].statements[0] + self.assertEqual(name.string, r"Carrot 🥕") + self.assertEqual(name.asFea(), r'nameid 9 "Carrot \d83e\dd55";') + + def test_nameid_mac_roman(self): + doc = self.parse( + r'table name { nameid 9 1 "Joachim M\9fller-Lanc\8e"; } name;') + name = doc.statements[0].statements[0] + self.assertIsInstance(name, ast.NameRecord) + self.assertEqual(name.nameID, 9) + self.assertEqual(name.platformID, 1) + self.assertEqual(name.platEncID, 0) + self.assertEqual(name.langID, 0) + self.assertEqual(name.string, "Joachim Müller-Lancé") + self.assertEqual(name.asFea(), + r'nameid 9 1 "Joachim M\9fller-Lanc\8e";') + + def test_nameid_mac_croatian(self): + doc = self.parse( + r'table name { nameid 9 1 0 18 "Jovica Veljovi\e6"; } name;') + name = doc.statements[0].statements[0] + self.assertEqual(name.nameID, 9) + self.assertEqual(name.platformID, 1) + self.assertEqual(name.platEncID, 0) + self.assertEqual(name.langID, 18) + self.assertEqual(name.string, "Jovica Veljović") + self.assertEqual(name.asFea(), r'nameid 9 1 0 18 "Jovica Veljovi\e6";') + + def test_nameid_unsupported_platform(self): + self.assertRaisesRegex( + FeatureLibError, "Expected platform id 1 or 3", + self.parse, 'table name { nameid 9 666 "Foo"; } name;') + + def test_rsub_format_a(self): + doc = self.parse("feature test {rsub a [b B] c' d [e E] by C;} test;") + rsub = doc.statements[0].statements[0] + self.assertEqual(type(rsub), ast.ReverseChainSingleSubstStatement) + self.assertEqual(glyphstr(rsub.old_prefix), "a [B b]") + self.assertEqual(rsub.glyphs[0].glyphSet(), ("c",)) + self.assertEqual(rsub.replacements[0].glyphSet(), ("C",)) + self.assertEqual(glyphstr(rsub.old_suffix), "d [E e]") + + def test_rsub_format_a_cid(self): + doc = self.parse(r"feature test {rsub \1 [\2 \3] \4' \5 by \6;} test;") + rsub = doc.statements[0].statements[0] + self.assertEqual(type(rsub), ast.ReverseChainSingleSubstStatement) + self.assertEqual(glyphstr(rsub.old_prefix), + "cid00001 [cid00002 cid00003]") + self.assertEqual(rsub.glyphs[0].glyphSet(), ("cid00004",)) + self.assertEqual(rsub.replacements[0].glyphSet(), ("cid00006",)) + self.assertEqual(glyphstr(rsub.old_suffix), "cid00005") + + def test_rsub_format_b(self): + doc = self.parse( + "feature smcp {" + " reversesub A B [one.fitted one.oldstyle]' C [d D] by one;" + "} smcp;") + rsub = doc.statements[0].statements[0] + self.assertEqual(type(rsub), ast.ReverseChainSingleSubstStatement) + self.assertEqual(glyphstr(rsub.old_prefix), "A B") + self.assertEqual(glyphstr(rsub.old_suffix), "C [D d]") + self.assertEqual(mapping(rsub), { + "one.fitted": "one", + "one.oldstyle": "one" + }) + + def test_rsub_format_c(self): + doc = self.parse( + "feature test {" + " reversesub BACK TRACK [a-d]' LOOK AHEAD by [A.sc-D.sc];" + "} test;") + rsub = doc.statements[0].statements[0] + self.assertEqual(type(rsub), ast.ReverseChainSingleSubstStatement) + self.assertEqual(glyphstr(rsub.old_prefix), "BACK TRACK") + self.assertEqual(glyphstr(rsub.old_suffix), "LOOK AHEAD") + self.assertEqual(mapping(rsub), { + "a": "A.sc", + "b": "B.sc", + "c": "C.sc", + "d": "D.sc" + }) + + def test_rsub_from(self): + self.assertRaisesRegex( + FeatureLibError, + 'Reverse chaining substitutions do not support "from"', + self.parse, "feature test {rsub a from [a.1 a.2 a.3];} test;") + + def test_rsub_nonsingle(self): + self.assertRaisesRegex( + FeatureLibError, + "In reverse chaining single substitutions, only a single glyph " + "or glyph class can be replaced", + self.parse, "feature test {rsub c d by c_d;} test;") + + def test_rsub_multiple_replacement_glyphs(self): + self.assertRaisesRegex( + FeatureLibError, + 'In reverse chaining single substitutions, the replacement ' + '\(after "by"\) must be a single glyph or glyph class', + self.parse, "feature test {rsub f_i by f i;} test;") + + def test_script(self): + doc = self.parse("feature test {script cyrl;} test;") + s = doc.statements[0].statements[0] + self.assertEqual(type(s), ast.ScriptStatement) + self.assertEqual(s.script, "cyrl") + + def test_script_dflt(self): + self.assertRaisesRegex( + FeatureLibError, + '"dflt" is not a valid script tag; use "DFLT" instead', + self.parse, "feature test {script dflt;} test;") + + def test_sub_single_format_a(self): # GSUB LookupType 1 + doc = self.parse("feature smcp {substitute a by a.sc;} smcp;") + sub = doc.statements[0].statements[0] + self.assertIsInstance(sub, ast.SingleSubstStatement) + self.assertEqual(glyphstr(sub.prefix), "") + self.assertEqual(mapping(sub), {"a": "a.sc"}) + self.assertEqual(glyphstr(sub.suffix), "") + + def test_sub_single_format_a_chained(self): # chain to GSUB LookupType 1 + doc = self.parse("feature test {sub [A a] d' [C] by d.alt;} test;") + sub = doc.statements[0].statements[0] + self.assertIsInstance(sub, ast.SingleSubstStatement) + self.assertEqual(mapping(sub), {"d": "d.alt"}) + self.assertEqual(glyphstr(sub.prefix), "[A a]") + self.assertEqual(glyphstr(sub.suffix), "C") + + def test_sub_single_format_a_cid(self): # GSUB LookupType 1 + doc = self.parse(r"feature smcp {substitute \12345 by \78987;} smcp;") + sub = doc.statements[0].statements[0] + self.assertIsInstance(sub, ast.SingleSubstStatement) + self.assertEqual(glyphstr(sub.prefix), "") + self.assertEqual(mapping(sub), {"cid12345": "cid78987"}) + self.assertEqual(glyphstr(sub.suffix), "") + + def test_sub_single_format_b(self): # GSUB LookupType 1 + doc = self.parse( + "feature smcp {" + " substitute [one.fitted one.oldstyle] by one;" + "} smcp;") + sub = doc.statements[0].statements[0] + self.assertIsInstance(sub, ast.SingleSubstStatement) + self.assertEqual(mapping(sub), { + "one.fitted": "one", + "one.oldstyle": "one" + }) + self.assertEqual(glyphstr(sub.prefix), "") + self.assertEqual(glyphstr(sub.suffix), "") + + def test_sub_single_format_b_chained(self): # chain to GSUB LookupType 1 + doc = self.parse( + "feature smcp {" + " substitute PRE FIX [one.fitted one.oldstyle]' SUF FIX by one;" + "} smcp;") + sub = doc.statements[0].statements[0] + self.assertIsInstance(sub, ast.SingleSubstStatement) + self.assertEqual(mapping(sub), { + "one.fitted": "one", + "one.oldstyle": "one" + }) + self.assertEqual(glyphstr(sub.prefix), "PRE FIX") + self.assertEqual(glyphstr(sub.suffix), "SUF FIX") + + def test_sub_single_format_c(self): # GSUB LookupType 1 + doc = self.parse( + "feature smcp {" + " substitute [a-d] by [A.sc-D.sc];" + "} smcp;") + sub = doc.statements[0].statements[0] + self.assertIsInstance(sub, ast.SingleSubstStatement) + self.assertEqual(mapping(sub), { + "a": "A.sc", + "b": "B.sc", + "c": "C.sc", + "d": "D.sc" + }) + self.assertEqual(glyphstr(sub.prefix), "") + self.assertEqual(glyphstr(sub.suffix), "") + + def test_sub_single_format_c_chained(self): # chain to GSUB LookupType 1 + doc = self.parse( + "feature smcp {" + " substitute [a-d]' X Y [Z z] by [A.sc-D.sc];" + "} smcp;") + sub = doc.statements[0].statements[0] + self.assertIsInstance(sub, ast.SingleSubstStatement) + self.assertEqual(mapping(sub), { + "a": "A.sc", + "b": "B.sc", + "c": "C.sc", + "d": "D.sc" + }) + self.assertEqual(glyphstr(sub.prefix), "") + self.assertEqual(glyphstr(sub.suffix), "X Y [Z z]") + + def test_sub_single_format_c_different_num_elements(self): + self.assertRaisesRegex( + FeatureLibError, + 'Expected a glyph class with 4 elements after "by", ' + 'but found a glyph class with 26 elements', + self.parse, "feature smcp {sub [a-d] by [A.sc-Z.sc];} smcp;") + + def test_sub_with_values(self): + self.assertRaisesRegex( + FeatureLibError, + "Substitution statements cannot contain values", + self.parse, "feature smcp {sub A' 20 by A.sc;} smcp;") + + def test_substitute_multiple(self): # GSUB LookupType 2 + doc = self.parse("lookup Look {substitute f_f_i by f f i;} Look;") + sub = doc.statements[0].statements[0] + self.assertIsInstance(sub, ast.MultipleSubstStatement) + self.assertEqual(sub.glyph, "f_f_i") + self.assertEqual(sub.replacement, ("f", "f", "i")) + + def test_substitute_multiple_chained(self): # chain to GSUB LookupType 2 + doc = self.parse("lookup L {sub [A-C] f_f_i' [X-Z] by f f i;} L;") + sub = doc.statements[0].statements[0] + self.assertIsInstance(sub, ast.MultipleSubstStatement) + self.assertEqual(sub.glyph, "f_f_i") + self.assertEqual(sub.replacement, ("f", "f", "i")) + + def test_substitute_multiple_by_mutliple(self): + self.assertRaisesRegex( + FeatureLibError, + "Direct substitution of multiple glyphs by multiple glyphs " + "is not supported", + self.parse, + "lookup MxM {sub a b c by d e f;} MxM;") + + def test_split_marked_glyphs_runs(self): + self.assertRaisesRegex( + FeatureLibError, + "Unsupported contextual target sequence", + self.parse, "feature test{" + " ignore pos a' x x A';" + "} test;") + self.assertRaisesRegex( + FeatureLibError, + "Unsupported contextual target sequence", + self.parse, "lookup shift {" + " pos a <0 -10 0 0>;" + " pos A <0 10 0 0>;" + "} shift;" + "feature test {" + " sub a' lookup shift x x A' lookup shift;" + "} test;") + self.assertRaisesRegex( + FeatureLibError, + "Unsupported contextual target sequence", + self.parse, "feature test {" + " ignore sub a' x x A';" + "} test;") + self.assertRaisesRegex( + FeatureLibError, + "Unsupported contextual target sequence", + self.parse, "lookup upper {" + " sub a by A;" + "} upper;" + "lookup lower {" + " sub A by a;" + "} lower;" + "feature test {" + " sub a' lookup upper x x A' lookup lower;" + "} test;") + + def test_substitute_mix_single_multiple(self): + doc = self.parse("lookup Look {" + " sub f_f by f f;" + " sub f by f;" + " sub f_f_i by f f i;" + "} Look;") + statements = doc.statements[0].statements + for sub in statements: + self.assertIsInstance(sub, ast.MultipleSubstStatement) + self.assertEqual(statements[1].glyph, "f") + self.assertEqual(statements[1].replacement, ["f"]) + + def test_substitute_from(self): # GSUB LookupType 3 + doc = self.parse("feature test {" + " substitute a from [a.1 a.2 a.3];" + "} test;") + sub = doc.statements[0].statements[0] + self.assertIsInstance(sub, ast.AlternateSubstStatement) + self.assertEqual(glyphstr(sub.prefix), "") + self.assertEqual(glyphstr([sub.glyph]), "a") + self.assertEqual(glyphstr(sub.suffix), "") + self.assertEqual(glyphstr([sub.replacement]), "[a.1 a.2 a.3]") + + def test_substitute_from_chained(self): # chain to GSUB LookupType 3 + doc = self.parse("feature test {" + " substitute A B a' [Y y] Z from [a.1 a.2 a.3];" + "} test;") + sub = doc.statements[0].statements[0] + self.assertIsInstance(sub, ast.AlternateSubstStatement) + self.assertEqual(glyphstr(sub.prefix), "A B") + self.assertEqual(glyphstr([sub.glyph]), "a") + self.assertEqual(glyphstr(sub.suffix), "[Y y] Z") + self.assertEqual(glyphstr([sub.replacement]), "[a.1 a.2 a.3]") + + def test_substitute_from_cid(self): # GSUB LookupType 3 + doc = self.parse(r"feature test {" + r" substitute \7 from [\111 \222];" + r"} test;") + sub = doc.statements[0].statements[0] + self.assertIsInstance(sub, ast.AlternateSubstStatement) + self.assertEqual(glyphstr(sub.prefix), "") + self.assertEqual(glyphstr([sub.glyph]), "cid00007") + self.assertEqual(glyphstr(sub.suffix), "") + self.assertEqual(glyphstr([sub.replacement]), "[cid00111 cid00222]") + + def test_substitute_from_glyphclass(self): # GSUB LookupType 3 + doc = self.parse("feature test {" + " @Ampersands = [ampersand.1 ampersand.2];" + " substitute ampersand from @Ampersands;" + "} test;") + [glyphclass, sub] = doc.statements[0].statements + self.assertIsInstance(sub, ast.AlternateSubstStatement) + self.assertEqual(glyphstr(sub.prefix), "") + self.assertEqual(glyphstr([sub.glyph]), "ampersand") + self.assertEqual(glyphstr(sub.suffix), "") + self.assertEqual(glyphstr([sub.replacement]), + "[ampersand.1 ampersand.2]") + + def test_substitute_ligature(self): # GSUB LookupType 4 + doc = self.parse("feature liga {substitute f f i by f_f_i;} liga;") + sub = doc.statements[0].statements[0] + self.assertIsInstance(sub, ast.LigatureSubstStatement) + self.assertEqual(glyphstr(sub.glyphs), "f f i") + self.assertEqual(sub.replacement, "f_f_i") + self.assertEqual(glyphstr(sub.prefix), "") + self.assertEqual(glyphstr(sub.suffix), "") + + def test_substitute_ligature_chained(self): # chain to GSUB LookupType 4 + doc = self.parse("feature F {substitute A B f' i' Z by f_i;} F;") + sub = doc.statements[0].statements[0] + self.assertIsInstance(sub, ast.LigatureSubstStatement) + self.assertEqual(glyphstr(sub.glyphs), "f i") + self.assertEqual(sub.replacement, "f_i") + self.assertEqual(glyphstr(sub.prefix), "A B") + self.assertEqual(glyphstr(sub.suffix), "Z") + + def test_substitute_lookups(self): # GSUB LookupType 6 + doc = Parser(self.getpath("spec5fi1.fea"), GLYPHNAMES).parse() + [_, _, _, langsys, ligs, sub, feature] = doc.statements + self.assertEqual(feature.statements[0].lookups, [ligs, None, sub]) + self.assertEqual(feature.statements[1].lookups, [ligs, None, sub]) + + def test_substitute_missing_by(self): + self.assertRaisesRegex( + FeatureLibError, + 'Expected "by", "from" or explicit lookup references', + self.parse, "feature liga {substitute f f i;} liga;") + + def test_subtable(self): + doc = self.parse("feature test {subtable;} test;") + s = doc.statements[0].statements[0] + self.assertIsInstance(s, ast.SubtableStatement) + + def test_table_badEnd(self): + self.assertRaisesRegex( + FeatureLibError, 'Expected "GDEF"', self.parse, + "table GDEF {LigatureCaretByPos f_i 400;} ABCD;") + + def test_table_comment(self): + for table in "BASE GDEF OS/2 head hhea name vhea".split(): + doc = self.parse("table %s { # Comment\n } %s;" % (table, table)) + comment = doc.statements[0].statements[0] + self.assertIsInstance(comment, ast.Comment) + self.assertEqual(comment.text, "# Comment") + + def test_table_unsupported(self): + self.assertRaisesRegex( + FeatureLibError, '"table Foo" is not supported', self.parse, + "table Foo {LigatureCaretByPos f_i 400;} Foo;") + + def test_valuerecord_format_a_horizontal(self): + doc = self.parse("feature liga {valueRecordDef 123 foo;} liga;") + value = doc.statements[0].statements[0].value + self.assertIsNone(value.xPlacement) + self.assertIsNone(value.yPlacement) + self.assertEqual(value.xAdvance, 123) + self.assertIsNone(value.yAdvance) + self.assertIsNone(value.xPlaDevice) + self.assertIsNone(value.yPlaDevice) + self.assertIsNone(value.xAdvDevice) + self.assertIsNone(value.yAdvDevice) + self.assertEqual(value.makeString(vertical=False), "123") + + def test_valuerecord_format_a_vertical(self): + doc = self.parse("feature vkrn {valueRecordDef 123 foo;} vkrn;") + value = doc.statements[0].statements[0].value + self.assertIsNone(value.xPlacement) + self.assertIsNone(value.yPlacement) + self.assertIsNone(value.xAdvance) + self.assertEqual(value.yAdvance, 123) + self.assertIsNone(value.xPlaDevice) + self.assertIsNone(value.yPlaDevice) + self.assertIsNone(value.xAdvDevice) + self.assertIsNone(value.yAdvDevice) + self.assertEqual(value.makeString(vertical=True), "123") + + def test_valuerecord_format_a_zero_horizontal(self): + doc = self.parse("feature liga {valueRecordDef 0 foo;} liga;") + value = doc.statements[0].statements[0].value + self.assertIsNone(value.xPlacement) + self.assertIsNone(value.yPlacement) + self.assertEqual(value.xAdvance, 0) + self.assertIsNone(value.yAdvance) + self.assertIsNone(value.xPlaDevice) + self.assertIsNone(value.yPlaDevice) + self.assertIsNone(value.xAdvDevice) + self.assertIsNone(value.yAdvDevice) + self.assertEqual(value.makeString(vertical=False), "0") + + def test_valuerecord_format_a_zero_vertical(self): + doc = self.parse("feature vkrn {valueRecordDef 0 foo;} vkrn;") + value = doc.statements[0].statements[0].value + self.assertIsNone(value.xPlacement) + self.assertIsNone(value.yPlacement) + self.assertIsNone(value.xAdvance) + self.assertEqual(value.yAdvance, 0) + self.assertIsNone(value.xPlaDevice) + self.assertIsNone(value.yPlaDevice) + self.assertIsNone(value.xAdvDevice) + self.assertIsNone(value.yAdvDevice) + self.assertEqual(value.makeString(vertical=True), "0") + + def test_valuerecord_format_a_vertical_contexts_(self): + for tag in "vkrn vpal vhal valt".split(): + doc = self.parse( + "feature %s {valueRecordDef 77 foo;} %s;" % (tag, tag)) + value = doc.statements[0].statements[0].value + if value.yAdvance != 77: + self.fail(msg="feature %s should be a vertical context " + "for ValueRecord format A" % tag) + + def test_valuerecord_format_b(self): + doc = self.parse("feature liga {valueRecordDef <1 2 3 4> foo;} liga;") + value = doc.statements[0].statements[0].value + self.assertEqual(value.xPlacement, 1) + self.assertEqual(value.yPlacement, 2) + self.assertEqual(value.xAdvance, 3) + self.assertEqual(value.yAdvance, 4) + self.assertIsNone(value.xPlaDevice) + self.assertIsNone(value.yPlaDevice) + self.assertIsNone(value.xAdvDevice) + self.assertIsNone(value.yAdvDevice) + self.assertEqual(value.makeString(vertical=False), "<1 2 3 4>") + + def test_valuerecord_format_b_zero(self): + doc = self.parse("feature liga {valueRecordDef <0 0 0 0> foo;} liga;") + value = doc.statements[0].statements[0].value + self.assertEqual(value.xPlacement, 0) + self.assertEqual(value.yPlacement, 0) + self.assertEqual(value.xAdvance, 0) + self.assertEqual(value.yAdvance, 0) + self.assertIsNone(value.xPlaDevice) + self.assertIsNone(value.yPlaDevice) + self.assertIsNone(value.xAdvDevice) + self.assertIsNone(value.yAdvDevice) + self.assertEqual(value.makeString(vertical=False), "<0 0 0 0>") + + def test_valuerecord_format_c(self): + doc = self.parse( + "feature liga {" + " valueRecordDef <" + " 1 2 3 4" + " <device 8 88>" + " <device 11 111, 12 112>" + " <device NULL>" + " <device 33 -113, 44 -114, 55 115>" + " > foo;" + "} liga;") + value = doc.statements[0].statements[0].value + self.assertEqual(value.xPlacement, 1) + self.assertEqual(value.yPlacement, 2) + self.assertEqual(value.xAdvance, 3) + self.assertEqual(value.yAdvance, 4) + self.assertEqual(value.xPlaDevice, ((8, 88),)) + self.assertEqual(value.yPlaDevice, ((11, 111), (12, 112))) + self.assertIsNone(value.xAdvDevice) + self.assertEqual(value.yAdvDevice, ((33, -113), (44, -114), (55, 115))) + self.assertEqual(value.makeString(vertical=False), + "<1 2 3 4 <device 8 88> <device 11 111, 12 112>" + " <device NULL> <device 33 -113, 44 -114, 55 115>>") + + def test_valuerecord_format_d(self): + doc = self.parse("feature test {valueRecordDef <NULL> foo;} test;") + value = doc.statements[0].statements[0].value + self.assertIsNone(value) + + def test_valuerecord_named(self): + doc = self.parse("valueRecordDef <1 2 3 4> foo;" + "feature liga {valueRecordDef <foo> bar;} liga;") + value = doc.statements[1].statements[0].value + self.assertEqual(value.xPlacement, 1) + self.assertEqual(value.yPlacement, 2) + self.assertEqual(value.xAdvance, 3) + self.assertEqual(value.yAdvance, 4) + + def test_valuerecord_named_unknown(self): + self.assertRaisesRegex( + FeatureLibError, "Unknown valueRecordDef \"unknown\"", + self.parse, "valueRecordDef <unknown> foo;") + + def test_valuerecord_scoping(self): + [foo, liga, smcp] = self.parse( + "valueRecordDef 789 foo;" + "feature liga {valueRecordDef <foo> bar;} liga;" + "feature smcp {valueRecordDef <foo> bar;} smcp;" + ).statements + self.assertEqual(foo.value.xAdvance, 789) + self.assertEqual(liga.statements[0].value.xAdvance, 789) + self.assertEqual(smcp.statements[0].value.xAdvance, 789) + + def test_valuerecord_device_value_out_of_range(self): + self.assertRaisesRegex( + FeatureLibError, r"Device value out of valid range \(-128..127\)", + self.parse, + "valueRecordDef <1 2 3 4 <device NULL> <device NULL> " + "<device NULL> <device 11 128>> foo;") + + def test_languagesystem(self): + [langsys] = self.parse("languagesystem latn DEU;").statements + self.assertEqual(langsys.script, "latn") + self.assertEqual(langsys.language, "DEU ") + self.assertRaisesRegex( + FeatureLibError, + 'For script "DFLT", the language must be "dflt"', + self.parse, "languagesystem DFLT DEU;") + self.assertRaisesRegex( + FeatureLibError, + '"dflt" is not a valid script tag; use "DFLT" instead', + self.parse, "languagesystem dflt dflt;") + self.assertRaisesRegex( + FeatureLibError, + '"DFLT" is not a valid language tag; use "dflt" instead', + self.parse, "languagesystem latn DFLT;") + self.assertRaisesRegex( + FeatureLibError, "Expected ';'", + self.parse, "languagesystem latn DEU") + self.assertRaisesRegex( + FeatureLibError, "longer than 4 characters", + self.parse, "languagesystem foobar DEU;") + self.assertRaisesRegex( + FeatureLibError, "longer than 4 characters", + self.parse, "languagesystem latn FOOBAR;") + + def test_empty_statement_ignored(self): + doc = self.parse("feature test {;} test;") + self.assertFalse(doc.statements[0].statements) + doc = self.parse(";;;") + self.assertFalse(doc.statements) + for table in "BASE GDEF OS/2 head hhea name vhea".split(): + doc = self.parse("table %s { ;;; } %s;" % (table, table)) + self.assertEqual(doc.statements[0].statements, []) + + def parse(self, text, glyphNames=GLYPHNAMES, followIncludes=True): + featurefile = UnicodeIO(text) + p = Parser(featurefile, glyphNames, followIncludes=followIncludes) + return p.parse() + + @staticmethod + def getpath(testfile): + path, _ = os.path.split(__file__) + return os.path.join(path, "data", testfile) + + +class SymbolTableTest(unittest.TestCase): + def test_scopes(self): + symtab = SymbolTable() + symtab.define("foo", 23) + self.assertEqual(symtab.resolve("foo"), 23) + symtab.enter_scope() + self.assertEqual(symtab.resolve("foo"), 23) + symtab.define("foo", 42) + self.assertEqual(symtab.resolve("foo"), 42) + symtab.exit_scope() + self.assertEqual(symtab.resolve("foo"), 23) + + def test_resolve_undefined(self): + self.assertEqual(SymbolTable().resolve("abc"), None) + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/merge_test.py b/Tests/merge_test.py new file mode 100644 index 0000000..00e719b --- /dev/null +++ b/Tests/merge_test.py @@ -0,0 +1,118 @@ +from fontTools.misc.py23 import * +from fontTools import ttLib +from fontTools.merge import * +import unittest + + +class MergeIntegrationTest(unittest.TestCase): + # TODO + pass + +class gaspMergeUnitTest(unittest.TestCase): + def setUp(self): + self.merger = Merger() + + self.table1 = ttLib.newTable('gasp') + self.table1.version = 1 + self.table1.gaspRange = { + 0x8: 0xA , + 0x10: 0x5, + } + + self.table2 = ttLib.newTable('gasp') + self.table2.version = 1 + self.table2.gaspRange = { + 0x6: 0xB , + 0xFF: 0x4, + } + + self.result = ttLib.newTable('gasp') + + def test_gasp_merge_basic(self): + result = self.result.merge(self.merger, [self.table1, self.table2]) + self.assertEqual(result, self.table1) + + result = self.result.merge(self.merger, [self.table2, self.table1]) + self.assertEqual(result, self.table2) + + def test_gasp_merge_notImplemented(self): + result = self.result.merge(self.merger, [NotImplemented, self.table1]) + self.assertEqual(result, NotImplemented) + + result = self.result.merge(self.merger, [self.table1, NotImplemented]) + self.assertEqual(result, self.table1) + + +class CmapMergeUnitTest(unittest.TestCase): + def setUp(self): + self.merger = Merger() + self.table1 = ttLib.newTable('cmap') + self.table2 = ttLib.newTable('cmap') + self.mergedTable = ttLib.newTable('cmap') + pass + + def tearDown(self): + pass + + + def makeSubtable(self, format, platformID, platEncID, cmap): + module = ttLib.getTableModule('cmap') + subtable = module.cmap_classes[format](format) + (subtable.platformID, + subtable.platEncID, + subtable.language, + subtable.cmap) = (platformID, platEncID, 0, cmap) + return subtable + + # 4-3-1 table merged with 12-3-10 table with no dupes with codepoints outside BMP + def test_cmap_merge_no_dupes(self): + table1 = self.table1 + table2 = self.table2 + mergedTable = self.mergedTable + + cmap1 = {0x2603: 'SNOWMAN'} + table1.tables = [self.makeSubtable(4,3,1, cmap1)] + + cmap2 = {0x26C4: 'SNOWMAN WITHOUT SNOW'} + cmap2Extended = {0x1F93C: 'WRESTLERS'} + cmap2Extended.update(cmap2) + table2.tables = [self.makeSubtable(4,3,1, cmap2), self.makeSubtable(12,3,10, cmap2Extended)] + + self.merger.alternateGlyphsPerFont = [{},{}] + mergedTable.merge(self.merger, [table1, table2]) + + expectedCmap = cmap2.copy() + expectedCmap.update(cmap1) + expectedCmapExtended = cmap2Extended.copy() + expectedCmapExtended.update(cmap1) + self.assertEqual(mergedTable.numSubTables, 2) + self.assertEqual([(table.format, table.platformID, table.platEncID, table.language) for table in mergedTable.tables], + [(4,3,1,0),(12,3,10,0)]) + self.assertEqual(mergedTable.tables[0].cmap, expectedCmap) + self.assertEqual(mergedTable.tables[1].cmap, expectedCmapExtended) + + # Tests Issue #322 + def test_cmap_merge_three_dupes(self): + table1 = self.table1 + table2 = self.table2 + mergedTable = self.mergedTable + + cmap1 = {0x20: 'space#0', 0xA0: 'space#0'} + table1.tables = [self.makeSubtable(4,3,1,cmap1)] + cmap2 = {0x20: 'space#1', 0xA0: 'uni00A0#1'} + table2.tables = [self.makeSubtable(4,3,1,cmap2)] + + self.merger.duplicateGlyphsPerFont = [{},{}] + mergedTable.merge(self.merger, [table1, table2]) + + expectedCmap = cmap1.copy() + self.assertEqual(mergedTable.numSubTables, 1) + table = mergedTable.tables[0] + self.assertEqual((table.format, table.platformID, table.platEncID, table.language), (4,3,1,0)) + self.assertEqual(table.cmap, expectedCmap) + self.assertEqual(self.merger.duplicateGlyphsPerFont, [{}, {'space#0': 'space#1'}]) + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/misc/arrayTools_test.py b/Tests/misc/arrayTools_test.py new file mode 100644 index 0000000..108b50d --- /dev/null +++ b/Tests/misc/arrayTools_test.py @@ -0,0 +1,85 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.arrayTools import ( + calcBounds, calcIntBounds, updateBounds, pointInRect, pointsInRect, + vectorLength, asInt16, normRect, scaleRect, offsetRect, insetRect, + sectRect, unionRect, rectCenter, intRect) +import math + + +def test_calcBounds(): + assert calcBounds([]) == (0, 0, 0, 0) + assert calcBounds( + [(0, 40), (0, 100), (50, 50), (80, 10)]) == (0, 10, 80, 100) + + +def test_calcIntBounds(): + assert calcIntBounds( + [(0.1, 40.1), (0.1, 100.1), (49.9, 49.9), (79.5, 9.5)] + ) == (0, 10, 80, 100) + + +def test_updateBounds(): + assert updateBounds((0, 0, 0, 0), (100, 100)) == (0, 0, 100, 100) + + +def test_pointInRect(): + assert pointInRect((50, 50), (0, 0, 100, 100)) + assert pointInRect((0, 0), (0, 0, 100, 100)) + assert pointInRect((100, 100), (0, 0, 100, 100)) + assert not pointInRect((101, 100), (0, 0, 100, 100)) + + +def test_pointsInRect(): + assert pointsInRect([], (0, 0, 100, 100)) == [] + assert pointsInRect( + [(50, 50), (0, 0), (100, 100), (101, 100)], + (0, 0, 100, 100)) == [True, True, True, False] + + +def test_vectorLength(): + assert vectorLength((1, 1)) == math.sqrt(2) + + +def test_asInt16(): + assert asInt16([0, 0.1, 0.5, 0.9]) == [0, 0, 1, 1] + + +def test_normRect(): + assert normRect((0, 10, 100, 200)) == (0, 10, 100, 200) + assert normRect((100, 200, 0, 10)) == (0, 10, 100, 200) + + +def test_scaleRect(): + assert scaleRect((10, 20, 50, 150), 1.5, 2) == (15.0, 40, 75.0, 300) + + +def test_offsetRect(): + assert offsetRect((10, 20, 30, 40), 5, 6) == (15, 26, 35, 46) + + +def test_insetRect(): + assert insetRect((10, 20, 50, 60), 5, 10) == (15, 30, 45, 50) + assert insetRect((10, 20, 50, 60), -5, -10) == (5, 10, 55, 70) + + +def test_sectRect(): + intersects, rect = sectRect((0, 10, 20, 30), (0, 40, 20, 50)) + assert not intersects + + intersects, rect = sectRect((0, 10, 20, 30), (5, 20, 35, 50)) + assert intersects + assert rect == (5, 20, 20, 30) + + +def test_unionRect(): + assert unionRect((0, 10, 20, 30), (0, 40, 20, 50)) == (0, 10, 20, 50) + + +def test_rectCenter(): + assert rectCenter((0, 0, 100, 200)) == (50.0, 100.0) + assert rectCenter((0, 0, 100, 199.0)) == (50.0, 99.5) + + +def test_intRect(): + assert intRect((0.9, 2.9, 3.1, 4.1)) == (0, 2, 4, 5) diff --git a/Tests/misc/bezierTools_test.py b/Tests/misc/bezierTools_test.py new file mode 100644 index 0000000..1519543 --- /dev/null +++ b/Tests/misc/bezierTools_test.py @@ -0,0 +1,133 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.bezierTools import ( + calcQuadraticBounds, calcCubicBounds, splitLine, splitQuadratic, + splitCubic, splitQuadraticAtT, splitCubicAtT, solveCubic) +import pytest + + +def test_calcQuadraticBounds(): + assert calcQuadraticBounds( + (0, 0), (50, 100), (100, 0)) == (0, 0, 100, 50.0) + assert calcQuadraticBounds( + (0, 0), (100, 0), (100, 100)) == (0.0, 0.0, 100, 100) + + +def test_calcCubicBounds(): + assert calcCubicBounds( + (0, 0), (25, 100), (75, 100), (100, 0)) == ((0, 0, 100, 75.0)) + assert calcCubicBounds( + (0, 0), (50, 0), (100, 50), (100, 100)) == (0.0, 0.0, 100, 100) + assert calcCubicBounds( + (50, 0), (0, 100), (100, 100), (50, 0) + ) == pytest.approx((35.566243, 0.000000, 64.433757, 75.000000)) + + +def test_splitLine(): + assert splitLine( + (0, 0), (100, 100), where=50, isHorizontal=True + ) == [((0, 0), (50.0, 50.0)), ((50.0, 50.0), (100, 100))] + assert splitLine( + (0, 0), (100, 100), where=100, isHorizontal=True + ) == [((0, 0), (100, 100))] + assert splitLine( + (0, 0), (100, 100), where=0, isHorizontal=True + ) == [((0, 0), (0, 0)), ((0, 0), (100, 100))] + assert splitLine( + (0, 0), (100, 100), where=0, isHorizontal=False + ) == [((0, 0), (0, 0)), ((0, 0), (100, 100))] + assert splitLine( + (100, 0), (0, 0), where=50, isHorizontal=False + ) == [((100, 0), (50, 0)), ((50, 0), (0, 0))] + assert splitLine( + (0, 100), (0, 0), where=50, isHorizontal=True + ) == [((0, 100), (0, 50)), ((0, 50), (0, 0))] + assert splitLine( + (0, 100), (100, 100), where=50, isHorizontal=True + ) == [((0, 100), (100, 100))] + + +def assert_curves_approx_equal(actual_curves, expected_curves): + assert len(actual_curves) == len(expected_curves) + for acurve, ecurve in zip(actual_curves, expected_curves): + assert len(acurve) == len(ecurve) + for apt, ept in zip(acurve, ecurve): + assert apt == pytest.approx(ept) + + +def test_splitQuadratic(): + assert splitQuadratic( + (0, 0), (50, 100), (100, 0), where=150, isHorizontal=False + ) == [((0, 0), (50, 100), (100, 0))] + assert splitQuadratic( + (0, 0), (50, 100), (100, 0), where=50, isHorizontal=False + ) == [((0, 0), (25, 50), (50, 50)), + ((50, 50), (75, 50), (100, 0))] + assert splitQuadratic( + (0, 0), (50, 100), (100, 0), where=25, isHorizontal=False + ) == [((0, 0), (12.5, 25), (25, 37.5)), + ((25, 37.5), (62.5, 75), (100, 0))] + assert_curves_approx_equal( + splitQuadratic( + (0, 0), (50, 100), (100, 0), where=25, isHorizontal=True), + [((0, 0), (7.32233, 14.64466), (14.64466, 25)), + ((14.64466, 25), (50, 75), (85.3553, 25)), + ((85.3553, 25), (92.6777, 14.64466), (100, -7.10543e-15))]) + # XXX I'm not at all sure if the following behavior is desirable + assert splitQuadratic( + (0, 0), (50, 100), (100, 0), where=50, isHorizontal=True + ) == [((0, 0), (25, 50), (50, 50)), + ((50, 50), (50, 50), (50, 50)), + ((50, 50), (75, 50), (100, 0))] + + +def test_splitCubic(): + assert splitCubic( + (0, 0), (25, 100), (75, 100), (100, 0), where=150, isHorizontal=False + ) == [((0, 0), (25, 100), (75, 100), (100, 0))] + assert splitCubic( + (0, 0), (25, 100), (75, 100), (100, 0), where=50, isHorizontal=False + ) == [((0, 0), (12.5, 50), (31.25, 75), (50, 75)), + ((50, 75), (68.75, 75), (87.5, 50), (100, 0))] + assert_curves_approx_equal( + splitCubic( + (0, 0), (25, 100), (75, 100), (100, 0), where=25, + isHorizontal=True), + [((0, 0), (2.293792, 9.17517), (4.798045, 17.5085), (7.47414, 25)), + ((7.47414, 25), (31.2886, 91.6667), (68.7114, 91.6667), + (92.5259, 25)), + ((92.5259, 25), (95.202, 17.5085), (97.7062, 9.17517), + (100, 1.77636e-15))]) + + +def test_splitQuadraticAtT(): + assert splitQuadraticAtT( + (0, 0), (50, 100), (100, 0), 0.5 + ) == [((0, 0), (25, 50), (50, 50)), + ((50, 50), (75, 50), (100, 0))] + assert splitQuadraticAtT( + (0, 0), (50, 100), (100, 0), 0.5, 0.75 + ) == [((0, 0), (25, 50), (50, 50)), + ((50, 50), (62.5, 50), (75, 37.5)), + ((75, 37.5), (87.5, 25), (100, 0))] + + +def test_splitCubicAtT(): + assert splitCubicAtT( + (0, 0), (25, 100), (75, 100), (100, 0), 0.5 + ) == [((0, 0), (12.5, 50), (31.25, 75), (50, 75)), + ((50, 75), (68.75, 75), (87.5, 50), (100, 0))] + assert splitCubicAtT( + (0, 0), (25, 100), (75, 100), (100, 0), 0.5, 0.75 + ) == [((0, 0), (12.5, 50), (31.25, 75), (50, 75)), + ((50, 75), (59.375, 75), (68.75, 68.75), (77.34375, 56.25)), + ((77.34375, 56.25), (85.9375, 43.75), (93.75, 25), (100, 0))] + + +def test_solveCubic(): + assert solveCubic(1, 1, -6, 0) == [-3.0, -0.0, 2.0] + assert solveCubic(-10.0, -9.0, 48.0, -29.0) == [-2.9, 1.0, 1.0] + assert solveCubic(-9.875, -9.0, 47.625, -28.75) == [-2.911392, 1.0, 1.0] + assert solveCubic(1.0, -4.5, 6.75, -3.375) == [1.5, 1.5, 1.5] + assert solveCubic(-12.0, 18.0, -9.0, 1.50023651123) == [0.5, 0.5, 0.5] + assert solveCubic(9.0, 0.0, 0.0, -7.62939453125e-05) == [-0.0, -0.0, -0.0] diff --git a/Tests/misc/classifyTools_test.py b/Tests/misc/classifyTools_test.py new file mode 100644 index 0000000..ac1f656 --- /dev/null +++ b/Tests/misc/classifyTools_test.py @@ -0,0 +1,30 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.classifyTools import classify + + +def test_classify(): + assert classify([]) == ([], {}) + assert classify([[]]) == ([], {}) + assert classify([[], []]) == ([], {}) + assert classify([[1]]) == ([{1}], {1: {1}}) + assert classify([[1,2]]) == ([{1, 2}], {1: {1, 2}, 2: {1, 2}}) + assert classify([[1],[2]]) == ([{1}, {2}], {1: {1}, 2: {2}}) + assert classify([[1,2],[2]]) == ([{1}, {2}], {1: {1}, 2: {2}}) + assert classify([[1,2],[2,4]]) == ( + [{1}, {2}, {4}], {1: {1}, 2: {2}, 4: {4}}) + assert classify([[1,2],[2,4,5]]) == ( + [{4, 5}, {1}, {2}], {1: {1}, 2: {2}, 4: {4, 5}, 5: {4, 5}}) + assert classify([[1,2],[2,4,5]], sort=False) == ( + [{1}, {4, 5}, {2}], {1: {1}, 2: {2}, 4: {4, 5}, 5: {4, 5}}) + assert classify([[1,2,9],[2,4,5]], sort=False) == ( + [{1, 9}, {4, 5}, {2}], + {1: {1, 9}, 2: {2}, 4: {4, 5}, 5: {4, 5}, 9: {1, 9}}) + assert classify([[1,2,9,15],[2,4,5]], sort=False) == ( + [{1, 9, 15}, {4, 5}, {2}], + {1: {1, 9, 15}, 2: {2}, 4: {4, 5}, 5: {4, 5}, 9: {1, 9, 15}, + 15: {1, 9, 15}}) + classes, mapping = classify([[1,2,9,15],[2,4,5],[15,5]], sort=False) + assert set([frozenset(c) for c in classes]) == set( + [frozenset(s) for s in ({1, 9}, {4}, {2}, {5}, {15})]) + assert mapping == {1: {1, 9}, 2: {2}, 4: {4}, 5: {5}, 9: {1, 9}, 15: {15}} diff --git a/Tests/misc/eexec_test.py b/Tests/misc/eexec_test.py new file mode 100644 index 0000000..2043095 --- /dev/null +++ b/Tests/misc/eexec_test.py @@ -0,0 +1,17 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.eexec import decrypt, encrypt + + +def test_decrypt(): + testStr = b"\0\0asdadads asds\265" + decryptedStr, R = decrypt(testStr, 12321) + assert decryptedStr == b'0d\nh\x15\xe8\xc4\xb2\x15\x1d\x108\x1a<6\xa1' + assert R == 36142 + + +def test_encrypt(): + testStr = b'0d\nh\x15\xe8\xc4\xb2\x15\x1d\x108\x1a<6\xa1' + encryptedStr, R = encrypt(testStr, 12321) + assert encryptedStr == b"\0\0asdadads asds\265" + assert R == 36142 diff --git a/Tests/misc/encodingTools_test.py b/Tests/misc/encodingTools_test.py new file mode 100644 index 0000000..4c9628b --- /dev/null +++ b/Tests/misc/encodingTools_test.py @@ -0,0 +1,32 @@ +from __future__ import print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +import unittest +from fontTools.misc.encodingTools import getEncoding + +class EncodingTest(unittest.TestCase): + + def test_encoding_unicode(self): + + self.assertEqual(getEncoding(3, 0, None), "utf_16_be") # MS Symbol is Unicode as well + self.assertEqual(getEncoding(3, 1, None), "utf_16_be") + self.assertEqual(getEncoding(3, 10, None), "utf_16_be") + self.assertEqual(getEncoding(0, 3, None), "utf_16_be") + + def test_encoding_macroman_misc(self): + self.assertEqual(getEncoding(1, 0, 17), "mac_turkish") + self.assertEqual(getEncoding(1, 0, 37), "mac_romanian") + self.assertEqual(getEncoding(1, 0, 45), "mac_roman") + + def test_extended_mac_encodings(self): + encoding = getEncoding(1, 1, 0) # Mac Japanese + decoded = b'\xfe'.decode(encoding) + self.assertEqual(decoded, unichr(0x2122)) + + def test_extended_unknown(self): + self.assertEqual(getEncoding(10, 11, 12), None) + self.assertEqual(getEncoding(10, 11, 12, "ascii"), "ascii") + self.assertEqual(getEncoding(10, 11, 12, default="ascii"), "ascii") + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/misc/filenames_test.py b/Tests/misc/filenames_test.py new file mode 100644 index 0000000..47d137f --- /dev/null +++ b/Tests/misc/filenames_test.py @@ -0,0 +1,137 @@ +from __future__ import unicode_literals +import unittest +from fontTools.misc.filenames import ( + userNameToFileName, handleClash1, handleClash2) + + +class UserNameToFilenameTest(unittest.TestCase): + + def test_names(self): + self.assertEqual(userNameToFileName("a"),"a") + self.assertEqual(userNameToFileName("A"), "A_") + self.assertEqual(userNameToFileName("AE"), "A_E_") + self.assertEqual(userNameToFileName("Ae"), "A_e") + self.assertEqual(userNameToFileName("ae"), "ae") + self.assertEqual(userNameToFileName("aE"), "aE_") + self.assertEqual(userNameToFileName("a.alt"), "a.alt") + self.assertEqual(userNameToFileName("A.alt"), "A_.alt") + self.assertEqual(userNameToFileName("A.Alt"), "A_.A_lt") + self.assertEqual(userNameToFileName("A.aLt"), "A_.aL_t") + self.assertEqual(userNameToFileName(u"A.alT"), "A_.alT_") + self.assertEqual(userNameToFileName("T_H"), "T__H_") + self.assertEqual(userNameToFileName("T_h"), "T__h") + self.assertEqual(userNameToFileName("t_h"), "t_h") + self.assertEqual(userNameToFileName("F_F_I"), "F__F__I_") + self.assertEqual(userNameToFileName("f_f_i"), "f_f_i") + self.assertEqual( + userNameToFileName("Aacute_V.swash"), + "A_acute_V_.swash") + self.assertEqual(userNameToFileName(".notdef"), "_notdef") + self.assertEqual(userNameToFileName("con"), "_con") + self.assertEqual(userNameToFileName("CON"), "C_O_N_") + self.assertEqual(userNameToFileName("con.alt"), "_con.alt") + self.assertEqual(userNameToFileName("alt.con"), "alt._con") + + def test_prefix_suffix(self): + prefix = "TEST_PREFIX" + suffix = "TEST_SUFFIX" + name = "NAME" + name_file = "N_A_M_E_" + self.assertEqual( + userNameToFileName(name, prefix=prefix, suffix=suffix), + prefix + name_file + suffix) + + def test_collide(self): + prefix = "TEST_PREFIX" + suffix = "TEST_SUFFIX" + name = "NAME" + name_file = "N_A_M_E_" + collision_avoidance1 = "000000000000001" + collision_avoidance2 = "000000000000002" + exist = set() + generated = userNameToFileName( + name, exist, prefix=prefix, suffix=suffix) + exist.add(generated.lower()) + self.assertEqual(generated, prefix + name_file + suffix) + generated = userNameToFileName( + name, exist, prefix=prefix, suffix=suffix) + exist.add(generated.lower()) + self.assertEqual( + generated, + prefix + name_file + collision_avoidance1 + suffix) + generated = userNameToFileName( + name, exist, prefix=prefix, suffix=suffix) + self.assertEqual( + generated, + prefix + name_file + collision_avoidance2+ suffix) + + def test_ValueError(self): + with self.assertRaises(ValueError): + userNameToFileName(b"a") + with self.assertRaises(ValueError): + userNameToFileName({"a"}) + with self.assertRaises(ValueError): + userNameToFileName(("a",)) + with self.assertRaises(ValueError): + userNameToFileName(["a"]) + with self.assertRaises(ValueError): + userNameToFileName(["a"]) + with self.assertRaises(ValueError): + userNameToFileName(b"\xd8\x00") + + def test_handleClash1(self): + prefix = ("0" * 5) + "." + suffix = "." + ("0" * 10) + existing = ["a" * 5] + + e = list(existing) + self.assertEqual( + handleClash1(userName="A" * 5, existing=e, prefix=prefix, + suffix=suffix), + '00000.AAAAA000000000000001.0000000000' + ) + + e = list(existing) + e.append(prefix + "aaaaa" + "1".zfill(15) + suffix) + self.assertEqual( + handleClash1(userName="A" * 5, existing=e, prefix=prefix, + suffix=suffix), + '00000.AAAAA000000000000002.0000000000' + ) + + e = list(existing) + e.append(prefix + "AAAAA" + "2".zfill(15) + suffix) + self.assertEqual( + handleClash1(userName="A" * 5, existing=e, prefix=prefix, + suffix=suffix), + '00000.AAAAA000000000000001.0000000000' + ) + + def test_handleClash2(self): + prefix = ("0" * 5) + "." + suffix = "." + ("0" * 10) + existing = [prefix + str(i) + suffix for i in range(100)] + + e = list(existing) + self.assertEqual( + handleClash2(existing=e, prefix=prefix, suffix=suffix), + '00000.100.0000000000' + ) + + e = list(existing) + e.remove(prefix + "1" + suffix) + self.assertEqual( + handleClash2(existing=e, prefix=prefix, suffix=suffix), + '00000.1.0000000000' + ) + + e = list(existing) + e.remove(prefix + "2" + suffix) + self.assertEqual( + handleClash2(existing=e, prefix=prefix, suffix=suffix), + '00000.2.0000000000' + ) + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/misc/fixedTools_test.py b/Tests/misc/fixedTools_test.py new file mode 100644 index 0000000..5ef1777 --- /dev/null +++ b/Tests/misc/fixedTools_test.py @@ -0,0 +1,43 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import fixedToFloat, floatToFixed +import unittest + + +class FixedToolsTest(unittest.TestCase): + + def test_roundtrip(self): + for bits in range(0, 15): + for value in range(-(2**(bits+1)), 2**(bits+1)): + self.assertEqual(value, floatToFixed(fixedToFloat(value, bits), bits)) + + def test_fixedToFloat_precision14(self): + self.assertEqual(0.8, fixedToFloat(13107, 14)) + self.assertEqual(0.0, fixedToFloat(0, 14)) + self.assertEqual(1.0, fixedToFloat(16384, 14)) + self.assertEqual(-1.0, fixedToFloat(-16384, 14)) + self.assertEqual(0.99994, fixedToFloat(16383, 14)) + self.assertEqual(-0.99994, fixedToFloat(-16383, 14)) + + def test_fixedToFloat_precision6(self): + self.assertAlmostEqual(-9.98, fixedToFloat(-639, 6)) + self.assertAlmostEqual(-10.0, fixedToFloat(-640, 6)) + self.assertAlmostEqual(9.98, fixedToFloat(639, 6)) + self.assertAlmostEqual(10.0, fixedToFloat(640, 6)) + + def test_floatToFixed_precision14(self): + self.assertEqual(13107, floatToFixed(0.8, 14)) + self.assertEqual(16384, floatToFixed(1.0, 14)) + self.assertEqual(16384, floatToFixed(1, 14)) + self.assertEqual(-16384, floatToFixed(-1.0, 14)) + self.assertEqual(-16384, floatToFixed(-1, 14)) + self.assertEqual(0, floatToFixed(0, 14)) + + def test_fixedToFloat_return_float(self): + value = fixedToFloat(16384, 14) + self.assertIsInstance(value, float) + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/misc/loggingTools_test.py b/Tests/misc/loggingTools_test.py new file mode 100644 index 0000000..694c109 --- /dev/null +++ b/Tests/misc/loggingTools_test.py @@ -0,0 +1,210 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.loggingTools import ( + LevelFormatter, + Timer, + configLogger, + ChannelsFilter, + LogMixin, + StderrHandler, + LastResortLogger, + _resetExistingLoggers, +) +import logging +import textwrap +import time +import re +import sys +import pytest + + +def logger_name_generator(): + basename = "fontTools.test#" + num = 1 + while True: + yield basename+str(num) + num += 1 + +unique_logger_name = logger_name_generator() + + +@pytest.fixture +def logger(): + log = logging.getLogger(next(unique_logger_name)) + configLogger(logger=log, level="DEBUG", stream=StringIO()) + return log + + +def test_LevelFormatter(): + stream = StringIO() + handler = logging.StreamHandler(stream) + formatter = LevelFormatter( + fmt={ + '*': '[%(levelname)s] %(message)s', + 'DEBUG': '%(name)s [%(levelname)s] %(message)s', + 'INFO': '%(message)s', + }) + handler.setFormatter(formatter) + name = next(unique_logger_name) + log = logging.getLogger(name) + log.setLevel(logging.DEBUG) + log.addHandler(handler) + + log.debug("this uses a custom format string") + log.info("this also uses a custom format string") + log.warning("this one uses the default format string") + + assert stream.getvalue() == textwrap.dedent("""\ + %s [DEBUG] this uses a custom format string + this also uses a custom format string + [WARNING] this one uses the default format string + """ % name) + + +class TimerTest(object): + + def test_split(self): + timer = Timer() + time.sleep(0.01) + fist_lap = timer.split() + assert timer.elapsed == fist_lap + time.sleep(0.1) + second_lap = timer.split() + assert second_lap > fist_lap + assert timer.elapsed == second_lap + + def test_time(self): + timer = Timer() + time.sleep(0.01) + overall_time = timer.time() + assert overall_time > 0 + + def test_context_manager(self): + with Timer() as t: + time.sleep(0.01) + assert t.elapsed > 0 + + def test_using_logger(self, logger): + with Timer(logger, 'do something'): + time.sleep(0.01) + + assert re.match( + "Took [0-9]\.[0-9]{3}s to do something", + logger.handlers[0].stream.getvalue()) + + def test_using_logger_calling_instance(self, logger): + timer = Timer(logger) + with timer(): + time.sleep(0.01) + + assert re.match( + "elapsed time: [0-9]\.[0-9]{3}s", + logger.handlers[0].stream.getvalue()) + + # do it again but with custom level + with timer('redo it', level=logging.WARNING): + time.sleep(0.02) + + assert re.search( + "WARNING: Took [0-9]\.[0-9]{3}s to redo it", + logger.handlers[0].stream.getvalue()) + + def test_function_decorator(self, logger): + timer = Timer(logger) + + @timer() + def test1(): + time.sleep(0.01) + @timer('run test 2', level=logging.INFO) + def test2(): + time.sleep(0.02) + + test1() + + assert re.match( + "Took [0-9]\.[0-9]{3}s to run 'test1'", + logger.handlers[0].stream.getvalue()) + + test2() + + assert re.search( + "Took [0-9]\.[0-9]{3}s to run test 2", + logger.handlers[0].stream.getvalue()) + + +def test_ChannelsFilter(logger): + n = logger.name + filtr = ChannelsFilter(n+".A.B", n+".C.D") + handler = logger.handlers[0] + handler.addFilter(filtr) + stream = handler.stream + + logging.getLogger(n+".A.B").debug('this record passes through') + assert 'this record passes through' in stream.getvalue() + + logging.getLogger(n+'.A.B.C').debug('records from children also pass') + assert 'records from children also pass' in stream.getvalue() + + logging.getLogger(n+'.C.D').debug('this one as well') + assert 'this one as well' in stream.getvalue() + + logging.getLogger(n+'.A.B.').debug('also this one') + assert 'also this one' in stream.getvalue() + + before = stream.getvalue() + logging.getLogger(n+'.A.F').debug('but this one does not!') + assert before == stream.getvalue() + + logging.getLogger(n+'.C.DE').debug('neither this one!') + assert before == stream.getvalue() + + +def test_LogMixin(): + + class Base(object): + pass + + class A(LogMixin, Base): + pass + + class B(A): + pass + + a = A() + b = B() + + assert hasattr(a, 'log') + assert hasattr(b, 'log') + assert isinstance(a.log, logging.Logger) + assert isinstance(b.log, logging.Logger) + assert a.log.name == "loggingTools_test.A" + assert b.log.name == "loggingTools_test.B" + + +@pytest.mark.skipif(sys.version_info[:2] > (2, 7), reason="only for python2.7") +@pytest.mark.parametrize( + "reset", [True, False], ids=["reset", "no-reset"] +) +def test_LastResortLogger(reset, capsys, caplog): + current = logging.getLoggerClass() + msg = "The quick brown fox jumps over the lazy dog" + try: + if reset: + _resetExistingLoggers() + else: + caplog.set_level(logging.ERROR, logger="myCustomLogger") + logging.lastResort = StderrHandler(logging.WARNING) + logging.setLoggerClass(LastResortLogger) + logger = logging.getLogger("myCustomLogger") + logger.error(msg) + finally: + del logging.lastResort + logging.setLoggerClass(current) + + captured = capsys.readouterr() + if reset: + assert msg in captured.err + msg not in caplog.text + else: + msg in caplog.text + msg not in captured.err diff --git a/Tests/misc/macRes_test.py b/Tests/misc/macRes_test.py new file mode 100644 index 0000000..fde13f8 --- /dev/null +++ b/Tests/misc/macRes_test.py @@ -0,0 +1,97 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +import sys +import os +import tempfile +import unittest +from fontTools.misc.textTools import deHexStr +from fontTools.misc.macRes import ResourceReader + + +# test resource data in DeRez notation +""" +data 'TEST' (128, "name1") { $"4865 6C6C 6F" }; /* Hello */ +data 'TEST' (129, "name2") { $"576F 726C 64" }; /* World */ +data 'test' (130, "name3") { $"486F 7720 6172 6520 796F 753F" }; /* How are you? */ +""" +# the same data, compiled using Rez +# $ /usr/bin/Rez testdata.rez -o compiled +# $ hexdump -v compiled/..namedfork/rsrc +TEST_RSRC_FORK = deHexStr( + "00 00 01 00 00 00 01 22 00 00 00 22 00 00 00 64 " # 0x00000000 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x00000010 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x00000020 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x00000030 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x00000040 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x00000050 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x00000060 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x00000070 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x00000080 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x00000090 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x000000A0 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x000000B0 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x000000C0 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x000000D0 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x000000E0 + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x000000F0 + "00 00 00 05 48 65 6c 6c 6f 00 00 00 05 57 6f 72 " # 0x00000100 + "6c 64 00 00 00 0c 48 6f 77 20 61 72 65 20 79 6f " # 0x00000110 + "75 3f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " # 0x00000120 + "00 00 00 00 00 00 00 00 00 00 00 1c 00 52 00 01 " # 0x00000130 + "54 45 53 54 00 01 00 12 74 65 73 74 00 00 00 2a " # 0x00000140 + "00 80 00 00 00 00 00 00 00 00 00 00 00 81 00 06 " # 0x00000150 + "00 00 00 09 00 00 00 00 00 82 00 0c 00 00 00 12 " # 0x00000160 + "00 00 00 00 05 6e 61 6d 65 31 05 6e 61 6d 65 32 " # 0x00000170 + "05 6e 61 6d 65 33 " # 0x00000180 +) + + +class ResourceReaderTest(unittest.TestCase): + + def test_read_file(self): + infile = BytesIO(TEST_RSRC_FORK) + reader = ResourceReader(infile) + resources = [res for typ in reader.keys() for res in reader[typ]] + self.assertExpected(resources) + + def test_read_datafork(self): + with tempfile.NamedTemporaryFile(delete=False) as tmp: + tmp.write(TEST_RSRC_FORK) + try: + reader = ResourceReader(tmp.name) + resources = [res for typ in reader.keys() for res in reader[typ]] + reader.close() + self.assertExpected(resources) + finally: + os.remove(tmp.name) + + def test_read_namedfork_rsrc(self): + if sys.platform != 'darwin': + self.skipTest('Not supported on "%s"' % sys.platform) + tmp = tempfile.NamedTemporaryFile(delete=False) + tmp.close() + try: + with open(tmp.name + '/..namedfork/rsrc', 'wb') as fork: + fork.write(TEST_RSRC_FORK) + reader = ResourceReader(tmp.name) + resources = [res for typ in reader.keys() for res in reader[typ]] + reader.close() + self.assertExpected(resources) + finally: + os.remove(tmp.name) + + def assertExpected(self, resources): + self.assertRezEqual(resources[0], 'TEST', b'Hello', 128, 'name1') + self.assertRezEqual(resources[1], 'TEST', b'World', 129, 'name2') + self.assertRezEqual( + resources[2], 'test', b'How are you?', 130, 'name3') + + def assertRezEqual(self, res, type_, data, id, name): + self.assertEqual(res.type, type_) + self.assertEqual(res.data, data) + self.assertEqual(res.id, id) + self.assertEqual(res.name, name) + + +if __name__ == '__main__': + sys.exit(unittest.main()) diff --git a/Tests/misc/psCharStrings_test.py b/Tests/misc/psCharStrings_test.py new file mode 100644 index 0000000..1e36246 --- /dev/null +++ b/Tests/misc/psCharStrings_test.py @@ -0,0 +1,32 @@ +from __future__ import print_function, division, absolute_import +from fontTools.cffLib import PrivateDict +from fontTools.cffLib.specializer import stringToProgram +from fontTools.misc.psCharStrings import T2CharString +import unittest + + +class T2CharStringTest(unittest.TestCase): + + @classmethod + def stringToT2CharString(cls, string): + return T2CharString(program=stringToProgram(string), private=PrivateDict()) + + def test_calcBounds_empty(self): + cs = self.stringToT2CharString("endchar") + bounds = cs.calcBounds(None) + self.assertEqual(bounds, None) + + def test_calcBounds_line(self): + cs = self.stringToT2CharString("100 100 rmoveto 40 10 rlineto -20 50 rlineto endchar") + bounds = cs.calcBounds(None) + self.assertEqual(bounds, (100, 100, 140, 160)) + + def test_calcBounds_curve(self): + cs = self.stringToT2CharString("100 100 rmoveto -50 -150 200 0 -50 150 rrcurveto endchar") + bounds = cs.calcBounds(None) + self.assertEqual(bounds, (91.90524980688875, -12.5, 208.09475019311125, 100)) + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/misc/py23_test.py b/Tests/misc/py23_test.py new file mode 100644 index 0000000..22b8044 --- /dev/null +++ b/Tests/misc/py23_test.py @@ -0,0 +1,485 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.textTools import deHexStr +import filecmp +import tempfile +from subprocess import check_call +import sys +import os +import unittest + +from fontTools.misc.py23 import ( + round2, round3, isclose, redirect_stdout, redirect_stderr) + + +PIPE_SCRIPT = """\ +import sys +binary_stdin = open(sys.stdin.fileno(), mode='rb', closefd=False) +binary_stdout = open(sys.stdout.fileno(), mode='wb', closefd=False) +binary_stdout.write(binary_stdin.read()) +""" + +# the string contains a mix of line endings, plus the Win "EOF" charater (0x1A) +# 'hello\rworld\r\n\x1a\r\n' +TEST_BIN_DATA = deHexStr( + "68 65 6c 6c 6f 0d 77 6f 72 6c 64 0d 0a 1a 0d 0a" +) + +class OpenFuncWrapperTest(unittest.TestCase): + + @staticmethod + def make_temp(data): + with tempfile.NamedTemporaryFile(delete=False) as f: + f.write(tobytes(data)) + return f.name + + def diff_piped(self, data, import_statement): + script = self.make_temp("\n".join([import_statement, PIPE_SCRIPT])) + datafile = self.make_temp(data) + try: + with open(datafile, 'rb') as infile, \ + tempfile.NamedTemporaryFile(delete=False) as outfile: + env = dict(os.environ) + env["PYTHONPATH"] = os.pathsep.join(sys.path) + check_call( + [sys.executable, script], stdin=infile, stdout=outfile, + env=env) + result = not filecmp.cmp(infile.name, outfile.name, shallow=False) + finally: + os.remove(script) + os.remove(datafile) + os.remove(outfile.name) + return result + + def test_binary_pipe_py23_open_wrapper(self): + if self.diff_piped( + TEST_BIN_DATA, "from fontTools.misc.py23 import open"): + self.fail("Input and output data differ!") + + def test_binary_pipe_built_in_io_open(self): + if sys.version_info.major < 3 and sys.platform == 'win32': + # On Windows Python 2.x, the piped input and output data are + # expected to be different when using io.open, because of issue + # https://bugs.python.org/issue10841. + expected = True + else: + expected = False + result = self.diff_piped(TEST_BIN_DATA, "from io import open") + self.assertEqual(result, expected) + + +class Round2Test(unittest.TestCase): + """ + Test cases taken from cpython 2.7 test suite: + + https://github.com/python/cpython/blob/2.7/Lib/test/test_float.py#L748 + + Excludes the test cases that are not supported when using the `decimal` + module's `quantize` method. + """ + + def test_second_argument_type(self): + # floats should be illegal + self.assertRaises(TypeError, round2, 3.14159, 2.0) + + def test_halfway_cases(self): + # Halfway cases need special attention, since the current + # implementation has to deal with them specially. Note that + # 2.x rounds halfway values up (i.e., away from zero) while + # 3.x does round-half-to-even. + self.assertAlmostEqual(round2(0.125, 2), 0.13) + self.assertAlmostEqual(round2(0.375, 2), 0.38) + self.assertAlmostEqual(round2(0.625, 2), 0.63) + self.assertAlmostEqual(round2(0.875, 2), 0.88) + self.assertAlmostEqual(round2(-0.125, 2), -0.13) + self.assertAlmostEqual(round2(-0.375, 2), -0.38) + self.assertAlmostEqual(round2(-0.625, 2), -0.63) + self.assertAlmostEqual(round2(-0.875, 2), -0.88) + + self.assertAlmostEqual(round2(0.25, 1), 0.3) + self.assertAlmostEqual(round2(0.75, 1), 0.8) + self.assertAlmostEqual(round2(-0.25, 1), -0.3) + self.assertAlmostEqual(round2(-0.75, 1), -0.8) + + self.assertEqual(round2(-6.5, 0), -7.0) + self.assertEqual(round2(-5.5, 0), -6.0) + self.assertEqual(round2(-1.5, 0), -2.0) + self.assertEqual(round2(-0.5, 0), -1.0) + self.assertEqual(round2(0.5, 0), 1.0) + self.assertEqual(round2(1.5, 0), 2.0) + self.assertEqual(round2(2.5, 0), 3.0) + self.assertEqual(round2(3.5, 0), 4.0) + self.assertEqual(round2(4.5, 0), 5.0) + self.assertEqual(round2(5.5, 0), 6.0) + self.assertEqual(round2(6.5, 0), 7.0) + + # same but without an explicit second argument; in 3.x these + # will give integers + self.assertEqual(round2(-6.5), -7.0) + self.assertEqual(round2(-5.5), -6.0) + self.assertEqual(round2(-1.5), -2.0) + self.assertEqual(round2(-0.5), -1.0) + self.assertEqual(round2(0.5), 1.0) + self.assertEqual(round2(1.5), 2.0) + self.assertEqual(round2(2.5), 3.0) + self.assertEqual(round2(3.5), 4.0) + self.assertEqual(round2(4.5), 5.0) + self.assertEqual(round2(5.5), 6.0) + self.assertEqual(round2(6.5), 7.0) + + self.assertEqual(round2(-25.0, -1), -30.0) + self.assertEqual(round2(-15.0, -1), -20.0) + self.assertEqual(round2(-5.0, -1), -10.0) + self.assertEqual(round2(5.0, -1), 10.0) + self.assertEqual(round2(15.0, -1), 20.0) + self.assertEqual(round2(25.0, -1), 30.0) + self.assertEqual(round2(35.0, -1), 40.0) + self.assertEqual(round2(45.0, -1), 50.0) + self.assertEqual(round2(55.0, -1), 60.0) + self.assertEqual(round2(65.0, -1), 70.0) + self.assertEqual(round2(75.0, -1), 80.0) + self.assertEqual(round2(85.0, -1), 90.0) + self.assertEqual(round2(95.0, -1), 100.0) + self.assertEqual(round2(12325.0, -1), 12330.0) + self.assertEqual(round2(0, -1), 0.0) + + self.assertEqual(round2(350.0, -2), 400.0) + self.assertEqual(round2(450.0, -2), 500.0) + + self.assertAlmostEqual(round2(0.5e21, -21), 1e21) + self.assertAlmostEqual(round2(1.5e21, -21), 2e21) + self.assertAlmostEqual(round2(2.5e21, -21), 3e21) + self.assertAlmostEqual(round2(5.5e21, -21), 6e21) + self.assertAlmostEqual(round2(8.5e21, -21), 9e21) + + self.assertAlmostEqual(round2(-1.5e22, -22), -2e22) + self.assertAlmostEqual(round2(-0.5e22, -22), -1e22) + self.assertAlmostEqual(round2(0.5e22, -22), 1e22) + self.assertAlmostEqual(round2(1.5e22, -22), 2e22) + + +class Round3Test(unittest.TestCase): + """ Same as above but results adapted for Python 3 round() """ + + def test_second_argument_type(self): + # floats should be illegal + self.assertRaises(TypeError, round3, 3.14159, 2.0) + + # None should be allowed + self.assertEqual(round3(1.0, None), 1) + # the following would raise an error with the built-in Python3.5 round: + # TypeError: 'NoneType' object cannot be interpreted as an integer + self.assertEqual(round3(1, None), 1) + + def test_halfway_cases(self): + self.assertAlmostEqual(round3(0.125, 2), 0.12) + self.assertAlmostEqual(round3(0.375, 2), 0.38) + self.assertAlmostEqual(round3(0.625, 2), 0.62) + self.assertAlmostEqual(round3(0.875, 2), 0.88) + self.assertAlmostEqual(round3(-0.125, 2), -0.12) + self.assertAlmostEqual(round3(-0.375, 2), -0.38) + self.assertAlmostEqual(round3(-0.625, 2), -0.62) + self.assertAlmostEqual(round3(-0.875, 2), -0.88) + + self.assertAlmostEqual(round3(0.25, 1), 0.2) + self.assertAlmostEqual(round3(0.75, 1), 0.8) + self.assertAlmostEqual(round3(-0.25, 1), -0.2) + self.assertAlmostEqual(round3(-0.75, 1), -0.8) + + self.assertEqual(round3(-6.5, 0), -6.0) + self.assertEqual(round3(-5.5, 0), -6.0) + self.assertEqual(round3(-1.5, 0), -2.0) + self.assertEqual(round3(-0.5, 0), 0.0) + self.assertEqual(round3(0.5, 0), 0.0) + self.assertEqual(round3(1.5, 0), 2.0) + self.assertEqual(round3(2.5, 0), 2.0) + self.assertEqual(round3(3.5, 0), 4.0) + self.assertEqual(round3(4.5, 0), 4.0) + self.assertEqual(round3(5.5, 0), 6.0) + self.assertEqual(round3(6.5, 0), 6.0) + + # same but without an explicit second argument; in 2.x these + # will give floats + self.assertEqual(round3(-6.5), -6) + self.assertEqual(round3(-5.5), -6) + self.assertEqual(round3(-1.5), -2.0) + self.assertEqual(round3(-0.5), 0) + self.assertEqual(round3(0.5), 0) + self.assertEqual(round3(1.5), 2) + self.assertEqual(round3(2.5), 2) + self.assertEqual(round3(3.5), 4) + self.assertEqual(round3(4.5), 4) + self.assertEqual(round3(5.5), 6) + self.assertEqual(round3(6.5), 6) + + # no ndigits and input is already an integer: output == input + rv = round3(1) + self.assertEqual(rv, 1) + self.assertTrue(isinstance(rv, int)) + rv = round3(1.0) + self.assertEqual(rv, 1) + self.assertTrue(isinstance(rv, int)) + + self.assertEqual(round3(-25.0, -1), -20.0) + self.assertEqual(round3(-15.0, -1), -20.0) + self.assertEqual(round3(-5.0, -1), 0.0) + self.assertEqual(round3(5.0, -1), 0.0) + self.assertEqual(round3(15.0, -1), 20.0) + self.assertEqual(round3(25.0, -1), 20.0) + self.assertEqual(round3(35.0, -1), 40.0) + self.assertEqual(round3(45.0, -1), 40.0) + self.assertEqual(round3(55.0, -1), 60.0) + self.assertEqual(round3(65.0, -1), 60.0) + self.assertEqual(round3(75.0, -1), 80.0) + self.assertEqual(round3(85.0, -1), 80.0) + self.assertEqual(round3(95.0, -1), 100.0) + self.assertEqual(round3(12325.0, -1), 12320.0) + self.assertEqual(round3(0, -1), 0.0) + + self.assertEqual(round3(350.0, -2), 400.0) + self.assertEqual(round3(450.0, -2), 400.0) + + self.assertAlmostEqual(round3(0.5e21, -21), 0.0) + self.assertAlmostEqual(round3(1.5e21, -21), 2e21) + self.assertAlmostEqual(round3(2.5e21, -21), 2e21) + self.assertAlmostEqual(round3(5.5e21, -21), 6e21) + self.assertAlmostEqual(round3(8.5e21, -21), 8e21) + + self.assertAlmostEqual(round3(-1.5e22, -22), -2e22) + self.assertAlmostEqual(round3(-0.5e22, -22), 0.0) + self.assertAlmostEqual(round3(0.5e22, -22), 0.0) + self.assertAlmostEqual(round3(1.5e22, -22), 2e22) + + +NAN = float('nan') +INF = float('inf') +NINF = float('-inf') + + +class IsCloseTests(unittest.TestCase): + """ + Tests taken from Python 3.5 test_math.py: + https://hg.python.org/cpython/file/v3.5.2/Lib/test/test_math.py + """ + isclose = staticmethod(isclose) + + def assertIsClose(self, a, b, *args, **kwargs): + self.assertTrue( + self.isclose(a, b, *args, **kwargs), + msg="%s and %s should be close!" % (a, b)) + + def assertIsNotClose(self, a, b, *args, **kwargs): + self.assertFalse( + self.isclose(a, b, *args, **kwargs), + msg="%s and %s should not be close!" % (a, b)) + + def assertAllClose(self, examples, *args, **kwargs): + for a, b in examples: + self.assertIsClose(a, b, *args, **kwargs) + + def assertAllNotClose(self, examples, *args, **kwargs): + for a, b in examples: + self.assertIsNotClose(a, b, *args, **kwargs) + + def test_negative_tolerances(self): + # ValueError should be raised if either tolerance is less than zero + with self.assertRaises(ValueError): + self.assertIsClose(1, 1, rel_tol=-1e-100) + with self.assertRaises(ValueError): + self.assertIsClose(1, 1, rel_tol=1e-100, abs_tol=-1e10) + + def test_identical(self): + # identical values must test as close + identical_examples = [ + (2.0, 2.0), + (0.1e200, 0.1e200), + (1.123e-300, 1.123e-300), + (12345, 12345.0), + (0.0, -0.0), + (345678, 345678)] + self.assertAllClose(identical_examples, rel_tol=0.0, abs_tol=0.0) + + def test_eight_decimal_places(self): + # examples that are close to 1e-8, but not 1e-9 + eight_decimal_places_examples = [ + (1e8, 1e8 + 1), + (-1e-8, -1.000000009e-8), + (1.12345678, 1.12345679)] + self.assertAllClose(eight_decimal_places_examples, rel_tol=1e-8) + self.assertAllNotClose(eight_decimal_places_examples, rel_tol=1e-9) + + def test_near_zero(self): + # values close to zero + near_zero_examples = [ + (1e-9, 0.0), + (-1e-9, 0.0), + (-1e-150, 0.0)] + # these should not be close to any rel_tol + self.assertAllNotClose(near_zero_examples, rel_tol=0.9) + # these should be close to abs_tol=1e-8 + self.assertAllClose(near_zero_examples, abs_tol=1e-8) + + def test_identical_infinite(self): + # these are close regardless of tolerance -- i.e. they are equal + self.assertIsClose(INF, INF) + self.assertIsClose(INF, INF, abs_tol=0.0) + self.assertIsClose(NINF, NINF) + self.assertIsClose(NINF, NINF, abs_tol=0.0) + + def test_inf_ninf_nan(self): + # these should never be close (following IEEE 754 rules for equality) + not_close_examples = [ + (NAN, NAN), + (NAN, 1e-100), + (1e-100, NAN), + (INF, NAN), + (NAN, INF), + (INF, NINF), + (INF, 1.0), + (1.0, INF), + (INF, 1e308), + (1e308, INF)] + # use largest reasonable tolerance + self.assertAllNotClose(not_close_examples, abs_tol=0.999999999999999) + + def test_zero_tolerance(self): + # test with zero tolerance + zero_tolerance_close_examples = [ + (1.0, 1.0), + (-3.4, -3.4), + (-1e-300, -1e-300)] + self.assertAllClose(zero_tolerance_close_examples, rel_tol=0.0) + + zero_tolerance_not_close_examples = [ + (1.0, 1.000000000000001), + (0.99999999999999, 1.0), + (1.0e200, .999999999999999e200)] + self.assertAllNotClose(zero_tolerance_not_close_examples, rel_tol=0.0) + + def test_assymetry(self): + # test the assymetry example from PEP 485 + self.assertAllClose([(9, 10), (10, 9)], rel_tol=0.1) + + def test_integers(self): + # test with integer values + integer_examples = [ + (100000001, 100000000), + (123456789, 123456788)] + + self.assertAllClose(integer_examples, rel_tol=1e-8) + self.assertAllNotClose(integer_examples, rel_tol=1e-9) + + def test_decimals(self): + # test with Decimal values + from decimal import Decimal + + decimal_examples = [ + (Decimal('1.00000001'), Decimal('1.0')), + (Decimal('1.00000001e-20'), Decimal('1.0e-20')), + (Decimal('1.00000001e-100'), Decimal('1.0e-100'))] + self.assertAllClose(decimal_examples, rel_tol=1e-8) + self.assertAllNotClose(decimal_examples, rel_tol=1e-9) + + def test_fractions(self): + # test with Fraction values + from fractions import Fraction + + # could use some more examples here! + fraction_examples = [(Fraction(1, 100000000) + 1, Fraction(1))] + self.assertAllClose(fraction_examples, rel_tol=1e-8) + self.assertAllNotClose(fraction_examples, rel_tol=1e-9) + + +@unittest.skipUnless( + (sys.version_info[0] == 2 and sys.maxunicode < 0x10FFFF), + "requires 'narrow' Python 2.7 build") +class NarrowUnicodeBuildTest(unittest.TestCase): + + def test_unichr(self): + from __builtin__ import unichr as narrow_unichr + + self.assertRaises( + ValueError, + narrow_unichr, 0xFFFF + 1) + + self.assertEqual(unichr(1114111), u'\U0010FFFF') + + self.assertRaises( + ValueError, + unichr, 0x10FFFF + 1) + + def test_byteord(self): + from __builtin__ import ord as narrow_ord + + self.assertRaises( + TypeError, + narrow_ord, u'\U00010000') + + self.assertEqual(byteord(u'\U00010000'), 0xFFFF + 1) + self.assertEqual(byteord(u'\U0010FFFF'), 1114111) + + +class TestRedirectStream: + + redirect_stream = None + orig_stream = None + + def test_no_redirect_in_init(self): + orig_stdout = getattr(sys, self.orig_stream) + self.redirect_stream(None) + self.assertIs(getattr(sys, self.orig_stream), orig_stdout) + + def test_redirect_to_string_io(self): + f = StringIO() + msg = "Consider an API like help(), which prints directly to stdout" + orig_stdout = getattr(sys, self.orig_stream) + with self.redirect_stream(f): + print(msg, file=getattr(sys, self.orig_stream)) + self.assertIs(getattr(sys, self.orig_stream), orig_stdout) + s = f.getvalue().strip() + self.assertEqual(s, msg) + + def test_enter_result_is_target(self): + f = StringIO() + with self.redirect_stream(f) as enter_result: + self.assertIs(enter_result, f) + + def test_cm_is_reusable(self): + f = StringIO() + write_to_f = self.redirect_stream(f) + orig_stdout = getattr(sys, self.orig_stream) + with write_to_f: + print("Hello", end=" ", file=getattr(sys, self.orig_stream)) + with write_to_f: + print("World!", file=getattr(sys, self.orig_stream)) + self.assertIs(getattr(sys, self.orig_stream), orig_stdout) + s = f.getvalue() + self.assertEqual(s, "Hello World!\n") + + def test_cm_is_reentrant(self): + f = StringIO() + write_to_f = self.redirect_stream(f) + orig_stdout = getattr(sys, self.orig_stream) + with write_to_f: + print("Hello", end=" ", file=getattr(sys, self.orig_stream)) + with write_to_f: + print("World!", file=getattr(sys, self.orig_stream)) + self.assertIs(getattr(sys, self.orig_stream), orig_stdout) + s = f.getvalue() + self.assertEqual(s, "Hello World!\n") + + +class TestRedirectStdout(TestRedirectStream, unittest.TestCase): + + redirect_stream = redirect_stdout + orig_stream = "stdout" + + +class TestRedirectStderr(TestRedirectStream, unittest.TestCase): + + redirect_stream = redirect_stderr + orig_stream = "stderr" + + +if __name__ == "__main__": + sys.exit(unittest.main()) diff --git a/Tests/misc/testTools_test.py b/Tests/misc/testTools_test.py new file mode 100644 index 0000000..11c04b1 --- /dev/null +++ b/Tests/misc/testTools_test.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.misc.py23 import * +import fontTools.misc.testTools as testTools +import unittest + + +class TestToolsTest(unittest.TestCase): + + def test_parseXML_str(self): + self.assertEqual(testTools.parseXML( + '<Foo n="1"/>' + '<Foo n="2">' + ' some ünıcòðe text' + ' <Bar color="red"/>' + ' some more text' + '</Foo>' + '<Foo n="3"/>'), [ + ("Foo", {"n": "1"}, []), + ("Foo", {"n": "2"}, [ + " some ünıcòðe text ", + ("Bar", {"color": "red"}, []), + " some more text", + ]), + ("Foo", {"n": "3"}, []) + ]) + + def test_parseXML_bytes(self): + self.assertEqual(testTools.parseXML( + b'<Foo n="1"/>' + b'<Foo n="2">' + b' some \xc3\xbcn\xc4\xb1c\xc3\xb2\xc3\xb0e text' + b' <Bar color="red"/>' + b' some more text' + b'</Foo>' + b'<Foo n="3"/>'), [ + ("Foo", {"n": "1"}, []), + ("Foo", {"n": "2"}, [ + " some ünıcòðe text ", + ("Bar", {"color": "red"}, []), + " some more text", + ]), + ("Foo", {"n": "3"}, []) + ]) + + def test_parseXML_str_list(self): + self.assertEqual(testTools.parseXML( + ['<Foo n="1"/>' + '<Foo n="2"/>']), [ + ("Foo", {"n": "1"}, []), + ("Foo", {"n": "2"}, []) + ]) + + def test_parseXML_bytes_list(self): + self.assertEqual(testTools.parseXML( + [b'<Foo n="1"/>' + b'<Foo n="2"/>']), [ + ("Foo", {"n": "1"}, []), + ("Foo", {"n": "2"}, []) + ]) + + def test_getXML(self): + def toXML(writer, ttFont): + writer.simpletag("simple") + writer.newline() + writer.begintag("tag", attr='value') + writer.newline() + writer.write("hello world") + writer.newline() + writer.endtag("tag") + writer.newline() # toXML always ends with a newline + + self.assertEqual(testTools.getXML(toXML), + ['<simple/>', + '<tag attr="value">', + ' hello world', + '</tag>']) + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/misc/textTools_test.py b/Tests/misc/textTools_test.py new file mode 100644 index 0000000..547569a --- /dev/null +++ b/Tests/misc/textTools_test.py @@ -0,0 +1,11 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.textTools import pad + + +def test_pad(): + assert len(pad(b'abcd', 4)) == 4 + assert len(pad(b'abcde', 2)) == 6 + assert len(pad(b'abcde', 4)) == 8 + assert pad(b'abcdef', 4) == b'abcdef\x00\x00' + assert pad(b'abcdef', 1) == b'abcdef' diff --git a/Tests/misc/timeTools_test.py b/Tests/misc/timeTools_test.py new file mode 100644 index 0000000..6c98f64 --- /dev/null +++ b/Tests/misc/timeTools_test.py @@ -0,0 +1,24 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.timeTools import asctime, timestampNow, epoch_diff +import os +import time +import pytest + + +def test_asctime(): + assert isinstance(asctime(), basestring) + assert asctime(time.gmtime(0)) == 'Thu Jan 1 00:00:00 1970' + + +def test_source_date_epoch(): + os.environ["SOURCE_DATE_EPOCH"] = "150687315" + assert timestampNow() + epoch_diff == 150687315 + + # Check that malformed value fail, any better way? + os.environ["SOURCE_DATE_EPOCH"] = "ABCDEFGHI" + with pytest.raises(ValueError): + timestampNow() + + del os.environ["SOURCE_DATE_EPOCH"] + assert timestampNow() + epoch_diff != 150687315 diff --git a/Tests/misc/transform_test.py b/Tests/misc/transform_test.py new file mode 100644 index 0000000..7a7a67d --- /dev/null +++ b/Tests/misc/transform_test.py @@ -0,0 +1,101 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.transform import Transform, Identity, Offset, Scale +import math +import pytest + + +class TransformTest(object): + + def test_examples(self): + t = Transform() + assert repr(t) == "<Transform [1 0 0 1 0 0]>" + assert t.scale(2) == Transform(2, 0, 0, 2, 0, 0) + assert t.scale(2.5, 5.5) == Transform(2.5, 0, 0, 5.5, 0, 0) + assert t.scale(2, 3).transformPoint((100, 100)) == (200, 300) + + def test__init__(self): + assert Transform(12) == Transform(12, 0, 0, 1, 0, 0) + assert Transform(dx=12) == Transform(1, 0, 0, 1, 12, 0) + assert Transform(yx=12) == Transform(1, 0, 12, 1, 0, 0) + + def test_transformPoints(self): + t = Transform(2, 0, 0, 3, 0, 0) + assert t.transformPoints( + [(0, 0), (0, 100), (100, 100), (100, 0)] + ) == [(0, 0), (0, 300), (200, 300), (200, 0)] + + def test_translate(self): + t = Transform() + assert t.translate(20, 30) == Transform(1, 0, 0, 1, 20, 30) + + def test_scale(self): + t = Transform() + assert t.scale(5) == Transform(5, 0, 0, 5, 0, 0) + assert t.scale(5, 6) == Transform(5, 0, 0, 6, 0, 0) + + def test_rotate(self): + t = Transform() + assert t.rotate(math.pi / 2) == Transform(0, 1, -1, 0, 0, 0) + t = Transform() + assert t.rotate(-math.pi / 2) == Transform(0, -1, 1, 0, 0, 0) + t = Transform() + assert tuple(t.rotate(math.radians(30))) == pytest.approx( + tuple(Transform(0.866025, 0.5, -0.5, 0.866025, 0, 0))) + + def test_skew(self): + t = Transform().skew(math.pi / 4) + assert tuple(t) == pytest.approx(tuple(Transform(1, 0, 1, 1, 0, 0))) + + def test_transform(self): + t = Transform(2, 0, 0, 3, 1, 6) + assert t.transform((4, 3, 2, 1, 5, 6)) == Transform(8, 9, 4, 3, 11, 24) + + def test_reverseTransform(self): + t = Transform(2, 0, 0, 3, 1, 6) + reverse_t = t.reverseTransform((4, 3, 2, 1, 5, 6)) + assert reverse_t == Transform(8, 6, 6, 3, 21, 15) + t = Transform(4, 3, 2, 1, 5, 6) + reverse_t = t.transform((2, 0, 0, 3, 1, 6)) + assert reverse_t == Transform(8, 6, 6, 3, 21, 15) + + def test_inverse(self): + t = Transform().translate(2, 3).scale(4, 5) + assert t.transformPoint((10, 20)) == (42, 103) + it = t.inverse() + assert it.transformPoint((42, 103)) == (10.0, 20.0) + assert Transform().inverse() == Transform() + + def test_toPS(self): + t = Transform().scale(2, 3).translate(4, 5) + assert t.toPS() == '[2 0 0 3 8 15]' + + def test__ne__(self): + assert Transform() != Transform(2, 0, 0, 2, 0, 0) + + def test__hash__(self): + t = Transform(12, 0, 0, 13, 0, 0) + d = {t: None} + assert t in d.keys() + + def test__bool__(self): + assert not bool(Transform()) + assert Transform(2, 0, 0, 2, 0, 0) + assert Transform(1, 0, 0, 1, 1, 0) + + def test__repr__(self): + assert repr(Transform(1, 2, 3, 4, 5, 6)) == '<Transform [1 2 3 4 5 6]>' + + def test_Identity(self): + assert isinstance(Identity, Transform) + assert Identity == Transform(1, 0, 0, 1, 0, 0) + + def test_Offset(self): + assert Offset() == Transform(1, 0, 0, 1, 0, 0) + assert Offset(1) == Transform(1, 0, 0, 1, 1, 0) + assert Offset(1, 2) == Transform(1, 0, 0, 1, 1, 2) + + def test_Scale(self): + assert Scale(1) == Transform(1, 0, 0, 1, 0, 0) + assert Scale(2) == Transform(2, 0, 0, 2, 0, 0) + assert Scale(1, 2) == Transform(1, 0, 0, 2, 0, 0) diff --git a/Tests/misc/xmlReader_test.py b/Tests/misc/xmlReader_test.py new file mode 100644 index 0000000..33fddcc --- /dev/null +++ b/Tests/misc/xmlReader_test.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- + +from __future__ import print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +import os +import unittest +from fontTools.ttLib import TTFont +from fontTools.misc.xmlReader import XMLReader, ProgressPrinter, BUFSIZE +import tempfile + + +class TestXMLReader(unittest.TestCase): + + def test_decode_utf8(self): + + class DebugXMLReader(XMLReader): + + def __init__(self, fileOrPath, ttFont, progress=None): + super(DebugXMLReader, self).__init__( + fileOrPath, ttFont, progress) + self.contents = [] + + def _endElementHandler(self, name): + if self.stackSize == 3: + name, attrs, content = self.root + self.contents.append(content) + super(DebugXMLReader, self)._endElementHandler(name) + + expected = 'fôôbär' + data = '''\ +<?xml version="1.0" encoding="UTF-8"?> +<ttFont> + <name> + <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409"> + %s + </namerecord> + </name> +</ttFont> +''' % expected + + with BytesIO(data.encode('utf-8')) as tmp: + reader = DebugXMLReader(tmp, TTFont()) + reader.read() + content = strjoin(reader.contents[0]).strip() + self.assertEqual(expected, content) + + def test_normalise_newlines(self): + + class DebugXMLReader(XMLReader): + + def __init__(self, fileOrPath, ttFont, progress=None): + super(DebugXMLReader, self).__init__( + fileOrPath, ttFont, progress) + self.newlines = [] + + def _characterDataHandler(self, data): + self.newlines.extend([c for c in data if c in ('\r', '\n')]) + + # notice how when CR is escaped, it is not normalised by the XML parser + data = ( + '<ttFont>\r' # \r -> \n + ' <test>\r\n' # \r\n -> \n + ' a line of text\n' # \n + ' escaped CR and unix newline &#13;\n' # &#13;\n -> \r\n + ' escaped CR and macintosh newline &#13;\r' # &#13;\r -> \r\n + ' escaped CR and windows newline &#13;\r\n' # &#13;\r\n -> \r\n + ' </test>\n' # \n + '</ttFont>') + + with BytesIO(data.encode('utf-8')) as tmp: + reader = DebugXMLReader(tmp, TTFont()) + reader.read() + expected = ['\n'] * 3 + ['\r', '\n'] * 3 + ['\n'] + self.assertEqual(expected, reader.newlines) + + def test_progress(self): + + class DummyProgressPrinter(ProgressPrinter): + + def __init__(self, title, maxval=100): + self.label = title + self.maxval = maxval + self.pos = 0 + + def set(self, val, maxval=None): + if maxval is not None: + self.maxval = maxval + self.pos = val + + def increment(self, val=1): + self.pos += val + + def setLabel(self, text): + self.label = text + + data = ( + '<ttFont>\n' + ' <test>\n' + ' %s\n' + ' </test>\n' + '</ttFont>\n' + % ("z" * 2 * BUFSIZE) + ).encode('utf-8') + + dataSize = len(data) + progressBar = DummyProgressPrinter('test') + with BytesIO(data) as tmp: + reader = XMLReader(tmp, TTFont(), progress=progressBar) + self.assertEqual(progressBar.pos, 0) + reader.read() + self.assertEqual(progressBar.pos, dataSize // 100) + self.assertEqual(progressBar.maxval, dataSize // 100) + self.assertTrue('test' in progressBar.label) + with BytesIO(b"<ttFont></ttFont>") as tmp: + reader = XMLReader(tmp, TTFont(), progress=progressBar) + reader.read() + # when data size is less than 100 bytes, 'maxval' is 1 + self.assertEqual(progressBar.maxval, 1) + + def test_close_file_path(self): + with tempfile.NamedTemporaryFile(delete=False) as tmp: + tmp.write(b'<ttFont></ttFont>') + reader = XMLReader(tmp.name, TTFont()) + reader.read() + # when reading from path, the file is closed automatically at the end + self.assertTrue(reader.file.closed) + # this does nothing + reader.close() + self.assertTrue(reader.file.closed) + os.remove(tmp.name) + + def test_close_file_obj(self): + with tempfile.NamedTemporaryFile(delete=False) as tmp: + tmp.write(b'<ttFont>"hello"</ttFont>') + with open(tmp.name, "rb") as f: + reader = XMLReader(f, TTFont()) + reader.read() + # when reading from a file or file-like object, the latter is kept open + self.assertFalse(reader.file.closed) + # ... until the user explicitly closes it + reader.close() + self.assertTrue(reader.file.closed) + os.remove(tmp.name) + + def test_read_sub_file(self): + # Verifies that sub-file content is able to be read to a table. + expectedContent = 'testContent' + expectedNameID = '1' + expectedPlatform = '3' + expectedLangId = '0x409' + + with tempfile.NamedTemporaryFile(delete=False) as tmp: + subFileData = ( + '<ttFont ttLibVersion="3.15">' + '<name>' + '<namerecord nameID="%s" platformID="%s" platEncID="1" langID="%s">' + '%s' + '</namerecord>' + '</name>' + '</ttFont>' + ) % (expectedNameID, expectedPlatform, expectedLangId, expectedContent) + tmp.write(subFileData.encode("utf-8")) + + with tempfile.NamedTemporaryFile(delete=False) as tmp2: + fileData = ( + '<ttFont ttLibVersion="3.15">' + '<name>' + '<namerecord src="%s"/>' + '</name>' + '</ttFont>' + ) % tmp.name + tmp2.write(fileData.encode('utf-8')) + + ttf = TTFont() + with open(tmp2.name, "rb") as f: + reader = XMLReader(f, ttf) + reader.read() + reader.close() + nameTable = ttf['name'] + self.assertTrue(int(expectedNameID) == nameTable.names[0].nameID) + self.assertTrue(int(expectedLangId, 16) == nameTable.names[0].langID) + self.assertTrue(int(expectedPlatform) == nameTable.names[0].platformID) + self.assertEqual(expectedContent, nameTable.names[0].string.decode(nameTable.names[0].getEncoding())) + + os.remove(tmp.name) + os.remove(tmp2.name) + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/misc/xmlWriter_test.py b/Tests/misc/xmlWriter_test.py new file mode 100644 index 0000000..0930e1c --- /dev/null +++ b/Tests/misc/xmlWriter_test.py @@ -0,0 +1,128 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +import os +import unittest +from fontTools.misc.xmlWriter import XMLWriter + +linesep = tobytes(os.linesep) +HEADER = b'<?xml version="1.0" encoding="UTF-8"?>' + linesep + +class TestXMLWriter(unittest.TestCase): + + def test_comment_escaped(self): + writer = XMLWriter(BytesIO()) + writer.comment("This&that are <comments>") + self.assertEqual(HEADER + b"<!-- This&amp;that are &lt;comments&gt; -->", writer.file.getvalue()) + + def test_comment_multiline(self): + writer = XMLWriter(BytesIO()) + writer.comment("Hello world\nHow are you?") + self.assertEqual(HEADER + b"<!-- Hello world" + linesep + b" How are you? -->", + writer.file.getvalue()) + + def test_encoding_default(self): + writer = XMLWriter(BytesIO()) + self.assertEqual(b'<?xml version="1.0" encoding="UTF-8"?>' + linesep, + writer.file.getvalue()) + + def test_encoding_utf8(self): + # https://github.com/behdad/fonttools/issues/246 + writer = XMLWriter(BytesIO(), encoding="utf8") + self.assertEqual(b'<?xml version="1.0" encoding="UTF-8"?>' + linesep, + writer.file.getvalue()) + + def test_encoding_UTF_8(self): + # https://github.com/behdad/fonttools/issues/246 + writer = XMLWriter(BytesIO(), encoding="UTF-8") + self.assertEqual(b'<?xml version="1.0" encoding="UTF-8"?>' + linesep, + writer.file.getvalue()) + + def test_encoding_UTF8(self): + # https://github.com/behdad/fonttools/issues/246 + writer = XMLWriter(BytesIO(), encoding="UTF8") + self.assertEqual(b'<?xml version="1.0" encoding="UTF-8"?>' + linesep, + writer.file.getvalue()) + + def test_encoding_other(self): + self.assertRaises(Exception, XMLWriter, BytesIO(), + encoding="iso-8859-1") + + def test_write(self): + writer = XMLWriter(BytesIO()) + writer.write("foo&bar") + self.assertEqual(HEADER + b"foo&amp;bar", writer.file.getvalue()) + + def test_indent_dedent(self): + writer = XMLWriter(BytesIO()) + writer.write("foo") + writer.newline() + writer.indent() + writer.write("bar") + writer.newline() + writer.dedent() + writer.write("baz") + self.assertEqual(HEADER + bytesjoin(["foo", " bar", "baz"], linesep), + writer.file.getvalue()) + + def test_writecdata(self): + writer = XMLWriter(BytesIO()) + writer.writecdata("foo&bar") + self.assertEqual(HEADER + b"<![CDATA[foo&bar", writer.file.getvalue()) + + def test_simpletag(self): + writer = XMLWriter(BytesIO()) + writer.simpletag("tag", a="1", b="2") + self.assertEqual(HEADER + b'', writer.file.getvalue()) + + def test_begintag_endtag(self): + writer = XMLWriter(BytesIO()) + writer.begintag("tag", attr="value") + writer.write("content") + writer.endtag("tag") + self.assertEqual(HEADER + b'content', writer.file.getvalue()) + + def test_dumphex(self): + writer = XMLWriter(BytesIO()) + writer.dumphex("Type is a beautiful group of letters, not a group of beautiful letters.") + self.assertEqual(HEADER + bytesjoin([ + "54797065 20697320 61206265 61757469", + "66756c20 67726f75 70206f66 206c6574", + "74657273 2c206e6f 74206120 67726f75", + "70206f66 20626561 75746966 756c206c", + "65747465 72732e ", ""], joiner=linesep), writer.file.getvalue()) + + def test_stringifyattrs(self): + writer = XMLWriter(BytesIO()) + expected = ' attr="0"' + self.assertEqual(expected, writer.stringifyattrs(attr=0)) + self.assertEqual(expected, writer.stringifyattrs(attr=b'0')) + self.assertEqual(expected, writer.stringifyattrs(attr='0')) + self.assertEqual(expected, writer.stringifyattrs(attr=u'0')) + + def test_carriage_return_escaped(self): + writer = XMLWriter(BytesIO()) + writer.write("two lines\r\nseparated by Windows line endings") + self.assertEqual( + HEADER + b'two lines \nseparated by Windows line endings', + writer.file.getvalue()) + + def test_newlinestr(self): + header = b'' + + for nls in (None, '\n', '\r\n', '\r', ''): + writer = XMLWriter(BytesIO(), newlinestr=nls) + writer.write("hello") + writer.newline() + writer.write("world") + writer.newline() + + linesep = tobytes(os.linesep) if nls is None else tobytes(nls) + + self.assertEqual( + header + linesep + b"hello" + linesep + b"world" + linesep, + writer.file.getvalue()) + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/mtiLib/data/featurename-backward.ttx.GSUB b/Tests/mtiLib/data/featurename-backward.ttx.GSUB new file mode 100644 index 0000000..9469c79 --- /dev/null +++ b/Tests/mtiLib/data/featurename-backward.ttx.GSUB @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/featurename-backward.txt b/Tests/mtiLib/data/featurename-backward.txt new file mode 100644 index 0000000..7e3d5d6 --- /dev/null +++ b/Tests/mtiLib/data/featurename-backward.txt @@ -0,0 +1,14 @@ + +feature table begin +f0 akhn l1 +1 akh2 l1 +feature table end + +script table begin +telu default 0, 1 +tel2 default f0, 1 +script table end + +lookup l1 single +a b +lookup end diff --git a/Tests/mtiLib/data/featurename-forward.ttx.GSUB b/Tests/mtiLib/data/featurename-forward.ttx.GSUB new file mode 100644 index 0000000..9469c79 --- /dev/null +++ b/Tests/mtiLib/data/featurename-forward.ttx.GSUB @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/featurename-forward.txt b/Tests/mtiLib/data/featurename-forward.txt new file mode 100644 index 0000000..971ca76 --- /dev/null +++ b/Tests/mtiLib/data/featurename-forward.txt @@ -0,0 +1,14 @@ + +script table begin +telu default 0, 1 +tel2 default f0, 1 +script table end + +feature table begin +f0 akhn l1 +1 akh2 l1 +feature table end + +lookup l1 single +a b +lookup end diff --git a/Tests/mtiLib/data/lookupnames-backward.ttx.GSUB b/Tests/mtiLib/data/lookupnames-backward.ttx.GSUB new file mode 100644 index 0000000..698012c --- /dev/null +++ b/Tests/mtiLib/data/lookupnames-backward.ttx.GSUB @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/lookupnames-backward.txt b/Tests/mtiLib/data/lookupnames-backward.txt new file mode 100644 index 0000000..067d2c1 --- /dev/null +++ b/Tests/mtiLib/data/lookupnames-backward.txt @@ -0,0 +1,36 @@ + +lookup l1 single + +uvowelsignkannada uvowelsignaltkannada +uuvowelsignkannada uuvowelsignaltkannada + +lookup end + +lookup l0 chained + +backtrackclass definition begin +pakannada 1 +phakannada 1 +vakannada 1 +pevowelkannada 1 +phevowelkannada 1 +vevowelkannada 1 +class definition end + +class definition begin +uvowelsignkannada 1 +uuvowelsignkannada 1 +class definition end + +class-chain 1 1 1,l1 + +lookup end + +script table begin +telu default 0, 1 +script table end + +feature table begin +0 akhn l1 +1 akh2 l0 +feature table end diff --git a/Tests/mtiLib/data/lookupnames-forward.ttx.GSUB b/Tests/mtiLib/data/lookupnames-forward.ttx.GSUB new file mode 100644 index 0000000..15e48d0 --- /dev/null +++ b/Tests/mtiLib/data/lookupnames-forward.ttx.GSUB @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/lookupnames-forward.txt b/Tests/mtiLib/data/lookupnames-forward.txt new file mode 100644 index 0000000..16c9819 --- /dev/null +++ b/Tests/mtiLib/data/lookupnames-forward.txt @@ -0,0 +1,36 @@ + +lookup l0 chained + +backtrackclass definition begin +pakannada 1 +phakannada 1 +vakannada 1 +pevowelkannada 1 +phevowelkannada 1 +vevowelkannada 1 +class definition end + +class definition begin +uvowelsignkannada 1 +uuvowelsignkannada 1 +class definition end + +class-chain 1 1 1,l1 + +lookup end + +script table begin +telu default 0, 1 +script table end + +lookup l1 single + +uvowelsignkannada uvowelsignaltkannada +uuvowelsignkannada uuvowelsignaltkannada + +lookup end + +feature table begin +0 akhn l1 +1 akh2 l0 +feature table end diff --git a/Tests/mtiLib/data/mixed-toplevels.ttx.GSUB b/Tests/mtiLib/data/mixed-toplevels.ttx.GSUB new file mode 100644 index 0000000..15e48d0 --- /dev/null +++ b/Tests/mtiLib/data/mixed-toplevels.ttx.GSUB @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mixed-toplevels.txt b/Tests/mtiLib/data/mixed-toplevels.txt new file mode 100644 index 0000000..dc09057 --- /dev/null +++ b/Tests/mtiLib/data/mixed-toplevels.txt @@ -0,0 +1,36 @@ + +lookup 0 chained + +backtrackclass definition begin +pakannada 1 +phakannada 1 +vakannada 1 +pevowelkannada 1 +phevowelkannada 1 +vevowelkannada 1 +class definition end + +class definition begin +uvowelsignkannada 1 +uuvowelsignkannada 1 +class definition end + +class-chain 1 1 1,1 + +lookup end + +script table begin +telu default 0, 1 +script table end + +lookup 1 single + +uvowelsignkannada uvowelsignaltkannada +uuvowelsignkannada uuvowelsignaltkannada + +lookup end + +feature table begin +0 akhn 1 +1 akh2 0 +feature table end diff --git a/Tests/mtiLib/data/mti/README b/Tests/mtiLib/data/mti/README new file mode 100644 index 0000000..edf6120 --- /dev/null +++ b/Tests/mtiLib/data/mti/README @@ -0,0 +1,12 @@ +The *.txt data in this directory was imported from: + +https://github.com/Monotype/OpenType_Table_Source/tree/gh-pages/downloads + +at the following revision: + +8a4db481c63efe468092d7e9cd149d2e9786369a + +plus fixes for the following issues: + +https://github.com/Monotype/OpenType_Table_Source/issues/11 +https://github.com/Monotype/OpenType_Table_Source/issues/13 diff --git a/Tests/mtiLib/data/mti/chained-glyph.ttx.GPOS b/Tests/mtiLib/data/mti/chained-glyph.ttx.GPOS new file mode 100644 index 0000000..811b1df --- /dev/null +++ b/Tests/mtiLib/data/mti/chained-glyph.ttx.GPOS @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/chained-glyph.ttx.GSUB b/Tests/mtiLib/data/mti/chained-glyph.ttx.GSUB new file mode 100644 index 0000000..67aa902 --- /dev/null +++ b/Tests/mtiLib/data/mti/chained-glyph.ttx.GSUB @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/chained-glyph.txt b/Tests/mtiLib/data/mti/chained-glyph.txt new file mode 100644 index 0000000..ca0f304 --- /dev/null +++ b/Tests/mtiLib/data/mti/chained-glyph.txt @@ -0,0 +1,11 @@ +lookup raucontext-sinh chained + +markattachmenttype 2 + +glyph rakarsinh uvowelsignsinh 1,u2aelow-sinh +glyph rakarsinh uuvowelsignsinh 1,u2aelow-sinh + +lookup end + +lookup u2aelow-sinh single +lookup end diff --git a/Tests/mtiLib/data/mti/chainedclass.ttx.GSUB b/Tests/mtiLib/data/mti/chainedclass.ttx.GSUB new file mode 100644 index 0000000..cfa391f --- /dev/null +++ b/Tests/mtiLib/data/mti/chainedclass.ttx.GSUB @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/chainedclass.txt b/Tests/mtiLib/data/mti/chainedclass.txt new file mode 100644 index 0000000..6c73c6e --- /dev/null +++ b/Tests/mtiLib/data/mti/chainedclass.txt @@ -0,0 +1 @@ + lookup swashes-knda chained backtrackclass definition begin pakannada 1 phakannada 1 vakannada 1 pevowelkannada 1 phevowelkannada 1 vevowelkannada 1 class definition end class definition begin uvowelsignkannada 1 uuvowelsignkannada 1 class definition end class-chain 1 1 1,u-swash-knda lookup end lookup u-swash-knda single uvowelsignkannada uvowelsignaltkannada uuvowelsignkannada uuvowelsignaltkannada lookup end \ No newline at end of file diff --git a/Tests/mtiLib/data/mti/chainedcoverage.ttx.GSUB b/Tests/mtiLib/data/mti/chainedcoverage.ttx.GSUB new file mode 100644 index 0000000..7c807a4 --- /dev/null +++ b/Tests/mtiLib/data/mti/chainedcoverage.ttx.GSUB @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/chainedcoverage.txt b/Tests/mtiLib/data/mti/chainedcoverage.txt new file mode 100644 index 0000000..b07d939 --- /dev/null +++ b/Tests/mtiLib/data/mti/chainedcoverage.txt @@ -0,0 +1 @@ +lookup slashcontext chained RightToLeft no IgnoreBaseGlyphs no IgnoreLigatures no IgnoreMarks no backtrackcoverage definition begin zero one two three four five six seven eight nine coverage definition end inputcoverage definition begin slash coverage definition end lookaheadcoverage definition begin zero one two three four five six seven eight nine coverage definition end coverage 1, slashTofraction lookup end lookup slashTofraction single RightToLeft no IgnoreBaseGlyphs no IgnoreLigatures no IgnoreMarks no slash fraction lookup end \ No newline at end of file diff --git a/Tests/mtiLib/data/mti/cmap.ttx b/Tests/mtiLib/data/mti/cmap.ttx new file mode 100644 index 0000000..9c1931b --- /dev/null +++ b/Tests/mtiLib/data/mti/cmap.ttx @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/cmap.ttx.cmap b/Tests/mtiLib/data/mti/cmap.ttx.cmap new file mode 100644 index 0000000..9c1931b --- /dev/null +++ b/Tests/mtiLib/data/mti/cmap.ttx.cmap @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/cmap.txt b/Tests/mtiLib/data/mti/cmap.txt new file mode 100644 index 0000000..18760dd --- /dev/null +++ b/Tests/mtiLib/data/mti/cmap.txt @@ -0,0 +1,52 @@ +Font Chef Table cmap + +cmap subtable 0 +platformID 0 +encodingID 3 +format 4 +language 0 +0x0000 null \#1 +0x000D CR \#2 +0x0020 space \#3 +0x0021 exclam \#4 +0x0022 quotedbl \#5 +0x0023 numbersign \#6 +0x0041 A +0x0042 B +0x0061 a +0x0062 b +end subtable + +cmap subtable 1 +platformID 1 +encodingID 0 +format 6 +language 0 +0x0000 null \#1 +0x000D CR \#2 +0x0020 space \#3 +0x0021 exclam \#4 +0x0022 quotedbl \#5 +0x0023 numbersign \#6 +0x0041 A +0x0042 B +0x0061 a +0x0062 b +end subtable + +cmap subtable 2 +platformID 3 +encodingID 1 +format 4 +language 0 +0x0000 null \#1 +0x000D CR \#2 +0x0020 space \#3 +0x0021 exclam \#4 +0x0022 quotedbl \#5 +0x0023 numbersign \#6 +0x0041 A +0x0042 B +0x0061 a +0x0062 b +end subtable diff --git a/Tests/mtiLib/data/mti/context-glyph.txt b/Tests/mtiLib/data/mti/context-glyph.txt new file mode 100644 index 0000000..59863eb --- /dev/null +++ b/Tests/mtiLib/data/mti/context-glyph.txt @@ -0,0 +1,14 @@ + +lookup 7 context + +RightToLeft no +IgnoreBaseGlyphs no +IgnoreLigatures no +IgnoreMarks no + +glyph gabardeva, viramadeva, radeva 2, 8 +glyph jabardeva, viramadeva, radeva 2, 8 +glyph ddabardeva, viramadeva, radeva 2, 8 +glyph babardeva, viramadeva, radeva 2, 8 + +lookup end diff --git a/Tests/mtiLib/data/mti/contextclass.txt b/Tests/mtiLib/data/mti/contextclass.txt new file mode 100644 index 0000000..4f5d3ec --- /dev/null +++ b/Tests/mtiLib/data/mti/contextclass.txt @@ -0,0 +1 @@ + lookup i-ligatures-deva context class definition begin ivowelsigndeva 1 ivowelsign1deva 1 ivowelsign2deva 1 anusvaradeva 2 candrabindudeva 3 rephdeva 4 rephanusvaradeva 5 class definition end class 1,0,0,0,2 1,i-anusvara-deva 5,removemark-deva class 1,0,0,0,4 1,i-reph-deva 5,removemark-deva class 1,0,0,0,5 1,i-rephanusvara-deva 5,removemark-deva class 1,0,0,2 1,i-anusvara-deva 4,removemark-deva class 1,0,0,4 1,i-reph-deva 4,removemark-deva class 1,0,0,5 1,i-rephanusvara-deva 4,removemark-deva class 1,0,2 1,i-anusvara-deva 3,removemark-deva class 1,0,3 1,i-candrabindu-deva 3,removemark-deva class 1,0,4 1,i-reph-deva 3,removemark-deva class 1,0,5 1,i-rephanusvara-deva 3,removemark-deva lookup end \ No newline at end of file diff --git a/Tests/mtiLib/data/mti/contextcoverage.txt b/Tests/mtiLib/data/mti/contextcoverage.txt new file mode 100644 index 0000000..a22d7b2 --- /dev/null +++ b/Tests/mtiLib/data/mti/contextcoverage.txt @@ -0,0 +1 @@ +lookup slashcontext context RightToLeft no IgnoreBaseGlyphs no IgnoreLigatures no IgnoreMarks no coverage definition begin 0 zero one two three four five six seven eight nine coverage definition end coverage definition begin 1 slash coverage definition end coverage definition begin 2 zero one two three four five six seven eight nine coverage definition end coverage 35, 40 1, 8 2, slashTofraction lookup end lookup slashTofraction single RightToLeft no IgnoreBaseGlyphs no IgnoreLigatures no IgnoreMarks no slash fraction lookup end \ No newline at end of file diff --git a/Tests/mtiLib/data/mti/featuretable.txt b/Tests/mtiLib/data/mti/featuretable.txt new file mode 100644 index 0000000..af13d72 --- /dev/null +++ b/Tests/mtiLib/data/mti/featuretable.txt @@ -0,0 +1 @@ +script table begin telu default 0, 1, 2, 3, 4 script table end feature table begin 0 akhn akhand-telugu 1 blwf belowbase-telugu 2 abvs tripleligatures-telugu,vattucontext-telugu,markra-telugu,lowsubscript-telugu,above-subst-telugu,akhand-ra-telugu 3 psts postbase-telugu 4 haln halant-telugu feature table end \ No newline at end of file diff --git a/Tests/mtiLib/data/mti/gdefattach.ttx.GDEF b/Tests/mtiLib/data/mti/gdefattach.ttx.GDEF new file mode 100644 index 0000000..cc4c70d --- /dev/null +++ b/Tests/mtiLib/data/mti/gdefattach.ttx.GDEF @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/gdefattach.txt b/Tests/mtiLib/data/mti/gdefattach.txt new file mode 100644 index 0000000..a1859cd --- /dev/null +++ b/Tests/mtiLib/data/mti/gdefattach.txt @@ -0,0 +1,7 @@ + +attachment list begin + +A 5 +B 0 3 9 + +attachment list end diff --git a/Tests/mtiLib/data/mti/gdefclasses.ttx.GDEF b/Tests/mtiLib/data/mti/gdefclasses.ttx.GDEF new file mode 100644 index 0000000..e298c28 --- /dev/null +++ b/Tests/mtiLib/data/mti/gdefclasses.ttx.GDEF @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/gdefclasses.txt b/Tests/mtiLib/data/mti/gdefclasses.txt new file mode 100644 index 0000000..e22a609 --- /dev/null +++ b/Tests/mtiLib/data/mti/gdefclasses.txt @@ -0,0 +1 @@ + class definition begin A 1 C 1 fi 2 fl 2 breve 3 acute 3 class definition end \ No newline at end of file diff --git a/Tests/mtiLib/data/mti/gdefligcaret.ttx.GDEF b/Tests/mtiLib/data/mti/gdefligcaret.ttx.GDEF new file mode 100644 index 0000000..48f7347 --- /dev/null +++ b/Tests/mtiLib/data/mti/gdefligcaret.ttx.GDEF @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/gdefligcaret.txt b/Tests/mtiLib/data/mti/gdefligcaret.txt new file mode 100644 index 0000000..926cdb6 --- /dev/null +++ b/Tests/mtiLib/data/mti/gdefligcaret.txt @@ -0,0 +1,7 @@ + +carets begin + +uniFB01 1 236 +ffi 2 210 450 + +carets end diff --git a/Tests/mtiLib/data/mti/gdefmarkattach.ttx.GDEF b/Tests/mtiLib/data/mti/gdefmarkattach.ttx.GDEF new file mode 100644 index 0000000..b6cbe10 --- /dev/null +++ b/Tests/mtiLib/data/mti/gdefmarkattach.ttx.GDEF @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/gdefmarkattach.txt b/Tests/mtiLib/data/mti/gdefmarkattach.txt new file mode 100644 index 0000000..b729927 --- /dev/null +++ b/Tests/mtiLib/data/mti/gdefmarkattach.txt @@ -0,0 +1 @@ + mark attachment class definition begin breve 1 grave 1 commaacent 2 dotbelow 2 class definition end \ No newline at end of file diff --git a/Tests/mtiLib/data/mti/gdefmarkfilter.ttx.GDEF b/Tests/mtiLib/data/mti/gdefmarkfilter.ttx.GDEF new file mode 100644 index 0000000..3b23481 --- /dev/null +++ b/Tests/mtiLib/data/mti/gdefmarkfilter.ttx.GDEF @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/gdefmarkfilter.txt b/Tests/mtiLib/data/mti/gdefmarkfilter.txt new file mode 100644 index 0000000..bbda48a --- /dev/null +++ b/Tests/mtiLib/data/mti/gdefmarkfilter.txt @@ -0,0 +1 @@ + markfilter set definition begin breve 1 acute 1 dotabove 1 dotbelow 2 commaaccent 2 cedilla 2 dotabove 3 dotbelow 3 set definition end \ No newline at end of file diff --git a/Tests/mtiLib/data/mti/gposcursive.ttx.GPOS b/Tests/mtiLib/data/mti/gposcursive.ttx.GPOS new file mode 100644 index 0000000..2965139 --- /dev/null +++ b/Tests/mtiLib/data/mti/gposcursive.ttx.GPOS @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/gposcursive.txt b/Tests/mtiLib/data/mti/gposcursive.txt new file mode 100644 index 0000000..7668a82 --- /dev/null +++ b/Tests/mtiLib/data/mti/gposcursive.txt @@ -0,0 +1 @@ + lookup kernpairs cursive entry A 560, 1466 1 exit A 769, 1466 2 entry B 150, 1466 1 exit B 1186, 1091 6 lookup end \ No newline at end of file diff --git a/Tests/mtiLib/data/mti/gposkernset.ttx.GPOS b/Tests/mtiLib/data/mti/gposkernset.ttx.GPOS new file mode 100644 index 0000000..edfea10 --- /dev/null +++ b/Tests/mtiLib/data/mti/gposkernset.ttx.GPOS @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/gposkernset.txt b/Tests/mtiLib/data/mti/gposkernset.txt new file mode 100644 index 0000000..20571ce --- /dev/null +++ b/Tests/mtiLib/data/mti/gposkernset.txt @@ -0,0 +1,37 @@ +lookup 0 kernset + +RightToLeft no +IgnoreBaseGlyphs no +IgnoreLigatures no +IgnoreMarks no + +left x advance Acircumflex V -10 +left x advance T acircumflex -18 +% Below are the class definitions. Above are the exceptions. +subtable end + +firstclass definition begin +A 1 +Aacute 1 +Agrave 1 +Acircumflex 1 +O 2 +Oacute 2 +Ograve 2 +Ocircumflex 2 +T 3 +class definition end + +secondclass definition begin +V 1 +a 2 +aacute 2 +agrave 2 +acircumflex 2 +class definition end + +left x advance 1 1 -50 +left x advance 2 1 -10 +left x advance 3 2 -35 + +lookup end diff --git a/Tests/mtiLib/data/mti/gposmarktobase.ttx.GPOS b/Tests/mtiLib/data/mti/gposmarktobase.ttx.GPOS new file mode 100644 index 0000000..b0a74e7 --- /dev/null +++ b/Tests/mtiLib/data/mti/gposmarktobase.ttx.GPOS @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/gposmarktobase.txt b/Tests/mtiLib/data/mti/gposmarktobase.txt new file mode 100644 index 0000000..8b177f3 --- /dev/null +++ b/Tests/mtiLib/data/mti/gposmarktobase.txt @@ -0,0 +1 @@ +lookup topmarktobase-guru mark to base mark bindigurmukhi 0 -184, 1183 16 base ngagurmukhi 0 816, 1183 41 base tthagurmukhi 0 816, 1183 30 base nnagurmukhi 0 976, 1183 35 base nagurmukhi 0 816, 1183 32 base lagurmukhi 0 996, 1183 46 base lanuktagurmukhi 0 996, 1183 46 mark eematragurmukhi 0 -184, 1183 15 mark aimatragurmukhi 0 -184, 1183 28 mark oomatragurmukhi 0 -184, 1183 20 mark aumatragurmukhi 0 -184, 1183 38 base nganuktagurmukhi 0 816, 1183 41 base tthanuktagurmukhi 0 816, 1183 30 base nnanuktagurmukhi 0 976, 1183 35 base nanuktagurmukhi 0 816, 1183 32 mark eematrabindigurmukhi 0 -184, 1183 27 mark aimatrabindigurmukhi 0 -184, 1183 40 mark oomatrabindigurmukhi 0 -184, 1183 36 mark aumatrabindigurmukhi 0 -184, 1183 54 mark eematratippigurmukhi 0 -184, 1183 15 mark aimatratippigurmukhi 0 -184, 1183 28 mark oomatratippigurmukhi 0 -184, 1183 20 lookup end \ No newline at end of file diff --git a/Tests/mtiLib/data/mti/gpospairclass.ttx.GPOS b/Tests/mtiLib/data/mti/gpospairclass.ttx.GPOS new file mode 100644 index 0000000..567b2a7 --- /dev/null +++ b/Tests/mtiLib/data/mti/gpospairclass.ttx.GPOS @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/gpospairclass.txt b/Tests/mtiLib/data/mti/gpospairclass.txt new file mode 100644 index 0000000..21a47fd --- /dev/null +++ b/Tests/mtiLib/data/mti/gpospairclass.txt @@ -0,0 +1,27 @@ +lookup 0 pair + +firstclass definition begin +A 1 +Aacute 1 +Agrave 1 +Acircumflex 1 +O 2 +Oacute 2 +Ograve 2 +Ocircumflex 2 +T 3 +class definition end + +secondclass definition begin +V 1 +a 2 +aacute 2 +agrave 2 +acircumflex 2 +class definition end + +left x advance 1 1 -50 +left x advance 2 1 -10 +left x advance 3 2 -35 + +lookup end diff --git a/Tests/mtiLib/data/mti/gpospairglyph.ttx.GPOS b/Tests/mtiLib/data/mti/gpospairglyph.ttx.GPOS new file mode 100644 index 0000000..ea0161b --- /dev/null +++ b/Tests/mtiLib/data/mti/gpospairglyph.ttx.GPOS @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/gpospairglyph.txt b/Tests/mtiLib/data/mti/gpospairglyph.txt new file mode 100644 index 0000000..b943ec0 --- /dev/null +++ b/Tests/mtiLib/data/mti/gpospairglyph.txt @@ -0,0 +1,17 @@ + +lookup 0 pair + +left x advance A V -50 +left x advance Aacute V -50 +left x advance Agrave V -50 +left x advance Acircumflex V -50 +left x advance O V -10 +left x advance Oacute V -10 +left x advance Ograve V -10 +left x advance Ocircumflex V -10 +left x advance T a -35 +left x advance T aacute -35 +left x advance T agrave -35 +left x advance T acircumflex -35 + +lookup end diff --git a/Tests/mtiLib/data/mti/gpossingle.ttx.GPOS b/Tests/mtiLib/data/mti/gpossingle.ttx.GPOS new file mode 100644 index 0000000..adbb44f --- /dev/null +++ b/Tests/mtiLib/data/mti/gpossingle.ttx.GPOS @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/gpossingle.txt b/Tests/mtiLib/data/mti/gpossingle.txt new file mode 100644 index 0000000..9e6f429 --- /dev/null +++ b/Tests/mtiLib/data/mti/gpossingle.txt @@ -0,0 +1 @@ + lookup supsToInferiors single RightToLeft no IgnoreBaseGlyphs no IgnoreLigatures no IgnoreMarks no y placement asuperior -560 y placement bsuperior -560 y placement csuperior -560 y placement dsuperior -560 y placement esuperior -560 y placement fsuperior -560 y placement gsuperior -560 y placement hsuperior -560 y placement isuperior -560 y placement jsuperior -560 y placement ksuperior -560 y placement lsuperior -560 y placement msuperior -560 y placement nsuperior -560 y placement osuperior -560 y placement psuperior -560 y placement qsuperior -560 y placement rsuperior -560 y placement ssuperior -560 y placement tsuperior -560 y placement usuperior -560 y placement vsuperior -560 y placement wsuperior -560 y placement xsuperior -560 y placement ysuperior -560 y placement zsuperior -560 y placement periodsuperior -560 y placement commasuperior -560 y placement dollarsuperior -560 y placement centsuperior -560 y placement aesuperior -560 y placement oesuperior -560 y placement egravesuperior -560 lookup end \ No newline at end of file diff --git a/Tests/mtiLib/data/mti/gsubalternate.ttx.GSUB b/Tests/mtiLib/data/mti/gsubalternate.ttx.GSUB new file mode 100644 index 0000000..4184325 --- /dev/null +++ b/Tests/mtiLib/data/mti/gsubalternate.ttx.GSUB @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/gsubalternate.txt b/Tests/mtiLib/data/mti/gsubalternate.txt new file mode 100644 index 0000000..66e0997 --- /dev/null +++ b/Tests/mtiLib/data/mti/gsubalternate.txt @@ -0,0 +1,25 @@ + +Comment: taken from Adobe garamond Pro, showing Unicode glyph references + + +lookup 27 alternate + +RightToLeft no +IgnoreBaseGlyphs no +IgnoreLigatures no +IgnoreMarks no + +U 30 U F730 U E13D U E13E U E13A U 2070 U 2080 U E13B U E139 U E13C +U 31 U F731 U E0F3 U E0F4 U E0F1 U B9 U 2081 U E0F2 U E0F0 U E0F8 +U 32 U F732 U E133 U E134 U E131 U B2 U 2082 U E132 U E130 U E0F9 +U 33 U F733 U E12B U E12C U E129 U B3 U 2083 U E12A U E128 +U 34 U F734 U E0D4 U E0D5 U E0D2 U 2074 U 2084 U E0D3 U E0D1 +U 35 U F735 U E0CD U E0CE U E0CB U 2075 U 2085 U E0CC U E0CA +U 36 U F736 U E121 U E122 U E11F U 2076 U 2086 U E120 U E11E +U 37 U F737 U E11C U E11D U E11A U 2077 U 2087 U E11B U E119 +U 38 U F738 U E0C0 U E0C1 U E0BE U 2078 U 2088 U E0BF U E0BD +U 39 U F739 U E0EC U E0ED U E0EA U 2079 U 2089 U E0EB U E0E9 +U 2039 U E0DB U E0DC +U 203A U E0DD U E0DE + +lookup end diff --git a/Tests/mtiLib/data/mti/gsubligature.ttx.GSUB b/Tests/mtiLib/data/mti/gsubligature.ttx.GSUB new file mode 100644 index 0000000..ad8f505 --- /dev/null +++ b/Tests/mtiLib/data/mti/gsubligature.ttx.GSUB @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/gsubligature.txt b/Tests/mtiLib/data/mti/gsubligature.txt new file mode 100644 index 0000000..23d56b0 --- /dev/null +++ b/Tests/mtiLib/data/mti/gsubligature.txt @@ -0,0 +1 @@ +lookup latinLigatures ligature RightToLeft no IgnoreBaseGlyphs no IgnoreLigatures no IgnoreMarks no IJ I J ffi f f i ffl f f l fft f f t ffb f f b ffh f f h ffk f f k fi f i fl f l ff f f ft f t fb f b fh f h fk f k fj f j ij i j tt t t IJsmall Ismall Jsmall lookup end \ No newline at end of file diff --git a/Tests/mtiLib/data/mti/gsubmultiple.ttx.GSUB b/Tests/mtiLib/data/mti/gsubmultiple.ttx.GSUB new file mode 100644 index 0000000..a68a45a --- /dev/null +++ b/Tests/mtiLib/data/mti/gsubmultiple.ttx.GSUB @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/gsubmultiple.txt b/Tests/mtiLib/data/mti/gsubmultiple.txt new file mode 100644 index 0000000..d4c301c --- /dev/null +++ b/Tests/mtiLib/data/mti/gsubmultiple.txt @@ -0,0 +1 @@ +lookup replace-akhand-telugu multiple kassevoweltelugu kaivoweltelugu ssasubscripttelugu janyevoweltelugu jaivoweltelugu nyasubscripttelugu lookup end \ No newline at end of file diff --git a/Tests/mtiLib/data/mti/gsubreversechanined.ttx.GSUB b/Tests/mtiLib/data/mti/gsubreversechanined.ttx.GSUB new file mode 100644 index 0000000..62ba14f --- /dev/null +++ b/Tests/mtiLib/data/mti/gsubreversechanined.ttx.GSUB @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/gsubreversechanined.txt b/Tests/mtiLib/data/mti/gsubreversechanined.txt new file mode 100644 index 0000000..8444a07 --- /dev/null +++ b/Tests/mtiLib/data/mti/gsubreversechanined.txt @@ -0,0 +1 @@ +lookup arabicReverse reversechained RightToLeft yes IgnoreBaseGlyphs no IgnoreLigatures no IgnoreMarks yes backtrackcoverage definition begin bayi1 jeemi1 kafi1 ghafi1 laami1 kafm1 ghafm1 laamm1 coverage definition end rayf2 rayf1 reyf2 reyf1 zayf2 zayf1 yayf2 yayf1 % subtable backtrackcoverage definition begin bayi1 fayi1 kafi1 ghafi1 laami1 kafm1 ghafm1 laamm1 coverage definition end hamzayehf2 hamzayehf1 hamzayeharabf2 hamzayeharabf1 ayehf2 ayehf1 yehf2 yehf1 % subtable lookaheadcoverage definition begin ray rey zay yay coverage definition end dal dal1 del del1 zal zal1 lookup end \ No newline at end of file diff --git a/Tests/mtiLib/data/mti/gsubsingle.ttx.GSUB b/Tests/mtiLib/data/mti/gsubsingle.ttx.GSUB new file mode 100644 index 0000000..525b365 --- /dev/null +++ b/Tests/mtiLib/data/mti/gsubsingle.ttx.GSUB @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/gsubsingle.txt b/Tests/mtiLib/data/mti/gsubsingle.txt new file mode 100644 index 0000000..76e7459 --- /dev/null +++ b/Tests/mtiLib/data/mti/gsubsingle.txt @@ -0,0 +1 @@ + lookup alt-fractions single RightToLeft no IgnoreBaseGlyphs no IgnoreLigatures no IgnoreMarks no onehalf onehalf.alt onequarter onequarter.alt threequarters threequarters.alt lookup end \ No newline at end of file diff --git a/Tests/mtiLib/data/mti/mark-to-ligature.ttx.GPOS b/Tests/mtiLib/data/mti/mark-to-ligature.ttx.GPOS new file mode 100644 index 0000000..7e02fe0 --- /dev/null +++ b/Tests/mtiLib/data/mti/mark-to-ligature.ttx.GPOS @@ -0,0 +1,826 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/mark-to-ligature.txt b/Tests/mtiLib/data/mti/mark-to-ligature.txt new file mode 100644 index 0000000..efdd2cf --- /dev/null +++ b/Tests/mtiLib/data/mti/mark-to-ligature.txt @@ -0,0 +1,93 @@ +lookup LigMk0 mark to ligature + +mark FathatanNS 0 281, 1388 0 +mark DammatanNS 0 354, 1409 0 +mark FathaNS 0 277, 1379 0 +mark DammaNS 0 394, 1444 0 +mark ShaddaNS 0 283, 1581 0 +mark SukunNS 0 220, 1474 1 +mark MaddaNS 0 397, 1472 1 +mark HamzaAboveNS 0 266, 1425 2 +mark UltapeshNS 0 454, 1128 1 +mark DammaRflxNS 0 454, 1128 1 +mark Fatha2dotsNS 0 272, 1097 0 +mark AlefSuperiorNS 0 141, 874 1 +mark ShaddaAlefNS 0 283, 1581 0 +mark WaslaNS 0 357, 1470 0 +mark OneDotAboveNS 0 215, 1001 3 +mark TwoDotsAboveNS 0 346, 1003 0 +mark ThreeDotsUpAboveNS 0 346, 1003 0 +mark ThreeDotsDownAboveNS 0 346, 687 0 +mark FourDotsAboveNS 0 347, 860 0 +mark TwoDotsVerticalAboveNS 0 357, 707 1 +mark OneDotAbove2NS 0 215, 1001 3 +mark SharetKafNS 0 382, 520 1 +mark ShaddaKasratanNS 0 315, 1164 55 +mark ShaddaKasraNS 0 426, 1340 55 +mark ShaddaFathatanNS 0 369, 1604 0 +mark ShaddaDammatanNS 0 283, 1581 0 +mark ShaddaDammaNS 0 283, 1581 0 +ligature LamAlefFin.short 1 2 0 1122, 1620 96 +ligature LamAlefFin.short 2 2 0 162, 1487 99 +ligature LamAlefFin.cup 1 2 0 1122, 1620 105 +ligature LamAlefFin.cup 2 2 0 162, 1487 106 +ligature LamAlefFin.cut 1 2 0 1122, 1620 110 +ligature LamAlefFin.cut 2 2 0 162, 1487 108 +ligature BehxIni_RehFin 1 2 0 618, 813 59 +ligature BehxIni_RehFin 2 2 0 282, 523 58 +ligature BehxIni_RehFin.b 1 2 0 708, 813 65 +ligature BehxIni_RehFin.b 2 2 0 282, 543 64 +ligature BehxIni_NoonGhunnaFin 1 2 0 1205, 871 78 +ligature BehxIni_NoonGhunnaFin 2 2 0 516, 565 74 +ligature BehxIni_MeemFin 1 2 0 785, 1255 90 +ligature BehxIni_MeemFin 2 2 0 269, 952 93 +ligature HahIni_YehBarreeFin 1 2 0 1017, 732 83 +ligature HahIni_YehBarreeFin 2 2 0 344, 743 86 +ligature AinMed_YehBarreeFin 1 2 0 774, 860 105 +ligature AinMed_YehBarreeFin 2 2 0 312, 618 108 +ligature TahIni_YehBarreeFin 1 2 0 1253, 1065 143 +ligature TahIni_YehBarreeFin 2 2 0 263, 419 142 +ligature BehxMed_NoonGhunnaFin 1 2 0 1205, 1061 78 +ligature BehxMed_NoonGhunnaFin 2 2 0 516, 755 74 +ligature KafMed_MeemFin 1 2 0 238, 1435 182 +ligature KafMed_MeemFin 2 2 0 84, 308 186 +ligature LamMed_MeemFin 1 2 0 555, 1627 154 +ligature LamMed_MeemFin 2 2 0 175, 472 155 +ligature LamMed_MeemFin.b 1 2 0 555, 1627 156 +ligature LamMed_MeemFin.b 2 2 0 175, 472 157 +ligature LamIni_MeemFin 1 2 0 386, 1808 70 +ligature LamIni_MeemFin 2 2 0 130, 701 150 +ligature AinIni.12m_MeemFin.02 1 2 0 720, 1281 160 +ligature AinIni.12m_MeemFin.02 2 2 0 75, 631 158 +ligature KafMed.12_YehxFin.01 1 2 0 807, 1457 106 +ligature KafMed.12_YehxFin.01 2 2 0 440, 418 176 +ligature LamMed_YehxFin 1 2 0 925, 1620 157 +ligature LamMed_YehxFin 2 2 0 490, 196 152 +ligature LamMed_YehxFin.cup 1 2 0 935, 1620 159 +ligature LamMed_YehxFin.cup 2 2 0 500, 196 155 +ligature FehxMed_YehBarreeFin 1 2 0 397, 804 158 +ligature FehxMed_YehBarreeFin 2 2 0 6,-65 161 +ligature KafIni_YehBarreeFin 1 2 0 496, 1549 81 +ligature KafIni_YehBarreeFin 2 2 0 328, 339 171 +ligature KafMed_YehBarreeFin 1 2 0 465, 1407 106 +ligature KafMed_YehBarreeFin 2 2 0 328, 251 197 +ligature LamIni_YehBarreeFin 1 2 0 719, 1633 70 +ligature LamIni_YehBarreeFin 2 2 0 328, 339 160 +ligature AinIni_YehBarreeFin 1 2 0 766, 1036 82 +ligature AinIni_YehBarreeFin 2 2 0 194, 312 151 +ligature BehxMed_YehxFin 1 2 0 913,-285 117 +ligature BehxMed_YehxFin 2 2 0 1223,-305 112 +ligature BehxMed_MeemFin.py 1 2 0 777, 699 99 +ligature BehxMed_MeemFin.py 2 2 0 194, 481 102 +ligature BehxMed_RehFin 1 2 0 708, 1083 65 +ligature BehxMed_RehFin 2 2 0 282, 813 64 +ligature BehxMed_RehFin.cup 1 2 0 708, 1083 65 +ligature BehxMed_RehFin.cup 2 2 0 282, 813 64 +ligature BehxMed_NoonGhunnaFin.cup 1 2 0 1205, 1061 78 +ligature BehxMed_NoonGhunnaFin.cup 2 2 0 516, 755 74 +ligature LamAlefSep 1 2 0 1055, 1583 105 +ligature LamAlefSep 2 2 0 198, 1528 106 +ligature LamAlefFin 1 2 0 1122, 1620 98 +ligature LamAlefFin 2 2 0 162, 1487 99 + +lookup end \ No newline at end of file diff --git a/Tests/mtiLib/data/mti/scripttable.ttx.GPOS b/Tests/mtiLib/data/mti/scripttable.ttx.GPOS new file mode 100644 index 0000000..7615bb7 --- /dev/null +++ b/Tests/mtiLib/data/mti/scripttable.ttx.GPOS @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/scripttable.ttx.GSUB b/Tests/mtiLib/data/mti/scripttable.ttx.GSUB new file mode 100644 index 0000000..2e2ef71 --- /dev/null +++ b/Tests/mtiLib/data/mti/scripttable.ttx.GSUB @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Tests/mtiLib/data/mti/scripttable.txt b/Tests/mtiLib/data/mti/scripttable.txt new file mode 100644 index 0000000..3955ff4 --- /dev/null +++ b/Tests/mtiLib/data/mti/scripttable.txt @@ -0,0 +1 @@ + script table begin cyrl default 3, 4, 1 grek default 3, 4, 2 latn default 3, 4, 0 latn DEU 3, 4, 0 latn ROM 3, 4, 0 latn TRK 3, 4, 0 latn VIT 3, 4, 5, 0 script table end \ No newline at end of file diff --git a/Tests/mtiLib/mti_test.py b/Tests/mtiLib/mti_test.py new file mode 100644 index 0000000..4916828 --- /dev/null +++ b/Tests/mtiLib/mti_test.py @@ -0,0 +1,236 @@ +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc.xmlWriter import XMLWriter +from fontTools.ttLib import TTFont +from fontTools import mtiLib +import difflib +import os +import sys +import unittest + + +class MtiTest(unittest.TestCase): + + GLYPH_ORDER = ['.notdef', + 'a', 'b', 'pakannada', 'phakannada', 'vakannada', 'pevowelkannada', + 'phevowelkannada', 'vevowelkannada', 'uvowelsignkannada', 'uuvowelsignkannada', + 'uvowelsignaltkannada', 'uuvowelsignaltkannada', 'uuvowelsignsinh', + 'uvowelsignsinh', 'rakarsinh', 'zero', 'one', 'two', 'three', 'four', 'five', + 'six', 'seven', 'eight', 'nine', 'slash', 'fraction', 'A', 'B', 'C', 'fi', + 'fl', 'breve', 'acute', 'uniFB01', 'ffi', 'grave', 'commaacent', 'dotbelow', + 'dotabove', 'cedilla', 'commaaccent', 'Acircumflex', 'V', 'T', 'acircumflex', + 'Aacute', 'Agrave', 'O', 'Oacute', 'Ograve', 'Ocircumflex', 'aacute', 'agrave', + 'aimatrabindigurmukhi', 'aimatragurmukhi', 'aimatratippigurmukhi', + 'aumatrabindigurmukhi', 'aumatragurmukhi', 'bindigurmukhi', + 'eematrabindigurmukhi', 'eematragurmukhi', 'eematratippigurmukhi', + 'oomatrabindigurmukhi', 'oomatragurmukhi', 'oomatratippigurmukhi', + 'lagurmukhi', 'lanuktagurmukhi', 'nagurmukhi', 'nanuktagurmukhi', + 'ngagurmukhi', 'nganuktagurmukhi', 'nnagurmukhi', 'nnanuktagurmukhi', + 'tthagurmukhi', 'tthanuktagurmukhi', 'bsuperior', 'isuperior', 'vsuperior', + 'wsuperior', 'periodsuperior', 'osuperior', 'tsuperior', 'dollarsuperior', + 'fsuperior', 'gsuperior', 'zsuperior', 'dsuperior', 'psuperior', 'hsuperior', + 'oesuperior', 'aesuperior', 'centsuperior', 'esuperior', 'lsuperior', + 'qsuperior', 'csuperior', 'asuperior', 'commasuperior', 'xsuperior', + 'egravesuperior', 'usuperior', 'rsuperior', 'nsuperior', 'ssuperior', + 'msuperior', 'jsuperior', 'ysuperior', 'ksuperior', 'guilsinglright', + 'guilsinglleft', 'uniF737', 'uniE11C', 'uniE11D', 'uniE11A', 'uni2077', + 'uni2087', 'uniE11B', 'uniE119', 'uniE0DD', 'uniE0DE', 'uniF736', 'uniE121', + 'uniE122', 'uniE11F', 'uni2076', 'uni2086', 'uniE120', 'uniE11E', 'uniE0DB', + 'uniE0DC', 'uniF733', 'uniE12B', 'uniE12C', 'uniE129', 'uni00B3', 'uni2083', + 'uniE12A', 'uniE128', 'uniF732', 'uniE133', 'uniE134', 'uniE131', 'uni00B2', + 'uni2082', 'uniE132', 'uniE130', 'uniE0F9', 'uniF734', 'uniE0D4', 'uniE0D5', + 'uniE0D2', 'uni2074', 'uni2084', 'uniE0D3', 'uniE0D1', 'uniF730', 'uniE13D', + 'uniE13E', 'uniE13A', 'uni2070', 'uni2080', 'uniE13B', 'uniE139', 'uniE13C', + 'uniF739', 'uniE0EC', 'uniE0ED', 'uniE0EA', 'uni2079', 'uni2089', 'uniE0EB', + 'uniE0E9', 'uniF735', 'uniE0CD', 'uniE0CE', 'uniE0CB', 'uni2075', 'uni2085', + 'uniE0CC', 'uniE0CA', 'uniF731', 'uniE0F3', 'uniE0F4', 'uniE0F1', 'uni00B9', + 'uni2081', 'uniE0F2', 'uniE0F0', 'uniE0F8', 'uniF738', 'uniE0C0', 'uniE0C1', + 'uniE0BE', 'uni2078', 'uni2088', 'uniE0BF', 'uniE0BD', 'I', 'Ismall', 't', 'i', + 'f', 'IJ', 'J', 'IJsmall', 'Jsmall', 'tt', 'ij', 'j', 'ffb', 'ffh', 'h', 'ffk', + 'k', 'ffl', 'l', 'fft', 'fb', 'ff', 'fh', 'fj', 'fk', 'ft', 'janyevoweltelugu', + 'kassevoweltelugu', 'jaivoweltelugu', 'nyasubscripttelugu', 'kaivoweltelugu', + 'ssasubscripttelugu', 'bayi1', 'jeemi1', 'kafi1', 'ghafi1', 'laami1', 'kafm1', + 'ghafm1', 'laamm1', 'rayf2', 'reyf2', 'yayf2', 'zayf2', 'fayi1', 'ayehf2', + 'hamzayeharabf2', 'hamzayehf2', 'yehf2', 'ray', 'rey', 'zay', 'yay', 'dal', + 'del', 'zal', 'rayf1', 'reyf1', 'yayf1', 'zayf1', 'ayehf1', 'hamzayeharabf1', + 'hamzayehf1', 'yehf1', 'dal1', 'del1', 'zal1', 'onehalf', 'onehalf.alt', + 'onequarter', 'onequarter.alt', 'threequarters', 'threequarters.alt', + 'AlefSuperiorNS', 'DammaNS', 'DammaRflxNS', 'DammatanNS', 'Fatha2dotsNS', + 'FathaNS', 'FathatanNS', 'FourDotsAboveNS', 'HamzaAboveNS', 'MaddaNS', + 'OneDotAbove2NS', 'OneDotAboveNS', 'ShaddaAlefNS', 'ShaddaDammaNS', + 'ShaddaDammatanNS', 'ShaddaFathatanNS', 'ShaddaKasraNS', 'ShaddaKasratanNS', + 'ShaddaNS', 'SharetKafNS', 'SukunNS', 'ThreeDotsDownAboveNS', + 'ThreeDotsUpAboveNS', 'TwoDotsAboveNS', 'TwoDotsVerticalAboveNS', 'UltapeshNS', + 'WaslaNS', 'AinIni.12m_MeemFin.02', 'AinIni_YehBarreeFin', + 'AinMed_YehBarreeFin', 'BehxIni_MeemFin', 'BehxIni_NoonGhunnaFin', + 'BehxIni_RehFin', 'BehxIni_RehFin.b', 'BehxMed_MeemFin.py', + 'BehxMed_NoonGhunnaFin', 'BehxMed_NoonGhunnaFin.cup', 'BehxMed_RehFin', + 'BehxMed_RehFin.cup', 'BehxMed_YehxFin', 'FehxMed_YehBarreeFin', + 'HahIni_YehBarreeFin', 'KafIni_YehBarreeFin', 'KafMed.12_YehxFin.01', + 'KafMed_MeemFin', 'KafMed_YehBarreeFin', 'LamAlefFin', 'LamAlefFin.cup', + 'LamAlefFin.cut', 'LamAlefFin.short', 'LamAlefSep', 'LamIni_MeemFin', + 'LamIni_YehBarreeFin', 'LamMed_MeemFin', 'LamMed_MeemFin.b', 'LamMed_YehxFin', + 'LamMed_YehxFin.cup', 'TahIni_YehBarreeFin', 'null', 'CR', 'space', + 'exclam', 'quotedbl', 'numbersign', + ] + + # Feature files in data/*.txt; output gets compared to data/*.ttx. + TESTS = { + None: ( + 'mti/cmap', + ), + 'cmap': ( + 'mti/cmap', + ), + 'GSUB': ( + 'featurename-backward', + 'featurename-forward', + 'lookupnames-backward', + 'lookupnames-forward', + 'mixed-toplevels', + + 'mti/scripttable', + 'mti/chainedclass', + 'mti/chainedcoverage', + 'mti/chained-glyph', + 'mti/gsubalternate', + 'mti/gsubligature', + 'mti/gsubmultiple', + 'mti/gsubreversechanined', + 'mti/gsubsingle', + ), + 'GPOS': ( + 'mti/scripttable', + 'mti/chained-glyph', + 'mti/gposcursive', + 'mti/gposkernset', + 'mti/gposmarktobase', + 'mti/gpospairclass', + 'mti/gpospairglyph', + 'mti/gpossingle', + 'mti/mark-to-ligature', + ), + 'GDEF': ( + 'mti/gdefattach', + 'mti/gdefclasses', + 'mti/gdefligcaret', + 'mti/gdefmarkattach', + 'mti/gdefmarkfilter', + ), + } + # TODO: + # https://github.com/Monotype/OpenType_Table_Source/issues/12 + # + # 'mti/featuretable' + # 'mti/contextclass' + # 'mti/contextcoverage' + # 'mti/context-glyph' + + def __init__(self, methodName): + unittest.TestCase.__init__(self, methodName) + # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, + # and fires deprecation warnings if a program uses the old name. + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + + def setUp(self): + pass + + def tearDown(self): + pass + + @staticmethod + def getpath(testfile): + path, _ = os.path.split(__file__) + return os.path.join(path, "data", testfile) + + def expect_ttx(self, expected_ttx, actual_ttx, fromfile=None, tofile=None): + expected = [l+'\n' for l in expected_ttx.split('\n')] + actual = [l+'\n' for l in actual_ttx.split('\n')] + if actual != expected: + sys.stderr.write('\n') + for line in difflib.unified_diff( + expected, actual, fromfile=fromfile, tofile=tofile): + sys.stderr.write(line) + self.fail("TTX output is different from expected") + + @classmethod + def create_font(celf): + font = TTFont() + font.setGlyphOrder(celf.GLYPH_ORDER) + return font + + def check_mti_file(self, name, tableTag=None): + + xml_expected_path = self.getpath("%s.ttx" % name + ('.'+tableTag if tableTag is not None else '')) + with open(xml_expected_path, 'rt', encoding="utf-8") as xml_expected_file: + xml_expected = xml_expected_file.read() + + font = self.create_font() + + with open(self.getpath("%s.txt" % name), 'rt', encoding="utf-8") as f: + table = mtiLib.build(f, font, tableTag=tableTag) + + if tableTag is not None: + self.assertEqual(tableTag, table.tableTag) + tableTag = table.tableTag + + # Make sure it compiles. + blob = table.compile(font) + + # Make sure it decompiles. + decompiled = table.__class__() + decompiled.decompile(blob, font) + + # XML from built object. + writer = XMLWriter(StringIO(), newlinestr='\n') + writer.begintag(tableTag); writer.newline() + table.toXML(writer, font) + writer.endtag(tableTag); writer.newline() + xml_built = writer.file.getvalue() + + # XML from decompiled object. + writer = XMLWriter(StringIO(), newlinestr='\n') + writer.begintag(tableTag); writer.newline() + decompiled.toXML(writer, font) + writer.endtag(tableTag); writer.newline() + xml_binary = writer.file.getvalue() + + self.expect_ttx(xml_binary, xml_built, fromfile='decompiled', tofile='built') + self.expect_ttx(xml_expected, xml_built, fromfile=xml_expected_path, tofile='built') + + from fontTools.misc import xmlReader + f = StringIO() + f.write(xml_expected) + f.seek(0) + font2 = TTFont() + font2.setGlyphOrder(font.getGlyphOrder()) + reader = xmlReader.XMLReader(f, font2) + reader.read(rootless=True) + + # XML from object read from XML. + writer = XMLWriter(StringIO(), newlinestr='\n') + writer.begintag(tableTag); writer.newline() + font2[tableTag].toXML(writer, font) + writer.endtag(tableTag); writer.newline() + xml_fromxml = writer.file.getvalue() + + self.expect_ttx(xml_expected, xml_fromxml, fromfile=xml_expected_path, tofile='fromxml') + +def generate_mti_file_test(name, tableTag=None): + return lambda self: self.check_mti_file(os.path.join(*name.split('/')), tableTag=tableTag) + + +for tableTag,tests in MtiTest.TESTS.items(): + for name in tests: + setattr(MtiTest, "test_MtiFile_%s%s" % (name, '_'+tableTag if tableTag else ''), + generate_mti_file_test(name, tableTag=tableTag)) + + +if __name__ == "__main__": + if len(sys.argv) > 1: + from fontTools.mtiLib import main + font = MtiTest.create_font() + sys.exit(main(sys.argv[1:], font)) + sys.exit(unittest.main()) diff --git a/Tests/otlLib/builder_test.py b/Tests/otlLib/builder_test.py new file mode 100644 index 0000000..63a35d6 --- /dev/null +++ b/Tests/otlLib/builder_test.py @@ -0,0 +1,1062 @@ +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.misc.testTools import getXML +from fontTools.otlLib import builder +from fontTools.ttLib.tables import otTables +from itertools import chain +import unittest + + +class BuilderTest(unittest.TestCase): + GLYPHS = (".notdef space zero one two three four five six " + "A B C a b c grave acute cedilla f_f_i f_i c_t").split() + GLYPHMAP = {name: num for num, name in enumerate(GLYPHS)} + + ANCHOR1 = builder.buildAnchor(11, -11) + ANCHOR2 = builder.buildAnchor(22, -22) + ANCHOR3 = builder.buildAnchor(33, -33) + + def __init__(self, methodName): + unittest.TestCase.__init__(self, methodName) + # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, + # and fires deprecation warnings if a program uses the old name. + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + + @classmethod + def setUpClass(cls): + cls.maxDiff = None + + def test_buildAnchor_format1(self): + anchor = builder.buildAnchor(23, 42) + self.assertEqual(getXML(anchor.toXML), + ['', + ' ', + ' ', + '']) + + def test_buildAnchor_format2(self): + anchor = builder.buildAnchor(23, 42, point=17) + self.assertEqual(getXML(anchor.toXML), + ['', + ' ', + ' ', + ' ', + '']) + + def test_buildAnchor_format3(self): + anchor = builder.buildAnchor( + 23, 42, + deviceX=builder.buildDevice({1: 1, 0: 0}), + deviceY=builder.buildDevice({7: 7})) + self.assertEqual(getXML(anchor.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildAttachList(self): + attachList = builder.buildAttachList({ + "zero": [23, 7], "one": [1], + }, self.GLYPHMAP) + self.assertEqual(getXML(attachList.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildAttachList_empty(self): + self.assertIsNone(builder.buildAttachList({}, self.GLYPHMAP)) + + def test_buildAttachPoint(self): + attachPoint = builder.buildAttachPoint([7, 3]) + self.assertEqual(getXML(attachPoint.toXML), + ['', + ' ', + ' ', + ' ', + '']) + + def test_buildAttachPoint_empty(self): + self.assertIsNone(builder.buildAttachPoint([])) + + def test_buildAttachPoint_duplicate(self): + attachPoint = builder.buildAttachPoint([7, 3, 7]) + self.assertEqual(getXML(attachPoint.toXML), + ['', + ' ', + ' ', + ' ', + '']) + + + def test_buildBaseArray(self): + anchor = builder.buildAnchor + baseArray = builder.buildBaseArray({ + "a": {2: anchor(300, 80)}, + "c": {1: anchor(300, 80), 2: anchor(300, -20)} + }, numMarkClasses=4, glyphMap=self.GLYPHMAP) + self.assertEqual(getXML(baseArray.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildBaseRecord(self): + a = builder.buildAnchor + rec = builder.buildBaseRecord([a(500, -20), None, a(300, -15)]) + self.assertEqual(getXML(rec.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildCaretValueForCoord(self): + caret = builder.buildCaretValueForCoord(500) + self.assertEqual(getXML(caret.toXML), + ['', + ' ', + '']) + + def test_buildCaretValueForPoint(self): + caret = builder.buildCaretValueForPoint(23) + self.assertEqual(getXML(caret.toXML), + ['', + ' ', + '']) + + def test_buildComponentRecord(self): + a = builder.buildAnchor + rec = builder.buildComponentRecord([a(500, -20), None, a(300, -15)]) + self.assertEqual(getXML(rec.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildComponentRecord_empty(self): + self.assertIsNone(builder.buildComponentRecord([])) + + def test_buildComponentRecord_None(self): + self.assertIsNone(builder.buildComponentRecord(None)) + + def test_buildCoverage(self): + cov = builder.buildCoverage({"two", "four"}, {"two": 2, "four": 4}) + self.assertEqual(getXML(cov.toXML), + ['', + ' ', + ' ', + '']) + + def test_buildCursivePos(self): + pos = builder.buildCursivePosSubtable({ + "two": (self.ANCHOR1, self.ANCHOR2), + "four": (self.ANCHOR3, self.ANCHOR1) + }, self.GLYPHMAP) + self.assertEqual(getXML(pos.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildDevice_format1(self): + device = builder.buildDevice({1:1, 0:0}) + self.assertEqual(getXML(device.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildDevice_format2(self): + device = builder.buildDevice({2:2, 0:1, 1:0}) + self.assertEqual(getXML(device.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildDevice_format3(self): + device = builder.buildDevice({5:3, 1:77}) + self.assertEqual(getXML(device.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildLigatureArray(self): + anchor = builder.buildAnchor + ligatureArray = builder.buildLigatureArray({ + "f_i": [{2: anchor(300, -20)}, {}], + "c_t": [{}, {1: anchor(500, 350), 2: anchor(1300, -20)}] + }, numMarkClasses=4, glyphMap=self.GLYPHMAP) + self.assertEqual(getXML(ligatureArray.toXML), + ['', + ' ', + ' ', # f_i + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildLigatureAttach(self): + anchor = builder.buildAnchor + attach = builder.buildLigatureAttach([ + [anchor(500, -10), None], + [None, anchor(300, -20), None]]) + self.assertEqual(getXML(attach.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildLigatureAttach_emptyComponents(self): + attach = builder.buildLigatureAttach([[], None]) + self.assertEqual(getXML(attach.toXML), + ['', + ' ', + ' ', + ' ', + '']) + + def test_buildLigatureAttach_noComponents(self): + attach = builder.buildLigatureAttach([]) + self.assertEqual(getXML(attach.toXML), + ['', + ' ', + '']) + + def test_buildLigCaretList(self): + carets = builder.buildLigCaretList( + {"f_f_i": [300, 600]}, {"c_t": [42]}, self.GLYPHMAP) + self.assertEqual(getXML(carets.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildLigCaretList_bothCoordsAndPointsForSameGlyph(self): + carets = builder.buildLigCaretList( + {"f_f_i": [300]}, {"f_f_i": [7]}, self.GLYPHMAP) + self.assertEqual(getXML(carets.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildLigCaretList_empty(self): + self.assertIsNone(builder.buildLigCaretList({}, {}, self.GLYPHMAP)) + + def test_buildLigCaretList_None(self): + self.assertIsNone(builder.buildLigCaretList(None, None, self.GLYPHMAP)) + + def test_buildLigGlyph_coords(self): + lig = builder.buildLigGlyph([500, 800], None) + self.assertEqual(getXML(lig.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildLigGlyph_empty(self): + self.assertIsNone(builder.buildLigGlyph([], [])) + + def test_buildLigGlyph_None(self): + self.assertIsNone(builder.buildLigGlyph(None, None)) + + def test_buildLigGlyph_points(self): + lig = builder.buildLigGlyph(None, [2]) + self.assertEqual(getXML(lig.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildLookup(self): + s1 = builder.buildSingleSubstSubtable({"one": "two"}) + s2 = builder.buildSingleSubstSubtable({"three": "four"}) + lookup = builder.buildLookup([s1, s2], flags=7) + self.assertEqual(getXML(lookup.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildLookup_badFlags(self): + s = builder.buildSingleSubstSubtable({"one": "two"}) + self.assertRaisesRegex( + AssertionError, "if markFilterSet is None, " + "flags must not set LOOKUP_FLAG_USE_MARK_FILTERING_SET; " + "flags=0x0010", + builder.buildLookup, [s], + builder.LOOKUP_FLAG_USE_MARK_FILTERING_SET, None) + self.assertRaisesRegex( + AssertionError, "if markFilterSet is not None, " + "flags must set LOOKUP_FLAG_USE_MARK_FILTERING_SET; " + "flags=0x0004", + builder.buildLookup, [s], + builder.LOOKUP_FLAG_IGNORE_LIGATURES, 777) + + def test_buildLookup_conflictingSubtableTypes(self): + s1 = builder.buildSingleSubstSubtable({"one": "two"}) + s2 = builder.buildAlternateSubstSubtable({"one": ["two", "three"]}) + self.assertRaisesRegex( + AssertionError, "all subtables must have the same LookupType", + builder.buildLookup, [s1, s2]) + + def test_buildLookup_noSubtables(self): + self.assertIsNone(builder.buildLookup([])) + self.assertIsNone(builder.buildLookup(None)) + self.assertIsNone(builder.buildLookup([None])) + self.assertIsNone(builder.buildLookup([None, None])) + + def test_buildLookup_markFilterSet(self): + s = builder.buildSingleSubstSubtable({"one": "two"}) + flags = (builder.LOOKUP_FLAG_RIGHT_TO_LEFT | + builder.LOOKUP_FLAG_USE_MARK_FILTERING_SET) + lookup = builder.buildLookup([s], flags, markFilterSet=999) + self.assertEqual(getXML(lookup.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildMarkArray(self): + markArray = builder.buildMarkArray({ + "acute": (7, builder.buildAnchor(300, 800)), + "grave": (2, builder.buildAnchor(10, 80)) + }, self.GLYPHMAP) + self.assertLess(self.GLYPHMAP["grave"], self.GLYPHMAP["acute"]) + self.assertEqual(getXML(markArray.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildMarkBasePosSubtable(self): + anchor = builder.buildAnchor + marks = { + "acute": (0, anchor(300, 700)), + "cedilla": (1, anchor(300, -100)), + "grave": (0, anchor(300, 700)) + } + bases = { + # Make sure we can handle missing entries. + "A": {}, # no entry for any markClass + "B": {0: anchor(500, 900)}, # only markClass 0 specified + "C": {1: anchor(500, -10)}, # only markClass 1 specified + + "a": {0: anchor(500, 400), 1: anchor(500, -20)}, + "b": {0: anchor(500, 800), 1: anchor(500, -20)} + } + table = builder.buildMarkBasePosSubtable(marks, bases, self.GLYPHMAP) + self.assertEqual(getXML(table.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', # grave + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', # acute + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', # cedilla + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', # A + ' ', + ' ', + ' ', + ' ', # B + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', # C + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', # a + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', # b + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildMarkGlyphSetsDef(self): + marksets = builder.buildMarkGlyphSetsDef( + [{"acute", "grave"}, {"cedilla", "grave"}], self.GLYPHMAP) + self.assertEqual(getXML(marksets.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildMarkGlyphSetsDef_empty(self): + self.assertIsNone(builder.buildMarkGlyphSetsDef([], self.GLYPHMAP)) + + def test_buildMarkGlyphSetsDef_None(self): + self.assertIsNone(builder.buildMarkGlyphSetsDef(None, self.GLYPHMAP)) + + def test_buildMarkLigPosSubtable(self): + anchor = builder.buildAnchor + marks = { + "acute": (0, anchor(300, 700)), + "cedilla": (1, anchor(300, -100)), + "grave": (0, anchor(300, 700)) + } + bases = { + "f_i": [{}, {0: anchor(200, 400)}], # nothing on f; only 1 on i + "c_t": [ + {0: anchor(500, 600), 1: anchor(500, -20)}, # c + {0: anchor(1300, 800), 1: anchor(1300, -20)} # t + ] + } + table = builder.buildMarkLigPosSubtable(marks, bases, self.GLYPHMAP) + self.assertEqual(getXML(table.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildMarkRecord(self): + rec = builder.buildMarkRecord(17, builder.buildAnchor(500, -20)) + self.assertEqual(getXML(rec.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildMark2Record(self): + a = builder.buildAnchor + rec = builder.buildMark2Record([a(500, -20), None, a(300, -15)]) + self.assertEqual(getXML(rec.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildPairPosClassesSubtable(self): + d20 = builder.buildValue({"XPlacement": -20}) + d50 = builder.buildValue({"XPlacement": -50}) + d0 = builder.buildValue({}) + d8020 = builder.buildValue({"XPlacement": -80, "YPlacement": -20}) + subtable = builder.buildPairPosClassesSubtable({ + (tuple("A",), tuple(["zero"])): (d0, d50), + (tuple("A",), tuple(["one", "two"])): (None, d20), + (tuple(["B", "C"]), tuple(["zero"])): (d8020, d50), + }, self.GLYPHMAP) + self.assertEqual(getXML(subtable.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildPairPosGlyphs(self): + d50 = builder.buildValue({"XPlacement": -50}) + d8020 = builder.buildValue({"XPlacement": -80, "YPlacement": -20}) + subtables = builder.buildPairPosGlyphs({ + ("A", "zero"): (None, d50), + ("A", "one"): (d8020, d50), + }, self.GLYPHMAP) + self.assertEqual(sum([getXML(t.toXML) for t in subtables], []), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildPairPosGlyphsSubtable(self): + d20 = builder.buildValue({"XPlacement": -20}) + d50 = builder.buildValue({"XPlacement": -50}) + d0 = builder.buildValue({}) + d8020 = builder.buildValue({"XPlacement": -80, "YPlacement": -20}) + subtable = builder.buildPairPosGlyphsSubtable({ + ("A", "zero"): (d0, d50), + ("A", "one"): (None, d20), + ("B", "five"): (d8020, d50), + }, self.GLYPHMAP) + self.assertEqual(getXML(subtable.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildSinglePos(self): + subtables = builder.buildSinglePos({ + "one": builder.buildValue({"XPlacement": 500}), + "two": builder.buildValue({"XPlacement": 500}), + "three": builder.buildValue({"XPlacement": 200}), + "four": builder.buildValue({"XPlacement": 400}), + "five": builder.buildValue({"XPlacement": 500}), + "six": builder.buildValue({"YPlacement": -6}), + }, self.GLYPHMAP) + self.assertEqual(sum([getXML(t.toXML) for t in subtables], []), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildSinglePos_ValueFormat0(self): + subtables = builder.buildSinglePos({ + "zero": builder.buildValue({}) + }, self.GLYPHMAP) + self.assertEqual(sum([getXML(t.toXML) for t in subtables], []), + ['', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildSinglePosSubtable_format1(self): + subtable = builder.buildSinglePosSubtable({ + "one": builder.buildValue({"XPlacement": 777}), + "two": builder.buildValue({"XPlacement": 777}), + }, self.GLYPHMAP) + self.assertEqual(getXML(subtable.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildSinglePosSubtable_format2(self): + subtable = builder.buildSinglePosSubtable({ + "one": builder.buildValue({"XPlacement": 777}), + "two": builder.buildValue({"YPlacement": -888}), + }, self.GLYPHMAP) + self.assertEqual(getXML(subtable.toXML), + ['', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '']) + + def test_buildValue(self): + value = builder.buildValue({"XPlacement": 7, "YPlacement": 23}) + func = lambda writer, font: value.toXML(writer, font, valueName="Val") + self.assertEqual(getXML(func), + ['']) + + def test_getLigatureKey(self): + components = lambda s: [tuple(word) for word in s.split()] + c = components("fi fl ff ffi fff") + c.sort(key=builder._getLigatureKey) + self.assertEqual(c, components("fff ffi ff fi fl")) + + def test_getSinglePosValueKey(self): + device = builder.buildDevice({10:1, 11:3}) + a1 = builder.buildValue({"XPlacement": 500, "XPlaDevice": device}) + a2 = builder.buildValue({"XPlacement": 500, "XPlaDevice": device}) + b = builder.buildValue({"XPlacement": 500}) + keyA1 = builder._getSinglePosValueKey(a1) + keyA2 = builder._getSinglePosValueKey(a1) + keyB = builder._getSinglePosValueKey(b) + self.assertEqual(keyA1, keyA2) + self.assertEqual(hash(keyA1), hash(keyA2)) + self.assertNotEqual(keyA1, keyB) + self.assertNotEqual(hash(keyA1), hash(keyB)) + + +class ClassDefBuilderTest(unittest.TestCase): + def test_build_usingClass0(self): + b = builder.ClassDefBuilder(useClass0=True) + b.add({"aa", "bb"}) + b.add({"a", "b"}) + b.add({"c"}) + b.add({"e", "f", "g", "h"}) + cdef = b.build() + self.assertIsInstance(cdef, otTables.ClassDef) + self.assertEqual(cdef.classDefs, { + "a": 2, + "b": 2, + "c": 3, + "aa": 1, + "bb": 1 + }) + + def test_build_notUsingClass0(self): + b = builder.ClassDefBuilder(useClass0=False) + b.add({"a", "b"}) + b.add({"c"}) + b.add({"e", "f", "g", "h"}) + cdef = b.build() + self.assertIsInstance(cdef, otTables.ClassDef) + self.assertEqual(cdef.classDefs, { + "a": 2, + "b": 2, + "c": 3, + "e": 1, + "f": 1, + "g": 1, + "h": 1 + }) + + def test_canAdd(self): + b = builder.ClassDefBuilder(useClass0=True) + b.add({"a", "b", "c", "d"}) + b.add({"e", "f"}) + self.assertTrue(b.canAdd({"a", "b", "c", "d"})) + self.assertTrue(b.canAdd({"e", "f"})) + self.assertTrue(b.canAdd({"g", "h", "i"})) + self.assertFalse(b.canAdd({"b", "c", "d"})) + self.assertFalse(b.canAdd({"a", "b", "c", "d", "e", "f"})) + self.assertFalse(b.canAdd({"d", "e", "f"})) + self.assertFalse(b.canAdd({"f"})) + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/pens/areaPen_test.py b/Tests/pens/areaPen_test.py new file mode 100644 index 0000000..ae915df --- /dev/null +++ b/Tests/pens/areaPen_test.py @@ -0,0 +1,180 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.pens.areaPen import AreaPen +import unittest + +precision = 6 + +def draw1_(pen): + pen.moveTo( (254, 360) ) + pen.lineTo( (771, 367) ) + pen.curveTo( (800, 393), (808, 399), (819, 412) ) + pen.curveTo( (818, 388), (774, 138), (489, 145) ) + pen.curveTo( (188, 145), (200, 398), (200, 421) ) + pen.curveTo( (209, 409), (220, 394), (254, 360) ) + pen.closePath() + +def draw2_(pen): + pen.moveTo( (254, 360) ) + pen.curveTo( (220, 394), (209, 409), (200, 421) ) + pen.curveTo( (200, 398), (188, 145), (489, 145) ) + pen.curveTo( (774, 138), (818, 388), (819, 412) ) + pen.curveTo( (808, 399), (800, 393), (771, 367) ) + pen.closePath() + +def draw3_(pen): + pen.moveTo( (771, 367) ) + pen.curveTo( (800, 393), (808, 399), (819, 412) ) + pen.curveTo( (818, 388), (774, 138), (489, 145) ) + pen.curveTo( (188, 145), (200, 398), (200, 421) ) + pen.curveTo( (209, 409), (220, 394), (254, 360) ) + pen.closePath() + +def draw4_(pen): + pen.moveTo( (771, 367) ) + pen.lineTo( (254, 360) ) + pen.curveTo( (220, 394), (209, 409), (200, 421) ) + pen.curveTo( (200, 398), (188, 145), (489, 145) ) + pen.curveTo( (774, 138), (818, 388), (819, 412) ) + pen.curveTo( (808, 399), (800, 393), (771, 367) ) + pen.closePath() + +def draw5_(pen): + pen.moveTo( (254, 360) ) + pen.lineTo( (771, 367) ) + pen.qCurveTo( (793, 386), (802, 394) ) + pen.qCurveTo( (811, 402), (819, 412) ) + pen.qCurveTo( (819, 406), (814, 383.5) ) + pen.qCurveTo( (809, 361), (796, 330.5) ) + pen.qCurveTo( (783, 300), (760.5, 266.5) ) + pen.qCurveTo( (738, 233), (701, 205.5) ) + pen.qCurveTo( (664, 178), (612, 160.5) ) + pen.qCurveTo( (560, 143), (489, 145) ) + pen.qCurveTo( (414, 145), (363, 164) ) + pen.qCurveTo( (312, 183), (280, 211.5) ) + pen.qCurveTo( (248, 240), (231.5, 274.5) ) + pen.qCurveTo( (215, 309), (208, 339.5) ) + pen.qCurveTo( (201, 370), (200.5, 392.5) ) + pen.qCurveTo( (200, 415), (200, 421) ) + pen.qCurveTo( (207, 412), (217.5, 399) ) + pen.qCurveTo( (228, 386), (254, 360) ) + pen.closePath() + +def draw6_(pen): + pen.moveTo( (254, 360) ) + pen.qCurveTo( (228, 386), (217.5, 399) ) + pen.qCurveTo( (207, 412), (200, 421) ) + pen.qCurveTo( (200, 415), (200.5, 392.5) ) + pen.qCurveTo( (201, 370), (208, 339.5) ) + pen.qCurveTo( (215, 309), (231.5, 274.5) ) + pen.qCurveTo( (248, 240), (280, 211.5) ) + pen.qCurveTo( (312, 183), (363, 164) ) + pen.qCurveTo( (414, 145), (489, 145) ) + pen.qCurveTo( (560, 143), (612, 160.5) ) + pen.qCurveTo( (664, 178), (701, 205.5) ) + pen.qCurveTo( (738, 233), (760.5, 266.5) ) + pen.qCurveTo( (783, 300), (796, 330.5) ) + pen.qCurveTo( (809, 361), (814, 383.5) ) + pen.qCurveTo( (819, 406), (819, 412) ) + pen.qCurveTo( (811, 402), (802, 394) ) + pen.qCurveTo( (793, 386), (771, 367) ) + pen.closePath() + +def draw7_(pen): + pen.moveTo( (771, 367) ) + pen.qCurveTo( (793, 386), (802, 394) ) + pen.qCurveTo( (811, 402), (819, 412) ) + pen.qCurveTo( (819, 406), (814, 383.5) ) + pen.qCurveTo( (809, 361), (796, 330.5) ) + pen.qCurveTo( (783, 300), (760.5, 266.5) ) + pen.qCurveTo( (738, 233), (701, 205.5) ) + pen.qCurveTo( (664, 178), (612, 160.5) ) + pen.qCurveTo( (560, 143), (489, 145) ) + pen.qCurveTo( (414, 145), (363, 164) ) + pen.qCurveTo( (312, 183), (280, 211.5) ) + pen.qCurveTo( (248, 240), (231.5, 274.5) ) + pen.qCurveTo( (215, 309), (208, 339.5) ) + pen.qCurveTo( (201, 370), (200.5, 392.5) ) + pen.qCurveTo( (200, 415), (200, 421) ) + pen.qCurveTo( (207, 412), (217.5, 399) ) + pen.qCurveTo( (228, 386), (254, 360) ) + pen.closePath() + +def draw8_(pen): + pen.moveTo( (771, 367) ) + pen.lineTo( (254, 360) ) + pen.qCurveTo( (228, 386), (217.5, 399) ) + pen.qCurveTo( (207, 412), (200, 421) ) + pen.qCurveTo( (200, 415), (200.5, 392.5) ) + pen.qCurveTo( (201, 370), (208, 339.5) ) + pen.qCurveTo( (215, 309), (231.5, 274.5) ) + pen.qCurveTo( (248, 240), (280, 211.5) ) + pen.qCurveTo( (312, 183), (363, 164) ) + pen.qCurveTo( (414, 145), (489, 145) ) + pen.qCurveTo( (560, 143), (612, 160.5) ) + pen.qCurveTo( (664, 178), (701, 205.5) ) + pen.qCurveTo( (738, 233), (760.5, 266.5) ) + pen.qCurveTo( (783, 300), (796, 330.5) ) + pen.qCurveTo( (809, 361), (814, 383.5) ) + pen.qCurveTo( (819, 406), (819, 412) ) + pen.qCurveTo( (811, 402), (802, 394) ) + pen.qCurveTo( (793, 386), (771, 367) ) + pen.closePath() + + +class AreaPenTest(unittest.TestCase): + def test_PScontour_clockwise_line_first(self): + pen = AreaPen(None) + draw1_(pen) + self.assertEqual(-104561.35, round(pen.value, precision)) + + def test_PScontour_counterclockwise_line_last(self): + pen = AreaPen(None) + draw2_(pen) + self.assertEqual(104561.35, round(pen.value, precision)) + + def test_PScontour_clockwise_line_last(self): + pen = AreaPen(None) + draw3_(pen) + self.assertEqual(-104561.35, round(pen.value, precision)) + + def test_PScontour_counterclockwise_line_first(self): + pen = AreaPen(None) + draw4_(pen) + self.assertEqual(104561.35, round(pen.value, precision)) + + def test_TTcontour_clockwise_line_first(self): + pen = AreaPen(None) + draw5_(pen) + self.assertEqual(-104602.791667, round(pen.value, precision)) + + def test_TTcontour_counterclockwise_line_last(self): + pen = AreaPen(None) + draw6_(pen) + self.assertEqual(104602.791667, round(pen.value, precision)) + + def test_TTcontour_clockwise_line_last(self): + pen = AreaPen(None) + draw7_(pen) + self.assertEqual(-104602.791667, round(pen.value, precision)) + + def test_TTcontour_counterclockwise_line_first(self): + pen = AreaPen(None) + draw8_(pen) + self.assertEqual(104602.791667, round(pen.value, precision)) + + def test_openPaths(self): + pen = AreaPen() + pen.moveTo((0, 0)) + pen.endPath() + self.assertEqual(0, pen.value) + + pen.moveTo((0, 0)) + pen.lineTo((1, 0)) + with self.assertRaises(NotImplementedError): + pen.endPath() + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/pens/basePen_test.py b/Tests/pens/basePen_test.py new file mode 100644 index 0000000..2a7e43c --- /dev/null +++ b/Tests/pens/basePen_test.py @@ -0,0 +1,179 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.pens.basePen import \ + BasePen, decomposeSuperBezierSegment, decomposeQuadraticSegment +from fontTools.misc.loggingTools import CapturingLogHandler +import unittest + + +class _TestPen(BasePen): + def __init__(self): + BasePen.__init__(self, glyphSet={}) + self._commands = [] + + def __repr__(self): + return " ".join(self._commands) + + def getCurrentPoint(self): + return self._getCurrentPoint() + + def _moveTo(self, pt): + self._commands.append("%s %s moveto" % (pt[0], pt[1])) + + def _lineTo(self, pt): + self._commands.append("%s %s lineto" % (pt[0], pt[1])) + + def _curveToOne(self, bcp1, bcp2, pt): + self._commands.append("%s %s %s %s %s %s curveto" % + (bcp1[0], bcp1[1], + bcp2[0], bcp2[1], + pt[0], pt[1])) + + def _closePath(self): + self._commands.append("closepath") + + def _endPath(self): + self._commands.append("endpath") + + +class _TestGlyph: + def draw(self, pen): + pen.moveTo((0.0, 0.0)) + pen.lineTo((0.0, 100.0)) + pen.curveTo((50.0, 75.0), (60.0, 50.0), (50.0, 25.0), (0.0, 0.0)) + pen.closePath() + + +class BasePenTest(unittest.TestCase): + def test_moveTo(self): + pen = _TestPen() + pen.moveTo((0.5, -4.3)) + self.assertEqual("0.5 -4.3 moveto", repr(pen)) + self.assertEqual((0.5, -4.3), pen.getCurrentPoint()) + + def test_lineTo(self): + pen = _TestPen() + pen.moveTo((4, 5)) + pen.lineTo((7, 8)) + self.assertEqual("4 5 moveto 7 8 lineto", repr(pen)) + self.assertEqual((7, 8), pen.getCurrentPoint()) + + def test_curveTo_zeroPoints(self): + pen = _TestPen() + pen.moveTo((0.0, 0.0)) + self.assertRaises(AssertionError, pen.curveTo) + + def test_curveTo_onePoint(self): + pen = _TestPen() + pen.moveTo((0.0, 0.0)) + pen.curveTo((1.0, 1.1)) + self.assertEqual("0.0 0.0 moveto 1.0 1.1 lineto", repr(pen)) + self.assertEqual((1.0, 1.1), pen.getCurrentPoint()) + + def test_curveTo_twoPoints(self): + pen = _TestPen() + pen.moveTo((0.0, 0.0)) + pen.curveTo((6.0, 3.0), (3.0, 6.0)) + self.assertEqual("0.0 0.0 moveto 4.0 2.0 5.0 4.0 3.0 6.0 curveto", + repr(pen)) + self.assertEqual((3.0, 6.0), pen.getCurrentPoint()) + + def test_curveTo_manyPoints(self): + pen = _TestPen() + pen.moveTo((0.0, 0.0)) + pen.curveTo((1.0, 1.1), (2.0, 2.1), (3.0, 3.1), (4.0, 4.1)) + self.assertEqual("0.0 0.0 moveto " + "1.0 1.1 1.5 1.6 2.0 2.1 curveto " + "2.5 2.6 3.0 3.1 4.0 4.1 curveto", repr(pen)) + self.assertEqual((4.0, 4.1), pen.getCurrentPoint()) + + def test_qCurveTo_zeroPoints(self): + pen = _TestPen() + pen.moveTo((0.0, 0.0)) + self.assertRaises(AssertionError, pen.qCurveTo) + + def test_qCurveTo_onePoint(self): + pen = _TestPen() + pen.moveTo((0.0, 0.0)) + pen.qCurveTo((77.7, 99.9)) + self.assertEqual("0.0 0.0 moveto 77.7 99.9 lineto", repr(pen)) + self.assertEqual((77.7, 99.9), pen.getCurrentPoint()) + + def test_qCurveTo_manyPoints(self): + pen = _TestPen() + pen.moveTo((0.0, 0.0)) + pen.qCurveTo((6.0, 3.0), (3.0, 6.0)) + self.assertEqual("0.0 0.0 moveto 4.0 2.0 5.0 4.0 3.0 6.0 curveto", + repr(pen)) + self.assertEqual((3.0, 6.0), pen.getCurrentPoint()) + + def test_qCurveTo_onlyOffCurvePoints(self): + pen = _TestPen() + pen.moveTo((0.0, 0.0)) + pen.qCurveTo((6.0, -6.0), (12.0, 12.0), (18.0, -18.0), None) + self.assertEqual("0.0 0.0 moveto " + "12.0 -12.0 moveto " + "8.0 -8.0 7.0 -3.0 9.0 3.0 curveto " + "11.0 9.0 13.0 7.0 15.0 -3.0 curveto " + "17.0 -13.0 16.0 -16.0 12.0 -12.0 curveto", repr(pen)) + self.assertEqual((12.0, -12.0), pen.getCurrentPoint()) + + def test_closePath(self): + pen = _TestPen() + pen.lineTo((3, 4)) + pen.closePath() + self.assertEqual("3 4 lineto closepath", repr(pen)) + self.assertEqual(None, pen.getCurrentPoint()) + + def test_endPath(self): + pen = _TestPen() + pen.lineTo((3, 4)) + pen.endPath() + self.assertEqual("3 4 lineto endpath", repr(pen)) + self.assertEqual(None, pen.getCurrentPoint()) + + def test_addComponent(self): + pen = _TestPen() + pen.glyphSet["oslash"] = _TestGlyph() + pen.addComponent("oslash", (2, 3, 0.5, 2, -10, 0)) + self.assertEqual("-10.0 0.0 moveto " + "40.0 200.0 lineto " + "127.5 300.0 131.25 290.0 125.0 265.0 curveto " + "118.75 240.0 102.5 200.0 -10.0 0.0 curveto " + "closepath", repr(pen)) + self.assertEqual(None, pen.getCurrentPoint()) + + def test_addComponent_skip_missing(self): + pen = _TestPen() + with CapturingLogHandler(pen.log, "WARNING") as captor: + pen.addComponent("nonexistent", (1, 0, 0, 1, 0, 0)) + captor.assertRegex("glyph '.*' is missing from glyphSet; skipped") + + +class DecomposeSegmentTest(unittest.TestCase): + def test_decomposeSuperBezierSegment(self): + decompose = decomposeSuperBezierSegment + self.assertRaises(AssertionError, decompose, []) + self.assertRaises(AssertionError, decompose, [(0, 0)]) + self.assertRaises(AssertionError, decompose, [(0, 0), (1, 1)]) + self.assertEqual([((0, 0), (1, 1), (2, 2))], + decompose([(0, 0), (1, 1), (2, 2)])) + self.assertEqual( + [((0, 0), (2, -2), (4, 0)), ((6, 2), (8, 8), (12, -12))], + decompose([(0, 0), (4, -4), (8, 8), (12, -12)])) + + def test_decomposeQuadraticSegment(self): + decompose = decomposeQuadraticSegment + self.assertRaises(AssertionError, decompose, []) + self.assertRaises(AssertionError, decompose, [(0, 0)]) + self.assertEqual([((0,0), (4, 8))], decompose([(0, 0), (4, 8)])) + self.assertEqual([((0,0), (2, 4)), ((4, 8), (9, -9))], + decompose([(0, 0), (4, 8), (9, -9)])) + self.assertEqual( + [((0, 0), (2.0, 4.0)), ((4, 8), (6.5, -0.5)), ((9, -9), (10, 10))], + decompose([(0, 0), (4, 8), (9, -9), (10, 10)])) + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/pens/boundsPen_test.py b/Tests/pens/boundsPen_test.py new file mode 100644 index 0000000..533c575 --- /dev/null +++ b/Tests/pens/boundsPen_test.py @@ -0,0 +1,77 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.pens.boundsPen import BoundsPen, ControlBoundsPen +import unittest + + +def draw_(pen): + pen.moveTo((0, 0)) + pen.lineTo((0, 100)) + pen.qCurveTo((50, 75), (60, 50), (50, 25), (0, 0)) + pen.curveTo((-50, 25), (-60, 50), (-50, 75), (0, 100)) + pen.closePath() + + +def bounds_(pen): + return " ".join(["%.0f" % c for c in pen.bounds]) + + +class BoundsPenTest(unittest.TestCase): + def test_draw(self): + pen = BoundsPen(None) + draw_(pen) + self.assertEqual("-55 0 58 100", bounds_(pen)) + + def test_empty(self): + pen = BoundsPen(None) + self.assertEqual(None, pen.bounds) + + def test_curve(self): + pen = BoundsPen(None) + pen.moveTo((0, 0)) + pen.curveTo((20, 10), (90, 40), (0, 0)) + self.assertEqual("0 0 45 20", bounds_(pen)) + + def test_quadraticCurve(self): + pen = BoundsPen(None) + pen.moveTo((0, 0)) + pen.qCurveTo((6, 6), (10, 0)) + self.assertEqual("0 0 10 3", bounds_(pen)) + + +class ControlBoundsPenTest(unittest.TestCase): + def test_draw(self): + pen = ControlBoundsPen(None) + draw_(pen) + self.assertEqual("-55 0 60 100", bounds_(pen)) + + def test_empty(self): + pen = ControlBoundsPen(None) + self.assertEqual(None, pen.bounds) + + def test_curve(self): + pen = ControlBoundsPen(None) + pen.moveTo((0, 0)) + pen.curveTo((20, 10), (90, 40), (0, 0)) + self.assertEqual("0 0 90 40", bounds_(pen)) + + def test_quadraticCurve(self): + pen = ControlBoundsPen(None) + pen.moveTo((0, 0)) + pen.qCurveTo((6, 6), (10, 0)) + self.assertEqual("0 0 10 6", bounds_(pen)) + + def test_singlePoint(self): + pen = ControlBoundsPen(None) + pen.moveTo((-5, 10)) + self.assertEqual("-5 10 -5 10", bounds_(pen)) + + def test_ignoreSinglePoint(self): + pen = ControlBoundsPen(None, ignoreSinglePoints=True) + pen.moveTo((0, 10)) + self.assertEqual(None, pen.bounds) + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/pens/perimeterPen_test.py b/Tests/pens/perimeterPen_test.py new file mode 100644 index 0000000..aa73c3d --- /dev/null +++ b/Tests/pens/perimeterPen_test.py @@ -0,0 +1,167 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.pens.perimeterPen import PerimeterPen +import unittest + +def draw1_(pen): + pen.moveTo( (254, 360) ) + pen.lineTo( (771, 367) ) + pen.curveTo( (800, 393), (808, 399), (819, 412) ) + pen.curveTo( (818, 388), (774, 138), (489, 145) ) + pen.curveTo( (188, 145), (200, 398), (200, 421) ) + pen.curveTo( (209, 409), (220, 394), (254, 360) ) + pen.closePath() + +def draw2_(pen): + pen.moveTo( (254, 360) ) + pen.curveTo( (220, 394), (209, 409), (200, 421) ) + pen.curveTo( (200, 398), (188, 145), (489, 145) ) + pen.curveTo( (774, 138), (818, 388), (819, 412) ) + pen.curveTo( (808, 399), (800, 393), (771, 367) ) + pen.closePath() + +def draw3_(pen): + pen.moveTo( (771, 367) ) + pen.curveTo( (800, 393), (808, 399), (819, 412) ) + pen.curveTo( (818, 388), (774, 138), (489, 145) ) + pen.curveTo( (188, 145), (200, 398), (200, 421) ) + pen.curveTo( (209, 409), (220, 394), (254, 360) ) + pen.closePath() + +def draw4_(pen): + pen.moveTo( (771, 367) ) + pen.lineTo( (254, 360) ) + pen.curveTo( (220, 394), (209, 409), (200, 421) ) + pen.curveTo( (200, 398), (188, 145), (489, 145) ) + pen.curveTo( (774, 138), (818, 388), (819, 412) ) + pen.curveTo( (808, 399), (800, 393), (771, 367) ) + pen.closePath() + +def draw5_(pen): + pen.moveTo( (254, 360) ) + pen.lineTo( (771, 367) ) + pen.qCurveTo( (793, 386), (802, 394) ) + pen.qCurveTo( (811, 402), (819, 412) ) + pen.qCurveTo( (819, 406), (814, 383.5) ) + pen.qCurveTo( (809, 361), (796, 330.5) ) + pen.qCurveTo( (783, 300), (760.5, 266.5) ) + pen.qCurveTo( (738, 233), (701, 205.5) ) + pen.qCurveTo( (664, 178), (612, 160.5) ) + pen.qCurveTo( (560, 143), (489, 145) ) + pen.qCurveTo( (414, 145), (363, 164) ) + pen.qCurveTo( (312, 183), (280, 211.5) ) + pen.qCurveTo( (248, 240), (231.5, 274.5) ) + pen.qCurveTo( (215, 309), (208, 339.5) ) + pen.qCurveTo( (201, 370), (200.5, 392.5) ) + pen.qCurveTo( (200, 415), (200, 421) ) + pen.qCurveTo( (207, 412), (217.5, 399) ) + pen.qCurveTo( (228, 386), (254, 360) ) + pen.closePath() + +def draw6_(pen): + pen.moveTo( (254, 360) ) + pen.qCurveTo( (228, 386), (217.5, 399) ) + pen.qCurveTo( (207, 412), (200, 421) ) + pen.qCurveTo( (200, 415), (200.5, 392.5) ) + pen.qCurveTo( (201, 370), (208, 339.5) ) + pen.qCurveTo( (215, 309), (231.5, 274.5) ) + pen.qCurveTo( (248, 240), (280, 211.5) ) + pen.qCurveTo( (312, 183), (363, 164) ) + pen.qCurveTo( (414, 145), (489, 145) ) + pen.qCurveTo( (560, 143), (612, 160.5) ) + pen.qCurveTo( (664, 178), (701, 205.5) ) + pen.qCurveTo( (738, 233), (760.5, 266.5) ) + pen.qCurveTo( (783, 300), (796, 330.5) ) + pen.qCurveTo( (809, 361), (814, 383.5) ) + pen.qCurveTo( (819, 406), (819, 412) ) + pen.qCurveTo( (811, 402), (802, 394) ) + pen.qCurveTo( (793, 386), (771, 367) ) + pen.closePath() + +def draw7_(pen): + pen.moveTo( (771, 367) ) + pen.qCurveTo( (793, 386), (802, 394) ) + pen.qCurveTo( (811, 402), (819, 412) ) + pen.qCurveTo( (819, 406), (814, 383.5) ) + pen.qCurveTo( (809, 361), (796, 330.5) ) + pen.qCurveTo( (783, 300), (760.5, 266.5) ) + pen.qCurveTo( (738, 233), (701, 205.5) ) + pen.qCurveTo( (664, 178), (612, 160.5) ) + pen.qCurveTo( (560, 143), (489, 145) ) + pen.qCurveTo( (414, 145), (363, 164) ) + pen.qCurveTo( (312, 183), (280, 211.5) ) + pen.qCurveTo( (248, 240), (231.5, 274.5) ) + pen.qCurveTo( (215, 309), (208, 339.5) ) + pen.qCurveTo( (201, 370), (200.5, 392.5) ) + pen.qCurveTo( (200, 415), (200, 421) ) + pen.qCurveTo( (207, 412), (217.5, 399) ) + pen.qCurveTo( (228, 386), (254, 360) ) + pen.closePath() + +def draw8_(pen): + pen.moveTo( (771, 367) ) + pen.lineTo( (254, 360) ) + pen.qCurveTo( (228, 386), (217.5, 399) ) + pen.qCurveTo( (207, 412), (200, 421) ) + pen.qCurveTo( (200, 415), (200.5, 392.5) ) + pen.qCurveTo( (201, 370), (208, 339.5) ) + pen.qCurveTo( (215, 309), (231.5, 274.5) ) + pen.qCurveTo( (248, 240), (280, 211.5) ) + pen.qCurveTo( (312, 183), (363, 164) ) + pen.qCurveTo( (414, 145), (489, 145) ) + pen.qCurveTo( (560, 143), (612, 160.5) ) + pen.qCurveTo( (664, 178), (701, 205.5) ) + pen.qCurveTo( (738, 233), (760.5, 266.5) ) + pen.qCurveTo( (783, 300), (796, 330.5) ) + pen.qCurveTo( (809, 361), (814, 383.5) ) + pen.qCurveTo( (819, 406), (819, 412) ) + pen.qCurveTo( (811, 402), (802, 394) ) + pen.qCurveTo( (793, 386), (771, 367) ) + pen.closePath() + + +class PerimeterPenTest(unittest.TestCase): + def test_PScontour_clockwise_line_first(self): + pen = PerimeterPen(None) + draw1_(pen) + self.assertEqual(1589, round(pen.value)) + + def test_PScontour_counterclockwise_line_last(self): + pen = PerimeterPen(None) + draw2_(pen) + self.assertEqual(1589, round(pen.value)) + + def test_PScontour_clockwise_line_last(self): + pen = PerimeterPen(None) + draw3_(pen) + self.assertEqual(1589, round(pen.value)) + + def test_PScontour_counterclockwise_line_first(self): + pen = PerimeterPen(None) + draw4_(pen) + self.assertEqual(1589, round(pen.value)) + + def test_TTcontour_clockwise_line_first(self): + pen = PerimeterPen(None) + draw5_(pen) + self.assertEqual(1589, round(pen.value)) + + def test_TTcontour_counterclockwise_line_last(self): + pen = PerimeterPen(None) + draw6_(pen) + self.assertEqual(1589, round(pen.value)) + + def test_TTcontour_clockwise_line_last(self): + pen = PerimeterPen(None) + draw7_(pen) + self.assertEqual(1589, round(pen.value)) + + def test_TTcontour_counterclockwise_line_first(self): + pen = PerimeterPen(None) + draw8_(pen) + self.assertEqual(1589, round(pen.value)) + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/pens/pointInsidePen_test.py b/Tests/pens/pointInsidePen_test.py new file mode 100644 index 0000000..3696ece --- /dev/null +++ b/Tests/pens/pointInsidePen_test.py @@ -0,0 +1,225 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.pens.pointInsidePen import PointInsidePen +import unittest + + +class PointInsidePenTest(unittest.TestCase): + def test_line(self): + def draw_triangles(pen): + pen.moveTo((0,0)); pen.lineTo((10,5)); pen.lineTo((10,0)) + pen.moveTo((9,1)); pen.lineTo((4,1)); pen.lineTo((9,4)) + pen.closePath() + + self.assertEqual( + " *********" + " ** *" + " ** *" + " * *" + " *", + self.render(draw_triangles, even_odd=True)) + + self.assertEqual( + " *********" + " *******" + " *****" + " ***" + " *", + self.render(draw_triangles, even_odd=False)) + + def test_curve(self): + def draw_curves(pen): + pen.moveTo((0,0)); pen.curveTo((9,1), (9,4), (0,5)) + pen.moveTo((10,5)); pen.curveTo((1,4), (1,1), (10,0)) + pen.closePath() + + self.assertEqual( + "*** ***" + "**** ****" + "*** ***" + "**** ****" + "*** ***", + self.render(draw_curves, even_odd=True)) + + self.assertEqual( + "*** ***" + "**********" + "**********" + "**********" + "*** ***", + self.render(draw_curves, even_odd=False)) + + def test_qCurve(self): + def draw_qCurves(pen): + pen.moveTo((0,0)); pen.qCurveTo((15,2), (0,5)) + pen.moveTo((10,5)); pen.qCurveTo((-5,3), (10,0)) + pen.closePath() + + self.assertEqual( + "*** **" + "**** ***" + "*** ***" + "*** ****" + "** ***", + self.render(draw_qCurves, even_odd=True)) + + self.assertEqual( + "*** **" + "**********" + "**********" + "**********" + "** ***", + self.render(draw_qCurves, even_odd=False)) + + @staticmethod + def render(draw_function, even_odd): + result = BytesIO() + for y in range(5): + for x in range(10): + pen = PointInsidePen(None, (x + 0.5, y + 0.5), even_odd) + draw_function(pen) + if pen.getResult(): + result.write(b"*") + else: + result.write(b" ") + return tounicode(result.getvalue()) + + + def test_contour_no_solutions(self): + def draw_contour(pen): + pen.moveTo( (969, 230) ) + pen.curveTo( (825, 348) , (715, 184) , (614, 202) ) + pen.lineTo( (614, 160) ) + pen.lineTo( (969, 160) ) + pen.closePath() + + piPen = PointInsidePen(None, (750, 295)) # this point is outside + draw_contour(piPen) + self.assertEqual(piPen.getWinding(), 0) + self.assertEqual(piPen.getResult(), False) + + piPen = PointInsidePen(None, (835, 190)) # this point is inside + draw_contour(piPen) + self.assertEqual(piPen.getWinding(), 1) + self.assertEqual(piPen.getResult(), True) + + def test_contour_square_closed(self): + def draw_contour(pen): + pen.moveTo( (100, 100) ) + pen.lineTo( (-100, 100) ) + pen.lineTo( (-100, -100) ) + pen.lineTo( (100, -100) ) + pen.closePath() + + piPen = PointInsidePen(None, (0, 0)) # this point is inside + draw_contour(piPen) + self.assertEqual(piPen.getWinding(), 1) + self.assertEqual(piPen.getResult(), True) + + def test_contour_square_opened(self): + def draw_contour(pen): + pen.moveTo( (100, 100) ) + pen.lineTo( (-100, 100) ) + pen.lineTo( (-100, -100) ) + pen.lineTo( (100, -100) ) + # contour not explicitly closed + + piPen = PointInsidePen(None, (0, 0)) # this point is inside + draw_contour(piPen) + self.assertEqual(piPen.getWinding(), 1) + self.assertEqual(piPen.getResult(), True) + + def test_contour_circle(self): + def draw_contour(pen): + pen.moveTo( (0, 100) ) + pen.curveTo( (-55, 100) , (-100, 55) , (-100, 0) ) + pen.curveTo( (-100, -55) , (-55, -100) , (0, -100) ) + pen.curveTo( (55, -100) , (100, -55) , (100, 0) ) + pen.curveTo( (100, 55) , (55, 100) , (0, 100) ) + + piPen = PointInsidePen(None, (50, 50)) # this point is inside + draw_contour(piPen) + self.assertEqual(piPen.getResult(), True) + + piPen = PointInsidePen(None, (50, -50)) # this point is inside + draw_contour(piPen) + self.assertEqual(piPen.getResult(), True) + + def test_contour_diamond(self): + def draw_contour(pen): + pen.moveTo( (0, 100) ) + pen.lineTo( (100, 0) ) + pen.lineTo( (0, -100) ) + pen.lineTo( (-100, 0) ) + pen.closePath() + + piPen = PointInsidePen(None, (-200, 0)) # this point is outside + draw_contour(piPen) + self.assertEqual(piPen.getWinding(), 0) + + piPen = PointInsidePen(None, (-200, 100)) # this point is outside + draw_contour(piPen) + self.assertEqual(piPen.getWinding(), 0) + + piPen = PointInsidePen(None, (-200, -100)) # this point is outside + draw_contour(piPen) + self.assertEqual(piPen.getWinding(), 0) + + piPen = PointInsidePen(None, (-200, 50)) # this point is outside + draw_contour(piPen) + self.assertEqual(piPen.getWinding(), 0) + + def test_contour_integers(self): + def draw_contour(pen): + pen.moveTo( (728, 697) ) + pen.lineTo( (504, 699) ) + pen.curveTo( (487, 719) , (508, 783) , (556, 783) ) + pen.lineTo( (718, 783) ) + pen.curveTo( (739, 783) , (749, 712) , (728, 697) ) + pen.closePath() + + piPen = PointInsidePen(None, (416, 783)) # this point is outside + draw_contour(piPen) + self.assertEqual(piPen.getWinding(), 0) + + def test_contour_decimals(self): + def draw_contour(pen): + pen.moveTo( (727.546875, 697.0) ) + pen.lineTo( (504.375, 698.515625) ) + pen.curveTo( (487.328125, 719.359375), (507.84375, 783.140625), (555.796875, 783.140625) ) + pen.lineTo( (717.96875, 783.140625) ) + pen.curveTo( (738.890625, 783.140625), (748.796875, 711.5), (727.546875, 697.0) ) + pen.closePath() + + piPen = PointInsidePen(None, (416.625, 783.140625)) # this point is outside + draw_contour(piPen) + self.assertEqual(piPen.getWinding(), 0) + + def test_contour2_integers(self): + def draw_contour(pen): + pen.moveTo( (51, 22) ) + pen.lineTo( (51, 74) ) + pen.lineTo( (83, 50) ) + pen.curveTo( (83, 49) , (82, 48) , (82, 47) ) + pen.closePath() + + piPen = PointInsidePen(None, (21, 50)) # this point is outside + draw_contour(piPen) + self.assertEqual(piPen.getWinding(), 0) + + def test_contour2_decimals(self): + def draw_contour(pen): + pen.moveTo( (51.25, 21.859375) ) + pen.lineTo( (51.25, 73.828125) ) + pen.lineTo( (82.5, 50.0) ) + pen.curveTo( (82.5, 49.09375) , (82.265625, 48.265625) , (82.234375, 47.375) ) + pen.closePath() + + piPen = PointInsidePen(None, (21.25, 50.0)) # this point is outside + draw_contour(piPen) + self.assertEqual(piPen.getWinding(), 0) + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) + diff --git a/Tests/pens/recordingPen_test.py b/Tests/pens/recordingPen_test.py new file mode 100644 index 0000000..fdc5d06 --- /dev/null +++ b/Tests/pens/recordingPen_test.py @@ -0,0 +1,39 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.pens.recordingPen import RecordingPen, DecomposingRecordingPen +import pytest + + +class _TestGlyph(object): + + def draw(self, pen): + pen.moveTo((0.0, 0.0)) + pen.lineTo((0.0, 100.0)) + pen.curveTo((50.0, 75.0), (60.0, 50.0), (50.0, 0.0)) + pen.closePath() + + +class RecordingPenTest(object): + + def test_addComponent(self): + pen = RecordingPen() + pen.addComponent("a", (2, 0, 0, 3, -10, 5)) + assert pen.value == [("addComponent", ("a", (2, 0, 0, 3, -10, 5)))] + + +class DecomposingRecordingPenTest(object): + + def test_addComponent_decomposed(self): + pen = DecomposingRecordingPen({"a": _TestGlyph()}) + pen.addComponent("a", (2, 0, 0, 3, -10, 5)) + assert pen.value == [ + ('moveTo', ((-10.0, 5.0),)), + ('lineTo', ((-10.0, 305.0),)), + ('curveTo', ((90.0, 230.0), (110.0, 155.0), (90.0, 5.0),)), + ('closePath', ())] + + def test_addComponent_missing_raises(self): + pen = DecomposingRecordingPen(dict()) + with pytest.raises(KeyError) as excinfo: + pen.addComponent("a", (1, 0, 0, 1, 0, 0)) + assert excinfo.value.args[0] == "a" diff --git a/Tests/pens/reverseContourPen_test.py b/Tests/pens/reverseContourPen_test.py new file mode 100644 index 0000000..bace806 --- /dev/null +++ b/Tests/pens/reverseContourPen_test.py @@ -0,0 +1,319 @@ +from fontTools.pens.recordingPen import RecordingPen +from fontTools.pens.reverseContourPen import ReverseContourPen +import pytest + + +TEST_DATA = [ + ( + [ + ('moveTo', ((0, 0),)), + ('lineTo', ((1, 1),)), + ('lineTo', ((2, 2),)), + ('lineTo', ((3, 3),)), # last not on move, line is implied + ('closePath', ()), + ], + [ + ('moveTo', ((0, 0),)), + ('lineTo', ((3, 3),)), + ('lineTo', ((2, 2),)), + ('lineTo', ((1, 1),)), + ('closePath', ()), + ] + ), + ( + [ + ('moveTo', ((0, 0),)), + ('lineTo', ((1, 1),)), + ('lineTo', ((2, 2),)), + ('lineTo', ((0, 0),)), # last on move, no implied line + ('closePath', ()), + ], + [ + ('moveTo', ((0, 0),)), + ('lineTo', ((2, 2),)), + ('lineTo', ((1, 1),)), + ('closePath', ()), + ] + ), + ( + [ + ('moveTo', ((0, 0),)), + ('lineTo', ((0, 0),)), + ('lineTo', ((1, 1),)), + ('lineTo', ((2, 2),)), + ('closePath', ()), + ], + [ + ('moveTo', ((0, 0),)), + ('lineTo', ((2, 2),)), + ('lineTo', ((1, 1),)), + ('lineTo', ((0, 0),)), + ('lineTo', ((0, 0),)), + ('closePath', ()), + ] + ), + ( + [ + ('moveTo', ((0, 0),)), + ('lineTo', ((1, 1),)), + ('closePath', ()), + ], + [ + ('moveTo', ((0, 0),)), + ('lineTo', ((1, 1),)), + ('closePath', ()), + ] + ), + ( + [ + ('moveTo', ((0, 0),)), + ('curveTo', ((1, 1), (2, 2), (3, 3))), + ('curveTo', ((4, 4), (5, 5), (0, 0))), + ('closePath', ()), + ], + [ + ('moveTo', ((0, 0),)), + ('curveTo', ((5, 5), (4, 4), (3, 3))), + ('curveTo', ((2, 2), (1, 1), (0, 0))), + ('closePath', ()), + ] + ), + ( + [ + ('moveTo', ((0, 0),)), + ('curveTo', ((1, 1), (2, 2), (3, 3))), + ('curveTo', ((4, 4), (5, 5), (6, 6))), + ('closePath', ()), + ], + [ + ('moveTo', ((0, 0),)), + ('lineTo', ((6, 6),)), # implied line + ('curveTo', ((5, 5), (4, 4), (3, 3))), + ('curveTo', ((2, 2), (1, 1), (0, 0))), + ('closePath', ()), + ] + ), + ( + [ + ('moveTo', ((0, 0),)), + ('lineTo', ((1, 1),)), # this line becomes implied + ('curveTo', ((2, 2), (3, 3), (4, 4))), + ('curveTo', ((5, 5), (6, 6), (7, 7))), + ('closePath', ()), + ], + [ + ('moveTo', ((0, 0),)), + ('lineTo', ((7, 7),)), + ('curveTo', ((6, 6), (5, 5), (4, 4))), + ('curveTo', ((3, 3), (2, 2), (1, 1))), + ('closePath', ()), + ] + ), + ( + [ + ('moveTo', ((0, 0),)), + ('qCurveTo', ((1, 1), (2, 2))), + ('qCurveTo', ((3, 3), (0, 0))), + ('closePath', ()), + ], + [ + ('moveTo', ((0, 0),)), + ('qCurveTo', ((3, 3), (2, 2))), + ('qCurveTo', ((1, 1), (0, 0))), + ('closePath', ()), + ] + ), + ( + [ + ('moveTo', ((0, 0),)), + ('qCurveTo', ((1, 1), (2, 2))), + ('qCurveTo', ((3, 3), (4, 4))), + ('closePath', ()), + ], + [ + ('moveTo', ((0, 0),)), + ('lineTo', ((4, 4),)), + ('qCurveTo', ((3, 3), (2, 2))), + ('qCurveTo', ((1, 1), (0, 0))), + ('closePath', ()), + ] + ), + ( + [ + ('moveTo', ((0, 0),)), + ('lineTo', ((1, 1),)), + ('qCurveTo', ((2, 2), (3, 3))), + ('closePath', ()), + ], + [ + ('moveTo', ((0, 0),)), + ('lineTo', ((3, 3),)), + ('qCurveTo', ((2, 2), (1, 1))), + ('closePath', ()), + ] + ), + ( + [ + ('addComponent', ('a', (1, 0, 0, 1, 0, 0))) + ], + [ + ('addComponent', ('a', (1, 0, 0, 1, 0, 0))) + ] + ), + ( + [], [] + ), + ( + [ + ('moveTo', ((0, 0),)), + ('endPath', ()), + ], + [ + ('moveTo', ((0, 0),)), + ('endPath', ()), + ], + ), + ( + [ + ('moveTo', ((0, 0),)), + ('closePath', ()), + ], + [ + ('moveTo', ((0, 0),)), + ('endPath', ()), # single-point paths is always open + ], + ), + ( + [ + ('moveTo', ((0, 0),)), + ('lineTo', ((1, 1),)), + ('endPath', ()) + ], + [ + ('moveTo', ((1, 1),)), + ('lineTo', ((0, 0),)), + ('endPath', ()) + ] + ), + ( + [ + ('moveTo', ((0, 0),)), + ('curveTo', ((1, 1), (2, 2), (3, 3))), + ('endPath', ()) + ], + [ + ('moveTo', ((3, 3),)), + ('curveTo', ((2, 2), (1, 1), (0, 0))), + ('endPath', ()) + ] + ), + ( + [ + ('moveTo', ((0, 0),)), + ('curveTo', ((1, 1), (2, 2), (3, 3))), + ('lineTo', ((4, 4),)), + ('endPath', ()) + ], + [ + ('moveTo', ((4, 4),)), + ('lineTo', ((3, 3),)), + ('curveTo', ((2, 2), (1, 1), (0, 0))), + ('endPath', ()) + ] + ), + ( + [ + ('moveTo', ((0, 0),)), + ('lineTo', ((1, 1),)), + ('curveTo', ((2, 2), (3, 3), (4, 4))), + ('endPath', ()) + ], + [ + ('moveTo', ((4, 4),)), + ('curveTo', ((3, 3), (2, 2), (1, 1))), + ('lineTo', ((0, 0),)), + ('endPath', ()) + ] + ), + ( + [ + ('qCurveTo', ((0, 0), (1, 1), (2, 2), None)), + ('closePath', ()) + ], + [ + ('qCurveTo', ((0, 0), (2, 2), (1, 1), None)), + ('closePath', ()) + ] + ), + ( + [ + ('qCurveTo', ((0, 0), (1, 1), (2, 2), None)), + ('endPath', ()) + ], + [ + ('qCurveTo', ((0, 0), (2, 2), (1, 1), None)), + ('closePath', ()) # this is always "closed" + ] + ), + # Test case from: + # https://github.com/googlei18n/cu2qu/issues/51#issue-179370514 + ( + [ + ('moveTo', ((848, 348),)), + ('lineTo', ((848, 348),)), # duplicate lineTo point after moveTo + ('qCurveTo', ((848, 526), (649, 704), (449, 704))), + ('qCurveTo', ((449, 704), (248, 704), (50, 526), (50, 348))), + ('lineTo', ((50, 348),)), + ('qCurveTo', ((50, 348), (50, 171), (248, -3), (449, -3))), + ('qCurveTo', ((449, -3), (649, -3), (848, 171), (848, 348))), + ('closePath', ()) + ], + [ + ('moveTo', ((848, 348),)), + ('qCurveTo', ((848, 171), (649, -3), (449, -3), (449, -3))), + ('qCurveTo', ((248, -3), (50, 171), (50, 348), (50, 348))), + ('lineTo', ((50, 348),)), + ('qCurveTo', ((50, 526), (248, 704), (449, 704), (449, 704))), + ('qCurveTo', ((649, 704), (848, 526), (848, 348))), + ('lineTo', ((848, 348),)), # the duplicate point is kept + ('closePath', ()) + ] + ) +] + + +@pytest.mark.parametrize("contour, expected", TEST_DATA) +def test_reverse_pen(contour, expected): + recpen = RecordingPen() + revpen = ReverseContourPen(recpen) + for operator, operands in contour: + getattr(revpen, operator)(*operands) + assert recpen.value == expected + + +@pytest.mark.parametrize("contour, expected", TEST_DATA) +def test_reverse_point_pen(contour, expected): + try: + from ufoLib.pointPen import ( + ReverseContourPointPen, PointToSegmentPen, SegmentToPointPen) + except ImportError: + pytest.skip("ufoLib not installed") + + recpen = RecordingPen() + pt2seg = PointToSegmentPen(recpen, outputImpliedClosingLine=True) + revpen = ReverseContourPointPen(pt2seg) + seg2pt = SegmentToPointPen(revpen) + for operator, operands in contour: + getattr(seg2pt, operator)(*operands) + + # for closed contours that have a lineTo following the moveTo, + # and whose points don't overlap, our current implementation diverges + # from the ReverseContourPointPen as wrapped by ufoLib's pen converters. + # In the latter case, an extra lineTo is added because of + # outputImpliedClosingLine=True. This is redundant but not incorrect, + # as the number of points is the same in both. + if (contour and contour[-1][0] == "closePath" and + contour[1][0] == "lineTo" and contour[1][1] != contour[0][1]): + expected = expected[:-1] + [("lineTo", contour[0][1])] + expected[-1:] + + assert recpen.value == expected diff --git a/Tests/pens/t2CharStringPen_test.py b/Tests/pens/t2CharStringPen_test.py new file mode 100644 index 0000000..c09d810 --- /dev/null +++ b/Tests/pens/t2CharStringPen_test.py @@ -0,0 +1,184 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.pens.t2CharStringPen import T2CharStringPen +import unittest + + +class T2CharStringPenTest(unittest.TestCase): + + def __init__(self, methodName): + unittest.TestCase.__init__(self, methodName) + # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, + # and fires deprecation warnings if a program uses the old name. + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + + def assertAlmostEqualProgram(self, expected, actual): + self.assertEqual(len(expected), len(actual)) + for i1, i2 in zip(expected, actual): + if isinstance(i1, basestring): + self.assertIsInstance(i2, basestring) + self.assertEqual(i1, i2) + else: + self.assertAlmostEqual(i1, i2) + + def test_draw_h_v_lines(self): + pen = T2CharStringPen(100, {}) + pen.moveTo((0, 0)) + pen.lineTo((10, 0)) + pen.lineTo((10, 10)) + pen.lineTo((0, 10)) + pen.closePath() # no-op + pen.moveTo((10, 10)) + pen.lineTo((10, 20)) + pen.lineTo((0, 20)) + pen.lineTo((0, 10)) + pen.closePath() + charstring = pen.getCharString(None, None) + + self.assertEqual( + [100, + 0, 'hmoveto', + 10, 10, -10, 'hlineto', + 10, 'hmoveto', + 10, -10, -10, 'vlineto', + 'endchar'], + charstring.program) + + def test_draw_lines(self): + pen = T2CharStringPen(100, {}) + pen.moveTo((5, 5)) + pen.lineTo((25, 15)) + pen.lineTo((35, 35)) + pen.lineTo((15, 25)) + pen.closePath() # no-op + charstring = pen.getCharString(None, None) + + self.assertEqual( + [100, + 5, 5, 'rmoveto', + 20, 10, 10, 20, -20, -10, 'rlineto', + 'endchar'], + charstring.program) + + def test_draw_h_v_curves(self): + pen = T2CharStringPen(100, {}) + pen.moveTo((0, 0)) + pen.curveTo((10, 0), (20, 10), (20, 20)) + pen.curveTo((20, 30), (10, 40), (0, 40)) + pen.endPath() # no-op + charstring = pen.getCharString(None, None) + + self.assertEqual( + [100, + 0, 'hmoveto', + 10, 10, 10, 10, 10, -10, 10, -10, 'hvcurveto', + 'endchar'], + charstring.program) + + def test_draw_curves(self): + pen = T2CharStringPen(100, {}) + pen.moveTo((95, 25)) + pen.curveTo((115, 44), (115, 76), (95, 95)) + pen.curveTo((76, 114), (44, 115), (25, 95)) + pen.endPath() # no-op + charstring = pen.getCharString(None, None) + + self.assertEqual( + [100, + 95, 25, 'rmoveto', + 20, 19, 0, 32, -20, 19, -19, 19, -32, 1, -19, -20, 'rrcurveto', + 'endchar'], + charstring.program) + + def test_draw_more_curves(self): + pen = T2CharStringPen(100, {}) + pen.moveTo((10, 10)) + pen.curveTo((20, 10), (50, 10), (60, 10)) + pen.curveTo((60, 20), (60, 50), (60, 60)) + pen.curveTo((50, 50), (40, 60), (30, 60)) + pen.curveTo((40, 50), (30, 40), (30, 30)) + pen.curveTo((30, 25), (25, 19), (20, 20)) + pen.curveTo((15, 20), (9, 25), (10, 30)) + pen.curveTo((7, 25), (6, 15), (10, 10)) + pen.endPath() # no-op + charstring = pen.getCharString(None, None) + + self.assertEqual( + [100, + 10, 10, 'rmoveto', + 10, 30, 0, 10, 'hhcurveto', + 10, 0, 30, 10, 'vvcurveto', + -10, -10, -10, 10, -10, 'hhcurveto', + 10, -10, -10, -10, -10, 'vvcurveto', + -5, -5, -6, -5, 1, 'vhcurveto', + -5, -6, 5, 5, 1, 'hvcurveto', + -3, -5, -1, -10, 4, -5, 'rrcurveto', + 'endchar'], + charstring.program) + + def test_default_width(self): + pen = T2CharStringPen(None, {}) + charstring = pen.getCharString(None, None) + self.assertEqual(['endchar'], charstring.program) + + def test_no_round(self): + pen = T2CharStringPen(100.1, {}, roundTolerance=0.0) + pen.moveTo((0, 0)) + pen.curveTo((10.1, 0.1), (19.9, 9.9), (20.49, 20.49)) + pen.curveTo((20.49, 30.49), (9.9, 39.9), (0.1, 40.1)) + pen.closePath() + charstring = pen.getCharString(None, None) + + self.assertAlmostEqualProgram( + [100, # we always round the advance width + 0, 'hmoveto', + 10.1, 0.1, 9.8, 9.8, 0.59, 10.59, 'rrcurveto', + 10, -10.59, 9.41, -9.8, 0.2, 'vhcurveto', + 'endchar'], + charstring.program) + + def test_round_all(self): + pen = T2CharStringPen(100.1, {}, roundTolerance=0.5) + pen.moveTo((0, 0)) + pen.curveTo((10.1, 0.1), (19.9, 9.9), (20.49, 20.49)) + pen.curveTo((20.49, 30.5), (9.9, 39.9), (0.1, 40.1)) + pen.closePath() + charstring = pen.getCharString(None, None) + + self.assertEqual( + [100, + 0, 'hmoveto', + 10, 10, 10, 10, 11, -10, 9, -10, 'hvcurveto', + 'endchar'], + charstring.program) + + def test_round_some(self): + pen = T2CharStringPen(100, {}, roundTolerance=0.2) + pen.moveTo((0, 0)) + # the following two are rounded as within the tolerance + pen.lineTo((10.1, 0.1)) + pen.lineTo((19.9, 9.9)) + # this one is not rounded as it exceeds the tolerance + pen.lineTo((20.49, 20.49)) + pen.closePath() + charstring = pen.getCharString(None, None) + + self.assertAlmostEqualProgram( + [100, + 0, 'hmoveto', + 10, 'hlineto', + 10, 10, 0.49, 10.49, 'rlineto', + 'endchar'], + charstring.program) + + def test_invalid_tolerance(self): + self.assertRaisesRegex( + ValueError, + "Rounding tolerance must be positive", + T2CharStringPen, None, {}, roundTolerance=-0.1) + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/pens/ttGlyphPen_test.py b/Tests/pens/ttGlyphPen_test.py new file mode 100644 index 0000000..ede240a --- /dev/null +++ b/Tests/pens/ttGlyphPen_test.py @@ -0,0 +1,257 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * + +import os +import unittest +import struct + +from fontTools import ttLib +from fontTools.misc.testTools import TestCase +from fontTools.pens.ttGlyphPen import TTGlyphPen, MAX_F2DOT14 + + +class TTGlyphPenTest(TestCase): + + def runEndToEnd(self, filename): + font = ttLib.TTFont() + ttx_path = os.path.join( + os.path.abspath(os.path.dirname(os.path.realpath(__file__))), + '..', 'ttLib', 'data', filename) + font.importXML(ttx_path) + + glyphSet = font.getGlyphSet() + glyfTable = font['glyf'] + pen = TTGlyphPen(font.getGlyphSet()) + + for name in font.getGlyphOrder(): + oldGlyph = glyphSet[name] + oldGlyph.draw(pen) + oldGlyph = oldGlyph._glyph + newGlyph = pen.glyph() + + if hasattr(oldGlyph, 'program'): + newGlyph.program = oldGlyph.program + + self.assertEqual( + oldGlyph.compile(glyfTable), newGlyph.compile(glyfTable)) + + def test_e2e_linesAndSimpleComponents(self): + self.runEndToEnd('TestTTF-Regular.ttx') + + def test_e2e_curvesAndComponentTransforms(self): + self.runEndToEnd('TestTTFComplex-Regular.ttx') + + def test_moveTo_errorWithinContour(self): + pen = TTGlyphPen(None) + pen.moveTo((0, 0)) + with self.assertRaises(AssertionError): + pen.moveTo((1, 0)) + + def test_closePath_ignoresAnchors(self): + pen = TTGlyphPen(None) + pen.moveTo((0, 0)) + pen.closePath() + self.assertFalse(pen.points) + self.assertFalse(pen.types) + self.assertFalse(pen.endPts) + + def test_endPath_sameAsClosePath(self): + pen = TTGlyphPen(None) + + pen.moveTo((0, 0)) + pen.lineTo((0, 1)) + pen.lineTo((1, 0)) + pen.closePath() + closePathGlyph = pen.glyph() + + pen.moveTo((0, 0)) + pen.lineTo((0, 1)) + pen.lineTo((1, 0)) + pen.endPath() + endPathGlyph = pen.glyph() + + self.assertEqual(closePathGlyph, endPathGlyph) + + def test_glyph_errorOnUnendedContour(self): + pen = TTGlyphPen(None) + pen.moveTo((0, 0)) + with self.assertRaises(AssertionError): + pen.glyph() + + def test_glyph_decomposes(self): + componentName = 'a' + glyphSet = {} + pen = TTGlyphPen(glyphSet) + + pen.moveTo((0, 0)) + pen.lineTo((0, 1)) + pen.lineTo((1, 0)) + pen.closePath() + glyphSet[componentName] = _TestGlyph(pen.glyph()) + + pen.moveTo((0, 0)) + pen.lineTo((0, 1)) + pen.lineTo((1, 0)) + pen.closePath() + pen.addComponent(componentName, (1, 0, 0, 1, 2, 0)) + pen.addComponent("missing", (1, 0, 0, 1, 0, 0)) # skipped + compositeGlyph = pen.glyph() + + pen.moveTo((0, 0)) + pen.lineTo((0, 1)) + pen.lineTo((1, 0)) + pen.closePath() + pen.moveTo((2, 0)) + pen.lineTo((2, 1)) + pen.lineTo((3, 0)) + pen.closePath() + plainGlyph = pen.glyph() + + self.assertEqual(plainGlyph, compositeGlyph) + + def test_remove_extra_move_points(self): + pen = TTGlyphPen(None) + pen.moveTo((0, 0)) + pen.lineTo((100, 0)) + pen.qCurveTo((100, 50), (50, 100), (0, 0)) + pen.closePath() + self.assertEqual(len(pen.points), 4) + self.assertEqual(pen.points[0], (0, 0)) + + def test_keep_move_point(self): + pen = TTGlyphPen(None) + pen.moveTo((0, 0)) + pen.lineTo((100, 0)) + pen.qCurveTo((100, 50), (50, 100), (30, 30)) + # when last and move pts are different, closePath() implies a lineTo + pen.closePath() + self.assertEqual(len(pen.points), 5) + self.assertEqual(pen.points[0], (0, 0)) + + def test_keep_duplicate_end_point(self): + pen = TTGlyphPen(None) + pen.moveTo((0, 0)) + pen.lineTo((100, 0)) + pen.qCurveTo((100, 50), (50, 100), (0, 0)) + pen.lineTo((0, 0)) # the duplicate point is not removed + pen.closePath() + self.assertEqual(len(pen.points), 5) + self.assertEqual(pen.points[0], (0, 0)) + + def test_within_range_component_transform(self): + componentName = 'a' + glyphSet = {} + pen = TTGlyphPen(glyphSet) + + pen.moveTo((0, 0)) + pen.lineTo((0, 1)) + pen.lineTo((1, 0)) + pen.closePath() + glyphSet[componentName] = _TestGlyph(pen.glyph()) + + pen.addComponent(componentName, (1.5, 0, 0, 1, 0, 0)) + pen.addComponent(componentName, (1, 0, 0, -1.5, 0, 0)) + compositeGlyph = pen.glyph() + + pen.addComponent(componentName, (1.5, 0, 0, 1, 0, 0)) + pen.addComponent(componentName, (1, 0, 0, -1.5, 0, 0)) + expectedGlyph = pen.glyph() + + self.assertEqual(expectedGlyph, compositeGlyph) + + def test_clamp_to_almost_2_component_transform(self): + componentName = 'a' + glyphSet = {} + pen = TTGlyphPen(glyphSet) + + pen.moveTo((0, 0)) + pen.lineTo((0, 1)) + pen.lineTo((1, 0)) + pen.closePath() + glyphSet[componentName] = _TestGlyph(pen.glyph()) + + pen.addComponent(componentName, (1.99999, 0, 0, 1, 0, 0)) + pen.addComponent(componentName, (1, 2, 0, 1, 0, 0)) + pen.addComponent(componentName, (1, 0, 2, 1, 0, 0)) + pen.addComponent(componentName, (1, 0, 0, 2, 0, 0)) + pen.addComponent(componentName, (-2, 0, 0, -2, 0, 0)) + compositeGlyph = pen.glyph() + + almost2 = MAX_F2DOT14 # 0b1.11111111111111 + pen.addComponent(componentName, (almost2, 0, 0, 1, 0, 0)) + pen.addComponent(componentName, (1, almost2, 0, 1, 0, 0)) + pen.addComponent(componentName, (1, 0, almost2, 1, 0, 0)) + pen.addComponent(componentName, (1, 0, 0, almost2, 0, 0)) + pen.addComponent(componentName, (-2, 0, 0, -2, 0, 0)) + expectedGlyph = pen.glyph() + + self.assertEqual(expectedGlyph, compositeGlyph) + + def test_out_of_range_transform_decomposed(self): + componentName = 'a' + glyphSet = {} + pen = TTGlyphPen(glyphSet) + + pen.moveTo((0, 0)) + pen.lineTo((0, 1)) + pen.lineTo((1, 0)) + pen.closePath() + glyphSet[componentName] = _TestGlyph(pen.glyph()) + + pen.addComponent(componentName, (3, 0, 0, 2, 0, 0)) + pen.addComponent(componentName, (1, 0, 0, 1, -1, 2)) + pen.addComponent(componentName, (2, 0, 0, -3, 0, 0)) + compositeGlyph = pen.glyph() + + pen.moveTo((0, 0)) + pen.lineTo((0, 2)) + pen.lineTo((3, 0)) + pen.closePath() + pen.moveTo((-1, 2)) + pen.lineTo((-1, 3)) + pen.lineTo((0, 2)) + pen.closePath() + pen.moveTo((0, 0)) + pen.lineTo((0, -3)) + pen.lineTo((2, 0)) + pen.closePath() + expectedGlyph = pen.glyph() + + self.assertEqual(expectedGlyph, compositeGlyph) + + def test_no_handle_overflowing_transform(self): + componentName = 'a' + glyphSet = {} + pen = TTGlyphPen(glyphSet, handleOverflowingTransforms=False) + + pen.moveTo((0, 0)) + pen.lineTo((0, 1)) + pen.lineTo((1, 0)) + pen.closePath() + baseGlyph = pen.glyph() + glyphSet[componentName] = _TestGlyph(baseGlyph) + + pen.addComponent(componentName, (3, 0, 0, 1, 0, 0)) + compositeGlyph = pen.glyph() + + self.assertEqual(compositeGlyph.components[0].transform, + ((3, 0), (0, 1))) + + with self.assertRaises(struct.error): + compositeGlyph.compile({'a': baseGlyph}) + + +class _TestGlyph(object): + def __init__(self, glyph): + self.coordinates = glyph.coordinates + + def draw(self, pen): + pen.moveTo(self.coordinates[0]) + for point in self.coordinates[1:]: + pen.lineTo(point) + pen.closePath() + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/subset/data/Lobster.subset.ttx b/Tests/subset/data/Lobster.subset.ttx new file mode 100644 index 0000000..c35e570 --- /dev/null +++ b/Tests/subset/data/Lobster.subset.ttx @@ -0,0 +1,661 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright (c) 2010 by Pablo Impallari. www.impallari.com. All rights reserved. + + + Lobster 1.4 + + + Regular + + + PabloImpallari.www.impallari.com: Lobster 1.4: 2010 + + + Lobster1.4 + + + Version 1.4 + + + Lobster1.4 + + + Lobster 1.4 is a trademark of Pablo Impallari. www.impallari.com. + + + Pablo Impallari. www.impallari.com + + + Pablo Impallari + + + Copyright (c) 2010 by Pablo Impallari. All rights reserved. + + + www.impallari.com + + + www.impallari.com + + + Copyright (c) 2010, Pablo Impallari (www.impallari.com|impallari@gmail.com), +with Reserved Font Name Lobster. +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is available with a FAQ at: http://scripts.sil.org/OFL + + + http://scripts.sil.org/OFL + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 397 748 rmoveto + 1 -13 -13 1 -14 hhcurveto + -106 callsubr + 53 75 87 36 vhcurveto + -145 -679 rlineto + return + + + -167 -184 -127 -133 -72 38 -25 69 hvcurveto + -1 9 -13 8 51 vvcurveto + 107 return + + + 119 hintmask 01111100 + 230 636 rmoveto + -136 -636 rlineto + 144 hlineto + return + + + -67 41 -25 66 vhcurveto + -1 9 -13 8 51 vvcurveto + return + + + hintmask 00110101 + return + + + hintmask 10111010 + return + + + hintmask 11100110 + return + + + + + + -63 endchar + + + 220 -93 -21 114 -20 297 181 -59 59 292 -20 hstemhm + 9 118 -43 120 hintmask 11101100 + 535 hmoveto + 157 736 rlineto + 10 -24 -32 4 -23 hhcurveto + -117 -130 -135 -160 -101 hvcurveto + 2 -21 -17 1 -14 hhcurveto + -118 -86 -55 -68 -39 28 -19 34 31 25 15 24 14 -8 17 -5 hvcurveto + hintmask 11011010 + 13 34 42 14 62 4 rrcurveto + -87 -153 -60 -164 -90 vvcurveto + -104 80 -2 54 vhcurveto + -6 9 -8 15 32 vvcurveto + 104 55 190 75 163 vhcurveto + 44 -4 39 -9 51 -23 -77 -363 rcurveline + 86 407 rmoveto + -39 16 -43 11 -40 8 56 112 64 93 60 32 rrcurveto + endchar + + + 142 -92 -21 113 -20 386 52 333 -20 hstem + 8 120 vstem + 459 hmoveto + 157 736 rlineto + 12 -30 -26 3 -24 hhcurveto + -238 -290 -563 -189 -106 65 -2 69 -4 hvcurveto + -1 9 -13 -4 51 vvcurveto + 97 42 172 64 154 vhcurveto + 158 hlineto + -77 -366 rlineto + -59 418 rmoveto + 58 126 72 106 73 32 -56 -264 rcurveline + endchar + + + 187 -17 96 -79 -20 406 48 270 46 hstemhm + 6 93 362 139 -119 101 -101 -105 callsubr + 82 383 rlineto + 2 18 20 1 8 hhcurveto + 73 22 -57 -70 hvcurveto + hintmask 10111001 + -76 -26 -104 -73 -23 -19 10 26 -25 vhcurveto + -9 -23 -4 -19 -16 vvcurveto + -61 56 -13 43 167 52 192 96 75 -33 69 -85 17 vhcurveto + -102 callsubr + 65 37 35 63 59 vvcurveto + 82 -66 77 -147 -189 -174 -127 -138 -104 callsubr + 165 133 78 117 95 37 -51 -57 -75 -64 -87 -80 vhcurveto + -6 hlineto + 47 222 rlineto + endchar + + + 185 -28 92 -64 -20 413 41 270 46 hstemhm + 6 93 350 149 -119 105 -105 -105 callsubr + 6 30 rlineto + hintmask 10111001 + -41 39 41 -17 39 hhcurveto + 125 110 175 136 72 -32 62 -82 15 hvcurveto + hintmask 10111010 + 64 38 36 61 58 vvcurveto + 83 -74 78 -144 -183 -177 -126 -139 -104 callsubr + 152 116 91 138 101 25 -49 -53 -81 -59 -87 -83 vhcurveto + -6 hlineto + 47 222 rlineto + -59 -592 rmoveto + -20 -21 8 21 -20 hvcurveto + 62 290 rlineto + 2 18 20 1 7 hhcurveto + hintmask 10111100 + 63 21 -49 -57 -96 -58 -120 -72 hvcurveto + endchar + + + -73 21 -21 750 -20 hstem + 6 93 vstem + -107 callsubr + 144 hlineto + endchar + + + 215 -207 50 157 -20 770 -20 hstemhm + 6 93 13 84 -84 205 hintmask 11111000 + -107 callsubr + 34 hlineto + -11 -20 -5 -23 -27 vvcurveto + -79 48 -58 113 155 66 109 138 29 vhcurveto + 150 710 -150 -33 -164 -751 rlineto + -100 -22 -30 -23 -40 hhcurveto + -44 -27 29 39 40 29 33 36 16 17 -7 -16 16 hvcurveto + hintmask 11110100 + 4 11 3 11 11 vvcurveto + 34 -26 24 -41 6 vhcurveto + endchar + + + 88 -207 50 144 81 682 -20 hstemhm + 17 84 -84 220 -50 93 hintmask 11110100 + 538 750 rmoveto + -106 callsubr + 54 76 87 36 vhcurveto + -157 -714 rlineto + -103 -23 -27 -20 -45 hhcurveto + -29 -39 18 52 37 24 37 46 20 15 -5 -21 25 hvcurveto + hintmask 11101000 + 4 15 2 14 11 vvcurveto + 64 -58 3 -40 -79 -43 -66 -68 -83 53 -58 95 164 67 94 153 32 vhcurveto + 150 710 rlineto + endchar + + + -131 21 -21 624 46 78 -20 hstem + 324 748 rmoveto + -72 -121 -78 -6 -55 hhcurveto + -12 -46 rlineto + 95 hlineto + -132 -624 rlineto + 144 hlineto + endchar + + + 66 -5 65 197 51 204 237 -54 54 hstemhm + 6 111 -12 110 117 155 -117 117 hintmask 11101001 + 205 257 rmoveto + 38 -8 -33 13 -37 hhcurveto + -80 -41 -60 -83 -154 141 -16 58 171 111 136 121 71 -38 65 -88 29 hvcurveto + 92 46 45 74 66 vvcurveto + 78 -63 68 -123 vhcurveto + -101 callsubr + -116 -91 -61 -91 -54 32 -31 40 24 27 11 23 25 hvcurveto + -28 8 -10 36 27 vvcurveto + hintmask 11011001 + 47 31 31 48 51 25 -36 -46 -70 -58 -94 -113 -31 vhcurveto + hintmask 11101010 + 93 -33 40 -80 -76 vvcurveto + -87 -53 -82 -86 -37 -39 13 76 40 10 62 78 6 vhcurveto + endchar + + + 44 -11 125 -89 89 -89 107 380 237 -54 54 hstemhm + 66 110 142 119 -119 144 -103 callsubr + 111 132 rmoveto + -5 hlineto + 83 135 273 98 223 vvcurveto + 97 -53 64 -137 -151 -55 -79 -68 -58 31 -32 41 24 26 11 23 26 vhcurveto + -28 8 -10 37 23 vvcurveto + hintmask 01001110 + 50 14 31 67 29 32 -33 -49 vhcurveto + -266 -329 -98 -219 vvcurveto + -11 0 -11 2 -11 vhcurveto + 7 20 36 21 23 hhcurveto + hintmask 10010110 + 102 37 -36 109 hhcurveto + 99 20 52 98 14 0 14 -1 16 hvcurveto + -44 -47 -17 -25 -70 hhcurveto + hintmask 00110110 + -75 -57 18 -59 hhcurveto + endchar + + + 98 -9 84 623 52 hstem + 30 158 236 131 vstem + 377 750 rmoveto + -215 -132 -223 -273 -166 35 -97 172 205 113 299 199 168 -53 93 -125 hvcurveto + -189 -425 rmoveto + 225 17 105 148 60 hhcurveto + 47 7 -63 -82 -232 -68 -246 -114 -48 -11 77 74 37 3 35 2 27 hvcurveto + endchar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/NotdefWidthCID-Regular.ttx b/Tests/subset/data/NotdefWidthCID-Regular.ttx new file mode 100644 index 0000000..ddb1b0f --- /dev/null +++ b/Tests/subset/data/NotdefWidthCID-Regular.ttx @@ -0,0 +1,267 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Notdef Width CID + + + Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -120 50 900 50 hstem + 100 50 700 50 vstem + 100 -120 rmoveto + 800 1000 -800 hlineto + 400 -459 rmoveto + -318 409 rlineto + 636 hlineto + -286 -450 rmoveto + 318 409 rlineto + -818 vlineto + -668 -41 rmoveto + 318 409 318 -409 rlineto + -668 859 rmoveto + 318 -409 -318 -409 rlineto + endchar + + + -407 endchar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/TestANKR.ttx b/Tests/subset/data/TestANKR.ttx new file mode 100644 index 0000000..8bb37c7 --- /dev/null +++ b/Tests/subset/data/TestANKR.ttx @@ -0,0 +1,325 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TestANKR + + + Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/TestBSLN-0.ttx b/Tests/subset/data/TestBSLN-0.ttx new file mode 100644 index 0000000..9a446e1 --- /dev/null +++ b/Tests/subset/data/TestBSLN-0.ttx @@ -0,0 +1,342 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TestBSLN-0 + + + Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/TestBSLN-1.ttx b/Tests/subset/data/TestBSLN-1.ttx new file mode 100644 index 0000000..3ad83a2 --- /dev/null +++ b/Tests/subset/data/TestBSLN-1.ttx @@ -0,0 +1,348 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TestBSLN-1 + + + Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/TestBSLN-2.ttx b/Tests/subset/data/TestBSLN-2.ttx new file mode 100644 index 0000000..7a00500 --- /dev/null +++ b/Tests/subset/data/TestBSLN-2.ttx @@ -0,0 +1,342 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TestBSLN-2 + + + Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/TestBSLN-3.ttx b/Tests/subset/data/TestBSLN-3.ttx new file mode 100644 index 0000000..4bba6b4 --- /dev/null +++ b/Tests/subset/data/TestBSLN-3.ttx @@ -0,0 +1,363 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TestBSLN-3 + + + Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/TestCID-Regular.ttx b/Tests/subset/data/TestCID-Regular.ttx new file mode 100644 index 0000000..1ecfd39 --- /dev/null +++ b/Tests/subset/data/TestCID-Regular.ttx @@ -0,0 +1,389 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test CID + + + Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 129 216 -105 callsubr + -82 hlineto + 2 -98 rlineto + return + + + -106 callgsubr + 67 return + + + rmoveto + 56 hlineto + 11 433 2 98 rlineto + return + + + -33 -28 -26 -40 vhcurveto + hintmask 01100000 + -38 28 -26 33 vhcurveto + endchar + + + -13 130 -109 -21 return + + + 493 277 hstem + 91 -105 callgsubr + return + + + + + + + + -120 50 900 50 hstem + 100 50 700 50 vstem + 100 -120 rmoveto + 800 1000 -800 hlineto + 400 -459 rmoveto + -318 409 rlineto + 636 hlineto + -286 -450 rmoveto + 318 409 rlineto + -818 vlineto + -668 -41 rmoveto + 318 409 318 -409 rlineto + -668 859 rmoveto + 318 -409 -318 -409 rlineto + endchar + + + -407 endchar + + + -316 -103 callsubr + -106 callsubr + hintmask 01010000 + -107 callsubr + hintmask 01100000 + 39 -662 rmoveto + 33 29 26 38 hvcurveto + hintmask 10100000 + 40 -29 26 -33 -104 callsubr + + + -173 -102 callsubr + -104 callgsubr + 86 vstem + 108 493 rmoveto + 52 hlineto + 15 180 3 97 rlineto + -88 hlineto + 2 -97 rlineto + 203 -103 callgsubr + 51 hlineto + 17 180 2 97 rlineto + -88 hlineto + 2 -97 rlineto + endchar + + + + + + + + 123 -95.5 return + + + hstemhm + 96 -107 callgsubr + return + + + 85.5 return + + + 101.5 return + + + -180 rmoveto + return + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/TestCLR-Regular.ttx b/Tests/subset/data/TestCLR-Regular.ttx new file mode 100644 index 0000000..cb63553 --- /dev/null +++ b/Tests/subset/data/TestCLR-Regular.ttx @@ -0,0 +1,763 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PUSH[ ] /* 2 values pushed */ + 1 0 + MDAP[1] /* MoveDirectAbsPt */ + ALIGNRP[ ] /* AlignRelativePt */ + PUSH[ ] /* 3 values pushed */ + 7 4 0 + MIRP[01101] /* MoveIndirectRelPt */ + SHP[0] /* ShiftPointByLastPoint */ + PUSH[ ] /* 2 values pushed */ + 6 5 + MDRP[11100] /* MoveDirectRelPt */ + ALIGNRP[ ] /* AlignRelativePt */ + PUSH[ ] /* 3 values pushed */ + 3 2 0 + MIRP[01101] /* MoveIndirectRelPt */ + SHP[0] /* ShiftPointByLastPoint */ + SVTCA[0] /* SetFPVectorToAxis */ + PUSH[ ] /* 2 values pushed */ + 3 0 + MDAP[1] /* MoveDirectAbsPt */ + ALIGNRP[ ] /* AlignRelativePt */ + PUSH[ ] /* 3 values pushed */ + 5 4 0 + MIRP[01101] /* MoveIndirectRelPt */ + SHP[0] /* ShiftPointByLastPoint */ + PUSH[ ] /* 3 values pushed */ + 7 6 1 + MIRP[11100] /* MoveIndirectRelPt */ + ALIGNRP[ ] /* AlignRelativePt */ + PUSH[ ] /* 3 values pushed */ + 1 2 0 + MIRP[01101] /* MoveIndirectRelPt */ + SHP[0] /* ShiftPointByLastPoint */ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Created by Khaled Hosny with Sorts Mill Tools 2.1.0_alpha1 <http://bitbucket.org/sortsmill/sortsmill-tools> + + + TestCLR + + + Regular + + + FontForge : TestCLR : 1-12-2015 + + + TestCLR + + + Version 001.000 + + + TestCLR-Regular + + + Created by Khaled Hosny with Sorts Mill Tools 2.1.0_alpha1 <http://bitbucket.org/sortsmill/sortsmill-tools> + + + TestCLR + + + Regular + + + FontForge : TestCLR : 1-12-2015 + + + TestCLR + + + Version 001.000 + + + TestCLR-Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/TestGVAR.ttx b/Tests/subset/data/TestGVAR.ttx new file mode 100644 index 0000000..b14466d --- /dev/null +++ b/Tests/subset/data/TestGVAR.ttx @@ -0,0 +1,655 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TestGVAR + + + Regular + + + 1.000;UKWN;TestGVAR-Regular + + + TestGVAR + + + Version 1.000 + + + TestGVAR-Regular + + + Sascha Brawer + + + Weight + + + Thin + + + Light + + + Regular + + + Bold + + + Black + + + TestGVAR + + + Regular + + + 1.000;UKWN;TestGVAR-Regular + + + TestGVAR-Regular + + + Version 1.000 + + + TestGVAR-Regular + + + Sascha Brawer + + + Weight + + + Thin + + + Light + + + Regular + + + Bold + + + Black + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + wght + 100.0 + 400.0 + 900.0 + 257 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/TestLCAR-0.ttx b/Tests/subset/data/TestLCAR-0.ttx new file mode 100644 index 0000000..b9d6fc0 --- /dev/null +++ b/Tests/subset/data/TestLCAR-0.ttx @@ -0,0 +1,320 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TestLCAR + + + Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/TestLCAR-1.ttx b/Tests/subset/data/TestLCAR-1.ttx new file mode 100644 index 0000000..6e8d85b --- /dev/null +++ b/Tests/subset/data/TestLCAR-1.ttx @@ -0,0 +1,320 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TestLCAR + + + Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/TestMATH-Regular.ttx b/Tests/subset/data/TestMATH-Regular.ttx new file mode 100644 index 0000000..63b8430 --- /dev/null +++ b/Tests/subset/data/TestMATH-Regular.ttx @@ -0,0 +1,7590 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + XITS Math + + + Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 14 0 45 381 rlineto + 1 11 1 15 0 12 rrcurveto + 49 -19 32 -34 -38 -15 -33 -43 vhcurveto + 0 -11 1 -12 2 -20 rrcurveto + return + + + 30 -24 25 -29 -30 -23 -24 -30 -30 22 -22 30 31 23 22 29 vhcurveto + return + + + -106 -65 -101 -69 -32 -24 29 39 vhcurveto + 0 68 34 88 44 47 12 14 21 9 16 0 rrcurveto + 38 25 -35 -52 hvcurveto + return + + + -44 hlineto + -92 -89 -13 -8 -62 0 -37 0 -24 10 -35 29 -27 22 -12 6 -21 0 rrcurveto + -102 -91 -100 -113 hvcurveto + return + + + -78 51 -59 67 vhcurveto + 33 0 26 11 27 26 48 45 32 75 0 70 0 16 -1 11 -3 17 32 -11 18 -3 26 0 53 0 31 14 37 43 rrcurveto + -394 -658 47 0 rlineto + return + + + -115 -61 -101 -71 -34 -23 28 42 vhcurveto + 0 55 17 59 31 49 20 33 11 10 41 20 17 -18 13 -9 14 -7 21 -10 4 -7 0 -29 rrcurveto + return + + + 49 28 19 6 44 7 39 6 13 11 0 26 0 23 -15 15 -22 0 -22 0 -31 -41 -24 -24 -19 -18 -11 -8 -28 -16 rrcurveto + 24 vlineto + 0 24 5 27 12 32 7 20 4 15 0 12 rrcurveto + 20 -17 16 -20 -20 -16 -15 -19 vhcurveto + 0 -13 4 -18 7 -21 10 -33 5 -31 0 -27 rrcurveto + -13 vlineto + -38 21 -19 16 -32 36 -22 24 -11 8 -16 0 -20 0 -13 -16 0 -21 0 -25 18 -11 49 -10 34 -7 25 -7 38 -23 -41 -25 -16 -6 -46 -9 rrcurveto + -45 -8 -18 -11 0 -26 0 -21 14 -15 23 0 16 0 10 6 17 19 29 33 2 2 10 10 10 9 7 5 35 22 rrcurveto + -9 vlineto + 0 -43 -4 -24 -12 -32 -7 -19 -5 -16 0 -12 rrcurveto + -19 17 -17 19 20 20 19 20 vhcurveto + 0 7 -4 12 -5 14 -13 35 -7 36 0 39 rrcurveto + 0 9 8 -5 rlineto + 20 -13 22 -12 27 -33 25 -31 16 -13 19 0 21 0 15 17 0 22 0 26 -16 10 -44 7 -37 6 -32 10 -27 18 rrcurveto + return + + + 66 -261 261 -66 -261 -261 -66 261 -261 66 261 vlineto + return + + + 30 -26 27 -29 -31 -25 -25 -31 -31 24 -24 31 30 26 25 29 vhcurveto + return + + + 30 -25 26 -30 -32 -24 -24 -32 -30 26 -25 30 30 25 25 30 vhcurveto + return + + + 30 -25 26 -30 -32 -24 -24 -32 -30 26 -25 30 vhcurveto + 30 25 25 30 hvcurveto + return + + + 70 33 42 58 0 63 rrcurveto + 54 -35 35 -45 vhcurveto + return + + + 9 0 13 4 6 0 8 0 6 -7 0 -7 0 -33 -28 -37 -55 -36 rrcurveto + return + + + 0 63 -432 215 432 215 0 65 -565 -275 0 -10 rlineto + return + + + 66 -589 -66 vlineto + return + + + 0 10 -565 275 0 -65 432 -215 -432 -215 0 -63 rlineto + return + + + 11 68 18 46 36 43 86 102 19 34 0 61 rrcurveto + 97 -85 61 -97 -96 -68 -60 -83 -41 20 -25 28 25 16 18 25 vhcurveto + 0 33 -38 4 0 37 rrcurveto + 32 46 30 48 61 48 -49 -69 vhcurveto + 0 -60 -25 -63 -26 -64 -35 -87 -8 -50 -1 -40 rrcurveto + return + + + 28 -24 25 -28 -31 -23 -23 -30 -28 20 -25 34 30 22 24 29 vhcurveto + return + + + 19 vlineto + -52 6 -14 21 -28 65 rrcurveto + -246 563 -20 0 -206 -488 rlineto + -59 -140 -9 -21 -58 -6 rrcurveto + -19 199 19 vlineto + -48 -22 10 31 hvcurveto + 0 12 4 17 5 13 rrcurveto + 46 114 262 0 41 -94 rlineto + 12 -28 7 -27 0 -15 0 -9 -6 -11 -8 -4 -12 -7 -7 -2 -36 0 rrcurveto + -19 vlineto + return + + + -231 0 115 275 rlineto + return + + + -75 -71 -55 -30 -90 0 -69 0 -65 25 -44 47 -47 51 -25 78 0 110 0 173 89 122 151 0 65 0 52 -29 44 -47 25 -27 15 -29 12 -55 rrcurveto + 23 0 -9 227 -20 0 rlineto + -6 -21 -17 -12 -19 0 -17 0 -31 12 -21 6 -40 11 -40 4 -39 0 -93 0 -86 -33 -64 -71 -57 -64 -33 -83 0 -100 0 -99 35 -91 61 -60 rrcurveto + 58 -57 87 -32 91 0 118 0 97 44 58 83 rrcurveto + return + + + -19 vlineto + 76 -5 12 -9 0 -79 rrcurveto + -435 vlineto + 0 -78 -10 -13 -78 -5 rrcurveto + -19 281 vlineto + 101 0 89 24 62 40 89 57 47 94 0 119 0 103 -34 78 -65 56 -68 59 -103 32 -129 0 rrcurveto + return + + + 33 15 9 38 vhcurveto + 100 0 63 -17 59 -52 63 -55 32 -69 0 -104 0 -113 -34 -79 -70 -47 -60 -40 -55 -12 -103 0 rrcurveto + -39 -9 12 35 hvcurveto + return + + + -28 hlineto + -45 -106 -43 -26 -143 0 rrcurveto + -36 hlineto + -74 -27 5 42 hvcurveto + 243 151 vlineto + 86 0 15 -13 12 -84 rrcurveto + 23 234 -23 hlineto + -12 -79 -15 -17 -86 0 rrcurveto + -151 220 hlineto + 32 7 4 28 vhcurveto + 131 hlineto + 116 0 23 -15 15 -90 rrcurveto + 25 0 -4 143 -530 0 0 -19 rlineto + 75 -4 12 -15 0 -74 rrcurveto + -436 vlineto + 0 -77 -10 -13 -77 -5 rrcurveto + -19 539 vlineto + return + + + -256 -18 hlineto + 79 -6 10 -8 0 -78 rrcurveto + -157 vlineto + -33 -56 -28 -77 -157 -106 112 186 vhcurveto + 0 99 24 91 53 53 43 44 55 25 69 0 56 0 52 -19 38 -36 30 -29 16 -26 25 -61 rrcurveto + 23 0 -8 211 -22 0 rlineto + -6 -19 -18 -14 -21 0 -10 0 -15 3 -19 7 -45 15 -34 8 -54 0 -97 0 -82 -31 -60 -55 -67 -61 -40 -92 0 -110 0 -98 27 -77 56 -59 rrcurveto + 66 -69 102 -38 102 0 103 0 96 25 55 45 rrcurveto + 200 vlineto + 0 65 12 10 58 5 rrcurveto + return + + + 19 vlineto + -75 6 -14 15 0 71 rrcurveto + 437 vlineto + 0 77 15 12 74 6 rrcurveto + 19 -280 -19 vlineto + 76 -6 13 -10 0 -79 rrcurveto + -189 -303 189 vlineto + 0 78 16 11 73 6 rrcurveto + 19 -280 -19 vlineto + 77 -6 12 -11 0 -78 rrcurveto + -426 vlineto + 0 -86 -11 -12 -78 -5 rrcurveto + -19 279 vlineto + -1 19 rlineto + -74 4 -13 17 0 73 rrcurveto + 202 303 -191 vlineto + 0 -84 -10 -16 -78 -5 rrcurveto + -19 vlineto + return + + + 19 vlineto + -82 3 -16 15 0 75 rrcurveto + 439 vlineto + 0 76 14 12 84 4 rrcurveto + 19 -297 -19 vlineto + 85 -5 12 -8 0 -79 rrcurveto + -439 vlineto + 0 -78 -13 -12 -84 -3 rrcurveto + -19 vlineto + return + + + -287 -19 hlineto + 81 -7 12 -5 0 -81 rrcurveto + -456 vlineto + -50 -6 -20 -31 vhcurveto + -20 0 -1 19 -10 25 -11 28 -15 14 -23 0 rrcurveto + -27 -22 -26 -26 -47 41 -25 60 106 61 62 137 hvcurveto + 364 vlineto + 0 77 11 11 81 6 rrcurveto + return + + + 19 vlineto + -46 0 -27 18 -83 91 rrcurveto + -234 256 186 178 rlineto + 69 66 19 10 68 5 rrcurveto + 19 -259 -19 vlineto + 25 -1 rlineto + 33 -1 8 -10 0 -20 0 -25 -30 -29 -48 -45 rrcurveto + -178 -164 0 200 rlineto + 0 77 7 12 83 6 rrcurveto + 19 -282 -19 vlineto + 79 -5 11 -15 0 -74 rrcurveto + -424 vlineto + 0 -87 -12 -15 -79 -4 rrcurveto + -19 282 19 vlineto + -73 6 -16 9 0 77 rrcurveto + 0 185 26 21 100 -103 rlineto + 79 -82 58 -61 0 -27 0 -14 -13 -9 -29 -1 rrcurveto + -27 -1 0 -19 rlineto + return + + + -26 hlineto + -16 -35 -13 -22 -13 -17 -34 -43 -54 -18 -81 0 rrcurveto + -70 hlineto + -73 -17 7 38 hvcurveto + 464 vlineto + 0 77 12 13 82 5 rrcurveto + 19 -283 -19 vlineto + 78 -6 9 -14 0 -77 rrcurveto + -431 vlineto + 0 -76 -13 -16 -74 -4 rrcurveto + -19 538 vlineto + return + + + 19 vlineto + -73 5 -15 17 0 71 rrcurveto + 438 vlineto + 0 75 15 17 72 1 rrcurveto + 19 -199 vlineto + -221 -502 -231 502 -198 0 0 -19 rlineto + 82 -5 13 -13 0 -76 rrcurveto + -398 vlineto + 0 -112 -14 -14 -83 -6 rrcurveto + -19 234 19 vlineto + -77 4 -16 18 0 110 rrcurveto + 0 398 252 -549 14 0 255 572 0 -449 rlineto + 0 -83 -11 -16 -79 -5 rrcurveto + -19 vlineto + return + + + -237 -19 hlineto + 44 -5 18 -2 17 -19 15 -17 4 -30 0 -51 rrcurveto + 0 -340 -386 483 -170 0 0 -19 rlineto + 49 0 15 -12 33 -41 rrcurveto + -440 vlineto + 0 -107 -15 -15 -82 -9 rrcurveto + -19 234 19 vlineto + -77 11 -16 17 0 103 rrcurveto + 0 388 rlineto + return + + + 441 -549 18 0 0 518 rlineto + 0 79 10 22 19 17 13 11 15 3 38 4 rrcurveto + return + + + 198 -131 147 -196 -195 -132 -139 -203 -208 133 -140 194 192 135 139 206 vhcurveto + return + + + 0 -120 -28 -94 -55 -53 -35 -33 -44 -15 -50 0 -46 0 -44 14 -34 31 -61 56 -29 90 0 119 0 95 26 97 43 49 39 45 50 22 54 0 rrcurveto + 47 0 40 -17 36 -30 57 -48 34 -94 0 -114 rrcurveto + return + + + -19 vlineto + 74 -6 10 -12 0 -74 rrcurveto + -429 vlineto + 0 -86 -6 -11 -78 -6 rrcurveto + -19 280 19 vlineto + -81 4 -13 13 0 76 rrcurveto + 179 vlineto + 27 -2 16 -1 26 0 107 0 49 9 58 52 37 33 20 45 0 53 0 50 -17 41 -33 28 -45 38 -72 25 -100 0 rrcurveto + return + + + 27 6 9 29 136 60 -40 -110 -90 -60 -57 -110 vhcurveto + -20 0 -18 1 -23 2 rrcurveto + return + + + 19 vlineto + -44 4 -21 6 -29 37 rrcurveto + -200 253 rlineto + 125 19 56 58 0 87 0 44 -16 49 -33 27 -43 35 -71 24 -95 0 rrcurveto + -272 -19 hlineto + 77 -6 8 -12 0 -74 rrcurveto + -427 vlineto + 0 -88 -10 -11 -75 -6 rrcurveto + -19 276 19 vlineto + -77 5 -12 12 0 76 rrcurveto + 0 194 56 2 238 -308 rlineto + return + + + 35 17 7 34 124 59 -44 -90 vhcurveto + 0 -50 -21 -44 -36 -20 -46 -25 -35 -7 -96 -2 rrcurveto + return + + + -22 213 -21 0 rlineto + return + + + -4 -22 -11 -12 -17 0 -10 0 -16 4 -18 8 rrcurveto + return + + + -38 16 -26 6 -41 0 -99 0 -75 -66 0 -109 0 -62 32 -62 90 -48 64 -34 69 -44 35 -38 19 -21 10 -23 0 -35 0 -66 -49 -46 -70 0 rrcurveto + -95 0 -65 56 -46 121 rrcurveto + -22 0 29 -212 22 0 rlineto + return + + + 21 13 12 15 vhcurveto + 11 0 17 -3 19 -8 rrcurveto + return + + + 37 -16 39 -7 42 0 116 0 88 75 0 112 0 76 -49 58 -106 62 -105 61 -74 42 0 68 0 55 39 40 62 0 60 0 50 -33 35 -45 rrcurveto + 18 -24 11 -26 12 -44 rrcurveto + return + + + -7 170 -562 0 -7 -170 24 0 rlineto + 20 110 27 18 106 0 rrcurveto + 60 -497 hlineto + 0 -88 -11 -11 -83 -5 rrcurveto + -19 292 19 vlineto + -82 4 -14 12 0 76 rrcurveto + 509 59 vlineto + 106 0 26 -18 22 -110 rrcurveto + return + + + -232 -19 hlineto + 81 -5 13 -22 0 -103 rrcurveto + -263 vlineto + 0 -81 -4 -40 -33 -40 -32 -37 -56 -22 -64 0 -57 0 -42 17 -29 28 -40 39 -4 45 0 78 rrcurveto + 312 vlineto + 0 75 11 12 80 7 rrcurveto + 19 -283 -19 vlineto + 79 -6 11 -11 0 -67 rrcurveto + -310 vlineto + 0 -106 26 -70 55 -41 41 -31 56 -15 73 0 92 0 68 25 42 48 45 51 9 54 0 97 rrcurveto + 255 vlineto + 0 95 10 24 84 8 rrcurveto + return + + + -197 -19 hlineto + 51 -1 19 -13 0 -27 0 -21 -6 -25 -11 -31 rrcurveto + -126 -339 -143 364 rlineto + -9 24 -8 23 0 11 0 21 27 13 50 1 rrcurveto + 19 -265 -19 vlineto + 57 -1 12 -7 33 -88 rrcurveto + 28 -75 -104 -283 -144 376 rlineto + -7 19 -4 16 0 12 0 21 13 9 51 1 rrcurveto + 19 -244 -19 vlineto + 51 -1 20 -24 33 -95 59 -168 72 -186 61 -180 rrcurveto + 15 0 154 423 rlineto + 52 -133 57 -148 51 -142 rrcurveto + 15 hlineto + 85 261 10 29 109 294 19 52 11 8 53 10 rrcurveto + return + + + 19 vlineto + -53 6 -19 12 -39 56 rrcurveto + -192 274 143 177 rlineto + 63 78 25 17 64 4 rrcurveto + 19 -237 -19 vlineto + 54 -2 14 -7 0 -23 0 -15 -10 -15 -30 -37 rrcurveto + -112 -140 -44 64 rlineto + -53 78 -30 38 0 29 0 23 18 5 28 1 rrcurveto + 29 1 0 19 -301 0 0 -19 rlineto + 66 -7 21 -12 96 -141 rrcurveto + 106 -155 -156 -195 rlineto + -77 -97 -16 -12 -52 -5 rrcurveto + -19 232 19 vlineto + -58 4 -18 9 0 19 0 15 16 28 37 46 rrcurveto + 119 149 95 -141 rlineto + 34 -51 17 -31 0 -16 0 -18 -18 -9 -32 -2 rrcurveto + -26 -2 0 -19 rlineto + return + + + -218 -19 hlineto + 52 -1 15 -8 1 -22 0 -11 -4 -16 -10 -15 rrcurveto + -145 -222 -146 221 rlineto + -11 16 -7 18 0 13 0 16 15 9 32 1 rrcurveto + 24 1 0 19 -279 0 0 -19 rlineto + 48 -2 22 -20 92 -135 rrcurveto + 131 -192 0 -171 rlineto + 0 -89 -12 -9 -89 -6 rrcurveto + -19 306 19 vlineto + -84 4 -19 10 0 78 rrcurveto + 0 192 148 228 rlineto + 56 86 23 21 59 5 rrcurveto + return + + + -23 hlineto + -18 -59 -9 -28 -33 -23 -25 -18 -36 -10 -56 0 rrcurveto + -252 0 432 609 0 15 -525 0 -21 -171 26 0 rlineto + 10 55 6 17 25 26 24 24 39 11 71 0 rrcurveto + 214 0 -437 -609 0 -15 563 0 rlineto + return + + + 50 -500 -50 vlineto + return + + + -145 148 rlineto + -17 18 -11 5 -15 0 -22 0 -14 -11 0 -21 0 -16 11 -15 19 -12 rrcurveto + 154 -96 rlineto + return + + + 27 -23 23 -27 -27 -22 -22 -27 -28 21 -22 27 28 23 22 27 vhcurveto + return + + + 54 -311 -54 vlineto + return + + + -35 0 -41 -99 7 -5 rlineto + 8 3 8 1 12 0 rrcurveto + 38 15 -10 -26 -27 -21 -17 -34 hvcurveto + -21 0 -17 3 -27 9 rrcurveto + -14 -31 rlineto + 29 -12 24 -4 31 0 rrcurveto + 77 48 32 52 44 -32 25 -54 hvcurveto + -10 0 -6 -1 -10 -2 rrcurveto + return + + + -125 167 -62 0 -124 -167 34 0 121 103 122 -103 rlineto + return + + + -17 -37 -13 -13 -23 0 -15 0 -19 5 -19 10 rrcurveto + -24 12 rlineto + return + + + -24 12 -24 6 -22 0 rrcurveto + return + + + -50 0 -36 -36 -15 -65 rrcurveto + 29 hlineto + return + + + 11 31 17 15 23 0 12 0 14 -4 15 -7 rrcurveto + 23 -11 rlineto + return + + + 41 -20 15 -4 25 0 55 0 29 30 21 76 rrcurveto + return + + + 54 -45 46 -53 -56 -45 -44 -55 -56 43 -44 55 56 45 44 55 vhcurveto + return + + + -37 -29 -29 -38 -35 -29 29 37 35 30 30 34 37 30 -29 -36 vhcurveto + return + + + -29 -71 -28 -26 -54 0 -63 0 -36 34 -13 63 rrcurveto + -29 hlineto + -97 45 -60 94 vhcurveto + 82 0 48 51 12 106 rrcurveto + return + + + 28 -19 22 -31 -30 -19 -23 -27 -27 22 -22 27 27 23 20 29 vhcurveto + return + + + -35 0 -121 -103 -120 103 -35 0 123 -167 64 0 rlineto + return + + + 40 0 154 96 rlineto + 21 13 8 14 0 18 0 19 -14 11 -20 0 -13 0 -10 -3 -20 -20 rrcurveto + return + + + -30 -10 -82 -66 0 -71 rrcurveto + -83 58 -13 23 25 33 15 43 40 -36 10 -26 vhcurveto + -11 0 -7 -4 -6 0 -7 0 -7 5 0 9 0 19 16 45 66 42 rrcurveto + return + + + 30 10 82 66 0 71 rrcurveto + 83 -58 13 -23 -25 -33 -15 -43 -40 36 -10 26 vhcurveto + 11 0 7 4 6 0 7 0 7 -5 0 -9 0 -19 -16 -45 -66 -42 rrcurveto + return + + + -66 42 -16 45 0 19 0 9 7 5 7 0 6 0 7 -4 11 0 rrcurveto + 26 36 10 40 43 -33 15 -25 -23 -58 -13 -83 hvcurveto + 0 -71 82 -66 30 -10 rrcurveto + return + + + -37 30 vlineto + 34 27 -26 -34 -34 -27 -27 -34 hvcurveto + -30 -37 30 hlineto + 54 44 44 54 54 -44 43 -54 hvcurveto + return + + + 37 -30 vlineto + -34 -27 27 34 34 27 26 34 hvcurveto + 30 37 -30 hlineto + -54 -44 -43 -54 -54 44 -44 54 hvcurveto + return + + + 40 -95 147 -40 -147 -95 -40 vlineto + return + + + 40 -230 -40 95 -147 40 147 vlineto + return + + + -29 -27 -21 -9 -21 0 -22 0 -16 11 0 28 0 23 13 22 31 25 rrcurveto + -46 hlineto + -38 -27 -16 -28 0 -36 0 -47 31 -27 43 0 46 0 29 22 36 54 rrcurveto + return + + + 54 -45 45 -53 -56 -45 -44 -55 -56 43 -44 55 56 45 45 55 vhcurveto + return + + + -17 -37 -13 -14 -23 0 -20 0 -14 6 -43 22 rrcurveto + return + + + 11 31 17 15 23 0 18 0 8 -4 38 -18 rrcurveto + return + + + 40 0 154 96 rlineto + 19 12 11 15 0 16 0 21 -14 11 -22 0 -15 0 -11 -5 -17 -18 rrcurveto + return + + + -146 148 rlineto + -20 20 -10 3 -13 0 -20 0 -14 -11 0 -19 0 -18 8 -14 21 -13 rrcurveto + 154 -96 rlineto + return + + + -12 106 -48 51 -82 0 rrcurveto + -94 -45 -60 -97 hvcurveto + 29 hlineto + 13 63 36 34 63 0 54 0 28 -26 29 -71 rrcurveto + return + + + 27 -23 22 -27 -27 -22 -22 -27 -28 21 -22 27 28 23 22 28 vhcurveto + return + + + -133 106 -18 -8 rlineto + 29 -29 21 -18 0 -9 rrcurveto + -13 -21 -2 -28 vhcurveto + -178 hlineto + -33 -16 3 12 hvcurveto + 0 7 17 22 32 27 rrcurveto + -18 8 -132 -106 133 -106 18 8 rlineto + -29 29 -21 18 0 9 rrcurveto + 10 16 5 34 vhcurveto + 177 hlineto + 33 16 -3 -12 hvcurveto + 0 -7 -17 -22 -32 -27 rrcurveto + 18 -8 rlineto + return + + + 14 -10 16 -15 vhcurveto + -4 0 -5 -2 -6 -3 -20 -12 -17 -35 -51 -28 0 49 21 44 0 22 rrcurveto + 14 -14 13 -12 -14 -13 -13 -14 vhcurveto + 0 -22 20 -44 0 -50 -39 21 -30 42 -21 13 -6 3 -5 2 -4 0 -15 0 -10 -16 0 -14 0 -9 5 -7 8 -5 20 -12 54 -6 36 -22 rrcurveto + -36 -21 -52 -6 -19 -11 -10 -5 -4 -13 0 -9 0 -11 9 -11 17 0 5 0 7 2 6 4 16 9 32 43 36 20 0 -48 -20 -43 0 -22 rrcurveto + -12 13 -15 14 12 14 15 12 vhcurveto + 0 22 -21 43 0 48 54 -30 17 -48 28 0 18 0 9 13 0 13 0 8 -4 10 -11 5 -17 11 -44 2 -45 25 51 29 39 0 16 10 rrcurveto + 12 6 5 7 0 9 rrcurveto + return + + + 54 -286 vlineto + -33 -16 3 12 hvcurveto + 0 7 17 22 32 27 rrcurveto + -18 8 -132 -106 133 -106 18 8 rlineto + -29 29 -21 18 0 9 rrcurveto + 13 21 2 28 vhcurveto + return + + + 25 vlineto + -41 0 -18 22 -51 121 rrcurveto + -222 522 -28 0 -221 -545 rlineto + -38 -94 -15 -18 -46 -8 rrcurveto + -25 202 25 vlineto + -59 4 -22 11 0 26 0 24 19 42 12 31 rrcurveto + 13 34 225 0 rlineto + 34 -79 12 -35 0 -22 0 -22 -13 -8 -33 -3 rrcurveto + -32 -3 0 -25 rlineto + return + + + 117 24 42 51 0 76 rrcurveto + 103 -91 57 -161 vhcurveto + -317 -25 hlineto + 68 -5 20 -17 0 -59 rrcurveto + -467 vlineto + 0 -56 -15 -14 -73 -8 rrcurveto + -25 329 vlineto + 178 96 81 107 hvcurveto + 0 95 -83 67 -110 14 rrcurveto + return + + + 39 17 15 40 68 31 -49 -88 -98 -31 -38 -125 vhcurveto + return + + + 26 hlineto + 106 51 -53 -102 -101 -42 -55 -78 -44 -19 18 48 hvcurveto + return + + + -15 -36 -10 -10 -19 0 -10 0 -13 4 -23 10 rrcurveto + return + + + -28 hlineto + -30 -65 -18 -30 -34 -28 -47 -39 -57 -15 -78 0 rrcurveto + -65 -18 13 43 hvcurveto + 242 vlineto + 109 0 43 -36 10 -119 rrcurveto + 26 338 -26 hlineto + -12 -116 -44 -32 -106 -1 rrcurveto + 225 vlineto + 38 13 16 52 vhcurveto + 163 0 51 -33 23 -134 rrcurveto + 25 201 -577 -25 hlineto + 69 -4 19 -16 0 -51 rrcurveto + -479 vlineto + 0 -52 -14 -16 -74 -8 rrcurveto + -25 585 vlineto + return + + + 25 vlineto + -72 9 -16 11 0 53 rrcurveto + 479 vlineto + 0 54 20 15 68 5 rrcurveto + 25 -339 -25 vlineto + 70 -5 19 -13 0 -56 rrcurveto + -204 -241 204 vlineto + 0 56 19 13 70 5 rrcurveto + 25 -336 -25 vlineto + 67 -6 18 -13 0 -55 rrcurveto + -479 vlineto + 0 -53 -15 -11 -70 -9 rrcurveto + -25 336 25 vlineto + -73 8 -16 12 0 53 rrcurveto + 228 241 -228 vlineto + 0 -51 -16 -14 -73 -8 rrcurveto + -25 vlineto + return + + + 25 vlineto + -76 5 -19 11 0 65 rrcurveto + 470 vlineto + 0 56 21 16 74 3 rrcurveto + 25 -350 -25 vlineto + 72 -5 21 -14 0 -56 rrcurveto + -470 vlineto + 0 -62 -18 -12 -75 -7 rrcurveto + -25 vlineto + return + + + 25 vlineto + -25 1 -12 5 -12 15 rrcurveto + -305 378 rlineto + 186 198 37 17 99 12 rrcurveto + 25 -289 -25 vlineto + 35 -3 rlineto + 36 -3 12 -8 0 -20 0 -16 -9 -11 -31 -31 rrcurveto + -212 -215 0 223 rlineto + 0 61 14 18 74 5 rrcurveto + 25 -337 -25 vlineto + 69 -4 18 -12 0 -62 rrcurveto + -472 vlineto + 0 -57 -13 -12 -74 -7 rrcurveto + -25 336 25 vlineto + -72 9 -15 12 0 49 rrcurveto + 0 196 27 25 181 -225 rlineto + 19 -24 6 -10 0 -11 0 -13 -12 -6 -65 -2 rrcurveto + -25 vlineto + return + + + 25 vlineto + -80 7 -8 21 0 73 rrcurveto + 428 vlineto + 0 75 15 17 73 5 rrcurveto + 25 -252 vlineto + -201 -472 -199 472 -253 0 0 -25 rlineto + 73 -6 16 -14 0 -54 rrcurveto + -469 vlineto + 0 -65 -13 -10 -78 -8 rrcurveto + -25 234 25 vlineto + -82 6 -19 22 0 76 rrcurveto + 0 465 252 -594 27 0 252 605 0 -496 rlineto + 0 -59 -14 -18 -75 -7 rrcurveto + -25 vlineto + return + + + -214 -25 hlineto + 74 -7 18 -19 0 -80 rrcurveto + 0 -299 -349 430 -211 0 0 -25 rlineto + 20 0 23 -14 42 -58 rrcurveto + -470 vlineto + 0 -61 -15 -14 -73 -9 rrcurveto + -25 226 25 vlineto + -77 9 -17 20 0 77 rrcurveto + 0 396 rlineto + return + + + 447 -545 28 0 0 586 rlineto + 0 63 13 10 65 10 rrcurveto + return + + + 207 -148 149 -201 -208 -151 -144 -213 -207 146 -146 207 207 148 146 208 vhcurveto + return + + + -206 -61 -108 -115 -114 -64 105 211 210 63 118 110 117 64 -117 -213 vhcurveto + return + + + -25 vlineto + 70 -6 14 -14 0 -78 rrcurveto + -427 vlineto + 0 -51 -6 -28 -33 -13 -11 -5 -15 -2 -19 -2 rrcurveto + -25 333 25 vlineto + -79 10 -8 9 0 67 rrcurveto + 192 vlineto + 132 1 34 3 43 16 83 31 46 61 0 80 rrcurveto + 122 -102 59 -167 vhcurveto + return + + + 23 17 12 33 83 33 -46 -111 vhcurveto + 0 -111 -41 -34 -125 -1 rrcurveto + return + + + -2 201 -600 0 -3 -201 29 0 rlineto + 17 119 52 46 124 4 rrcurveto + -527 vlineto + 0 -74 -11 -11 -86 -7 rrcurveto + -25 357 25 vlineto + -87 6 -11 10 0 76 rrcurveto + 527 vlineto + 123 -4 52 -46 17 -119 rrcurveto + return + + + 25 vlineto + -34 1 -16 12 -47 73 rrcurveto + -182 283 80 111 rlineto + 87 121 26 18 75 7 rrcurveto + 25 -250 -25 vlineto + 21 -2 rlineto + 38 -4 13 -7 0 -24 0 -22 -11 -19 -47 -65 -20 -27 -18 -25 -18 -25 rrcurveto + -97 150 rlineto + -13 20 -2 5 0 12 0 21 12 8 36 2 rrcurveto + 31 2 0 25 -346 0 0 -25 rlineto + 36 -3 13 -10 31 -45 rrcurveto + 198 -291 -175 -221 rlineto + -30 -37 -24 -13 -50 -6 rrcurveto + -25 250 25 vlineto + -61 6 -20 10 0 26 0 21 19 31 75 101 rrcurveto + 38 51 101 -162 rlineto + 12 -19 9 -23 0 -12 0 -17 -17 -9 -35 -2 rrcurveto + -28 -2 0 -25 rlineto + return + + + 24 vlineto + -35 5 -13 13 0 43 rrcurveto + 376 -193 -24 vlineto + 44 -9 10 -10 0 -45 rrcurveto + -285 vlineto + 0 -46 -7 -7 -46 -11 rrcurveto + -24 vlineto + return + + + -202 -24 hlineto + 51 -4 12 -12 0 -43 rrcurveto + -474 vlineto + -53 -14 -23 -34 -19 -13 7 10 vhcurveto + 0 5 3 7 6 10 10 15 4 12 0 10 rrcurveto + 30 -26 24 -33 -37 -25 -25 -36 -59 58 -41 82 vhcurveto + 69 0 53 27 28 49 19 32 8 38 0 59 rrcurveto + return + + + 142 -96 102 -126 -132 -97 -99 -146 -142 100 -100 125 129 97 102 141 vhcurveto + return + + + -149 -20 -52 -58 -58 -21 56 145 171 18 53 61 59 19 -53 -171 vhcurveto + return + + + 16 -16 vlineto + -42 0 -25 21 -7 49 rrcurveto + -78 581 -18 0 -356 -577 rlineto + -36 -58 -37 -12 -35 -4 rrcurveto + -16 218 16 -29 vlineto + -26 0 -16 11 -1 18 0 7 3 11 7 8 rrcurveto + 84 140 198 0 13 -113 rlineto + 1 -7 1 -8 0 -6 rrcurveto + -40 -16 -21 -56 vhcurveto + -16 -16 hlineto + return + + + -170 0 139 227 6 0 rlineto + return + + + -5 -16 19 0 rlineto + 59 19 -16 -26 hvcurveto + 0 -6 -1 -10 -1 -5 rrcurveto + -123 -495 rlineto + -8 -34 -19 -29 -76 0 rrcurveto + -20 0 -4 -16 324 0 rlineto + 207 82 94 99 hvcurveto + 0 85 -67 42 -60 11 rrcurveto + -3 4 rlineto + 20 5 29 8 25 13 48 24 43 45 0 78 rrcurveto + 136 -152 9 -84 vhcurveto + return + + + 13 2 15 1 15 0 rrcurveto + 54 65 -21 -77 -97 -56 -66 -112 hvcurveto + -15 0 -18 2 -20 4 rrcurveto + return + + + 15 4 20 0 16 0 rrcurveto + 78 64 -35 -73 -130 -96 -42 -94 hvcurveto + -26 0 -20 2 -25 7 rrcurveto + return + + + -534 0 -4 -16 19 0 rlineto + 59 16 -11 -24 hvcurveto + 0 -7 -1 -11 -4 -18 rrcurveto + -120 -488 rlineto + -8 -34 -23 -28 -73 0 rrcurveto + -19 0 -4 -16 571 0 66 198 -16 0 rlineto + -43 -113 -56 -47 -158 0 rrcurveto + -67 hlineto + -47 -16 11 16 hvcurveto + 0 3 1 3 1 4 rrcurveto + 66 258 90 0 rlineto + 31 41 -4 -47 hvcurveto + 0 -17 -5 -30 -1 -13 rrcurveto + 17 0 58 242 -16 0 rlineto + -22 -82 -44 -13 -60 0 rrcurveto + -80 0 60 248 160 0 rlineto + 51 0 55 -13 3 -74 0 -11 -1 -11 -1 -13 rrcurveto + 16 hlineto + return + + + -330 0 -4 -16 20 0 rlineto + 57 19 -15 -24 hvcurveto + 0 -7 -2 -7 -2 -10 rrcurveto + -53 -217 -278 0 52 217 rlineto + 8 35 18 28 79 0 rrcurveto + 18 0 4 16 -330 0 -4 -16 20 0 rlineto + 62 16 -18 -21 hvcurveto + 0 -7 -2 -11 -2 -6 rrcurveto + -123 -496 rlineto + -8 -34 -23 -28 -75 0 rrcurveto + -18 0 -4 -16 330 0 4 16 -20 0 rlineto + -59 -18 14 24 hvcurveto + 0 8 2 10 2 6 rrcurveto + 61 242 280 0 -61 -242 rlineto + -9 -37 -24 -25 -72 0 rrcurveto + -18 0 -4 -16 329 0 4 16 -19 0 rlineto + -59 -19 13 21 hvcurveto + 0 10 2 9 2 9 rrcurveto + 123 496 rlineto + 9 35 21 28 76 0 rrcurveto + 18 hlineto + return + + + -328 0 -4 -16 18 0 rlineto + 60 17 -16 -22 hvcurveto + 0 -7 -2 -9 -2 -8 rrcurveto + -123 -497 rlineto + -8 -34 -21 -28 -76 0 rrcurveto + -19 0 -4 -16 328 0 4 16 -17 0 rlineto + -59 -19 12 21 hvcurveto + 0 10 1 11 2 8 rrcurveto + 123 497 rlineto + 9 36 21 26 76 0 rrcurveto + 19 hlineto + return + + + -216 0 -4 -16 rlineto + 42 0 8 -7 1 -14 0 -13 -8 -8 -10 -8 rrcurveto + -282 -221 -5 0 53 209 rlineto + 9 35 13 27 75 0 rrcurveto + 11 0 4 16 -312 0 -4 -16 18 0 rlineto + 60 16 -16 -22 hvcurveto + 0 -21 -3 -1 -7 -27 rrcurveto + -117 -472 rlineto + -7 -27 -17 -35 -60 0 rrcurveto + -18 0 -4 -16 304 0 4 16 -17 0 rlineto + -59 -19 16 24 hvcurveto + 0 7 1 7 2 8 rrcurveto + 62 243 4 0 156 -237 rlineto + 8 -13 8 -6 0 -16 rrcurveto + -28 -19 -5 -28 vhcurveto + -11 0 -4 -16 287 0 4 16 rlineto + -65 0 -33 36 -29 43 rrcurveto + -187 278 271 211 rlineto + 42 33 27 20 51 0 rrcurveto + return + + + -215 0 -333 -493 -4 0 -89 493 -213 0 -4 -16 19 0 rlineto + 62 15 -15 -20 hvcurveto + 0 -9 -1 -12 -4 -17 rrcurveto + -119 -475 rlineto + -10 -40 -19 -33 -79 0 rrcurveto + -19 0 -4 -16 267 0 4 16 -20 0 rlineto + -59 -17 12 21 hvcurveto + 0 12 2 15 3 13 rrcurveto + 109 428 3 0 94 -517 25 0 345 517 6 0 -112 -439 rlineto + -8 -32 -18 -30 -79 0 rrcurveto + -17 0 -4 -16 330 0 4 16 -19 0 rlineto + -58 -19 15 19 hvcurveto + 0 9 0 12 2 7 rrcurveto + 127 498 rlineto + 9 36 16 25 80 0 rrcurveto + 17 hlineto + return + + + -266 0 -4 -16 20 0 rlineto + 59 17 -17 -20 hvcurveto + 0 -10 -2 -7 -2 -9 rrcurveto + -97 -386 -5 0 -236 465 -200 0 -4 -16 19 0 rlineto + 30 0 43 -7 19 -43 rrcurveto + -126 -509 rlineto + -8 -34 -23 -28 -75 0 rrcurveto + -18 0 -4 -16 266 0 4 16 -19 0 rlineto + -59 -17 15 20 hvcurveto + 0 11 1 8 2 8 rrcurveto + 108 448 6 0 274 -526 24 0 144 574 rlineto + 9 37 23 26 75 0 rrcurveto + 18 hlineto + return + + + 126 -82 123 -167 -255 -158 -244 -203 -137 104 -96 149 253 156 233 198 vhcurveto + return + + + -141 -73 -275 -212 -78 -62 53 97 142 52 313 232 101 40 -82 -107 vhcurveto + return + + + -4 -16 19 0 rlineto + 61 17 -16 -21 hvcurveto + 0 -8 -2 -9 -2 -8 rrcurveto + -125 -497 rlineto + -9 -34 -20 -28 -77 0 rrcurveto + -17 0 -4 -16 341 0 4 16 -32 0 rlineto + -59 -18 15 24 hvcurveto + 0 8 1 7 2 8 rrcurveto + 56 223 rlineto + 4 -1 3 -1 5 0 24 -4 22 -1 28 0 rrcurveto + 204 81 101 98 74 -62 86 -170 hvcurveto + return + + + 17 2 15 1 17 0 rrcurveto + 95 30 -53 -60 -102 -80 -70 -102 hvcurveto + -20 0 -24 4 -16 4 rrcurveto + return + + + -599 0 -46 -179 16 0 rlineto + 18 81 70 61 83 0 rrcurveto + 91 0 -133 -538 rlineto + -8 -34 -20 -28 -78 0 rrcurveto + -18 0 -4 -16 329 0 4 16 -21 0 rlineto + -60 -16 16 21 hvcurveto + 0 9 3 7 2 9 rrcurveto + 133 538 98 0 rlineto + 41 58 -5 -66 hvcurveto + 0 -27 -3 -27 -2 -17 rrcurveto + 16 hlineto + return + + + -222 0 -4 -16 9 0 rlineto + 32 0 12 -8 1 -16 0 -11 -6 -12 -9 -11 rrcurveto + -164 -184 -60 179 rlineto + -4 11 -2 9 0 8 rrcurveto + 31 22 4 30 vhcurveto + 19 0 4 16 -283 0 -4 -16 9 0 rlineto + 74 0 17 -52 11 -31 rrcurveto + 85 -231 -208 -230 rlineto + -37 -40 -49 -30 -54 -7 rrcurveto + -4 -16 249 0 4 16 -22 0 rlineto + -38 -18 13 19 hvcurveto + 0 9 3 9 9 10 rrcurveto + 181 201 69 -194 rlineto + 3 -10 1 -8 0 -8 rrcurveto + -37 -29 -4 -38 vhcurveto + -11 0 -4 -16 281 0 4 16 -14 0 rlineto + -50 0 -17 34 -12 34 rrcurveto + -96 266 187 213 rlineto + 37 42 29 32 69 0 rrcurveto + 4 hlineto + return + + + -563 0 -48 -158 17 0 rlineto + 38 115 89 6 96 0 rrcurveto + 202 0 -573 -616 581 0 46 178 -16 0 rlineto + -35 -129 -88 -11 -94 0 rrcurveto + -218 hlineto + return + + + 25 -22 25 -26 -29 -19 -25 -25 -26 19 -20 29 26 22 20 26 vhcurveto + return + + + 108 -67 46 -80 -148 -103 -161 -128 -73 34 -90 101 153 110 159 139 vhcurveto + return + + + -76 -71 -223 -106 -56 -3 74 33 136 87 149 82 53 14 -55 -38 vhcurveto + return + + + -8 -61 -40 -32 -53 0 -22 0 -37 10 -50 22 -93 40 -64 21 -33 0 rrcurveto + -62 -73 -48 -104 hvcurveto + 26 hlineto + 10 60 41 31 50 0 21 0 36 -10 50 -21 108 -44 10 -16 69 0 rrcurveto + 70 69 56 96 hvcurveto + return + + + -80 0 -676 -1845 80 0 rlineto + return + + + 50 -1500 -50 vlineto + return + + + -15 -61 -62 -32 -99 0 -42 0 -70 10 -94 22 -174 40 -120 21 -62 0 -116 0 -79 -40 -20 -112 rrcurveto + 26 hlineto + 19 60 61 31 94 0 39 0 68 -11 94 -20 203 -44 19 -16 129 0 131 0 77 46 19 106 rrcurveto + return + + + 50 -2000 -50 vlineto + return + + + -21 -67 -85 -35 -142 0 -60 0 -142 12 -134 23 -251 43 -183 24 -90 0 -167 0 -127 -45 -32 -121 rrcurveto + 26 hlineto + 28 66 115 33 135 0 56 0 109 -11 134 -22 292 -48 70 -18 185 0 188 0 96 49 26 117 rrcurveto + return + + + 50 -2500 -50 vlineto + return + + + -27 -66 -164 -31 -153 0 -64 0 -225 11 -143 22 -268 41 -280 23 -97 0 -179 0 -222 -44 -35 -119 rrcurveto + 29 hlineto + 33 65 216 32 144 0 60 0 170 -12 144 -22 311 -47 179 -16 199 0 200 0 172 48 29 115 rrcurveto + return + + + 50 -3000 -50 vlineto + return + + + -27 -66 -279 -31 -153 0 -64 0 -337 11 -143 22 -268 41 -380 23 -97 0 -179 0 -337 -44 -35 -119 rrcurveto + 29 hlineto + 33 65 331 32 144 0 60 0 270 -12 144 -22 311 -47 291 -16 199 0 200 0 287 48 29 115 rrcurveto + return + + + + + + -351 endchar + + + 121 0 20 196 41 397 20 hstem + 707 hmoveto + -89 callsubr + -5 257 rmoveto + -88 callsubr + endchar + + + 66 0 37 588 37 hstem + 422 349 rmoveto + 52 13 20 8 24 21 25 22 16 41 0 41 rrcurveto + 106 -89 61 -173 vhcurveto + -280 -19 hlineto + 81 -5 15 -9 0 -79 rrcurveto + -437 vlineto + 0 -76 -17 -16 -79 -2 rrcurveto + -19 337 vlineto + 145 94 67 111 hvcurveto + 0 44 -17 42 -32 29 -29 26 -27 18 -66 11 rrcurveto + -207 18 rmoveto + 227 vlineto + 23 7 9 17 vhcurveto + 41 hlineto + 119 58 -51 -84 -83 -49 -41 -101 hvcurveto + -92 -40 rmoveto + 118 -4 38 4 60 -40 33 -22 14 -37 0 -48 0 -43 -13 -34 -25 -22 -44 -39 -35 -4 -84 0 rrcurveto + -46 -16 11 34 hvcurveto + endchar + + + 66 -14 44 606 40 hstem + 614 131 rmoveto + -87 callsubr + endchar + + + 121 0 37 588 37 hstem + 16 662 rmoveto + -86 callsubr + -80 -79 rmoveto + -85 callsubr + endchar + + + 10 0 37 290 41 256 38 hstem + 597 169 rmoveto + -84 callsubr + endchar + + + -45 0 20 307 41 256 38 hstem + 546 519 rmoveto + -5 143 -529 0 0 -19 rlineto + 77 -6 10 -13 0 -73 rrcurveto + -428 vlineto + 0 -87 -12 -12 -76 -5 rrcurveto + -19 281 19 vlineto + -77 4 -14 13 0 76 rrcurveto + 215 142 vlineto + 86 0 19 -15 8 -82 rrcurveto + 23 233 -23 hlineto + -9 -78 -20 -17 -84 0 rrcurveto + -142 217 hlineto + 31 5 8 29 vhcurveto + 132 hlineto + 121 0 18 -16 15 -89 rrcurveto + endchar + + + 121 -14 40 610 40 hstem + 709 354 rmoveto + -83 callsubr + endchar + + + 121 0 20 295 44 283 20 hstem + 703 hmoveto + -82 callsubr + endchar + + + -268 0 20 622 20 hstem + 315 hmoveto + -81 callsubr + endchar + + + -228 -14 38 618 20 hstem + 354 662 rmoveto + -80 callsubr + endchar + + + 121 0 20 622 20 hstem + 723 hmoveto + -79 callsubr + endchar + + + 10 0 39 603 20 hstem + 598 174 rmoveto + -78 callsubr + endchar + + + 288 0 20 622 20 hstem + 109 44 vstem + 864 hmoveto + -77 callsubr + endchar + + + 121 -11 20 -9 20 622 20 hstemhm + 109 44 415 44 hintmask 01111000 + 707 662 rmoveto + -76 callsubr + hintmask 10111000 + -75 callsubr + endchar + + + 121 -14 36 618 36 hstem + 688 331 rmoveto + -74 callsubr + -114 6 rmoveto + -73 callsubr + endchar + + + -44 0 20 268 40 297 37 hstem + 16 662 rmoveto + -72 callsubr + -73 -73 rmoveto + -71 callsubr + endchar + + + 121 640 36 hstem + 701 -177 rmoveto + 18 vlineto + -114 7 -88 41 -73 104 68 13 42 15 44 43 73 70 35 80 0 119 rrcurveto + 207 -136 136 -191 -187 -140 -132 -212 vhcurveto + 0 -90 28 -78 48 -64 38 -51 39 -23 78 -27 rrcurveto + 41 -52 rlineto + 68 -86 123 -38 156 0 rrcurveto + -79 515 rmoveto + 0 -100 -24 -98 -40 -46 -37 -43 -50 -29 -62 0 -57 0 -65 30 -33 48 -38 53 -20 86 0 96 0 95 26 92 40 47 37 45 53 26 54 0 rrcurveto + 47 0 42 -15 36 -30 57 -48 34 -94 0 -115 rrcurveto + endchar + + + 66 0 20 605 37 hstem + 660 hmoveto + -70 callsubr + -294 583 rmoveto + -69 callsubr + endchar + + + -45 -14 36 -22 20 615 41 -34 20 hstemhm + 71 86 hintmask 00101000 + 469 463 rmoveto + -68 callsubr + hintmask 00011000 + -67 callsubr + hintmask 10101000 + -66 callsubr + hintmask 01001000 + -65 callsubr + hintmask 10101000 + -64 callsubr + endchar + + + 10 0 20 600 42 hstem + 593 492 rmoveto + -63 callsubr + endchar + + + 121 -14 44 612 20 hstem + 567 44 vstem + 705 662 rmoveto + -62 callsubr + endchar + + + 121 -11 20 633 20 hstem + 697 662 rmoveto + -204 -19 hlineto + 40 -4 32 2 0 -39 0 -17 -7 -25 -12 -30 rrcurveto + -147 -369 -152 342 rlineto + -30 67 -9 24 0 16 0 20 13 9 32 2 rrcurveto + 28 2 0 19 -265 0 0 -19 rlineto + 50 -1 15 -12 43 -97 rrcurveto + 244 -544 15 0 222 563 rlineto + 29 74 23 16 40 1 rrcurveto + endchar + + + 343 -11 20 633 20 hstem + 932 662 rmoveto + -61 callsubr + endchar + + + 121 0 20 622 20 hstem + 704 hmoveto + -60 callsubr + endchar + + + 121 0 20 622 20 hstem + 703 662 rmoveto + -59 callsubr + endchar + + + 11 0 38 586 38 hstem + 598 176 rmoveto + -58 callsubr + endchar + + + 177 -13 53 -53 71 586 32 hstemhm + 42 92 68 79 157 53 hintmask 01111100 + 735 111 rmoveto + -32 -40 -21 -13 -39 0 -61 0 -44 31 -47 61 55 74 25 49 46 73 25 40 19 19 50 0 rrcurveto + 21 -216 -21 vlineto + 50 -5 15 -12 0 -30 0 -46 -27 -48 -65 -86 -56 63 -35 55 -41 88 117 50 38 46 0 72 rrcurveto + 79 -58 45 -79 -85 -67 -60 -90 vhcurveto + 0 -45 7 -36 28 -68 rrcurveto + -38 -22 rlineto + -102 -59 -55 -78 0 -81 rrcurveto + hintmask 10011100 + -100 64 -50 107 vhcurveto + 81 0 60 25 75 66 rrcurveto + hintmask 01111100 + 63 -65 49 -26 58 0 67 0 55 40 29 73 rrcurveto + -312 456 rmoveto + 0 -59 -31 -42 -86 -39 -33 61 -7 33 0 51 rrcurveto + 50 32 33 47 46 32 -39 -49 vhcurveto + hintmask 10011100 + -34 -452 rmoveto + -60 -47 -43 -17 -43 0 -71 0 -53 49 0 85 0 69 31 44 87 56 rrcurveto + 54 -113 40 -58 58 -68 rrcurveto + endchar + + + -101 656 20 hstem + 268 471 rmoveto + -101 callsubr + endchar + + + 320 -14 39 118 33 -32 43 277 44 137 31 hstemhm + 116 84 121 71 377 40 hintmask 11011111 + 688 73 rmoveto + -72 -35 -62 -13 -67 0 -171 0 -116 123 0 174 0 74 23 78 39 56 51 73 74 42 95 0 rrcurveto + 157 130 -123 -149 -101 -60 -96 -66 -29 -13 14 33 hvcurveto + 0 7 1 5 1 5 rrcurveto + 65 254 -69 0 -9 -38 -2 0 rlineto + -13 35 -24 17 -36 0 -52 0 -41 -24 -35 -46 -39 -51 -27 -66 0 -65 rrcurveto + hintmask 00100111 + -69 39 -43 49 vhcurveto + 47 0 47 27 31 45 rrcurveto + 2 hlineto + hintmask 11001111 + 5 -44 40 -29 44 0 rrcurveto + 99 85 111 129 165 -143 128 -185 hvcurveto + -86 0 -76 -23 -62 -46 -84 -63 -57 -103 0 -114 0 -197 158 -144 208 0 70 0 55 13 93 44 rrcurveto + hintmask 00110111 + -128 361 rmoveto + 0 -45 -18 -65 -26 -49 -20 -36 -29 -22 -27 0 -37 0 -23 33 0 54 0 44 14 43 22 33 31 46 38 24 29 0 rrcurveto + 29 17 -21 -39 hvcurveto + endchar + + + -323 -11 111 248 111 hstem + 81 111 vstem + 192 403 rmoveto + -98 callsubr + -359 vmoveto + -98 callsubr + endchar + + + -351 -6 20 -16 20 hstemhm + 156 39 hintmask 10100000 + 83 -141 rmoveto + 69 33 43 63 0 60 rrcurveto + 52 -35 35 -45 -36 -24 -23 -34 -34 21 -17 37 vhcurveto + hintmask 01100000 + 11 0 10 4 8 0 8 0 6 -6 0 -7 0 -33 -28 -37 -55 -37 rrcurveto + endchar + + + -101 0 20 hstem + 52 74 104 34 114 80 vstem + 264 637 rmoveto + 80 -10 46 -44 20 -83 rrcurveto + 15 111 hlineto + -31 31 -49 17 -81 5 rrcurveto + 63 -34 -63 vlineto + -93 -6 -85 -45 0 -100 0 -84 40 -41 138 -78 rrcurveto + -282 vlineto + -88 0 -62 53 -21 100 rrcurveto + -15 -130 hlineto + 55 -37 44 -14 87 0 rrcurveto + -87 34 87 vlineto + 115 16 79 38 0 116 0 37 -7 32 -14 21 -27 41 -25 25 -121 65 rrcurveto + -34 16 rmoveto + -84 57 -20 24 0 44 0 51 33 40 71 14 rrcurveto + 34 -344 rmoveto + 91 -54 23 -31 0 -61 0 -71 -33 -31 -81 -17 rrcurveto + endchar + + + -101 -14 28 634 28 hstemhm + 56 76 -70 74 219 69 -55 76 hintmask 11000100 + 445 155 rmoveto + 0 80 -32 54 -123 82 rrcurveto + hintmask 11011000 + 99 53 35 34 0 75 rrcurveto + 78 -69 65 -99 -111 -83 -60 -96 vhcurveto + 0 -65 21 -38 103 -85 rrcurveto + hintmask 11100100 + -107 -75 -23 -39 0 -69 rrcurveto + -94 79 -69 113 vhcurveto + 120 77 66 103 hvcurveto + hintmask 11011000 + -90 378 rmoveto + 0 -62 -26 -42 -68 -40 -88 52 -37 46 0 62 rrcurveto + 62 43 37 67 vhcurveto + 68 41 -46 -69 hvcurveto + hintmask 11100100 + -84 -261 rmoveto + 68 -46 30 -40 0 -62 rrcurveto + -65 -45 -45 -65 -76 -51 52 92 vhcurveto + 0 65 21 41 59 48 rrcurveto + endchar + + + 120 66 134 66 hstem + 637 320 rmoveto + -93 callsubr + 589 -200 rmoveto + -93 callsubr + endchar + + + -268 -9 106 559 20 hstem + 130 106 vstem + 175 176 rmoveto + -107 callsubr + 103 -515 rmoveto + -106 callsubr + endchar + + + -101 -14 37 560 79 hstem + 356 70 vstem + 438 681 rmoveto + -9 7 rlineto + -16 -21 -9 -5 -23 0 rrcurveto + -207 0 -109 -237 rlineto + 0 -1 -3 -2 -6 3 -2 9 hvcurveto + 96 0 70 -32 47 -38 45 -36 22 -50 0 -64 0 -86 -65 -83 -70 0 -20 0 -23 9 -28 23 -32 26 -19 5 -23 0 rrcurveto + -28 -17 -13 -25 -38 52 -24 75 hvcurveto + 68 0 55 15 47 34 68 50 30 62 0 96 0 53 -9 38 -26 36 -57 79 -50 22 -143 27 rrcurveto + 40 85 194 0 rlineto + 16 0 8 6 3 7 rrcurveto + endchar + + + -101 0 20 147 64 425 20 hstem + 292 78 vstem + 473 167 rmoveto + 64 -103 445 -44 vlineto + -314 -445 0 -64 280 0 0 -167 78 0 0 167 rlineto + -78 64 rmoveto + -240 0 240 343 rlineto + endchar + + + 621 249 rmoveto + -92 callsubr + endchar + + + -268 194 63 hstem + 285 194 rmoveto + 63 -246 -63 vlineto + endchar + + + 621 -24 rmoveto + -94 callsubr + endchar + + + -101 237 43 368 28 hstem + 30 92 vstem + 59 -22 rmoveto + 130 18 65 24 85 77 80 73 40 109 0 115 0 84 -24 72 -40 50 -39 48 -54 28 -64 0 rrcurveto + -119 -89 -101 -135 -122 72 -81 108 hvcurveto + 59 0 48 15 43 42 -40 -164 -112 -105 -152 -27 rrcurveto + 306 357 rmoveto + -53 -73 -22 -44 -75 -48 75 119 vhcurveto + 0 54 15 59 20 27 17 22 26 12 30 0 rrcurveto + 87 45 -86 -168 hvcurveto + endchar + + + -101 0 20 196 55 134 55 182 20 hstem + 495 405 rmoveto + 55 -96 vlineto + 30 202 -58 0 -30 -202 -133 0 31 202 -58 0 -31 -202 -117 0 0 -55 109 0 -21 -134 -115 0 0 -55 106 0 -33 -216 rlineto + 58 0 33 216 134 0 -31 -216 58 0 31 216 108 0 0 55 -99 0 20 134 rlineto + -58 hmoveto + -20 -134 -134 0 21 134 rlineto + endchar + + + -101 0 20 636 20 hstem + 213 86 vstem + 394 hmoveto + 15 vlineto + -75 -20 18 43 hvcurveto + 0 597 -9 3 -179 -91 0 -14 27 10 rlineto + 18 7 17 5 10 0 rrcurveto + 21 9 -15 -34 hvcurveto + -449 vlineto + 0 -55 -21 -21 -74 -4 rrcurveto + -15 vlineto + endchar + + + -268 656 20 hstem + 48 86 vstem + 304 -161 rmoveto + -140 117 -30 113 0 186 0 193 31 93 139 119 rrcurveto + -9 16 rlineto + -160 -95 -87 -144 0 -185 0 -170 86 -169 158 -90 rrcurveto + endchar + + + -133 139 81 vstem + 382 -134 rmoveto + -90 110 -72 169 0 306 0 303 72 172 90 110 rrcurveto + 30 vlineto + -142 -134 -101 -214 0 -267 0 -272 101 -209 142 -134 rrcurveto + endchar + + + -12 139 95 vstem + 503 -243 rmoveto + -134 165 -135 265 0 456 0 458 135 294 134 138 rrcurveto + 33 vlineto + -213 -171 -151 -352 0 -400 0 -409 151 -313 213 -200 rrcurveto + endchar + + + 149 182 110 vstem + 667 -346 rmoveto + -178 220 -197 349 0 613 0 606 197 396 178 184 rrcurveto + 44 vlineto + -284 -228 -201 -464 0 -538 0 -541 201 -422 284 -267 rrcurveto + endchar + + + 207 124 130 vstem + 732 -453 rmoveto + -224 232 -254 504 0 746 0 777 254 473 224 231 rrcurveto + 56 vlineto + -355 -286 -253 -578 0 -673 0 -675 253 -577 355 -286 rrcurveto + endchar + + + -268 656 20 hstem + 199 86 vstem + 29 660 rmoveto + 145 -114 25 -115 0 -187 0 -194 -28 -94 -142 -117 rrcurveto + 9 -16 rlineto + 159 97 88 142 0 185 0 170 -91 167 -153 92 rrcurveto + endchar + + + -133 248 81 vstem + 86 1036 rmoveto + 90 -110 72 -169 0 -306 0 -303 -72 -172 -90 -110 rrcurveto + -30 vlineto + 142 134 101 214 0 267 0 272 -101 209 -142 134 rrcurveto + endchar + + + 7 383 95 vstem + 114 1530 rmoveto + 134 -165 135 -262 0 -460 0 -454 -135 -297 -134 -138 rrcurveto + -33 vlineto + 213 171 151 351 0 400 0 409 -151 314 -213 200 rrcurveto + endchar + + + 149 458 110 vstem + 83 2018 rmoveto + 178 -217 197 -351 0 -614 0 -606 -197 -396 -178 -184 rrcurveto + -44 vlineto + 284 228 201 468 0 534 0 545 -201 418 -284 267 rrcurveto + endchar + + + 207 554 130 vstem + 76 2510 rmoveto + 224 -232 254 -482 0 -766 0 -757 -254 -495 -224 -231 rrcurveto + -56 vlineto + 355 286 253 585 0 667 0 681 -253 570 -355 286 rrcurveto + endchar + + + 146 -19 27 294 28 -4 27 230 26 47 20 hstemhm + 61 71 189 24 56 72 190 23 hintmask 1101111110000000 + 686 213 rmoveto + 74 -35 43 -61 -96 -93 -104 -107 -78 53 -60 68 vhcurveto + 86 78 111 121 hvcurveto + -23 2 rmoveto + -105 callsubr + -42 491 rmoveto + -104 callsubr + hintmask 0011011110000000 + -103 callsubr + 130 587 rmoveto + -102 callsubr + endchar + + + -351 -11 111 hstem + 70 111 vstem + 181 43 rmoveto + -99 callsubr + endchar + + + 220 66 hstem + 309 66 vstem + 636 220 rmoveto + -100 callsubr + endchar + + + -157 -8 106 349 20 179 30 hstem + 68 51 65 106 32 92 vstem + 244 164 rmoveto + -91 callsubr + 63 -119 rmoveto + -90 callsubr + endchar + + + -193 656 20 hstem + 299 431 rmoveto + 15 98 17 76 0 30 rrcurveto + 23 -18 18 -24 -25 -18 -18 -25 vhcurveto + 0 -17 14 -87 18 -98 rrcurveto + -148 hmoveto + 20 96 12 78 0 30 rrcurveto + 24 -18 17 -24 -25 -18 -17 -26 vhcurveto + 0 -17 14 -87 18 -98 rrcurveto + endchar + + + -421 656 20 hstem + 101 431 rmoveto + 20 100 12 75 0 30 rrcurveto + 23 -15 17 -27 -24 -19 -17 -25 vhcurveto + 0 -28 14 -77 18 -98 rrcurveto + endchar + + + -323 -6 20 -16 20 330 111 hstemhm + 80 111 -11 39 hintmask 10110000 + 191 403 rmoveto + -97 callsubr + hintmask 10101000 + -84 -544 rmoveto + -96 callsubr + hintmask 10110000 + -33 -26 -23 -33 -33 21 -19 37 hvcurveto + hintmask 01001000 + -95 callsubr + endchar + + + -101 -8 20 576 74 hstem + 449 646 rmoveto + 16 -369 vlineto + -60 -147 18 -9 rlineto + 42 68 17 14 58 0 rrcurveto + 215 0 -198 -596 65 0 rlineto + endchar + + + -101 -14 28 368 46 hstem + 34 93 251 90 vstem + 446 684 rmoveto + -138 -15 -79 -24 -86 -90 -71 -73 -38 -95 0 -108 0 -70 19 -71 28 -51 35 -64 63 -37 79 0 66 0 56 27 37 46 33 40 18 56 0 64 rrcurveto + 129 -72 80 -117 vhcurveto + -44 0 -34 -7 -49 -38 27 151 112 108 157 26 rrcurveto + -70 -480 rmoveto + -102 -37 -72 -72 -94 -48 100 152 92 59 24 57 93 42 -66 -128 vhcurveto + endchar + + + -323 -14 20 650 20 hstem + 287 676 rmoveto + -67 0 -229 -690 68 0 rlineto + endchar + + + -22 552 1066 rmoveto + -73 0 -454 -1230 73 0 rlineto + endchar + + + 205 781 1566 rmoveto + 22 callsubr + endchar + + + 500 1071 2066 rmoveto + -103 0 -938 -2460 103 0 rlineto + endchar + + + 708 1293 2566 rmoveto + -104 0 -1173 -3075 104 0 rlineto + endchar + + + -101 -14 36 594 60 hstemhm + 318 79 -38 72 hintmask 11100000 + 61 510 rmoveto + 41 74 46 32 62 0 66 0 42 -34 0 -68 0 -61 -32 -45 -49 -28 -20 -12 -26 -11 -38 -13 rrcurveto + -14 vlineto + 57 0 23 -3 22 -7 rrcurveto + hintmask 11010000 + 69 -20 35 -49 0 -76 0 -85 -56 -68 -74 0 -28 0 -21 5 -37 26 -28 20 -16 6 -17 0 rrcurveto + -23 -18 -15 -21 -36 39 -21 73 hvcurveto + 89 0 95 29 48 64 29 38 17 49 0 53 0 52 -16 46 -28 31 -21 22 -18 12 -44 19 rrcurveto + hintmask 11100000 + 67 40 26 50 0 48 0 82 -63 55 -93 0 -104 0 -63 -67 -29 -95 rrcurveto + endchar + + + -101 0 76 526 74 hstem + 337 86 vstem + 474 137 rmoveto + -14 6 rlineto + -33 -56 -21 -11 -42 0 rrcurveto + -234 0 165 176 rlineto + 89 94 39 75 0 79 0 99 -72 77 -113 0 -123 0 -64 -82 -21 -117 rrcurveto + 21 -5 rlineto + 40 98 35 32 72 0 85 0 54 -50 0 -91 0 -85 -36 -76 -94 -99 rrcurveto + -178 -189 0 -12 391 0 rlineto + endchar + + + 121 0 20 177 39 hstem + 689 hmoveto + -22 callsubr + 17 236 rmoveto + -195 0 94 243 rlineto + endchar + + + 66 0 32 614 30 hstem + 426 365 rmoveto + -21 callsubr + -162 228 rmoveto + -20 callsubr + -30 vmoveto + -19 callsubr + endchar + + + 121 -19 48 613 20 -4 33 hstemhm + hintmask 10100000 + 657 152 rmoveto + -44 -46 -28 -25 -32 -18 -40 -22 -42 -12 -44 0 -59 0 -59 30 -29 46 -42 66 -12 73 0 101 0 200 79 113 105 0 67 0 54 -35 46 -55 rrcurveto + 24 -28 24 -33 19 -54 rrcurveto + 25 235 -27 hlineto + hintmask 01000000 + -18 callsubr + hintmask 10100000 + -58 24 -50 11 -48 0 rrcurveto + -204 -143 -156 -212 -195 135 -147 215 hvcurveto + 115 0 80 31 93 115 rrcurveto + endchar + + + 121 0 35 607 34 hstem + 14 676 rmoveto + -25 vlineto + 64 -6 19 -15 0 -46 rrcurveto + -486 vlineto + 0 -48 -25 -23 -58 -2 rrcurveto + -25 309 vlineto + 101 0 90 28 61 53 72 63 43 98 0 103 rrcurveto + 206 -136 125 -228 vhcurveto + -67 -78 rmoveto + 28 14 16 39 vhcurveto + 75 0 50 -35 38 -66 29 -51 12 -74 0 -86 0 -88 -19 -90 -31 -45 -34 -48 -49 -24 -66 0 rrcurveto + -45 -13 20 45 hvcurveto + endchar + + + 66 0 31 611 34 hstem + 641 208 rmoveto + -17 callsubr + endchar + + + 10 0 20 622 34 hstem + 583 474 rmoveto + 202 -567 -25 vlineto + 69 -4 19 -14 0 -58 rrcurveto + -472 vlineto + 0 -56 -14 -14 -74 -8 rrcurveto + -25 360 25 vlineto + -92 4 -18 15 0 57 rrcurveto + 228 vlineto + 102 -2 41 -35 10 -118 rrcurveto + 25 338 -25 hlineto + -14 -113 -39 -36 -100 0 rrcurveto + 228 vlineto + 37 19 14 51 vhcurveto + 88 0 44 -12 31 -27 35 -30 11 -23 14 -76 rrcurveto + endchar + + + 177 -19 33 628 20 -4 33 hstemhm + hintmask 10100000 + 755 287 rmoveto + -344 -25 hlineto + 86 -5 16 -17 0 -52 rrcurveto + -89 vlineto + -51 -25 -34 -71 vhcurveto + -77 0 -43 29 -35 58 -33 55 -15 79 0 103 0 207 68 113 116 0 47 0 48 -18 43 -38 43 -38 18 -35 35 -76 rrcurveto + 25 235 -27 hlineto + hintmask 01000000 + -18 callsubr + hintmask 10100000 + -58 24 -45 11 -52 0 rrcurveto + -200 -148 -161 -199 -214 154 -136 201 hvcurveto + 104 0 108 25 65 38 rrcurveto + 118 vlineto + 0 63 11 29 75 8 rrcurveto + endchar + + + 177 0 20 306 47 283 20 hstem + 759 hmoveto + -16 callsubr + endchar + + + -212 0 20 636 20 hstem + 370 hmoveto + -15 callsubr + endchar + + + -101 -96 33 719 20 hstem + 478 676 rmoveto + -351 -25 hlineto + 81 -3 20 -13 0 -58 rrcurveto + -550 vlineto + -62 -18 -28 -46 -28 -17 12 20 vhcurveto + 0 26 27 10 0 30 rrcurveto + 40 -34 36 -38 -37 -34 -37 -37 vhcurveto + 0 -42 30 -43 37 -25 25 -17 43 -6 40 0 rrcurveto + 139 73 71 151 hvcurveto + 430 vlineto + 0 72 13 18 75 5 rrcurveto + endchar + + + 177 0 20 636 20 hstem + 769 hmoveto + -14 callsubr + endchar + + + 66 0 31 625 20 hstem + 638 227 rmoveto + -29 hlineto + -27 -62 -26 -48 -34 -31 -41 -37 -46 -18 -83 0 rrcurveto + -65 -20 15 43 hvcurveto + 464 vlineto + 0 82 13 13 87 4 rrcurveto + 24 -348 -25 vlineto + 68 -4 18 -14 0 -57 rrcurveto + -478 vlineto + 0 -53 -13 -11 -73 -9 rrcurveto + -25 577 vlineto + endchar + + + 343 0 20 636 20 hstem + 105 42 vstem + 921 hmoveto + -13 callsubr + endchar + + + 121 -18 20 -2 20 636 20 hstemhm + 104 44 431 44 hintmask 01111000 + 701 676 rmoveto + -12 callsubr + hintmask 10111000 + -11 callsubr + endchar + + + 177 -19 33 644 33 hstem + 743 335 rmoveto + -10 callsubr + -177 -7 rmoveto + -9 callsubr + endchar + + + 10 0 20 621 35 hstem + 16 676 rmoveto + -8 callsubr + -69 -70 rmoveto + -7 callsubr + endchar + + + 177 -176 47 787 33 hstem + 730 -117 rmoveto + -33 -10 -17 -2 -20 0 -47 0 -42 18 -34 34 -19 19 -10 15 -18 37 158 48 95 114 0 171 rrcurveto + 220 -148 144 -206 -205 -149 -144 -217 vhcurveto + 0 -155 85 -131 153 -46 21 -44 12 -19 23 -23 56 -56 84 -32 88 0 67 0 47 10 65 25 rrcurveto + -170 476 rmoveto + -205 -61 -116 -116 -116 -61 114 207 209 64 114 112 114 64 -111 -212 vhcurveto + endchar + + + 121 0 20 622 34 hstem + 716 hmoveto + 25 vlineto + -18 0 -17 9 -9 13 rrcurveto + -199 282 rlineto + 58 16 26 16 28 25 29 27 16 46 0 45 rrcurveto + 115 -101 57 -195 vhcurveto + -308 -25 hlineto + 74 -5 14 -17 0 -78 rrcurveto + -431 vlineto + 0 -79 -10 -7 -78 -9 rrcurveto + -25 338 25 vlineto + -78 10 -10 9 0 73 rrcurveto + 196 28 vlineto + 207 -313 rlineto + -235 597 rmoveto + 33 14 12 39 93 37 -42 -106 vhcurveto + 0 -122 -52 -26 -131 -1 rrcurveto + endchar + + + -45 -19 33 646 31 hstem + 484 474 rmoveto + 218 -30 vlineto + -7 -27 -7 -6 -18 0 -9 0 -12 2 -21 7 -46 17 -32 6 -39 0 -136 0 -83 -74 0 -127 0 -101 58 -55 113 -54 43 -21 45 -20 34 -24 rrcurveto + 34 -24 23 -28 0 -36 0 -69 -49 -44 -77 0 -58 0 -48 24 -38 48 -29 37 -14 35 -17 71 rrcurveto + -29 -248 29 hlineto + 6 26 8 8 16 0 8 0 11 -3 22 -7 49 -17 37 -7 44 0 148 0 100 87 0 126 0 76 -45 65 -64 35 -51 28 -48 23 -49 26 rrcurveto + -80 42 -23 23 0 49 0 57 41 42 69 0 45 0 42 -19 36 -37 34 -35 16 -30 20 -65 rrcurveto + endchar + + + 66 0 20 636 20 hstem + 636 475 rmoveto + -6 callsubr + endchar + + + 121 -19 49 626 20 hstem + 579 44 vstem + 701 676 rmoveto + -219 -25 hlineto + 74 -6 23 -20 0 -73 rrcurveto + -300 vlineto + -148 -59 -74 -117 -97 -49 63 139 vhcurveto + 318 vlineto + 0 82 17 13 78 6 rrcurveto + 25 -336 -25 vlineto + 70 -7 12 -8 0 -81 rrcurveto + -324 vlineto + 0 -97 23 -54 57 -45 47 -37 64 -17 74 0 71 0 70 20 43 35 49 39 27 77 0 100 rrcurveto + 314 vlineto + 0 59 14 16 64 10 rrcurveto + endchar + + + 121 -18 20 654 20 hstem + 701 676 rmoveto + -213 -25 hlineto + 71 -5 15 -7 0 -32 0 -16 -3 -11 -17 -44 rrcurveto + -127 -329 -138 334 rlineto + -20 48 -3 9 0 15 0 23 15 11 38 2 rrcurveto + 33 2 0 25 -336 0 0 -25 rlineto + 50 -7 10 -7 24 -56 rrcurveto + 256 -599 27 0 228 587 rlineto + 24 62 14 13 52 7 rrcurveto + endchar + + + 399 -15 20 651 20 hstem + 981 676 rmoveto + -182 -25 hlineto + 54 -3 15 -10 0 -31 0 -13 -2 -15 -5 -14 rrcurveto + -112 -343 -108 336 rlineto + -11 34 -3 12 0 10 0 24 15 9 44 3 rrcurveto + 13 1 0 25 -312 0 0 -25 rlineto + 41 -2 22 -8 9 -25 rrcurveto + 35 -96 -118 -308 -120 364 rlineto + -6 17 -1 7 0 9 0 29 12 9 52 4 rrcurveto + 25 -294 -25 vlineto + 42 -6 10 -9 17 -49 rrcurveto + 212 -602 28 0 186 477 171 -477 27 0 200 602 rlineto + 13 40 23 21 33 3 rrcurveto + endchar + + + 121 0 20 636 20 hstem + 699 hmoveto + -5 callsubr + endchar + + + 121 0 20 636 20 hstem + 699 676 rmoveto + -220 -25 hlineto + 68 -5 16 -8 0 -29 0 -20 -15 -39 -26 -43 rrcurveto + -109 -178 -120 232 rlineto + -12 23 -12 27 0 9 0 23 14 3 40 3 rrcurveto + 27 2 0 25 -335 0 0 -25 rlineto + 34 -2 30 -23 19 -34 rrcurveto + 180 -328 0 -136 rlineto + 0 -75 -10 -21 -83 -7 rrcurveto + -25 347 25 vlineto + -79 7 -13 13 0 78 rrcurveto + 0 178 191 311 rlineto + 14 23 20 11 34 5 rrcurveto + endchar + + + 66 0 35 621 20 hstem + 634 242 rmoveto + -26 hlineto + -29 -85 -13 -42 -56 -40 -41 -29 -66 -11 -95 0 rrcurveto + -83 0 379 625 0 16 -523 0 -22 -207 28 0 rlineto + 45 143 41 24 154 2 rrcurveto + 83 1 -382 -623 0 -16 579 0 rlineto + endchar + + + -101 -14 20 435 32 hstem + 473 64 rmoveto + -10 -10 rlineto + -3 -3 -3 -1 -5 0 rrcurveto + -14 -7 11 17 hvcurveto + 261 vlineto + 86 -74 48 -122 -113 -78 -46 -80 -42 24 -26 41 40 28 26 34 vhcurveto + 0 14 -6 12 -13 16 -9 10 -2 7 0 6 rrcurveto + 24 29 13 36 59 22 -29 -64 vhcurveto + -68 vlineto + -116 -33 -47 -18 -38 -25 -45 -30 -22 -38 0 -44 0 -74 46 -32 64 0 58 0 46 19 55 50 11 -51 22 -18 49 0 43 0 31 16 38 41 rrcurveto + -195 54 rmoveto + -22 -31 -24 -9 -24 0 -30 0 -22 23 0 44 0 58 42 41 80 22 rrcurveto + endchar + + + -45 -14 32 401 54 183 20 hstem + 211 676 rmoveto + -194 -24 hlineto + 46 -9 9 -9 0 -40 rrcurveto + -607 12 vlineto + 79 56 rlineto + 46 -42 36 -15 50 0 rrcurveto + 134 92 103 150 139 -77 95 -111 hvcurveto + -49 0 -35 -16 -38 -40 rrcurveto + -57 vmoveto + 18 43 19 16 33 0 rrcurveto + 62 31 -66 -132 -139 -30 -64 -64 -42 -27 31 48 hvcurveto + endchar + + + -157 -14 67 389 31 hstem + 412 109 rmoveto + -37 -42 -29 -14 -41 0 rrcurveto + -87 -52 84 136 109 35 60 49 42 0 -23 -47 -48 22 -25 34 42 26 25 39 64 -67 46 -87 -133 -104 -102 -149 -143 89 -93 125 hvcurveto + 80 0 56 30 55 75 rrcurveto + endchar + + + -45 -14 56 -42 20 397 56 183 20 hstemhm + hintmask 01110000 + 534 20 rmoveto + 23 vlineto + -46 3 -13 13 0 42 rrcurveto + 575 -215 -24 vlineto + 67 -5 9 -7 0 -46 rrcurveto + -183 vlineto + -43 46 -30 16 -46 0 rrcurveto + -110 -82 -107 -145 hvcurveto + hintmask 10000000 + -137 76 -98 105 vhcurveto + 53 0 33 16 47 50 rrcurveto + -65 vlineto + 46 13 25 4 62 8 rrcurveto + hintmask 10100000 + -136 100 rmoveto + 0 -5 -9 -15 -10 -13 -20 -25 -23 -12 -23 0 rrcurveto + -53 -25 60 127 129 27 59 58 hvcurveto + 33 0 30 -24 15 -38 rrcurveto + endchar + + + -157 -14 72 187 37 160 31 hstem + 403 126 rmoveto + -41 -49 -31 -19 -43 0 -98 0 -15 92 -6 95 rrcurveto + 252 hlineto + -4 74 -18 63 -37 41 -29 32 -41 18 -58 0 rrcurveto + -125 -84 -100 -149 -137 83 -101 121 hvcurveto + 85 0 53 31 60 95 rrcurveto + -129 170 rmoveto + -134 hlineto + 120 24 40 46 vhcurveto + 29 0 12 -17 14 -30 9 -19 0 -21 0 -52 rrcurveto + endchar + + + -268 0 20 397 44 199 31 hstem + 14 461 rmoveto + -44 57 -330 vlineto + 0 -45 -11 -13 -46 -5 rrcurveto + -24 278 24 vlineto + -70 2 -12 16 0 65 rrcurveto + 310 86 44 -86 120 vlineto + 59 14 20 36 18 11 -7 -12 vhcurveto + 0 -14 -22 -8 0 -35 rrcurveto + -31 26 -26 34 37 25 27 37 59 -58 41 -85 vhcurveto + -64 0 -44 -18 -27 -33 -33 -40 -7 -60 0 -79 rrcurveto + endchar + + + -101 -206 32 571 53 -8 30 hstemhm + 28 88 288 79 hintmask 01011000 + 254 68 rmoveto + -80 -21 10 27 30 30 19 39 hvcurveto + 91 0 3 0 33 14 61 25 31 43 0 63 0 40 -12 33 -28 25 rrcurveto + 80 53 -129 hlineto + hintmask 10111000 + -44 16 -28 6 -40 0 -119 0 -84 -62 0 -99 0 -76 53 -55 71 -20 -80 -23 -39 -35 0 -59 0 -37 18 -22 64 -23 -63 -9 -33 -25 0 -41 rrcurveto + -60 68 -32 126 167 94 60 94 74 -69 46 -108 vhcurveto + 244 vmoveto + -94 -18 -36 -48 -48 -18 36 93 98 18 33 47 48 19 -37 -93 vhcurveto + 9 -364 rmoveto + 65 24 -22 -34 -38 -55 -28 -99 -88 -46 23 44 hvcurveto + 0 21 7 11 28 23 rrcurveto + endchar + + + -45 0 20 386 67 183 20 hstem + 534 hmoveto + 24 vlineto + -35 -14 17 46 hvcurveto + 250 vlineto + 79 -48 57 -82 vhcurveto + -53 0 -45 -19 -49 -58 rrcurveto + 280 -193 -24 vlineto + 46 -9 8 -4 0 -48 rrcurveto + -502 vlineto + 0 -56 -8 0 -45 -9 rrcurveto + -24 240 24 vlineto + -37 5 -11 15 0 41 rrcurveto + 263 vlineto + 0 4 7 10 10 10 22 22 24 12 24 0 rrcurveto + 39 12 -26 -71 hvcurveto + -224 vlineto + 0 -41 -12 -16 -34 -4 rrcurveto + -24 vlineto + endchar + + + -323 0 20 516 155 hstem + 60 155 vstem + 215 613 rmoveto + 43 -34 35 -42 -45 -34 -34 -44 -44 33 -33 44 44 34 33 44 vhcurveto + 41 -613 rmoveto + -4 callsubr + endchar + + + -268 -203 31 708 155 hstem + 108 155 vstem + 263 613 rmoveto + 42 -35 36 -42 -44 -34 -34 -44 -44 32 -33 44 45 34 33 44 vhcurveto + -3 -152 rmoveto + -3 callsubr + endchar + + + -45 0 20 636 20 hstem + 543 hmoveto + 24 vlineto + -11 0 -5 3 -9 12 rrcurveto + -194 268 rlineto + 100 105 26 18 63 8 rrcurveto + 23 -214 -23 vlineto + 24 -4 rlineto + 26 -4 9 -6 0 -13 0 -9 -10 -18 -11 -11 rrcurveto + -128 -128 0 431 -187 0 0 -24 rlineto + 34 -3 14 -17 0 -38 rrcurveto + -510 vlineto + 0 -39 -15 -18 -33 -3 rrcurveto + -24 239 24 vlineto + -47 7 -5 6 0 47 rrcurveto + 0 114 23 24 95 -134 rlineto + 18 -25 6 -12 0 -8 0 -12 -14 -6 -28 -1 rrcurveto + -24 vlineto + endchar + + + -323 0 20 636 20 hstem + 256 hmoveto + 24 vlineto + -33 1 -17 16 0 46 rrcurveto + 589 -191 -24 vlineto + 35 -3 17 -25 0 -37 rrcurveto + -500 vlineto + 0 -44 -17 -16 -34 -3 rrcurveto + -24 vlineto + endchar + + + 232 0 20 386 67 hstem + 814 hmoveto + 24 vlineto + -39 2 -11 14 0 43 rrcurveto + 248 vlineto + 89 -53 53 -82 vhcurveto + -57 0 -38 -23 -52 -64 -30 63 -35 24 -63 0 -64 0 -42 -26 -38 -61 rrcurveto + -3 75 -192 -24 hlineto + 44 -6 12 -11 0 -47 rrcurveto + -287 vlineto + 0 -41 -11 -13 -44 -8 rrcurveto + -24 239 24 vlineto + -35 5 -10 16 0 38 rrcurveto + 261 vlineto + 24 61 38 23 39 15 -23 -60 vhcurveto + -240 vlineto + 0 -41 -10 -15 -37 -3 rrcurveto + -24 232 24 vlineto + -35 4 -11 17 0 38 rrcurveto + 265 vlineto + 0 5 14 18 13 10 21 18 19 7 18 0 rrcurveto + 38 15 -33 -62 hvcurveto + -228 vlineto + 0 -42 -10 -14 -38 -3 rrcurveto + -24 vlineto + endchar + + + -45 0 20 386 67 hstem + 539 hmoveto + 24 vlineto + -39 3 -10 12 0 45 rrcurveto + 247 vlineto + 87 -53 55 -82 vhcurveto + -60 0 -48 -27 -34 -60 rrcurveto + -1 75 -191 -24 hlineto + 44 -7 9 -11 0 -43 rrcurveto + -289 vlineto + 0 -42 -8 -12 -45 -9 rrcurveto + -24 240 24 vlineto + -37 5 -11 18 0 37 rrcurveto + 264 vlineto + 0 4 7 10 10 10 22 22 24 12 24 0 rrcurveto + 34 17 -29 -56 hvcurveto + -237 vlineto + 0 -40 -12 -16 -34 -4 rrcurveto + -24 vlineto + endchar + + + -101 -14 31 425 31 hstem + 476 229 rmoveto + -2 callsubr + -147 -11 rmoveto + -1 callsubr + endchar + + + -45 -13 55 374 57 hstem + 212 461 rmoveto + -192 -24 hlineto + 44 -7 11 -14 0 -44 rrcurveto + -490 vlineto + 0 -42 -8 -13 -48 -8 rrcurveto + -24 272 24 vlineto + -61 3 -16 32 0 67 rrcurveto + 126 vlineto + 48 -47 25 -13 45 0 rrcurveto + 120 72 107 155 147 -70 77 -111 hvcurveto + -59 0 -38 -21 -34 -60 rrcurveto + 2 -46 rmoveto + 0 6 7 14 11 13 20 24 25 13 23 0 rrcurveto + 51 25 -59 -123 -138 -30 -54 -55 hvcurveto + -33 0 -29 27 -15 38 rrcurveto + endchar + + + -45 -14 54 401 32 hstem + 536 -205 rmoveto + 24 vlineto + -46 9 -9 9 0 40 rrcurveto + 595 -16 vlineto + -77 -55 rlineto + -51 43 -31 13 -50 0 rrcurveto + -135 -87 -114 -140 -145 82 -88 103 hvcurveto + 48 0 40 11 35 45 rrcurveto + -132 vlineto + 0 -66 -8 -16 -65 -9 rrcurveto + -24 vlineto + 73 303 rmoveto + -38 -35 -20 -34 -54 -39 60 136 142 39 63 56 40 27 -32 -55 vhcurveto + endchar + + + -157 0 20 433 20 hstem + 218 461 rmoveto + -190 -24 hlineto + 43 -6 12 -11 0 -47 rrcurveto + -289 vlineto + 0 -41 -10 -11 -44 -8 rrcurveto + -24 265 24 vlineto + -61 4 -11 14 0 64 rrcurveto + 177 vlineto + 59 26 45 35 vhcurveto + 8 0 9 -5 11 -18 17 -28 17 -10 26 0 rrcurveto + 37 26 28 40 46 -34 33 -47 hvcurveto + -50 0 -38 -26 -47 -68 rrcurveto + endchar + + + -212 -14 34 411 20 -11 33 hstemhm + hintmask 01000000 + 340 326 rmoveto + 145 -22 vlineto + -6 -15 -6 -5 -13 0 -6 0 -9 2 -16 5 rrcurveto + hintmask 10100000 + -32 11 -23 4 -23 0 -91 0 -66 -62 0 -84 0 -66 41 -46 101 -43 69 -30 27 -25 0 -32 0 -39 -30 -26 -45 0 -70 0 -46 45 -21 87 rrcurveto + -28 -165 25 hlineto + 11 21 6 7 9 0 5 0 8 -2 10 -4 29 -12 51 -11 28 0 91 0 63 62 0 90 0 71 -38 43 -100 42 -68 28 -28 26 0 34 rrcurveto + 33 28 25 38 vhcurveto + 27 0 26 -11 22 -21 21 -20 11 -19 15 -43 rrcurveto + endchar + + + -268 -12 71 358 44 hstem + 307 112 rmoveto + -20 -38 -16 -15 -21 0 rrcurveto + -28 -11 20 40 hvcurveto + 298 95 44 -95 169 -25 vlineto + -58 -84 -37 -53 -72 -49 rrcurveto + -27 53 -322 vlineto + -67 43 -40 69 vhcurveto + 67 0 40 31 41 82 rrcurveto + endchar + + + -45 -14 65 -51 20 421 20 hstemhm + hintmask 01100000 + 538 20 rmoveto + 23 vlineto + -44 2 -13 16 0 44 rrcurveto + 356 -200 -24 vlineto + 50 -4 11 -14 0 -45 rrcurveto + -276 vlineto + hintmask 10100000 + -31 -34 -22 -13 -29 0 rrcurveto + -36 -20 20 56 hvcurveto + 334 -188 -24 vlineto + 41 -8 8 -11 0 -46 rrcurveto + -241 vlineto + -93 50 -52 83 vhcurveto + 55 0 38 17 52 49 rrcurveto + -65 vlineto + 43 15 24 4 65 7 rrcurveto + endchar + + + -101 -14 20 435 20 hstem + 485 461 rmoveto + -151 -24 hlineto + 43 -2 12 -7 0 -24 0 -12 -3 -14 -8 -19 rrcurveto + -72 -182 -79 203 rlineto + -8 20 -1 4 0 7 0 15 10 6 25 3 rrcurveto + 18 2 0 24 -250 0 0 -24 rlineto + 22 -3 6 -3 6 -8 9 -12 37 -80 20 -49 rrcurveto + 120 -296 26 0 160 396 rlineto + 19 46 8 6 31 3 rrcurveto + endchar + + + 121 -14 20 435 20 hstem + 707 461 rmoveto + -135 -24 hlineto + 37 -4 11 -8 0 -23 0 -13 -15 -37 -29 -78 rrcurveto + -35 -94 rlineto + -10 42 -4 15 -20 70 -20 68 -7 28 0 10 0 16 10 5 38 3 rrcurveto + 24 -234 -24 vlineto + 39 -4 1 -1 19 -66 2 -7 2 -6 2 -6 rrcurveto + -68 -171 -45 118 rlineto + -27 71 -12 31 0 13 0 17 10 7 28 4 rrcurveto + 24 -222 -24 vlineto + 26 -5 6 -10 25 -62 rrcurveto + 148 -374 24 0 125 310 102 -310 23 0 155 401 rlineto + 13 34 11 11 26 5 rrcurveto + endchar + + + -101 0 20 421 20 hstem + 484 hmoveto + 24 vlineto + -16 5 -7 4 -7 11 rrcurveto + -148 228 101 126 rlineto + 19 23 20 11 31 5 rrcurveto + 24 -168 -24 vlineto + 20 -2 rlineto + 23 -2 8 -5 0 -15 0 -15 -11 -14 -27 -35 rrcurveto + -36 -46 rlineto + -4 6 -5 6 -4 5 -33 43 -25 42 0 13 0 12 14 6 33 1 rrcurveto + 24 -250 -24 vlineto + 26 -4 6 -5 20 -30 rrcurveto + 128 -197 rlineto + -15 -18 8 9 -15 -19 -15 -19 -14 -18 -14 -19 -56 -75 -20 -16 -37 -2 rrcurveto + -24 169 24 vlineto + -36 2 -14 7 0 15 0 16 25 40 39 47 6 7 5 7 5 6 13 -21 14 -21 15 -21 24 -35 9 -17 0 -12 0 -12 -13 -6 -32 -2 rrcurveto + -24 vlineto + endchar + + + -101 -205 57 589 20 hstem + 482 461 rmoveto + -152 -24 hlineto + 43 -2 11 -7 0 -24 0 -12 -2 -11 -9 -26 rrcurveto + -68 -188 -72 186 rlineto + -11 28 -9 26 0 4 0 15 11 8 25 2 rrcurveto + 16 1 0 24 -249 0 0 -24 rlineto + 22 -3 6 -3 6 -8 9 -12 38 -84 20 -50 rrcurveto + 119 -290 -18 -53 rlineto + -17 -50 -25 -32 -24 0 -9 0 -8 8 0 9 0 0 0 3 1 3 1 5 1 5 0 4 rrcurveto + 29 -26 23 -34 -38 -24 -29 -38 -47 40 -32 57 vhcurveto + 34 0 29 11 21 22 21 23 21 39 35 94 rrcurveto + 148 398 rlineto + 16 43 13 8 31 4 rrcurveto + endchar + + + -157 0 20 421 20 hstem + 420 160 rmoveto + -28 hlineto + -9 -32 -8 -16 -14 -21 -33 -46 -33 -13 -81 0 rrcurveto + -29 0 231 403 0 26 -371 0 -7 -142 26 0 rlineto + 25 95 24 15 142 0 rrcurveto + -234 -404 0 -25 383 0 rlineto + endchar + + + 116 0 20 191 37 399 20 hstem + 685 hmoveto + 0 callsubr + 66 248 rmoveto + 1 callsubr + endchar + + + 95 0 38 280 35 261 39 hstem + 198 653 rmoveto + 2 callsubr + -44 -42 rmoveto + 3 callsubr + -9 -45 rmoveto + 4 callsubr + endchar + + + 70 -12 48 586 37 hstem + 711 659 rmoveto + -18 hlineto + -15 -25 -16 -2 -11 0 -52 0 -52 27 -54 0 rrcurveto + -242 -201 -163 -230 -147 95 -131 193 hvcurveto + 39 0 76 6 78 55 29 21 33 30 27 41 rrcurveto + -20 13 rlineto + -5 -7 -5 -6 -7 -7 -45 -49 -73 -49 -98 0 rrcurveto + -135 -60 102 112 171 118 201 201 121 31 -79 -65 hvcurveto + 0 -25 0 -9 -1 -8 rrcurveto + 16 hlineto + endchar + + + 189 0 20 595 38 hstem + 194 653 rmoveto + -5 -17 25 0 rlineto + 59 18 -15 -22 hvcurveto + 0 -7 -2 -9 -2 -8 rrcurveto + -122 -497 rlineto + -8 -34 -23 -28 -74 0 rrcurveto + -18 0 -4 -16 306 0 rlineto + 258 163 167 211 172 -101 103 -188 hvcurveto + -71 -41 rmoveto + 16 2 23 1 20 0 rrcurveto + 147 33 -137 -115 -141 -79 -184 -192 -35 -60 2 41 hvcurveto + 0 3 0 5 5 19 rrcurveto + endchar + + + 113 0 38 295 36 248 36 hstem + 734 653 rmoveto + 5 callsubr + endchar + + + 17 0 20 312 36 248 37 hstem + 723 653 rmoveto + -520 0 -4 -15 19 0 rlineto + 60 16 -18 -21 hvcurveto + 0 -7 -2 -9 -2 -8 rrcurveto + -124 -497 rlineto + -9 -34 -21 -28 -76 0 rrcurveto + -18 0 -4 -16 342 0 4 16 -32 0 rlineto + -60 -19 16 25 hvcurveto + 0 7 3 8 1 6 rrcurveto + 62 254 97 0 rlineto + 29 41 -3 -47 hvcurveto + 0 -18 -2 -29 -3 -14 rrcurveto + 16 0 60 242 -16 0 rlineto + -21 -85 -46 -10 -62 0 rrcurveto + -83 0 62 248 147 0 rlineto + 47 0 60 -9 3 -78 0 -14 0 -14 -1 -8 rrcurveto + 16 hlineto + endchar + + + 133 -12 37 606 37 hstem + 734 329 rmoveto + -313 0 -4 -16 19 0 rlineto + 61 14 -15 -22 hvcurveto + 0 -7 -1 -10 -2 -8 rrcurveto + -57 -201 rlineto + -27 -17 -29 -8 -41 0 rrcurveto + -147 -34 97 82 179 89 248 229 113 47 -81 -73 hvcurveto + 0 -11 -1 -11 -1 -10 rrcurveto + 16 0 45 223 -16 0 rlineto + -3 -16 -18 -21 -22 0 -22 0 -19 15 -22 6 -22 7 -32 9 -49 0 rrcurveto + -253 -182 -216 -210 -179 135 -75 157 hvcurveto + 63 0 75 0 79 47 rrcurveto + 62 216 rlineto + 10 36 18 26 85 0 rrcurveto + endchar + + + 272 0 20 300 37 hstem + 923 653 rmoveto + 6 callsubr + endchar + + + -121 0 20 hstem + 530 653 rmoveto + 7 callsubr + endchar + + + -61 -12 37 hstem + 620 653 rmoveto + -329 0 -4 -16 20 0 rlineto + 57 18 -15 -24 hvcurveto + 0 -8 -80 -325 -37 -144 -7 -27 -18 -69 -49 0 -11 0 -13 6 0 15 0 27 27 0 0 32 rrcurveto + 31 -33 20 -31 -50 -20 -34 -37 -60 57 -37 69 vhcurveto + 25 0 45 3 39 25 40 27 33 43 19 75 rrcurveto + 103 413 rlineto + 9 36 25 27 74 0 rrcurveto + 18 hlineto + endchar + + + 161 0 20 hstem + 802 653 rmoveto + 8 callsubr + endchar + + + 107 0 38 hstem + 668 190 rmoveto + -16 hlineto + -5 -14 -5 -12 -7 -11 -46 -93 -51 -22 -102 0 rrcurveto + -122 hlineto + -12 -28 2 22 hvcurveto + 0 2 1 14 4 15 rrcurveto + 122 481 rlineto + 9 37 21 26 75 0 rrcurveto + 18 0 4 16 -327 0 -4 -16 18 0 rlineto + 62 14 -18 -19 hvcurveto + 0 -9 -3 -20 -4 -16 rrcurveto + -118 -477 rlineto + -8 -34 -24 -28 -75 0 rrcurveto + -17 0 -4 -16 569 0 rlineto + endchar + + + 404 0 20 hstem + 1055 653 rmoveto + 9 callsubr + endchar + + + 250 0 20 hstem + 901 653 rmoveto + 10 callsubr + endchar + + + 131 -11 38 605 37 hstem + 712 420 rmoveto + 11 callsubr + -116 23 rmoveto + 12 callsubr + endchar + + + -7 0 20 274 37 285 37 hstem + 201 653 rmoveto + 13 callsubr + -61 -40 rmoveto + 14 callsubr + endchar + + + 180 -152 64 718 37 hstem + 83 -115 rmoveto + 11 -12 rlineto + 27 13 23 5 8 0 30 0 54 -16 50 -11 37 -8 52 -8 53 0 118 0 70 46 59 73 rrcurveto + -13 11 rlineto + -50 -53 -47 -13 -39 0 -87 0 -93 34 -65 0 -13 0 -14 -1 -16 -3 rrcurveto + -4 5 64 43 rlineto + 245 188 226 204 155 -93 92 -146 -254 -188 -241 -204 hvcurveto + 0 -132 83 -81 114 -17 rrcurveto + 365 475 rmoveto + -157 -94 -283 -217 -80 -51 52 97 138 90 316 222 65 65 -31 -132 vhcurveto + endchar + + + 139 0 20 293 36 267 37 hstem + 725 16 rmoveto + -57 5 -26 42 -19 39 rrcurveto + -115 229 rlineto + 107 21 71 67 0 83 rrcurveto + 85 -56 66 -160 vhcurveto + -271 0 -4 -16 20 0 rlineto + 60 17 -16 -21 hvcurveto + 0 -8 -3 -9 -2 -8 rrcurveto + -124 -497 rlineto + -9 -34 -20 -28 -76 0 rrcurveto + -16 0 -4 -16 325 0 4 16 -19 0 rlineto + -58 -18 15 24 hvcurveto + 0 8 1 7 2 8 rrcurveto + 62 239 rlineto + 14 -2 10 -2 14 0 8 0 11 1 10 1 rrcurveto + 150 -315 169 0 rlineto + -313 613 rmoveto + 11 2 9 1 14 0 rrcurveto + 63 62 -17 -108 -84 -58 -58 -133 hvcurveto + -9 0 -10 1 -13 3 rrcurveto + endchar + + + 49 -10 38 603 37 hstem + 680 668 rmoveto + -16 hlineto + -3 -15 -14 -22 -20 0 -43 0 -57 37 -83 0 -158 0 -65 -94 0 -81 0 -49 45 -54 28 -28 36 -35 48 -36 30 -31 28 -28 18 -33 0 -35 rrcurveto + -65 -29 -71 -127 vhcurveto + -131 0 -37 88 -3 79 0 10 -1 11 0 10 rrcurveto + -16 0 -60 -236 16 0 rlineto + 16 27 14 11 25 0 76 0 31 -38 94 0 157 0 87 92 0 99 0 87 -59 58 -59 49 -56 48 -61 42 0 69 rrcurveto + 71 78 26 43 112 48 -76 -84 vhcurveto + 0 -8 0 -8 -1 -8 rrcurveto + 16 hlineto + endchar + + + -51 0 20 596 37 hstem + 670 653 rmoveto + 15 callsubr + endchar + + + 104 -13 47 hstem + 775 653 rmoveto + -263 0 -4 -16 19 0 rlineto + 58 0 16 -14 1 -23 0 -11 -2 -8 -2 -7 rrcurveto + -86 -351 rlineto + -26 -108 -58 -81 -111 0 -97 0 -47 58 0 54 0 17 3 36 4 15 rrcurveto + 90 360 rlineto + 9 36 18 27 79 0 rrcurveto + 17 0 4 16 -328 0 -4 -16 18 0 rlineto + 54 21 -11 -20 hvcurveto + 0 -2 0 -3 -1 -2 rrcurveto + -76 -330 rlineto + -2 -9 -12 -78 0 -8 0 -91 62 -96 160 0 190 0 56 125 26 106 rrcurveto + 87 356 rlineto + 9 36 23 27 75 0 rrcurveto + 16 hlineto + endchar + + + -26 -16 20 hstem + 760 653 rmoveto + -241 0 -4 -16 20 0 rlineto + 38 14 -7 -15 hvcurveto + 0 -21 -19 -29 -19 -24 rrcurveto + -280 -366 -4 1 0 391 rlineto + 33 5 37 65 vhcurveto + 19 0 4 16 -294 0 -4 -16 rlineto + 61 38 -24 -58 hvcurveto + -571 24 vlineto + 441 576 rlineto + 26 34 41 39 65 4 rrcurveto + endchar + + + 315 -16 20 hstem + 1101 653 rmoveto + -242 0 -4 -16 17 0 rlineto + 41 13 -9 -16 hvcurveto + 0 -19 -23 -31 -17 -22 rrcurveto + -278 -370 -6 0 0 395 rlineto + 35 9 37 62 vhcurveto + 18 0 4 16 -285 0 -4 -16 8 0 rlineto + 63 19 -24 -31 hvcurveto + 0 -61 -226 -332 -5 0 0 377 rlineto + 71 37 0 39 vhcurveto + 7 0 4 16 -288 0 -4 -16 10 0 rlineto + 56 33 -29 -55 hvcurveto + -569 29 vlineto + 297 442 11 0 0 -442 29 0 438 576 rlineto + 25 33 43 41 66 3 rrcurveto + endchar + + + 189 0 20 hstem + 810 653 rmoveto + 16 callsubr + endchar + + + -66 0 20 hstem + 695 653 rmoveto + -237 0 -4 -16 rlineto + 58 21 -9 -19 hvcurveto + 0 -11 -4 -8 -12 -15 rrcurveto + -174 -212 -4 0 -75 207 rlineto + -2 7 -5 11 0 9 rrcurveto + 25 8 15 50 vhcurveto + 19 0 4 16 -299 0 -4 -16 rlineto + 77 0 29 -31 17 -45 rrcurveto + 89 -237 -61 -246 rlineto + -8 -34 -20 -28 -77 0 rrcurveto + -17 0 -4 -16 328 0 4 16 -20 0 rlineto + -58 -18 15 21 hvcurveto + 0 11 1 6 2 9 rrcurveto + 59 234 209 256 rlineto + 31 38 38 27 55 4 rrcurveto + endchar + + + 171 0 38 578 37 hstem + 802 653 rmoveto + 17 callsubr + endchar + + + -99 -10 61 361 29 hstem + 40 87 vstem + 472 428 rmoveto + -80 0 -9 -33 -1 0 rlineto + -2 22 -21 24 -43 0 rrcurveto + -144 -132 -200 -132 -64 26 -55 63 hvcurveto + 39 0 62 13 72 92 rrcurveto + 4 hlineto + -5 -16 -6 -21 0 -13 0 -32 9 -23 33 0 48 0 54 60 31 47 rrcurveto + -12 12 rlineto + -39 -52 -15 -2 -10 0 rrcurveto + -8 -6 6 11 9 1 2 0 hvcurveto + -14 258 rmoveto + 0 -34 -7 -40 -8 -26 -28 -95 -70 -95 -69 0 -27 0 -31 13 0 53 0 35 10 44 18 45 34 84 60 87 68 0 38 0 12 -30 0 -41 rrcurveto + endchar + + + -131 -11 29 368 55 207 20 hstem + 367 83 vstem + 214 382 rmoveto + 73 286 -158 -24 3 -16 rlineto + 8 3 12 1 10 0 13 0 15 -8 1 -13 0 -3 -1 -4 -1 -4 rrcurveto + -144 -563 rlineto + 45 -30 41 -18 53 0 rrcurveto + 157 109 180 121 82 -41 69 -70 hvcurveto + -49 0 -39 -31 -33 -32 rrcurveto + -39 -126 rmoveto + 11 45 34 89 72 0 rrcurveto + 33 38 -22 -70 -102 -66 -174 -122 -23 -32 12 7 hvcurveto + endchar + + + -186 -11 55 368 29 hstem + 40 87 210 63 vstem + 363 111 rmoveto + -35 -34 -46 -33 -61 0 rrcurveto + -45 -49 22 87 80 57 179 119 24 10 -9 -12 hvcurveto + 0 -22 -29 -12 0 -24 rrcurveto + -17 12 -21 26 38 16 32 38 49 -47 27 -50 -142 -121 -157 -140 -77 40 -78 97 vhcurveto + 85 0 62 49 51 60 rrcurveto + endchar + + + -69 -12 61 362 30 207 20 hstem + 40 87 vstem + 527 668 rmoveto + -156 -24 4 -15 rlineto + 7 1 11 2 9 0 26 0 7 -7 0 -13 0 -4 -1 -4 -1 -4 rrcurveto + -53 -209 -2 0 rlineto + -1 24 -23 26 -57 0 rrcurveto + -154 -103 -223 -103 -53 15 -74 73 hvcurveto + 37 0 60 12 74 93 rrcurveto + 5 hlineto + -8 -30 -2 -14 0 -20 0 -18 8 -23 32 0 47 0 53 60 30 45 rrcurveto + -11 12 rlineto + -14 -21 -28 -35 -20 0 -10 0 -5 3 0 6 0 3 1 6 1 3 rrcurveto + -15 270 rmoveto + 0 -31 -5 -39 -11 -35 -28 -92 -66 -94 -70 0 rrcurveto + -28 -28 17 49 96 81 200 99 41 15 -24 -47 hvcurveto + endchar + + + -156 -11 59 363 30 hstem + 331 79 vstem + 363 112 rmoveto + -36 -46 -48 -18 -42 0 rrcurveto + -61 -52 43 64 hvcurveto + 28 vlineto + 119 6 167 49 0 127 0 62 -60 14 -42 0 -127 0 -110 -125 -25 -111 -5 -23 -1 -14 0 -23 0 -72 39 -84 98 0 62 0 68 24 70 86 rrcurveto + -253 112 rmoveto + 29 85 60 115 74 0 rrcurveto + 25 19 -18 -25 -88 -94 -69 -104 hvcurveto + endchar + + + -46 -187 29 557 38 202 29 hstem + 40 64 444 67 vstem + 248 437 rmoveto + -9 -38 77 0 -87 -342 rlineto + -15 -57 -30 -158 -65 0 -9 0 -6 3 0 6 0 12 16 1 0 19 rrcurveto + 15 -10 18 -27 -29 -14 -25 -24 -30 24 -24 48 vhcurveto + 88 0 54 95 33 83 15 37 8 38 7 27 rrcurveto + 79 306 76 0 12 38 -80 0 19 70 rlineto + 21 78 39 54 50 0 12 0 3 -3 0 -5 0 -8 -9 -15 0 -14 rrcurveto + -14 7 -11 28 20 21 12 28 35 -29 24 -50 vhcurveto + -76 0 -43 -43 -31 -47 -43 -64 5 -77 -63 0 rrcurveto + endchar + + + -109 -187 28 527 45 -2 30 hstemhm + 20 63 21 80 -71 73 150 79 -66 70 hintmask 01001000 + 492 413 rmoveto + -126 hlineto + hintmask 10101000 + -15 15 -31 13 -42 0 -106 0 -68 -79 0 -79 0 -47 27 -47 67 -15 rrcurveto + -4 vlineto + hintmask 10110101 + -48 -37 -49 -38 hvcurveto + 0 -17 7 -19 17 -11 -62 -20 -55 -30 0 -58 rrcurveto + -58 50 -57 134 149 66 80 71 vhcurveto + 0 125 -233 -40 0 63 0 25 29 25 59 8 rrcurveto + hintmask 01001010 + 88 11 53 62 0 75 0 22 -4 13 -13 15 rrcurveto + 81 hlineto + -143 -19 rmoveto + -56 -25 -102 -71 -44 -12 35 46 vhcurveto + hintmask 10101010 + 68 44 71 54 44 10 -25 -37 vhcurveto + hintmask 10110001 + 13 -418 rmoveto + -44 -36 -46 -91 -95 -44 50 47 vhcurveto + 0 27 22 26 26 19 8 6 10 5 8 5 rrcurveto + 60 -22 132 5 0 -78 rrcurveto + endchar + + + -290 -11 20 511 96 hstem + 50 84 27 96 vstem + 257 566 rmoveto + 18 callsubr + -30 -125 rmoveto + -158 -22 3 -16 rlineto + 7 2 12 2 9 0 23 0 8 -5 0 -17 0 -3 -1 -7 -1 -2 -79 -307 0 -19 0 -12 0 -27 12 -19 28 0 49 0 50 60 35 45 rrcurveto + -13 10 rlineto + -36 -39 -6 -12 -22 0 -7 0 -6 3 0 7 0 4 0 4 1 6 rrcurveto + endchar + + + -212 -187 31 676 96 hstem + -16 66 226 96 vstem + 372 566 rmoveto + 18 callsubr + -21 -125 rmoveto + -158 -28 3 -17 rlineto + 7 3 12 2 8 0 rrcurveto + 22 10 -10 -12 hvcurveto + -92 -363 rlineto + -24 -96 -33 -76 -46 0 -5 0 -5 6 0 4 0 16 16 3 0 15 rrcurveto + 23 -16 15 -19 -35 -12 -30 -22 -28 20 -33 53 vhcurveto + 108 0 56 115 22 88 rrcurveto + endchar + + + -59 -11 66 -55 20 628 20 hstemhm + hintmask 01100000 + 527 428 rmoveto + -194 0 -4 -12 12 0 rlineto + 10 16 -7 -12 hvcurveto + 0 -13 -10 -13 -11 -10 rrcurveto + -167 -149 113 456 -158 -25 3 -14 rlineto + 8 2 10 0 9 0 22 0 10 -11 0 -11 0 -7 -1 -7 -2 -6 rrcurveto + -148 -589 76 0 45 166 56 49 63 -147 rlineto + hintmask 10000000 + 11 -26 21 -53 46 0 68 0 47 107 12 21 rrcurveto + -15 11 rlineto + -22 -30 -22 -43 -30 0 -26 0 -12 24 -15 35 rrcurveto + -64 152 106 92 rlineto + 38 33 28 18 67 7 rrcurveto + endchar + + + -283 -10 63 595 20 hstem + 278 668 rmoveto + -157 -23 3 -15 rlineto + 8 1 12 2 7 0 23 0 8 -10 0 -12 0 -3 0 -8 -3 -13 rrcurveto + -128 -510 rlineto + -3 -10 -3 -14 0 -12 0 -25 9 -26 30 0 58 0 43 66 34 40 rrcurveto + -16 12 rlineto + -9 -16 -30 -39 -23 0 rrcurveto + -8 -4 6 7 7 1 4 0 hvcurveto + endchar + + + 109 -8 20 -12 20 357 64 hstemhm + 344 85 149 82 hintmask 01111000 + 667 107 rmoveto + -19 -37 -26 -16 -17 0 -9 0 -6 4 0 8 0 3 1 6 1 3 rrcurveto + 61 231 rlineto + 6 24 1 19 0 18 0 45 -18 26 -36 0 -20 0 -19 -7 -22 -15 -49 -33 -52 -62 -29 -52 rrcurveto + -5 0 10 36 rlineto + 6 20 3 15 0 17 0 44 -18 37 -37 0 -38 0 -60 -26 -94 -143 rrcurveto + -6 0 44 169 -157 -24 2 -14 rlineto + 8 1 11 1 8 0 rrcurveto + 27 6 -13 -18 hvcurveto + -95 -374 78 0 35 135 rlineto + 16 61 110 181 52 0 18 0 5 -18 0 -20 0 -15 -3 -16 -2 -9 rrcurveto + -77 -299 79 0 34 135 rlineto + 15 61 110 181 54 0 18 0 6 -18 0 -20 0 -17 -2 -9 -4 -14 rrcurveto + -56 -221 rlineto + -5 -20 -1 -10 0 -7 rrcurveto + hintmask 10011000 + -39 17 -10 22 vhcurveto + 47 0 52 50 32 54 rrcurveto + endchar + + + -104 -8 62 -54 20 359 62 hstemhm + 364 84 hintmask 10110000 + 467 96 rmoveto + -13 11 rlineto + -14 -17 -24 -36 -24 0 -11 0 -3 8 -1 6 0 5 0 3 1 2 rrcurveto + 59 231 rlineto + 5 20 6 25 0 25 0 33 -12 29 -44 0 -60 0 -60 -66 -40 -50 -2 -3 -49 -70 -5 0 rrcurveto + -3 0 51 189 -159 -23 2 -15 rlineto + 7 1 11 2 10 0 rrcurveto + 27 3 -14 -18 hvcurveto + hintmask 01110000 + -95 -374 77 0 33 124 rlineto + 6 21 26 47 36 54 31 47 63 86 38 0 18 0 6 -19 0 -19 0 -14 -5 -20 -2 -8 rrcurveto + -57 -221 rlineto + -3 -11 -4 -16 0 -13 rrcurveto + hintmask 10010000 + -23 9 -23 32 vhcurveto + 52 0 48 64 28 34 rrcurveto + endchar + + + -143 -11 30 392 30 hstem + 40 81 236 81 vstem + 438 287 rmoveto + 19 callsubr + -81 31 rmoveto + 20 callsubr + endchar + + + -112 -12 29 368 56 hstem + 390 84 vstem + 253 370 rmoveto + -3 2 18 69 -158 -24 2 -16 rlineto + 8 2 12 2 8 0 rrcurveto + 30 1 -14 -19 hvcurveto + -117 -464 rlineto + -12 -46 -7 -30 -62 -3 rrcurveto + -3 -12 234 0 3 12 -16 0 rlineto + -50 -11 21 22 hvcurveto + 0 11 2 11 3 13 rrcurveto + 24 100 rlineto + 19 -15 18 -4 24 0 rrcurveto + 144 110 169 129 101 -33 54 -61 hvcurveto + -35 0 -39 -21 -39 -37 rrcurveto + -53 -145 rmoveto + 20 86 52 61 53 0 rrcurveto + 23 28 -20 -63 -112 -75 -173 -101 hvcurveto + -12 0 -20 5 -14 16 rrcurveto + endchar + + + -143 -12 61 363 29 hstem + 40 85 vstem + 463 428 rmoveto + -81 0 -7 -39 -3 0 rlineto + -2 26 -22 26 -48 0 -28 0 -32 -13 -31 -21 -89 -58 -80 -123 0 -123 0 -57 25 -58 64 0 36 0 60 10 67 93 rrcurveto + 6 0 -44 -184 rlineto + -9 -37 -16 -41 -68 0 rrcurveto + -19 0 -3 -12 250 0 3 12 -14 0 rlineto + -30 -19 18 30 hvcurveto + 0 9 0 10 3 10 rrcurveto + 26 432 rmoveto + 0 -113 -60 -114 -63 -45 -18 -13 -23 -4 -17 0 -43 0 -9 36 0 27 0 88 59 133 64 52 20 16 20 11 21 0 36 0 13 -31 0 -43 rrcurveto + endchar + + + -193 0 20 hstem + 175 267 rmoveto + 46 174 -157 -23 2 -15 rlineto + 6 1 14 2 7 0 22 0 11 -6 0 -13 0 -5 -1 -7 -3 -11 rrcurveto + -92 -364 77 0 21 76 rlineto + 5 17 87 256 61 0 18 0 -10 -38 39 0 rrcurveto + 41 24 44 38 27 -15 21 -29 hvcurveto + -55 0 -48 -65 -34 -54 -11 -18 -12 -20 -9 -18 rrcurveto + endchar + + + -161 -11 20 -19 25 400 26 hstemhm + 138 70 60 74 hintmask 01111000 + 390 441 rmoveto + -16 hlineto + -8 -10 -11 -10 -11 0 -29 0 -20 20 -43 0 -55 0 -59 -28 0 -79 0 -98 130 -50 0 -82 rrcurveto + -39 -14 -50 -59 -63 -41 67 63 vhcurveto + -16 0 rlineto + hintmask 10011000 + -25 -156 17 0 rlineto + 12 17 9 4 9 0 rrcurveto + hintmask 01111000 + 12 0 64 -20 34 0 87 0 48 63 0 62 0 95 -134 92 0 55 rrcurveto + 41 26 17 25 56 36 -56 -64 vhcurveto + 15 hlineto + endchar + + + -288 -9 66 343 38 hstem + 283 438 rmoveto + -70 0 31 129 -13 0 rlineto + -41 -87 -61 -37 -75 -14 rrcurveto + -5 -29 77 0 -81 -320 rlineto + -3 -11 -2 -13 0 -13 0 -27 8 -25 31 0 59 0 50 76 24 31 rrcurveto + -13 11 rlineto + -37 -50 -23 -2 0 0 -13 0 -3 6 0 6 0 3 1 5 1 3 rrcurveto + 78 320 70 0 rlineto + endchar + + + -127 -9 66 hstem + 30 81 vstem + 444 428 rmoveto + -77 0 -21 -75 rlineto + -20 -73 -46 -89 -43 -57 -35 -46 -40 -31 -25 0 -18 0 -8 13 0 21 0 8 1 7 2 9 rrcurveto + 84 326 -157 -26 3 -15 rlineto + 7 2 12 1 7 0 22 0 11 -11 0 -12 0 -4 -2 -6 -4 -15 rrcurveto + -59 -231 rlineto + -5 -19 -3 -19 0 -21 0 -38 14 -36 47 0 23 0 26 11 23 17 40 30 90 121 7 5 rrcurveto + 1 0 -26 -95 rlineto + -3 -12 -1 -16 0 -9 0 -33 11 -19 29 0 58 0 51 74 23 33 rrcurveto + -13 11 -5 -6 rlineto + -17 -20 -19 -26 -21 0 -10 0 -4 6 0 8 0 3 0 2 1 4 rrcurveto + endchar + + + -95 -9 39 408 20 hstem + 72 82 285 40 vstem + 247 454 rmoveto + -4 4 rlineto + -50 -22 -54 -7 -52 -13 rrcurveto + -15 vlineto + 13 44 0 -20 hvcurveto + 0 -8 -3 -9 -2 -8 rrcurveto + -58 -210 rlineto + -6 -20 -3 -21 0 -21 0 -73 52 -20 64 0 74 0 77 44 43 60 58 80 39 101 0 99 rrcurveto + 32 -5 51 -42 -29 -25 -8 -34 vhcurveto + 0 -44 61 9 0 -74 0 -25 -7 -25 -8 -24 -32 -90 -70 -113 -108 0 -40 0 -20 28 0 38 0 11 1 11 3 10 rrcurveto + endchar + + + 174 -9 39 400 20 -12 20 -18 20 hstemhm + 72 84 205 78 269 40 hintmask 11001110 + 531 450 rmoveto + -84 hlineto + -31 -110 -26 -78 -53 -81 -24 -37 -71 -113 -58 -1 -22 0 -6 21 0 26 0 28 8 32 3 13 rrcurveto + 81 307 rlineto + hintmask 10011110 + -4 3 rlineto + -50 -21 -52 -14 -53 -9 rrcurveto + -15 vlineto + 16 38 2 -24 hvcurveto + 0 -10 -2 -10 -3 -9 rrcurveto + -47 -177 rlineto + -8 -31 -11 -31 0 -33 0 -53 22 -34 56 0 112 0 82 136 40 89 -11 -40 -12 -43 0 -42 rrcurveto + -72 42 -28 69 171 105 238 146 vhcurveto + hintmask 10101110 + 32 -5 51 -42 vhcurveto + -24 0 -32 -6 2 -36 2 -43 59 8 0 -74 0 -25 -7 -25 -8 -24 -31 -88 -56 -115 -108 0 -40 0 -19 22 0 39 0 24 6 23 6 23 rrcurveto + endchar + + + -51 -9 64 320 66 hstem + 305 288 rmoveto + -20 55 rlineto + -12 33 -19 41 -44 24 rrcurveto + -132 -33 3 -18 rlineto + 10 4 15 2 13 0 55 0 26 -41 17 -48 rrcurveto + 33 -92 -65 -101 rlineto + -32 -49 -26 -10 -12 0 -25 0 4 20 -26 0 rrcurveto + -25 -13 -21 -16 -22 16 -25 36 hvcurveto + 56 0 29 43 32 49 rrcurveto + 63 98 40 -113 rlineto + 13 -36 19 -41 41 0 58 0 45 96 12 15 rrcurveto + -14 10 rlineto + -21 -43 -24 -14 -16 0 -45 0 -23 135 -30 62 rrcurveto + 55 85 rlineto + 14 22 11 16 30 0 17 0 5 -9 22 0 rrcurveto + 26 13 23 19 21 -17 12 -32 hvcurveto + -50 0 -35 -44 -26 -40 rrcurveto + endchar + + + -105 -183 65 hstem + 444 52 vstem + 270 307 rmoveto + -9 74 -7 26 -23 33 rrcurveto + -150 -25 2 -16 rlineto + 9 1 16 3 11 0 56 0 9 -60 5 -45 rrcurveto + 38 -314 rlineto + -26 -40 -57 -62 -17 0 -16 0 2 31 -43 0 rrcurveto + -13 -27 -11 -30 -24 19 -31 37 hvcurveto + 68 0 65 98 73 92 42 53 162 240 0 78 rrcurveto + 39 -25 23 -32 -29 -18 -18 -22 vhcurveto + 0 -38 52 -10 0 -21 0 -18 -8 -20 -20 -37 -22 -41 -35 -60 -61 -83 rrcurveto + endchar + + + -102 -14 22 -8 20 -6 80 276 80 hstemhm + 382 66 hintmask 01011000 + 467 450 rmoveto + -314 0 -36 -137 18 -4 rlineto + 23 55 24 6 57 0 rrcurveto + 131 hlineto + -107 -125 -112 -122 -109 -123 rrcurveto + 11 -10 rlineto + hintmask 00101000 + 37 16 45 8 40 0 rrcurveto + hintmask 01001000 + 33 0 34 -5 32 -9 rrcurveto + hintmask 10111000 + 21 -5 25 -9 22 0 rrcurveto + 46 60 26 54 24 -15 18 -25 -19 -19 -19 -19 hvcurveto + 0 -22 12 -3 0 -13 0 -16 -12 -8 -15 0 -45 0 -3 86 -92 0 -23 0 -25 -7 -19 -14 rrcurveto + -3 1 rlineto + 107 120 107 120 108 120 rrcurveto + endchar + + + -601 658 20 hstem + -147 507 rmoveto + -56 callsubr + endchar + + + -601 658 20 hstem + -371 507 rmoveto + -29 callsubr + endchar + + + -601 654 20 hstem + -75 507 rmoveto + -52 callsubr + endchar + + + -41 560 554 rmoveto + -256 213 -48 0 -256 -213 64 0 216 146 216 -146 rlineto + endchar + + + 378 979 564 rmoveto + -465 213 -48 0 -466 -213 99 0 391 145 390 -145 rlineto + endchar + + + 859 1460 564 rmoveto + -706 213 -48 0 -706 -213 153 0 577 145 577 -145 rlineto + endchar + + + 1285 1886 599 rmoveto + -943 197 -943 -197 5 -26 937 161 939 -161 rlineto + endchar + + + 1727 2328 603 rmoveto + -1164 213 -1164 -213 5 -31 1158 182 1160 -182 rlineto + endchar + + + -601 532 55 -9 55 hstemhm + hintmask 10000000 + -94 638 rmoveto + -31 callsubr + hintmask 01000000 + -50 callsubr + hintmask 10000000 + -49 callsubr + hintmask 01000000 + -30 callsubr + hintmask 10000000 + -47 callsubr + endchar + + + -41 598 59 32 61 hstem + 535 750 rmoveto + 21 callsubr + endchar + + + 378 608 59 32 61 hstem + 953 760 rmoveto + 24 callsubr + endchar + + + 859 608 64 35 67 hstem + 1434 774 rmoveto + 26 callsubr + endchar + + + 1285 608 66 31 66 hstem + 1857 771 rmoveto + 28 callsubr + endchar + + + 1727 617 66 31 66 hstem + 2299 780 rmoveto + 30 callsubr + endchar + + + -601 547 54 hstem + -74 547 rmoveto + -54 callsubr + endchar + + + -601 770 50 hstem + 20 770 rmoveto + -57 callsubr + endchar + + + 399 770 50 hstem + 1000 770 rmoveto + 50 -1000 -50 vlineto + endchar + + + 899 770 50 hstem + 1500 770 rmoveto + 23 callsubr + endchar + + + 1399 770 50 hstem + 2000 770 rmoveto + 25 callsubr + endchar + + + 1899 770 50 hstem + 2500 770 rmoveto + 27 callsubr + endchar + + + 2399 770 50 hstem + 3000 770 rmoveto + 29 callsubr + endchar + + + -601 507 60 77 20 hstem + -121 664 rmoveto + -44 callsubr + endchar + + + -601 523 99 hstem + -280 99 vstem + -181 572 rmoveto + -43 callsubr + endchar + + + -601 523 99 hstem + -379 99 100 99 vstem + -81 572 rmoveto + -55 callsubr + -199 hmoveto + -55 callsubr + endchar + + + -601 644 20 64 23 hstem + -173 55 vstem + -261 581 rmoveto + -89 33 vlineto + 2 35 8 0 rlineto + 54 46 50 66 53 -21 55 -83 hvcurveto + -23 0 -23 -7 -15 -11 -16 -11 -8 -19 0 -11 0 -14 11 -14 17 0 37 0 -19 64 43 0 rrcurveto + 31 14 -40 -37 -40 -32 -30 -44 hvcurveto + endchar + + + -601 512 34 131 34 hstem + -329 34 131 34 vstem + -130 611 rmoveto + -46 callsubr + -34 1 rmoveto + -45 callsubr + endchar + + + -601 658 20 hstem + -245 507 rmoveto + -41 callsubr + -302 -148 rmoveto + -41 callsubr + endchar + + + -601 654 20 hstem + -74 674 rmoveto + -42 callsubr + endchar + + + -41 560 767 rmoveto + -64 0 -216 -146 -216 146 -64 0 256 -213 48 0 rlineto + endchar + + + 378 979 777 rmoveto + -99 0 -390 -145 -391 145 -99 0 466 -213 48 0 rlineto + endchar + + + 859 1460 777 rmoveto + -153 0 -577 -145 -577 145 -153 0 706 -213 48 0 rlineto + endchar + + + 1285 1886 770 rmoveto + -5 26 -939 -161 -937 161 -5 -26 943 -197 rlineto + endchar + + + 1727 2328 785 rmoveto + -5 31 -1160 -182 -1158 182 -5 -31 1164 -213 rlineto + endchar + + + -601 -250 55 vstem + -195 500 rmoveto + 200 -55 -200 vlineto + endchar + + + -601 -326 54 84 55 vstem + -133 500 rmoveto + 200 -55 -200 vlineto + -84 hmoveto + 200 -54 -200 vlineto + endchar + + + -601 658 20 hstem + -22 507 rmoveto + -28 callsubr + -116 hmoveto + -28 callsubr + endchar + + + -601 507 60 101 99 hstem + -280 99 vstem + -181 717 rmoveto + -43 callsubr + 60 -53 rmoveto + -44 callsubr + endchar + + + -601 604 60 hstem + -92 507 rmoveto + -27 callsubr + endchar + + + -601 -299 39 vstem + -187 745 rmoveto + -40 callsubr + endchar + + + -601 -199 39 vstem + -272 502 rmoveto + -39 callsubr + endchar + + + -601 -299 39 vstem + -178 521 rmoveto + -38 callsubr + endchar + + + -601 15 39 vstem + -58 502 rmoveto + -39 callsubr + endchar + + + -601 -127 -224 rmoveto + -56 callsubr + endchar + + + -601 -371 -224 rmoveto + -29 callsubr + endchar + + + -601 -188 40 hstem + -250 40 vstem + -210 -283 rmoveto + 230 -40 -95 -147 -40 147 -95 vlineto + endchar + + + -601 -188 40 hstem + -267 40 vstem + -80 -188 rmoveto + 40 -147 95 -40 -230 40 95 vlineto + endchar + + + -601 680 55 hstem + -135 55 vstem + -80 531 rmoveto + 204 -300 -55 245 -149 vlineto + endchar + + + -601 454 20 hstem + -36 345 rmoveto + 14 2 23 4 17 10 24 14 9 21 0 22 rrcurveto + 35 -25 21 -24 -27 -19 -22 -19 vhcurveto + 0 -32 35 -10 0 -9 0 -3 -8 -8 -19 -4 rrcurveto + endchar + + + -601 -360 37 vstem + -232 -266 rmoveto + -36 callsubr + endchar + + + -601 -240 40 hstem + -250 40 vstem + -115 -240 rmoveto + -35 callsubr + endchar + + + -601 -93 40 hstem + -250 40 vstem + -115 -93 rmoveto + -34 callsubr + endchar + + + -601 -168 44 hstem + -249 38 vstem + -134 -168 rmoveto + 44 -77 71 -38 -71 -77 -44 77 -82 38 82 vlineto + endchar + + + -601 -168 44 hstem + -134 -168 rmoveto + 44 -192 -44 vlineto + endchar + + + -601 -287 19 hstem + -54 55 vstem + 1 75 rmoveto + -55 -252 hlineto + -44 -12 -47 -44 vhcurveto + -22 0 -18 15 -14 16 -14 16 -8 16 -21 0 rrcurveto + -16 -12 -12 -15 -32 58 -23 70 61 47 62 77 hvcurveto + endchar + + + -601 -287 19 hstem + -54 55 vstem + 1 75 rmoveto + -55 -223 hlineto + -77 47 -62 61 70 58 23 32 15 -12 12 -16 vhcurveto + -21 0 -8 -16 -14 -16 -14 -16 -18 -15 -22 0 rrcurveto + -44 -12 47 44 hvcurveto + endchar + + + -601 -217 99 hstem + -280 99 vstem + -181 -168 rmoveto + -43 callsubr + endchar + + + -601 -218 99 hstem + -379 99 100 99 vstem + -81 -168 rmoveto + -26 callsubr + -199 hmoveto + -26 callsubr + endchar + + + -601 -268 34 131 34 hstem + -329 34 131 34 vstem + -130 -168 rmoveto + -32 callsubr + -34 hmoveto + -45 callsubr + endchar + + + -601 -199 39 vstem + -272 -353 rmoveto + -39 callsubr + endchar + + + -601 -215 35 180 20 hstem + -200 75 vstem + -212 hmoveto + -53 callsubr + endchar + + + -601 -165 56 109 20 hstem + -322 56 vstem + -157 -73 rmoveto + -33 callsubr + endchar + + + -601 -250 40 vstem + -210 -234 rmoveto + 132 -40 -132 vlineto + endchar + + + -601 -153 55 hstem + -385 55 202 55 vstem + -73 -235 rmoveto + 137 -312 -137 55 82 202 -82 vlineto + endchar + + + -601 -227 40 hstem + -380 40 90 40 95 40 vstem + -75 -110 rmoveto + -40 -35 hlineto + -28 -15 -14 -34 -26 -20 14 28 vhcurveto + 35 -40 -36 vlineto + -28 -11 -13 -32 -27 -20 13 28 vhcurveto + 36 -40 -35 vlineto + -50 35 -32 54 vhcurveto + 32 0 21 16 8 18 7 -18 30 -16 31 0 rrcurveto + 54 33 32 50 hvcurveto + endchar + + + -601 -74 -73 rmoveto + -42 callsubr + endchar + + + -601 -74 -240 rmoveto + -52 callsubr + endchar + + + -601 -225 60 hstem + -118 -68 rmoveto + -44 callsubr + endchar + + + -601 -119 60 hstem + -89 -216 rmoveto + -27 callsubr + endchar + + + -601 -219 55 -9 55 hstemhm + hintmask 10000000 + -94 -113 rmoveto + -31 callsubr + hintmask 01000000 + -50 callsubr + hintmask 10000000 + -49 callsubr + hintmask 01000000 + -30 callsubr + hintmask 10000000 + -47 callsubr + endchar + + + -41 -269 59 32 61 hstem + 535 -117 rmoveto + 21 callsubr + endchar + + + 378 -269 59 32 61 hstem + 953 -117 rmoveto + 24 callsubr + endchar + + + 859 -283 64 35 67 hstem + 1434 -117 rmoveto + 26 callsubr + endchar + + + 1285 -280 66 31 66 hstem + 1857 -117 rmoveto + 28 callsubr + endchar + + + 1727 -280 66 31 66 hstem + 2299 -117 rmoveto + 30 callsubr + endchar + + + -601 -195 54 hstem + -74 -195 rmoveto + -54 callsubr + endchar + + + -601 -191 50 hstem + 20 -191 rmoveto + -57 callsubr + endchar + + + 399 -177 50 hstem + 1000 -177 rmoveto + 50 -1000 -50 vlineto + endchar + + + 899 -177 50 hstem + 1500 -177 rmoveto + 23 callsubr + endchar + + + 1399 -177 50 hstem + 2000 -177 rmoveto + 25 callsubr + endchar + + + 1899 -177 50 hstem + 2500 -177 rmoveto + 27 callsubr + endchar + + + 2399 -177 50 hstem + 3000 -177 rmoveto + 29 callsubr + endchar + + + -601 -300 50 59 50 hstem + 20 -191 rmoveto + -57 callsubr + 500 -109 rmoveto + -57 callsubr + endchar + + + -601 214 55 -9 55 hstemhm + hintmask 10000000 + -100 320 rmoveto + -31 callsubr + hintmask 01000000 + -50 callsubr + hintmask 10000000 + -49 callsubr + hintmask 01000000 + -30 callsubr + hintmask 10000000 + -47 callsubr + endchar + + + -601 230 44 hstem + -78 230 rmoveto + 44 -306 -44 vlineto + endchar + + + -601 230 44 hstem + 20 230 rmoveto + 44 -500 -44 vlineto + endchar + + + -601 -41 580 rmoveto + -54 0 -285 -654 54 0 rlineto + endchar + + + -601 642 20 hstem + 31 662 rmoveto + -54 0 -357 -818 54 0 rlineto + endchar + + + -601 -554 -181 rmoveto + 54 0 375 861 -54 0 rlineto + endchar + + + -601 -103 729 rmoveto + -54 0 -418 -958 54 0 rlineto + endchar + + + -601 -82 778 rmoveto + -54 0 -461 -1058 54 0 rlineto + endchar + + + -601 -141 830 rmoveto + -54 0 -323 -1157 54 0 rlineto + endchar + + + -601 -554 -429 rmoveto + 54 0 369 1360 -54 0 rlineto + endchar + + + -601 -210 1565 rmoveto + -54 0 -497 -1846 54 0 rlineto + endchar + + + -601 -189 37 vstem + -280 -71 rmoveto + -37 callsubr + endchar + + + -601 -190 55 hstem + -385 55 202 55 vstem + -73 -190 rmoveto + 137 -55 -82 -202 82 -55 -137 vlineto + endchar + + + -601 -227 40 94 40 hstem + -313 40 86 40 vstem + -147 -227 rmoveto + 174 -166 -174 vlineto + 126 40 rmoveto + -86 94 86 hlineto + endchar + + + -601 -120 55 hstem + -79 -91 rmoveto + -28 19 -13 7 -24 0 -29 0 -29 -13 -28 -40 -24 40 -29 13 -32 0 -19 0 -16 -6 -30 -20 rrcurveto + -51 vlineto + 32 20 19 2 8 0 46 0 16 -32 20 -37 rrcurveto + 20 hlineto + 19 41 20 28 32 0 12 0 21 0 36 -22 rrcurveto + endchar + + + -601 -135 688 rmoveto + -27 27 -68 -67 -67 67 -29 -27 68 -68 -64 -65 26 -28 66 65 67 -67 26 30 -65 65 rlineto + endchar + + + -601 202 55 -9 55 hintmask 01000000 + -177 829 rmoveto + hintmask 10000000 + -65 -15 -36 -36 0 -50 0 -22 6 -24 12 -24 rrcurveto + hintmask 01000000 + 22 -43 6 -14 0 -20 0 -23 -14 -13 -37 -17 rrcurveto + -29 vlineto + 76 21 30 29 0 55 0 25 -4 15 -20 41 rrcurveto + hintmask 10000000 + -18 38 -4 8 0 18 rrcurveto + hintmask 01000000 + 0 23 15 17 31 11 rrcurveto + endchar + + + -601 770 50 58 50 hstem + 20 878 rmoveto + -57 callsubr + 500 -108 rmoveto + -57 callsubr + endchar + + + -601 627 54 hstem + -350 55 173 54 vstem + -68 538 rmoveto + 143 -282 -143 55 89 173 -89 vlineto + endchar + + + -600 -292 54 44 54 hstem + 323 -194 rmoveto + 54 -312 -54 vlineto + 312 -98 rmoveto + 54 -312 -54 vlineto + endchar + + + -601 532 56 -10 55 38 56 -10 55 hstemhm + hintmask 00100000 + -85 777 rmoveto + -51 callsubr + hintmask 00010000 + -50 callsubr + hintmask 00100000 + -49 callsubr + hintmask 00010000 + -48 callsubr + hintmask 10100000 + -47 callsubr + -29 -139 rmoveto + -51 callsubr + hintmask 01000000 + -50 callsubr + hintmask 10000000 + -49 callsubr + hintmask 01000000 + -48 callsubr + hintmask 10000000 + -47 callsubr + endchar + + + -123 -173 54 hstem + 478 -146 rmoveto + -25 callsubr + endchar + + + -601 -87 -157 rmoveto + -24 callsubr + endchar + + + -601 -233 60 hstem + 266 -76 rmoveto + -18 -84 -218 -13 -69 0 -69 0 -218 13 -18 84 rrcurveto + -29 hlineto + -140 233 -17 101 101 233 17 140 vhcurveto + endchar + + + -601 517 55 6 55 hstem + 336 618 rmoveto + -15 -50 -84 4 -40 0 -79 0 -106 21 -77 19 -51 12 -55 9 -52 0 -73 0 -84 -18 -15 -83 rrcurveto + 29 hlineto + 15 50 84 -4 40 0 51 0 54 -10 50 -12 85 -21 92 -18 88 0 73 0 84 18 15 83 rrcurveto + endchar + + + -601 604 60 hstem + 295 507 rmoveto + 140 -233 17 -101 -101 -233 -17 -140 vhcurveto + 29 hlineto + 18 84 218 13 69 0 69 0 218 -13 18 -84 rrcurveto + endchar + + + -601 -195 55 hstem + 355 -160 rmoveto + -45 19 -36 40 -31 36 rrcurveto + -20 -19 27 -33 rlineto + 4 -5 3 -3 0 -4 rrcurveto + -5 -8 -6 -4 vhcurveto + -640 -55 639 hlineto + 9 3 -5 -4 hvcurveto + 0 -4 -2 -5 -4 -5 rrcurveto + -27 -33 19 -19 rlineto + 32 36 36 40 45 19 rrcurveto + endchar + + + -165 -173 54 hstem + 436 -173 rmoveto + -23 callsubr + endchar + + + -301 -173 54 hstem + 300 -173 rmoveto + 54 -300 -54 vlineto + endchar + + + -165 -173 54 hstem + 436 -146 rmoveto + -132 106 -18 -8 rlineto + 32 -27 17 -22 0 -7 rrcurveto + -12 -16 -3 -33 vhcurveto + -286 -54 286 hlineto + 28 21 -2 -13 hvcurveto + 0 -9 -21 -18 -29 -29 rrcurveto + 18 -8 rlineto + endchar + + + -151 50 124 vstem + 400 1005 rmoveto + -261 -184 -89 -359 0 -303 rrcurveto + -159 124 239 vlineto + 0 254 54 285 172 197 rrcurveto + endchar + + + -151 50 124 vstem + 174 hmoveto + 1010 -124 -1010 vlineto + endchar + + + -151 50 124 vstem + 400 30 rmoveto + -172 197 -54 285 0 254 rrcurveto + 239 -124 -159 vlineto + 0 -303 89 -359 261 -184 rrcurveto + endchar + + + -151 276 124 vstem + 400 hmoveto + 159 vlineto + 0 303 -89 359 -261 184 rrcurveto + -30 vlineto + 172 -197 54 -285 0 -254 rrcurveto + -239 vlineto + endchar + + + -151 276 124 vstem + 400 hmoveto + 1010 -124 -1010 vlineto + endchar + + + -151 276 124 vstem + 400 1005 rmoveto + -124 -239 hlineto + 0 -254 -54 -285 -172 -197 rrcurveto + -30 vlineto + 261 184 89 359 0 303 rrcurveto + endchar + + + -101 -14 26 638 26 hstem + 476 330 rmoveto + 205 -91 141 -131 -161 -69 -163 -177 -164 55 -186 171 163 63 172 172 vhcurveto + -96 -5 rmoveto + -198 -45 -115 -85 -86 -44 114 203 203 45 118 83 88 44 -117 -208 vhcurveto + endchar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/TestOPBD-0.ttx b/Tests/subset/data/TestOPBD-0.ttx new file mode 100644 index 0000000..8d7af05 --- /dev/null +++ b/Tests/subset/data/TestOPBD-0.ttx @@ -0,0 +1,299 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TestOPBD + + + Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/TestOPBD-1.ttx b/Tests/subset/data/TestOPBD-1.ttx new file mode 100644 index 0000000..0552181 --- /dev/null +++ b/Tests/subset/data/TestOPBD-1.ttx @@ -0,0 +1,299 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TestOPBD + + + Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/TestOTF-Regular.ttx b/Tests/subset/data/TestOTF-Regular.ttx new file mode 100644 index 0000000..a650301 --- /dev/null +++ b/Tests/subset/data/TestOTF-Regular.ttx @@ -0,0 +1,262 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test OTF + + + Regular + + + 1.000;UKWN;TestOTF-Regular + + + Test OTF + + + Version 1.000;PS 1.0;hotconv 1.0.88;makeotf.lib2.5.647800 + + + TestOTF-Regular + + + Test OTF + + + Regular + + + 1.000;UKWN;TestOTF-Regular + + + Test OTF + + + Version 1.000;PS 1.0;hotconv 1.0.88;makeotf.lib2.5.647800 + + + TestOTF-Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 132 304 rmoveto + 233 263 -233 hlineto + endchar + + + + + + 196 10 hmoveto + 476 660 -476 hlineto + 108 -602 rmoveto + 74 132 54 103 rlineto + 4 hlineto + 52 -103 73 -132 rlineto + -129 329 rmoveto + -50 94 -66 119 rlineto + 235 hlineto + -66 -119 -49 -94 rlineto + -175 -277 rmoveto + 462 vlineto + 127 -232 rlineto + 217 -230 rmoveto + -126 230 126 232 rlineto + endchar + + + -107 callsubr + + + 100 304 263 hstem + 132 233 vstem + -107 callsubr + + + endchar + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/TestPROP.ttx b/Tests/subset/data/TestPROP.ttx new file mode 100644 index 0000000..c783fce --- /dev/null +++ b/Tests/subset/data/TestPROP.ttx @@ -0,0 +1,322 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TestPROP + + + Regular + + + TestPROP + + + TestPROP-Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/TestTTF-Regular.ttx b/Tests/subset/data/TestTTF-Regular.ttx new file mode 100644 index 0000000..b6859e5 --- /dev/null +++ b/Tests/subset/data/TestTTF-Regular.ttx @@ -0,0 +1,705 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PUSH[ ] /* 1 value pushed */ + 0 + FDEF[ ] /* FunctionDefinition */ + MPPEM[ ] /* MeasurePixelPerEm */ + PUSH[ ] /* 1 value pushed */ + 9 + LT[ ] /* LessThan */ + IF[ ] /* If */ + PUSH[ ] /* 2 values pushed */ + 1 1 + INSTCTRL[ ] /* SetInstrExecControl */ + EIF[ ] /* EndIf */ + PUSH[ ] /* 1 value pushed */ + 511 + SCANCTRL[ ] /* ScanConversionControl */ + PUSH[ ] /* 1 value pushed */ + 68 + SCVTCI[ ] /* SetCVTCutIn */ + PUSH[ ] /* 2 values pushed */ + 9 3 + SDS[ ] /* SetDeltaShiftInGState */ + SDB[ ] /* SetDeltaBaseInGState */ + ENDF[ ] /* EndFunctionDefinition */ + PUSH[ ] /* 1 value pushed */ + 1 + FDEF[ ] /* FunctionDefinition */ + DUP[ ] /* DuplicateTopStack */ + DUP[ ] /* DuplicateTopStack */ + RCVT[ ] /* ReadCVT */ + ROUND[01] /* Round */ + WCVTP[ ] /* WriteCVTInPixels */ + PUSH[ ] /* 1 value pushed */ + 1 + ADD[ ] /* Add */ + ENDF[ ] /* EndFunctionDefinition */ + PUSH[ ] /* 1 value pushed */ + 2 + FDEF[ ] /* FunctionDefinition */ + PUSH[ ] /* 1 value pushed */ + 1 + LOOPCALL[ ] /* LoopAndCallFunction */ + POP[ ] /* PopTopStack */ + ENDF[ ] /* EndFunctionDefinition */ + PUSH[ ] /* 1 value pushed */ + 3 + FDEF[ ] /* FunctionDefinition */ + DUP[ ] /* DuplicateTopStack */ + GC[0] /* GetCoordOnPVector */ + PUSH[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + GC[0] /* GetCoordOnPVector */ + GT[ ] /* GreaterThan */ + IF[ ] /* If */ + SWAP[ ] /* SwapTopStack */ + EIF[ ] /* EndIf */ + DUP[ ] /* DuplicateTopStack */ + ROLL[ ] /* RollTopThreeStack */ + DUP[ ] /* DuplicateTopStack */ + ROLL[ ] /* RollTopThreeStack */ + MD[0] /* MeasureDistance */ + ABS[ ] /* Absolute */ + ROLL[ ] /* RollTopThreeStack */ + DUP[ ] /* DuplicateTopStack */ + GC[0] /* GetCoordOnPVector */ + DUP[ ] /* DuplicateTopStack */ + ROUND[00] /* Round */ + SUB[ ] /* Subtract */ + ABS[ ] /* Absolute */ + PUSH[ ] /* 1 value pushed */ + 4 + CINDEX[ ] /* CopyXToTopStack */ + GC[0] /* GetCoordOnPVector */ + DUP[ ] /* DuplicateTopStack */ + ROUND[00] /* Round */ + SUB[ ] /* Subtract */ + ABS[ ] /* Absolute */ + GT[ ] /* GreaterThan */ + IF[ ] /* If */ + SWAP[ ] /* SwapTopStack */ + NEG[ ] /* Negate */ + ROLL[ ] /* RollTopThreeStack */ + EIF[ ] /* EndIf */ + MDAP[1] /* MoveDirectAbsPt */ + DUP[ ] /* DuplicateTopStack */ + PUSH[ ] /* 1 value pushed */ + 0 + GTEQ[ ] /* GreaterThanOrEqual */ + IF[ ] /* If */ + ROUND[01] /* Round */ + DUP[ ] /* DuplicateTopStack */ + PUSH[ ] /* 1 value pushed */ + 0 + EQ[ ] /* Equal */ + IF[ ] /* If */ + POP[ ] /* PopTopStack */ + PUSH[ ] /* 1 value pushed */ + 64 + EIF[ ] /* EndIf */ + ELSE[ ] /* Else */ + ROUND[01] /* Round */ + DUP[ ] /* DuplicateTopStack */ + PUSH[ ] /* 1 value pushed */ + 0 + EQ[ ] /* Equal */ + IF[ ] /* If */ + POP[ ] /* PopTopStack */ + PUSH[ ] /* 1 value pushed */ + 64 + NEG[ ] /* Negate */ + EIF[ ] /* EndIf */ + EIF[ ] /* EndIf */ + MSIRP[0] /* MoveStackIndirRelPt */ + ENDF[ ] /* EndFunctionDefinition */ + PUSH[ ] /* 1 value pushed */ + 4 + FDEF[ ] /* FunctionDefinition */ + DUP[ ] /* DuplicateTopStack */ + GC[0] /* GetCoordOnPVector */ + PUSH[ ] /* 1 value pushed */ + 4 + CINDEX[ ] /* CopyXToTopStack */ + GC[0] /* GetCoordOnPVector */ + GT[ ] /* GreaterThan */ + IF[ ] /* If */ + SWAP[ ] /* SwapTopStack */ + ROLL[ ] /* RollTopThreeStack */ + EIF[ ] /* EndIf */ + DUP[ ] /* DuplicateTopStack */ + GC[0] /* GetCoordOnPVector */ + DUP[ ] /* DuplicateTopStack */ + ROUND[10] /* Round */ + SUB[ ] /* Subtract */ + ABS[ ] /* Absolute */ + PUSH[ ] /* 1 value pushed */ + 4 + CINDEX[ ] /* CopyXToTopStack */ + GC[0] /* GetCoordOnPVector */ + DUP[ ] /* DuplicateTopStack */ + ROUND[10] /* Round */ + SUB[ ] /* Subtract */ + ABS[ ] /* Absolute */ + GT[ ] /* GreaterThan */ + IF[ ] /* If */ + SWAP[ ] /* SwapTopStack */ + ROLL[ ] /* RollTopThreeStack */ + EIF[ ] /* EndIf */ + MDAP[1] /* MoveDirectAbsPt */ + MIRP[11101] /* MoveIndirectRelPt */ + ENDF[ ] /* EndFunctionDefinition */ + PUSH[ ] /* 1 value pushed */ + 5 + FDEF[ ] /* FunctionDefinition */ + MPPEM[ ] /* MeasurePixelPerEm */ + DUP[ ] /* DuplicateTopStack */ + PUSH[ ] /* 1 value pushed */ + 3 + MINDEX[ ] /* MoveXToTopStack */ + LT[ ] /* LessThan */ + IF[ ] /* If */ + LTEQ[ ] /* LessThenOrEqual */ + IF[ ] /* If */ + PUSH[ ] /* 1 value pushed */ + 128 + WCVTP[ ] /* WriteCVTInPixels */ + ELSE[ ] /* Else */ + PUSH[ ] /* 1 value pushed */ + 64 + WCVTP[ ] /* WriteCVTInPixels */ + EIF[ ] /* EndIf */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + DUP[ ] /* DuplicateTopStack */ + RCVT[ ] /* ReadCVT */ + PUSH[ ] /* 1 value pushed */ + 192 + LT[ ] /* LessThan */ + IF[ ] /* If */ + PUSH[ ] /* 1 value pushed */ + 192 + WCVTP[ ] /* WriteCVTInPixels */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + EIF[ ] /* EndIf */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + PUSH[ ] /* 1 value pushed */ + 6 + FDEF[ ] /* FunctionDefinition */ + DUP[ ] /* DuplicateTopStack */ + DUP[ ] /* DuplicateTopStack */ + RCVT[ ] /* ReadCVT */ + ROUND[01] /* Round */ + WCVTP[ ] /* WriteCVTInPixels */ + PUSH[ ] /* 1 value pushed */ + 1 + ADD[ ] /* Add */ + DUP[ ] /* DuplicateTopStack */ + DUP[ ] /* DuplicateTopStack */ + RCVT[ ] /* ReadCVT */ + RDTG[ ] /* RoundDownToGrid */ + ROUND[01] /* Round */ + RTG[ ] /* RoundToGrid */ + WCVTP[ ] /* WriteCVTInPixels */ + PUSH[ ] /* 1 value pushed */ + 1 + ADD[ ] /* Add */ + ENDF[ ] /* EndFunctionDefinition */ + PUSH[ ] /* 1 value pushed */ + 7 + FDEF[ ] /* FunctionDefinition */ + PUSH[ ] /* 1 value pushed */ + 6 + LOOPCALL[ ] /* LoopAndCallFunction */ + ENDF[ ] /* EndFunctionDefinition */ + PUSH[ ] /* 1 value pushed */ + 8 + FDEF[ ] /* FunctionDefinition */ + MPPEM[ ] /* MeasurePixelPerEm */ + DUP[ ] /* DuplicateTopStack */ + PUSH[ ] /* 1 value pushed */ + 3 + MINDEX[ ] /* MoveXToTopStack */ + GTEQ[ ] /* GreaterThanOrEqual */ + IF[ ] /* If */ + PUSH[ ] /* 1 value pushed */ + 64 + ELSE[ ] /* Else */ + PUSH[ ] /* 1 value pushed */ + 0 + EIF[ ] /* EndIf */ + ROLL[ ] /* RollTopThreeStack */ + ROLL[ ] /* RollTopThreeStack */ + DUP[ ] /* DuplicateTopStack */ + PUSH[ ] /* 1 value pushed */ + 3 + MINDEX[ ] /* MoveXToTopStack */ + GTEQ[ ] /* GreaterThanOrEqual */ + IF[ ] /* If */ + SWAP[ ] /* SwapTopStack */ + POP[ ] /* PopTopStack */ + PUSH[ ] /* 1 value pushed */ + 128 + ROLL[ ] /* RollTopThreeStack */ + ROLL[ ] /* RollTopThreeStack */ + ELSE[ ] /* Else */ + ROLL[ ] /* RollTopThreeStack */ + SWAP[ ] /* SwapTopStack */ + EIF[ ] /* EndIf */ + DUP[ ] /* DuplicateTopStack */ + PUSH[ ] /* 1 value pushed */ + 3 + MINDEX[ ] /* MoveXToTopStack */ + GTEQ[ ] /* GreaterThanOrEqual */ + IF[ ] /* If */ + SWAP[ ] /* SwapTopStack */ + POP[ ] /* PopTopStack */ + PUSH[ ] /* 1 value pushed */ + 192 + ROLL[ ] /* RollTopThreeStack */ + ROLL[ ] /* RollTopThreeStack */ + ELSE[ ] /* Else */ + ROLL[ ] /* RollTopThreeStack */ + SWAP[ ] /* SwapTopStack */ + EIF[ ] /* EndIf */ + DUP[ ] /* DuplicateTopStack */ + PUSH[ ] /* 1 value pushed */ + 3 + MINDEX[ ] /* MoveXToTopStack */ + GTEQ[ ] /* GreaterThanOrEqual */ + IF[ ] /* If */ + SWAP[ ] /* SwapTopStack */ + POP[ ] /* PopTopStack */ + PUSH[ ] /* 1 value pushed */ + 256 + ROLL[ ] /* RollTopThreeStack */ + ROLL[ ] /* RollTopThreeStack */ + ELSE[ ] /* Else */ + ROLL[ ] /* RollTopThreeStack */ + SWAP[ ] /* SwapTopStack */ + EIF[ ] /* EndIf */ + DUP[ ] /* DuplicateTopStack */ + PUSH[ ] /* 1 value pushed */ + 3 + MINDEX[ ] /* MoveXToTopStack */ + GTEQ[ ] /* GreaterThanOrEqual */ + IF[ ] /* If */ + SWAP[ ] /* SwapTopStack */ + POP[ ] /* PopTopStack */ + PUSH[ ] /* 1 value pushed */ + 320 + ROLL[ ] /* RollTopThreeStack */ + ROLL[ ] /* RollTopThreeStack */ + ELSE[ ] /* Else */ + ROLL[ ] /* RollTopThreeStack */ + SWAP[ ] /* SwapTopStack */ + EIF[ ] /* EndIf */ + DUP[ ] /* DuplicateTopStack */ + PUSH[ ] /* 1 value pushed */ + 3 + MINDEX[ ] /* MoveXToTopStack */ + GTEQ[ ] /* GreaterThanOrEqual */ + IF[ ] /* If */ + PUSH[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + PUSH[ ] /* 1 value pushed */ + 384 + LT[ ] /* LessThan */ + IF[ ] /* If */ + SWAP[ ] /* SwapTopStack */ + POP[ ] /* PopTopStack */ + PUSH[ ] /* 1 value pushed */ + 384 + SWAP[ ] /* SwapTopStack */ + POP[ ] /* PopTopStack */ + ELSE[ ] /* Else */ + PUSH[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + SWAP[ ] /* SwapTopStack */ + POP[ ] /* PopTopStack */ + SWAP[ ] /* SwapTopStack */ + POP[ ] /* PopTopStack */ + EIF[ ] /* EndIf */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + EIF[ ] /* EndIf */ + WCVTP[ ] /* WriteCVTInPixels */ + ENDF[ ] /* EndFunctionDefinition */ + PUSH[ ] /* 1 value pushed */ + 9 + FDEF[ ] /* FunctionDefinition */ + MPPEM[ ] /* MeasurePixelPerEm */ + GTEQ[ ] /* GreaterThanOrEqual */ + IF[ ] /* If */ + RCVT[ ] /* ReadCVT */ + WCVTP[ ] /* WriteCVTInPixels */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + + + + + + PUSH[ ] /* 1 value pushed */ + 0 + CALL[ ] /* CallFunction */ + SVTCA[0] /* SetFPVectorToAxis */ + PUSH[ ] /* 3 values pushed */ + 1 1 2 + CALL[ ] /* CallFunction */ + SVTCA[1] /* SetFPVectorToAxis */ + PUSH[ ] /* 3 values pushed */ + 2 1 2 + CALL[ ] /* CallFunction */ + SVTCA[1] /* SetFPVectorToAxis */ + PUSH[ ] /* 8 values pushed */ + 2 275 225 175 125 75 0 8 + CALL[ ] /* CallFunction */ + SVTCA[0] /* SetFPVectorToAxis */ + PUSH[ ] /* 8 values pushed */ + 1 275 225 175 125 75 0 8 + CALL[ ] /* CallFunction */ + SVTCA[0] /* SetFPVectorToAxis */ + PUSH[ ] /* 3 values pushed */ + 3 2 7 + CALL[ ] /* CallFunction */ + PUSH[ ] /* 1 value pushed */ + 0 + DUP[ ] /* DuplicateTopStack */ + RCVT[ ] /* ReadCVT */ + RDTG[ ] /* RoundDownToGrid */ + ROUND[01] /* Round */ + RTG[ ] /* RoundToGrid */ + WCVTP[ ] /* WriteCVTInPixels */ + MPPEM[ ] /* MeasurePixelPerEm */ + PUSH[ ] /* 1 value pushed */ + 96 + GT[ ] /* GreaterThan */ + IF[ ] /* If */ + PUSH[ ] /* 1 value pushed */ + 1 + ELSE[ ] /* Else */ + PUSH[ ] /* 1 value pushed */ + 0 + EIF[ ] /* EndIf */ + PUSH[ ] /* 1 value pushed */ + 1 + INSTCTRL[ ] /* SetInstrExecControl */ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SVTCA[0] /* SetFPVectorToAxis */ + PUSH[ ] /* 3 values pushed */ + 1 2 3 + CALL[ ] /* CallFunction */ + IUP[0] /* InterpolateUntPts */ + IUP[1] /* InterpolateUntPts */ + + + + + + + + + + TestTTF + + + Regular + + + 1.000;UKWN;TestTTF-Regular + + + TestTTF + + + Version 1.000;PS 1.000;hotconv 1.0.88;makeotf.lib2.5.647800 DEVELOPMENT + + + TestTTF-Regular + + + TestTTF + + + Regular + + + 1.000;UKWN;TestTTF-Regular + + + TestTTF + + + Version 1.000;PS 1.000;hotconv 1.0.88;makeotf.lib2.5.647800 DEVELOPMENT + + + TestTTF-Regular + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/TestTTF-Regular_non_BMP_char.ttx b/Tests/subset/data/TestTTF-Regular_non_BMP_char.ttx new file mode 100644 index 0000000..c5890b4 --- /dev/null +++ b/Tests/subset/data/TestTTF-Regular_non_BMP_char.ttx @@ -0,0 +1,722 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PUSHB[ ] /* 1 value pushed */ + 0 + FDEF[ ] /* FunctionDefinition */ + MPPEM[ ] /* MeasurePixelPerEm */ + PUSHB[ ] /* 1 value pushed */ + 9 + LT[ ] /* LessThan */ + IF[ ] /* If */ + PUSHB[ ] /* 2 values pushed */ + 1 1 + INSTCTRL[ ] /* SetInstrExecControl */ + EIF[ ] /* EndIf */ + PUSHW[ ] /* 1 value pushed */ + 511 + SCANCTRL[ ] /* ScanConversionControl */ + PUSHB[ ] /* 1 value pushed */ + 68 + SCVTCI[ ] /* SetCVTCutIn */ + PUSHB[ ] /* 2 values pushed */ + 9 3 + SDS[ ] /* SetDeltaShiftInGState */ + SDB[ ] /* SetDeltaBaseInGState */ + ENDF[ ] /* EndFunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 1 + FDEF[ ] /* FunctionDefinition */ + DUP[ ] /* DuplicateTopStack */ + DUP[ ] /* DuplicateTopStack */ + RCVT[ ] /* ReadCVT */ + ROUND[01] /* Round */ + WCVTP[ ] /* WriteCVTInPixels */ + PUSHB[ ] /* 1 value pushed */ + 1 + ADD[ ] /* Add */ + ENDF[ ] /* EndFunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 2 + FDEF[ ] /* FunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 1 + LOOPCALL[ ] /* LoopAndCallFunction */ + POP[ ] /* PopTopStack */ + ENDF[ ] /* EndFunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 3 + FDEF[ ] /* FunctionDefinition */ + DUP[ ] /* DuplicateTopStack */ + GC[0] /* GetCoordOnPVector */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + GC[0] /* GetCoordOnPVector */ + GT[ ] /* GreaterThan */ + IF[ ] /* If */ + SWAP[ ] /* SwapTopStack */ + EIF[ ] /* EndIf */ + DUP[ ] /* DuplicateTopStack */ + ROLL[ ] /* RollTopThreeStack */ + DUP[ ] /* DuplicateTopStack */ + ROLL[ ] /* RollTopThreeStack */ + MD[0] /* MeasureDistance */ + ABS[ ] /* Absolute */ + ROLL[ ] /* RollTopThreeStack */ + DUP[ ] /* DuplicateTopStack */ + GC[0] /* GetCoordOnPVector */ + DUP[ ] /* DuplicateTopStack */ + ROUND[00] /* Round */ + SUB[ ] /* Subtract */ + ABS[ ] /* Absolute */ + PUSHB[ ] /* 1 value pushed */ + 4 + CINDEX[ ] /* CopyXToTopStack */ + GC[0] /* GetCoordOnPVector */ + DUP[ ] /* DuplicateTopStack */ + ROUND[00] /* Round */ + SUB[ ] /* Subtract */ + ABS[ ] /* Absolute */ + GT[ ] /* GreaterThan */ + IF[ ] /* If */ + SWAP[ ] /* SwapTopStack */ + NEG[ ] /* Negate */ + ROLL[ ] /* RollTopThreeStack */ + EIF[ ] /* EndIf */ + MDAP[1] /* MoveDirectAbsPt */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 0 + GTEQ[ ] /* GreaterThanOrEqual */ + IF[ ] /* If */ + ROUND[01] /* Round */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 0 + EQ[ ] /* Equal */ + IF[ ] /* If */ + POP[ ] /* PopTopStack */ + PUSHB[ ] /* 1 value pushed */ + 64 + EIF[ ] /* EndIf */ + ELSE[ ] /* Else */ + ROUND[01] /* Round */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 0 + EQ[ ] /* Equal */ + IF[ ] /* If */ + POP[ ] /* PopTopStack */ + PUSHB[ ] /* 1 value pushed */ + 64 + NEG[ ] /* Negate */ + EIF[ ] /* EndIf */ + EIF[ ] /* EndIf */ + MSIRP[0] /* MoveStackIndirRelPt */ + ENDF[ ] /* EndFunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 4 + FDEF[ ] /* FunctionDefinition */ + DUP[ ] /* DuplicateTopStack */ + GC[0] /* GetCoordOnPVector */ + PUSHB[ ] /* 1 value pushed */ + 4 + CINDEX[ ] /* CopyXToTopStack */ + GC[0] /* GetCoordOnPVector */ + GT[ ] /* GreaterThan */ + IF[ ] /* If */ + SWAP[ ] /* SwapTopStack */ + ROLL[ ] /* RollTopThreeStack */ + EIF[ ] /* EndIf */ + DUP[ ] /* DuplicateTopStack */ + GC[0] /* GetCoordOnPVector */ + DUP[ ] /* DuplicateTopStack */ + ROUND[10] /* Round */ + SUB[ ] /* Subtract */ + ABS[ ] /* Absolute */ + PUSHB[ ] /* 1 value pushed */ + 4 + CINDEX[ ] /* CopyXToTopStack */ + GC[0] /* GetCoordOnPVector */ + DUP[ ] /* DuplicateTopStack */ + ROUND[10] /* Round */ + SUB[ ] /* Subtract */ + ABS[ ] /* Absolute */ + GT[ ] /* GreaterThan */ + IF[ ] /* If */ + SWAP[ ] /* SwapTopStack */ + ROLL[ ] /* RollTopThreeStack */ + EIF[ ] /* EndIf */ + MDAP[1] /* MoveDirectAbsPt */ + MIRP[11101] /* MoveIndirectRelPt */ + ENDF[ ] /* EndFunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 5 + FDEF[ ] /* FunctionDefinition */ + MPPEM[ ] /* MeasurePixelPerEm */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 3 + MINDEX[ ] /* MoveXToTopStack */ + LT[ ] /* LessThan */ + IF[ ] /* If */ + LTEQ[ ] /* LessThenOrEqual */ + IF[ ] /* If */ + PUSHB[ ] /* 1 value pushed */ + 128 + WCVTP[ ] /* WriteCVTInPixels */ + ELSE[ ] /* Else */ + PUSHB[ ] /* 1 value pushed */ + 64 + WCVTP[ ] /* WriteCVTInPixels */ + EIF[ ] /* EndIf */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + DUP[ ] /* DuplicateTopStack */ + RCVT[ ] /* ReadCVT */ + PUSHB[ ] /* 1 value pushed */ + 192 + LT[ ] /* LessThan */ + IF[ ] /* If */ + PUSHB[ ] /* 1 value pushed */ + 192 + WCVTP[ ] /* WriteCVTInPixels */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + EIF[ ] /* EndIf */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 6 + FDEF[ ] /* FunctionDefinition */ + DUP[ ] /* DuplicateTopStack */ + DUP[ ] /* DuplicateTopStack */ + RCVT[ ] /* ReadCVT */ + ROUND[01] /* Round */ + WCVTP[ ] /* WriteCVTInPixels */ + PUSHB[ ] /* 1 value pushed */ + 1 + ADD[ ] /* Add */ + DUP[ ] /* DuplicateTopStack */ + DUP[ ] /* DuplicateTopStack */ + RCVT[ ] /* ReadCVT */ + RDTG[ ] /* RoundDownToGrid */ + ROUND[01] /* Round */ + RTG[ ] /* RoundToGrid */ + WCVTP[ ] /* WriteCVTInPixels */ + PUSHB[ ] /* 1 value pushed */ + 1 + ADD[ ] /* Add */ + ENDF[ ] /* EndFunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 7 + FDEF[ ] /* FunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 6 + LOOPCALL[ ] /* LoopAndCallFunction */ + ENDF[ ] /* EndFunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 8 + FDEF[ ] /* FunctionDefinition */ + MPPEM[ ] /* MeasurePixelPerEm */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 3 + MINDEX[ ] /* MoveXToTopStack */ + GTEQ[ ] /* GreaterThanOrEqual */ + IF[ ] /* If */ + PUSHB[ ] /* 1 value pushed */ + 64 + ELSE[ ] /* Else */ + PUSHB[ ] /* 1 value pushed */ + 0 + EIF[ ] /* EndIf */ + ROLL[ ] /* RollTopThreeStack */ + ROLL[ ] /* RollTopThreeStack */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 3 + MINDEX[ ] /* MoveXToTopStack */ + GTEQ[ ] /* GreaterThanOrEqual */ + IF[ ] /* If */ + SWAP[ ] /* SwapTopStack */ + POP[ ] /* PopTopStack */ + PUSHB[ ] /* 1 value pushed */ + 128 + ROLL[ ] /* RollTopThreeStack */ + ROLL[ ] /* RollTopThreeStack */ + ELSE[ ] /* Else */ + ROLL[ ] /* RollTopThreeStack */ + SWAP[ ] /* SwapTopStack */ + EIF[ ] /* EndIf */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 3 + MINDEX[ ] /* MoveXToTopStack */ + GTEQ[ ] /* GreaterThanOrEqual */ + IF[ ] /* If */ + SWAP[ ] /* SwapTopStack */ + POP[ ] /* PopTopStack */ + PUSHB[ ] /* 1 value pushed */ + 192 + ROLL[ ] /* RollTopThreeStack */ + ROLL[ ] /* RollTopThreeStack */ + ELSE[ ] /* Else */ + ROLL[ ] /* RollTopThreeStack */ + SWAP[ ] /* SwapTopStack */ + EIF[ ] /* EndIf */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 3 + MINDEX[ ] /* MoveXToTopStack */ + GTEQ[ ] /* GreaterThanOrEqual */ + IF[ ] /* If */ + SWAP[ ] /* SwapTopStack */ + POP[ ] /* PopTopStack */ + PUSHW[ ] /* 1 value pushed */ + 256 + ROLL[ ] /* RollTopThreeStack */ + ROLL[ ] /* RollTopThreeStack */ + ELSE[ ] /* Else */ + ROLL[ ] /* RollTopThreeStack */ + SWAP[ ] /* SwapTopStack */ + EIF[ ] /* EndIf */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 3 + MINDEX[ ] /* MoveXToTopStack */ + GTEQ[ ] /* GreaterThanOrEqual */ + IF[ ] /* If */ + SWAP[ ] /* SwapTopStack */ + POP[ ] /* PopTopStack */ + PUSHW[ ] /* 1 value pushed */ + 320 + ROLL[ ] /* RollTopThreeStack */ + ROLL[ ] /* RollTopThreeStack */ + ELSE[ ] /* Else */ + ROLL[ ] /* RollTopThreeStack */ + SWAP[ ] /* SwapTopStack */ + EIF[ ] /* EndIf */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 3 + MINDEX[ ] /* MoveXToTopStack */ + GTEQ[ ] /* GreaterThanOrEqual */ + IF[ ] /* If */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + PUSHW[ ] /* 1 value pushed */ + 384 + LT[ ] /* LessThan */ + IF[ ] /* If */ + SWAP[ ] /* SwapTopStack */ + POP[ ] /* PopTopStack */ + PUSHW[ ] /* 1 value pushed */ + 384 + SWAP[ ] /* SwapTopStack */ + POP[ ] /* PopTopStack */ + ELSE[ ] /* Else */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + SWAP[ ] /* SwapTopStack */ + POP[ ] /* PopTopStack */ + SWAP[ ] /* SwapTopStack */ + POP[ ] /* PopTopStack */ + EIF[ ] /* EndIf */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + EIF[ ] /* EndIf */ + WCVTP[ ] /* WriteCVTInPixels */ + ENDF[ ] /* EndFunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 9 + FDEF[ ] /* FunctionDefinition */ + MPPEM[ ] /* MeasurePixelPerEm */ + GTEQ[ ] /* GreaterThanOrEqual */ + IF[ ] /* If */ + RCVT[ ] /* ReadCVT */ + WCVTP[ ] /* WriteCVTInPixels */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + + + + + + PUSHB[ ] /* 1 value pushed */ + 0 + CALL[ ] /* CallFunction */ + SVTCA[0] /* SetFPVectorToAxis */ + PUSHB[ ] /* 3 values pushed */ + 1 1 2 + CALL[ ] /* CallFunction */ + SVTCA[1] /* SetFPVectorToAxis */ + PUSHB[ ] /* 3 values pushed */ + 2 1 2 + CALL[ ] /* CallFunction */ + SVTCA[1] /* SetFPVectorToAxis */ + PUSHW[ ] /* 2 values pushed */ + 2 275 + PUSHB[ ] /* 6 values pushed */ + 225 175 125 75 0 8 + CALL[ ] /* CallFunction */ + SVTCA[0] /* SetFPVectorToAxis */ + PUSHW[ ] /* 2 values pushed */ + 1 275 + PUSHB[ ] /* 6 values pushed */ + 225 175 125 75 0 8 + CALL[ ] /* CallFunction */ + SVTCA[0] /* SetFPVectorToAxis */ + PUSHB[ ] /* 3 values pushed */ + 3 2 7 + CALL[ ] /* CallFunction */ + PUSHB[ ] /* 1 value pushed */ + 0 + DUP[ ] /* DuplicateTopStack */ + RCVT[ ] /* ReadCVT */ + RDTG[ ] /* RoundDownToGrid */ + ROUND[01] /* Round */ + RTG[ ] /* RoundToGrid */ + WCVTP[ ] /* WriteCVTInPixels */ + MPPEM[ ] /* MeasurePixelPerEm */ + PUSHB[ ] /* 1 value pushed */ + 96 + GT[ ] /* GreaterThan */ + IF[ ] /* If */ + PUSHB[ ] /* 1 value pushed */ + 1 + ELSE[ ] /* Else */ + PUSHB[ ] /* 1 value pushed */ + 0 + EIF[ ] /* EndIf */ + PUSHB[ ] /* 1 value pushed */ + 1 + INSTCTRL[ ] /* SetInstrExecControl */ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SVTCA[0] /* SetFPVectorToAxis */ + PUSHB[ ] /* 3 values pushed */ + 1 2 3 + CALL[ ] /* CallFunction */ + IUP[0] /* InterpolateUntPts */ + IUP[1] /* InterpolateUntPts */ + + + + + + + + + + + + TestTTF + + + Regular + + + 1.000;UKWN;TestTTF-Regular + + + TestTTF + + + Version 1.000;PS 1.000;hotconv 1.0.88;makeotf.lib2.5.647800 DEVELOPMENT + + + TestTTF-Regular + + + TestTTF + + + Regular + + + 1.000;UKWN;TestTTF-Regular + + + TestTTF + + + Version 1.000;PS 1.000;hotconv 1.0.88;makeotf.lib2.5.647800 DEVELOPMENT + + + TestTTF-Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/expect_ankr.ttx b/Tests/subset/data/expect_ankr.ttx new file mode 100644 index 0000000..faad147 --- /dev/null +++ b/Tests/subset/data/expect_ankr.ttx @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/expect_bsln_0.ttx b/Tests/subset/data/expect_bsln_0.ttx new file mode 100644 index 0000000..89f68d4 --- /dev/null +++ b/Tests/subset/data/expect_bsln_0.ttx @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/expect_bsln_1.ttx b/Tests/subset/data/expect_bsln_1.ttx new file mode 100644 index 0000000..71e1df4 --- /dev/null +++ b/Tests/subset/data/expect_bsln_1.ttx @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/expect_bsln_2.ttx b/Tests/subset/data/expect_bsln_2.ttx new file mode 100644 index 0000000..6d2da18 --- /dev/null +++ b/Tests/subset/data/expect_bsln_2.ttx @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/expect_bsln_3.ttx b/Tests/subset/data/expect_bsln_3.ttx new file mode 100644 index 0000000..04f3f9a --- /dev/null +++ b/Tests/subset/data/expect_bsln_3.ttx @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/expect_desubroutinize_CFF.ttx b/Tests/subset/data/expect_desubroutinize_CFF.ttx new file mode 100644 index 0000000..81b5369 --- /dev/null +++ b/Tests/subset/data/expect_desubroutinize_CFF.ttx @@ -0,0 +1,238 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -63 endchar + + + 220 -93 -21 114 -20 297 181 -59 59 292 -20 hstemhm + 9 118 -43 120 hintmask 11101100 + 535 hmoveto + 157 736 rlineto + 10 -24 -32 4 -23 hhcurveto + -117 -130 -135 -160 -101 hvcurveto + 2 -21 -17 1 -14 hhcurveto + -118 -86 -55 -68 -39 28 -19 34 31 25 15 24 14 -8 17 -5 hvcurveto + hintmask 11011010 + 13 34 42 14 62 4 rrcurveto + -87 -153 -60 -164 -90 vvcurveto + -104 80 -2 54 vhcurveto + -6 9 -8 15 32 vvcurveto + 104 55 190 75 163 vhcurveto + 44 -4 39 -9 51 -23 -77 -363 rcurveline + 86 407 rmoveto + -39 16 -43 11 -40 8 56 112 64 93 60 32 rrcurveto + endchar + + + 142 -92 -21 113 -20 386 52 333 -20 hstem + 8 120 vstem + 459 hmoveto + 157 736 rlineto + 12 -30 -26 3 -24 hhcurveto + -238 -290 -563 -189 -106 65 -2 69 -4 hvcurveto + -1 9 -13 -4 51 vvcurveto + 97 42 172 64 154 vhcurveto + 158 hlineto + -77 -366 rlineto + -59 418 rmoveto + 58 126 72 106 73 32 -56 -264 rcurveline + endchar + + + 187 -17 96 -79 -20 406 48 270 46 hstemhm + 6 93 362 139 -119 101 -101 119 hintmask 01111100 + 230 636 rmoveto + -136 -636 rlineto + 144 hlineto + 82 383 rlineto + 2 18 20 1 8 hhcurveto + 73 22 -57 -70 hvcurveto + hintmask 10111001 + -76 -26 -104 -73 -23 -19 10 26 -25 vhcurveto + -9 -23 -4 -19 -16 vvcurveto + -61 56 -13 43 167 52 192 96 75 -33 69 -85 17 vhcurveto + hintmask 10111010 + 65 37 35 63 59 vvcurveto + 82 -66 77 -147 -189 -174 -127 -138 -67 41 -25 66 vhcurveto + -1 9 -13 8 51 vvcurveto + 165 133 78 117 95 37 -51 -57 -75 -64 -87 -80 vhcurveto + -6 hlineto + 47 222 rlineto + endchar + + + 185 -28 92 -64 -20 413 41 270 46 hstemhm + 6 93 350 149 -119 105 -105 119 hintmask 01111100 + 230 636 rmoveto + -136 -636 rlineto + 144 hlineto + 6 30 rlineto + hintmask 10111001 + -41 39 41 -17 39 hhcurveto + 125 110 175 136 72 -32 62 -82 15 hvcurveto + hintmask 10111010 + 64 38 36 61 58 vvcurveto + 83 -74 78 -144 -183 -177 -126 -139 -67 41 -25 66 vhcurveto + -1 9 -13 8 51 vvcurveto + 152 116 91 138 101 25 -49 -53 -81 -59 -87 -83 vhcurveto + -6 hlineto + 47 222 rlineto + -59 -592 rmoveto + -20 -21 8 21 -20 hvcurveto + 62 290 rlineto + 2 18 20 1 7 hhcurveto + hintmask 10111100 + 63 21 -49 -57 -96 -58 -120 -72 hvcurveto + endchar + + + -73 21 -21 750 -20 hstem + 6 93 vstem + 397 748 rmoveto + 1 -13 -13 1 -14 hhcurveto + -167 -184 -127 -133 -72 38 -25 69 hvcurveto + -1 9 -13 8 51 vvcurveto + 107 53 75 87 36 vhcurveto + -145 -679 rlineto + 144 hlineto + endchar + + + 215 -207 50 157 -20 770 -20 hstemhm + 6 93 13 84 -84 205 hintmask 11111000 + 397 748 rmoveto + 1 -13 -13 1 -14 hhcurveto + -167 -184 -127 -133 -72 38 -25 69 hvcurveto + -1 9 -13 8 51 vvcurveto + 107 53 75 87 36 vhcurveto + -145 -679 rlineto + 34 hlineto + -11 -20 -5 -23 -27 vvcurveto + -79 48 -58 113 155 66 109 138 29 vhcurveto + 150 710 -150 -33 -164 -751 rlineto + -100 -22 -30 -23 -40 hhcurveto + -44 -27 29 39 40 29 33 36 16 17 -7 -16 16 hvcurveto + hintmask 11110100 + 4 11 3 11 11 vvcurveto + 34 -26 24 -41 6 vhcurveto + endchar + + + 88 -207 50 144 81 682 -20 hstemhm + 17 84 -84 220 -50 93 hintmask 11110100 + 538 750 rmoveto + -167 -184 -127 -133 -72 38 -25 69 hvcurveto + -1 9 -13 8 51 vvcurveto + 107 54 76 87 36 vhcurveto + -157 -714 rlineto + -103 -23 -27 -20 -45 hhcurveto + -29 -39 18 52 37 24 37 46 20 15 -5 -21 25 hvcurveto + hintmask 11101000 + 4 15 2 14 11 vvcurveto + 64 -58 3 -40 -79 -43 -66 -68 -83 53 -58 95 164 67 94 153 32 vhcurveto + 150 710 rlineto + endchar + + + -131 21 -21 624 46 78 -20 hstem + 324 748 rmoveto + -72 -121 -78 -6 -55 hhcurveto + -12 -46 rlineto + 95 hlineto + -132 -624 rlineto + 144 hlineto + endchar + + + 66 -5 65 197 51 204 237 -54 54 hstemhm + 6 111 -12 110 117 155 -117 117 hintmask 11101001 + 205 257 rmoveto + 38 -8 -33 13 -37 hhcurveto + -80 -41 -60 -83 -154 141 -16 58 171 111 136 121 71 -38 65 -88 29 hvcurveto + 92 46 45 74 66 vvcurveto + 78 -63 68 -123 vhcurveto + hintmask 11100110 + -116 -91 -61 -91 -54 32 -31 40 24 27 11 23 25 hvcurveto + -28 8 -10 36 27 vvcurveto + hintmask 11011001 + 47 31 31 48 51 25 -36 -46 -70 -58 -94 -113 -31 vhcurveto + hintmask 11101010 + 93 -33 40 -80 -76 vvcurveto + -87 -53 -82 -86 -37 -39 13 76 40 10 62 78 6 vhcurveto + endchar + + + 44 -11 125 -89 89 -89 107 380 237 -54 54 hstemhm + 66 110 142 119 -119 144 hintmask 00110101 + 111 132 rmoveto + -5 hlineto + 83 135 273 98 223 vvcurveto + 97 -53 64 -137 -151 -55 -79 -68 -58 31 -32 41 24 26 11 23 26 vhcurveto + -28 8 -10 37 23 vvcurveto + hintmask 01001110 + 50 14 31 67 29 32 -33 -49 vhcurveto + -266 -329 -98 -219 vvcurveto + -11 0 -11 2 -11 vhcurveto + 7 20 36 21 23 hhcurveto + hintmask 10010110 + 102 37 -36 109 hhcurveto + 99 20 52 98 14 0 14 -1 16 hvcurveto + -44 -47 -17 -25 -70 hhcurveto + hintmask 00110110 + -75 -57 18 -59 hhcurveto + endchar + + + 98 -9 84 623 52 hstem + 30 158 236 131 vstem + 377 750 rmoveto + -215 -132 -223 -273 -166 35 -97 172 205 113 299 199 168 -53 93 -125 hvcurveto + -189 -425 rmoveto + 225 17 105 148 60 hhcurveto + 47 7 -63 -82 -232 -68 -246 -114 -48 -11 77 74 37 3 35 2 27 hvcurveto + endchar + + + + + + + + + + diff --git a/Tests/subset/data/expect_keep_colr.ttx b/Tests/subset/data/expect_keep_colr.ttx new file mode 100644 index 0000000..d6736ef --- /dev/null +++ b/Tests/subset/data/expect_keep_colr.ttx @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/expect_keep_gvar.ttx b/Tests/subset/data/expect_keep_gvar.ttx new file mode 100644 index 0000000..43e4a34 --- /dev/null +++ b/Tests/subset/data/expect_keep_gvar.ttx @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + wght + 0x0 + 100.0 + 400.0 + 900.0 + 257 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TestGVAR + + + Regular + + + 1.000;UKWN;TestGVAR-Regular + + + TestGVAR-Regular + + + Version 1.000 + + + TestGVAR-Regular + + + Weight + + + Thin + + + Light + + + Regular + + + Bold + + + Black + + + + diff --git a/Tests/subset/data/expect_keep_gvar_notdef_outline.ttx b/Tests/subset/data/expect_keep_gvar_notdef_outline.ttx new file mode 100644 index 0000000..d4cc79f --- /dev/null +++ b/Tests/subset/data/expect_keep_gvar_notdef_outline.ttx @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + wght + 0x0 + 100.0 + 400.0 + 900.0 + 257 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TestGVAR + + + Regular + + + 1.000;UKWN;TestGVAR-Regular + + + TestGVAR-Regular + + + Version 1.000 + + + TestGVAR-Regular + + + Weight + + + Thin + + + Light + + + Regular + + + Bold + + + Black + + + + diff --git a/Tests/subset/data/expect_keep_math.ttx b/Tests/subset/data/expect_keep_math.ttx new file mode 100644 index 0000000..c734dd9 --- /dev/null +++ b/Tests/subset/data/expect_keep_math.ttx @@ -0,0 +1,634 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 19 vlineto + -52 6 -14 21 -28 65 rrcurveto + -246 563 -20 0 -206 -488 rlineto + -59 -140 -9 -21 -58 -6 rrcurveto + -19 199 19 vlineto + -48 -22 10 31 hvcurveto + 0 12 4 17 5 13 rrcurveto + 46 114 262 0 41 -94 rlineto + 12 -28 7 -27 0 -15 0 -9 -6 -11 -8 -4 -12 -7 -7 -2 -36 0 rrcurveto + -19 vlineto + return + + + -231 0 115 275 rlineto + return + + + -125 167 -62 0 -124 -167 34 0 121 103 122 -103 rlineto + return + + + 25 vlineto + -41 0 -18 22 -51 121 rrcurveto + -222 522 -28 0 -221 -545 rlineto + -38 -94 -15 -18 -46 -8 rrcurveto + -25 202 25 vlineto + -59 4 -22 11 0 26 0 24 19 42 12 31 rrcurveto + 13 34 225 0 rlineto + 34 -79 12 -35 0 -22 0 -22 -13 -8 -33 -3 rrcurveto + -32 -3 0 -25 rlineto + return + + + -5 -16 19 0 rlineto + 59 19 -16 -26 hvcurveto + 0 -6 -1 -10 -1 -5 rrcurveto + -123 -495 rlineto + -8 -34 -19 -29 -76 0 rrcurveto + -20 0 -4 -16 324 0 rlineto + 207 82 94 99 hvcurveto + 0 85 -67 42 -60 11 rrcurveto + -3 4 rlineto + 20 5 29 8 25 13 48 24 43 45 0 78 rrcurveto + 136 -152 9 -84 vhcurveto + return + + + 13 2 15 1 15 0 rrcurveto + 54 65 -21 -77 -97 -56 -66 -112 hvcurveto + -15 0 -18 2 -20 4 rrcurveto + return + + + 15 4 20 0 16 0 rrcurveto + 78 64 -35 -73 -130 -96 -42 -94 hvcurveto + -26 0 -20 2 -25 7 rrcurveto + return + + + + + + -351 endchar + + + 121 0 20 196 41 397 20 hstem + 707 hmoveto + -107 callsubr + -5 257 rmoveto + -106 callsubr + endchar + + + -268 656 20 hstem + 48 86 vstem + 304 -161 rmoveto + -140 117 -30 113 0 186 0 193 31 93 139 119 rrcurveto + -9 16 rlineto + -160 -95 -87 -144 0 -185 0 -170 86 -169 158 -90 rrcurveto + endchar + + + -133 139 81 vstem + 382 -134 rmoveto + -90 110 -72 169 0 306 0 303 72 172 90 110 rrcurveto + 30 vlineto + -142 -134 -101 -214 0 -267 0 -272 101 -209 142 -134 rrcurveto + endchar + + + -12 139 95 vstem + 503 -243 rmoveto + -134 165 -135 265 0 456 0 458 135 294 134 138 rrcurveto + 33 vlineto + -213 -171 -151 -352 0 -400 0 -409 151 -313 213 -200 rrcurveto + endchar + + + 149 182 110 vstem + 667 -346 rmoveto + -178 220 -197 349 0 613 0 606 197 396 178 184 rrcurveto + 44 vlineto + -284 -228 -201 -464 0 -538 0 -541 201 -422 284 -267 rrcurveto + endchar + + + 207 124 130 vstem + 732 -453 rmoveto + -224 232 -254 504 0 746 0 777 254 473 224 231 rrcurveto + 56 vlineto + -355 -286 -253 -578 0 -673 0 -675 253 -577 355 -286 rrcurveto + endchar + + + 121 0 20 177 39 hstem + 689 hmoveto + -104 callsubr + 17 236 rmoveto + -195 0 94 243 rlineto + endchar + + + 95 0 38 280 35 261 39 hstem + 198 653 rmoveto + -103 callsubr + -44 -42 rmoveto + -102 callsubr + -9 -45 rmoveto + -101 callsubr + endchar + + + -601 654 20 hstem + -75 507 rmoveto + -105 callsubr + endchar + + + -41 560 554 rmoveto + -256 213 -48 0 -256 -213 64 0 216 146 216 -146 rlineto + endchar + + + 378 979 564 rmoveto + -465 213 -48 0 -466 -213 99 0 391 145 390 -145 rlineto + endchar + + + 859 1460 564 rmoveto + -706 213 -48 0 -706 -213 153 0 577 145 577 -145 rlineto + endchar + + + 1285 1886 599 rmoveto + -943 197 -943 -197 5 -26 937 161 939 -161 rlineto + endchar + + + 1727 2328 603 rmoveto + -1164 213 -1164 -213 5 -31 1158 182 1160 -182 rlineto + endchar + + + -151 50 124 vstem + 400 1005 rmoveto + -261 -184 -89 -359 0 -303 rrcurveto + -159 124 239 vlineto + 0 254 54 285 172 197 rrcurveto + endchar + + + -151 50 124 vstem + 174 hmoveto + 1010 -124 -1010 vlineto + endchar + + + -151 50 124 vstem + 400 30 rmoveto + -172 197 -54 285 0 254 rrcurveto + 239 -124 -159 vlineto + 0 -303 89 -359 261 -184 rrcurveto + endchar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/expect_lcar_0.ttx b/Tests/subset/data/expect_lcar_0.ttx new file mode 100644 index 0000000..feb866d --- /dev/null +++ b/Tests/subset/data/expect_lcar_0.ttx @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/expect_lcar_1.ttx b/Tests/subset/data/expect_lcar_1.ttx new file mode 100644 index 0000000..26b5008 --- /dev/null +++ b/Tests/subset/data/expect_lcar_1.ttx @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/expect_no_hinting_CFF.ttx b/Tests/subset/data/expect_no_hinting_CFF.ttx new file mode 100644 index 0000000..c2d1872 --- /dev/null +++ b/Tests/subset/data/expect_no_hinting_CFF.ttx @@ -0,0 +1,212 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 397 748 rmoveto + 1 -13 -13 1 -14 hhcurveto + -106 callsubr + 53 75 87 36 vhcurveto + -145 -679 rlineto + return + + + -167 -184 -127 -133 -72 38 -25 69 hvcurveto + -1 9 -13 8 51 vvcurveto + 107 return + + + 230 636 rmoveto + -136 -636 rlineto + 144 hlineto + return + + + -67 41 -25 66 vhcurveto + -1 9 -13 8 51 vvcurveto + return + + + + + + -63 endchar + + + 220 535 hmoveto + 157 736 rlineto + 10 -24 -32 4 -23 hhcurveto + -117 -130 -135 -160 -101 hvcurveto + 2 -21 -17 1 -14 hhcurveto + -118 -86 -55 -68 -39 28 -19 34 31 25 15 24 14 -8 17 -5 hvcurveto + 13 34 42 14 62 4 rrcurveto + -87 -153 -60 -164 -90 vvcurveto + -104 80 -2 54 vhcurveto + -6 9 -8 15 32 vvcurveto + 104 55 190 75 163 vhcurveto + 44 -4 39 -9 51 -23 -77 -363 rcurveline + 86 407 rmoveto + -39 16 -43 11 -40 8 56 112 64 93 60 32 rrcurveto + endchar + + + 142 459 hmoveto + 157 736 rlineto + 12 -30 -26 3 -24 hhcurveto + -238 -290 -563 -189 -106 65 -2 69 -4 hvcurveto + -1 9 -13 -4 51 vvcurveto + 97 42 172 64 154 vhcurveto + 158 hlineto + -77 -366 rlineto + -59 418 rmoveto + 58 126 72 106 73 32 -56 -264 rcurveline + endchar + + + 187 -105 callsubr + 82 383 rlineto + 2 18 20 1 8 hhcurveto + 73 22 -57 -70 hvcurveto + -76 -26 -104 -73 -23 -19 10 26 -25 vhcurveto + -9 -23 -4 -19 -16 vvcurveto + -61 56 -13 43 167 52 192 96 75 -33 69 -85 17 vhcurveto + 65 37 35 63 59 vvcurveto + 82 -66 77 -147 -189 -174 -127 -138 -104 callsubr + 165 133 78 117 95 37 -51 -57 -75 -64 -87 -80 vhcurveto + -6 hlineto + 47 222 rlineto + endchar + + + 185 -105 callsubr + 6 30 rlineto + -41 39 41 -17 39 hhcurveto + 125 110 175 136 72 -32 62 -82 15 hvcurveto + 64 38 36 61 58 vvcurveto + 83 -74 78 -144 -183 -177 -126 -139 -104 callsubr + 152 116 91 138 101 25 -49 -53 -81 -59 -87 -83 vhcurveto + -6 hlineto + 47 222 rlineto + -59 -592 rmoveto + -20 -21 8 21 -20 hvcurveto + 62 290 rlineto + 2 18 20 1 7 hhcurveto + 63 21 -49 -57 -96 -58 -120 -72 hvcurveto + endchar + + + -73 -107 callsubr + 144 hlineto + endchar + + + 215 -107 callsubr + 34 hlineto + -11 -20 -5 -23 -27 vvcurveto + -79 48 -58 113 155 66 109 138 29 vhcurveto + 150 710 -150 -33 -164 -751 rlineto + -100 -22 -30 -23 -40 hhcurveto + -44 -27 29 39 40 29 33 36 16 17 -7 -16 16 hvcurveto + 4 11 3 11 11 vvcurveto + 34 -26 24 -41 6 vhcurveto + endchar + + + 88 538 750 rmoveto + -106 callsubr + 54 76 87 36 vhcurveto + -157 -714 rlineto + -103 -23 -27 -20 -45 hhcurveto + -29 -39 18 52 37 24 37 46 20 15 -5 -21 25 hvcurveto + 4 15 2 14 11 vvcurveto + 64 -58 3 -40 -79 -43 -66 -68 -83 53 -58 95 164 67 94 153 32 vhcurveto + 150 710 rlineto + endchar + + + -131 324 748 rmoveto + -72 -121 -78 -6 -55 hhcurveto + -12 -46 rlineto + 95 hlineto + -132 -624 rlineto + 144 hlineto + endchar + + + 66 205 257 rmoveto + 38 -8 -33 13 -37 hhcurveto + -80 -41 -60 -83 -154 141 -16 58 171 111 136 121 71 -38 65 -88 29 hvcurveto + 92 46 45 74 66 vvcurveto + 78 -63 68 -123 vhcurveto + -116 -91 -61 -91 -54 32 -31 40 24 27 11 23 25 hvcurveto + -28 8 -10 36 27 vvcurveto + 47 31 31 48 51 25 -36 -46 -70 -58 -94 -113 -31 vhcurveto + 93 -33 40 -80 -76 vvcurveto + -87 -53 -82 -86 -37 -39 13 76 40 10 62 78 6 vhcurveto + endchar + + + 44 111 132 rmoveto + -5 hlineto + 83 135 273 98 223 vvcurveto + 97 -53 64 -137 -151 -55 -79 -68 -58 31 -32 41 24 26 11 23 26 vhcurveto + -28 8 -10 37 23 vvcurveto + 50 14 31 67 29 32 -33 -49 vhcurveto + -266 -329 -98 -219 vvcurveto + -11 0 -11 2 -11 vhcurveto + 7 20 36 21 23 hhcurveto + 102 37 -36 109 hhcurveto + 99 20 52 98 14 0 14 -1 16 hvcurveto + -44 -47 -17 -25 -70 hhcurveto + -75 -57 18 -59 hhcurveto + endchar + + + 98 377 750 rmoveto + -215 -132 -223 -273 -166 35 -97 172 205 113 299 199 168 -53 93 -125 hvcurveto + -189 -425 rmoveto + 225 17 105 148 60 hhcurveto + 47 7 -63 -82 -232 -68 -246 -114 -48 -11 77 74 37 3 35 2 27 hvcurveto + endchar + + + + + + + + + + diff --git a/Tests/subset/data/expect_no_hinting_TTF.ttx b/Tests/subset/data/expect_no_hinting_TTF.ttx new file mode 100644 index 0000000..4c2e55b --- /dev/null +++ b/Tests/subset/data/expect_no_hinting_TTF.ttx @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/expect_no_hinting_desubroutinize_CFF.ttx b/Tests/subset/data/expect_no_hinting_desubroutinize_CFF.ttx new file mode 100644 index 0000000..7f44c90 --- /dev/null +++ b/Tests/subset/data/expect_no_hinting_desubroutinize_CFF.ttx @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -63 endchar + + + 220 535 hmoveto + 157 736 rlineto + 10 -24 -32 4 -23 hhcurveto + -117 -130 -135 -160 -101 hvcurveto + 2 -21 -17 1 -14 hhcurveto + -118 -86 -55 -68 -39 28 -19 34 31 25 15 24 14 -8 17 -5 hvcurveto + 13 34 42 14 62 4 rrcurveto + -87 -153 -60 -164 -90 vvcurveto + -104 80 -2 54 vhcurveto + -6 9 -8 15 32 vvcurveto + 104 55 190 75 163 vhcurveto + 44 -4 39 -9 51 -23 -77 -363 rcurveline + 86 407 rmoveto + -39 16 -43 11 -40 8 56 112 64 93 60 32 rrcurveto + endchar + + + 142 459 hmoveto + 157 736 rlineto + 12 -30 -26 3 -24 hhcurveto + -238 -290 -563 -189 -106 65 -2 69 -4 hvcurveto + -1 9 -13 -4 51 vvcurveto + 97 42 172 64 154 vhcurveto + 158 hlineto + -77 -366 rlineto + -59 418 rmoveto + 58 126 72 106 73 32 -56 -264 rcurveline + endchar + + + 187 230 636 rmoveto + -136 -636 rlineto + 144 hlineto + 82 383 rlineto + 2 18 20 1 8 hhcurveto + 73 22 -57 -70 hvcurveto + -76 -26 -104 -73 -23 -19 10 26 -25 vhcurveto + -9 -23 -4 -19 -16 vvcurveto + -61 56 -13 43 167 52 192 96 75 -33 69 -85 17 vhcurveto + 65 37 35 63 59 vvcurveto + 82 -66 77 -147 -189 -174 -127 -138 -67 41 -25 66 vhcurveto + -1 9 -13 8 51 vvcurveto + 165 133 78 117 95 37 -51 -57 -75 -64 -87 -80 vhcurveto + -6 hlineto + 47 222 rlineto + endchar + + + 185 230 636 rmoveto + -136 -636 rlineto + 144 hlineto + 6 30 rlineto + -41 39 41 -17 39 hhcurveto + 125 110 175 136 72 -32 62 -82 15 hvcurveto + 64 38 36 61 58 vvcurveto + 83 -74 78 -144 -183 -177 -126 -139 -67 41 -25 66 vhcurveto + -1 9 -13 8 51 vvcurveto + 152 116 91 138 101 25 -49 -53 -81 -59 -87 -83 vhcurveto + -6 hlineto + 47 222 rlineto + -59 -592 rmoveto + -20 -21 8 21 -20 hvcurveto + 62 290 rlineto + 2 18 20 1 7 hhcurveto + 63 21 -49 -57 -96 -58 -120 -72 hvcurveto + endchar + + + -73 397 748 rmoveto + 1 -13 -13 1 -14 hhcurveto + -167 -184 -127 -133 -72 38 -25 69 hvcurveto + -1 9 -13 8 51 vvcurveto + 107 53 75 87 36 vhcurveto + -145 -679 rlineto + 144 hlineto + endchar + + + 215 397 748 rmoveto + 1 -13 -13 1 -14 hhcurveto + -167 -184 -127 -133 -72 38 -25 69 hvcurveto + -1 9 -13 8 51 vvcurveto + 107 53 75 87 36 vhcurveto + -145 -679 rlineto + 34 hlineto + -11 -20 -5 -23 -27 vvcurveto + -79 48 -58 113 155 66 109 138 29 vhcurveto + 150 710 -150 -33 -164 -751 rlineto + -100 -22 -30 -23 -40 hhcurveto + -44 -27 29 39 40 29 33 36 16 17 -7 -16 16 hvcurveto + 4 11 3 11 11 vvcurveto + 34 -26 24 -41 6 vhcurveto + endchar + + + 88 538 750 rmoveto + -167 -184 -127 -133 -72 38 -25 69 hvcurveto + -1 9 -13 8 51 vvcurveto + 107 54 76 87 36 vhcurveto + -157 -714 rlineto + -103 -23 -27 -20 -45 hhcurveto + -29 -39 18 52 37 24 37 46 20 15 -5 -21 25 hvcurveto + 4 15 2 14 11 vvcurveto + 64 -58 3 -40 -79 -43 -66 -68 -83 53 -58 95 164 67 94 153 32 vhcurveto + 150 710 rlineto + endchar + + + -131 324 748 rmoveto + -72 -121 -78 -6 -55 hhcurveto + -12 -46 rlineto + 95 hlineto + -132 -624 rlineto + 144 hlineto + endchar + + + 66 205 257 rmoveto + 38 -8 -33 13 -37 hhcurveto + -80 -41 -60 -83 -154 141 -16 58 171 111 136 121 71 -38 65 -88 29 hvcurveto + 92 46 45 74 66 vvcurveto + 78 -63 68 -123 vhcurveto + -116 -91 -61 -91 -54 32 -31 40 24 27 11 23 25 hvcurveto + -28 8 -10 36 27 vvcurveto + 47 31 31 48 51 25 -36 -46 -70 -58 -94 -113 -31 vhcurveto + 93 -33 40 -80 -76 vvcurveto + -87 -53 -82 -86 -37 -39 13 76 40 10 62 78 6 vhcurveto + endchar + + + 44 111 132 rmoveto + -5 hlineto + 83 135 273 98 223 vvcurveto + 97 -53 64 -137 -151 -55 -79 -68 -58 31 -32 41 24 26 11 23 26 vhcurveto + -28 8 -10 37 23 vvcurveto + 50 14 31 67 29 32 -33 -49 vhcurveto + -266 -329 -98 -219 vvcurveto + -11 0 -11 2 -11 vhcurveto + 7 20 36 21 23 hhcurveto + 102 37 -36 109 hhcurveto + 99 20 52 98 14 0 14 -1 16 hvcurveto + -44 -47 -17 -25 -70 hhcurveto + -75 -57 18 -59 hhcurveto + endchar + + + 98 377 750 rmoveto + -215 -132 -223 -273 -166 35 -97 172 205 113 299 199 168 -53 93 -125 hvcurveto + -189 -425 rmoveto + 225 17 105 148 60 hhcurveto + 47 7 -63 -82 -232 -68 -246 -114 -48 -11 77 74 37 3 35 2 27 hvcurveto + endchar + + + + + + + + + + diff --git a/Tests/subset/data/expect_no_notdef_outline_cid.ttx b/Tests/subset/data/expect_no_notdef_outline_cid.ttx new file mode 100644 index 0000000..5167c2c --- /dev/null +++ b/Tests/subset/data/expect_no_notdef_outline_cid.ttx @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + endchar + + + + + + + + + + diff --git a/Tests/subset/data/expect_no_notdef_outline_otf.ttx b/Tests/subset/data/expect_no_notdef_outline_otf.ttx new file mode 100644 index 0000000..22273c7 --- /dev/null +++ b/Tests/subset/data/expect_no_notdef_outline_otf.ttx @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 196 endchar + + + + + + + + + + diff --git a/Tests/subset/data/expect_no_notdef_outline_ttf.ttx b/Tests/subset/data/expect_no_notdef_outline_ttf.ttx new file mode 100644 index 0000000..4096adb --- /dev/null +++ b/Tests/subset/data/expect_no_notdef_outline_ttf.ttx @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/expect_notdef_width_cid.ttx b/Tests/subset/data/expect_notdef_width_cid.ttx new file mode 100644 index 0000000..ccd0f65 --- /dev/null +++ b/Tests/subset/data/expect_notdef_width_cid.ttx @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + endchar + + + -407 endchar + + + + + + + + + + diff --git a/Tests/subset/data/expect_opbd_0.ttx b/Tests/subset/data/expect_opbd_0.ttx new file mode 100644 index 0000000..55842a0 --- /dev/null +++ b/Tests/subset/data/expect_opbd_0.ttx @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/expect_opbd_1.ttx b/Tests/subset/data/expect_opbd_1.ttx new file mode 100644 index 0000000..080abd9 --- /dev/null +++ b/Tests/subset/data/expect_opbd_1.ttx @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/expect_prop_0.ttx b/Tests/subset/data/expect_prop_0.ttx new file mode 100644 index 0000000..f8ca150 --- /dev/null +++ b/Tests/subset/data/expect_prop_0.ttx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Tests/subset/data/expect_prop_1.ttx b/Tests/subset/data/expect_prop_1.ttx new file mode 100644 index 0000000..f7f2d23 --- /dev/null +++ b/Tests/subset/data/expect_prop_1.ttx @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/Tests/subset/data/google_color.ttx b/Tests/subset/data/google_color.ttx new file mode 100644 index 0000000..2eca0ac --- /dev/null +++ b/Tests/subset/data/google_color.ttx @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Meh + + + ZOMG + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + DEAD + + + + + + + + + + + + F00D + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/subset_test.py b/Tests/subset/subset_test.py new file mode 100644 index 0000000..e9b3cba --- /dev/null +++ b/Tests/subset/subset_test.py @@ -0,0 +1,482 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools import subset +from fontTools.ttLib import TTFont, newTable +from fontTools.misc.loggingTools import CapturingLogHandler +import difflib +import logging +import os +import shutil +import sys +import tempfile +import unittest + + +class SubsetTest(unittest.TestCase): + def __init__(self, methodName): + unittest.TestCase.__init__(self, methodName) + # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, + # and fires deprecation warnings if a program uses the old name. + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + + def setUp(self): + self.tempdir = None + self.num_tempfiles = 0 + + def tearDown(self): + if self.tempdir: + shutil.rmtree(self.tempdir) + + @staticmethod + def getpath(testfile): + path, _ = os.path.split(__file__) + return os.path.join(path, "data", testfile) + + def temp_path(self, suffix): + if not self.tempdir: + self.tempdir = tempfile.mkdtemp() + self.num_tempfiles += 1 + return os.path.join(self.tempdir, + "tmp%d%s" % (self.num_tempfiles, suffix)) + + def read_ttx(self, path): + lines = [] + with open(path, "r", encoding="utf-8") as ttx: + for line in ttx.readlines(): + # Elide ttFont attributes because ttLibVersion may change, + # and use os-native line separators so we can run difflib. + if line.startswith("" + os.linesep) + else: + lines.append(line.rstrip() + os.linesep) + return lines + + def expect_ttx(self, font, expected_ttx, tables): + path = self.temp_path(suffix=".ttx") + font.saveXML(path, tables=tables) + actual = self.read_ttx(path) + expected = self.read_ttx(expected_ttx) + if actual != expected: + for line in difflib.unified_diff( + expected, actual, fromfile=expected_ttx, tofile=path): + sys.stdout.write(line) + self.fail("TTX output is different from expected") + + def compile_font(self, path, suffix): + savepath = self.temp_path(suffix=suffix) + font = TTFont(recalcBBoxes=False, recalcTimestamp=False) + font.importXML(path) + font.save(savepath, reorderTables=None) + return font, savepath + +# ----- +# Tests +# ----- + + def test_no_notdef_outline_otf(self): + _, fontpath = self.compile_font(self.getpath("TestOTF-Regular.ttx"), ".otf") + subsetpath = self.temp_path(".otf") + subset.main([fontpath, "--no-notdef-outline", "--gids=0", "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("expect_no_notdef_outline_otf.ttx"), ["CFF "]) + + def test_no_notdef_outline_cid(self): + _, fontpath = self.compile_font(self.getpath("TestCID-Regular.ttx"), ".otf") + subsetpath = self.temp_path(".otf") + subset.main([fontpath, "--no-notdef-outline", "--gids=0", "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("expect_no_notdef_outline_cid.ttx"), ["CFF "]) + + def test_no_notdef_outline_ttf(self): + _, fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--no-notdef-outline", "--gids=0", "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("expect_no_notdef_outline_ttf.ttx"), ["glyf", "hmtx"]) + + def test_subset_ankr(self): + _, fontpath = self.compile_font(self.getpath("TestANKR.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--glyphs=one", "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("expect_ankr.ttx"), ["ankr"]) + + def test_subset_ankr_remove(self): + _, fontpath = self.compile_font(self.getpath("TestANKR.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--glyphs=two", "--output-file=%s" % subsetpath]) + self.assertNotIn("ankr", TTFont(subsetpath)) + + def test_subset_bsln_format_0(self): + _, fontpath = self.compile_font(self.getpath("TestBSLN-0.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--glyphs=one", "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("expect_bsln_0.ttx"), ["bsln"]) + + def test_subset_bsln_format_0_from_format_1(self): + # TestBSLN-1 defines the ideographic baseline to be the font's default, + # and specifies that glyphs {.notdef, zero, one, two} use the roman + # baseline instead of the default ideographic baseline. As we request + # a subsetted font with {zero, one} and the implicit .notdef, all + # glyphs in the resulting font use the Roman baseline. In this case, + # we expect a format 0 'bsln' table because it is the most compact. + _, fontpath = self.compile_font(self.getpath("TestBSLN-1.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--unicodes=U+0030-0031", + "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("expect_bsln_0.ttx"), ["bsln"]) + + def test_subset_bsln_format_1(self): + # TestBSLN-1 defines the ideographic baseline to be the font's default, + # and specifies that glyphs {.notdef, zero, one, two} use the roman + # baseline instead of the default ideographic baseline. We request + # a subset where the majority of glyphs use the roman baseline, + # but one single glyph (uni2EA2) is ideographic. In the resulting + # subsetted font, we expect a format 1 'bsln' table whose default + # is Roman, but with an override that uses the ideographic baseline + # for uni2EA2. + _, fontpath = self.compile_font(self.getpath("TestBSLN-1.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--unicodes=U+0030-0031,U+2EA2", + "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("expect_bsln_1.ttx"), ["bsln"]) + + def test_subset_bsln_format_2(self): + # The 'bsln' table in TestBSLN-2 refers to control points in glyph 'P' + # for defining its baselines. Therefore, the subsetted font should + # include this glyph even though it is not requested explicitly. + _, fontpath = self.compile_font(self.getpath("TestBSLN-2.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--glyphs=one", "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("expect_bsln_2.ttx"), ["bsln"]) + + def test_subset_bsln_format_2_from_format_3(self): + # TestBSLN-3 defines the ideographic baseline to be the font's default, + # and specifies that glyphs {.notdef, zero, one, two, P} use the roman + # baseline instead of the default ideographic baseline. As we request + # a subsetted font with zero and the implicit .notdef and P for + # baseline measurement, all glyphs in the resulting font use the Roman + # baseline. In this case, we expect a format 2 'bsln' table because it + # is the most compact encoding. + _, fontpath = self.compile_font(self.getpath("TestBSLN-3.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--unicodes=U+0030", + "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("expect_bsln_2.ttx"), ["bsln"]) + + def test_subset_bsln_format_3(self): + # TestBSLN-3 defines the ideographic baseline to be the font's default, + # and specifies that glyphs {.notdef, zero, one, two} use the roman + # baseline instead of the default ideographic baseline. We request + # a subset where the majority of glyphs use the roman baseline, + # but one single glyph (uni2EA2) is ideographic. In the resulting + # subsetted font, we expect a format 1 'bsln' table whose default + # is Roman, but with an override that uses the ideographic baseline + # for uni2EA2. + _, fontpath = self.compile_font(self.getpath("TestBSLN-3.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--unicodes=U+0030-0031,U+2EA2", + "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("expect_bsln_3.ttx"), ["bsln"]) + + def test_subset_clr(self): + _, fontpath = self.compile_font(self.getpath("TestCLR-Regular.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--glyphs=smileface", "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("expect_keep_colr.ttx"), ["GlyphOrder", "hmtx", "glyf", "COLR", "CPAL"]) + + def test_subset_gvar(self): + _, fontpath = self.compile_font(self.getpath("TestGVAR.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--unicodes=U+002B,U+2212", "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("expect_keep_gvar.ttx"), ["GlyphOrder", "avar", "fvar", "gvar", "name"]) + + def test_subset_gvar_notdef_outline(self): + _, fontpath = self.compile_font(self.getpath("TestGVAR.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--unicodes=U+0030", "--notdef_outline", "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("expect_keep_gvar_notdef_outline.ttx"), ["GlyphOrder", "avar", "fvar", "gvar", "name"]) + + def test_subset_lcar_remove(self): + _, fontpath = self.compile_font(self.getpath("TestLCAR-0.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--glyphs=one", "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.assertNotIn("lcar", subsetfont) + + def test_subset_lcar_format_0(self): + _, fontpath = self.compile_font(self.getpath("TestLCAR-0.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--unicodes=U+FB01", + "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("expect_lcar_0.ttx"), ["lcar"]) + + def test_subset_lcar_format_1(self): + _, fontpath = self.compile_font(self.getpath("TestLCAR-1.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--unicodes=U+FB01", + "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("expect_lcar_1.ttx"), ["lcar"]) + + def test_subset_math(self): + _, fontpath = self.compile_font(self.getpath("TestMATH-Regular.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--unicodes=U+0041,U+0028,U+0302,U+1D400,U+1D435", "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("expect_keep_math.ttx"), ["GlyphOrder", "CFF ", "MATH", "hmtx"]) + + def test_subset_opbd_remove(self): + # In the test font, only the glyphs 'A' and 'zero' have an entry in + # the Optical Bounds table. When subsetting, we do not request any + # of those glyphs. Therefore, the produced subsetted font should + # not contain an 'opbd' table. + _, fontpath = self.compile_font(self.getpath("TestOPBD-0.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--glyphs=one", "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.assertNotIn("opbd", subsetfont) + + def test_subset_opbd_format_0(self): + _, fontpath = self.compile_font(self.getpath("TestOPBD-0.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--glyphs=A", "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("expect_opbd_0.ttx"), ["opbd"]) + + def test_subset_opbd_format_1(self): + _, fontpath = self.compile_font(self.getpath("TestOPBD-1.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--glyphs=A", "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("expect_opbd_1.ttx"), ["opbd"]) + + def test_subset_prop_remove_default_zero(self): + # If all glyphs have an AAT glyph property with value 0, + # the "prop" table should be removed from the subsetted font. + _, fontpath = self.compile_font(self.getpath("TestPROP.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--unicodes=U+0041", + "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.assertNotIn("prop", subsetfont) + + def test_subset_prop_0(self): + # If all glyphs share the same AAT glyph properties, the "prop" table + # in the subsetted font should use format 0. + # + # Unless the shared value is zero, in which case the subsetted font + # should have no "prop" table at all. But that case has already been + # tested above in test_subset_prop_remove_default_zero(). + _, fontpath = self.compile_font(self.getpath("TestPROP.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--unicodes=U+0030-0032", "--no-notdef-glyph", + "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("expect_prop_0.ttx"), ["prop"]) + + def test_subset_prop_1(self): + # If not all glyphs share the same AAT glyph properties, the subsetted + # font should contain a "prop" table in format 1. To save space, the + # DefaultProperties should be set to the most frequent value. + _, fontpath = self.compile_font(self.getpath("TestPROP.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--unicodes=U+0030-0032", "--notdef-outline", + "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("expect_prop_1.ttx"), ["prop"]) + + def test_options(self): + # https://github.com/behdad/fonttools/issues/413 + opt1 = subset.Options() + self.assertTrue('Xyz-' not in opt1.layout_features) + opt2 = subset.Options() + opt2.layout_features.append('Xyz-') + self.assertTrue('Xyz-' in opt2.layout_features) + self.assertTrue('Xyz-' not in opt1.layout_features) + + def test_google_color(self): + _, fontpath = self.compile_font(self.getpath("google_color.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--gids=0,1", "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.assertTrue("CBDT" in subsetfont) + self.assertTrue("CBLC" in subsetfont) + self.assertTrue("x" in subsetfont['CBDT'].strikeData[0]) + self.assertFalse("y" in subsetfont['CBDT'].strikeData[0]) + + def test_google_color_all(self): + _, fontpath = self.compile_font(self.getpath("google_color.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--unicodes=*", "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.assertTrue("x" in subsetfont['CBDT'].strikeData[0]) + self.assertTrue("y" in subsetfont['CBDT'].strikeData[0]) + + def test_timing_publishes_parts(self): + _, fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf") + + options = subset.Options() + options.timing = True + subsetter = subset.Subsetter(options) + subsetter.populate(text='ABC') + font = TTFont(fontpath) + with CapturingLogHandler('fontTools.subset.timer', logging.DEBUG) as captor: + captor.logger.propagate = False + subsetter.subset(font) + logs = captor.records + captor.logger.propagate = True + + self.assertTrue(len(logs) > 5) + self.assertEqual(len(logs), len([l for l in logs if 'msg' in l.args and 'time' in l.args])) + # Look for a few things we know should happen + self.assertTrue(filter(lambda l: l.args['msg'] == "load 'cmap'", logs)) + self.assertTrue(filter(lambda l: l.args['msg'] == "subset 'cmap'", logs)) + self.assertTrue(filter(lambda l: l.args['msg'] == "subset 'glyf'", logs)) + + def test_passthrough_tables(self): + _, fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf") + font = TTFont(fontpath) + unknown_tag = 'ZZZZ' + unknown_table = newTable(unknown_tag) + unknown_table.data = b'\0'*10 + font[unknown_tag] = unknown_table + font.save(fontpath) + + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + + # tables we can't subset are dropped by default + self.assertFalse(unknown_tag in subsetfont) + + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--passthrough-tables", "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + + # unknown tables are kept if --passthrough-tables option is passed + self.assertTrue(unknown_tag in subsetfont) + + def test_non_BMP_text_arg_input(self): + _, fontpath = self.compile_font( + self.getpath("TestTTF-Regular_non_BMP_char.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + text = tostr(u"A\U0001F6D2", encoding='utf-8') + + subset.main([fontpath, "--text=%s" % text, "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + + self.assertEqual(subsetfont['maxp'].numGlyphs, 3) + self.assertEqual(subsetfont.getGlyphOrder(), ['.notdef', 'A', 'u1F6D2']) + + def test_non_BMP_text_file_input(self): + _, fontpath = self.compile_font( + self.getpath("TestTTF-Regular_non_BMP_char.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + text = tobytes(u"A\U0001F6D2", encoding='utf-8') + with tempfile.NamedTemporaryFile(delete=False) as tmp: + tmp.write(text) + + try: + subset.main([fontpath, "--text-file=%s" % tmp.name, + "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + finally: + os.remove(tmp.name) + + self.assertEqual(subsetfont['maxp'].numGlyphs, 3) + self.assertEqual(subsetfont.getGlyphOrder(), ['.notdef', 'A', 'u1F6D2']) + + def test_no_hinting_CFF(self): + ttxpath = self.getpath("Lobster.subset.ttx") + _, fontpath = self.compile_font(ttxpath, ".otf") + subsetpath = self.temp_path(".otf") + subset.main([fontpath, "--no-hinting", "--notdef-outline", + "--output-file=%s" % subsetpath, "*"]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath( + "expect_no_hinting_CFF.ttx"), ["CFF "]) + + def test_desubroutinize_CFF(self): + ttxpath = self.getpath("Lobster.subset.ttx") + _, fontpath = self.compile_font(ttxpath, ".otf") + subsetpath = self.temp_path(".otf") + subset.main([fontpath, "--desubroutinize", "--notdef-outline", + "--output-file=%s" % subsetpath, "*"]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath( + "expect_desubroutinize_CFF.ttx"), ["CFF "]) + + def test_no_hinting_desubroutinize_CFF(self): + ttxpath = self.getpath("Lobster.subset.ttx") + _, fontpath = self.compile_font(ttxpath, ".otf") + subsetpath = self.temp_path(".otf") + subset.main([fontpath, "--no-hinting", "--desubroutinize", "--notdef-outline", + "--output-file=%s" % subsetpath, "*"]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath( + "expect_no_hinting_desubroutinize_CFF.ttx"), ["CFF "]) + + def test_no_hinting_TTF(self): + _, fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf") + subsetpath = self.temp_path(".ttf") + subset.main([fontpath, "--no-hinting", "--notdef-outline", + "--output-file=%s" % subsetpath, "*"]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath( + "expect_no_hinting_TTF.ttx"), ["glyf", "maxp"]) + for tag in subset.Options().hinting_tables: + self.assertTrue(tag not in subsetfont) + + def test_notdef_width_cid(self): + # https://github.com/fonttools/fonttools/pull/845 + _, fontpath = self.compile_font(self.getpath("NotdefWidthCID-Regular.ttx"), ".otf") + subsetpath = self.temp_path(".otf") + subset.main([fontpath, "--no-notdef-outline", "--gids=0,1", "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("expect_notdef_width_cid.ttx"), ["CFF "]) + + def test_recalc_timestamp_ttf(self): + ttxpath = self.getpath("TestTTF-Regular.ttx") + font = TTFont() + font.importXML(ttxpath) + modified = font['head'].modified + _, fontpath = self.compile_font(ttxpath, ".ttf") + subsetpath = self.temp_path(".ttf") + + # by default, the subsetter does not recalculate the modified timestamp + subset.main([fontpath, "--output-file=%s" % subsetpath, "*"]) + self.assertEqual(modified, TTFont(subsetpath)['head'].modified) + + subset.main([fontpath, "--recalc-timestamp", "--output-file=%s" % subsetpath, "*"]) + self.assertLess(modified, TTFont(subsetpath)['head'].modified) + + def test_recalc_timestamp_otf(self): + ttxpath = self.getpath("TestOTF-Regular.ttx") + font = TTFont() + font.importXML(ttxpath) + modified = font['head'].modified + _, fontpath = self.compile_font(ttxpath, ".otf") + subsetpath = self.temp_path(".otf") + + # by default, the subsetter does not recalculate the modified timestamp + subset.main([fontpath, "--output-file=%s" % subsetpath, "*"]) + self.assertEqual(modified, TTFont(subsetpath)['head'].modified) + + subset.main([fontpath, "--recalc-timestamp", "--output-file=%s" % subsetpath, "*"]) + self.assertLess(modified, TTFont(subsetpath)['head'].modified) + + +if __name__ == "__main__": + sys.exit(unittest.main()) diff --git a/Tests/svgLib/path/__init__.py b/Tests/svgLib/path/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Tests/svgLib/path/parser_test.py b/Tests/svgLib/path/parser_test.py new file mode 100644 index 0000000..7865870 --- /dev/null +++ b/Tests/svgLib/path/parser_test.py @@ -0,0 +1,297 @@ +from __future__ import print_function, absolute_import, division + +from fontTools.misc.py23 import * +from fontTools.pens.recordingPen import RecordingPen +from fontTools.svgLib import parse_path + +import pytest + + +@pytest.mark.parametrize( + "pathdef, expected", + [ + + # Examples from the SVG spec + + ( + "M 100 100 L 300 100 L 200 300 z", + [ + ("moveTo", ((100.0, 100.0),)), + ("lineTo", ((300.0, 100.0),)), + ("lineTo", ((200.0, 300.0),)), + ("lineTo", ((100.0, 100.0),)), + ("closePath", ()), + ] + ), + # for Z command behavior when there is multiple subpaths + ( + "M 0 0 L 50 20 M 100 100 L 300 100 L 200 300 z", + [ + ("moveTo", ((0.0, 0.0),)), + ("lineTo", ((50.0, 20.0),)), + ("endPath", ()), + ("moveTo", ((100.0, 100.0),)), + ("lineTo", ((300.0, 100.0),)), + ("lineTo", ((200.0, 300.0),)), + ("lineTo", ((100.0, 100.0),)), + ("closePath", ()), + ] + ), + ( + "M100,200 C100,100 250,100 250,200 S400,300 400,200", + [ + ("moveTo", ((100.0, 200.0),)), + ("curveTo", ((100.0, 100.0), + (250.0, 100.0), + (250.0, 200.0))), + ("curveTo", ((250.0, 300.0), + (400.0, 300.0), + (400.0, 200.0))), + ("endPath", ()), + ] + ), + ( + "M100,200 C100,100 400,100 400,200", + [ + ("moveTo", ((100.0, 200.0),)), + ("curveTo", ((100.0, 100.0), + (400.0, 100.0), + (400.0, 200.0))), + ("endPath", ()), + ] + ), + ( + "M100,500 C25,400 475,400 400,500", + [ + ("moveTo", ((100.0, 500.0),)), + ("curveTo", ((25.0, 400.0), + (475.0, 400.0), + (400.0, 500.0))), + ("endPath", ()), + ] + ), + ( + "M100,800 C175,700 325,700 400,800", + [ + ("moveTo", ((100.0, 800.0),)), + ("curveTo", ((175.0, 700.0), + (325.0, 700.0), + (400.0, 800.0))), + ("endPath", ()), + ] + ), + ( + "M600,200 C675,100 975,100 900,200", + [ + ("moveTo", ((600.0, 200.0),)), + ("curveTo", ((675.0, 100.0), + (975.0, 100.0), + (900.0, 200.0))), + ("endPath", ()), + ] + ), + ( + "M600,500 C600,350 900,650 900,500", + [ + ("moveTo", ((600.0, 500.0),)), + ("curveTo", ((600.0, 350.0), + (900.0, 650.0), + (900.0, 500.0))), + ("endPath", ()), + ] + ), + ( + "M600,800 C625,700 725,700 750,800 S875,900 900,800", + [ + ("moveTo", ((600.0, 800.0),)), + ("curveTo", ((625.0, 700.0), + (725.0, 700.0), + (750.0, 800.0))), + ("curveTo", ((775.0, 900.0), + (875.0, 900.0), + (900.0, 800.0))), + ("endPath", ()), + ] + ), + ( + "M200,300 Q400,50 600,300 T1000,300", + [ + ("moveTo", ((200.0, 300.0),)), + ("qCurveTo", ((400.0, 50.0), + (600.0, 300.0))), + ("qCurveTo", ((800.0, 550.0), + (1000.0, 300.0))), + ("endPath", ()), + ] + ), + # End examples from SVG spec + + # Relative moveto + ( + "M 0 0 L 50 20 m 50 80 L 300 100 L 200 300 z", + [ + ("moveTo", ((0.0, 0.0),)), + ("lineTo", ((50.0, 20.0),)), + ("endPath", ()), + ("moveTo", ((100.0, 100.0),)), + ("lineTo", ((300.0, 100.0),)), + ("lineTo", ((200.0, 300.0),)), + ("lineTo", ((100.0, 100.0),)), + ("closePath", ()), + ] + ), + # Initial smooth and relative curveTo + ( + "M100,200 s 150,-100 150,0", + [ + ("moveTo", ((100.0, 200.0),)), + ("curveTo", ((100.0, 200.0), + (250.0, 100.0), + (250.0, 200.0))), + ("endPath", ()), + ] + ), + # Initial smooth and relative qCurveTo + ( + "M100,200 t 150,0", + [ + ("moveTo", ((100.0, 200.0),)), + ("qCurveTo", ((100.0, 200.0), + (250.0, 200.0))), + ("endPath", ()), + ] + ), + # relative l command + ( + "M 100 100 L 300 100 l -100 200 z", + [ + ("moveTo", ((100.0, 100.0),)), + ("lineTo", ((300.0, 100.0),)), + ("lineTo", ((200.0, 300.0),)), + ("lineTo", ((100.0, 100.0),)), + ("closePath", ()), + ] + ), + # relative q command + ( + "M200,300 q200,-250 400,0", + [ + ("moveTo", ((200.0, 300.0),)), + ("qCurveTo", ((400.0, 50.0), + (600.0, 300.0))), + ("endPath", ()), + ] + ), + # absolute H command + ( + "M 100 100 H 300 L 200 300 z", + [ + ("moveTo", ((100.0, 100.0),)), + ("lineTo", ((300.0, 100.0),)), + ("lineTo", ((200.0, 300.0),)), + ("lineTo", ((100.0, 100.0),)), + ("closePath", ()), + ] + ), + # relative h command + ( + "M 100 100 h 200 L 200 300 z", + [ + ("moveTo", ((100.0, 100.0),)), + ("lineTo", ((300.0, 100.0),)), + ("lineTo", ((200.0, 300.0),)), + ("lineTo", ((100.0, 100.0),)), + ("closePath", ()), + ] + ), + # absolute V command + ( + "M 100 100 V 300 L 200 300 z", + [ + ("moveTo", ((100.0, 100.0),)), + ("lineTo", ((100.0, 300.0),)), + ("lineTo", ((200.0, 300.0),)), + ("lineTo", ((100.0, 100.0),)), + ("closePath", ()), + ] + ), + # relative v command + ( + "M 100 100 v 200 L 200 300 z", + [ + ("moveTo", ((100.0, 100.0),)), + ("lineTo", ((100.0, 300.0),)), + ("lineTo", ((200.0, 300.0),)), + ("lineTo", ((100.0, 100.0),)), + ("closePath", ()), + ] + ), + ] +) +def test_parse_path(pathdef, expected): + pen = RecordingPen() + parse_path(pathdef, pen) + + assert pen.value == expected + + +@pytest.mark.parametrize( + "pathdef1, pathdef2", + [ + # don't need spaces between numbers and commands + ( + "M 100 100 L 200 200", + "M100 100L200 200", + ), + # repeated implicit command + ( + "M 100 200 L 200 100 L -100 -200", + "M 100 200 L 200 100 -100 -200" + ), + # don't need spaces before a minus-sign + ( + "M100,200c10-5,20-10,30-20", + "M 100 200 c 10 -5 20 -10 30 -20" + ), + # closed paths have an implicit lineTo if they don't + # end on the same point as the initial moveTo + ( + "M 100 100 L 300 100 L 200 300 z", + "M 100 100 L 300 100 L 200 300 L 100 100 z" + ) + ] +) +def test_equivalent_paths(pathdef1, pathdef2): + pen1 = RecordingPen() + parse_path(pathdef1, pen1) + + pen2 = RecordingPen() + parse_path(pathdef2, pen2) + + assert pen1.value == pen2.value + + +def test_exponents(): + # It can be e or E, the plus is optional, and a minimum of +/-3.4e38 must be supported. + pen = RecordingPen() + parse_path("M-3.4e38 3.4E+38L-3.4E-38,3.4e-38", pen) + expected = [ + ("moveTo", ((-3.4e+38, 3.4e+38),)), + ("lineTo", ((-3.4e-38, 3.4e-38),)), + ("endPath", ()), + ] + + assert pen.value == expected + + +def test_invalid_implicit_command(): + with pytest.raises(ValueError) as exc_info: + parse_path("M 100 100 L 200 200 Z 100 200", RecordingPen()) + assert exc_info.match("Unallowed implicit command") + + +def test_arc_not_implemented(): + pathdef = "M300,200 h-150 a150,150 0 1,0 150,-150 z" + with pytest.raises(NotImplementedError) as exc_info: + parse_path(pathdef, RecordingPen()) + assert exc_info.match("arcs are not supported") diff --git a/Tests/svgLib/path/path_test.py b/Tests/svgLib/path/path_test.py new file mode 100644 index 0000000..09b9447 --- /dev/null +++ b/Tests/svgLib/path/path_test.py @@ -0,0 +1,80 @@ +from __future__ import print_function, absolute_import, division + +from fontTools.misc.py23 import * +from fontTools.pens.recordingPen import RecordingPen +from fontTools.svgLib import SVGPath + +import os +from tempfile import NamedTemporaryFile + + +SVG_DATA = """\ + + + + + + +""" + +EXPECTED_PEN_COMMANDS = [ + ("moveTo", ((100.0, 100.0),)), + ("lineTo", ((300.0, 100.0),)), + ("lineTo", ((200.0, 300.0),)), + ("lineTo", ((100.0, 100.0),)), + ("closePath", ()), + ("moveTo", ((100.0, 200.0),)), + ("curveTo", ((100.0, 100.0), + (250.0, 100.0), + (250.0, 200.0))), + ("curveTo", ((250.0, 300.0), + (400.0, 300.0), + (400.0, 200.0))), + ("endPath", ()) +] + + +class SVGPathTest(object): + + def test_from_svg_file(self): + pen = RecordingPen() + with NamedTemporaryFile(delete=False) as tmp: + tmp.write(tobytes(SVG_DATA)) + try: + svg = SVGPath(tmp.name) + svg.draw(pen) + finally: + os.remove(tmp.name) + + assert pen.value == EXPECTED_PEN_COMMANDS + + def test_fromstring(self): + pen = RecordingPen() + svg = SVGPath.fromstring(SVG_DATA) + svg.draw(pen) + + assert pen.value == EXPECTED_PEN_COMMANDS + + def test_transform(self): + pen = RecordingPen() + svg = SVGPath.fromstring(SVG_DATA, + transform=(1.0, 0, 0, -1.0, 0, 1000)) + svg.draw(pen) + + assert pen.value == [ + ("moveTo", ((100.0, 900.0),)), + ("lineTo", ((300.0, 900.0),)), + ("lineTo", ((200.0, 700.0),)), + ("lineTo", ((100.0, 900.0),)), + ("closePath", ()), + ("moveTo", ((100.0, 800.0),)), + ("curveTo", ((100.0, 900.0), + (250.0, 900.0), + (250.0, 800.0))), + ("curveTo", ((250.0, 700.0), + (400.0, 700.0), + (400.0, 800.0))), + ("endPath", ()) + ] diff --git a/Tests/t1Lib/data/TestT1-Regular.lwfn b/Tests/t1Lib/data/TestT1-Regular.lwfn new file mode 100644 index 0000000000000000000000000000000000000000..c3c09dd59b7fad6821e8b0d99e4c68830376f77f GIT binary patch literal 2641 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 W>Yz4>YhDVhMFQsfyYX^0FarQ6P6=}W literal 0 HcmV?d00001 diff --git a/Tests/t1Lib/t1Lib_test.py b/Tests/t1Lib/t1Lib_test.py new file mode 100644 index 0000000..f5e934d --- /dev/null +++ b/Tests/t1Lib/t1Lib_test.py @@ -0,0 +1,100 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +import unittest +import os +from fontTools import t1Lib +from fontTools.pens.basePen import NullPen +import random + + +CWD = os.path.abspath(os.path.dirname(__file__)) +DATADIR = os.path.join(CWD, 'data') +# I used `tx` to convert PFA to LWFN (stored in the data fork) +LWFN = os.path.join(DATADIR, 'TestT1-Regular.lwfn') +PFA = os.path.join(DATADIR, 'TestT1-Regular.pfa') +PFB = os.path.join(DATADIR, 'TestT1-Regular.pfb') + + +class FindEncryptedChunksTest(unittest.TestCase): + + def test_findEncryptedChunks(self): + with open(PFA, "rb") as f: + data = f.read() + chunks = t1Lib.findEncryptedChunks(data) + self.assertEqual(len(chunks), 3) + self.assertFalse(chunks[0][0]) + # the second chunk is encrypted + self.assertTrue(chunks[1][0]) + self.assertFalse(chunks[2][0]) + + +class DecryptType1Test(unittest.TestCase): + + def test_decryptType1(self): + with open(PFA, "rb") as f: + data = f.read() + decrypted = t1Lib.decryptType1(data) + self.assertNotEqual(decrypted, data) + + +class ReadWriteTest(unittest.TestCase): + + def test_read_pfa_write_pfb(self): + font = t1Lib.T1Font(PFA) + data = self.write(font, 'PFB') + self.assertEqual(font.getData(), data) + + def test_read_pfb_write_pfa(self): + font = t1Lib.T1Font(PFB) + # 'OTHER' == 'PFA' + data = self.write(font, 'OTHER', dohex=True) + self.assertEqual(font.getData(), data) + + @staticmethod + def write(font, outtype, dohex=False): + temp = os.path.join(DATADIR, 'temp.' + outtype.lower()) + try: + font.saveAs(temp, outtype, dohex=dohex) + newfont = t1Lib.T1Font(temp) + data = newfont.getData() + finally: + if os.path.exists(temp): + os.remove(temp) + return data + + +class T1FontTest(unittest.TestCase): + + def test_parse_lwfn(self): + # the extended attrs are lost on git so we can't auto-detect 'LWFN' + font = t1Lib.T1Font(LWFN, kind="LWFN") + font.parse() + self.assertEqual(font['FontName'], 'TestT1-Regular') + self.assertTrue('Subrs' in font['Private']) + + def test_parse_pfa(self): + font = t1Lib.T1Font(PFA) + font.parse() + self.assertEqual(font['FontName'], 'TestT1-Regular') + self.assertTrue('Subrs' in font['Private']) + + def test_parse_pfb(self): + font = t1Lib.T1Font(PFB) + font.parse() + self.assertEqual(font['FontName'], 'TestT1-Regular') + self.assertTrue('Subrs' in font['Private']) + + def test_getGlyphSet(self): + font = t1Lib.T1Font(PFA) + glyphs = font.getGlyphSet() + i = random.randrange(len(glyphs)) + aglyph = list(glyphs.values())[i] + self.assertTrue(hasattr(aglyph, 'draw')) + self.assertFalse(hasattr(aglyph, 'width')) + aglyph.draw(NullPen()) + self.assertTrue(hasattr(aglyph, 'width')) + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/data/TestOTF-Regular.otx b/Tests/ttLib/data/TestOTF-Regular.otx new file mode 100644 index 0000000..573fe24 --- /dev/null +++ b/Tests/ttLib/data/TestOTF-Regular.otx @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright (c) 2015 by FontTools. No rights reserved. + + + Test OTF + + + Regular + + + FontTools: Test OTF: 2015 + + + Test OTF + + + Version 1.000 + + + TestOTF-Regular + + + Test OTF is not a trademark of FontTools. + + + FontTools + + + FontTools + + + https://github.com/behdad/fonttools + + + https://github.com/behdad/fonttools + + + https://github.com/behdad/fonttools/blob/master/LICENSE.txt + + + Test TTF + + + Copyright (c) 2015 by FontTools. No rights reserved. + + + Test OTF + + + Regular + + + FontTools: Test OTF: 2015 + + + Test OTF + + + Version 1.000 + + + TestOTF-Regular + + + Test OTF is not a trademark of FontTools. + + + FontTools + + + FontTools + + + https://github.com/behdad/fonttools + + + https://github.com/behdad/fonttools + + + https://github.com/behdad/fonttools/blob/master/LICENSE.txt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 131 122 -131 hlineto + return + + + + + + 500 450 hmoveto + 750 -400 -750 vlineto + 50 50 rmoveto + 650 300 -650 vlineto + endchar + + + 0 endchar + + + 250 endchar + + + 723 55 hmoveto + -107 callsubr + 241 -122 rmoveto + -107 callsubr + 241 -122 rmoveto + -107 callsubr + endchar + + + 241 55 hmoveto + -107 callsubr + endchar + + + 250 endchar + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/data/TestTTF-Regular.ttx b/Tests/ttLib/data/TestTTF-Regular.ttx new file mode 100644 index 0000000..8ffcefe --- /dev/null +++ b/Tests/ttLib/data/TestTTF-Regular.ttx @@ -0,0 +1,553 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SVTCA[0] /* SetFPVectorToAxis */ + + + + + + SVTCA[0] /* SetFPVectorToAxis */ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SVTCA[0] /* SetFPVectorToAxis */ + SVTCA[1] /* SetFPVectorToAxis */ + + + + + + + + + + + + + SVTCA[0] /* SetFPVectorToAxis */ + SVTCA[1] /* SetFPVectorToAxis */ + + + + + + + + + + + + SVTCA[0] /* SetFPVectorToAxis */ + SVTCA[1] /* SetFPVectorToAxis */ + + + + + + + + + + Copyright (c) 2015 by FontTools. No rights reserved. + + + Test TTF + + + Regular + + + FontTools: Test TTF: 2015 + + + Test TTF + + + Version 1.000 + + + TestTTF-Regular + + + Test TTF is not a trademark of FontTools. + + + FontTools + + + FontTools + + + https://github.com/behdad/fonttools + + + https://github.com/behdad/fonttools + + + https://github.com/behdad/fonttools/blob/master/LICENSE.txt + + + Test TTF + + + Copyright (c) 2015 by FontTools. No rights reserved. + + + Test TTF + + + Regular + + + FontTools: Test TTF: 2015 + + + Test TTF + + + Version 1.000 + + + TestTTF-Regular + + + Test TTF is not a trademark of FontTools. + + + FontTools + + + FontTools + + + https://github.com/behdad/fonttools + + + https://github.com/behdad/fonttools + + + https://github.com/behdad/fonttools/blob/master/LICENSE.txt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/data/TestTTFComplex-Regular.ttx b/Tests/ttLib/data/TestTTFComplex-Regular.ttx new file mode 100644 index 0000000..00e89cd --- /dev/null +++ b/Tests/ttLib/data/TestTTFComplex-Regular.ttx @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/data/test_woff2_metadata.xml b/Tests/ttLib/data/test_woff2_metadata.xml new file mode 100644 index 0000000..9ab26a4 --- /dev/null +++ b/Tests/ttLib/data/test_woff2_metadata.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + Description without language. + + + Description with "en" language. + + + Description with "fr" language. + + + + + License without language. + + + License with "en" language. + + + License with "fr" language. + + + + + Copyright without language. + + + Copyright with "en" language. + + + Copyright with "fr" language. + + + + + Trademark without language. + + + Trademark with "en" language. + + + Trademark with "fr" language. + + + + + Extension 1 - Name Without Language + Extension 1 - Name With "en" Language + Extension 1 - Name With "fr" Language + + Extension 1 - Item 1 - Name Without Language + Extension 1 - Item 1 - Name With "en" Language + Extension 1 - Item 1 - Name With "fr" Language + Extension 1 - Item 1 - Value Without Language + Extension 1 - Item 1 - Value With "en" Language + Extension 1 - Item 1 - Value With "fr" Language + + + Extension 1 - Item 2 - Name Without Language + Extension 1 - Item 2 - Name With "en" Language + Extension 1 - Item 2 - Name With "fr" Language + Extension 1 - Item 2 - Value Without Language + Extension 1 - Item 2 - Value With "en" Language + Extension 1 - Item 2 - Value With "fr" Language + + + + Extension 2 - Name Without Language + Extension 2 - Name With "en" Language + Extension 2 - Name With "fr" Language + + Extension 2 - Item 1 - Name Without Language + Extension 2 - Item 1 - Name With "en" Language + Extension 2 - Item 1 - Name With "fr" Language + Extension 2 - Item 1 - Value Without Language + Extension 2 - Item 1 - Value With "en" Language + Extension 2 - Item 1 - Value With "fr" Language + + + Extension 2 - Item 2 - Name Without Language + Extension 2 - Item 2 - Name With "en" Language + Extension 2 - Item 2 - Name With "fr" Language + Extension 2 - Item 2 - Value Without Language + Extension 2 - Item 2 - Value With "en" Language + Extension 2 - Item 2 - Value With "fr" Language + + + Extension 2 - Item 3 - Name Without Language + Extension 2 - Item 3 - Name With "en" Language + Extension 2 - Item 3 - Name With "fr" Language + Extension 2 - Item 3 - Value Without Language + Extension 2 - Item 3 - Value With "en" Language + + + diff --git a/Tests/ttLib/sfnt_test.py b/Tests/ttLib/sfnt_test.py new file mode 100644 index 0000000..2119351 --- /dev/null +++ b/Tests/ttLib/sfnt_test.py @@ -0,0 +1,8 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.ttLib.sfnt import calcChecksum + + +def test_calcChecksum(): + assert calcChecksum(b"abcd") == 1633837924 + assert calcChecksum(b"abcdxyz") == 3655064932 diff --git a/Tests/ttLib/tables/C_F_F__2_test.py b/Tests/ttLib/tables/C_F_F__2_test.py new file mode 100644 index 0000000..191f60b --- /dev/null +++ b/Tests/ttLib/tables/C_F_F__2_test.py @@ -0,0 +1,60 @@ +"""cff2Lib_test.py -- unit test for Adobe CFF fonts.""" + +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.misc.py23 import * +from fontTools.ttLib import TTFont, newTable +import re +import os +import unittest + + +CURR_DIR = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) +DATA_DIR = os.path.join(CURR_DIR, 'data') + +CFF_TTX = os.path.join(DATA_DIR, "C_F_F__2.ttx") +CFF_BIN = os.path.join(DATA_DIR, "C_F_F__2.bin") + + +def strip_VariableItems(string): + # ttlib changes with the fontTools version + string = re.sub(' ttLibVersion=".*"', '', string) + # head table checksum and mod date changes with each save. + string = re.sub('', '', string) + string = re.sub('', '', string) + return string + +class CFFTableTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + with open(CFF_BIN, 'rb') as f: + font = TTFont(file=CFF_BIN) + cffTable = font['CFF2'] + cls.cff2Data = cffTable.compile(font) + with open(CFF_TTX, 'r') as f: + cff2XML = f.read() + cff2XML = strip_VariableItems(cff2XML) + cls.cff2XML = cff2XML.splitlines() + + def test_toXML(self): + font = TTFont(file=CFF_BIN) + cffTable = font['CFF2'] + cffData = cffTable.compile(font) + out = UnicodeIO() + font.saveXML(out) + cff2XML = out.getvalue() + cff2XML = strip_VariableItems(cff2XML) + cff2XML = cff2XML.splitlines() + self.assertEqual(cff2XML, self.cff2XML) + + def test_fromXML(self): + font = TTFont(sfntVersion='OTTO') + font.importXML(CFF_TTX) + cffTable = font['CFF2'] + cff2Data = cffTable.compile(font) + self.assertEqual(cff2Data, self.cff2Data) + + +if __name__ == "__main__": + unittest.main() diff --git a/Tests/ttLib/tables/C_F_F_test.py b/Tests/ttLib/tables/C_F_F_test.py new file mode 100644 index 0000000..63f767c --- /dev/null +++ b/Tests/ttLib/tables/C_F_F_test.py @@ -0,0 +1,51 @@ +"""cffLib_test.py -- unit test for Adobe CFF fonts.""" + +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.misc.py23 import * +from fontTools.ttLib import TTFont, newTable +import re +import os +import unittest + + +CURR_DIR = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) +DATA_DIR = os.path.join(CURR_DIR, 'data') + +CFF_TTX = os.path.join(DATA_DIR, "C_F_F_.ttx") +CFF_BIN = os.path.join(DATA_DIR, "C_F_F_.bin") + + +def strip_ttLibVersion(string): + return re.sub(' ttLibVersion=".*"', '', string) + + +class CFFTableTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + with open(CFF_BIN, 'rb') as f: + cls.cffData = f.read() + with open(CFF_TTX, 'r') as f: + cls.cffXML = strip_ttLibVersion(f.read()).splitlines() + + def test_toXML(self): + font = TTFont(sfntVersion='OTTO') + cffTable = font['CFF '] = newTable('CFF ') + cffTable.decompile(self.cffData, font) + out = UnicodeIO() + font.saveXML(out) + cffXML = strip_ttLibVersion(out.getvalue()).splitlines() + self.assertEqual(cffXML, self.cffXML) + + def test_fromXML(self): + font = TTFont(sfntVersion='OTTO') + font.importXML(CFF_TTX) + cffTable = font['CFF '] + cffData = cffTable.compile(font) + self.assertEqual(cffData, self.cffData) + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/C_P_A_L_test.py b/Tests/ttLib/tables/C_P_A_L_test.py new file mode 100644 index 0000000..bacd4a8 --- /dev/null +++ b/Tests/ttLib/tables/C_P_A_L_test.py @@ -0,0 +1,227 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.testTools import getXML, parseXML +from fontTools.misc.textTools import deHexStr +from fontTools.ttLib import getTableModule, newTable +import unittest + + +CPAL_DATA_V0 = deHexStr( + '0000 0002 ' # version=0, numPaletteEntries=2 + '0002 0004 ' # numPalettes=2, numColorRecords=4 + '00000010 ' # offsetToFirstColorRecord=16 + '0000 0002 ' # colorRecordIndex=[0, 2] + '000000FF FFCC66FF ' # colorRecord #0, #1 (blue/green/red/alpha) + '000000FF 000080FF') # colorRecord #2, #3 + + +CPAL_DATA_V0_SHARING_COLORS = deHexStr( + '0000 0003 ' # version=0, numPaletteEntries=3 + '0004 0006 ' # numPalettes=4, numColorRecords=6 + '00000014 ' # offsetToFirstColorRecord=20 + '0000 0000 0003 0000 ' # colorRecordIndex=[0, 0, 3, 0] + '443322FF 77889911 55555555 ' # colorRecord #0, #1, #2 (BGRA) + '443322FF 77889911 FFFFFFFF') # colorRecord #3, #4, #5 + + +CPAL_DATA_V1_NOLABELS_NOTYPES = deHexStr( + '0001 0003 ' # version=1, numPaletteEntries=3 + '0002 0006 ' # numPalettes=2, numColorRecords=6 + '0000001C ' # offsetToFirstColorRecord=28 + '0000 0003 ' # colorRecordIndex=[0, 3] + '00000000 ' # offsetToPaletteTypeArray=0 + '00000000 ' # offsetToPaletteLabelArray=0 + '00000000 ' # offsetToPaletteEntryLabelArray=0 + 'CAFECAFE 00112233 44556677 ' # colorRecord #0, #1, #2 (BGRA) + '31415927 42424242 00331337') # colorRecord #3, #4, #5 + + +CPAL_DATA_V1 = deHexStr( + '0001 0003 ' # version=1, numPaletteEntries=3 + '0002 0006 ' # numPalettes=2, numColorRecords=6 + '0000001C ' # offsetToFirstColorRecord=28 + '0000 0003 ' # colorRecordIndex=[0, 3] + '00000034 ' # offsetToPaletteTypeArray=52 + '0000003C ' # offsetToPaletteLabelArray=60 + '00000040 ' # offsetToPaletteEntryLabelArray=64 + 'CAFECAFE 00112233 44556677 ' # colorRecord #0, #1, #2 (BGRA) + '31415927 42424242 00331337 ' # colorRecord #3, #4, #5 + '00000001 00000002 ' # paletteType=[1, 2] + '0102 0103 ' # paletteLabel=[258, 259] + '0201 0202 0203') # paletteEntryLabel=[513, 514, 515] + + +class FakeNameTable(object): + def __init__(self, names): + self.names = names + + def getDebugName(self, nameID): + return self.names.get(nameID) + + +class CPALTest(unittest.TestCase): + def test_decompile_v0(self): + cpal = newTable('CPAL') + cpal.decompile(CPAL_DATA_V0, ttFont=None) + self.assertEqual(cpal.version, 0) + self.assertEqual(cpal.numPaletteEntries, 2) + self.assertEqual(repr(cpal.palettes), + '[[#000000FF, #66CCFFFF], [#000000FF, #800000FF]]') + self.assertEqual(cpal.paletteLabels, [0, 0]) + self.assertEqual(cpal.paletteTypes, [0, 0]) + self.assertEqual(cpal.paletteEntryLabels, [0, 0]) + + def test_decompile_v0_sharingColors(self): + cpal = newTable('CPAL') + cpal.decompile(CPAL_DATA_V0_SHARING_COLORS, ttFont=None) + self.assertEqual(cpal.version, 0) + self.assertEqual(cpal.numPaletteEntries, 3) + self.assertEqual([repr(p) for p in cpal.palettes], [ + '[#223344FF, #99887711, #55555555]', + '[#223344FF, #99887711, #55555555]', + '[#223344FF, #99887711, #FFFFFFFF]', + '[#223344FF, #99887711, #55555555]']) + self.assertEqual(cpal.paletteLabels, [0, 0, 0, 0]) + self.assertEqual(cpal.paletteTypes, [0, 0, 0, 0]) + self.assertEqual(cpal.paletteEntryLabels, [0, 0, 0]) + + def test_decompile_v1_noLabelsNoTypes(self): + cpal = newTable('CPAL') + cpal.decompile(CPAL_DATA_V1_NOLABELS_NOTYPES, ttFont=None) + self.assertEqual(cpal.version, 1) + self.assertEqual(cpal.numPaletteEntries, 3) + self.assertEqual([repr(p) for p in cpal.palettes], [ + '[#CAFECAFE, #22110033, #66554477]', # RGBA + '[#59413127, #42424242, #13330037]']) + self.assertEqual(cpal.paletteLabels, [0, 0]) + self.assertEqual(cpal.paletteTypes, [0, 0]) + self.assertEqual(cpal.paletteEntryLabels, [0, 0, 0]) + + def test_decompile_v1(self): + cpal = newTable('CPAL') + cpal.decompile(CPAL_DATA_V1, ttFont=None) + self.assertEqual(cpal.version, 1) + self.assertEqual(cpal.numPaletteEntries, 3) + self.assertEqual([repr(p) for p in cpal.palettes], [ + '[#CAFECAFE, #22110033, #66554477]', # RGBA + '[#59413127, #42424242, #13330037]']) + self.assertEqual(cpal.paletteTypes, [1, 2]) + self.assertEqual(cpal.paletteLabels, [258, 259]) + self.assertEqual(cpal.paletteEntryLabels, [513, 514, 515]) + + def test_compile_v0(self): + cpal = newTable('CPAL') + cpal.decompile(CPAL_DATA_V0, ttFont=None) + self.assertEqual(cpal.compile(ttFont=None), CPAL_DATA_V0) + + def test_compile_v0_sharingColors(self): + cpal = newTable('CPAL') + cpal.version = 0 + Color = getTableModule('CPAL').Color + palette1 = [Color(red=0x22, green=0x33, blue=0x44, alpha=0xff), + Color(red=0x99, green=0x88, blue=0x77, alpha=0x11), + Color(red=0x55, green=0x55, blue=0x55, alpha=0x55)] + palette2 = [Color(red=0x22, green=0x33, blue=0x44, alpha=0xff), + Color(red=0x99, green=0x88, blue=0x77, alpha=0x11), + Color(red=0xFF, green=0xFF, blue=0xFF, alpha=0xFF)] + cpal.numPaletteEntries = len(palette1) + cpal.palettes = [palette1, palette1, palette2, palette1] + self.assertEqual(cpal.compile(ttFont=None), + CPAL_DATA_V0_SHARING_COLORS) + + def test_compile_v1(self): + cpal = newTable('CPAL') + cpal.decompile(CPAL_DATA_V1, ttFont=None) + self.assertEqual(cpal.compile(ttFont=None), CPAL_DATA_V1) + + def test_compile_v1_noLabelsNoTypes(self): + cpal = newTable('CPAL') + cpal.decompile(CPAL_DATA_V1_NOLABELS_NOTYPES, ttFont=None) + self.assertEqual(cpal.compile(ttFont=None), + CPAL_DATA_V1_NOLABELS_NOTYPES) + + def test_toXML_v0(self): + cpal = newTable('CPAL') + cpal.decompile(CPAL_DATA_V0, ttFont=None) + self.assertEqual(getXML(cpal.toXML), + ['', + '', + '', + ' ', + ' ', + '', + '', + ' ', + ' ', + '']) + + def test_toXML_v1(self): + name = FakeNameTable({258: "Spring theme", 259: "Winter theme", + 513: "darks", 515: "lights"}) + cpal = newTable('CPAL') + ttFont = {"name": name, "CPAL": cpal} + cpal.decompile(CPAL_DATA_V1, ttFont) + self.assertEqual(getXML(cpal.toXML, ttFont), + ['', + '', + '', + ' ', + ' ', + ' ', + ' ', + '', + '', + ' ', + ' ', + ' ', + ' ', + '', + '', + ' ']) + + def test_fromXML_v0(self): + cpal = newTable('CPAL') + for name, attrs, content in parseXML( + '' + '' + '' + ' ' + ' ' + ''): + cpal.fromXML(name, attrs, content, ttFont=None) + self.assertEqual(cpal.version, 0) + self.assertEqual(cpal.numPaletteEntries, 2) + self.assertEqual(repr(cpal.palettes), '[[#12345678, #FEDCBA98]]') + self.assertEqual(cpal.paletteLabels, [0]) + self.assertEqual(cpal.paletteTypes, [0]) + self.assertEqual(cpal.paletteEntryLabels, [0, 0]) + + def test_fromXML_v1(self): + cpal = newTable('CPAL') + for name, attrs, content in parseXML( + '' + '' + '' + ' ' + ' ' + ' ' + '' + '' + ' '): + cpal.fromXML(name, attrs, content, ttFont=None) + self.assertEqual(cpal.version, 1) + self.assertEqual(cpal.numPaletteEntries, 3) + self.assertEqual(repr(cpal.palettes), + '[[#12345678, #FEDCBA98, #CAFECAFE]]') + self.assertEqual(cpal.paletteLabels, [259]) + self.assertEqual(cpal.paletteTypes, [2]) + self.assertEqual(cpal.paletteEntryLabels, [0, 262, 0]) + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/M_V_A_R_test.py b/Tests/ttLib/tables/M_V_A_R_test.py new file mode 100644 index 0000000..05a92e8 --- /dev/null +++ b/Tests/ttLib/tables/M_V_A_R_test.py @@ -0,0 +1,139 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.testTools import FakeFont, getXML, parseXML +from fontTools.misc.textTools import deHexStr, hexStr +from fontTools.ttLib.tables._f_v_a_r import Axis +from fontTools.ttLib import newTable, TTFont +import unittest + + +MVAR_DATA = deHexStr( + '0001 0000 ' # 0: version=1.0 + '0000 0008 ' # 4: reserved=0, valueRecordSize=8 + '0007 ' # 8: valueRecordCount=7 + '0044 ' # 10: offsetToItemVariationStore=68 + '6861 7363 ' # 12: ValueRecord.valueTag="hasc" + '0000 ' # 16: ValueRecord.deltaSetOuterIndex + '0003 ' # 18: ValueRecord.deltaSetInnerIndex + '6863 6C61 ' # 20: ValueRecord.valueTag="hcla" + '0000 ' # 24: ValueRecord.deltaSetOuterIndex + '0003 ' # 26: ValueRecord.deltaSetInnerIndex + '6863 6C64 ' # 28: ValueRecord.valueTag="hcld" + '0000 ' # 32: ValueRecord.deltaSetOuterIndex + '0003 ' # 34: ValueRecord.deltaSetInnerIndex + '6864 7363 ' # 36: ValueRecord.valueTag="hdsc" + '0000 ' # 40: ValueRecord.deltaSetOuterIndex + '0000 ' # 42: ValueRecord.deltaSetInnerIndex + '686C 6770 ' # 44: ValueRecord.valueTag="hlgp" + '0000 ' # 48: ValueRecord.deltaSetOuterIndex + '0002 ' # 50: ValueRecord.deltaSetInnerIndex + '7362 796F ' # 52: ValueRecord.valueTag="sbyo" + '0000 ' # 56: ValueRecord.deltaSetOuterIndex + '0001 ' # 58: ValueRecord.deltaSetInnerIndex + '7370 796F ' # 60: ValueRecord.valueTag="spyo" + '0000 ' # 64: ValueRecord.deltaSetOuterIndex + '0002 ' # 66: ValueRecord.deltaSetInnerIndex + '0001 ' # 68: VarStore.format=1 + '0000 000C ' # 70: VarStore.offsetToVariationRegionList=12 + '0001 ' # 74: VarStore.itemVariationDataCount=1 + '0000 0016 ' # 76: VarStore.itemVariationDataOffsets[0]=22 + '0001 ' # 80: VarRegionList.axisCount=1 + '0001 ' # 82: VarRegionList.regionCount=1 + '0000 ' # 84: variationRegions[0].regionAxes[0].startCoord=0.0 + '4000 ' # 86: variationRegions[0].regionAxes[0].peakCoord=1.0 + '4000 ' # 88: variationRegions[0].regionAxes[0].endCoord=1.0 + '0004 ' # 90: VarData.ItemCount=4 + '0001 ' # 92: VarData.NumShorts=1 + '0001 ' # 94: VarData.VarRegionCount=1 + '0000 ' # 96: VarData.VarRegionIndex[0]=0 + 'FF38 ' # 98: VarData.deltaSets[0]=-200 + 'FFCE ' # 100: VarData.deltaSets[0]=-50 + '0064 ' # 102: VarData.deltaSets[0]=100 + '00C8 ' # 104: VarData.deltaSets[0]=200 +) + +MVAR_XML = [ + '', + '', + '', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', + '', + ' ', + ' ', + '', + '', + ' ', + ' ', + '', + '', + ' ', + ' ', + '', + '', + ' ', + ' ', + '', + '', + ' ', + ' ', + '', + '', + ' ', + ' ', + '', + '', + ' ', + ' ', + '', +] + + +class MVARTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.maxDiff = None + + def test_decompile_toXML(self): + mvar = newTable('MVAR') + font = TTFont() + mvar.decompile(MVAR_DATA, font) + self.assertEqual(getXML(mvar.toXML), MVAR_XML) + + def test_compile_fromXML(self): + mvar = newTable('MVAR') + font = TTFont() + for name, attrs, content in parseXML(MVAR_XML): + mvar.fromXML(name, attrs, content, font=font) + data = MVAR_DATA + self.assertEqual(hexStr(mvar.compile(font)), hexStr(data)) + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/O_S_2f_2_test.py b/Tests/ttLib/tables/O_S_2f_2_test.py new file mode 100644 index 0000000..116e82e --- /dev/null +++ b/Tests/ttLib/tables/O_S_2f_2_test.py @@ -0,0 +1,62 @@ +from __future__ import print_function, division, absolute_import +from fontTools.ttLib import TTFont, newTable, getTableModule +from fontTools.ttLib.tables.O_S_2f_2 import * +import unittest + + +class OS2TableTest(unittest.TestCase): + + def test_getUnicodeRanges(self): + table = table_O_S_2f_2() + table.ulUnicodeRange1 = 0xFFFFFFFF + table.ulUnicodeRange2 = 0xFFFFFFFF + table.ulUnicodeRange3 = 0xFFFFFFFF + table.ulUnicodeRange4 = 0xFFFFFFFF + bits = table.getUnicodeRanges() + for i in range(127): + self.assertIn(i, bits) + + def test_setUnicodeRanges(self): + table = table_O_S_2f_2() + table.ulUnicodeRange1 = 0 + table.ulUnicodeRange2 = 0 + table.ulUnicodeRange3 = 0 + table.ulUnicodeRange4 = 0 + bits = set(range(123)) + table.setUnicodeRanges(bits) + self.assertEqual(table.getUnicodeRanges(), bits) + with self.assertRaises(ValueError): + table.setUnicodeRanges([-1, 127, 255]) + + def test_recalcUnicodeRanges(self): + font = TTFont() + font['OS/2'] = os2 = newTable('OS/2') + font['cmap'] = cmap = newTable('cmap') + st = getTableModule('cmap').CmapSubtable.newSubtable(4) + st.platformID, st.platEncID, st.language = 3, 1, 0 + st.cmap = {0x0041:'A', 0x03B1: 'alpha', 0x0410: 'Acyr'} + cmap.tables = [] + cmap.tables.append(st) + os2.setUnicodeRanges({0, 1, 9}) + # 'pruneOnly' will clear any bits for which there's no intersection: + # bit 1 ('Latin 1 Supplement'), in this case. However, it won't set + # bit 7 ('Greek and Coptic') despite the "alpha" character is present. + self.assertEqual(os2.recalcUnicodeRanges(font, pruneOnly=True), {0, 9}) + # try again with pruneOnly=False: bit 7 is now set. + self.assertEqual(os2.recalcUnicodeRanges(font), {0, 7, 9}) + # add a non-BMP char from 'Mahjong Tiles' block (bit 122) + st.cmap[0x1F000] = 'eastwindtile' + # the bit 122 and the special bit 57 ('Non Plane 0') are also enabled + self.assertEqual(os2.recalcUnicodeRanges(font), {0, 7, 9, 57, 122}) + + def test_intersectUnicodeRanges(self): + self.assertEqual(intersectUnicodeRanges([0x0410]), {9}) + self.assertEqual(intersectUnicodeRanges([0x0410, 0x1F000]), {9, 57, 122}) + self.assertEqual( + intersectUnicodeRanges([0x0410, 0x1F000], inverse=True), + (set(range(123)) - {9, 57, 122})) + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/S_T_A_T_test.py b/Tests/ttLib/tables/S_T_A_T_test.py new file mode 100644 index 0000000..e851f98 --- /dev/null +++ b/Tests/ttLib/tables/S_T_A_T_test.py @@ -0,0 +1,264 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.testTools import FakeFont, getXML, parseXML +from fontTools.misc.textTools import deHexStr +from fontTools.ttLib import newTable +import unittest + + +STAT_DATA = deHexStr( + '0001 0000 ' # 0: Version=1.0 + '0008 0002 ' # 4: DesignAxisSize=8, DesignAxisCount=2 + '0000 0012 ' # 8: OffsetToDesignAxes=18 + '0003 0000 0022 ' # 12: AxisValueCount=3, OffsetToAxisValueOffsets=34 + '7767 6874 ' # 18: DesignAxis[0].AxisTag='wght' + '012D 0002 ' # 22: DesignAxis[0].NameID=301, .AxisOrdering=2 + '5445 5354 ' # 26: DesignAxis[1].AxisTag='TEST' + '012E 0001 ' # 30: DesignAxis[1].NameID=302, .AxisOrdering=1 + '0006 0012 0026 ' # 34: AxisValueOffsets = [6, 18, 38] (+34) + '0001 0000 0000 ' # 40: AxisValue[0].Format=1, .AxisIndex=0, .Flags=0 + '0191 0190 0000 ' # 46: AxisValue[0].ValueNameID=401, .Value=400.0 + '0002 0001 0000 ' # 52: AxisValue[1].Format=2, .AxisIndex=1, .Flags=0 + '0192 ' # 58: AxisValue[1].ValueNameID=402 + '0002 0000 ' # 60: AxisValue[1].NominalValue=2.0 + '0001 0000 ' # 64: AxisValue[1].RangeMinValue=1.0 + '0003 0000 ' # 68: AxisValue[1].RangeMaxValue=3.0 + '0003 0000 0000 ' # 72: AxisValue[2].Format=3, .AxisIndex=0, .Flags=0 + '0002 ' # 78: AxisValue[2].ValueNameID=2 'Regular' + '0190 0000 02BC 0000 ' # 80: AxisValue[2].Value=400.0, .LinkedValue=700.0 +) # 88: +assert(len(STAT_DATA) == 88) + + +STAT_XML = [ + '', + '', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +# Contains junk data for making sure we get our offset decoding right. +STAT_DATA_WITH_AXIS_JUNK = deHexStr( + '0001 0000 ' # 0: Version=1.0 + '000A 0002 ' # 4: DesignAxisSize=10, DesignAxisCount=2 + '0000 0012 ' # 8: OffsetToDesignAxes=18 + '0000 0000 0000 ' # 12: AxisValueCount=3, OffsetToAxisValueOffsets=34 + '7767 6874 ' # 18: DesignAxis[0].AxisTag='wght' + '012D 0002 ' # 22: DesignAxis[0].NameID=301, .AxisOrdering=2 + 'DEAD ' # 26: + '5445 5354 ' # 28: DesignAxis[1].AxisTag='TEST' + '012E 0001 ' # 32: DesignAxis[1].NameID=302, .AxisOrdering=1 + 'BEEF ' # 36: +) # 38: + +assert(len(STAT_DATA_WITH_AXIS_JUNK) == 38) + + +STAT_XML_WITH_AXIS_JUNK = [ + '', + '', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', # 0xDE + ' ', # 0xAD + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', # 0xBE + ' ', # 0xEF + ' ', + '', + '', +] + + +STAT_DATA_AXIS_VALUE_FORMAT3 = deHexStr( + '0001 0000 ' # 0: Version=1.0 + '0008 0001 ' # 4: DesignAxisSize=8, DesignAxisCount=1 + '0000 0012 ' # 8: OffsetToDesignAxes=18 + '0001 ' # 12: AxisValueCount=1 + '0000 001A ' # 14: OffsetToAxisValueOffsets=26 + '7767 6874 ' # 18: DesignAxis[0].AxisTag='wght' + '0102 ' # 22: DesignAxis[0].AxisNameID=258 'Weight' + '0000 ' # 24: DesignAxis[0].AxisOrdering=0 + '0002 ' # 26: AxisValueOffsets=[2] (+26) + '0003 ' # 28: AxisValue[0].Format=3 + '0000 0002 ' # 30: AxisValue[0].AxisIndex=0, .Flags=0x2 + '0002 ' # 34: AxisValue[0].ValueNameID=2 'Regular' + '0190 0000 ' # 36: AxisValue[0].Value=400.0 + '02BC 0000 ' # 40: AxisValue[0].LinkedValue=700.0 +) # 44: +assert(len(STAT_DATA_AXIS_VALUE_FORMAT3) == 44) + + +STAT_XML_AXIS_VALUE_FORMAT3 = [ + '', + '', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + '', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +STAT_DATA_VERSION_1_1 = deHexStr( + '0001 0001 ' # 0: Version=1.1 + '0008 0001 ' # 4: DesignAxisSize=8, DesignAxisCount=1 + '0000 0014 ' # 8: OffsetToDesignAxes=20 + '0001 ' # 12: AxisValueCount=1 + '0000 001C ' # 14: OffsetToAxisValueOffsets=28 + '0101 ' # 18: ElidedFallbackNameID: 257 + '7767 6874 ' # 20: DesignAxis[0].AxisTag='wght' + '0102 ' # 24: DesignAxis[0].AxisNameID=258 'Weight' + '0000 ' # 26: DesignAxis[0].AxisOrdering=0 + '0002 ' # 28: AxisValueOffsets=[2] (+28) + '0003 ' # 30: AxisValue[0].Format=3 + '0000 0002 ' # 32: AxisValue[0].AxisIndex=0, .Flags=0x2 + '0002 ' # 36: AxisValue[0].ValueNameID=2 'Regular' + '0190 0000 ' # 38: AxisValue[0].Value=400.0 + '02BC 0000 ' # 42: AxisValue[0].LinkedValue=700.0 +) # 46: +assert(len(STAT_DATA_VERSION_1_1) == 46) + + +STAT_XML_VERSION_1_1 = [ + '', + '', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + '', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', + '', +] + + +class STATTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.maxDiff = None + + def test_decompile_toXML(self): + table = newTable('STAT') + table.decompile(STAT_DATA, font=FakeFont(['.notdef'])) + self.assertEqual(getXML(table.toXML), STAT_XML) + + def test_decompile_toXML_withAxisJunk(self): + table = newTable('STAT') + table.decompile(STAT_DATA_WITH_AXIS_JUNK, font=FakeFont(['.notdef'])) + self.assertEqual(getXML(table.toXML), STAT_XML_WITH_AXIS_JUNK) + + def test_decompile_toXML_format3(self): + table = newTable('STAT') + table.decompile(STAT_DATA_AXIS_VALUE_FORMAT3, + font=FakeFont(['.notdef'])) + self.assertEqual(getXML(table.toXML), STAT_XML_AXIS_VALUE_FORMAT3) + + def test_decompile_toXML_version_1_1(self): + table = newTable('STAT') + table.decompile(STAT_DATA_VERSION_1_1, + font=FakeFont(['.notdef'])) + self.assertEqual(getXML(table.toXML), STAT_XML_VERSION_1_1) + + def test_compile_fromXML(self): + table = newTable('STAT') + font = FakeFont(['.notdef']) + for name, attrs, content in parseXML(STAT_XML): + table.fromXML(name, attrs, content, font=font) + self.assertEqual(table.compile(font), STAT_DATA) + + def test_compile_fromXML_withAxisJunk(self): + table = newTable('STAT') + font = FakeFont(['.notdef']) + for name, attrs, content in parseXML(STAT_XML_WITH_AXIS_JUNK): + table.fromXML(name, attrs, content, font=font) + self.assertEqual(table.compile(font), STAT_DATA_WITH_AXIS_JUNK) + + def test_compile_fromXML_format3(self): + table = newTable('STAT') + font = FakeFont(['.notdef']) + for name, attrs, content in parseXML(STAT_XML_AXIS_VALUE_FORMAT3): + table.fromXML(name, attrs, content, font=font) + self.assertEqual(table.compile(font), STAT_DATA_AXIS_VALUE_FORMAT3) + + def test_compile_fromXML_version_1_1(self): + table = newTable('STAT') + font = FakeFont(['.notdef']) + for name, attrs, content in parseXML(STAT_XML_VERSION_1_1): + table.fromXML(name, attrs, content, font=font) + self.assertEqual(table.compile(font), STAT_DATA_VERSION_1_1) + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/T_S_I__0_test.py b/Tests/ttLib/tables/T_S_I__0_test.py new file mode 100644 index 0000000..1de6b34 --- /dev/null +++ b/Tests/ttLib/tables/T_S_I__0_test.py @@ -0,0 +1,106 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import SimpleNamespace +from fontTools.misc.textTools import deHexStr +from fontTools.misc.testTools import getXML +from fontTools.ttLib.tables.T_S_I__0 import table_T_S_I__0 +import pytest + + +# (gid, length, offset) for glyph programs +TSI0_INDICES = [ + (0, 1, 0), + (1, 5, 1), + (2, 0, 1), + (3, 0, 1), + (4, 8, 6)] + +# (type, length, offset) for 'extra' programs +TSI0_EXTRA_INDICES = [ + (0xFFFA, 2, 14), # ppgm + (0xFFFB, 4, 16), # cvt + (0xFFFC, 6, 20), # reserved + (0xFFFD, 10, 26)] # fpgm + +# compiled TSI0 table from data above +TSI0_DATA = deHexStr( + "0000 0001 00000000" + "0001 0005 00000001" + "0002 0000 00000001" + "0003 0000 00000001" + "0004 0008 00000006" + "FFFE 0000 ABFC1F34" # 'magic' separates glyph from extra programs + "FFFA 0002 0000000E" + "FFFB 0004 00000010" + "FFFC 0006 00000014" + "FFFD 000A 0000001A") + +# empty font has no glyph programs but 4 extra programs are always present +EMPTY_TSI0_EXTRA_INDICES = [ + (0xFFFA, 0, 0), + (0xFFFB, 0, 0), + (0xFFFC, 0, 0), + (0xFFFD, 0, 0)] + +EMPTY_TSI0_DATA = deHexStr( + "FFFE 0000 ABFC1F34" + "FFFA 0000 00000000" + "FFFB 0000 00000000" + "FFFC 0000 00000000" + "FFFD 0000 00000000") + + +@pytest.fixture +def table(): + return table_T_S_I__0() + + +@pytest.mark.parametrize( + "numGlyphs, data, expected_indices, expected_extra_indices", + [ + (5, TSI0_DATA, TSI0_INDICES, TSI0_EXTRA_INDICES), + (0, EMPTY_TSI0_DATA, [], EMPTY_TSI0_EXTRA_INDICES) + ], + ids=["simple", "empty"] +) +def test_decompile(table, numGlyphs, data, expected_indices, + expected_extra_indices): + font = {'maxp': SimpleNamespace(numGlyphs=numGlyphs)} + + table.decompile(data, font) + + assert len(table.indices) == numGlyphs + assert table.indices == expected_indices + assert len(table.extra_indices) == 4 + assert table.extra_indices == expected_extra_indices + + +@pytest.mark.parametrize( + "numGlyphs, indices, extra_indices, expected_data", + [ + (5, TSI0_INDICES, TSI0_EXTRA_INDICES, TSI0_DATA), + (0, [], EMPTY_TSI0_EXTRA_INDICES, EMPTY_TSI0_DATA) + ], + ids=["simple", "empty"] +) +def test_compile(table, numGlyphs, indices, extra_indices, expected_data): + assert table.compile(ttFont=None) == b"" + + table.set(indices, extra_indices) + data = table.compile(ttFont=None) + assert data == expected_data + + +def test_set(table): + table.set(TSI0_INDICES, TSI0_EXTRA_INDICES) + assert table.indices == TSI0_INDICES + assert table.extra_indices == TSI0_EXTRA_INDICES + + +def test_toXML(table): + assert getXML(table.toXML, ttFont=None) == [ + ''] + + +if __name__ == "__main__": + import sys + sys.exit(pytest.main(sys.argv)) diff --git a/Tests/ttLib/tables/T_S_I__1_test.py b/Tests/ttLib/tables/T_S_I__1_test.py new file mode 100644 index 0000000..e529425 --- /dev/null +++ b/Tests/ttLib/tables/T_S_I__1_test.py @@ -0,0 +1,184 @@ +from __future__ import ( + print_function, division, absolute_import, unicode_literals +) +from fontTools.misc.py23 import unichr, tobytes +from fontTools.misc.loggingTools import CapturingLogHandler +from fontTools.ttLib import TTFont, TTLibError +from fontTools.ttLib.tables.T_S_I__0 import table_T_S_I__0 +from fontTools.ttLib.tables.T_S_I__1 import table_T_S_I__1 +import pytest + + +TSI1_DATA = b"""abcdefghijklmnopqrstuvxywz0123456789""" +TSI1_UTF8_DATA = b"""abcd\xc3\xa9ghijklmnopqrstuvxywz0123456789""" + + +@pytest.fixture +def indextable(): + table = table_T_S_I__0() + table.set( + [(0, 1, 0), # gid 0, length=1, offset=0, text='a' + (1, 5, 1), # gid 1, length=5, offset=1, text='bcdef' + (2, 0, 1), # gid 2, length=0, offset=1, text='' + (3, 0, 1), # gid 3, length=0, offset=1, text='' + (4, 8, 6)], # gid 4, length=8, offset=6, text='ghijklmn' + [(0xFFFA, 2, 14), # 'ppgm', length=2, offset=14, text='op' + (0xFFFB, 4, 16), # 'cvt', length=4, offset=16, text='qrst' + (0xFFFC, 6, 20), # 'reserved', length=6, offset=20, text='uvxywz' + (0xFFFD, 10, 26)] # 'fpgm', length=10, offset=26, text='0123456789' + ) + return table + + +@pytest.fixture +def font(indextable): + font = TTFont() + # ['a', 'b', 'c', ...] + ch = 0x61 + n = len(indextable.indices) + font.glyphOrder = [unichr(i) for i in range(ch, ch+n)] + font['TSI0'] = indextable + return font + + +@pytest.fixture +def empty_font(): + font = TTFont() + font.glyphOrder = [] + indextable = table_T_S_I__0() + indextable.set([], [(0xFFFA, 0, 0), + (0xFFFB, 0, 0), + (0xFFFC, 0, 0), + (0xFFFD, 0, 0)]) + font['TSI0'] = indextable + return font + + +def test_decompile(font): + table = table_T_S_I__1() + table.decompile(TSI1_DATA, font) + + assert table.glyphPrograms == { + 'a': 'a', + 'b': 'bcdef', + # 'c': '', # zero-length entries are skipped + # 'd': '', + 'e': 'ghijklmn'} + assert table.extraPrograms == { + 'ppgm': 'op', + 'cvt': 'qrst', + 'reserved': 'uvxywz', + 'fpgm': '0123456789'} + + +def test_decompile_utf8(font): + table = table_T_S_I__1() + table.decompile(TSI1_UTF8_DATA, font) + + assert table.glyphPrograms == { + 'a': 'a', + 'b': 'bcd\u00e9', + # 'c': '', # zero-length entries are skipped + # 'd': '', + 'e': 'ghijklmn'} + assert table.extraPrograms == { + 'ppgm': 'op', + 'cvt': 'qrst', + 'reserved': 'uvxywz', + 'fpgm': '0123456789'} + + +def test_decompile_empty(empty_font): + table = table_T_S_I__1() + table.decompile(b"", empty_font) + + assert table.glyphPrograms == {} + assert table.extraPrograms == {} + + +def test_decompile_invalid_length(empty_font): + empty_font.glyphOrder = ['a'] + empty_font['TSI0'].indices = [(0, 0x8000+1, 0)] + + table = table_T_S_I__1() + with pytest.raises(TTLibError) as excinfo: + table.decompile(b'', empty_font) + assert excinfo.match("textLength .* must not be > 32768") + + +def test_decompile_offset_past_end(empty_font): + empty_font.glyphOrder = ['foo', 'bar'] + content = 'baz' + data = tobytes(content) + empty_font['TSI0'].indices = [(0, len(data), 0), (1, 1, len(data)+1)] + + table = table_T_S_I__1() + with CapturingLogHandler(table.log, "WARNING") as captor: + table.decompile(data, empty_font) + + # the 'bar' program is skipped because its offset > len(data) + assert table.glyphPrograms == {'foo': 'baz'} + assert any("textOffset > totalLength" in r.msg for r in captor.records) + + +def test_decompile_magic_length_last_extra(empty_font): + indextable = empty_font['TSI0'] + indextable.extra_indices[-1] = (0xFFFD, 0x8000, 0) + content = "0" * (0x8000 + 1) + data = tobytes(content) + + table = table_T_S_I__1() + table.decompile(data, empty_font) + + assert table.extraPrograms['fpgm'] == content + + +def test_decompile_magic_length_last_glyph(empty_font): + empty_font.glyphOrder = ['foo', 'bar'] + indextable = empty_font['TSI0'] + indextable.indices = [ + (0, 3, 0), + (1, 0x8000, 3)] # the actual length of 'bar' program is + indextable.extra_indices = [ # the difference between the first extra's + (0xFFFA, 0, 0x8004), # offset and 'bar' offset: 0x8004 - 3 + (0xFFFB, 0, 0x8004), + (0xFFFC, 0, 0x8004), + (0xFFFD, 0, 0x8004)] + foo_content = "0" * 3 + bar_content = "1" * (0x8000 + 1) + data = tobytes(foo_content + bar_content) + + table = table_T_S_I__1() + table.decompile(data, empty_font) + + assert table.glyphPrograms['foo'] == foo_content + assert table.glyphPrograms['bar'] == bar_content + + +def test_decompile_magic_length_non_last(empty_font): + indextable = empty_font['TSI0'] + indextable.extra_indices = [ + (0xFFFA, 3, 0), + (0xFFFB, 0x8000, 3), # the actual length of 'cvt' program is: + (0xFFFC, 0, 0x8004), # nextTextOffset - textOffset: 0x8004 - 3 + (0xFFFD, 0, 0x8004)] + ppgm_content = "0" * 3 + cvt_content = "1" * (0x8000 + 1) + data = tobytes(ppgm_content + cvt_content) + + table = table_T_S_I__1() + table.decompile(data, empty_font) + + assert table.extraPrograms['ppgm'] == ppgm_content + assert table.extraPrograms['cvt'] == cvt_content + + table = table_T_S_I__1() + with CapturingLogHandler(table.log, "WARNING") as captor: + table.decompile(data[:-1], empty_font) # last entry is truncated + captor.assertRegex("nextTextOffset > totalLength") + assert table.extraPrograms['cvt'] == cvt_content[:-1] + + +if __name__ == "__main__": + import sys + sys.exit(pytest.main(sys.argv)) diff --git a/Tests/ttLib/tables/TupleVariation_test.py b/Tests/ttLib/tables/TupleVariation_test.py new file mode 100644 index 0000000..6986d5b --- /dev/null +++ b/Tests/ttLib/tables/TupleVariation_test.py @@ -0,0 +1,687 @@ +from __future__ import \ + print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc.loggingTools import CapturingLogHandler +from fontTools.misc.testTools import parseXML +from fontTools.misc.textTools import deHexStr, hexStr +from fontTools.misc.xmlWriter import XMLWriter +from fontTools.ttLib.tables.TupleVariation import \ + log, TupleVariation, compileSharedTuples, decompileSharedTuples, \ + compileTupleVariationStore, decompileTupleVariationStore, inferRegion_ +import random +import unittest + + +def hexencode(s): + h = hexStr(s).upper() + return ' '.join([h[i:i+2] for i in range(0, len(h), 2)]) + + +AXES = { + "wdth": (0.3, 0.4, 0.5), + "wght": (0.0, 1.0, 1.0), + "opsz": (-0.7, -0.7, 0.0) +} + + +# Shared tuples in the 'gvar' table of the Skia font, as printed +# in Apple's TrueType specification. +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gvar.html +SKIA_GVAR_SHARED_TUPLES_DATA = deHexStr( + "40 00 00 00 C0 00 00 00 00 00 40 00 00 00 C0 00 " + "C0 00 C0 00 40 00 C0 00 40 00 40 00 C0 00 40 00") + +SKIA_GVAR_SHARED_TUPLES = [ + {"wght": 1.0, "wdth": 0.0}, + {"wght": -1.0, "wdth": 0.0}, + {"wght": 0.0, "wdth": 1.0}, + {"wght": 0.0, "wdth": -1.0}, + {"wght": -1.0, "wdth": -1.0}, + {"wght": 1.0, "wdth": -1.0}, + {"wght": 1.0, "wdth": 1.0}, + {"wght": -1.0, "wdth": 1.0} +] + + +# Tuple Variation Store of uppercase I in the Skia font, as printed in Apple's +# TrueType spec. The actual Skia font uses a different table for uppercase I +# than what is printed in Apple's spec, but we still want to make sure that +# we can parse the data as it appears in the specification. +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gvar.html +SKIA_GVAR_I_DATA = deHexStr( + "00 08 00 24 00 33 20 00 00 15 20 01 00 1B 20 02 " + "00 24 20 03 00 15 20 04 00 26 20 07 00 0D 20 06 " + "00 1A 20 05 00 40 01 01 01 81 80 43 FF 7E FF 7E " + "FF 7E FF 7E 00 81 45 01 01 01 03 01 04 01 04 01 " + "04 01 02 80 40 00 82 81 81 04 3A 5A 3E 43 20 81 " + "04 0E 40 15 45 7C 83 00 0D 9E F3 F2 F0 F0 F0 F0 " + "F3 9E A0 A1 A1 A1 9F 80 00 91 81 91 00 0D 0A 0A " + "09 0A 0A 0A 0A 0A 0A 0A 0A 0A 0A 0B 80 00 15 81 " + "81 00 C4 89 00 C4 83 00 0D 80 99 98 96 96 96 96 " + "99 80 82 83 83 83 81 80 40 FF 18 81 81 04 E6 F9 " + "10 21 02 81 04 E8 E5 EB 4D DA 83 00 0D CE D3 D4 " + "D3 D3 D3 D5 D2 CE CC CD CD CD CD 80 00 A1 81 91 " + "00 0D 07 03 04 02 02 02 03 03 07 07 08 08 08 07 " + "80 00 09 81 81 00 28 40 00 A4 02 24 24 66 81 04 " + "08 FA FA FA 28 83 00 82 02 FF FF FF 83 02 01 01 " + "01 84 91 00 80 06 07 08 08 08 08 0A 07 80 03 FE " + "FF FF FF 81 00 08 81 82 02 EE EE EE 8B 6D 00") + + +class TupleVariationTest(unittest.TestCase): + def test_equal(self): + var1 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)]) + var2 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)]) + self.assertEqual(var1, var2) + + def test_equal_differentAxes(self): + var1 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)]) + var2 = TupleVariation({"wght":(0.7, 0.8, 0.9)}, [(0,0), (9,8), (7,6)]) + self.assertNotEqual(var1, var2) + + def test_equal_differentCoordinates(self): + var1 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8), (7,6)]) + var2 = TupleVariation({"wght":(0.0, 1.0, 1.0)}, [(0,0), (9,8)]) + self.assertNotEqual(var1, var2) + + def test_hasImpact_someDeltasNotZero(self): + axes = {"wght":(0.0, 1.0, 1.0)} + var = TupleVariation(axes, [(0,0), (9,8), (7,6)]) + self.assertTrue(var.hasImpact()) + + def test_hasImpact_allDeltasZero(self): + axes = {"wght":(0.0, 1.0, 1.0)} + var = TupleVariation(axes, [(0,0), (0,0), (0,0)]) + self.assertTrue(var.hasImpact()) + + def test_hasImpact_allDeltasNone(self): + axes = {"wght":(0.0, 1.0, 1.0)} + var = TupleVariation(axes, [None, None, None]) + self.assertFalse(var.hasImpact()) + + def test_toXML_badDeltaFormat(self): + writer = XMLWriter(BytesIO()) + g = TupleVariation(AXES, ["String"]) + with CapturingLogHandler(log, "ERROR") as captor: + g.toXML(writer, ["wdth"]) + self.assertIn("bad delta format", [r.msg for r in captor.records]) + self.assertEqual([ + '', + '', + '', + '', + ], TupleVariationTest.xml_lines(writer)) + + def test_toXML_constants(self): + writer = XMLWriter(BytesIO()) + g = TupleVariation(AXES, [42, None, 23, 0, -17, None]) + g.toXML(writer, ["wdth", "wght", "opsz"]) + self.assertEqual([ + '', + '', + '', + '', + '', + '', + '', + '', + '' + ], TupleVariationTest.xml_lines(writer)) + + def test_toXML_points(self): + writer = XMLWriter(BytesIO()) + g = TupleVariation(AXES, [(9,8), None, (7,6), (0,0), (-1,-2), None]) + g.toXML(writer, ["wdth", "wght", "opsz"]) + self.assertEqual([ + '', + '', + '', + '', + '', + '', + '', + '', + '' + ], TupleVariationTest.xml_lines(writer)) + + def test_toXML_allDeltasNone(self): + writer = XMLWriter(BytesIO()) + axes = {"wght":(0.0, 1.0, 1.0)} + g = TupleVariation(axes, [None] * 5) + g.toXML(writer, ["wght", "wdth"]) + self.assertEqual([ + '', + '', + '', + '' + ], TupleVariationTest.xml_lines(writer)) + + def test_fromXML_badDeltaFormat(self): + g = TupleVariation({}, []) + with CapturingLogHandler(log, "WARNING") as captor: + for name, attrs, content in parseXML(''): + g.fromXML(name, attrs, content) + self.assertIn("bad delta format: a, b", + [r.msg for r in captor.records]) + + def test_fromXML_constants(self): + g = TupleVariation({}, [None] * 4) + for name, attrs, content in parseXML( + '' + '' + '' + '' + ''): + g.fromXML(name, attrs, content) + self.assertEqual(AXES, g.axes) + self.assertEqual([None, 42, -23, None], g.coordinates) + + def test_fromXML_points(self): + g = TupleVariation({}, [None] * 4) + for name, attrs, content in parseXML( + '' + '' + '' + '' + ''): + g.fromXML(name, attrs, content) + self.assertEqual(AXES, g.axes) + self.assertEqual([None, (33, 44), (-2, 170), None], g.coordinates) + + def test_compile_sharedPeaks_nonIntermediate_sharedPoints(self): + var = TupleVariation( + {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, + [(7,4), (8,5), (9,6)]) + axisTags = ["wght", "wdth"] + sharedPeakIndices = { var.compileCoord(axisTags): 0x77 } + tup, deltas, _ = var.compile(axisTags, sharedPeakIndices, + sharedPoints={0,1,2}) + # len(deltas)=8; flags=None; tupleIndex=0x77 + # embeddedPeaks=[]; intermediateCoord=[] + self.assertEqual("00 08 00 77", hexencode(tup)) + self.assertEqual("02 07 08 09 " # deltaX: [7, 8, 9] + "02 04 05 06", # deltaY: [4, 5, 6] + hexencode(deltas)) + + def test_compile_sharedPeaks_intermediate_sharedPoints(self): + var = TupleVariation( + {"wght": (0.3, 0.5, 0.7), "wdth": (0.1, 0.8, 0.9)}, + [(7,4), (8,5), (9,6)]) + axisTags = ["wght", "wdth"] + sharedPeakIndices = { var.compileCoord(axisTags): 0x77 } + tup, deltas, _ = var.compile(axisTags, sharedPeakIndices, + sharedPoints={0,1,2}) + # len(deltas)=8; flags=INTERMEDIATE_REGION; tupleIndex=0x77 + # embeddedPeak=[]; intermediateCoord=[(0.3, 0.1), (0.7, 0.9)] + self.assertEqual("00 08 40 77 13 33 06 66 2C CD 39 9A", hexencode(tup)) + self.assertEqual("02 07 08 09 " # deltaX: [7, 8, 9] + "02 04 05 06", # deltaY: [4, 5, 6] + hexencode(deltas)) + + def test_compile_sharedPeaks_nonIntermediate_privatePoints(self): + var = TupleVariation( + {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, + [(7,4), (8,5), (9,6)]) + axisTags = ["wght", "wdth"] + sharedPeakIndices = { var.compileCoord(axisTags): 0x77 } + tup, deltas, _ = var.compile(axisTags, sharedPeakIndices, + sharedPoints=None) + # len(deltas)=9; flags=PRIVATE_POINT_NUMBERS; tupleIndex=0x77 + # embeddedPeak=[]; intermediateCoord=[] + self.assertEqual("00 09 20 77", hexencode(tup)) + self.assertEqual("00 " # all points in glyph + "02 07 08 09 " # deltaX: [7, 8, 9] + "02 04 05 06", # deltaY: [4, 5, 6] + hexencode(deltas)) + + def test_compile_sharedPeaks_intermediate_privatePoints(self): + var = TupleVariation( + {"wght": (0.0, 0.5, 1.0), "wdth": (0.0, 0.8, 1.0)}, + [(7,4), (8,5), (9,6)]) + axisTags = ["wght", "wdth"] + sharedPeakIndices = { var.compileCoord(axisTags): 0x77 } + tuple, deltas, _ = var.compile(axisTags, + sharedPeakIndices, sharedPoints=None) + # len(deltas)=9; flags=PRIVATE_POINT_NUMBERS; tupleIndex=0x77 + # embeddedPeak=[]; intermediateCoord=[(0.0, 0.0), (1.0, 1.0)] + self.assertEqual("00 09 60 77 00 00 00 00 40 00 40 00", + hexencode(tuple)) + self.assertEqual("00 " # all points in glyph + "02 07 08 09 " # deltaX: [7, 8, 9] + "02 04 05 06", # deltaY: [4, 5, 6] + hexencode(deltas)) + + def test_compile_embeddedPeak_nonIntermediate_sharedPoints(self): + var = TupleVariation( + {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, + [(7,4), (8,5), (9,6)]) + tup, deltas, _ = var.compile(axisTags=["wght", "wdth"], + sharedCoordIndices={}, sharedPoints={0, 1, 2}) + # len(deltas)=8; flags=EMBEDDED_PEAK_TUPLE + # embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[] + self.assertEqual("00 08 80 00 20 00 33 33", hexencode(tup)) + self.assertEqual("02 07 08 09 " # deltaX: [7, 8, 9] + "02 04 05 06", # deltaY: [4, 5, 6] + hexencode(deltas)) + + def test_compile_embeddedPeak_nonIntermediate_sharedConstants(self): + var = TupleVariation( + {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, + [3, 1, 4]) + tup, deltas, _ = var.compile(axisTags=["wght", "wdth"], + sharedCoordIndices={}, sharedPoints={0, 1, 2}) + # len(deltas)=4; flags=EMBEDDED_PEAK_TUPLE + # embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[] + self.assertEqual("00 04 80 00 20 00 33 33", hexencode(tup)) + self.assertEqual("02 03 01 04", # delta: [3, 1, 4] + hexencode(deltas)) + + def test_compile_embeddedPeak_intermediate_sharedPoints(self): + var = TupleVariation( + {"wght": (0.0, 0.5, 1.0), "wdth": (0.0, 0.8, 0.8)}, + [(7,4), (8,5), (9,6)]) + tup, deltas, _ = var.compile(axisTags=["wght", "wdth"], + sharedCoordIndices={}, + sharedPoints={0, 1, 2}) + # len(deltas)=8; flags=EMBEDDED_PEAK_TUPLE + # embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[(0.0, 0.0), (1.0, 0.8)] + self.assertEqual("00 08 C0 00 20 00 33 33 00 00 00 00 40 00 33 33", + hexencode(tup)) + self.assertEqual("02 07 08 09 " # deltaX: [7, 8, 9] + "02 04 05 06", # deltaY: [4, 5, 6] + hexencode(deltas)) + + def test_compile_embeddedPeak_nonIntermediate_privatePoints(self): + var = TupleVariation( + {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, + [(7,4), (8,5), (9,6)]) + tup, deltas, _ = var.compile( + axisTags=["wght", "wdth"], sharedCoordIndices={}, sharedPoints=None) + # len(deltas)=9; flags=PRIVATE_POINT_NUMBERS|EMBEDDED_PEAK_TUPLE + # embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[] + self.assertEqual("00 09 A0 00 20 00 33 33", hexencode(tup)) + self.assertEqual("00 " # all points in glyph + "02 07 08 09 " # deltaX: [7, 8, 9] + "02 04 05 06", # deltaY: [4, 5, 6] + hexencode(deltas)) + + def test_compile_embeddedPeak_nonIntermediate_privateConstants(self): + var = TupleVariation( + {"wght": (0.0, 0.5, 0.5), "wdth": (0.0, 0.8, 0.8)}, + [7, 8, 9]) + tup, deltas, _ = var.compile( + axisTags=["wght", "wdth"], sharedCoordIndices={}, sharedPoints=None) + # len(deltas)=5; flags=PRIVATE_POINT_NUMBERS|EMBEDDED_PEAK_TUPLE + # embeddedPeak=[(0.5, 0.8)]; intermediateCoord=[] + self.assertEqual("00 05 A0 00 20 00 33 33", hexencode(tup)) + self.assertEqual("00 " # all points in glyph + "02 07 08 09", # delta: [7, 8, 9] + hexencode(deltas)) + + def test_compile_embeddedPeak_intermediate_privatePoints(self): + var = TupleVariation( + {"wght": (0.4, 0.5, 0.6), "wdth": (0.7, 0.8, 0.9)}, + [(7,4), (8,5), (9,6)]) + tup, deltas, _ = var.compile( + axisTags = ["wght", "wdth"], + sharedCoordIndices={}, sharedPoints=None) + # len(deltas)=9; + # flags=PRIVATE_POINT_NUMBERS|INTERMEDIATE_REGION|EMBEDDED_PEAK_TUPLE + # embeddedPeak=(0.5, 0.8); intermediateCoord=[(0.4, 0.7), (0.6, 0.9)] + self.assertEqual("00 09 E0 00 20 00 33 33 19 9A 2C CD 26 66 39 9A", + hexencode(tup)) + self.assertEqual("00 " # all points in glyph + "02 07 08 09 " # deltaX: [7, 8, 9] + "02 04 05 06", # deltaY: [4, 5, 6] + hexencode(deltas)) + + def test_compile_embeddedPeak_intermediate_privateConstants(self): + var = TupleVariation( + {"wght": (0.4, 0.5, 0.6), "wdth": (0.7, 0.8, 0.9)}, + [7, 8, 9]) + tup, deltas, _ = var.compile( + axisTags = ["wght", "wdth"], + sharedCoordIndices={}, sharedPoints=None) + # len(deltas)=5; + # flags=PRIVATE_POINT_NUMBERS|INTERMEDIATE_REGION|EMBEDDED_PEAK_TUPLE + # embeddedPeak=(0.5, 0.8); intermediateCoord=[(0.4, 0.7), (0.6, 0.9)] + self.assertEqual("00 05 E0 00 20 00 33 33 19 9A 2C CD 26 66 39 9A", + hexencode(tup)) + self.assertEqual("00 " # all points in glyph + "02 07 08 09", # delta: [7, 8, 9] + hexencode(deltas)) + + def test_compileCoord(self): + var = TupleVariation({"wght": (-1.0, -1.0, -1.0), "wdth": (0.4, 0.5, 0.6)}, [None] * 4) + self.assertEqual("C0 00 20 00", hexencode(var.compileCoord(["wght", "wdth"]))) + self.assertEqual("20 00 C0 00", hexencode(var.compileCoord(["wdth", "wght"]))) + self.assertEqual("C0 00", hexencode(var.compileCoord(["wght"]))) + + def test_compileIntermediateCoord(self): + var = TupleVariation({"wght": (-1.0, -1.0, 0.0), "wdth": (0.4, 0.5, 0.6)}, [None] * 4) + self.assertEqual("C0 00 19 9A 00 00 26 66", hexencode(var.compileIntermediateCoord(["wght", "wdth"]))) + self.assertEqual("19 9A C0 00 26 66 00 00", hexencode(var.compileIntermediateCoord(["wdth", "wght"]))) + self.assertEqual(None, var.compileIntermediateCoord(["wght"])) + self.assertEqual("19 9A 26 66", hexencode(var.compileIntermediateCoord(["wdth"]))) + + def test_decompileCoord(self): + decompileCoord = TupleVariation.decompileCoord_ + data = deHexStr("DE AD C0 00 20 00 DE AD") + self.assertEqual(({"wght": -1.0, "wdth": 0.5}, 6), decompileCoord(["wght", "wdth"], data, 2)) + + def test_decompileCoord_roundTrip(self): + # Make sure we are not affected by https://github.com/behdad/fonttools/issues/286 + data = deHexStr("7F B9 80 35") + values, _ = TupleVariation.decompileCoord_(["wght", "wdth"], data, 0) + axisValues = {axis:(val, val, val) for axis, val in values.items()} + var = TupleVariation(axisValues, [None] * 4) + self.assertEqual("7F B9 80 35", hexencode(var.compileCoord(["wght", "wdth"]))) + + def test_compilePoints(self): + compilePoints = lambda p: TupleVariation.compilePoints(set(p), numPointsInGlyph=999) + self.assertEqual("00", hexencode(compilePoints(range(999)))) # all points in glyph + self.assertEqual("01 00 07", hexencode(compilePoints([7]))) + self.assertEqual("01 80 FF FF", hexencode(compilePoints([65535]))) + self.assertEqual("02 01 09 06", hexencode(compilePoints([9, 15]))) + self.assertEqual("06 05 07 01 F7 02 01 F2", hexencode(compilePoints([7, 8, 255, 257, 258, 500]))) + self.assertEqual("03 01 07 01 80 01 EC", hexencode(compilePoints([7, 8, 500]))) + self.assertEqual("04 01 07 01 81 BE E7 0C 0F", hexencode(compilePoints([7, 8, 0xBEEF, 0xCAFE]))) + self.maxDiff = None + self.assertEqual("81 2C" + # 300 points (0x12c) in total + " 7F 00" + (127 * " 01") + # first run, contains 128 points: [0 .. 127] + " 7F" + (128 * " 01") + # second run, contains 128 points: [128 .. 255] + " 2B" + (44 * " 01"), # third run, contains 44 points: [256 .. 299] + hexencode(compilePoints(range(300)))) + self.assertEqual("81 8F" + # 399 points (0x18f) in total + " 7F 00" + (127 * " 01") + # first run, contains 128 points: [0 .. 127] + " 7F" + (128 * " 01") + # second run, contains 128 points: [128 .. 255] + " 7F" + (128 * " 01") + # third run, contains 128 points: [256 .. 383] + " 0E" + (15 * " 01"), # fourth run, contains 15 points: [384 .. 398] + hexencode(compilePoints(range(399)))) + + def test_decompilePoints(self): + numPointsInGlyph = 65536 + allPoints = list(range(numPointsInGlyph)) + def decompilePoints(data, offset): + points, offset = TupleVariation.decompilePoints_(numPointsInGlyph, deHexStr(data), offset, "gvar") + # Conversion to list needed for Python 3. + return (list(points), offset) + # all points in glyph + self.assertEqual((allPoints, 1), decompilePoints("00", 0)) + # all points in glyph (in overly verbose encoding, not explicitly prohibited by spec) + self.assertEqual((allPoints, 2), decompilePoints("80 00", 0)) + # 2 points; first run: [9, 9+6] + self.assertEqual(([9, 15], 4), decompilePoints("02 01 09 06", 0)) + # 2 points; first run: [0xBEEF, 0xCAFE]. (0x0C0F = 0xCAFE - 0xBEEF) + self.assertEqual(([0xBEEF, 0xCAFE], 6), decompilePoints("02 81 BE EF 0C 0F", 0)) + # 1 point; first run: [7] + self.assertEqual(([7], 3), decompilePoints("01 00 07", 0)) + # 1 point; first run: [7] in overly verbose encoding + self.assertEqual(([7], 4), decompilePoints("01 80 00 07", 0)) + # 1 point; first run: [65535]; requires words to be treated as unsigned numbers + self.assertEqual(([65535], 4), decompilePoints("01 80 FF FF", 0)) + # 4 points; first run: [7, 8]; second run: [255, 257]. 257 is stored in delta-encoded bytes (0xFF + 2). + self.assertEqual(([7, 8, 263, 265], 7), decompilePoints("04 01 07 01 01 FF 02", 0)) + # combination of all encodings, preceded and followed by 4 bytes of unused data + data = "DE AD DE AD 04 01 07 01 81 BE E7 0C 0F DE AD DE AD" + self.assertEqual(([7, 8, 0xBEEF, 0xCAFE], 13), decompilePoints(data, 4)) + self.assertSetEqual(set(range(300)), set(decompilePoints( + "81 2C" + # 300 points (0x12c) in total + " 7F 00" + (127 * " 01") + # first run, contains 128 points: [0 .. 127] + " 7F" + (128 * " 01") + # second run, contains 128 points: [128 .. 255] + " AB" + (44 * " 00 01"), # third run, contains 44 points: [256 .. 299] + 0)[0])) + self.assertSetEqual(set(range(399)), set(decompilePoints( + "81 8F" + # 399 points (0x18f) in total + " 7F 00" + (127 * " 01") + # first run, contains 128 points: [0 .. 127] + " 7F" + (128 * " 01") + # second run, contains 128 points: [128 .. 255] + " FF" + (128 * " 00 01") + # third run, contains 128 points: [256 .. 383] + " 8E" + (15 * " 00 01"), # fourth run, contains 15 points: [384 .. 398] + 0)[0])) + + def test_decompilePoints_shouldAcceptBadPointNumbers(self): + decompilePoints = TupleVariation.decompilePoints_ + # 2 points; first run: [3, 9]. + numPointsInGlyph = 8 + with CapturingLogHandler(log, "WARNING") as captor: + decompilePoints(numPointsInGlyph, + deHexStr("02 01 03 06"), 0, "cvar") + self.assertIn("point 9 out of range in 'cvar' table", + [r.msg for r in captor.records]) + + def test_decompilePoints_roundTrip(self): + numPointsInGlyph = 500 # greater than 255, so we also exercise code path for 16-bit encoding + compile = lambda points: TupleVariation.compilePoints(points, numPointsInGlyph) + decompile = lambda data: set(TupleVariation.decompilePoints_(numPointsInGlyph, data, 0, "gvar")[0]) + for i in range(50): + points = set(random.sample(range(numPointsInGlyph), 30)) + self.assertSetEqual(points, decompile(compile(points)), + "failed round-trip decompile/compilePoints; points=%s" % points) + allPoints = set(range(numPointsInGlyph)) + self.assertSetEqual(allPoints, decompile(compile(allPoints))) + + def test_compileDeltas_points(self): + var = TupleVariation({}, [(0,0), (1, 0), (2, 0), None, (4, 0), (5, 0)]) + points = {1, 2, 3, 4} + # deltaX for points: [1, 2, 4]; deltaY for points: [0, 0, 0] + self.assertEqual("02 01 02 04 82", hexencode(var.compileDeltas(points))) + + def test_compileDeltas_constants(self): + var = TupleVariation({}, [0, 1, 2, None, 4, 5]) + cvts = {1, 2, 3, 4} + # delta for cvts: [1, 2, 4] + self.assertEqual("02 01 02 04", hexencode(var.compileDeltas(cvts))) + + def test_compileDeltaValues(self): + compileDeltaValues = lambda values: hexencode(TupleVariation.compileDeltaValues_(values)) + # zeroes + self.assertEqual("80", compileDeltaValues([0])) + self.assertEqual("BF", compileDeltaValues([0] * 64)) + self.assertEqual("BF 80", compileDeltaValues([0] * 65)) + self.assertEqual("BF A3", compileDeltaValues([0] * 100)) + self.assertEqual("BF BF BF BF", compileDeltaValues([0] * 256)) + # bytes + self.assertEqual("00 01", compileDeltaValues([1])) + self.assertEqual("06 01 02 03 7F 80 FF FE", compileDeltaValues([1, 2, 3, 127, -128, -1, -2])) + self.assertEqual("3F" + (64 * " 7F"), compileDeltaValues([127] * 64)) + self.assertEqual("3F" + (64 * " 7F") + " 00 7F", compileDeltaValues([127] * 65)) + # words + self.assertEqual("40 66 66", compileDeltaValues([0x6666])) + self.assertEqual("43 66 66 7F FF FF FF 80 00", compileDeltaValues([0x6666, 32767, -1, -32768])) + self.assertEqual("7F" + (64 * " 11 22"), compileDeltaValues([0x1122] * 64)) + self.assertEqual("7F" + (64 * " 11 22") + " 40 11 22", compileDeltaValues([0x1122] * 65)) + # bytes, zeroes, bytes: a single zero is more compact when encoded as part of the bytes run + self.assertEqual("04 7F 7F 00 7F 7F", compileDeltaValues([127, 127, 0, 127, 127])) + self.assertEqual("01 7F 7F 81 01 7F 7F", compileDeltaValues([127, 127, 0, 0, 127, 127])) + self.assertEqual("01 7F 7F 82 01 7F 7F", compileDeltaValues([127, 127, 0, 0, 0, 127, 127])) + self.assertEqual("01 7F 7F 83 01 7F 7F", compileDeltaValues([127, 127, 0, 0, 0, 0, 127, 127])) + # bytes, zeroes + self.assertEqual("01 01 00", compileDeltaValues([1, 0])) + self.assertEqual("00 01 81", compileDeltaValues([1, 0, 0])) + # words, bytes, words: a single byte is more compact when encoded as part of the words run + self.assertEqual("42 66 66 00 02 77 77", compileDeltaValues([0x6666, 2, 0x7777])) + self.assertEqual("40 66 66 01 02 02 40 77 77", compileDeltaValues([0x6666, 2, 2, 0x7777])) + # words, zeroes, words + self.assertEqual("40 66 66 80 40 77 77", compileDeltaValues([0x6666, 0, 0x7777])) + self.assertEqual("40 66 66 81 40 77 77", compileDeltaValues([0x6666, 0, 0, 0x7777])) + self.assertEqual("40 66 66 82 40 77 77", compileDeltaValues([0x6666, 0, 0, 0, 0x7777])) + # words, zeroes, bytes + self.assertEqual("40 66 66 80 02 01 02 03", compileDeltaValues([0x6666, 0, 1, 2, 3])) + self.assertEqual("40 66 66 81 02 01 02 03", compileDeltaValues([0x6666, 0, 0, 1, 2, 3])) + self.assertEqual("40 66 66 82 02 01 02 03", compileDeltaValues([0x6666, 0, 0, 0, 1, 2, 3])) + # words, zeroes + self.assertEqual("40 66 66 80", compileDeltaValues([0x6666, 0])) + self.assertEqual("40 66 66 81", compileDeltaValues([0x6666, 0, 0])) + # bytes or words from floats + self.assertEqual("00 01", compileDeltaValues([1.1])) + self.assertEqual("00 02", compileDeltaValues([1.9])) + self.assertEqual("40 66 66", compileDeltaValues([0x6666 + 0.1])) + self.assertEqual("40 66 66", compileDeltaValues([0x6665 + 0.9])) + + def test_decompileDeltas(self): + decompileDeltas = TupleVariation.decompileDeltas_ + # 83 = zero values (0x80), count = 4 (1 + 0x83 & 0x3F) + self.assertEqual(([0, 0, 0, 0], 1), decompileDeltas(4, deHexStr("83"), 0)) + # 41 01 02 FF FF = signed 16-bit values (0x40), count = 2 (1 + 0x41 & 0x3F) + self.assertEqual(([258, -1], 5), decompileDeltas(2, deHexStr("41 01 02 FF FF"), 0)) + # 01 81 07 = signed 8-bit values, count = 2 (1 + 0x01 & 0x3F) + self.assertEqual(([-127, 7], 3), decompileDeltas(2, deHexStr("01 81 07"), 0)) + # combination of all three encodings, preceded and followed by 4 bytes of unused data + data = deHexStr("DE AD BE EF 83 40 01 02 01 81 80 DE AD BE EF") + self.assertEqual(([0, 0, 0, 0, 258, -127, -128], 11), decompileDeltas(7, data, 4)) + + def test_decompileDeltas_roundTrip(self): + numDeltas = 30 + compile = TupleVariation.compileDeltaValues_ + decompile = lambda data: TupleVariation.decompileDeltas_(numDeltas, data, 0)[0] + for i in range(50): + deltas = random.sample(range(-128, 127), 10) + deltas.extend(random.sample(range(-32768, 32767), 10)) + deltas.extend([0] * 10) + random.shuffle(deltas) + self.assertListEqual(deltas, decompile(compile(deltas))) + + def test_compileSharedTuples(self): + # Below, the peak coordinate {"wght": 1.0, "wdth": 0.7} appears + # three times; {"wght": 1.0, "wdth": 0.8} appears twice. + # Because the start and end of variation ranges is not encoded + # into the shared pool, they should get ignored. + deltas = [None] * 4 + variations = [ + TupleVariation({ + "wght": (1.0, 1.0, 1.0), + "wdth": (0.5, 0.7, 1.0) + }, deltas), + TupleVariation({ + "wght": (1.0, 1.0, 1.0), + "wdth": (0.2, 0.7, 1.0) + }, deltas), + TupleVariation({ + "wght": (1.0, 1.0, 1.0), + "wdth": (0.2, 0.8, 1.0) + }, deltas), + TupleVariation({ + "wght": (1.0, 1.0, 1.0), + "wdth": (0.3, 0.7, 1.0) + }, deltas), + TupleVariation({ + "wght": (1.0, 1.0, 1.0), + "wdth": (0.3, 0.8, 1.0) + }, deltas), + TupleVariation({ + "wght": (1.0, 1.0, 1.0), + "wdth": (0.3, 0.9, 1.0) + }, deltas) + ] + result = compileSharedTuples(["wght", "wdth"], variations) + self.assertEqual([hexencode(c) for c in result], + ["40 00 2C CD", "40 00 33 33"]) + + def test_decompileSharedTuples_Skia(self): + sharedTuples = decompileSharedTuples( + axisTags=["wght", "wdth"], sharedTupleCount=8, + data=SKIA_GVAR_SHARED_TUPLES_DATA, offset=0) + self.assertEqual(sharedTuples, SKIA_GVAR_SHARED_TUPLES) + + def test_decompileSharedTuples_empty(self): + self.assertEqual(decompileSharedTuples(["wght"], 0, b"", 0), []) + + def test_compileTupleVariationStore_allVariationsRedundant(self): + axes = {"wght": (0.3, 0.4, 0.5), "opsz": (0.7, 0.8, 0.9)} + variations = [ + TupleVariation(axes, [None] * 4), + TupleVariation(axes, [None] * 4), + TupleVariation(axes, [None] * 4) + ] + self.assertEqual( + compileTupleVariationStore(variations, pointCount=8, + axisTags=["wght", "opsz"], + sharedTupleIndices={}), + (0, b"", b"")) + + def test_compileTupleVariationStore_noVariations(self): + self.assertEqual( + compileTupleVariationStore(variations=[], pointCount=8, + axisTags=["wght", "opsz"], + sharedTupleIndices={}), + (0, b"", b"")) + + def test_compileTupleVariationStore_roundTrip_cvar(self): + deltas = [1, 2, 3, 4] + variations = [ + TupleVariation({"wght": (0.5, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)}, + deltas), + TupleVariation({"wght": (1.0, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)}, + deltas) + ] + tupleVariationCount, tuples, data = compileTupleVariationStore( + variations, pointCount=4, axisTags=["wght", "wdth"], + sharedTupleIndices={}) + self.assertEqual( + decompileTupleVariationStore("cvar", ["wght", "wdth"], + tupleVariationCount, pointCount=4, + sharedTuples={}, data=(tuples + data), + pos=0, dataPos=len(tuples)), + variations) + + def test_compileTupleVariationStore_roundTrip_gvar(self): + deltas = [(1,1), (2,2), (3,3), (4,4)] + variations = [ + TupleVariation({"wght": (0.5, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)}, + deltas), + TupleVariation({"wght": (1.0, 1.0, 1.0), "wdth": (1.0, 1.0, 1.0)}, + deltas) + ] + tupleVariationCount, tuples, data = compileTupleVariationStore( + variations, pointCount=4, axisTags=["wght", "wdth"], + sharedTupleIndices={}) + self.assertEqual( + decompileTupleVariationStore("gvar", ["wght", "wdth"], + tupleVariationCount, pointCount=4, + sharedTuples={}, data=(tuples + data), + pos=0, dataPos=len(tuples)), + variations) + + def test_decompileTupleVariationStore_Skia_I(self): + tvar = decompileTupleVariationStore( + tableTag="gvar", axisTags=["wght", "wdth"], + tupleVariationCount=8, pointCount=18, + sharedTuples=SKIA_GVAR_SHARED_TUPLES, + data=SKIA_GVAR_I_DATA, pos=4, dataPos=36) + self.assertEqual(len(tvar), 8) + self.assertEqual(tvar[0].axes, {"wght": (0.0, 1.0, 1.0)}) + self.assertEqual( + " ".join(["%d,%d" % c for c in tvar[0].coordinates]), + "257,0 -127,0 -128,58 -130,90 -130,62 -130,67 -130,32 -127,0 " + "257,0 259,14 260,64 260,21 260,69 258,124 0,0 130,0 0,0 0,0") + + def test_decompileTupleVariationStore_empty(self): + self.assertEqual( + decompileTupleVariationStore(tableTag="gvar", axisTags=[], + tupleVariationCount=0, pointCount=5, + sharedTuples=[], + data=b"", pos=4, dataPos=4), + []) + + def test_getTupleSize(self): + getTupleSize = TupleVariation.getTupleSize_ + numAxes = 3 + self.assertEqual(4 + numAxes * 2, getTupleSize(0x8042, numAxes)) + self.assertEqual(4 + numAxes * 4, getTupleSize(0x4077, numAxes)) + self.assertEqual(4, getTupleSize(0x2077, numAxes)) + self.assertEqual(4, getTupleSize(11, numAxes)) + + def test_inferRegion(self): + start, end = inferRegion_({"wght": -0.3, "wdth": 0.7}) + self.assertEqual(start, {"wght": -0.3, "wdth": 0.0}) + self.assertEqual(end, {"wght": 0.0, "wdth": 0.7}) + + @staticmethod + def xml_lines(writer): + content = writer.file.getvalue().decode("utf-8") + return [line.strip() for line in content.splitlines()][1:] + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_a_n_k_r_test.py b/Tests/ttLib/tables/_a_n_k_r_test.py new file mode 100644 index 0000000..0327e46 --- /dev/null +++ b/Tests/ttLib/tables/_a_n_k_r_test.py @@ -0,0 +1,167 @@ +# coding: utf-8 +from __future__ import print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc.testTools import FakeFont, getXML, parseXML +from fontTools.misc.textTools import deHexStr, hexStr +from fontTools.ttLib import newTable +import unittest + + +# This is the anchor points table of the first font file in +# “/Library/Fonts/Devanagari Sangam MN.ttc” on macOS 10.12.6. +# For testing, we’ve changed the GlyphIDs to smaller values. +# Also, in the AATLookup, we’ve changed GlyphDataOffset value +# for the end-of-table marker from 0xFFFF to 0 since that is +# what our encoder emits. (The value for end-of-table markers +# does not actually matter). +ANKR_FORMAT_0_DATA = deHexStr( + '0000 0000 ' # 0: Format=0, Flags=0 + '0000 000C ' # 4: LookupTableOffset=12 + '0000 0024 ' # 8: GlyphDataTableOffset=36 + '0006 0004 0002 ' # 12: LookupFormat=6, UnitSize=4, NUnits=2 + '0008 0001 0000 ' # 18: SearchRange=8, EntrySelector=1, RangeShift=0 + '0001 0000 ' # 24: Glyph=A, Offset=0 (+GlyphDataTableOffset=36) + '0003 0008 ' # 28: Glyph=C, Offset=8 (+GlyphDataTableOffset=44) + 'FFFF 0000 ' # 32: Glyph=, Offset= + '0000 0001 ' # 36: GlyphData[A].NumPoints=1 + '0235 045E ' # 40: GlyphData[A].Points[0].X=565, .Y=1118 + '0000 0001 ' # 44: GlyphData[C].NumPoints=1 + 'FED2 045E ' # 48: GlyphData[C].Points[0].X=-302, .Y=1118 +) # 52: +assert len(ANKR_FORMAT_0_DATA) == 52 + + +ANKR_FORMAT_0_XML = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +# Same data as ANKR_FORMAT_0_DATA, but with chunks of unused data +# whose presence should not stop us from decompiling the table. +ANKR_FORMAT_0_STRAY_DATA = deHexStr( + '0000 0000 ' # 0: Format=0, Flags=0 + '0000 0018 ' # 4: LookupTableOffset=24 + '0000 0034 ' # 8: GlyphDataTableOffset=52 + 'DEAD BEEF CAFE ' # 12: + 'DEAD BEEF CAFE ' # 18: + '0006 0004 0002 ' # 24: LookupFormat=6, UnitSize=4, NUnits=2 + '0008 0001 0000 ' # 30: SearchRange=8, EntrySelector=1, RangeShift=0 + '0001 0000 ' # 36: Glyph=A, Offset=0 (+GlyphDataTableOffset=52) + '0003 0008 ' # 40: Glyph=C, Offset=8 (+GlyphDataTableOffset=60) + 'FFFF 0000 ' # 44: Glyph=, Offset= + 'BEEF F00D ' # 48: + '0000 0001 ' # 52: GlyphData[A].NumPoints=1 + '0235 045E ' # 56: GlyphData[A].Points[0].X=565, .Y=1118 + '0000 0001 ' # 60: GlyphData[C].NumPoints=1 + 'FED2 045E ' # 64: GlyphData[C].Points[0].X=-302, .Y=1118 +) # 68: +assert len(ANKR_FORMAT_0_STRAY_DATA) == 68 + + +# Constructed test case where glyphs A and D share the same anchor data. +ANKR_FORMAT_0_SHARING_DATA = deHexStr( + '0000 0000 ' # 0: Format=0, Flags=0 + '0000 000C ' # 4: LookupTableOffset=12 + '0000 0028 ' # 8: GlyphDataTableOffset=40 + '0006 0004 0003 ' # 12: LookupFormat=6, UnitSize=4, NUnits=3 + '0008 0001 0004 ' # 18: SearchRange=8, EntrySelector=1, RangeShift=4 + '0001 0000 ' # 24: Glyph=A, Offset=0 (+GlyphDataTableOffset=36) + '0003 0008 ' # 28: Glyph=C, Offset=8 (+GlyphDataTableOffset=44) + '0004 0000 ' # 32: Glyph=D, Offset=0 (+GlyphDataTableOffset=36) + 'FFFF 0000 ' # 36: Glyph=, Offset= + '0000 0001 ' # 40: GlyphData[A].NumPoints=1 + '0235 045E ' # 44: GlyphData[A].Points[0].X=565, .Y=1118 + '0000 0002 ' # 48: GlyphData[C].NumPoints=2 + '000B 000C ' # 52: GlyphData[C].Points[0].X=11, .Y=12 + '001B 001C ' # 56: GlyphData[C].Points[1].X=27, .Y=28 +) # 60: +assert len(ANKR_FORMAT_0_SHARING_DATA) == 60 + + +ANKR_FORMAT_0_SHARING_XML = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +class ANKRTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.maxDiff = None + cls.font = FakeFont(['.notdef', 'A', 'B', 'C', 'D']) + + def decompileToXML(self, data, xml): + table = newTable('ankr') + table.decompile(data, self.font) + self.assertEqual(getXML(table.toXML), xml) + + def compileFromXML(self, xml, data): + table = newTable('ankr') + for name, attrs, content in parseXML(xml): + table.fromXML(name, attrs, content, font=self.font) + self.assertEqual(hexStr(table.compile(self.font)), hexStr(data)) + + def roundtrip(self, data, xml): + self.decompileToXML(data, xml) + self.compileFromXML(xml, data) + + def testFormat0(self): + self.roundtrip(ANKR_FORMAT_0_DATA, ANKR_FORMAT_0_XML) + + def testFormat0_stray(self): + self.decompileToXML(ANKR_FORMAT_0_STRAY_DATA, ANKR_FORMAT_0_XML) + + def testFormat0_sharing(self): + self.roundtrip(ANKR_FORMAT_0_SHARING_DATA, ANKR_FORMAT_0_SHARING_XML) + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_a_v_a_r_test.py b/Tests/ttLib/tables/_a_v_a_r_test.py new file mode 100644 index 0000000..7b92118 --- /dev/null +++ b/Tests/ttLib/tables/_a_v_a_r_test.py @@ -0,0 +1,85 @@ +from __future__ import print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc.testTools import parseXML +from fontTools.misc.textTools import deHexStr +from fontTools.misc.xmlWriter import XMLWriter +from fontTools.ttLib import TTLibError +from fontTools.ttLib.tables._a_v_a_r import table__a_v_a_r +from fontTools.ttLib.tables._f_v_a_r import table__f_v_a_r, Axis +import collections +import logging +import unittest + + +TEST_DATA = deHexStr( + "00 01 00 00 00 00 00 02 " + "00 04 C0 00 C0 00 00 00 00 00 13 33 33 33 40 00 40 00 " + "00 03 C0 00 C0 00 00 00 00 00 40 00 40 00") + + +class AxisVariationTableTest(unittest.TestCase): + def test_compile(self): + avar = table__a_v_a_r() + avar.segments["wdth"] = {-1.0: -1.0, 0.0: 0.0, 0.3: 0.8, 1.0: 1.0} + avar.segments["wght"] = {-1.0: -1.0, 0.0: 0.0, 1.0: 1.0} + self.assertEqual(TEST_DATA, avar.compile(self.makeFont(["wdth", "wght"]))) + + def test_decompile(self): + avar = table__a_v_a_r() + avar.decompile(TEST_DATA, self.makeFont(["wdth", "wght"])) + self.assertEqual({ + "wdth": {-1.0: -1.0, 0.0: 0.0, 0.3: 0.8, 1.0: 1.0}, + "wght": {-1.0: -1.0, 0.0: 0.0, 1.0: 1.0} + }, avar.segments) + + def test_decompile_unsupportedVersion(self): + avar = table__a_v_a_r() + font = self.makeFont(["wdth", "wght"]) + self.assertRaises(TTLibError, avar.decompile, deHexStr("02 01 03 06 00 00 00 00"), font) + + def test_toXML(self): + avar = table__a_v_a_r() + avar.segments["opsz"] = {-1.0: -1.0, 0.0: 0.0, 0.3: 0.8, 1.0: 1.0} + writer = XMLWriter(BytesIO()) + avar.toXML(writer, self.makeFont(["opsz"])) + self.assertEqual([ + '', + '', + '', + '', + '', + '' + ], self.xml_lines(writer)) + + def test_fromXML(self): + avar = table__a_v_a_r() + for name, attrs, content in parseXML( + '' + ' ' + ' ' + ' ' + ' ' + ''): + avar.fromXML(name, attrs, content, ttFont=None) + self.assertEqual({"wdth": {-1: -1, 0: 0, 0.7: 0.2, 1.0: 1.0}}, + avar.segments) + + @staticmethod + def makeFont(axisTags): + """['opsz', 'wdth'] --> ttFont""" + fvar = table__f_v_a_r() + for tag in axisTags: + axis = Axis() + axis.axisTag = tag + fvar.axes.append(axis) + return {"fvar": fvar} + + @staticmethod + def xml_lines(writer): + content = writer.file.getvalue().decode("utf-8") + return [line.strip() for line in content.splitlines()][1:] + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_b_s_l_n_test.py b/Tests/ttLib/tables/_b_s_l_n_test.py new file mode 100644 index 0000000..97ffa6a --- /dev/null +++ b/Tests/ttLib/tables/_b_s_l_n_test.py @@ -0,0 +1,311 @@ +from __future__ import print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc.testTools import FakeFont, getXML, parseXML +from fontTools.misc.textTools import deHexStr, hexStr +from fontTools.ttLib import newTable +import unittest + + +# Apple's spec of the baseline table gives no example for 'bsln' format 0, +# but the Apple Chancery font contains the following data. +BSLN_FORMAT_0_DATA = deHexStr( + '0001 0000 0000 ' # 0: Version=1.0, Format=0 + '0000 ' # 6: DefaultBaseline=0 (Roman baseline) + '0000 01D1 0000 0541 ' # 8: Delta[0..3]=0, 465, 0, 1345 + '01FB 0000 0000 0000 ' # 16: Delta[4..7]=507, 0, 0, 0 + '0000 0000 0000 0000 ' # 24: Delta[8..11]=0, 0, 0, 0 + '0000 0000 0000 0000 ' # 32: Delta[12..15]=0, 0, 0, 0 + '0000 0000 0000 0000 ' # 40: Delta[16..19]=0, 0, 0, 0 + '0000 0000 0000 0000 ' # 48: Delta[20..23]=0, 0, 0, 0 + '0000 0000 0000 0000 ' # 56: Delta[24..27]=0, 0, 0, 0 + '0000 0000 0000 0000 ' # 64: Delta[28..31]=0, 0, 0, 0 +) # 72: +assert len(BSLN_FORMAT_0_DATA) == 72 + + +BSLN_FORMAT_0_XML = [ + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +# Example: Format 1 Baseline Table +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6bsln.html +# The example in the AAT specification uses the value 270 for Seg[0].LastGlyph, +# whereas we use the value 10 for testng to shorten the XML dump. +BSLN_FORMAT_1_DATA = deHexStr( + '0001 0000 0001 ' # 0: Version=1.0, Format=1 + '0001 ' # 6: DefaultBaseline=1 (Ideographic baseline) + '0000 0357 0000 05F0 ' # 8: Delta[0..3]=0, 855, 0, 1520 + '0000 0000 0000 0000 ' # 16: Delta[4..7]=0, 0, 0, 0 + '0000 0000 0000 0000 ' # 24: Delta[8..11]=0, 0, 0, 0 + '0000 0000 0000 0000 ' # 32: Delta[12..15]=0, 0, 0, 0 + '0000 0000 0000 0000 ' # 40: Delta[16..19]=0, 0, 0, 0 + '0000 0000 0000 0000 ' # 48: Delta[20..23]=0, 0, 0, 0 + '0000 0000 0000 0000 ' # 56: Delta[24..27]=0, 0, 0, 0 + '0000 0000 0000 0000 ' # 64: Delta[28..31]=0, 0, 0, 0 + '0002 0006 0001 ' # 72: LookupFormat=2, UnitSize=6, NUnits=1 + '0006 0000 0000 ' # 78: SearchRange=6, EntrySelector=0, RangeShift=0 + '000A 0002 0000 ' # 84: Seg[0].LastGlyph=10 FirstGl=2 Value=0/Roman + 'FFFF FFFF 0000 ' # 90: Seg[1]= +) # 96: +assert len(BSLN_FORMAT_1_DATA) == 96 + + +BSLN_FORMAT_1_XML = [ + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +BSLN_FORMAT_2_DATA = deHexStr( + '0001 0000 0002 ' # 0: Version=1.0, Format=2 + '0004 ' # 6: DefaultBaseline=4 (Math) + '0016 ' # 8: StandardGlyph=22 + '0050 0051 FFFF 0052 ' # 10: ControlPoint[0..3]=80, 81, , 82 + 'FFFF FFFF FFFF FFFF ' # 18: ControlPoint[4..7]= + 'FFFF FFFF FFFF FFFF ' # 26: ControlPoint[8..11]= + 'FFFF FFFF FFFF FFFF ' # 34: ControlPoint[12..15]= + 'FFFF FFFF FFFF FFFF ' # 42: ControlPoint[16..19]= + 'FFFF FFFF FFFF FFFF ' # 50: ControlPoint[20..23]= + 'FFFF FFFF FFFF FFFF ' # 58: ControlPoint[24..27]= + 'FFFF FFFF FFFF FFFF ' # 66: ControlPoint[28..31]= +) # 74: +assert len(BSLN_FORMAT_2_DATA) == 74 + + +BSLN_FORMAT_2_XML = [ + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +# Example: Format 3 Baseline Table +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6bsln.html +# The example in the AAT specification uses the value 270 for Seg[0].LastGlyph, +# whereas we use the value 10 for testng to shorten the XML dump. +BSLN_FORMAT_3_DATA = deHexStr( + '0001 0000 0003 ' # 0: Version=1.0, Format=3 + '0001 ' # 6: DefaultBaseline=1 (Ideographic) + '0016 ' # 8: StandardGlyph=22 + '0050 0051 FFFF 0052 ' # 10: ControlPoint[0..3]=80, 81, , 82 + 'FFFF FFFF FFFF FFFF ' # 18: ControlPoint[4..7]= + 'FFFF FFFF FFFF FFFF ' # 26: ControlPoint[8..11]= + 'FFFF FFFF FFFF FFFF ' # 34: ControlPoint[12..15]= + 'FFFF FFFF FFFF FFFF ' # 42: ControlPoint[16..19]= + 'FFFF FFFF FFFF FFFF ' # 50: ControlPoint[20..23]= + 'FFFF FFFF FFFF FFFF ' # 58: ControlPoint[24..27]= + 'FFFF FFFF FFFF FFFF ' # 66: ControlPoint[28..31]= + '0002 0006 0001 ' # 74: LookupFormat=2, UnitSize=6, NUnits=1 + '0006 0000 0000 ' # 80: SearchRange=6, EntrySelector=0, RangeShift=0 + '000A 0002 0000 ' # 86: Seg[0].LastGlyph=10 FirstGl=2 Value=0/Roman + 'FFFF FFFF 0000 ' # 92: Seg[1]= +) # 98: +assert len(BSLN_FORMAT_3_DATA) == 98 + + +BSLN_FORMAT_3_XML = [ + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +class BSLNTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.maxDiff = None + cls.font = FakeFont( + ['.notdef'] + [g for g in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ']) + + def decompileToXML(self, data, xml): + table = newTable('bsln') + table.decompile(data, self.font) + self.assertEqual(getXML(table.toXML), xml) + + def compileFromXML(self, xml, data): + table = newTable('bsln') + for name, attrs, content in parseXML(xml): + table.fromXML(name, attrs, content, font=self.font) + self.assertEqual(hexStr(table.compile(self.font)), hexStr(data)) + + def testFormat0(self): + self.decompileToXML(BSLN_FORMAT_0_DATA, BSLN_FORMAT_0_XML) + self.compileFromXML(BSLN_FORMAT_0_XML, BSLN_FORMAT_0_DATA) + + def testFormat1(self): + self.decompileToXML(BSLN_FORMAT_1_DATA, BSLN_FORMAT_1_XML) + self.compileFromXML(BSLN_FORMAT_1_XML, BSLN_FORMAT_1_DATA) + + def testFormat2(self): + self.decompileToXML(BSLN_FORMAT_2_DATA, BSLN_FORMAT_2_XML) + self.compileFromXML(BSLN_FORMAT_2_XML, BSLN_FORMAT_2_DATA) + + def testFormat3(self): + self.decompileToXML(BSLN_FORMAT_3_DATA, BSLN_FORMAT_3_XML) + self.compileFromXML(BSLN_FORMAT_3_XML, BSLN_FORMAT_3_DATA) + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_c_i_d_g_test.py b/Tests/ttLib/tables/_c_i_d_g_test.py new file mode 100644 index 0000000..c6e8d3c --- /dev/null +++ b/Tests/ttLib/tables/_c_i_d_g_test.py @@ -0,0 +1,70 @@ +# coding: utf-8 +from __future__ import print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc.testTools import FakeFont, getXML, parseXML +from fontTools.misc.textTools import deHexStr, hexStr +from fontTools.ttLib import newTable +import unittest + + +# On macOS X 10.12.6, the first font in /System/Library/Fonts/PingFang.ttc +# has a ‘cidg’ table with a similar structure as this test data, just larger. +CIDG_DATA = deHexStr( + "0000 0000 " # 0: Format=0, Flags=0 + "0000 0098 " # 4: StructLength=152 + "0000 " # 8: Registry=0 + "41 64 6F 62 65 " # 10: RegistryName="Adobe" + + ("00" * 59) + # 15: + "0002 " # 74: Order=2 + "43 4E 53 31 " # 76: Order="CNS1" + + ("00" * 60) + # 80: + "0000 " # 140: SupplementVersion=0 + "0004 " # 142: Count + "0000 " # 144: GlyphID[0]=.notdef + "FFFF " # 146: CIDs[1]= + "0003 " # 148: CIDs[2]=C + "0001 " # 150: CIDs[3]=A +) # 152: +assert len(CIDG_DATA) == 152, len(CIDG_DATA) + + +CIDG_XML = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +class GCIDTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.maxDiff = None + cls.font = FakeFont(['.notdef', 'A', 'B', 'C', 'D']) + + def testDecompileToXML(self): + table = newTable('cidg') + table.decompile(CIDG_DATA, self.font) + self.assertEqual(getXML(table.toXML, self.font), CIDG_XML) + + def testCompileFromXML(self): + table = newTable('cidg') + for name, attrs, content in parseXML(CIDG_XML): + table.fromXML(name, attrs, content, font=self.font) + self.assertEqual(hexStr(table.compile(self.font)), + hexStr(CIDG_DATA)) + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_c_m_a_p_test.py b/Tests/ttLib/tables/_c_m_a_p_test.py new file mode 100644 index 0000000..6647345 --- /dev/null +++ b/Tests/ttLib/tables/_c_m_a_p_test.py @@ -0,0 +1,88 @@ +from __future__ import print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +from fontTools import ttLib +import unittest +from fontTools.ttLib.tables._c_m_a_p import CmapSubtable, table__c_m_a_p + +class CmapSubtableTest(unittest.TestCase): + + def makeSubtable(self, cmapFormat, platformID, platEncID, langID): + subtable = CmapSubtable.newSubtable(cmapFormat) + subtable.platformID, subtable.platEncID, subtable.language = (platformID, platEncID, langID) + return subtable + + def test_toUnicode_utf16be(self): + subtable = self.makeSubtable(4, 0, 2, 7) + self.assertEqual("utf_16_be", subtable.getEncoding()) + self.assertEqual(True, subtable.isUnicode()) + + def test_toUnicode_macroman(self): + subtable = self.makeSubtable(4, 1, 0, 7) # MacRoman + self.assertEqual("mac_roman", subtable.getEncoding()) + self.assertEqual(False, subtable.isUnicode()) + + def test_toUnicode_macromanian(self): + subtable = self.makeSubtable(4, 1, 0, 37) # Mac Romanian + self.assertNotEqual(None, subtable.getEncoding()) + self.assertEqual(False, subtable.isUnicode()) + + def test_extended_mac_encodings(self): + subtable = self.makeSubtable(4, 1, 1, 0) # Mac Japanese + self.assertNotEqual(None, subtable.getEncoding()) + self.assertEqual(False, subtable.isUnicode()) + + def test_extended_unknown(self): + subtable = self.makeSubtable(4, 10, 11, 12) + self.assertEqual(subtable.getEncoding(), None) + self.assertEqual(subtable.getEncoding("ascii"), "ascii") + self.assertEqual(subtable.getEncoding(default="xyz"), "xyz") + + def test_decompile_4(self): + subtable = CmapSubtable.newSubtable(4) + font = ttLib.TTFont() + font.setGlyphOrder([]) + subtable.decompile(b'\0' * 3 + b'\x10' + b'\0' * 12, font) + + def test_decompile_12(self): + subtable = CmapSubtable.newSubtable(12) + font = ttLib.TTFont() + font.setGlyphOrder([]) + subtable.decompile(b'\0' * 7 + b'\x10' + b'\0' * 8, font) + + def test_buildReversed(self): + c4 = self.makeSubtable(4, 3, 1, 0) + c4.cmap = {0x0041:'A', 0x0391:'A'} + c12 = self.makeSubtable(12, 3, 10, 0) + c12.cmap = {0x10314: 'u10314'} + cmap = table__c_m_a_p() + cmap.tables = [c4, c12] + self.assertEqual(cmap.buildReversed(), {'A':{0x0041, 0x0391}, 'u10314':{0x10314}}) + + def test_getBestCmap(self): + c4 = self.makeSubtable(4, 3, 1, 0) + c4.cmap = {0x0041:'A', 0x0391:'A'} + c12 = self.makeSubtable(12, 3, 10, 0) + c12.cmap = {0x10314: 'u10314'} + cmap = table__c_m_a_p() + cmap.tables = [c4, c12] + self.assertEqual(cmap.getBestCmap(), {0x10314: 'u10314'}) + self.assertEqual(cmap.getBestCmap(cmapPreferences=[(3, 1)]), {0x0041:'A', 0x0391:'A'}) + self.assertEqual(cmap.getBestCmap(cmapPreferences=[(0, 4)]), None) + + def test_font_getBestCmap(self): + c4 = self.makeSubtable(4, 3, 1, 0) + c4.cmap = {0x0041:'A', 0x0391:'A'} + c12 = self.makeSubtable(12, 3, 10, 0) + c12.cmap = {0x10314: 'u10314'} + cmap = table__c_m_a_p() + cmap.tables = [c4, c12] + font = ttLib.TTFont() + font["cmap"] = cmap + self.assertEqual(font.getBestCmap(), {0x10314: 'u10314'}) + self.assertEqual(font.getBestCmap(cmapPreferences=[(3, 1)]), {0x0041:'A', 0x0391:'A'}) + self.assertEqual(font.getBestCmap(cmapPreferences=[(0, 4)]), None) + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_c_v_a_r_test.py b/Tests/ttLib/tables/_c_v_a_r_test.py new file mode 100644 index 0000000..0fd5531 --- /dev/null +++ b/Tests/ttLib/tables/_c_v_a_r_test.py @@ -0,0 +1,111 @@ +from __future__ import \ + print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc.testTools import getXML, parseXML +from fontTools.misc.textTools import deHexStr, hexStr +from fontTools.ttLib import TTLibError, getTableModule, newTable +from fontTools.ttLib.tables.TupleVariation import TupleVariation + +import unittest + + +CVAR_DATA = deHexStr( + "0001 0000 " # 0: majorVersion=1 minorVersion=0 + "8002 0018 " # 4: tupleVariationCount=2|TUPLES_SHARE_POINT_NUMBERS offsetToData=24 + "0004 " # 8: tvHeader[0].variationDataSize=4 + "8000 " # 10: tvHeader[0].tupleIndex=EMBEDDED_PEAK + "4000 0000 " # 12: tvHeader[0].peakTuple=[1.0, 0.0] + "0004 " # 16: tvHeader[1].variationDataSize=4 + "8000 " # 18: tvHeader[1].tupleIndex=EMBEDDED_PEAK + "C000 3333 " # 20: tvHeader[1].peakTuple=[-1.0, 0.8] + "03 02 02 01 01" # 24: shared_pointCount=03, run_count=2 cvt=[2, 3, 4] + "02 03 01 04 " # 25: deltas=[3, 1, 4] + "02 09 07 08") # 29: deltas=[9, 7, 8] + +CVAR_PRIVATE_POINT_DATA = deHexStr( + "0001 0000 " # 0: majorVersion=1 minorVersion=0 + "0002 0018 " # 4: tupleVariationCount=2 offsetToData=24 + "0009 " # 8: tvHeader[0].variationDataSize=9 + "A000 " # 10: tvHeader[0].tupleIndex=EMBEDDED_PEAK|PRIVATE_POINT_NUMBERS + "4000 0000 " # 12: tvHeader[0].peakTuple=[1.0, 0.0] + "0009 " # 16: tvHeader[1].variationDataSize=9 + "A000 " # 18: tvHeader[1].tupleIndex=EMBEDDED_PEAK|PRIVATE_POINT_NUMBERS + "C000 3333 " # 20: tvHeader[1].peakTuple=[-1.0, 0.8] + "03 02 02 01 01 02 03 01 04 " # 24: pointCount=3 run_count=2 cvt=2 1 1 run_count=2 deltas=[3, 1, 4] + "03 02 02 01 01 02 09 07 08 ") # 33: pointCount=3 run_count=2 cvt=2 1 1 run_count=2 deltas=[9, 7, 8] + +CVAR_XML = [ + '', + '', + ' ', + ' ', + ' ', + ' ', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + +CVAR_VARIATIONS = [ + TupleVariation({"wght": (0.0, 1.0, 1.0)}, [None, None, 3, 1, 4]), + TupleVariation({"wght": (-1, -1.0, 0.0), "wdth": (0.0, 0.8, 0.8)}, + [None, None, 9, 7, 8]), +] + + +class CVARTableTest(unittest.TestCase): + def makeFont(self): + cvt, cvar, fvar = newTable("cvt "), newTable("cvar"), newTable("fvar") + font = {"cvt ": cvt, "cvar": cvar, "fvar": fvar} + cvt.values = [0, 0, 0, 1000, -2000] + Axis = getTableModule("fvar").Axis + fvar.axes = [Axis(), Axis()] + fvar.axes[0].axisTag, fvar.axes[1].axisTag = "wght", "wdth" + return font, cvar + + def test_compile(self): + font, cvar = self.makeFont() + cvar.variations = CVAR_VARIATIONS + self.assertEqual(hexStr(cvar.compile(font)), hexStr(CVAR_PRIVATE_POINT_DATA)) + + def test_compile_shared_points(self): + font, cvar = self.makeFont() + cvar.variations = CVAR_VARIATIONS + self.assertEqual(hexStr(cvar.compile(font, useSharedPoints=True)), hexStr(CVAR_DATA)) + + def test_decompile(self): + font, cvar = self.makeFont() + cvar.decompile(CVAR_PRIVATE_POINT_DATA, font) + self.assertEqual(cvar.majorVersion, 1) + self.assertEqual(cvar.minorVersion, 0) + self.assertEqual(cvar.variations, CVAR_VARIATIONS) + + def test_decompile_shared_points(self): + font, cvar = self.makeFont() + cvar.decompile(CVAR_DATA, font) + self.assertEqual(cvar.majorVersion, 1) + self.assertEqual(cvar.minorVersion, 0) + self.assertEqual(cvar.variations, CVAR_VARIATIONS) + + def test_fromXML(self): + font, cvar = self.makeFont() + for name, attrs, content in parseXML(CVAR_XML): + cvar.fromXML(name, attrs, content, ttFont=font) + self.assertEqual(cvar.majorVersion, 1) + self.assertEqual(cvar.minorVersion, 0) + self.assertEqual(cvar.variations, CVAR_VARIATIONS) + + def test_toXML(self): + font, cvar = self.makeFont() + cvar.variations = CVAR_VARIATIONS + self.assertEqual(getXML(cvar.toXML, font), CVAR_XML) + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_f_p_g_m_test.py b/Tests/ttLib/tables/_f_p_g_m_test.py new file mode 100644 index 0000000..71fec48 --- /dev/null +++ b/Tests/ttLib/tables/_f_p_g_m_test.py @@ -0,0 +1,20 @@ +from __future__ import print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +from fontTools.ttLib.tables._f_p_g_m import table__f_p_g_m +from fontTools.ttLib.tables import ttProgram + + +def test__bool__(): + fpgm = table__f_p_g_m() + assert not bool(fpgm) + + p = ttProgram.Program() + fpgm.program = p + assert not bool(fpgm) + + bc = bytearray([0]) + p.fromBytecode(bc) + assert bool(fpgm) + + p.bytecode.pop() + assert not bool(fpgm) diff --git a/Tests/ttLib/tables/_f_v_a_r_test.py b/Tests/ttLib/tables/_f_v_a_r_test.py new file mode 100644 index 0000000..4a8e767 --- /dev/null +++ b/Tests/ttLib/tables/_f_v_a_r_test.py @@ -0,0 +1,264 @@ +from __future__ import print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc.testTools import parseXML +from fontTools.misc.textTools import deHexStr +from fontTools.misc.xmlWriter import XMLWriter +from fontTools.ttLib import TTLibError +from fontTools.ttLib.tables._f_v_a_r import table__f_v_a_r, Axis, NamedInstance +from fontTools.ttLib.tables._n_a_m_e import table__n_a_m_e, NameRecord +import unittest + + + +FVAR_DATA = deHexStr( + "00 01 00 00 00 10 00 02 00 02 00 14 00 02 00 0C " + "77 67 68 74 00 64 00 00 01 90 00 00 03 84 00 00 00 00 01 01 " + "77 64 74 68 00 32 00 00 00 64 00 00 00 c8 00 00 00 00 01 02 " + "01 03 00 00 01 2c 00 00 00 64 00 00 " + "01 04 00 00 01 2c 00 00 00 4b 00 00") + +FVAR_AXIS_DATA = deHexStr( + "6F 70 73 7a ff ff 80 00 00 01 4c cd 00 01 80 00 00 00 01 59") + +FVAR_INSTANCE_DATA_WITHOUT_PSNAME = deHexStr( + "01 59 00 00 00 00 b3 33 00 00 80 00") + +FVAR_INSTANCE_DATA_WITH_PSNAME = ( + FVAR_INSTANCE_DATA_WITHOUT_PSNAME + deHexStr("02 34")) + + +def xml_lines(writer): + content = writer.file.getvalue().decode("utf-8") + return [line.strip() for line in content.splitlines()][1:] + + +def AddName(font, name): + nameTable = font.get("name") + if nameTable is None: + nameTable = font["name"] = table__n_a_m_e() + nameTable.names = [] + namerec = NameRecord() + namerec.nameID = 1 + max([n.nameID for n in nameTable.names] + [256]) + namerec.string = name.encode('mac_roman') + namerec.platformID, namerec.platEncID, namerec.langID = (1, 0, 0) + nameTable.names.append(namerec) + return namerec + + +def MakeFont(): + axes = [("wght", "Weight", 100, 400, 900), ("wdth", "Width", 50, 100, 200)] + instances = [("Light", 300, 100), ("Light Condensed", 300, 75)] + fvarTable = table__f_v_a_r() + font = {"fvar": fvarTable} + for tag, name, minValue, defaultValue, maxValue in axes: + axis = Axis() + axis.axisTag = tag + axis.defaultValue = defaultValue + axis.minValue, axis.maxValue = minValue, maxValue + axis.axisNameID = AddName(font, name).nameID + fvarTable.axes.append(axis) + for name, weight, width in instances: + inst = NamedInstance() + inst.subfamilyNameID = AddName(font, name).nameID + inst.coordinates = {"wght": weight, "wdth": width} + fvarTable.instances.append(inst) + return font + + +class FontVariationTableTest(unittest.TestCase): + def test_compile(self): + font = MakeFont() + h = font["fvar"].compile(font) + self.assertEqual(FVAR_DATA, font["fvar"].compile(font)) + + def test_decompile(self): + fvar = table__f_v_a_r() + fvar.decompile(FVAR_DATA, ttFont={"fvar": fvar}) + self.assertEqual(["wght", "wdth"], [a.axisTag for a in fvar.axes]) + self.assertEqual([259, 260], [i.subfamilyNameID for i in fvar.instances]) + + def test_toXML(self): + font = MakeFont() + writer = XMLWriter(BytesIO()) + font["fvar"].toXML(writer, font) + xml = writer.file.getvalue().decode("utf-8") + self.assertEqual(2, xml.count("")) + self.assertTrue("wght" in xml) + self.assertTrue("wdth" in xml) + self.assertEqual(2, xml.count("" in xml) + self.assertTrue("" in xml) + + def test_fromXML(self): + fvar = table__f_v_a_r() + for name, attrs, content in parseXML( + '' + ' opsz' + '' + '' + ' slnt' + ' 0x123' + '' + '' + ''): + fvar.fromXML(name, attrs, content, ttFont=None) + self.assertEqual(["opsz", "slnt"], [a.axisTag for a in fvar.axes]) + self.assertEqual([0, 0x123], [a.flags for a in fvar.axes]) + self.assertEqual([765, 234], [i.subfamilyNameID for i in fvar.instances]) + + +class AxisTest(unittest.TestCase): + def test_compile(self): + axis = Axis() + axis.axisTag, axis.axisNameID = ('opsz', 345) + axis.minValue, axis.defaultValue, axis.maxValue = (-0.5, 1.3, 1.5) + self.assertEqual(FVAR_AXIS_DATA, axis.compile()) + + def test_decompile(self): + axis = Axis() + axis.decompile(FVAR_AXIS_DATA) + self.assertEqual("opsz", axis.axisTag) + self.assertEqual(345, axis.axisNameID) + self.assertEqual(-0.5, axis.minValue) + self.assertEqual(1.3, axis.defaultValue) + self.assertEqual(1.5, axis.maxValue) + + def test_toXML(self): + font = MakeFont() + axis = Axis() + axis.decompile(FVAR_AXIS_DATA) + AddName(font, "Optical Size").nameID = 256 + axis.axisNameID = 256 + axis.flags = 0xABC + writer = XMLWriter(BytesIO()) + axis.toXML(writer, font) + self.assertEqual([ + '', + '', + '', + 'opsz', + '0xABC', + '-0.5', + '1.3', + '1.5', + '256', + '' + ], xml_lines(writer)) + + def test_fromXML(self): + axis = Axis() + for name, attrs, content in parseXML( + '' + ' wght' + ' 0x123ABC' + ' 100' + ' 400' + ' 900' + ' 256' + ''): + axis.fromXML(name, attrs, content, ttFont=None) + self.assertEqual("wght", axis.axisTag) + self.assertEqual(0x123ABC, axis.flags) + self.assertEqual(100, axis.minValue) + self.assertEqual(400, axis.defaultValue) + self.assertEqual(900, axis.maxValue) + self.assertEqual(256, axis.axisNameID) + + +class NamedInstanceTest(unittest.TestCase): + def test_compile_withPostScriptName(self): + inst = NamedInstance() + inst.subfamilyNameID = 345 + inst.postscriptNameID = 564 + inst.coordinates = {"wght": 0.7, "wdth": 0.5} + self.assertEqual(FVAR_INSTANCE_DATA_WITH_PSNAME, + inst.compile(["wght", "wdth"], True)) + + def test_compile_withoutPostScriptName(self): + inst = NamedInstance() + inst.subfamilyNameID = 345 + inst.postscriptNameID = 564 + inst.coordinates = {"wght": 0.7, "wdth": 0.5} + self.assertEqual(FVAR_INSTANCE_DATA_WITHOUT_PSNAME, + inst.compile(["wght", "wdth"], False)) + + def test_decompile_withPostScriptName(self): + inst = NamedInstance() + inst.decompile(FVAR_INSTANCE_DATA_WITH_PSNAME, ["wght", "wdth"]) + self.assertEqual(564, inst.postscriptNameID) + self.assertEqual(345, inst.subfamilyNameID) + self.assertEqual({"wght": 0.7, "wdth": 0.5}, inst.coordinates) + + def test_decompile_withoutPostScriptName(self): + inst = NamedInstance() + inst.decompile(FVAR_INSTANCE_DATA_WITHOUT_PSNAME, ["wght", "wdth"]) + self.assertEqual(0xFFFF, inst.postscriptNameID) + self.assertEqual(345, inst.subfamilyNameID) + self.assertEqual({"wght": 0.7, "wdth": 0.5}, inst.coordinates) + + def test_toXML_withPostScriptName(self): + font = MakeFont() + inst = NamedInstance() + inst.flags = 0xE9 + inst.subfamilyNameID = AddName(font, "Light Condensed").nameID + inst.postscriptNameID = AddName(font, "Test-LightCondensed").nameID + inst.coordinates = {"wght": 0.7, "wdth": 0.5} + writer = XMLWriter(BytesIO()) + inst.toXML(writer, font) + self.assertEqual([ + '', + '', + '', + '' % ( + inst.postscriptNameID, inst.subfamilyNameID), + '', + '', + '' + ], xml_lines(writer)) + + def test_toXML_withoutPostScriptName(self): + font = MakeFont() + inst = NamedInstance() + inst.flags = 0xABC + inst.subfamilyNameID = AddName(font, "Light Condensed").nameID + inst.coordinates = {"wght": 0.7, "wdth": 0.5} + writer = XMLWriter(BytesIO()) + inst.toXML(writer, font) + self.assertEqual([ + '', + '', + '' % + inst.subfamilyNameID, + '', + '', + '' + ], xml_lines(writer)) + + def test_fromXML_withPostScriptName(self): + inst = NamedInstance() + for name, attrs, content in parseXML( + '' + ' ' + ' ' + ''): + inst.fromXML(name, attrs, content, ttFont=MakeFont()) + self.assertEqual(257, inst.postscriptNameID) + self.assertEqual(345, inst.subfamilyNameID) + self.assertEqual({"wght": 0.7, "wdth": 0.5}, inst.coordinates) + + def test_fromXML_withoutPostScriptName(self): + inst = NamedInstance() + for name, attrs, content in parseXML( + '' + ' ' + ' ' + ''): + inst.fromXML(name, attrs, content, ttFont=MakeFont()) + self.assertEqual(0x123ABC, inst.flags) + self.assertEqual(345, inst.subfamilyNameID) + self.assertEqual({"wght": 0.7, "wdth": 0.5}, inst.coordinates) + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_g_c_i_d_test.py b/Tests/ttLib/tables/_g_c_i_d_test.py new file mode 100644 index 0000000..1ec92fb --- /dev/null +++ b/Tests/ttLib/tables/_g_c_i_d_test.py @@ -0,0 +1,70 @@ +# coding: utf-8 +from __future__ import print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc.testTools import FakeFont, getXML, parseXML +from fontTools.misc.textTools import deHexStr, hexStr +from fontTools.ttLib import newTable +import unittest + + +# On macOS X 10.12.3, the font /Library/Fonts/AppleGothic.ttf has a ‘gcid’ +# table with a similar structure as this test data, just more CIDs. +GCID_DATA = deHexStr( + "0000 0000 " # 0: Format=0, Flags=0 + "0000 0098 " # 4: Size=152 + "0000 " # 8: Registry=0 + "41 64 6F 62 65 " # 10: RegistryName="Adobe" + + ("00" * 59) + # 15: + "0003 " # 74: Order=3 + "4B 6F 72 65 61 31 " # 76: Order="Korea1" + + ("00" * 58) + # 82: + "0001 " # 140: SupplementVersion + "0004 " # 142: Count + "1234 " # 144: CIDs[0/.notdef]=4660 + "FFFF " # 146: CIDs[1/A]=None + "0007 " # 148: CIDs[2/B]=7 + "DEF0 " # 150: CIDs[3/C]=57072 +) # 152: +assert len(GCID_DATA) == 152, len(GCID_DATA) + + +GCID_XML = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +class GCIDTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.maxDiff = None + cls.font = FakeFont(['.notdef', 'A', 'B', 'C', 'D']) + + def testDecompileToXML(self): + table = newTable('gcid') + table.decompile(GCID_DATA, self.font) + self.assertEqual(getXML(table.toXML, self.font), GCID_XML) + + def testCompileFromXML(self): + table = newTable('gcid') + for name, attrs, content in parseXML(GCID_XML): + table.fromXML(name, attrs, content, font=self.font) + self.assertEqual(hexStr(table.compile(self.font)), + hexStr(GCID_DATA)) + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_g_l_y_f_test.py b/Tests/ttLib/tables/_g_l_y_f_test.py new file mode 100644 index 0000000..205af84 --- /dev/null +++ b/Tests/ttLib/tables/_g_l_y_f_test.py @@ -0,0 +1,160 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.fixedTools import otRound +from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates +import sys +import array +import pytest + + +class GlyphCoordinatesTest(object): + + def test_translate(self): + g = GlyphCoordinates([(1,2)]) + g.translate((.5,0)) + assert g == GlyphCoordinates([(1.5,2.0)]) + + def test_scale(self): + g = GlyphCoordinates([(1,2)]) + g.scale((.5,0)) + assert g == GlyphCoordinates([(0.5,0.0)]) + + def test_transform(self): + g = GlyphCoordinates([(1,2)]) + g.transform(((.5,0),(.2,.5))) + assert g[0] == GlyphCoordinates([(0.9,1.0)])[0] + + def test__eq__(self): + g = GlyphCoordinates([(1,2)]) + g2 = GlyphCoordinates([(1.0,2)]) + g3 = GlyphCoordinates([(1.5,2)]) + assert g == g2 + assert not g == g3 + assert not g2 == g3 + assert not g == object() + + def test__ne__(self): + g = GlyphCoordinates([(1,2)]) + g2 = GlyphCoordinates([(1.0,2)]) + g3 = GlyphCoordinates([(1.5,2)]) + assert not (g != g2) + assert g != g3 + assert g2 != g3 + assert g != object() + + def test__pos__(self): + g = GlyphCoordinates([(1,2)]) + g2 = +g + assert g == g2 + + def test__neg__(self): + g = GlyphCoordinates([(1,2)]) + g2 = -g + assert g2 == GlyphCoordinates([(-1, -2)]) + + @pytest.mark.skipif(sys.version_info[0] < 3, + reason="__round___ requires Python 3") + def test__round__(self): + g = GlyphCoordinates([(-1.5,2)]) + g2 = round(g) + assert g2 == GlyphCoordinates([(-1,2)]) + + def test__add__(self): + g1 = GlyphCoordinates([(1,2)]) + g2 = GlyphCoordinates([(3,4)]) + g3 = GlyphCoordinates([(4,6)]) + assert g1 + g2 == g3 + assert g1 + (1, 1) == GlyphCoordinates([(2,3)]) + with pytest.raises(TypeError) as excinfo: + assert g1 + object() + assert 'unsupported operand' in str(excinfo.value) + + def test__sub__(self): + g1 = GlyphCoordinates([(1,2)]) + g2 = GlyphCoordinates([(3,4)]) + g3 = GlyphCoordinates([(-2,-2)]) + assert g1 - g2 == g3 + assert g1 - (1, 1) == GlyphCoordinates([(0,1)]) + with pytest.raises(TypeError) as excinfo: + assert g1 - object() + assert 'unsupported operand' in str(excinfo.value) + + def test__rsub__(self): + g = GlyphCoordinates([(1,2)]) + # other + (-self) + assert (1, 1) - g == GlyphCoordinates([(0,-1)]) + + def test__mul__(self): + g = GlyphCoordinates([(1,2)]) + assert g * 3 == GlyphCoordinates([(3,6)]) + assert g * (3,2) == GlyphCoordinates([(3,4)]) + assert g * (1,1) == g + with pytest.raises(TypeError) as excinfo: + assert g * object() + assert 'unsupported operand' in str(excinfo.value) + + def test__truediv__(self): + g = GlyphCoordinates([(1,2)]) + assert g / 2 == GlyphCoordinates([(.5,1)]) + assert g / (1, 2) == GlyphCoordinates([(1,1)]) + assert g / (1, 1) == g + with pytest.raises(TypeError) as excinfo: + assert g / object() + assert 'unsupported operand' in str(excinfo.value) + + def test__iadd__(self): + g = GlyphCoordinates([(1,2)]) + g += (.5,0) + assert g == GlyphCoordinates([(1.5, 2.0)]) + g2 = GlyphCoordinates([(3,4)]) + g += g2 + assert g == GlyphCoordinates([(4.5, 6.0)]) + + def test__isub__(self): + g = GlyphCoordinates([(1,2)]) + g -= (.5, 0) + assert g == GlyphCoordinates([(0.5, 2.0)]) + g2 = GlyphCoordinates([(3,4)]) + g -= g2 + assert g == GlyphCoordinates([(-2.5, -2.0)]) + + def __test__imul__(self): + g = GlyphCoordinates([(1,2)]) + g *= (2,.5) + g *= 2 + assert g == GlyphCoordinates([(4.0, 2.0)]) + g = GlyphCoordinates([(1,2)]) + g *= 2 + assert g == GlyphCoordinates([(2, 4)]) + + def test__itruediv__(self): + g = GlyphCoordinates([(1,3)]) + g /= (.5,1.5) + g /= 2 + assert g == GlyphCoordinates([(1.0, 1.0)]) + + def test__bool__(self): + g = GlyphCoordinates([]) + assert bool(g) == False + g = GlyphCoordinates([(0,0), (0.,0)]) + assert bool(g) == True + g = GlyphCoordinates([(0,0), (1,0)]) + assert bool(g) == True + g = GlyphCoordinates([(0,.5), (0,0)]) + assert bool(g) == True + + def test_double_precision_float(self): + # https://github.com/fonttools/fonttools/issues/963 + afloat = 242.50000000000003 + g = GlyphCoordinates([(afloat, 0)]) + g.toInt() + # this would return 242 if the internal array.array typecode is 'f', + # since the Python float is truncated to a C float. + # when using typecode 'd' it should return the correct value 243 + assert g[0][0] == otRound(afloat) + + def test__checkFloat_overflow(self): + g = GlyphCoordinates([(1, 1)], typecode="h") + g.append((0x8000, 0)) + assert g.array.typecode == "d" + assert g.array == array.array("d", [1.0, 1.0, 32768.0, 0.0]) diff --git a/Tests/ttLib/tables/_g_v_a_r_test.py b/Tests/ttLib/tables/_g_v_a_r_test.py new file mode 100644 index 0000000..64adbb6 --- /dev/null +++ b/Tests/ttLib/tables/_g_v_a_r_test.py @@ -0,0 +1,211 @@ +from __future__ import print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc.testTools import FakeFont, getXML, parseXML +from fontTools.misc.textTools import deHexStr, hexStr +from fontTools.ttLib import TTLibError, getTableClass, getTableModule, newTable +import unittest +from fontTools.ttLib.tables.TupleVariation import TupleVariation + + +gvarClass = getTableClass("gvar") + + +GVAR_DATA = deHexStr( + "0001 0000 " # 0: majorVersion=1 minorVersion=0 + "0002 0000 " # 4: axisCount=2 sharedTupleCount=0 + "0000001C " # 8: offsetToSharedTuples=28 + "0003 0000 " # 12: glyphCount=3 flags=0 + "0000001C " # 16: offsetToGlyphVariationData=28 + "0000 0000 000C 002F " # 20: offsets=[0,0,12,47], times 2: [0,0,24,94], + # # +offsetToGlyphVariationData: [28,28,52,122] + # + # 28: Glyph variation data for glyph #0, ".notdef" + # ------------------------------------------------ + # (no variation data for this glyph) + # + # 28: Glyph variation data for glyph #1, "space" + # ---------------------------------------------- + "8001 000C " # 28: tupleVariationCount=1|TUPLES_SHARE_POINT_NUMBERS, offsetToData=12(+28=40) + "000A " # 32: tvHeader[0].variationDataSize=10 + "8000 " # 34: tvHeader[0].tupleIndex=EMBEDDED_PEAK + "0000 2CCD " # 36: tvHeader[0].peakTuple={wght:0.0, wdth:0.7} + "00 " # 40: all points + "03 01 02 03 04 " # 41: deltaX=[1, 2, 3, 4] + "03 0b 16 21 2C " # 46: deltaY=[11, 22, 33, 44] + "00 " # 51: padding + # + # 52: Glyph variation data for glyph #2, "I" + # ------------------------------------------ + "8002 001c " # 52: tupleVariationCount=2|TUPLES_SHARE_POINT_NUMBERS, offsetToData=28(+52=80) + "0012 " # 56: tvHeader[0].variationDataSize=18 + "C000 " # 58: tvHeader[0].tupleIndex=EMBEDDED_PEAK|INTERMEDIATE_REGION + "2000 0000 " # 60: tvHeader[0].peakTuple={wght:0.5, wdth:0.0} + "0000 0000 " # 64: tvHeader[0].intermediateStart={wght:0.0, wdth:0.0} + "4000 0000 " # 68: tvHeader[0].intermediateEnd={wght:1.0, wdth:0.0} + "0016 " # 72: tvHeader[1].variationDataSize=22 + "A000 " # 74: tvHeader[1].tupleIndex=EMBEDDED_PEAK|PRIVATE_POINTS + "C000 3333 " # 76: tvHeader[1].peakTuple={wght:-1.0, wdth:0.8} + "00 " # 80: all points + "07 03 01 04 01 " # 81: deltaX.len=7, deltaX=[3, 1, 4, 1, + "05 09 02 06 " # 86: 5, 9, 2, 6] + "07 03 01 04 01 " # 90: deltaY.len=7, deltaY=[3, 1, 4, 1, + "05 09 02 06 " # 95: 5, 9, 2, 6] + "06 " # 99: 6 points + "05 00 01 03 01 " # 100: runLen=5(+1=6); delta-encoded run=[0, 1, 4, 5, + "01 01 " # 105: 6, 7] + "05 f8 07 fc 03 fe 01 " # 107: deltaX.len=5, deltaX=[-8,7,-4,3,-2,1] + "05 a8 4d 2c 21 ea 0b " # 114: deltaY.len=5, deltaY=[-88,77,44,33,-22,11] + "00" # 121: padding +) # 122: +assert len(GVAR_DATA) == 122 + + +GVAR_VARIATIONS = { + ".notdef": [ + ], + "space": [ + TupleVariation( + {"wdth": (0.0, 0.7, 0.7)}, + [(1, 11), (2, 22), (3, 33), (4, 44)]), + ], + "I": [ + TupleVariation( + {"wght": (0.0, 0.5, 1.0)}, + [(3,3), (1,1), (4,4), (1,1), (5,5), (9,9), (2,2), (6,6)]), + TupleVariation( + {"wght": (-1.0, -1.0, 0.0), "wdth": (0.0, 0.8, 0.8)}, + [(-8,-88), (7,77), None, None, (-4,44), (3,33), (-2,-22), (1,11)]), + ], +} + + +GVAR_XML = [ + '', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +GVAR_DATA_EMPTY_VARIATIONS = deHexStr( + "0001 0000 " # 0: majorVersion=1 minorVersion=0 + "0002 0000 " # 4: axisCount=2 sharedTupleCount=0 + "0000001c " # 8: offsetToSharedTuples=28 + "0003 0000 " # 12: glyphCount=3 flags=0 + "0000001c " # 16: offsetToGlyphVariationData=28 + "0000 0000 0000 0000" # 20: offsets=[0, 0, 0, 0] +) # 28: + + +def hexencode(s): + h = hexStr(s).upper() + return ' '.join([h[i:i+2] for i in range(0, len(h), 2)]) + + +class GVARTableTest(unittest.TestCase): + def makeFont(self, variations): + glyphs=[".notdef", "space", "I"] + Axis = getTableModule("fvar").Axis + Glyph = getTableModule("glyf").Glyph + glyf, fvar, gvar = newTable("glyf"), newTable("fvar"), newTable("gvar") + font = FakeFont(glyphs) + font.tables = {"glyf": glyf, "gvar": gvar, "fvar": fvar} + glyf.glyphs = {glyph: Glyph() for glyph in glyphs} + glyf.glyphs["I"].coordinates = [(10, 10), (10, 20), (20, 20), (20, 10)] + fvar.axes = [Axis(), Axis()] + fvar.axes[0].axisTag, fvar.axes[1].axisTag = "wght", "wdth" + gvar.variations = variations + return font, gvar + + def test_compile(self): + font, gvar = self.makeFont(GVAR_VARIATIONS) + self.assertEqual(hexStr(gvar.compile(font)), hexStr(GVAR_DATA)) + + def test_compile_noVariations(self): + font, gvar = self.makeFont({}) + self.assertEqual(hexStr(gvar.compile(font)), + hexStr(GVAR_DATA_EMPTY_VARIATIONS)) + + def test_compile_emptyVariations(self): + font, gvar = self.makeFont({".notdef": [], "space": [], "I": []}) + self.assertEqual(hexStr(gvar.compile(font)), + hexStr(GVAR_DATA_EMPTY_VARIATIONS)) + + def test_decompile(self): + font, gvar = self.makeFont({}) + gvar.decompile(GVAR_DATA, font) + self.assertEqual(gvar.variations, GVAR_VARIATIONS) + + def test_decompile_noVariations(self): + font, gvar = self.makeFont({}) + gvar.decompile(GVAR_DATA_EMPTY_VARIATIONS, font) + self.assertEqual(gvar.variations, + {".notdef": [], "space": [], "I": []}) + + def test_fromXML(self): + font, gvar = self.makeFont({}) + for name, attrs, content in parseXML(GVAR_XML): + gvar.fromXML(name, attrs, content, ttFont=font) + self.assertEqual(gvar.variations, + {g:v for g,v in GVAR_VARIATIONS.items() if v}) + + def test_toXML(self): + font, gvar = self.makeFont(GVAR_VARIATIONS) + self.assertEqual(getXML(gvar.toXML, font), GVAR_XML) + + def test_compileOffsets_shortFormat(self): + self.assertEqual((deHexStr("00 00 00 02 FF C0"), 0), + gvarClass.compileOffsets_([0, 4, 0x1ff80])) + + def test_compileOffsets_longFormat(self): + self.assertEqual((deHexStr("00 00 00 00 00 00 00 04 CA FE BE EF"), 1), + gvarClass.compileOffsets_([0, 4, 0xCAFEBEEF])) + + def test_decompileOffsets_shortFormat(self): + decompileOffsets = gvarClass.decompileOffsets_ + data = deHexStr("00 11 22 33 44 55 66 77 88 99 aa bb") + self.assertEqual( + [2*0x0011, 2*0x2233, 2*0x4455, 2*0x6677, 2*0x8899, 2*0xaabb], + list(decompileOffsets(data, tableFormat=0, glyphCount=5))) + + def test_decompileOffsets_longFormat(self): + decompileOffsets = gvarClass.decompileOffsets_ + data = deHexStr("00 11 22 33 44 55 66 77 88 99 aa bb") + self.assertEqual( + [0x00112233, 0x44556677, 0x8899aabb], + list(decompileOffsets(data, tableFormat=1, glyphCount=2))) + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_h_h_e_a_test.py b/Tests/ttLib/tables/_h_h_e_a_test.py new file mode 100644 index 0000000..64849e9 --- /dev/null +++ b/Tests/ttLib/tables/_h_h_e_a_test.py @@ -0,0 +1,198 @@ +from __future__ import absolute_import, unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc.loggingTools import CapturingLogHandler +from fontTools.misc.testTools import parseXML, getXML +from fontTools.misc.textTools import deHexStr +from fontTools.ttLib import TTFont, newTable +from fontTools.misc.fixedTools import log +import os +import unittest + + +CURR_DIR = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) +DATA_DIR = os.path.join(CURR_DIR, 'data') + +HHEA_DATA = deHexStr( + '0001 0000 ' # 1.0 version + '02EE ' # 750 ascent + 'FF06 ' # -250 descent + '00C8 ' # 200 lineGap + '03E8 ' # 1000 advanceWidthMax + 'FFE7 ' # -25 minLeftSideBearing + 'FFEC ' # -20 minRightSideBearing + '03D1 ' # 977 xMaxExtent + '0000 ' # 0 caretSlopeRise + '0001 ' # 1 caretSlopeRun + '0010 ' # 16 caretOffset + '0000 ' # 0 reserved0 + '0000 ' # 0 reserved1 + '0000 ' # 0 reserved2 + '0000 ' # 0 reserved3 + '0000 ' # 0 metricDataFormat + '002A ' # 42 numberOfHMetrics +) + +HHEA_AS_DICT = { + 'tableTag': 'hhea', + 'tableVersion': 0x00010000, + 'ascent': 750, + 'descent': -250, + 'lineGap': 200, + 'advanceWidthMax': 1000, + 'minLeftSideBearing': -25, + 'minRightSideBearing': -20, + 'xMaxExtent': 977, + 'caretSlopeRise': 0, + 'caretSlopeRun': 1, + 'caretOffset': 16, + 'reserved0': 0, + 'reserved1': 0, + 'reserved2': 0, + 'reserved3': 0, + 'metricDataFormat': 0, + 'numberOfHMetrics': 42, +} + +HHEA_XML = [ + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', +] + +HHEA_XML_VERSION_AS_FLOAT = [ + '', +] + HHEA_XML[1:] + + +class HheaCompileOrToXMLTest(unittest.TestCase): + + def setUp(self): + hhea = newTable('hhea') + hhea.tableVersion = 0x00010000 + hhea.ascent = 750 + hhea.descent = -250 + hhea.lineGap = 200 + hhea.advanceWidthMax = 1000 + hhea.minLeftSideBearing = -25 + hhea.minRightSideBearing = -20 + hhea.xMaxExtent = 977 + hhea.caretSlopeRise = 0 + hhea.caretSlopeRun = 1 + hhea.caretOffset = 16 + hhea.metricDataFormat = 0 + hhea.numberOfHMetrics = 42 + hhea.reserved0 = hhea.reserved1 = hhea.reserved2 = hhea.reserved3 = 0 + self.font = TTFont(sfntVersion='OTTO') + self.font['hhea'] = hhea + + def test_compile(self): + hhea = self.font['hhea'] + hhea.tableVersion = 0x00010000 + self.assertEqual(HHEA_DATA, hhea.compile(self.font)) + + def test_compile_version_10_as_float(self): + hhea = self.font['hhea'] + hhea.tableVersion = 1.0 + with CapturingLogHandler(log, "WARNING") as captor: + self.assertEqual(HHEA_DATA, hhea.compile(self.font)) + self.assertTrue( + len([r for r in captor.records + if "Table version value is a float" in r.msg]) == 1) + + def test_toXML(self): + hhea = self.font['hhea'] + self.font['hhea'].tableVersion = 0x00010000 + self.assertEqual(getXML(hhea.toXML), HHEA_XML) + + def test_toXML_version_as_float(self): + hhea = self.font['hhea'] + hhea.tableVersion = 1.0 + with CapturingLogHandler(log, "WARNING") as captor: + self.assertEqual(getXML(hhea.toXML), HHEA_XML) + self.assertTrue( + len([r for r in captor.records + if "Table version value is a float" in r.msg]) == 1) + + +class HheaDecompileOrFromXMLTest(unittest.TestCase): + + def setUp(self): + hhea = newTable('hhea') + self.font = TTFont(sfntVersion='OTTO') + self.font['hhea'] = hhea + + def test_decompile(self): + hhea = self.font['hhea'] + hhea.decompile(HHEA_DATA, self.font) + for key in hhea.__dict__: + self.assertEqual(getattr(hhea, key), HHEA_AS_DICT[key]) + + def test_fromXML(self): + hhea = self.font['hhea'] + for name, attrs, content in parseXML(HHEA_XML): + hhea.fromXML(name, attrs, content, self.font) + for key in hhea.__dict__: + self.assertEqual(getattr(hhea, key), HHEA_AS_DICT[key]) + + def test_fromXML_version_as_float(self): + hhea = self.font['hhea'] + with CapturingLogHandler(log, "WARNING") as captor: + for name, attrs, content in parseXML(HHEA_XML_VERSION_AS_FLOAT): + hhea.fromXML(name, attrs, content, self.font) + self.assertTrue( + len([r for r in captor.records + if "Table version value is a float" in r.msg]) == 1) + for key in hhea.__dict__: + self.assertEqual(getattr(hhea, key), HHEA_AS_DICT[key]) + + +class HheaRecalcTest(unittest.TestCase): + + def test_recalc_TTF(self): + font = TTFont() + font.importXML(os.path.join(DATA_DIR, '_h_h_e_a_recalc_TTF.ttx')) + hhea = font['hhea'] + hhea.recalc(font) + self.assertEqual(hhea.advanceWidthMax, 600) + self.assertEqual(hhea.minLeftSideBearing, -56) + self.assertEqual(hhea.minRightSideBearing, 100) + self.assertEqual(hhea.xMaxExtent, 400) + + def test_recalc_OTF(self): + font = TTFont() + font.importXML(os.path.join(DATA_DIR, '_h_h_e_a_recalc_OTF.ttx')) + hhea = font['hhea'] + hhea.recalc(font) + self.assertEqual(hhea.advanceWidthMax, 600) + self.assertEqual(hhea.minLeftSideBearing, -56) + self.assertEqual(hhea.minRightSideBearing, 100) + self.assertEqual(hhea.xMaxExtent, 400) + + def test_recalc_empty(self): + font = TTFont() + font.importXML(os.path.join(DATA_DIR, '_h_h_e_a_recalc_empty.ttx')) + hhea = font['hhea'] + hhea.recalc(font) + self.assertEqual(hhea.advanceWidthMax, 600) + self.assertEqual(hhea.minLeftSideBearing, 0) + self.assertEqual(hhea.minRightSideBearing, 0) + self.assertEqual(hhea.xMaxExtent, 0) + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_h_m_t_x_test.py b/Tests/ttLib/tables/_h_m_t_x_test.py new file mode 100644 index 0000000..007f3cd --- /dev/null +++ b/Tests/ttLib/tables/_h_m_t_x_test.py @@ -0,0 +1,219 @@ +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc.testTools import parseXML, getXML +from fontTools.misc.textTools import deHexStr +from fontTools.ttLib import TTFont, newTable, TTLibError +from fontTools.misc.loggingTools import CapturingLogHandler +from fontTools.ttLib.tables._h_m_t_x import table__h_m_t_x, log +import struct +import unittest + + +class HmtxTableTest(unittest.TestCase): + + def __init__(self, methodName): + unittest.TestCase.__init__(self, methodName) + # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, + # and fires deprecation warnings if a program uses the old name. + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + + @classmethod + def setUpClass(cls): + cls.tableClass = table__h_m_t_x + cls.tag = "hmtx" + + def makeFont(self, numGlyphs, numberOfMetrics): + font = TTFont() + maxp = font['maxp'] = newTable('maxp') + maxp.numGlyphs = numGlyphs + # from A to ... + font.glyphOrder = [chr(i) for i in range(65, 65+numGlyphs)] + headerTag = self.tableClass.headerTag + font[headerTag] = newTable(headerTag) + numberOfMetricsName = self.tableClass.numberOfMetricsName + setattr(font[headerTag], numberOfMetricsName, numberOfMetrics) + return font + + def test_decompile(self): + font = self.makeFont(numGlyphs=3, numberOfMetrics=3) + data = deHexStr("02A2 FFF5 0278 004F 02C6 0036") + + mtxTable = newTable(self.tag) + mtxTable.decompile(data, font) + + self.assertEqual(mtxTable['A'], (674, -11)) + self.assertEqual(mtxTable['B'], (632, 79)) + self.assertEqual(mtxTable['C'], (710, 54)) + + def test_decompile_additional_SB(self): + font = self.makeFont(numGlyphs=4, numberOfMetrics=2) + metrics = deHexStr("02A2 FFF5 0278 004F") + extraSideBearings = deHexStr("0036 FFFC") + data = metrics + extraSideBearings + + mtxTable = newTable(self.tag) + mtxTable.decompile(data, font) + + self.assertEqual(mtxTable['A'], (674, -11)) + self.assertEqual(mtxTable['B'], (632, 79)) + # all following have same width as the previous + self.assertEqual(mtxTable['C'], (632, 54)) + self.assertEqual(mtxTable['D'], (632, -4)) + + def test_decompile_not_enough_data(self): + font = self.makeFont(numGlyphs=1, numberOfMetrics=1) + mtxTable = newTable(self.tag) + msg = "not enough '%s' table data" % self.tag + + with self.assertRaisesRegex(TTLibError, msg): + mtxTable.decompile(b"\0\0\0", font) + + def test_decompile_too_much_data(self): + font = self.makeFont(numGlyphs=1, numberOfMetrics=1) + mtxTable = newTable(self.tag) + msg = "too much '%s' table data" % self.tag + + with CapturingLogHandler(log, "WARNING") as captor: + mtxTable.decompile(b"\0\0\0\0\0", font) + + self.assertTrue( + len([r for r in captor.records if msg == r.msg]) == 1) + + def test_decompile_num_metrics_greater_than_glyphs(self): + font = self.makeFont(numGlyphs=1, numberOfMetrics=2) + mtxTable = newTable(self.tag) + msg = "The %s.%s exceeds the maxp.numGlyphs" % ( + self.tableClass.headerTag, self.tableClass.numberOfMetricsName) + + with CapturingLogHandler(log, "WARNING") as captor: + mtxTable.decompile(b"\0\0\0\0", font) + + self.assertTrue( + len([r for r in captor.records if msg == r.msg]) == 1) + + def test_decompile_possibly_negative_advance(self): + font = self.makeFont(numGlyphs=1, numberOfMetrics=1) + # we warn if advance is > 0x7FFF as it might be interpreted as signed + # by some authoring tools + data = deHexStr("8000 0000") + mtxTable = newTable(self.tag) + + with CapturingLogHandler(log, "WARNING") as captor: + mtxTable.decompile(data, font) + + self.assertTrue( + len([r for r in captor.records + if "has a huge advance" in r.msg]) == 1) + + def test_compile(self): + # we set the wrong 'numberOfMetrics' to check it gets adjusted + font = self.makeFont(numGlyphs=3, numberOfMetrics=4) + mtxTable = font[self.tag] = newTable(self.tag) + mtxTable.metrics = { + 'A': (674, -11), + 'B': (632, 79), + 'C': (710, 54), + } + + data = mtxTable.compile(font) + + self.assertEqual(data, deHexStr("02A2 FFF5 0278 004F 02C6 0036")) + + headerTable = font[self.tableClass.headerTag] + self.assertEqual( + getattr(headerTable, self.tableClass.numberOfMetricsName), 3) + + def test_compile_additional_SB(self): + font = self.makeFont(numGlyphs=4, numberOfMetrics=1) + mtxTable = font[self.tag] = newTable(self.tag) + mtxTable.metrics = { + 'A': (632, -11), + 'B': (632, 79), + 'C': (632, 54), + 'D': (632, -4), + } + + data = mtxTable.compile(font) + + self.assertEqual(data, deHexStr("0278 FFF5 004F 0036 FFFC")) + + def test_compile_negative_advance(self): + font = self.makeFont(numGlyphs=1, numberOfMetrics=1) + mtxTable = font[self.tag] = newTable(self.tag) + mtxTable.metrics = {'A': [-1, 0]} + + with CapturingLogHandler(log, "ERROR") as captor: + with self.assertRaisesRegex(TTLibError, "negative advance"): + mtxTable.compile(font) + + self.assertTrue( + len([r for r in captor.records + if "Glyph 'A' has negative advance" in r.msg]) == 1) + + def test_compile_struct_out_of_range(self): + font = self.makeFont(numGlyphs=1, numberOfMetrics=1) + mtxTable = font[self.tag] = newTable(self.tag) + mtxTable.metrics = {'A': (0xFFFF+1, -0x8001)} + + with self.assertRaises(struct.error): + mtxTable.compile(font) + + def test_compile_round_float_values(self): + font = self.makeFont(numGlyphs=3, numberOfMetrics=2) + mtxTable = font[self.tag] = newTable(self.tag) + mtxTable.metrics = { + 'A': (0.5, 0.5), # round -> (1, 1) + 'B': (0.1, 0.9), # round -> (0, 1) + 'C': (0.1, 0.1), # round -> (0, 0) + } + + data = mtxTable.compile(font) + + self.assertEqual(data, deHexStr("0001 0001 0000 0001 0000")) + + def test_toXML(self): + font = self.makeFont(numGlyphs=2, numberOfMetrics=2) + mtxTable = font[self.tag] = newTable(self.tag) + mtxTable.metrics = {'B': (632, 79), 'A': (674, -11)} + + self.assertEqual( + getXML(mtxTable.toXML), + ('\n' + '' % ( + (self.tableClass.advanceName, + self.tableClass.sideBearingName) * 2)).split('\n')) + + def test_fromXML(self): + mtxTable = newTable(self.tag) + + for name, attrs, content in parseXML( + '' + '' % ( + (self.tableClass.advanceName, + self.tableClass.sideBearingName) * 2)): + mtxTable.fromXML(name, attrs, content, ttFont=None) + + self.assertEqual( + mtxTable.metrics, {'A': (674, -11), 'B': (632, 79)}) + + def test_delitem(self): + mtxTable = newTable(self.tag) + mtxTable.metrics = {'A': (0, 0)} + + del mtxTable['A'] + + self.assertTrue('A' not in mtxTable.metrics) + + def test_setitem(self): + mtxTable = newTable(self.tag) + mtxTable.metrics = {'A': (674, -11), 'B': (632, 79)} + mtxTable['B'] = [0, 0] # list is converted to tuple + + self.assertEqual(mtxTable.metrics, {'A': (674, -11), 'B': (0, 0)}) + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_k_e_r_n_test.py b/Tests/ttLib/tables/_k_e_r_n_test.py new file mode 100644 index 0000000..8ab1b1c --- /dev/null +++ b/Tests/ttLib/tables/_k_e_r_n_test.py @@ -0,0 +1,370 @@ +from __future__ import print_function, absolute_import +from fontTools.misc.py23 import * +from fontTools.ttLib import newTable +from fontTools.ttLib.tables._k_e_r_n import ( + KernTable_format_0, KernTable_format_unkown) +from fontTools.misc.textTools import deHexStr +from fontTools.misc.testTools import FakeFont, getXML, parseXML +import pytest + + +KERN_VER_0_FMT_0_DATA = deHexStr( + '0000 ' # 0: version=0 + '0001 ' # 2: nTables=1 + '0000 ' # 4: version=0 (bogus field, unused) + '0020 ' # 6: length=32 + '00 ' # 8: format=0 + '01 ' # 9: coverage=1 + '0003 ' # 10: nPairs=3 + '000C ' # 12: searchRange=12 + '0001 ' # 14: entrySelector=1 + '0006 ' # 16: rangeShift=6 + '0004 000C FFD8 ' # 18: l=4, r=12, v=-40 + '0004 001C 0028 ' # 24: l=4, r=28, v=40 + '0005 0028 FFCE ' # 30: l=5, r=40, v=-50 +) +assert len(KERN_VER_0_FMT_0_DATA) == 36 + +KERN_VER_0_FMT_0_XML = [ + '', + '', + ' ', + ' ', + ' ', + '', +] + +KERN_VER_1_FMT_0_DATA = deHexStr( + '0001 0000 ' # 0: version=1 + '0000 0001 ' # 4: nTables=1 + '0000 0022 ' # 8: length=34 + '00 ' # 12: coverage=0 + '00 ' # 13: format=0 + '0000 ' # 14: tupleIndex=0 + '0003 ' # 16: nPairs=3 + '000C ' # 18: searchRange=12 + '0001 ' # 20: entrySelector=1 + '0006 ' # 22: rangeShift=6 + '0004 000C FFD8 ' # 24: l=4, r=12, v=-40 + '0004 001C 0028 ' # 30: l=4, r=28, v=40 + '0005 0028 FFCE ' # 36: l=5, r=40, v=-50 +) +assert len(KERN_VER_1_FMT_0_DATA) == 42 + +KERN_VER_1_FMT_0_XML = [ + '', + '', + ' ', + ' ', + ' ', + '', +] + +KERN_VER_0_FMT_UNKNOWN_DATA = deHexStr( + '0000 ' # 0: version=0 + '0002 ' # 2: nTables=2 + '0000 ' # 4: version=0 + '000A ' # 6: length=10 + '04 ' # 8: format=4 (format 4 doesn't exist) + '01 ' # 9: coverage=1 + '1234 5678 ' # 10: garbage... + '0000 ' # 14: version=0 + '000A ' # 16: length=10 + '05 ' # 18: format=5 (format 5 doesn't exist) + '01 ' # 19: coverage=1 + '9ABC DEF0 ' # 20: garbage... +) +assert len(KERN_VER_0_FMT_UNKNOWN_DATA) == 24 + +KERN_VER_0_FMT_UNKNOWN_XML = [ + '', + '', + " ", + ' 0000000A 04011234', + ' 5678 ', + '', + '', + "", + ' 0000000A 05019ABC', + ' DEF0 ', + '', +] + +KERN_VER_1_FMT_UNKNOWN_DATA = deHexStr( + '0001 0000 ' # 0: version=1 + '0000 0002 ' # 4: nTables=2 + '0000 000C ' # 8: length=12 + '00 ' # 12: coverage=0 + '04 ' # 13: format=4 (format 4 doesn't exist) + '0000 ' # 14: tupleIndex=0 + '1234 5678' # 16: garbage... + '0000 000C ' # 20: length=12 + '00 ' # 24: coverage=0 + '05 ' # 25: format=5 (format 5 doesn't exist) + '0000 ' # 26: tupleIndex=0 + '9ABC DEF0 ' # 28: garbage... +) +assert len(KERN_VER_1_FMT_UNKNOWN_DATA) == 32 + +KERN_VER_1_FMT_UNKNOWN_XML = [ + '', + '', + " ", + ' 0000000C 00040000', + ' 12345678 ', + '', + '', + " ", + ' 0000000C 00050000', + ' 9ABCDEF0 ', + '', +] + + +@pytest.fixture +def font(): + return FakeFont(list("ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz")) + + +class KernTableTest(object): + + @pytest.mark.parametrize( + "data, version", + [ + (KERN_VER_0_FMT_0_DATA, 0), + (KERN_VER_1_FMT_0_DATA, 1.0), + ], + ids=["version_0", "version_1"] + ) + def test_decompile_single_format_0(self, data, font, version): + kern = newTable("kern") + kern.decompile(data, font) + + assert kern.version == version + assert len(kern.kernTables) == 1 + + st = kern.kernTables[0] + assert st.apple is (version == 1.0) + assert st.format == 0 + # horizontal kerning in OT kern is coverage 0x01, while in + # AAT kern it's the default (0) + assert st.coverage == (0 if st.apple else 1) + assert st.tupleIndex == (0 if st.apple else None) + assert len(st.kernTable) == 3 + assert st.kernTable == { + ('E', 'M'): -40, + ('E', 'c'): 40, + ('F', 'o'): -50 + } + + @pytest.mark.parametrize( + "version, expected", + [ + (0, KERN_VER_0_FMT_0_DATA), + (1.0, KERN_VER_1_FMT_0_DATA), + ], + ids=["version_0", "version_1"] + ) + def test_compile_single_format_0(self, font, version, expected): + kern = newTable("kern") + kern.version = version + apple = version == 1.0 + st = KernTable_format_0(apple) + kern.kernTables = [st] + st.coverage = (0 if apple else 1) + st.tupleIndex = 0 if apple else None + st.kernTable = { + ('E', 'M'): -40, + ('E', 'c'): 40, + ('F', 'o'): -50 + } + data = kern.compile(font) + assert data == expected + + @pytest.mark.parametrize( + "xml, version", + [ + (KERN_VER_0_FMT_0_XML, 0), + (KERN_VER_1_FMT_0_XML, 1.0), + ], + ids=["version_0", "version_1"] + ) + def test_fromXML_single_format_0(self, xml, font, version): + kern = newTable("kern") + for name, attrs, content in parseXML(xml): + kern.fromXML(name, attrs, content, ttFont=font) + + assert kern.version == version + assert len(kern.kernTables) == 1 + + st = kern.kernTables[0] + assert st.apple is (version == 1.0) + assert st.format == 0 + assert st.coverage == (0 if st.apple else 1) + assert st.tupleIndex == (0 if st.apple else None) + assert len(st.kernTable) == 3 + assert st.kernTable == { + ('E', 'M'): -40, + ('E', 'c'): 40, + ('F', 'o'): -50 + } + + @pytest.mark.parametrize( + "version, expected", + [ + (0, KERN_VER_0_FMT_0_XML), + (1.0, KERN_VER_1_FMT_0_XML), + ], + ids=["version_0", "version_1"] + ) + def test_toXML_single_format_0(self, font, version, expected): + kern = newTable("kern") + kern.version = version + apple = version == 1.0 + st = KernTable_format_0(apple) + kern.kernTables = [st] + st.coverage = 0 if apple else 1 + st.tupleIndex = 0 if apple else None + st.kernTable = { + ('E', 'M'): -40, + ('E', 'c'): 40, + ('F', 'o'): -50 + } + xml = getXML(kern.toXML, font) + assert xml == expected + + @pytest.mark.parametrize( + "data, version, header_length, st_length", + [ + (KERN_VER_0_FMT_UNKNOWN_DATA, 0, 4, 10), + (KERN_VER_1_FMT_UNKNOWN_DATA, 1.0, 8, 12), + ], + ids=["version_0", "version_1"] + ) + def test_decompile_format_unknown( + self, data, font, version, header_length, st_length): + kern = newTable("kern") + kern.decompile(data, font) + + assert kern.version == version + assert len(kern.kernTables) == 2 + + st_data = data[header_length:] + st0 = kern.kernTables[0] + assert st0.format == 4 + assert st0.data == st_data[:st_length] + st_data = st_data[st_length:] + + st1 = kern.kernTables[1] + assert st1.format == 5 + assert st1.data == st_data[:st_length] + + @pytest.mark.parametrize( + "version, st_length, expected", + [ + (0, 10, KERN_VER_0_FMT_UNKNOWN_DATA), + (1.0, 12, KERN_VER_1_FMT_UNKNOWN_DATA), + ], + ids=["version_0", "version_1"] + ) + def test_compile_format_unknown(self, version, st_length, expected): + kern = newTable("kern") + kern.version = version + kern.kernTables = [] + + for unknown_fmt, kern_data in zip((4, 5), ("1234 5678", "9ABC DEF0")): + if version > 0: + coverage = 0 + header_fmt = deHexStr( + "%08X %02X %02X %04X" % ( + st_length, coverage, unknown_fmt, 0)) + else: + coverage = 1 + header_fmt = deHexStr( + "%04X %04X %02X %02X" % ( + 0, st_length, unknown_fmt, coverage)) + st = KernTable_format_unkown(unknown_fmt) + st.data = header_fmt + deHexStr(kern_data) + kern.kernTables.append(st) + + data = kern.compile(font) + assert data == expected + + @pytest.mark.parametrize( + "xml, version, st_length", + [ + (KERN_VER_0_FMT_UNKNOWN_XML, 0, 10), + (KERN_VER_1_FMT_UNKNOWN_XML, 1.0, 12), + ], + ids=["version_0", "version_1"] + ) + def test_fromXML_format_unknown(self, xml, font, version, st_length): + kern = newTable("kern") + for name, attrs, content in parseXML(xml): + kern.fromXML(name, attrs, content, ttFont=font) + + assert kern.version == version + assert len(kern.kernTables) == 2 + + st0 = kern.kernTables[0] + assert st0.format == 4 + assert len(st0.data) == st_length + + st1 = kern.kernTables[1] + assert st1.format == 5 + assert len(st1.data) == st_length + + @pytest.mark.parametrize( + "version", [0, 1.0], ids=["version_0", "version_1"]) + def test_toXML_format_unknown(self, font, version): + kern = newTable("kern") + kern.version = version + st = KernTable_format_unkown(4) + st.data = b"ABCD" + kern.kernTables = [st] + + xml = getXML(kern.toXML, font) + + assert xml == [ + '' % version, + '', + ' ', + ' 41424344 ', + '', + ] + + def test_getkern(self): + table = newTable("kern") + table.version = 0 + table.kernTables = [] + + assert table.getkern(0) is None + + st0 = KernTable_format_0() + table.kernTables.append(st0) + + assert table.getkern(0) is st0 + assert table.getkern(4) is None + + st1 = KernTable_format_unkown(4) + table.kernTables.append(st1) + + +class KernTable_format_0_Test(object): + + def test_decompileBadGlyphId(self, font): + subtable = KernTable_format_0() + subtable.decompile( + b'\x00' + b'\x00' + b'\x00' + b'\x1a' + b'\x00' + b'\x00' + + b'\x00' + b'\x02' + b'\x00' * 6 + + b'\x00' + b'\x01' + b'\x00' + b'\x03' + b'\x00' + b'\x01' + + b'\x00' + b'\x01' + b'\xFF' + b'\xFF' + b'\x00' + b'\x02', + font) + assert subtable[("B", "D")] == 1 + assert subtable[("B", "glyph65535")] == 2 + + +if __name__ == "__main__": + import sys + sys.exit(pytest.main(sys.argv)) diff --git a/Tests/ttLib/tables/_l_c_a_r_test.py b/Tests/ttLib/tables/_l_c_a_r_test.py new file mode 100644 index 0000000..4525def --- /dev/null +++ b/Tests/ttLib/tables/_l_c_a_r_test.py @@ -0,0 +1,109 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.testTools import FakeFont, getXML, parseXML +from fontTools.misc.textTools import deHexStr, hexStr +from fontTools.ttLib import newTable +import unittest + + +# Example: Format 0 Ligature Caret Table +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6lcar.html +LCAR_FORMAT_0_DATA = deHexStr( + '0001 0000 0000 ' # 0: Version=1.0, Format=0 + '0006 0004 0002 ' # 6: LookupFormat=6, UnitSize=4, NUnits=2 + '0008 0001 0000 ' # 12: SearchRange=8, EntrySelector=1, RangeShift=0 + '0001 001E ' # 18: Glyph=1 (f_r), OffsetOfLigCaretEntry=30 + '0003 0022 ' # 22: Glyph=3 (f_f_l), OffsetOfLigCaretEntry=34 + 'FFFF 0000 ' # 26: Glyph=, OffsetOfLigCaretEntry=0 + '0001 00DC ' # 30: DivisionPointCount=1, DivisionPoint=[220] + '0002 00EF 01D8 ' # 34: DivisionPointCount=2, DivisionPoint=[239, 475] +) # 40: +assert(len(LCAR_FORMAT_0_DATA) == 40) + + +LCAR_FORMAT_0_XML = [ + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +# Example: Format 1 Ligature Caret Table +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6lcar.html +LCAR_FORMAT_1_DATA = deHexStr( + '0001 0000 0001 ' # 0: Version=1.0, Format=1 + '0006 0004 0002 ' # 6: LookupFormat=6, UnitSize=4, NUnits=2 + '0008 0001 0000 ' # 12: SearchRange=8, EntrySelector=1, RangeShift=0 + '0001 001E ' # 18: Glyph=1 (f_r), OffsetOfLigCaretEntry=30 + '0003 0022 ' # 22: Glyph=3 (f_f_l), OffsetOfLigCaretEntry=34 + 'FFFF 0000 ' # 26: Glyph=, OffsetOfLigCaretEntry=0 + '0001 0032 ' # 30: DivisionPointCount=1, DivisionPoint=[50] + '0002 0037 004B ' # 34: DivisionPointCount=2, DivisionPoint=[55, 75] +) # 40: +assert(len(LCAR_FORMAT_1_DATA) == 40) + + +LCAR_FORMAT_1_XML = [ + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +class LCARTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.maxDiff = None + cls.font = FakeFont(['.notdef', 'f_r', 'X', 'f_f_l']) + + def test_decompile_toXML_format0(self): + table = newTable('lcar') + table.decompile(LCAR_FORMAT_0_DATA, self.font) + self.assertEqual(getXML(table.toXML), LCAR_FORMAT_0_XML) + + def test_compile_fromXML_format0(self): + table = newTable('lcar') + for name, attrs, content in parseXML(LCAR_FORMAT_0_XML): + table.fromXML(name, attrs, content, font=self.font) + self.assertEqual(hexStr(table.compile(self.font)), + hexStr(LCAR_FORMAT_0_DATA)) + + def test_decompile_toXML_format1(self): + table = newTable('lcar') + table.decompile(LCAR_FORMAT_1_DATA, self.font) + self.assertEqual(getXML(table.toXML), LCAR_FORMAT_1_XML) + + def test_compile_fromXML_format1(self): + table = newTable('lcar') + for name, attrs, content in parseXML(LCAR_FORMAT_1_XML): + table.fromXML(name, attrs, content, font=self.font) + self.assertEqual(hexStr(table.compile(self.font)), + hexStr(LCAR_FORMAT_1_DATA)) + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_l_t_a_g_test.py b/Tests/ttLib/tables/_l_t_a_g_test.py new file mode 100644 index 0000000..a4c5a0e --- /dev/null +++ b/Tests/ttLib/tables/_l_t_a_g_test.py @@ -0,0 +1,63 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.testTools import parseXML +from fontTools.misc.xmlWriter import XMLWriter +import os +import struct +import unittest +from fontTools.ttLib import newTable + + +class Test_l_t_a_g(unittest.TestCase): + + DATA_ = struct.pack(b">LLLHHHHHH", 1, 0, 3, 24 + 0, 2, 24 + 2, 7, 24 + 2, 2) + b"enzh-Hant" + TAGS_ = ["en", "zh-Hant", "zh"] + + def test_addTag(self): + table = newTable("ltag") + self.assertEqual(table.addTag("de-CH"), 0) + self.assertEqual(table.addTag("gsw-LI"), 1) + self.assertEqual(table.addTag("de-CH"), 0) + self.assertEqual(table.tags, ["de-CH", "gsw-LI"]) + + def test_decompile_compile(self): + table = newTable("ltag") + table.decompile(self.DATA_, ttFont=None) + self.assertEqual(1, table.version) + self.assertEqual(0, table.flags) + self.assertEqual(self.TAGS_, table.tags) + compiled = table.compile(ttFont=None) + self.assertEqual(self.DATA_, compiled) + self.assertIsInstance(compiled, bytes) + + def test_fromXML(self): + table = newTable("ltag") + for name, attrs, content in parseXML( + '' + '' + '' + ''): + table.fromXML(name, attrs, content, ttFont=None) + self.assertEqual(1, table.version) + self.assertEqual(777, table.flags) + self.assertEqual(["sr-Latn", "fa"], table.tags) + + def test_toXML(self): + writer = XMLWriter(BytesIO()) + table = newTable("ltag") + table.decompile(self.DATA_, ttFont=None) + table.toXML(writer, ttFont=None) + expected = os.linesep.join([ + '', + '', + '', + '', + '', + '' + ]) + os.linesep + self.assertEqual(expected.encode("utf_8"), writer.file.getvalue()) + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_m_e_t_a_test.py b/Tests/ttLib/tables/_m_e_t_a_test.py new file mode 100644 index 0000000..dc0f8cd --- /dev/null +++ b/Tests/ttLib/tables/_m_e_t_a_test.py @@ -0,0 +1,95 @@ +from __future__ import print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc.testTools import parseXML +from fontTools.misc.textTools import deHexStr +from fontTools.misc.xmlWriter import XMLWriter +from fontTools.ttLib import TTLibError +from fontTools.ttLib.tables._m_e_t_a import table__m_e_t_a +import unittest + + +# From a real font on MacOS X, but substituted 'bild' tag by 'TEST', +# and shortened the payload. +META_DATA = deHexStr( + "00 00 00 01 00 00 00 00 00 00 00 1C 00 00 00 01 " + "54 45 53 54 00 00 00 1C 00 00 00 04 CA FE BE EF") + +# The 'dlng' and 'slng' tag with text data containing "augmented" BCP 47 +# comma-separated or comma-space-separated tags. These should be UTF-8 encoded +# text. +META_DATA_TEXT = deHexStr( + "00 00 00 01 00 00 00 00 00 00 00 28 00 00 00 02 " + "64 6C 6E 67 00 00 00 28 00 00 00 0E 73 6C 6E 67 " + "00 00 00 36 00 00 00 0E 4C 61 74 6E 2C 47 72 65 " + "6B 2C 43 79 72 6C 4C 61 74 6E 2C 47 72 65 6B 2C " + "43 79 72 6C") + +class MetaTableTest(unittest.TestCase): + def test_decompile(self): + table = table__m_e_t_a() + table.decompile(META_DATA, ttFont={"meta": table}) + self.assertEqual({"TEST": b"\xCA\xFE\xBE\xEF"}, table.data) + + def test_compile(self): + table = table__m_e_t_a() + table.data["TEST"] = b"\xCA\xFE\xBE\xEF" + self.assertEqual(META_DATA, table.compile(ttFont={"meta": table})) + + def test_decompile_text(self): + table = table__m_e_t_a() + table.decompile(META_DATA_TEXT, ttFont={"meta": table}) + self.assertEqual({"dlng": u"Latn,Grek,Cyrl", + "slng": u"Latn,Grek,Cyrl"}, table.data) + + def test_compile_text(self): + table = table__m_e_t_a() + table.data["dlng"] = u"Latn,Grek,Cyrl" + table.data["slng"] = u"Latn,Grek,Cyrl" + self.assertEqual(META_DATA_TEXT, table.compile(ttFont={"meta": table})) + + def test_toXML(self): + table = table__m_e_t_a() + table.data["TEST"] = b"\xCA\xFE\xBE\xEF" + writer = XMLWriter(BytesIO()) + table.toXML(writer, {"meta": table}) + xml = writer.file.getvalue().decode("utf-8") + self.assertEqual([ + '', + 'cafebeef', + '' + ], [line.strip() for line in xml.splitlines()][1:]) + + def test_fromXML(self): + table = table__m_e_t_a() + for name, attrs, content in parseXML( + '' + ' cafebeef' + ''): + table.fromXML(name, attrs, content, ttFont=None) + self.assertEqual({"TEST": b"\xCA\xFE\xBE\xEF"}, table.data) + + def test_toXML_text(self): + table = table__m_e_t_a() + table.data["dlng"] = u"Latn,Grek,Cyrl" + writer = XMLWriter(BytesIO()) + table.toXML(writer, {"meta": table}) + xml = writer.file.getvalue().decode("utf-8") + self.assertEqual([ + '', + 'Latn,Grek,Cyrl', + '' + ], [line.strip() for line in xml.splitlines()][1:]) + + def test_fromXML_text(self): + table = table__m_e_t_a() + for name, attrs, content in parseXML( + '' + ' Latn,Grek,Cyrl' + ''): + table.fromXML(name, attrs, content, ttFont=None) + self.assertEqual({"dlng": u"Latn,Grek,Cyrl"}, table.data) + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_m_o_r_t_test.py b/Tests/ttLib/tables/_m_o_r_t_test.py new file mode 100644 index 0000000..70696e8 --- /dev/null +++ b/Tests/ttLib/tables/_m_o_r_t_test.py @@ -0,0 +1,115 @@ +from __future__ import print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc.testTools import FakeFont, getXML, parseXML +from fontTools.misc.textTools import deHexStr, hexStr +from fontTools.ttLib import newTable +import unittest + + +# Glyph Metamorphosis Table Examples +# Example 1: Non-contextual Glyph Substitution +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6mort.html +# The example given by Apple's 'mort' specification is suboptimally +# encoded: it uses AAT lookup format 6 even though format 8 would be +# more compact. Because our encoder always uses the most compact +# encoding, this breaks our round-trip testing. Therefore, we changed +# the example to use GlyphID 13 instead of 12 for the 'parenright' +# character; the non-contiguous glyph range for the AAT lookup makes +# format 6 to be most compact. +MORT_NONCONTEXTUAL_DATA = deHexStr( + '0001 0000 ' # 0: Version=1.0 + '0000 0001 ' # 4: MorphChainCount=1 + '0000 0001 ' # 8: DefaultFlags=1 + '0000 0050 ' # 12: StructLength=80 + '0003 0001 ' # 16: MorphFeatureCount=3, MorphSubtableCount=1 + '0004 0000 ' # 20: Feature[0].FeatureType=4/VertSubst, .FeatureSetting=on + '0000 0001 ' # 24: Feature[0].EnableFlags=0x00000001 + 'FFFF FFFF ' # 28: Feature[0].DisableFlags=0xFFFFFFFF + '0004 0001 ' # 32: Feature[1].FeatureType=4/VertSubst, .FeatureSetting=off + '0000 0000 ' # 36: Feature[1].EnableFlags=0x00000000 + 'FFFF FFFE ' # 40: Feature[1].DisableFlags=0xFFFFFFFE + '0000 0001 ' # 44: Feature[2].FeatureType=0/GlyphEffects, .FeatSetting=off + '0000 0000 ' # 48: Feature[2].EnableFlags=0 (required for last feature) + '0000 0000 ' # 52: Feature[2].EnableFlags=0 (required for last feature) + '0020 ' # 56: Subtable[0].StructLength=32 + '80 ' # 58: Subtable[0].CoverageFlags=0x80 + '04 ' # 59: Subtable[0].MorphType=4/NoncontextualMorph + '0000 0001 ' # 60: Subtable[0].SubFeatureFlags=0x1 + '0006 0004 ' # 64: LookupFormat=6, UnitSize=4 + '0002 0008 ' # 68: NUnits=2, SearchRange=8 + '0001 0000 ' # 72: EntrySelector=1, RangeShift=0 + '000B 0087 ' # 76: Glyph=11 (parenleft); Value=135 (parenleft.vertical) + '000D 0088 ' # 80: Glyph=13 (parenright); Value=136 (parenright.vertical) + 'FFFF 0000 ' # 84: Glyph=; Value=0 +) # 88: +assert len(MORT_NONCONTEXTUAL_DATA) == 88 + + +MORT_NONCONTEXTUAL_XML = [ + '', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +class MORTNoncontextualGlyphSubstitutionTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.maxDiff = None + glyphs = ['.notdef'] + ['g.%d' % i for i in range (1, 140)] + glyphs[11], glyphs[13] = 'parenleft', 'parenright' + glyphs[135], glyphs[136] = 'parenleft.vertical', 'parenright.vertical' + cls.font = FakeFont(glyphs) + + def test_decompile_toXML(self): + table = newTable('mort') + table.decompile(MORT_NONCONTEXTUAL_DATA, self.font) + self.assertEqual(getXML(table.toXML), MORT_NONCONTEXTUAL_XML) + + def test_compile_fromXML(self): + table = newTable('mort') + for name, attrs, content in parseXML(MORT_NONCONTEXTUAL_XML): + table.fromXML(name, attrs, content, font=self.font) + self.assertEqual(hexStr(table.compile(self.font)), + hexStr(MORT_NONCONTEXTUAL_DATA)) + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_m_o_r_x_test.py b/Tests/ttLib/tables/_m_o_r_x_test.py new file mode 100644 index 0000000..7deb39f --- /dev/null +++ b/Tests/ttLib/tables/_m_o_r_x_test.py @@ -0,0 +1,903 @@ +# coding: utf-8 +from __future__ import print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc.testTools import FakeFont, getXML, parseXML +from fontTools.misc.textTools import deHexStr, hexStr +from fontTools.ttLib import newTable +import unittest + + +# A simple 'morx' table with non-contextual glyph substitution. +# Unfortunately, the Apple spec for 'morx' does not contain a complete example. +# The test case has therefore been adapted from the example 'mort' table in +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6mort.html +MORX_NONCONTEXTUAL_DATA = deHexStr( + '0002 0000 ' # 0: Version=2, Reserved=0 + '0000 0001 ' # 4: MorphChainCount=1 + '0000 0001 ' # 8: DefaultFlags=1 + '0000 0058 ' # 12: StructLength=88 + '0000 0003 ' # 16: MorphFeatureCount=3 + '0000 0001 ' # 20: MorphSubtableCount=1 + '0004 0000 ' # 24: Feature[0].FeatureType=4/VertSubst, .FeatureSetting=on + '0000 0001 ' # 28: Feature[0].EnableFlags=0x00000001 + 'FFFF FFFF ' # 32: Feature[0].DisableFlags=0xFFFFFFFF + '0004 0001 ' # 36: Feature[1].FeatureType=4/VertSubst, .FeatureSetting=off + '0000 0000 ' # 40: Feature[1].EnableFlags=0x00000000 + 'FFFF FFFE ' # 44: Feature[1].DisableFlags=0xFFFFFFFE + '0000 0001 ' # 48: Feature[2].FeatureType=0/GlyphEffects, .FeatSetting=off + '0000 0000 ' # 52: Feature[2].EnableFlags=0 (required for last feature) + '0000 0000 ' # 56: Feature[2].EnableFlags=0 (required for last feature) + '0000 0024 ' # 60: Subtable[0].StructLength=36 + '80 ' # 64: Subtable[0].CoverageFlags=0x80 + '00 00 ' # 65: Subtable[0].Reserved=0 + '04 ' # 67: Subtable[0].MorphType=4/NoncontextualMorph + '0000 0001 ' # 68: Subtable[0].SubFeatureFlags=0x1 + '0006 0004 ' # 72: LookupFormat=6, UnitSize=4 + '0002 0008 ' # 76: NUnits=2, SearchRange=8 + '0001 0000 ' # 80: EntrySelector=1, RangeShift=0 + '000B 0087 ' # 84: Glyph=11 (parenleft); Value=135 (parenleft.vertical) + '000D 0088 ' # 88: Glyph=13 (parenright); Value=136 (parenright.vertical) + 'FFFF 0000 ' # 92: Glyph=; Value=0 +) # 96: +assert len(MORX_NONCONTEXTUAL_DATA) == 96 + + +MORX_NONCONTEXTUAL_XML = [ + '', + '', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +MORX_REARRANGEMENT_DATA = deHexStr( + '0002 0000 ' # 0: Version=2, Reserved=0 + '0000 0001 ' # 4: MorphChainCount=1 + '0000 0001 ' # 8: DefaultFlags=1 + '0000 0078 ' # 12: StructLength=120 (+8=128) + '0000 0000 ' # 16: MorphFeatureCount=0 + '0000 0001 ' # 20: MorphSubtableCount=1 + '0000 0068 ' # 24: Subtable[0].StructLength=104 (+24=128) + '80 ' # 28: Subtable[0].CoverageFlags=0x80 + '00 00 ' # 29: Subtable[0].Reserved=0 + '00 ' # 31: Subtable[0].MorphType=0/RearrangementMorph + '0000 0001 ' # 32: Subtable[0].SubFeatureFlags=0x1 + '0000 0006 ' # 36: STXHeader.ClassCount=6 + '0000 0010 ' # 40: STXHeader.ClassTableOffset=16 (+36=52) + '0000 0028 ' # 44: STXHeader.StateArrayOffset=40 (+36=76) + '0000 004C ' # 48: STXHeader.EntryTableOffset=76 (+36=112) + '0006 0004 ' # 52: ClassTable.LookupFormat=6, .UnitSize=4 + '0002 0008 ' # 56: .NUnits=2, .SearchRange=8 + '0001 0000 ' # 60: .EntrySelector=1, .RangeShift=0 + '0001 0005 ' # 64: Glyph=A; Class=5 + '0003 0004 ' # 68: Glyph=C; Class=4 + 'FFFF 0000 ' # 72: Glyph=; Value=0 + '0000 0001 0002 0003 0002 0001 ' # 76: State[0][0..5] + '0003 0003 0003 0003 0003 0003 ' # 88: State[1][0..5] + '0001 0003 0003 0003 0002 0002 ' # 100: State[2][0..5] + '0002 FFFF ' # 112: Entries[0].NewState=2, .Flags=0xFFFF + '0001 A00D ' # 116: Entries[1].NewState=1, .Flags=0xA00D + '0000 8006 ' # 120: Entries[2].NewState=0, .Flags=0x8006 + '0002 0000 ' # 124: Entries[3].NewState=2, .Flags=0x0000 +) # 128: +assert len(MORX_REARRANGEMENT_DATA) == 128, len(MORX_REARRANGEMENT_DATA) + + +MORX_REARRANGEMENT_XML = [ + '', + '', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +# Taken from “Example 1: A contextal substituation table” in +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6morx.html +# as retrieved on 2017-09-05. +# +# Compared to the example table in Apple’s specification, we’ve +# made the following changes: +# +# * at offsets 0..35, we’ve prepended 36 bytes of boilerplate +# to make the data a structurally valid ‘morx’ table; +# +# * at offset 36 (offset 0 in Apple’s document), we’ve changed +# the number of glyph classes from 5 to 6 because the encoded +# finite-state machine has transitions for six different glyph +# classes (0..5); +# +# * at offset 52 (offset 16 in Apple’s document), we’ve replaced +# the presumably leftover ‘XXX’ mark by an actual data offset; +# +# * at offset 72 (offset 36 in Apple’s document), we’ve changed +# the input GlyphID from 51 to 52. With the original value of 51, +# the glyph class lookup table can be encoded with equally many +# bytes in either format 2 or 6; after changing the GlyphID to 52, +# the most compact encoding is lookup format 6, as used in Apple’s +# example; +# +# * at offset 90 (offset 54 in Apple’s document), we’ve changed +# the value for the lookup end-of-table marker from 1 to 0. +# Fonttools always uses zero for this value, whereas Apple’s +# spec examples are inconsistently using one of {0, 1, 0xFFFF} +# for this filler value; +# +# * at offset 172 (offset 136 in Apple’s document), we’ve again changed +# the input GlyphID from 51 to 52, for the same reason as above. +# +# TODO: Ask Apple to fix “Example 1” in the ‘morx’ specification. +MORX_CONTEXTUAL_DATA = deHexStr( + '0002 0000 ' # 0: Version=2, Reserved=0 + '0000 0001 ' # 4: MorphChainCount=1 + '0000 0001 ' # 8: DefaultFlags=1 + '0000 00B4 ' # 12: StructLength=180 (+8=188) + '0000 0000 ' # 16: MorphFeatureCount=0 + '0000 0001 ' # 20: MorphSubtableCount=1 + '0000 00A4 ' # 24: Subtable[0].StructLength=164 (+24=188) + '80 ' # 28: Subtable[0].CoverageFlags=0x80 + '00 00 ' # 29: Subtable[0].Reserved=0 + '01 ' # 31: Subtable[0].MorphType=1/ContextualMorph + '0000 0001 ' # 32: Subtable[0].SubFeatureFlags=0x1 + '0000 0006 ' # 36: STXHeader.ClassCount=6 + '0000 0014 ' # 40: STXHeader.ClassTableOffset=20 (+36=56) + '0000 0038 ' # 44: STXHeader.StateArrayOffset=56 (+36=92) + '0000 005C ' # 48: STXHeader.EntryTableOffset=92 (+36=128) + '0000 0074 ' # 52: STXHeader.PerGlyphTableOffset=116 (+36=152) + + # Glyph class table. + '0006 0004 ' # 56: ClassTable.LookupFormat=6, .UnitSize=4 + '0005 0010 ' # 60: .NUnits=5, .SearchRange=16 + '0002 0004 ' # 64: .EntrySelector=2, .RangeShift=4 + '0032 0004 ' # 68: Glyph=50; Class=4 + '0034 0004 ' # 72: Glyph=52; Class=4 + '0050 0005 ' # 76: Glyph=80; Class=5 + '00C9 0004 ' # 80: Glyph=201; Class=4 + '00CA 0004 ' # 84: Glyph=202; Class=4 + 'FFFF 0000 ' # 88: Glyph=; Value= + + # State array. + '0000 0000 0000 0000 0000 0001 ' # 92: State[0][0..5] + '0000 0000 0000 0000 0000 0001 ' # 104: State[1][0..5] + '0000 0000 0000 0000 0002 0001 ' # 116: State[2][0..5] + + # Entry table. + '0000 0000 ' # 128: Entries[0].NewState=0, .Flags=0 + 'FFFF FFFF ' # 132: Entries[0].MarkSubst=None, .CurSubst=None + '0002 0000 ' # 136: Entries[1].NewState=2, .Flags=0 + 'FFFF FFFF ' # 140: Entries[1].MarkSubst=None, .CurSubst=None + '0000 0000 ' # 144: Entries[2].NewState=0, .Flags=0 + 'FFFF 0000 ' # 148: Entries[2].MarkSubst=None, .CurSubst=PerGlyph #0 + # 152: + + # Per-glyph lookup tables. + '0000 0004 ' # 152: Offset from this point to per-glyph lookup #0. + + # Per-glyph lookup #0. + '0006 0004 ' # 156: ClassTable.LookupFormat=6, .UnitSize=4 + '0004 0010 ' # 160: .NUnits=4, .SearchRange=16 + '0002 0000 ' # 164: .EntrySelector=2, .RangeShift=0 + '0032 0258 ' # 168: Glyph=50; ReplacementGlyph=600 + '0034 0259 ' # 172: Glyph=52; ReplacementGlyph=601 + '00C9 025A ' # 176: Glyph=201; ReplacementGlyph=602 + '00CA 0384 ' # 180: Glyph=202; ReplacementGlyph=900 + 'FFFF 0000 ' # 184: Glyph=; Value= + +) # 188: +assert len(MORX_CONTEXTUAL_DATA) == 188, len(MORX_CONTEXTUAL_DATA) + + +MORX_CONTEXTUAL_XML = [ + '', + '', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +# Taken from “Example 2: A ligature table” in +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6morx.html +# as retrieved on 2017-09-11. +# +# Compared to the example table in Apple’s specification, we’ve +# made the following changes: +# +# * at offsets 0..35, we’ve prepended 36 bytes of boilerplate +# to make the data a structurally valid ‘morx’ table; +# +# * at offsets 88..91 (offsets 52..55 in Apple’s document), we’ve +# changed the range of the third segment from 23..24 to 26..28. +# The hexdump values in Apple’s specification are completely wrong; +# the values from the comments would work, but they can be encoded +# more compactly than in the specification example. For round-trip +# testing, we omit the ‘f’ glyph, which makes AAT lookup format 2 +# the most compact encoding; +# +# * at offsets 92..93 (offsets 56..57 in Apple’s document), we’ve +# changed the glyph class of the third segment from 5 to 6, which +# matches the values from the comments to the spec (but not the +# Apple’s hexdump). +# +# TODO: Ask Apple to fix “Example 2” in the ‘morx’ specification. +MORX_LIGATURE_DATA = deHexStr( + '0002 0000 ' # 0: Version=2, Reserved=0 + '0000 0001 ' # 4: MorphChainCount=1 + '0000 0001 ' # 8: DefaultFlags=1 + '0000 00DA ' # 12: StructLength=218 (+8=226) + '0000 0000 ' # 16: MorphFeatureCount=0 + '0000 0001 ' # 20: MorphSubtableCount=1 + '0000 00CA ' # 24: Subtable[0].StructLength=202 (+24=226) + '80 ' # 28: Subtable[0].CoverageFlags=0x80 + '00 00 ' # 29: Subtable[0].Reserved=0 + '02 ' # 31: Subtable[0].MorphType=2/LigatureMorph + '0000 0001 ' # 32: Subtable[0].SubFeatureFlags=0x1 + + # State table header. + '0000 0007 ' # 36: STXHeader.ClassCount=7 + '0000 001C ' # 40: STXHeader.ClassTableOffset=28 (+36=64) + '0000 0040 ' # 44: STXHeader.StateArrayOffset=64 (+36=100) + '0000 0078 ' # 48: STXHeader.EntryTableOffset=120 (+36=156) + '0000 0090 ' # 52: STXHeader.LigActionsOffset=144 (+36=180) + '0000 009C ' # 56: STXHeader.LigComponentsOffset=156 (+36=192) + '0000 00AE ' # 60: STXHeader.LigListOffset=174 (+36=210) + + # Glyph class table. + '0002 0006 ' # 64: ClassTable.LookupFormat=2, .UnitSize=6 + '0003 000C ' # 68: .NUnits=3, .SearchRange=12 + '0001 0006 ' # 72: .EntrySelector=1, .RangeShift=6 + '0016 0014 0004 ' # 76: GlyphID 20..22 [a..c] -> GlyphClass 4 + '0018 0017 0005 ' # 82: GlyphID 23..24 [d..e] -> GlyphClass 5 + '001C 001A 0006 ' # 88: GlyphID 26..28 [g..i] -> GlyphClass 6 + 'FFFF FFFF 0000 ' # 94: + + # State array. + '0000 0000 0000 0000 0001 0000 0000 ' # 100: State[0][0..6] + '0000 0000 0000 0000 0001 0000 0000 ' # 114: State[1][0..6] + '0000 0000 0000 0000 0001 0002 0000 ' # 128: State[2][0..6] + '0000 0000 0000 0000 0001 0002 0003 ' # 142: State[3][0..6] + + # Entry table. + '0000 0000 ' # 156: Entries[0].NewState=0, .Flags=0 + '0000 ' # 160: Entries[0].ActionIndex= because no 0x2000 flag + '0002 8000 ' # 162: Entries[1].NewState=2, .Flags=0x8000 (SetComponent) + '0000 ' # 166: Entries[1].ActionIndex= because no 0x2000 flag + '0003 8000 ' # 168: Entries[2].NewState=3, .Flags=0x8000 (SetComponent) + '0000 ' # 172: Entries[2].ActionIndex= because no 0x2000 flag + '0000 A000 ' # 174: Entries[3].NewState=0, .Flags=0xA000 (SetComponent,Act) + '0000 ' # 178: Entries[3].ActionIndex=0 (start at Action[0]) + + # Ligature actions table. + '3FFF FFE7 ' # 180: Action[0].Flags=0, .GlyphIndexDelta=-25 + '3FFF FFED ' # 184: Action[1].Flags=0, .GlyphIndexDelta=-19 + 'BFFF FFF2 ' # 188: Action[2].Flags=, .GlyphIndexDelta=-14 + + # Ligature component table. + '0000 0001 ' # 192: LigComponent[0]=0, LigComponent[1]=1 + '0002 0003 ' # 196: LigComponent[2]=2, LigComponent[3]=3 + '0000 0004 ' # 200: LigComponent[4]=0, LigComponent[5]=4 + '0000 0008 ' # 204: LigComponent[6]=0, LigComponent[7]=8 + '0010 ' # 208: LigComponent[8]=16 + + # Ligature list. + '03E8 03E9 ' # 210: LigList[0]=1000, LigList[1]=1001 + '03EA 03EB ' # 214: LigList[2]=1002, LigList[3]=1003 + '03EC 03ED ' # 218: LigList[4]=1004, LigList[3]=1005 + '03EE 03EF ' # 222: LigList[5]=1006, LigList[6]=1007 +) # 226: +assert len(MORX_LIGATURE_DATA) == 226, len(MORX_LIGATURE_DATA) + + +MORX_LIGATURE_XML = [ + '', + '', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +class MORXNoncontextualGlyphSubstitutionTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.maxDiff = None + glyphs = ['.notdef'] + ['g.%d' % i for i in range (1, 140)] + glyphs[11], glyphs[13] = 'parenleft', 'parenright' + glyphs[135], glyphs[136] = 'parenleft.vertical', 'parenright.vertical' + cls.font = FakeFont(glyphs) + + def test_decompile_toXML(self): + table = newTable('morx') + table.decompile(MORX_NONCONTEXTUAL_DATA, self.font) + self.assertEqual(getXML(table.toXML), MORX_NONCONTEXTUAL_XML) + + def test_compile_fromXML(self): + table = newTable('morx') + for name, attrs, content in parseXML(MORX_NONCONTEXTUAL_XML): + table.fromXML(name, attrs, content, font=self.font) + self.assertEqual(hexStr(table.compile(self.font)), + hexStr(MORX_NONCONTEXTUAL_DATA)) + + +class MORXRearrangementTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.maxDiff = None + cls.font = FakeFont(['.nodef', 'A', 'B', 'C']) + + def test_decompile_toXML(self): + table = newTable('morx') + table.decompile(MORX_REARRANGEMENT_DATA, self.font) + self.assertEqual(getXML(table.toXML), MORX_REARRANGEMENT_XML) + + def test_compile_fromXML(self): + table = newTable('morx') + for name, attrs, content in parseXML(MORX_REARRANGEMENT_XML): + table.fromXML(name, attrs, content, font=self.font) + self.assertEqual(hexStr(table.compile(self.font)), + hexStr(MORX_REARRANGEMENT_DATA)) + + +class MORXContextualSubstitutionTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.maxDiff = None + g = ['.notdef'] + ['g.%d' % i for i in range (1, 910)] + g[80] = 'C' + g[50], g[52], g[201], g[202] = 'A', 'B', 'X', 'Y' + g[600], g[601], g[602], g[900] = ( + 'A.swash', 'B.swash', 'X.swash', 'Y.swash') + cls.font = FakeFont(g) + + def test_decompile_toXML(self): + table = newTable('morx') + table.decompile(MORX_CONTEXTUAL_DATA, self.font) + self.assertEqual(getXML(table.toXML), MORX_CONTEXTUAL_XML) + + def test_compile_fromXML(self): + table = newTable('morx') + for name, attrs, content in parseXML(MORX_CONTEXTUAL_XML): + table.fromXML(name, attrs, content, font=self.font) + self.assertEqual(hexStr(table.compile(self.font)), + hexStr(MORX_CONTEXTUAL_DATA)) + + +class MORXLigatureSubstitutionTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.maxDiff = None + g = ['.notdef'] + ['g.%d' % i for i in range (1, 1515)] + g[20:29] = 'a b c d e f g h i'.split() + g[1000:1008] = 'adf adg adh adi aef aeg aeh aei'.split() + g[1008:1016] = 'bdf bdg bdh bdi bef beg beh bei'.split() + g[1500:1507] = 'cdf cdg cdh cdi cef ceg ceh'.split() + g[1511] = 'cei' + cls.font = FakeFont(g) + + def test_decompile_toXML(self): + table = newTable('morx') + table.decompile(MORX_LIGATURE_DATA, self.font) + self.assertEqual(getXML(table.toXML), MORX_LIGATURE_XML) + + def test_compile_fromXML(self): + table = newTable('morx') + for name, attrs, content in parseXML(MORX_LIGATURE_XML): + table.fromXML(name, attrs, content, font=self.font) + self.assertEqual(hexStr(table.compile(self.font)), + hexStr(MORX_LIGATURE_DATA)) + + +class MORXCoverageFlagsTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.maxDiff = None + cls.font = FakeFont(['.notdef', 'A', 'B', 'C']) + + def checkFlags(self, flags, textDirection, processingOrder, + checkCompile=True): + data = bytesjoin([ + MORX_REARRANGEMENT_DATA[:28], + bytechr(flags << 4), + MORX_REARRANGEMENT_DATA[29:]]) + xml = [] + for line in MORX_REARRANGEMENT_XML: + if line.startswith(' ', xml) + table2 = newTable('morx') + for name, attrs, content in parseXML(xml): + table2.fromXML(name, attrs, content, font=self.font) + self.assertEqual(hexStr(table2.compile(self.font)[28:31]), "8abcde") + + +class UnsupportedMorxLookupTest(unittest.TestCase): + def __init__(self, methodName): + unittest.TestCase.__init__(self, methodName) + # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, + # and fires deprecation warnings if a program uses the old name. + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + + def test_unsupportedLookupType(self): + data = bytesjoin([ + MORX_NONCONTEXTUAL_DATA[:67], + bytechr(66), + MORX_NONCONTEXTUAL_DATA[69:]]) + with self.assertRaisesRegex(AssertionError, + r"unsupported 'morx' lookup type 66"): + morx = newTable('morx') + morx.decompile(data, FakeFont(['.notdef'])) + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_n_a_m_e_test.py b/Tests/ttLib/tables/_n_a_m_e_test.py new file mode 100644 index 0000000..a27e3c1 --- /dev/null +++ b/Tests/ttLib/tables/_n_a_m_e_test.py @@ -0,0 +1,303 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc import sstruct +from fontTools.misc.loggingTools import CapturingLogHandler +from fontTools.misc.testTools import FakeFont +from fontTools.misc.xmlWriter import XMLWriter +import struct +import unittest +from fontTools.ttLib import newTable +from fontTools.ttLib.tables._n_a_m_e import ( + table__n_a_m_e, NameRecord, nameRecordFormat, nameRecordSize, makeName, log) + + +def names(nameTable): + result = [(n.nameID, n.platformID, n.platEncID, n.langID, n.string) + for n in nameTable.names] + result.sort() + return result + + +class NameTableTest(unittest.TestCase): + + def test_getDebugName(self): + table = table__n_a_m_e() + table.names = [ + makeName("Bold", 258, 1, 0, 0), # Mac, MacRoman, English + makeName("Gras", 258, 1, 0, 1), # Mac, MacRoman, French + makeName("Fett", 258, 1, 0, 2), # Mac, MacRoman, German + makeName("Sem Fracções", 292, 1, 0, 8) # Mac, MacRoman, Portuguese + ] + self.assertEqual("Bold", table.getDebugName(258)) + self.assertEqual("Sem Fracções", table.getDebugName(292)) + self.assertEqual(None, table.getDebugName(999)) + + def test_setName(self): + table = table__n_a_m_e() + table.setName("Regular", 2, 1, 0, 0) + table.setName("Version 1.000", 5, 3, 1, 0x409) + table.setName("寬鬆", 276, 1, 2, 0x13) + self.assertEqual("Regular", table.getName(2, 1, 0, 0).toUnicode()) + self.assertEqual("Version 1.000", table.getName(5, 3, 1, 0x409).toUnicode()) + self.assertEqual("寬鬆", table.getName(276, 1, 2, 0x13).toUnicode()) + self.assertTrue(len(table.names) == 3) + table.setName("緊縮", 276, 1, 2, 0x13) + self.assertEqual("緊縮", table.getName(276, 1, 2, 0x13).toUnicode()) + self.assertTrue(len(table.names) == 3) + # passing bytes issues a warning + with CapturingLogHandler(log, "WARNING") as captor: + table.setName(b"abc", 0, 1, 0, 0) + self.assertTrue( + len([r for r in captor.records if "string is bytes" in r.msg]) == 1) + # anything other than unicode or bytes raises an error + with self.assertRaises(TypeError): + table.setName(1.000, 5, 1, 0, 0) + + def test_addName(self): + table = table__n_a_m_e() + nameIDs = [] + for string in ("Width", "Weight", "Custom"): + nameIDs.append(table.addName(string)) + + self.assertEqual(nameIDs[0], 256) + self.assertEqual(nameIDs[1], 257) + self.assertEqual(nameIDs[2], 258) + self.assertEqual(len(table.names), 6) + self.assertEqual(table.names[0].string, "Width") + self.assertEqual(table.names[1].string, "Width") + self.assertEqual(table.names[2].string, "Weight") + self.assertEqual(table.names[3].string, "Weight") + self.assertEqual(table.names[4].string, "Custom") + self.assertEqual(table.names[5].string, "Custom") + + with self.assertRaises(ValueError): + table.addName('Invalid nameID', minNameID=32767) + with self.assertRaises(TypeError): + table.addName(b"abc") # must be unicode string + + def test_addMultilingualName(self): + # Microsoft Windows has language codes for “English” (en) + # and for “Standard German as used in Switzerland” (de-CH). + # In this case, we expect that the implementation just + # encodes the name for the Windows platform; Apple platforms + # have been able to decode Windows names since the early days + # of OSX (~2001). However, Windows has no language code for + # “Swiss German as used in Liechtenstein” (gsw-LI), so we + # expect that the implementation populates the 'ltag' table + # to represent that particular, rather exotic BCP47 code. + font = FakeFont(glyphs=[".notdef", "A"]) + nameTable = font.tables['name'] = newTable("name") + with CapturingLogHandler(log, "WARNING") as captor: + widthID = nameTable.addMultilingualName({ + "en": "Width", + "de-CH": "Breite", + "gsw-LI": "Bräiti", + }, ttFont=font) + self.assertEqual(widthID, 256) + xHeightID = nameTable.addMultilingualName({ + "en": "X-Height", + "gsw-LI": "X-Hööchi" + }, ttFont=font) + self.assertEqual(xHeightID, 257) + captor.assertRegex("cannot add Windows name in language gsw-LI") + self.assertEqual(names(nameTable), [ + (256, 0, 4, 0, "Bräiti"), + (256, 3, 1, 0x0409, "Width"), + (256, 3, 1, 0x0807, "Breite"), + (257, 0, 4, 0, "X-Hööchi"), + (257, 3, 1, 0x0409, "X-Height"), + ]) + self.assertEqual(set(font.tables.keys()), {"ltag", "name"}) + self.assertEqual(font["ltag"].tags, ["gsw-LI"]) + + def test_addMultilingualName_legacyMacEncoding(self): + # Windows has no language code for Latin; MacOS has a code; + # and we actually can convert the name to the legacy MacRoman + # encoding. In this case, we expect that the name gets encoded + # as Macintosh name (platformID 1) with the corresponding Mac + # language code (133); the 'ltag' table should not be used. + font = FakeFont(glyphs=[".notdef", "A"]) + nameTable = font.tables['name'] = newTable("name") + with CapturingLogHandler(log, "WARNING") as captor: + nameTable.addMultilingualName({"la": "SPQR"}, + ttFont=font) + captor.assertRegex("cannot add Windows name in language la") + self.assertEqual(names(nameTable), [(256, 1, 0, 131, "SPQR")]) + self.assertNotIn("ltag", font.tables.keys()) + + def test_addMultilingualName_legacyMacEncodingButUnencodableName(self): + # Windows has no language code for Latin; MacOS has a code; + # but we cannot encode the name into this encoding because + # it contains characters that are not representable. + # In this case, we expect that the name gets encoded as + # Unicode name (platformID 0) with the language tag being + # added to the 'ltag' table. + font = FakeFont(glyphs=[".notdef", "A"]) + nameTable = font.tables['name'] = newTable("name") + with CapturingLogHandler(log, "WARNING") as captor: + nameTable.addMultilingualName({"la": "ⱾƤℚⱤ"}, + ttFont=font) + captor.assertRegex("cannot add Windows name in language la") + self.assertEqual(names(nameTable), [(256, 0, 4, 0, "ⱾƤℚⱤ")]) + self.assertIn("ltag", font.tables) + self.assertEqual(font["ltag"].tags, ["la"]) + + def test_addMultilingualName_legacyMacEncodingButNoCodec(self): + # Windows has no language code for “Azeri written in the + # Arabic script” (az-Arab); MacOS would have a code (50); + # but we cannot encode the name into the legacy encoding + # because we have no codec for MacArabic in fonttools. + # In this case, we expect that the name gets encoded as + # Unicode name (platformID 0) with the language tag being + # added to the 'ltag' table. + font = FakeFont(glyphs=[".notdef", "A"]) + nameTable = font.tables['name'] = newTable("name") + with CapturingLogHandler(log, "WARNING") as captor: + nameTable.addMultilingualName({"az-Arab": "آذربايجان ديلی"}, + ttFont=font) + captor.assertRegex("cannot add Windows name in language az-Arab") + self.assertEqual(names(nameTable), [(256, 0, 4, 0, "آذربايجان ديلی")]) + self.assertIn("ltag", font.tables) + self.assertEqual(font["ltag"].tags, ["az-Arab"]) + + def test_addMultilingualName_noTTFont(self): + # If the ttFont argument is not passed, the implementation + # should add whatever names it can, but it should not crash + # just because it cannot build an ltag table. + nameTable = newTable("name") + with CapturingLogHandler(log, "WARNING") as captor: + nameTable.addMultilingualName({"en": "A", "la": "ⱾƤℚⱤ"}) + captor.assertRegex("cannot store language la into 'ltag' table") + + def test_decompile_badOffset(self): + # https://github.com/behdad/fonttools/issues/525 + table = table__n_a_m_e() + badRecord = { + "platformID": 1, + "platEncID": 3, + "langID": 7, + "nameID": 1, + "length": 3, + "offset": 8765 # out of range + } + data = bytesjoin([ + struct.pack(tostr(">HHH"), 1, 1, 6 + nameRecordSize), + sstruct.pack(nameRecordFormat, badRecord)]) + table.decompile(data, ttFont=None) + self.assertEqual(table.names, []) + + +class NameRecordTest(unittest.TestCase): + + def test_toUnicode_utf16be(self): + name = makeName("Foo Bold", 111, 0, 2, 7) + self.assertEqual("utf_16_be", name.getEncoding()) + self.assertEqual("Foo Bold", name.toUnicode()) + + def test_toUnicode_macroman(self): + name = makeName("Foo Italic", 222, 1, 0, 7) # MacRoman + self.assertEqual("mac_roman", name.getEncoding()) + self.assertEqual("Foo Italic", name.toUnicode()) + + def test_toUnicode_macromanian(self): + name = makeName(b"Foo Italic\xfb", 222, 1, 0, 37) # Mac Romanian + self.assertEqual("mac_romanian", name.getEncoding()) + self.assertEqual("Foo Italic"+unichr(0x02DA), name.toUnicode()) + + def test_toUnicode_UnicodeDecodeError(self): + name = makeName(b"\1", 111, 0, 2, 7) + self.assertEqual("utf_16_be", name.getEncoding()) + self.assertRaises(UnicodeDecodeError, name.toUnicode) + + def toXML(self, name): + writer = XMLWriter(BytesIO()) + name.toXML(writer, ttFont=None) + xml = writer.file.getvalue().decode("utf_8").strip() + return xml.split(writer.newlinestr.decode("utf_8"))[1:] + + def test_toXML_utf16be(self): + name = makeName("Foo Bold", 111, 0, 2, 7) + self.assertEqual([ + '', + ' Foo Bold', + '' + ], self.toXML(name)) + + def test_toXML_utf16be_odd_length1(self): + name = makeName(b"\0F\0o\0o\0", 111, 0, 2, 7) + self.assertEqual([ + '', + ' Foo', + '' + ], self.toXML(name)) + + def test_toXML_utf16be_odd_length2(self): + name = makeName(b"\0Fooz", 111, 0, 2, 7) + self.assertEqual([ + '', + ' Fooz', + '' + ], self.toXML(name)) + + def test_toXML_utf16be_double_encoded(self): + name = makeName(b"\0\0\0F\0\0\0o", 111, 0, 2, 7) + self.assertEqual([ + '', + ' Fo', + '' + ], self.toXML(name)) + + def test_toXML_macroman(self): + name = makeName("Foo Italic", 222, 1, 0, 7) # MacRoman + self.assertEqual([ + '', + ' Foo Italic', + '' + ], self.toXML(name)) + + def test_toXML_macroman_actual_utf16be(self): + name = makeName("\0F\0o\0o", 222, 1, 0, 7) + self.assertEqual([ + '', + ' Foo', + '' + ], self.toXML(name)) + + def test_toXML_unknownPlatEncID_nonASCII(self): + name = makeName(b"B\x8arli", 333, 1, 9876, 7) # Unknown Mac encodingID + self.assertEqual([ + '', + ' BŠrli', + '' + ], self.toXML(name)) + + def test_toXML_unknownPlatEncID_ASCII(self): + name = makeName(b"Barli", 333, 1, 9876, 7) # Unknown Mac encodingID + self.assertEqual([ + '', + ' Barli', + '' + ], self.toXML(name)) + + def test_encoding_macroman_misc(self): + name = makeName('', 123, 1, 0, 17) # Mac Turkish + self.assertEqual(name.getEncoding(), "mac_turkish") + name.langID = 37 + self.assertEqual(name.getEncoding(), "mac_romanian") + name.langID = 45 # Other + self.assertEqual(name.getEncoding(), "mac_roman") + + def test_extended_mac_encodings(self): + name = makeName(b'\xfe', 123, 1, 1, 0) # Mac Japanese + self.assertEqual(name.toUnicode(), unichr(0x2122)) + + def test_extended_unknown(self): + name = makeName(b'\xfe', 123, 10, 11, 12) + self.assertEqual(name.getEncoding(), "ascii") + self.assertEqual(name.getEncoding(None), None) + self.assertEqual(name.getEncoding(default=None), None) + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_o_p_b_d_test.py b/Tests/ttLib/tables/_o_p_b_d_test.py new file mode 100644 index 0000000..131d3f9 --- /dev/null +++ b/Tests/ttLib/tables/_o_p_b_d_test.py @@ -0,0 +1,183 @@ +from __future__ import print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc.testTools import FakeFont, getXML, parseXML +from fontTools.misc.textTools import deHexStr, hexStr +from fontTools.ttLib import newTable +import unittest + + +# Example: Format 0 Optical Bounds Table +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6opbd.html +OPBD_FORMAT_0_DATA = deHexStr( + '0001 0000 0000 ' # 0: Version=1.0, Format=0 + '0006 0004 0002 ' # 6: LookupFormat=6, UnitSize=4, NUnits=2 + '0008 0001 0000 ' # 12: SearchRange=8, EntrySelector=1, RangeShift=0 + '000A 001E ' # 18: Glyph=10(=C), OffsetOfOpticalBoundsDeltas=30 + '002B 0026 ' # 22: Glyph=43(=A), OffsetOfOpticalBoundsDeltas=38 + 'FFFF 0000 ' # 26: Glyph=, OffsetOfOpticalBoundsDeltas=0 + 'FFCE 0005 0037 FFFB ' # 30: Bounds[C].Left=-50 .Top=5 .Right=55 .Bottom=-5 + 'FFF6 000F 0000 0000 ' # 38: Bounds[A].Left=-10 .Top=15 .Right=0 .Bottom=0 +) # 46: +assert(len(OPBD_FORMAT_0_DATA) == 46) + + +OPBD_FORMAT_0_XML = [ + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +# Example: Format 1 Optical Bounds Table +# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6opbd.html +OPBD_FORMAT_1_DATA = deHexStr( + '0001 0000 0001 ' # 0: Version=1.0, Format=1 + '0006 0004 0002 ' # 6: LookupFormat=6, UnitSize=4, NUnits=2 + '0008 0001 0000 ' # 12: SearchRange=8, EntrySelector=1, RangeShift=0 + '000A 001E ' # 18: Glyph=10(=C), OffsetOfOpticalBoundsPoints=30 + '002B 0026 ' # 22: Glyph=43(=A), OffsetOfOpticalBoundsPoints=38 + 'FFFF 0000 ' # 26: Glyph=, OffsetOfOpticalBoundsPoints=0 + '0024 0025 0026 0027 ' # 30: Bounds[C].Left=36 .Top=37 .Right=38 .Bottom=39 + '0020 0029 FFFF FFFF ' # 38: Bounds[A].Left=32 .Top=41 .Right=-1 .Bottom=-1 +) # 46: +assert(len(OPBD_FORMAT_1_DATA) == 46) + + +OPBD_FORMAT_1_XML = [ + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +# This is the content of the Optical Bounds table in AppleChancery.ttf, +# font version 8.0d1e1 of 2013-02-06. An early version of fontTools +# was crashing when trying to decompile this table. +# https://github.com/fonttools/fonttools/issues/1031 +OPBD_APPLE_CHANCERY_DATA = deHexStr( + '0001 0000 0000 ' # 0: Version=1.0, Format=0 + '0004 0006 0011 ' # 6: LookupFormat=4, UnitSize=6, NUnits=17 + '0060 0004 0006 ' # 12: SearchRange=96, EntrySelector=4, RangeShift=6 + '017d 017d 0072 ' # 18: Seg[0].LastGlyph=381, FirstGlyph=381, Off=114(+6) + '0183 0180 0074 ' # 24: Seg[1].LastGlyph=387, FirstGlyph=384, Off=116(+6) + '0186 0185 007c ' # 30: Seg[2].LastGlyph=390, FirstGlyph=389, Off=124(+6) + '018f 018b 0080 ' # 36: Seg[3].LastGlyph=399, FirstGlyph=395, Off=128(+6) + '01a0 0196 008a ' # 42: Seg[4].LastGlyph=416, FirstGlyph=406, Off=138(+6) + '01a5 01a3 00a0 ' # 48: Seg[5].LastGlyph=421, FirstGlyph=419, Off=160(+6) + '01aa 01aa 00a6 ' # 54: Seg[6].LastGlyph=426, FirstGlyph=426, Off=166(+6) + '01ac 01ac 00a8 ' # 60: Seg[7].LastGlyph=428, FirstGlyph=428, Off=168(+6) + '01fb 01f1 00aa ' # 66: Seg[8].LastGlyph=507, FirstGlyph=497, Off=170(+6) + '0214 0209 00c0 ' # 72: Seg[9].LastGlyph=532, FirstGlyph=521, Off=192(+6) + '021d 0216 00d8 ' # 78: Seg[10].LastGlyph=541, FirstGlyph=534, Off=216(+6) + '0222 0220 00e8 ' # 84: Seg[11].LastGlyph=546, FirstGlyph=544, Off=232(+6) + '0227 0225 00ee ' # 90: Seg[12].LastGlyph=551, FirstGlyph=549, Off=238(+6) + '0229 0229 00f4 ' # 96: Seg[13].LastGlyph=553, FirstGlyph=553, Off=244(+6) + '023b 023b 00f6 ' # 102: Seg[14].LastGlyph=571, FirstGlyph=571, Off=246(+6) + '023e 023e 00f8 ' # 108: Seg[15].LastGlyph=574, FirstGlyph=574, Off=248(+6) + 'ffff ffff 00fa ' # 114: Seg[16]= + '0100 0108 0110 0118 0120 0128 0130 0138 0140 0148 0150 0158 ' + '0160 0168 0170 0178 0180 0188 0190 0198 01a0 01a8 01b0 01b8 ' + '01c0 01c8 01d0 01d8 01e0 01e8 01f0 01f8 0200 0208 0210 0218 ' + '0220 0228 0230 0238 0240 0248 0250 0258 0260 0268 0270 0278 ' + '0280 0288 0290 0298 02a0 02a8 02b0 02b8 02c0 02c8 02d0 02d8 ' + '02e0 02e8 02f0 02f8 0300 0308 0310 0318 fd98 0000 0000 0000 ' + 'fdbc 0000 0000 0000 fdbc 0000 0000 0000 fdbf 0000 0000 0000 ' + 'fdbc 0000 0000 0000 fd98 0000 0000 0000 fda9 0000 0000 0000 ' + 'fd98 0000 0000 0000 fd98 0000 0000 0000 fd98 0000 0000 0000 ' + '0000 0000 0205 0000 0000 0000 0205 0000 0000 0000 02a4 0000 ' + '0000 0000 027e 0000 0000 0000 02f4 0000 0000 0000 02a4 0000 ' + '0000 0000 0365 0000 0000 0000 0291 0000 0000 0000 0291 0000 ' + '0000 0000 026a 0000 0000 0000 02b8 0000 0000 0000 02cb 0000 ' + '0000 0000 02a4 0000 0000 0000 01a9 0000 0000 0000 0244 0000 ' + '0000 0000 02a4 0000 0000 0000 02cb 0000 0000 0000 0244 0000 ' + '0000 0000 0307 0000 0000 0000 0307 0000 0000 0000 037f 0000 ' + '0000 0000 0307 0000 0000 0000 0307 0000 0000 0000 0307 0000 ' + '0000 0000 0307 0000 0000 0000 0307 0000 0000 0000 03e3 0000 ' + '0000 0000 030c 0000 0000 0000 0307 0000 fe30 0000 0000 0000 ' + 'fe7e 0000 0000 0000 fe91 0000 0000 0000 fe6a 0000 0000 0000 ' + 'fe6a 0000 0000 0000 fecb 0000 0000 0000 fe6a 0000 0000 0000 ' + 'fe7e 0000 0000 0000 fea4 0000 0000 0000 fe7e 0000 0000 0000 ' + 'fe44 0000 0000 0000 fea4 0000 0000 0000 feb8 0000 0000 0000 ' + 'fe7e 0000 0000 0000 fe5e 0000 0000 0000 fe37 0000 0000 0000 ' + 'fe37 0000 0000 0000 fcbd 0000 0000 0000 fd84 0000 0000 0000 ' + 'fd98 0000 0000 0000 fd82 0000 0000 0000 fcbd 0000 0000 0000 ' + 'fd84 0000 0000 0000 fcbd 0000 0000 0000 fcbd 0000 0000 0000 ' + 'fe72 0000 0000 0000 ff9d 0000 0000 0000 0000 0000 032f 0000 ' + '0000 0000 03ba 0000 ' +) +assert len(OPBD_APPLE_CHANCERY_DATA) == 800 + + +class OPBDTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.maxDiff = None + glyphs = ['.notdef'] + ['X.alt%d' for g in range(1, 50)] + glyphs[10] = 'C' + glyphs[43] = 'A' + cls.font = FakeFont(glyphs) + + def test_decompile_toXML_format0(self): + table = newTable('opbd') + table.decompile(OPBD_FORMAT_0_DATA, self.font) + self.assertEqual(getXML(table.toXML), OPBD_FORMAT_0_XML) + + def test_compile_fromXML_format0(self): + table = newTable('opbd') + for name, attrs, content in parseXML(OPBD_FORMAT_0_XML): + table.fromXML(name, attrs, content, font=self.font) + self.assertEqual(hexStr(table.compile(self.font)), + hexStr(OPBD_FORMAT_0_DATA)) + + def test_decompile_toXML_format1(self): + table = newTable('opbd') + table.decompile(OPBD_FORMAT_1_DATA, self.font) + self.assertEqual(getXML(table.toXML), OPBD_FORMAT_1_XML) + + def test_compile_fromXML_format1(self): + table = newTable('opbd') + for name, attrs, content in parseXML(OPBD_FORMAT_1_XML): + table.fromXML(name, attrs, content, font=self.font) + self.assertEqual(hexStr(table.compile(self.font)), + hexStr(OPBD_FORMAT_1_DATA)) + + def test_decompile_AppleChancery(self): + # Make sure we do not crash when decompiling the 'opbd' table of + # AppleChancery.ttf. https://github.com/fonttools/fonttools/issues/1031 + table = newTable('opbd') + table.decompile(OPBD_APPLE_CHANCERY_DATA, self.font) + self.assertIn('', getXML(table.toXML)) + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_p_r_o_p_test.py b/Tests/ttLib/tables/_p_r_o_p_test.py new file mode 100644 index 0000000..edac915 --- /dev/null +++ b/Tests/ttLib/tables/_p_r_o_p_test.py @@ -0,0 +1,84 @@ +from __future__ import print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc.testTools import FakeFont, getXML, parseXML +from fontTools.misc.textTools import deHexStr, hexStr +from fontTools.ttLib import newTable +import unittest + + +PROP_FORMAT_0_DATA = deHexStr( + '0001 0000 0000 ' # 0: Version=1.0, Format=0 + '0005 ' # 6: DefaultProperties=European number terminator +) # 8: +assert(len(PROP_FORMAT_0_DATA) == 8) + + +PROP_FORMAT_0_XML = [ + '', + '', + ' ', + '', +] + + +PROP_FORMAT_1_DATA = deHexStr( + '0003 0000 0001 ' # 0: Version=3.0, Format=1 + '0000 ' # 6: DefaultProperties=left-to-right; non-whitespace + '0008 0003 0004 ' # 8: LookupFormat=8, FirstGlyph=3, GlyphCount=4 + '000B ' # 14: Properties[C]=other neutral + '000A ' # 16: Properties[D]=whitespace + '600B ' # 18: Properties[E]=other neutral; hanging punct + '0005 ' # 20: Properties[F]=European number terminator +) # 22: +assert(len(PROP_FORMAT_1_DATA) == 22) + + +PROP_FORMAT_1_XML = [ + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +class PROPTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.maxDiff = None + cls.font = FakeFont(['.notdef', 'A', 'B', 'C', 'D', 'E', 'F', 'G']) + + def test_decompile_toXML_format0(self): + table = newTable('prop') + table.decompile(PROP_FORMAT_0_DATA, self.font) + self.assertEqual(getXML(table.toXML), PROP_FORMAT_0_XML) + + def test_compile_fromXML_format0(self): + table = newTable('prop') + for name, attrs, content in parseXML(PROP_FORMAT_0_XML): + table.fromXML(name, attrs, content, font=self.font) + self.assertEqual(hexStr(table.compile(self.font)), + hexStr(PROP_FORMAT_0_DATA)) + + def test_decompile_toXML_format1(self): + table = newTable('prop') + table.decompile(PROP_FORMAT_1_DATA, self.font) + self.assertEqual(getXML(table.toXML), PROP_FORMAT_1_XML) + + def test_compile_fromXML_format1(self): + table = newTable('prop') + for name, attrs, content in parseXML(PROP_FORMAT_1_XML): + table.fromXML(name, attrs, content, font=self.font) + self.assertEqual(hexStr(table.compile(self.font)), + hexStr(PROP_FORMAT_1_DATA)) + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_t_r_a_k_test.py b/Tests/ttLib/tables/_t_r_a_k_test.py new file mode 100644 index 0000000..c223c81 --- /dev/null +++ b/Tests/ttLib/tables/_t_r_a_k_test.py @@ -0,0 +1,341 @@ +from __future__ import print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc.testTools import parseXML, getXML +from fontTools.misc.textTools import deHexStr +from fontTools.ttLib import TTFont, TTLibError +from fontTools.ttLib.tables._t_r_a_k import * +from fontTools.ttLib.tables._n_a_m_e import table__n_a_m_e, NameRecord +import unittest + + +# /Library/Fonts/Osaka.ttf from OSX has trak table with both horiz and vertData +OSAKA_TRAK_TABLE_DATA = deHexStr( + '00 01 00 00 00 00 00 0c 00 40 00 00 00 03 00 02 00 00 00 2c ff ff ' + '00 00 01 06 00 34 00 00 00 00 01 07 00 38 00 01 00 00 01 08 00 3c ' + '00 0c 00 00 00 18 00 00 ff f4 ff f4 00 00 00 00 00 0c 00 0c 00 03 ' + '00 02 00 00 00 60 ff ff 00 00 01 09 00 68 00 00 00 00 01 0a 00 6c ' + '00 01 00 00 01 0b 00 70 00 0c 00 00 00 18 00 00 ff f4 ff f4 00 00 ' + '00 00 00 0c 00 0c') + +# decompiled horizData and vertData entries from Osaka.ttf +OSAKA_HORIZ_TRACK_ENTRIES = { + -1.0: TrackTableEntry({24.0: -12, 12.0: -12}, nameIndex=262), + 0.0: TrackTableEntry({24.0: 0, 12.0: 0}, nameIndex=263), + 1.0: TrackTableEntry({24.0: 12, 12.0: 12}, nameIndex=264) + } + +OSAKA_VERT_TRACK_ENTRIES = { + -1.0: TrackTableEntry({24.0: -12, 12.0: -12}, nameIndex=265), + 0.0: TrackTableEntry({24.0: 0, 12.0: 0}, nameIndex=266), + 1.0: TrackTableEntry({24.0: 12, 12.0: 12}, nameIndex=267) + } + +OSAKA_TRAK_TABLE_XML = [ + '', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + +# made-up table containing only vertData (no horizData) +OSAKA_VERT_ONLY_TRAK_TABLE_DATA = deHexStr( + '00 01 00 00 00 00 00 00 00 0c 00 00 00 03 00 02 00 00 00 2c ff ff ' + '00 00 01 09 00 34 00 00 00 00 01 0a 00 38 00 01 00 00 01 0b 00 3c ' + '00 0c 00 00 00 18 00 00 ff f4 ff f4 00 00 00 00 00 0c 00 0c') + +OSAKA_VERT_ONLY_TRAK_TABLE_XML = [ + '', + '', + '', + ' ', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', +] + + +# also /Library/Fonts/Skia.ttf contains a trak table with horizData +SKIA_TRAK_TABLE_DATA = deHexStr( + '00 01 00 00 00 00 00 0c 00 00 00 00 00 03 00 05 00 00 00 2c ff ff ' + '00 00 01 13 00 40 00 00 00 00 01 2f 00 4a 00 01 00 00 01 14 00 54 ' + '00 09 00 00 00 0a 00 00 00 0c 00 00 00 12 00 00 00 13 00 00 ff f6 ' + 'ff e2 ff c4 ff c1 ff c1 00 0f 00 00 ff fb ff e7 ff e7 00 8c 00 82 ' + '00 7d 00 73 00 73') + +SKIA_TRACK_ENTRIES = { + -1.0: TrackTableEntry( + {9.0: -10, 10.0: -30, 19.0: -63, 12.0: -60, 18.0: -63}, nameIndex=275), + 0.0: TrackTableEntry( + {9.0: 15, 10.0: 0, 19.0: -25, 12.0: -5, 18.0: -25}, nameIndex=303), + 1.0: TrackTableEntry( + {9.0: 140, 10.0: 130, 19.0: 115, 12.0: 125, 18.0: 115}, nameIndex=276) + } + +SKIA_TRAK_TABLE_XML = [ + '', + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', + '', + ' ', + '', +] + + +class TrackingTableTest(unittest.TestCase): + + def __init__(self, methodName): + unittest.TestCase.__init__(self, methodName) + # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, + # and fires deprecation warnings if a program uses the old name. + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + + def setUp(self): + table = table__t_r_a_k() + table.version = 1.0 + table.format = 0 + self.font = {'trak': table} + + def test_compile_horiz(self): + table = self.font['trak'] + table.horizData = TrackData(SKIA_TRACK_ENTRIES) + trakData = table.compile(self.font) + self.assertEqual(trakData, SKIA_TRAK_TABLE_DATA) + + def test_compile_vert(self): + table = self.font['trak'] + table.vertData = TrackData(OSAKA_VERT_TRACK_ENTRIES) + trakData = table.compile(self.font) + self.assertEqual(trakData, OSAKA_VERT_ONLY_TRAK_TABLE_DATA) + + def test_compile_horiz_and_vert(self): + table = self.font['trak'] + table.horizData = TrackData(OSAKA_HORIZ_TRACK_ENTRIES) + table.vertData = TrackData(OSAKA_VERT_TRACK_ENTRIES) + trakData = table.compile(self.font) + self.assertEqual(trakData, OSAKA_TRAK_TABLE_DATA) + + def test_compile_longword_aligned(self): + table = self.font['trak'] + # without padding, this 'horizData' would end up 46 byte long + table.horizData = TrackData({ + 0.0: TrackTableEntry(nameIndex=256, values={12.0: 0, 24.0: 0, 36.0: 0}) + }) + table.vertData = TrackData({ + 0.0: TrackTableEntry(nameIndex=257, values={12.0: 0, 24.0: 0, 36.0: 0}) + }) + trakData = table.compile(self.font) + self.assertTrue(table.vertOffset % 4 == 0) + + def test_compile_sizes_mismatch(self): + table = self.font['trak'] + table.horizData = TrackData({ + -1.0: TrackTableEntry(nameIndex=256, values={9.0: -10, 10.0: -30}), + 0.0: TrackTableEntry(nameIndex=257, values={8.0: 20, 12.0: 0}) + }) + with self.assertRaisesRegex(TTLibError, 'entries must specify the same sizes'): + table.compile(self.font) + + def test_decompile_horiz(self): + table = self.font['trak'] + table.decompile(SKIA_TRAK_TABLE_DATA, self.font) + self.assertEqual(table.horizData, SKIA_TRACK_ENTRIES) + self.assertEqual(table.vertData, TrackData()) + + def test_decompile_vert(self): + table = self.font['trak'] + table.decompile(OSAKA_VERT_ONLY_TRAK_TABLE_DATA, self.font) + self.assertEqual(table.horizData, TrackData()) + self.assertEqual(table.vertData, OSAKA_VERT_TRACK_ENTRIES) + + def test_decompile_horiz_and_vert(self): + table = self.font['trak'] + table.decompile(OSAKA_TRAK_TABLE_DATA, self.font) + self.assertEqual(table.horizData, OSAKA_HORIZ_TRACK_ENTRIES) + self.assertEqual(table.vertData, OSAKA_VERT_TRACK_ENTRIES) + + def test_roundtrip_decompile_compile(self): + for trakData in ( + OSAKA_TRAK_TABLE_DATA, + OSAKA_VERT_ONLY_TRAK_TABLE_DATA, + SKIA_TRAK_TABLE_DATA): + table = table__t_r_a_k() + table.decompile(trakData, ttFont=None) + newTrakData = table.compile(ttFont=None) + self.assertEqual(trakData, newTrakData) + + def test_fromXML_horiz(self): + table = self.font['trak'] + for name, attrs, content in parseXML(SKIA_TRAK_TABLE_XML): + table.fromXML(name, attrs, content, self.font) + self.assertEqual(table.version, 1.0) + self.assertEqual(table.format, 0) + self.assertEqual(table.horizData, SKIA_TRACK_ENTRIES) + self.assertEqual(table.vertData, TrackData()) + + def test_fromXML_horiz_and_vert(self): + table = self.font['trak'] + for name, attrs, content in parseXML(OSAKA_TRAK_TABLE_XML): + table.fromXML(name, attrs, content, self.font) + self.assertEqual(table.version, 1.0) + self.assertEqual(table.format, 0) + self.assertEqual(table.horizData, OSAKA_HORIZ_TRACK_ENTRIES) + self.assertEqual(table.vertData, OSAKA_VERT_TRACK_ENTRIES) + + def test_fromXML_vert(self): + table = self.font['trak'] + for name, attrs, content in parseXML(OSAKA_VERT_ONLY_TRAK_TABLE_XML): + table.fromXML(name, attrs, content, self.font) + self.assertEqual(table.version, 1.0) + self.assertEqual(table.format, 0) + self.assertEqual(table.horizData, TrackData()) + self.assertEqual(table.vertData, OSAKA_VERT_TRACK_ENTRIES) + + def test_toXML_horiz(self): + table = self.font['trak'] + table.horizData = TrackData(SKIA_TRACK_ENTRIES) + add_name(self.font, 'Tight', nameID=275) + add_name(self.font, 'Normal', nameID=303) + add_name(self.font, 'Loose', nameID=276) + self.assertEqual( + SKIA_TRAK_TABLE_XML, + getXML(table.toXML, self.font)) + + def test_toXML_horiz_and_vert(self): + table = self.font['trak'] + table.horizData = TrackData(OSAKA_HORIZ_TRACK_ENTRIES) + table.vertData = TrackData(OSAKA_VERT_TRACK_ENTRIES) + add_name(self.font, 'Tight', nameID=262) + add_name(self.font, 'Normal', nameID=263) + add_name(self.font, 'Loose', nameID=264) + add_name(self.font, 'Tight', nameID=265) + add_name(self.font, 'Normal', nameID=266) + add_name(self.font, 'Loose', nameID=267) + self.assertEqual( + OSAKA_TRAK_TABLE_XML, + getXML(table.toXML, self.font)) + + def test_toXML_vert(self): + table = self.font['trak'] + table.vertData = TrackData(OSAKA_VERT_TRACK_ENTRIES) + add_name(self.font, 'Tight', nameID=265) + add_name(self.font, 'Normal', nameID=266) + add_name(self.font, 'Loose', nameID=267) + self.assertEqual( + OSAKA_VERT_ONLY_TRAK_TABLE_XML, + getXML(table.toXML, self.font)) + + def test_roundtrip_fromXML_toXML(self): + font = {} + add_name(font, 'Tight', nameID=275) + add_name(font, 'Normal', nameID=303) + add_name(font, 'Loose', nameID=276) + add_name(font, 'Tight', nameID=262) + add_name(font, 'Normal', nameID=263) + add_name(font, 'Loose', nameID=264) + add_name(font, 'Tight', nameID=265) + add_name(font, 'Normal', nameID=266) + add_name(font, 'Loose', nameID=267) + for input_xml in ( + SKIA_TRAK_TABLE_XML, + OSAKA_TRAK_TABLE_XML, + OSAKA_VERT_ONLY_TRAK_TABLE_XML): + table = table__t_r_a_k() + font['trak'] = table + for name, attrs, content in parseXML(input_xml): + table.fromXML(name, attrs, content, font) + output_xml = getXML(table.toXML, font) + self.assertEqual(input_xml, output_xml) + + +def add_name(font, string, nameID): + nameTable = font.get("name") + if nameTable is None: + nameTable = font["name"] = table__n_a_m_e() + nameTable.names = [] + namerec = NameRecord() + namerec.nameID = nameID + namerec.string = string.encode('mac_roman') + namerec.platformID, namerec.platEncID, namerec.langID = (1, 0, 0) + nameTable.names.append(namerec) + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_v_h_e_a_test.py b/Tests/ttLib/tables/_v_h_e_a_test.py new file mode 100644 index 0000000..8979e92 --- /dev/null +++ b/Tests/ttLib/tables/_v_h_e_a_test.py @@ -0,0 +1,275 @@ +from __future__ import absolute_import, unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc.loggingTools import CapturingLogHandler +from fontTools.misc.testTools import parseXML, getXML +from fontTools.misc.textTools import deHexStr +from fontTools.ttLib import TTFont, newTable +from fontTools.misc.fixedTools import log +import os +import unittest + + +CURR_DIR = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) +DATA_DIR = os.path.join(CURR_DIR, 'data') + +VHEA_DATA_VERSION_11 = deHexStr( + '0001 1000 ' # 1.1 version + '01F4 ' # 500 ascent + 'FE0C ' # -500 descent + '0000 ' # 0 lineGap + '0BB8 ' # 3000 advanceHeightMax + 'FC16 ' # -1002 minTopSideBearing + 'FD5B ' # -677 minBottomSideBearing + '0B70 ' # 2928 yMaxExtent + '0000 ' # 0 caretSlopeRise + '0001 ' # 1 caretSlopeRun + '0000 ' # 0 caretOffset + '0000 ' # 0 reserved1 + '0000 ' # 0 reserved2 + '0000 ' # 0 reserved3 + '0000 ' # 0 reserved4 + '0000 ' # 0 metricDataFormat + '000C ' # 12 numberOfVMetrics +) + +VHEA_DATA_VERSION_10 = deHexStr('00010000') + VHEA_DATA_VERSION_11[4:] + +VHEA_VERSION_11_AS_DICT = { + 'tableTag': 'vhea', + 'tableVersion': 0x00011000, + 'ascent': 500, + 'descent': -500, + 'lineGap': 0, + 'advanceHeightMax': 3000, + 'minTopSideBearing': -1002, + 'minBottomSideBearing': -677, + 'yMaxExtent': 2928, + 'caretSlopeRise': 0, + 'caretSlopeRun': 1, + 'caretOffset': 0, + 'reserved1': 0, + 'reserved2': 0, + 'reserved3': 0, + 'reserved4': 0, + 'metricDataFormat': 0, + 'numberOfVMetrics': 12, +} + +VHEA_VERSION_10_AS_DICT = dict(VHEA_VERSION_11_AS_DICT) +VHEA_VERSION_10_AS_DICT['tableVersion'] = 0x00010000 + +VHEA_XML_VERSION_11 = [ + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', +] + +VHEA_XML_VERSION_11_AS_FLOAT = [ + '', +] + VHEA_XML_VERSION_11[1:] + +VHEA_XML_VERSION_10 = [ + '', +] + VHEA_XML_VERSION_11[1:] + +VHEA_XML_VERSION_10_AS_FLOAT = [ + '', +] + VHEA_XML_VERSION_11[1:] + + +class VheaCompileOrToXMLTest(unittest.TestCase): + + def setUp(self): + vhea = newTable('vhea') + vhea.tableVersion = 0x00010000 + vhea.ascent = 500 + vhea.descent = -500 + vhea.lineGap = 0 + vhea.advanceHeightMax = 3000 + vhea.minTopSideBearing = -1002 + vhea.minBottomSideBearing = -677 + vhea.yMaxExtent = 2928 + vhea.caretSlopeRise = 0 + vhea.caretSlopeRun = 1 + vhea.caretOffset = 0 + vhea.metricDataFormat = 0 + vhea.numberOfVMetrics = 12 + vhea.reserved1 = vhea.reserved2 = vhea.reserved3 = vhea.reserved4 = 0 + self.font = TTFont(sfntVersion='OTTO') + self.font['vhea'] = vhea + + def test_compile_caretOffset_as_reserved0(self): + vhea = self.font['vhea'] + del vhea.caretOffset + vhea.reserved0 = 0 + self.assertEqual(VHEA_DATA_VERSION_10, vhea.compile(self.font)) + + def test_compile_version_10(self): + vhea = self.font['vhea'] + vhea.tableVersion = 0x00010000 + self.assertEqual(VHEA_DATA_VERSION_10, vhea.compile(self.font)) + + def test_compile_version_10_as_float(self): + vhea = self.font['vhea'] + vhea.tableVersion = 1.0 + with CapturingLogHandler(log, "WARNING") as captor: + self.assertEqual(VHEA_DATA_VERSION_10, vhea.compile(self.font)) + self.assertTrue( + len([r for r in captor.records + if "Table version value is a float" in r.msg]) == 1) + + def test_compile_version_11(self): + vhea = self.font['vhea'] + vhea.tableVersion = 0x00011000 + self.assertEqual(VHEA_DATA_VERSION_11, vhea.compile(self.font)) + + def test_compile_version_11_as_float(self): + vhea = self.font['vhea'] + vhea.tableVersion = 1.0625 + with CapturingLogHandler(log, "WARNING") as captor: + self.assertEqual(VHEA_DATA_VERSION_11, vhea.compile(self.font)) + self.assertTrue( + len([r for r in captor.records + if "Table version value is a float" in r.msg]) == 1) + + def test_toXML_caretOffset_as_reserved0(self): + vhea = self.font['vhea'] + del vhea.caretOffset + vhea.reserved0 = 0 + self.assertEqual(getXML(vhea.toXML), VHEA_XML_VERSION_10) + + def test_toXML_version_10(self): + vhea = self.font['vhea'] + self.font['vhea'].tableVersion = 0x00010000 + self.assertEqual(getXML(vhea.toXML), VHEA_XML_VERSION_10) + + def test_toXML_version_10_as_float(self): + vhea = self.font['vhea'] + vhea.tableVersion = 1.0 + with CapturingLogHandler(log, "WARNING") as captor: + self.assertEqual(getXML(vhea.toXML), VHEA_XML_VERSION_10) + self.assertTrue( + len([r for r in captor.records + if "Table version value is a float" in r.msg]) == 1) + + def test_toXML_version_11(self): + vhea = self.font['vhea'] + self.font['vhea'].tableVersion = 0x00011000 + self.assertEqual(getXML(vhea.toXML), VHEA_XML_VERSION_11) + + def test_toXML_version_11_as_float(self): + vhea = self.font['vhea'] + vhea.tableVersion = 1.0625 + with CapturingLogHandler(log, "WARNING") as captor: + self.assertEqual(getXML(vhea.toXML), VHEA_XML_VERSION_11) + self.assertTrue( + len([r for r in captor.records + if "Table version value is a float" in r.msg]) == 1) + + +class VheaDecompileOrFromXMLTest(unittest.TestCase): + + def setUp(self): + vhea = newTable('vhea') + self.font = TTFont(sfntVersion='OTTO') + self.font['vhea'] = vhea + + def test_decompile_version_10(self): + vhea = self.font['vhea'] + vhea.decompile(VHEA_DATA_VERSION_10, self.font) + for key in vhea.__dict__: + self.assertEqual(getattr(vhea, key), VHEA_VERSION_10_AS_DICT[key]) + + def test_decompile_version_11(self): + vhea = self.font['vhea'] + vhea.decompile(VHEA_DATA_VERSION_11, self.font) + for key in vhea.__dict__: + self.assertEqual(getattr(vhea, key), VHEA_VERSION_11_AS_DICT[key]) + + def test_fromXML_version_10(self): + vhea = self.font['vhea'] + for name, attrs, content in parseXML(VHEA_XML_VERSION_10): + vhea.fromXML(name, attrs, content, self.font) + for key in vhea.__dict__: + self.assertEqual(getattr(vhea, key), VHEA_VERSION_10_AS_DICT[key]) + + def test_fromXML_version_10_as_float(self): + vhea = self.font['vhea'] + with CapturingLogHandler(log, "WARNING") as captor: + for name, attrs, content in parseXML(VHEA_XML_VERSION_10_AS_FLOAT): + vhea.fromXML(name, attrs, content, self.font) + self.assertTrue( + len([r for r in captor.records + if "Table version value is a float" in r.msg]) == 1) + for key in vhea.__dict__: + self.assertEqual(getattr(vhea, key), VHEA_VERSION_10_AS_DICT[key]) + + def test_fromXML_version_11(self): + vhea = self.font['vhea'] + for name, attrs, content in parseXML(VHEA_XML_VERSION_11): + vhea.fromXML(name, attrs, content, self.font) + for key in vhea.__dict__: + self.assertEqual(getattr(vhea, key), VHEA_VERSION_11_AS_DICT[key]) + + def test_fromXML_version_11_as_float(self): + vhea = self.font['vhea'] + with CapturingLogHandler(log, "WARNING") as captor: + for name, attrs, content in parseXML(VHEA_XML_VERSION_11_AS_FLOAT): + vhea.fromXML(name, attrs, content, self.font) + self.assertTrue( + len([r for r in captor.records + if "Table version value is a float" in r.msg]) == 1) + for key in vhea.__dict__: + self.assertEqual(getattr(vhea, key), VHEA_VERSION_11_AS_DICT[key]) + + +class VheaRecalcTest(unittest.TestCase): + + def test_recalc_TTF(self): + font = TTFont() + font.importXML(os.path.join(DATA_DIR, '_v_h_e_a_recalc_TTF.ttx')) + vhea = font['vhea'] + vhea.recalc(font) + self.assertEqual(vhea.advanceHeightMax, 900) + self.assertEqual(vhea.minTopSideBearing, 200) + self.assertEqual(vhea.minBottomSideBearing, 377) + self.assertEqual(vhea.yMaxExtent, 312) + + def test_recalc_OTF(self): + font = TTFont() + font.importXML(os.path.join(DATA_DIR, '_v_h_e_a_recalc_OTF.ttx')) + vhea = font['vhea'] + vhea.recalc(font) + self.assertEqual(vhea.advanceHeightMax, 900) + self.assertEqual(vhea.minTopSideBearing, 200) + self.assertEqual(vhea.minBottomSideBearing, 377) + self.assertEqual(vhea.yMaxExtent, 312) + + def test_recalc_empty(self): + font = TTFont() + font.importXML(os.path.join(DATA_DIR, '_v_h_e_a_recalc_empty.ttx')) + vhea = font['vhea'] + vhea.recalc(font) + self.assertEqual(vhea.advanceHeightMax, 900) + self.assertEqual(vhea.minTopSideBearing, 0) + self.assertEqual(vhea.minBottomSideBearing, 0) + self.assertEqual(vhea.yMaxExtent, 0) + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/_v_m_t_x_test.py b/Tests/ttLib/tables/_v_m_t_x_test.py new file mode 100644 index 0000000..f55dbec --- /dev/null +++ b/Tests/ttLib/tables/_v_m_t_x_test.py @@ -0,0 +1,19 @@ +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.misc.py23 import * +from fontTools.ttLib.tables._v_m_t_x import table__v_m_t_x +import _h_m_t_x_test +import unittest + + +class VmtxTableTest(_h_m_t_x_test.HmtxTableTest): + + @classmethod + def setUpClass(cls): + cls.tableClass = table__v_m_t_x + cls.tag = "vmtx" + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/data/C_F_F_.bin b/Tests/ttLib/tables/data/C_F_F_.bin new file mode 100644 index 0000000000000000000000000000000000000000..0099b3b687d0b062396439c278c2222a05d819e7 GIT binary patch literal 1167 zcmZQ%U}0ilWMmX`c5^cdNi8mM%g-w@^i3=-NiEVf0LjSwkYV^C%k)Ezx%;=sPx+sH ztluU1PyKo#^j%GmfrXKgS3tuqB|j%8u}Ci|CowtOP|pBlmIBBy1+Zxf5aSeZh%+!S zGO#i*2r)4zF)_9?FmN(4FfwE_N-?G|rZT26USOKQ^ox0gR`=c?Z}u{F@BN{&m$|!J z^hZZGYxmCp);}QnZvgA}z8~_UzwN$HX8r!6oAtNd@5!ty<3zuM1iJrBVg<|mj`@86 zBJ>?3|NBKZYwnNzq7V*9r2EGtR=)3@6GXkbQ)b2PbIk0XJgH|UNAY8i-R9jK`rVch-M<+)x~+GFJx}D=P{sP4EqrNg zw--mZo>z!rH^mNbYVAZ_YW`D(wVH?eG_a95vZr3iCPG^p_-K?FLyDoKKaM`ol@|chO%<)@9wS-_w6;uvRs)e$V+U$GWzg^>^h@ zOV;vkR=$q!*56Nwb}M%)cdCLN`?33T_jeVJ?n$iO-`To9_kQHa>}Ks%1*zcZ4q~1B z{e|eHZr1K)tiJ`i!4BtaVeS6QEczQnm$QBci7sPp>1Lhu{etL+Zr1N+-5313RXMs< z9fE!fc60oe=w|IMW&JMR&HCM-``V4}j~w0a-LL;P>*Z){X8o=>?ew+o`yAb`JWu^r z>*n~)4p#lSoAtYD_uimxdyZ}$2k+l9-5kbX+xd3=5St|W+q(NV|96q@@9`Yn-=n{C ze3$(0(amwCdvn#{G>)>vM%msZT~LM>c1{(u5~xZ@1XAA(!W)@f2VPD|4#ib z`dj6DcsIx8?jzIBZQTT*K+Rp6M1-+_w(@ns1y0|QWOB5Ao}$H literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/C_F_F_.ttx b/Tests/ttLib/tables/data/C_F_F_.ttx new file mode 100644 index 0000000..dd09577 --- /dev/null +++ b/Tests/ttLib/tables/data/C_F_F_.ttx @@ -0,0 +1,282 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -97 0 50 600 50 hstem + 0 50 400 50 vstem + 0 0 rmoveto + 500 0 rlineto + 0 700 rlineto + -500 0 rlineto + 0 -700 rlineto + 250 395 rmoveto + -170 255 rlineto + 340 0 rlineto + -170 -255 rlineto + 30 -45 rmoveto + 170 255 rlineto + 0 -510 rlineto + -170 255 rlineto + -200 -300 rmoveto + 170 255 rlineto + 170 -255 rlineto + -340 0 rlineto + -30 555 rmoveto + 170 -255 rlineto + -170 -255 rlineto + 0 510 rlineto + endchar + + + 56 523 26 rmoveto + -120 -6 rlineto + 0 -20 rlineto + 248 0 rlineto + 0 20 rlineto + -114 6 rlineto + -14 0 rlineto + -424 0 rmoveto + -87 -6 rlineto + 0 -20 rlineto + 198 0 rlineto + 0 20 rlineto + -97 6 rlineto + -14 0 rlineto + 369 221 rmoveto + -8 20 rlineto + -278 0 rlineto + -9 -20 rlineto + 295 0 rlineto + -161 430 rmoveto + -222 -677 rlineto + 27 0 rlineto + 211 660 rlineto + -17 -10 rlineto + 216 -650 rlineto + 34 0 rlineto + -229 677 rlineto + -20 0 rlineto + endchar + + + -3 167 310 rmoveto + 0 -104 0 -104 -2 -102 rrcurveto + 34 0 rlineto + -2 102 0 104 0 144 rrcurveto + 0 7 rlineto + 0 114 0 104 2 102 rrcurveto + -34 0 rlineto + 2 -102 0 -104 0 -104 rrcurveto + 0 -57 rlineto + 8 340 rmoveto + 7 0 rlineto + 0 27 rlineto + -124 0 rlineto + 0 -20 rlineto + 117 -7 rlineto + 0 -623 rmoveto + -117 -7 rlineto + 0 -20 rlineto + 124 0 rlineto + 0 27 rlineto + -7 0 rlineto + 7 316 rmoveto + 101 0 rlineto + 162 0 69 -60 0 -102 rrcurveto + 0 -102 -75 -57 -125 0 rrcurveto + -132 0 rlineto + 0 -22 rlineto + 131 0 rlineto + 156 0 75 77 0 102 rrcurveto + 0 100 -68 76 -162 2 rrcurveto + -10 -8 rlineto + 141 11 64 75 0 84 rrcurveto + 0 95 -66 63 -146 0 rrcurveto + -115 0 rlineto + 0 -22 rlineto + 104 0 rlineto + 145 0 50 -57 0 -76 rrcurveto + 0 -95 -75 -64 -136 0 rrcurveto + -88 0 rlineto + 0 -20 rlineto + endchar + + + 47 386 7 rmoveto + -167 0 -123 128 0 203 rrcurveto + 0 199 116 133 180 0 rrcurveto + 73 0 40 -17 56 -37 rrcurveto + -21 29 rlineto + 18 -145 rlineto + 24 0 rlineto + -4 139 rlineto + -60 35 -49 18 -80 0 rrcurveto + -190 0 -135 -144 0 -210 rrcurveto + 0 -209 129 -144 195 0 rrcurveto + 72 0 57 12 67 41 rrcurveto + 4 139 rlineto + -24 0 rlineto + -18 -139 rlineto + 17 20 rlineto + -55 -37 -55 -14 -67 0 rrcurveto + endchar + + + 245 5 rmoveto + -65 0 -39 15 -46 50 rrcurveto + 36 -48 rlineto + -28 100 rlineto + -6 15 -10 5 -11 0 rrcurveto + -14 0 -8 -7 -1 -14 rrcurveto + 24 -85 61 -51 107 0 rrcurveto + 91 0 90 54 0 112 rrcurveto + 0 70 -26 66 -134 57 rrcurveto + -19 8 rlineto + -93 39 -42 49 0 68 rrcurveto + 0 91 60 48 88 0 rrcurveto + 56 0 35 -14 44 -50 rrcurveto + -38 47 rlineto + 28 -100 rlineto + 6 -15 10 -5 11 0 rrcurveto + 14 0 8 7 1 14 rrcurveto + -24 88 -67 48 -84 0 rrcurveto + -92 0 -82 -51 0 -108 rrcurveto + 0 -80 45 -53 92 -42 rrcurveto + 37 -17 rlineto + 114 -52 26 -46 0 -65 rrcurveto + 0 -93 -65 -55 -90 0 rrcurveto + 18 318 rmoveto + 0 439 rlineto + -22 0 rlineto + 0 -425 rlineto + 22 -14 rlineto + -20 -438 rmoveto + 22 0 rlineto + 0 438 rlineto + -22 14 rlineto + 0 -452 rlineto + endchar + + + 3 245 5 rmoveto + -65 0 -39 15 -46 50 rrcurveto + 36 -48 rlineto + -28 100 rlineto + -6 15 -10 5 -11 0 rrcurveto + -14 0 -8 -7 -1 -14 rrcurveto + 24 -85 61 -51 107 0 rrcurveto + 91 0 90 54 0 112 rrcurveto + 0 70 -26 66 -134 57 rrcurveto + -19 8 rlineto + -93 39 -42 49 0 68 rrcurveto + 0 91 60 48 88 0 rrcurveto + 56 0 35 -14 44 -50 rrcurveto + -38 47 rlineto + 28 -100 rlineto + 6 -15 10 -5 11 0 rrcurveto + 14 0 8 7 1 14 rrcurveto + -24 88 -67 48 -84 0 rrcurveto + -92 0 -82 -51 0 -108 rrcurveto + 0 -80 45 -53 92 -42 rrcurveto + 37 -17 rlineto + 114 -52 26 -46 0 -65 rrcurveto + 0 -93 -65 -55 -90 0 rrcurveto + 17 651 rmoveto + 1 106 rlineto + -22 0 rlineto + 1 -107 rlineto + 20 1 rlineto + -15 -784 rmoveto + 22 0 rlineto + -3 121 rlineto + -20 2 rlineto + 1 -123 rlineto + endchar + + + 91 618 rmoveto + 0 -20 rlineto + 155 35 rlineto + 0 -421 rlineto + 0 -70 -1 -71 -2 -72 rrcurveto + 34 0 rlineto + -2 72 -1 71 0 70 rrcurveto + 0 297 rlineto + 4 146 rlineto + -14 12 rlineto + -173 -49 rlineto + 176 -593 rmoveto + -14 0 rlineto + -170 -6 rlineto + 0 -20 rlineto + 344 0 rlineto + 0 20 rlineto + -160 6 rlineto + endchar + + + endchar + + + endchar + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/C_F_F__2.bin b/Tests/ttLib/tables/data/C_F_F__2.bin new file mode 100644 index 0000000000000000000000000000000000000000..faa0e919ba3e30a7339e268bdf395e2776839079 GIT binary patch literal 2312 zcmeYd3GruOVQ^qzVqkD~b2EAt^k_E&1H%yp1`d~u)Wj67wK|Fn3=BRD3=C$ui4_G5 z%mEAx3=Ay{3=C`q`NbtMsRx%cFfjBmFfdp!FfuSOFfusWu~%P?=ePOFz|8ytr1;8I zqX%Gm?$((M#{YYn448f~FfcHK>;#E3Fa&^j3^4xxtqevGaWs;dfq{XYk%>PgKPM-# zNG~tHxTGjQJC%unm4Qc=fq}h*N98-a-~tJ-wivK;c)+9&0}})50fy_nU>ODmh64=9 zJcy_Rg9BU?!iJ~;vB4@>!QNwHU}j(eIiHDvk>NG-2ez-bySt~hHueZ~i{35|NnWzK zN1&VahssZd`G@A$mCs$$-QC?Qz-m4J(B`6KkklWQzY66cH(yfJQSYhz<7*g(!5E$xOFlRqtLaaBWicXw-lcXxMRABY8#>lfg-8B&zk-Q7KJ(uDT5 zUIEtT2TkSO-Q7*IW;NCH3b3~AFP)#z($mw`)sqe~Yid_dPfImO{p#-S?g;|j9EJVU zA$Anh)~3#w2Qq#VNNPrRclX@I-QC^&0vz3jmm z?CI?T`KYM}Yz#;Cj8>4n-EH8|V)a>)JGVKvySuw{W_NdYV|{mb_ncK=Cw1?hJ%4gr zGsx6a+m@V|zPz=6;>Cv<=UaH_|oxqY1-O<6^=m-Gs-PIK5$yQ!zGtEXqt z@t&@(`huRWuI?#aJv~+VT|GUZ09@Kq4+>s^?iI5pY^g8n?CEW5?&<34YwzvqEHCKl z=>Y}cee?ald0UG?-U9izf6nB#=E?2d-QDvyG*6feGUm6@wzH4`G_&png;@6rkZOT$ zl^#%NfPAWSI^;WN)C5q9@2mi+?d|UFt_P*^>EI|6;8;~RaUv+1K_>SKuwKqxUNK`U zNT9U4yL)nHcXxL~0od2wKHZ&deG@>YMx|tU7A17{^wt)3_Vl(?)J>UDT?`GsGvAkI z#xzcxI(`5Ay7FED(e59n)24xJ7GQN*wP${PIVi(*|1oUHgt3c|N8Yj$oA|AQJ@a`YnA5oC)Tx0p+oIojtwXp!8qe3e5zw zz?q=41swmZAxjGvRHycLw0Fz^MN)NVZ|}_I{hjUo0^K|2ErMo(;~SS8n6;|4Z{oa# z9ld?i7tL<0Ua+pW6PyXQ)y?SY=@}ZCK!5WoaMI{Dx9$O@Zp=)ed?xBUODMQ3>Hwv! zHc(n`0kP(Q3bF|T9II-;nV=Vx+W$3<`^SaxG|SvDvWeCKYjt)D!- z2PG5y=I)<4y}2GG6S)1&UcPH;Yhy`V{p9J;OfUmf5wUiE7wxQUoH!L~Mja$0{TA(= z3$E(GRUsoI=l4)hz1u8QKL1Dc?CG;7%)UH(`ut7vCrs*|FsZGjyRD_Js=KbLqocc{ zWBI1;<(t5)3H{v@`oXN}GrFhG0JAzfyE{9|08$q+F4U(+?_Rb{^t1{zk8>0$}KRD=i%(;;l5fCU7R1?BhK|b=r_}Evu+U4 nBhGz%W_faHesWK9XLob2I1kVFps64kuyl88cXw;AI1euXPO}RD literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/C_F_F__2.ttx b/Tests/ttLib/tables/data/C_F_F__2.ttx new file mode 100644 index 0000000..c86252b --- /dev/null +++ b/Tests/ttLib/tables/data/C_F_F__2.ttx @@ -0,0 +1,395 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 80 0 0 10 -6 -10 1 blend + 0 rmoveto + 80 -20 -55 -40 25 40 1 blend + 0 rlineto + 400 652 20 55 20 -13 -20 18 25 0 0 0 2 blend + rlineto + -80 20 55 40 -25 -40 1 blend + 0 rlineto + -400 -652 -20 -55 -20 13 20 -18 -25 0 0 0 2 blend + rlineto + 480 0 0 -20 12 20 1 blend + 0 rmoveto + -400 652 -20 -55 -20 13 20 18 25 0 0 0 2 blend + rlineto + -80 20 55 40 -25 -40 1 blend + 0 rlineto + 400 -652 20 55 20 -13 -20 -18 -25 0 0 0 2 blend + rlineto + 80 -20 -55 -40 25 40 1 blend + 0 rlineto + -410 60 -10 -45 10 -6 -10 -10 -38 0 0 0 2 blend + rmoveto + 0 532 38 101 0 0 0 1 blend + rlineto + 340 20 90 0 0 0 1 blend + 0 rlineto + 0 -532 -38 -101 0 0 0 1 blend + rlineto + -340 -20 -90 0 0 0 1 blend + 0 rlineto + -70 -60 10 45 0 0 0 10 38 0 0 0 2 blend + rmoveto + 480 0 rlineto + 0 652 18 25 0 0 0 1 blend + rlineto + -480 0 rlineto + 0 -652 -18 -25 0 0 0 1 blend + rlineto + + + 260 39 -12 -15 0 0 0 -4 -32 -20 13 20 2 blend + rmoveto + -65 26 0 0 0 0 1 blend + 0 -28 11 -49 24 -17 -11 0 0 0 -6 4 0 0 0 3 3 0 0 0 -6 26 0 0 0 4 blend + rrcurveto + 78 -55 -25 -42 0 0 0 19 7 5 -4 -5 2 blend + rlineto + -8 85 -9 -20 0 0 0 -9 15 15 -9 -15 2 blend + rlineto + -5 52 -22 20 -43 -7 1 1 -1 -1 1 -36 0 0 0 0 10 -1 1 1 -7 -16 0 0 0 19 32 0 0 0 5 blend + 0 rrcurveto + -26 4 12 0 0 0 1 blend + 0 -27 -14 -14 -38 13 19 0 0 0 3 7 0 0 0 5 13 0 0 0 18 24 0 0 0 4 blend + rrcurveto + 0 -90 71 -50 139 4 24 0 0 0 3 5 0 0 0 10 -10 0 0 0 -9 -1 0 0 0 -32 -32 0 0 0 5 blend + 0 rrcurveto + 163 -27 -72 0 0 0 1 blend + 0 99 84 -17 -9 0 0 0 -8 -31 2 -1 -2 2 blend + 0 108 -1 3 0 0 0 1 blend + rrcurveto + 0 107 -56 54 -138 56 -25 -37 0 0 0 15 30 0 0 0 11 12 -2 1 2 3 4 0 0 0 -9 1 0 0 0 5 blend + rrcurveto + -32 13 -6 13 0 0 0 0 -5 0 0 0 2 blend + rlineto + -63 25 -30 18 -8 -30 0 0 0 -2 14 0 0 0 -10 -12 0 0 0 17 31 0 0 0 4 blend + 0 48 16 20 8 -5 -8 1 blend + rrcurveto + 0 63 43 25 61 12 28 -6 4 6 14 17 -2 1 2 12 23 18 -12 -18 13 27 2 -1 -2 4 blend + 0 rrcurveto + 42 -12 14 0 0 0 1 blend + 0 27 -4 52 -24 9 8 0 0 0 -1 -10 0 0 0 -10 -8 0 0 0 7 -26 0 0 0 4 blend + rrcurveto + -85 47 33 47 -3 2 3 -11 0 5 -3 -5 2 blend + rlineto + 10 -67 7 18 3 -2 -3 -9 -33 -25 16 25 2 blend + rlineto + 11 -75 37 -14 39 1 -5 -1 1 1 23 60 1 -1 -1 -12 -27 1 -1 -1 0 9 -1 1 1 -17 -28 -1 1 1 5 blend + 0 rrcurveto + 26 -7 -12 1 -1 -1 1 blend + 0 29 15 5 41 -12 -21 -2 1 2 -5 -8 1 -1 -1 3 -4 2 -1 -2 -20 -27 -1 1 1 4 blend + rrcurveto + 0 84 -84 52 -121 -6 -24 0 0 0 2 4 0 0 0 4 17 8 -5 -8 8 -4 0 0 0 20 37 -8 5 8 5 blend + 0 rrcurveto + -158 43 66 0 0 0 1 blend + 0 -85 -80 2 3 0 0 0 0 29 0 0 0 2 blend + 0 -103 1 -5 0 0 0 1 blend + rrcurveto + 0 -105 64 -55 117 -49 5 25 0 0 0 -2 -19 0 0 0 1 2 0 0 0 -12 -25 0 0 0 12 7 0 0 0 5 blend + rrcurveto + 31 -13 6 6 0 0 0 0 -4 0 0 0 2 blend + rlineto + 72 -30 28 -19 13 42 0 0 0 0 -22 0 0 0 8 -2 0 0 0 -11 -27 0 0 0 4 blend + 0 -63 0 -2 -5 3 5 1 blend + rrcurveto + 0 -49 -39 -35 -66 -25 -43 -2 1 2 -14 -26 -2 1 2 -7 -19 -13 9 13 -16 -24 2 -1 -2 4 blend + 0 rrcurveto + 65 275 -34 -47 -10 6 10 12 52 20 -13 -20 2 blend + rmoveto + 0 417 11 11 0 0 0 1 blend + rlineto + -71 31 49 20 -12 -20 1 blend + 0 rlineto + 0 -417 -11 -11 0 0 0 1 blend + rlineto + 71 -31 -49 -20 12 20 1 blend + 0 rlineto + -79 -429 38 57 20 -12 -20 -8 -20 0 0 0 2 blend + rmoveto + 71 -31 -49 -20 12 20 1 blend + 0 rlineto + 0 429 8 20 0 0 0 1 blend + rlineto + -71 31 49 20 -12 -20 1 blend + 0 rlineto + 0 -429 -8 -20 0 0 0 1 blend + rlineto + + + 260 39 -12 -15 0 0 0 -4 -32 -20 13 20 2 blend + rmoveto + -65 26 0 0 0 0 1 blend + 0 -28 11 -49 24 -17 -11 0 0 0 -6 4 0 0 0 3 3 0 0 0 -6 26 0 0 0 4 blend + rrcurveto + 78 -55 -25 -42 0 0 0 19 7 5 -4 -5 2 blend + rlineto + -8 85 -9 -20 0 0 0 -9 15 15 -9 -15 2 blend + rlineto + -5 52 -22 20 -43 -7 1 1 -1 -1 1 -36 0 0 0 0 10 -1 1 1 -7 -16 0 0 0 19 32 0 0 0 5 blend + 0 rrcurveto + -26 4 12 0 0 0 1 blend + 0 -27 -14 -14 -38 13 19 0 0 0 3 7 0 0 0 5 13 0 0 0 18 24 0 0 0 4 blend + rrcurveto + 0 -90 71 -50 139 4 24 0 0 0 3 5 0 0 0 10 -10 0 0 0 -9 -1 0 0 0 -32 -32 0 0 0 5 blend + 0 rrcurveto + 163 -27 -72 0 0 0 1 blend + 0 99 84 -17 -9 0 0 0 -8 -31 2 -1 -2 2 blend + 0 108 -1 3 0 0 0 1 blend + rrcurveto + 0 107 -59 47 -135 63 -25 -37 0 0 0 18 33 0 0 0 18 19 -2 1 2 0 1 0 0 0 -16 -6 0 0 0 5 blend + rrcurveto + -32 15 -6 13 0 0 0 -2 -7 0 0 0 2 blend + rlineto + -55 26 -26 21 -16 -38 4 -3 -4 -3 13 -2 1 2 -14 -16 -2 2 2 14 28 4 -2 -4 4 blend + 0 45 19 23 8 -5 -8 1 blend + rrcurveto + 0 60 38 25 53 15 31 -6 3 6 19 22 -3 2 3 12 23 16 -10 -16 21 35 2 -2 -2 4 blend + 0 rrcurveto + 43 -13 13 -1 1 1 1 blend + 0 27 -4 52 -24 9 8 0 0 0 -1 -10 0 0 0 -10 -8 0 0 0 7 -26 0 0 0 4 blend + rrcurveto + -85 47 33 47 -3 2 3 -11 0 5 -3 -5 2 blend + rlineto + 10 -67 7 18 3 -2 -3 -9 -33 -25 16 25 2 blend + rlineto + 11 -75 37 -14 39 1 -5 -1 1 1 23 60 1 -1 -1 -12 -27 1 -1 -1 0 9 -1 1 1 -17 -28 -1 1 1 5 blend + 0 rrcurveto + 26 -7 -12 1 -1 -1 1 blend + 0 29 15 5 41 -12 -21 -2 1 2 -5 -8 1 -1 -1 3 -4 2 -1 -2 -20 -27 -1 1 1 4 blend + rrcurveto + 0 84 -84 52 -121 -6 -24 0 0 0 2 4 0 0 0 4 17 8 -5 -8 8 -4 0 0 0 20 37 -8 5 8 5 blend + 0 rrcurveto + -155 40 63 0 0 0 1 blend + 0 -84 -80 1 2 0 0 0 0 29 0 0 0 2 blend + 0 -103 1 -5 0 0 0 1 blend + rrcurveto + 0 -104 65 -49 112 -54 4 24 0 0 0 -3 -20 0 0 0 -5 -4 0 0 0 -7 -20 0 0 0 17 12 0 0 0 5 blend + rrcurveto + 31 -15 6 6 0 0 0 2 -2 0 0 0 2 blend + rlineto + 66 -32 28 -22 19 48 0 0 0 2 -20 0 0 0 8 -2 -4 3 4 -8 -24 -10 6 10 4 blend + 0 -55 -8 -10 -4 3 4 1 blend + rrcurveto + 0 -49 -41 -38 -58 -25 -43 -3 2 3 -12 -24 1 -1 -1 -4 -16 -3 2 3 -24 -32 3 -2 -3 4 blend + 0 rrcurveto + 65 573 -34 -47 -10 6 10 27 77 32 -21 -32 2 blend + rmoveto + 0 119 -4 -14 -12 8 12 1 blend + rlineto + -71 31 49 20 -12 -20 1 blend + 0 rlineto + 0 -119 4 14 12 -8 -12 1 blend + rlineto + 71 -31 -49 -20 12 20 1 blend + 0 rlineto + -69 -727 28 47 10 -6 -10 -23 -45 -12 8 12 2 blend + rmoveto + 71 -31 -49 -20 13 20 1 blend + 0 rlineto + 0 129 -2 -18 -10 6 10 1 blend + rlineto + -71 31 49 20 -13 -20 1 blend + 0 rlineto + 0 -129 2 18 10 -6 -10 1 blend + rlineto + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/_h_h_e_a_recalc_OTF.ttx b/Tests/ttLib/tables/data/_h_h_e_a_recalc_OTF.ttx new file mode 100644 index 0000000..ff84817 --- /dev/null +++ b/Tests/ttLib/tables/data/_h_h_e_a_recalc_OTF.ttx @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 300 + 0 0 rmoveto + 1 1 rlineto + endchar + + + 400 + -55.2 -55.2 rmoveto + 110.4 110.4 rlineto + endchar + + + 500 + 100 0 rmoveto + 300 0 rlineto + endchar + + + 600 + endchar + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/_h_h_e_a_recalc_TTF.ttx b/Tests/ttLib/tables/data/_h_h_e_a_recalc_TTF.ttx new file mode 100644 index 0000000..1167842 --- /dev/null +++ b/Tests/ttLib/tables/data/_h_h_e_a_recalc_TTF.ttx @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/_h_h_e_a_recalc_empty.ttx b/Tests/ttLib/tables/data/_h_h_e_a_recalc_empty.ttx new file mode 100644 index 0000000..0bc5b80 --- /dev/null +++ b/Tests/ttLib/tables/data/_h_h_e_a_recalc_empty.ttx @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 300 endchar + + + 400 endchar + + + 500 endchar + + + 600 endchar + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/_v_h_e_a_recalc_OTF.ttx b/Tests/ttLib/tables/data/_v_h_e_a_recalc_OTF.ttx new file mode 100644 index 0000000..d9851db --- /dev/null +++ b/Tests/ttLib/tables/data/_v_h_e_a_recalc_OTF.ttx @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 300 + 0 0 rmoveto + 1 1 rlineto + endchar + + + 400 + -55.2 -55.2 rmoveto + 110.4 110.4 rlineto + endchar + + + 500 + 100 0 rmoveto + 300 0 rlineto + endchar + + + 600 + endchar + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/_v_h_e_a_recalc_TTF.ttx b/Tests/ttLib/tables/data/_v_h_e_a_recalc_TTF.ttx new file mode 100644 index 0000000..ef3035d --- /dev/null +++ b/Tests/ttLib/tables/data/_v_h_e_a_recalc_TTF.ttx @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/_v_h_e_a_recalc_empty.ttx b/Tests/ttLib/tables/data/_v_h_e_a_recalc_empty.ttx new file mode 100644 index 0000000..2960020 --- /dev/null +++ b/Tests/ttLib/tables/data/_v_h_e_a_recalc_empty.ttx @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 300 endchar + + + 400 endchar + + + 500 endchar + + + 600 endchar + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/README b/Tests/ttLib/tables/data/aots/README new file mode 100644 index 0000000..1b673bd --- /dev/null +++ b/Tests/ttLib/tables/data/aots/README @@ -0,0 +1,10 @@ +The *.otf data in this directory was built from: + +https://github.com/adobe-type-tools/aots + +at the following revision: + +1c41fd20d2b020177625541a228c4c7c934879ef + +Fonts were built by running "make" and copying tests/*.otf over. +Original .xml files were not copied to save space. diff --git a/Tests/ttLib/tables/data/aots/base.otf b/Tests/ttLib/tables/data/aots/base.otf new file mode 100644 index 0000000000000000000000000000000000000000..2ed6f1fafe7b8456eb186e9bc947e1adf053964a GIT binary patch literal 4944 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 yr3^U?i3~+>QJ8EPLn=cNLoq`pLq0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 145 665 rmoveto + -74 -43 -28 -166 -6 -75 -10 -124 21 -220 148 -44 rrcurveto + -11 37 40 7 39 hhcurveto + 6 8 3 4 4 hvcurveto + 69 60 39 31 2 103 1 44 5 43 -4 43 -7 87 -50 217 -88 45 -24 13 -29 1 -28 7 -12 -1 -8 -4 -6 -7 -16 -2 -16 -3 -13 -8 rrcurveto + 122 -50 rmoveto + 97 -69 31 -246 -15 -107 -13 -95 -42 -80 -111 33 -52 16 -30 55 -16 46 -32 98 -1 279 95 68 13 9 18 -2 15 4 1 1 2 1 2 1 14 -2 13 -2 11 -8 rrcurveto + 233 -615 rmoveto + return + + + 175 661 rmoveto + 1 -215 6 -215 1 -215 4 -42 54 3 2 41 -1 216 -6 214 -1 215 -11 35 -42 0 -7 -37 rrcurveto + 325 -661 rmoveto + return + + + 143 536 rmoveto + 59 22 61 39 64 3 78 5 3 -97 -32 -48 -76 -117 -268 -55 -9 -168 -2 -31 11 -30 5 -31 5 -10 5 -5 10 -5 50 -15 58 8 50 1 65 1 66 3 65 1 37 7 0 42 -35 11 rrcurveto + -106 -2 -108 -7 -107 4 -2 18 -8 18 2 17 16 141 259 55 69 117 72 122 -67 142 -156 -40 -52 -14 -48 -26 -51 -19 -40 -14 16 -51 41 8 rrcurveto + 357 -536 rmoveto + return + + + 92 580 rmoveto + 13 6 13 7 14 4 54 16 184 1 9 -81 1 -13 -3 -13 -3 -14 -9 -45 -124 -14 -42 -8 rrcurveto + -2 -2 1 -1 hhcurveto + -2 vlineto + -30 -15 5 -40 35 -4 60 -5 62 -4 47 -43 83 -75 -108 -134 -82 -20 -75 -17 -101 91 -42 -14 -22 -8 -7 -18 10 -21 2 -2 2 -2 1 -2 10 -10 11 -3 10 2 rrcurveto + 2 2 -1 1 hhcurveto + 16 -7 15 -7 15 -7 33 -14 33 -14 35 -7 103 -18 81 94 48 78 51 83 -64 98 -77 36 -4 1 -3 2 -4 2 17 7 16 9 15 12 77 61 -32 107 -79 40 -91 47 -115 -9 -91 -40 rrcurveto + -27 -24 18 -37 36 7 rrcurveto + 408 -580 rmoveto + return + + + 336 627 rmoveto + -73 -94 -78 -92 -70 -97 -32 -45 -39 -39 -2 -56 2 -16 5 -7 14 -7 76 -39 130 16 102 10 -2 -44 -2 -44 -1 -43 4 -42 54 3 2 41 1 45 2 45 2 45 rrcurveto + 6 6 0 1 5 hvcurveto + 41 8 -6 54 -42 -3 rrcurveto + -2 -3 -1 -2 hhcurveto + 4 135 -3 133 -49 127 -2 3 -3 2 -2 2 -6 6 -8 4 -9 -1 rrcurveto + -6 -6 -3 -4 -4 hvcurveto + -2 -1 -1 -1 -1 -1 rrcurveto + -230 -408 rmoveto + 9 14 6 14 9 13 16 24 37 51 17 22 48 64 50 62 50 62 29 -105 1 -110 -4 -109 -87 -9 -131 -13 -50 20 rrcurveto + 394 -219 rmoveto + return + + + 41 642 rmoveto + 1 -2 1 -1 -1 vvcurveto + -7 2 -7 5 -5 vhcurveto + 15 -69 -71 -105 61 -45 71 -50 214 60 48 -116 9 -20 3 -24 -3 -22 -13 -128 -51 -35 -120 -6 -38 -1 -62 -5 -26 34 -29 22 -33 -28 16 -33 39 -51 75 0 59 2 83 5 76 21 49 69 rrcurveto + 25 36 0 48 11 42 19 72 -43 43 -42 45 -62 68 -159 -25 -76 26 -20 43 44 56 -6 66 101 14 102 -5 103 -1 37 7 0 42 -35 11 -109 1 -110 5 -108 -17 rrcurveto + -1 1 0 0 1 vvcurveto + -25 33 -45 -26 18 -38 rrcurveto + 407 -673 rmoveto + return + + + 399 660 rmoveto + -36 2 -37 10 -35 -8 -152 -32 -56 -137 -37 -134 -35 -130 55 -175 141 -42 156 -46 135 253 -64 123 -39 78 -32 -3 -81 14 -26 5 -36 -14 -24 -10 -36 -15 -28 -18 -26 -26 19 101 63 130 114 18 rrcurveto + 32 5 31 -8 32 -1 37 7 0 42 -35 11 rrcurveto + -263 -360 rmoveto + 52 57 149 71 42 -110 33 -84 -77 -193 -113 33 -98 30 -29 103 4 92 9 -7 14 -1 14 9 rrcurveto + 401 -299 rmoveto + return + + + 99 610 rmoveto + 63 14 62 -15 64 -2 rrcurveto + 22 23 1 2 22 hvcurveto + -24 -33 -19 -38 -22 -38 -85 -149 -77 -149 -19 -173 4 -37 43 -4 12 34 19 165 74 145 83 142 34 57 25 61 56 36 21 24 -14 30 -32 -2 rrcurveto + -6 -47 -49 -8 -48 hhcurveto + -71 2 -67 15 -70 -17 -40 -14 16 -51 41 8 rrcurveto + 418 -667 rmoveto + return + + + 289 676 rmoveto + -88 12 -105 -100 -7 -86 -1 -23 -10 -26 9 -22 9 -21 8 -23 13 -20 6 -8 8 -7 9 -5 -42 -15 -31 -26 -21 -57 -31 -83 41 -138 89 -34 25 -9 24 -16 27 1 90 2 -6 -5 70 46 rrcurveto + 60 39 -5 113 -8 58 -2 24 -13 22 -9 22 -8 20 -18 15 -15 16 -7 7 -9 4 -9 3 3 5 3 5 3 6 43 84 -21 87 -3 90 -6 20 -17 8 -14 -3 -10 9 -11 8 -13 1 rrcurveto + -12 -364 rmoveto + 2 -2 2 -1 3 -1 12 -4 13 -1 9 -8 26 -18 13 -38 6 -28 24 -103 -43 -94 -120 16 -104 15 -73 140 80 83 31 33 22 -2 42 7 19 -4 19 3 17 7 rrcurveto + 32 196 rmoveto + 2 -48 -9 -48 -33 -37 -30 -34 -85 64 -8 41 -11 56 73 136 70 -23 8 -3 8 -6 7 -6 2 -31 4 -31 2 -30 rrcurveto + 191 -508 rmoveto + return + + + 379 635 rmoveto + -50 16 -48 25 -52 6 -169 23 -32 -255 81 -95 66 -76 -16 4 97 -2 rrcurveto + 6 9 3 4 4 hvcurveto + 21 21 19 16 16 17 8 -65 4 -65 -6 -62 -4 -33 -9 -54 -40 -14 -66 -23 -78 47 -54 20 -40 13 -19 -50 37 -19 46 -17 45 -17 45 -16 31 -11 34 12 32 2 104 6 0 190 -4 62 rrcurveto + -1 36 -5 36 -5 36 -2 23 -4 24 -3 23 13 51 -17 20 19 51 5 16 -4 13 -9 8 15 11 0 23 -20 16 rrcurveto + -72 -84 rmoveto + 2 -34 4 -35 5 -35 -3 -19 -4 -16 -6 -7 -19 -22 -22 -20 -21 -21 -14 1 -14 0 -15 1 -53 58 -34 59 18 84 5 21 15 17 10 18 21 7 21 16 22 -3 41 -5 38 -19 40 -14 rrcurveto + -1 -2 -2 -1 -1 -2 -14 3 -15 -9 -4 -21 rrcurveto + 193 -551 rmoveto + return + + + f75af910 158c838c 828d8387 5d8a7d7a + 4d5ffb37 3afb2878 fb3e8f66 b68797ad + 92c79ac5 9dc3c287 bf99c18f 9d559f55 + a4569e66 bd9e7eb3 0838f74a 65f7516b + f7570892 8c938c93 1e8da478 977a887a + 8d797d8e 7208acfb 50159847 9b489e49 + 61866381 618ca4d2 a8d2a1d4 08f7a1fc + 54150b + + + c0f8f115 78538277 884f8830 a6318e30 + 8e468891 7e480888 8c878c89 1e867b95 + 78a389c9 91f72b8d c3c1a0a0 a49d9aa4 + c7f22be8 2baea298 a39ba6a1 cfc272f6 + 57be799e 71937497 50aa4068 55790871 + 82897396 7d898989 898a8808 b4fba915 + dd8daf97 d367d665 9f323c5c 47625089 + 428593b6 8e9f89c0 89b584b6 84b708f7 + 28f7bd15 ce79b23d 5852564f 3b7f3f7f + 088a8b8a 8a1b8c84 07898b8a 8c8a1e73 + 7b9168aa 86d696bb 96bda779 9179907a + 8d618f61 85608a86 b98ab995 b990a194 + 9e92a008 928a9188 901ebe9d a79ac37d + 08fb36fb 85158f78 90798f78 088c0688 + 9f889e89 9f08f833 fc17150b + + + f83bf8f9 155ea564 b85791fb 5ba649fc + 1bb1fb10 bafb2b70 a9f70734 08879092 + 89911bda 90d09eb3 cc9ba696 a1a29fa4 + a771ac69 7f7e8080 82807d7d 78745176 + 85698168 83688276 9b6e967d a143f70b + 9df7b1f4 ec089e9d aa8ba393 b175b075 + b075b179 a5b86aa4 08d4fcf9 150b + + + cef8ec15 93948c94 1b6efb1f 9efb1d9d + fb200889 0791578a 998e4408 8807838d + 848c848e 89997f97 778a0888 888b8a88 + 1f808787 88848008 8a8b8a8a 891e887c + 8e829680 df3ff75a cbc2e0e8 f724a2f7 + 0e4ef734 81a6729e 79a14dd6 fb1b7c34 + 81088a85 858b851b 62898855 b58708f7 + 9a6b15f7 052b64fb 6f38266a 624c6e54 + 82088e07 84f7455a f743b4f7 42089007 + c991c88a bf5f08f7 3ffccc15 0b + + + bff8fa15 9f36903a 87338957 88678757 + 08857bfb 4392751e 9669b487 a98a08f3 + ecb091f2 1fb49385 c1618820 85215cfb + 019a94c4 8ac48ec4 8daa8dab 8daabd8d + be90bd8c b0928bb5 6896598a 5a865889 + 8ed988d9 7bd908e8 a0f7088b e7799e77 + ab958dab 8a8e8b8e 8a8e869a 85927b8f + 21a3fb2e 88216d08 7a85857f 801a8a88 + 8b888c88 08f854fc fa150b + + + cef90215 90068e6c 7969876d 876b8c7f + 8a61082d 0783808c 7d93828d fb0190fb + 018cfb01 8f61c18e 8db48af5 85f689f6 + d79ad97b d99caa9d 7fb46789 437b4298 + 44818ab6 8cb68db6 088db59e b48cb2f7 + 0287f584 f702a3aa 9d7fb467 89fb0174 + fb0095fb 028f7ba1 7286817b 67858b5a + b38708f8 45fd0215 0b + + + 500 0 rmoveto + return + + + 0b + + + + + + -91 callsubr + -91 callsubr + endchar + + + -107 callsubr + -106 callsubr + endchar + + + -106 callsubr + -107 callsubr + endchar + + + -106 callsubr + -106 callsubr + endchar + + + -106 callsubr + -105 callsubr + endchar + + + -106 callsubr + -104 callsubr + endchar + + + -106 callsubr + -103 callsubr + endchar + + + -106 callsubr + -102 callsubr + endchar + + + -106 callsubr + -101 callsubr + endchar + + + -106 callsubr + -100 callsubr + endchar + + + -106 callsubr + -99 callsubr + endchar + + + -106 callsubr + -98 callsubr + endchar + + + -107 callsubr + -105 callsubr + endchar + + + -105 callsubr + -107 callsubr + endchar + + + -105 callsubr + -106 callsubr + endchar + + + -105 callsubr + -105 callsubr + endchar + + + -105 callsubr + -104 callsubr + endchar + + + -105 callsubr + -103 callsubr + endchar + + + -105 callsubr + -102 callsubr + endchar + + + -105 callsubr + -101 callsubr + endchar + + + -105 callsubr + -100 callsubr + endchar + + + -105 callsubr + -99 callsubr + endchar + + + -105 callsubr + -98 callsubr + endchar + + + -107 callsubr + -104 callsubr + endchar + + + -104 callsubr + -107 callsubr + endchar + + + -104 callsubr + -106 callsubr + endchar + + + -104 callsubr + -105 callsubr + endchar + + + -104 callsubr + -104 callsubr + endchar + + + -104 callsubr + -103 callsubr + endchar + + + -104 callsubr + -102 callsubr + endchar + + + -104 callsubr + -101 callsubr + endchar + + + -104 callsubr + -100 callsubr + endchar + + + -104 callsubr + -99 callsubr + endchar + + + -104 callsubr + -98 callsubr + endchar + + + -107 callsubr + -103 callsubr + endchar + + + -103 callsubr + -107 callsubr + endchar + + + -103 callsubr + -106 callsubr + endchar + + + -103 callsubr + -105 callsubr + endchar + + + -103 callsubr + -104 callsubr + endchar + + + -103 callsubr + -103 callsubr + endchar + + + -103 callsubr + -102 callsubr + endchar + + + -103 callsubr + -101 callsubr + endchar + + + -103 callsubr + -100 callsubr + endchar + + + -103 callsubr + -99 callsubr + endchar + + + -103 callsubr + -98 callsubr + endchar + + + -107 callsubr + -102 callsubr + endchar + + + -102 callsubr + -107 callsubr + endchar + + + -102 callsubr + -106 callsubr + endchar + + + -102 callsubr + -105 callsubr + endchar + + + -102 callsubr + -104 callsubr + endchar + + + -102 callsubr + -103 callsubr + endchar + + + -102 callsubr + -102 callsubr + endchar + + + -102 callsubr + -101 callsubr + endchar + + + -102 callsubr + -100 callsubr + endchar + + + -102 callsubr + -99 callsubr + endchar + + + -102 callsubr + -98 callsubr + endchar + + + -107 callsubr + -101 callsubr + endchar + + + -101 callsubr + -107 callsubr + endchar + + + -101 callsubr + -106 callsubr + endchar + + + -101 callsubr + -105 callsubr + endchar + + + -101 callsubr + -104 callsubr + endchar + + + -101 callsubr + -103 callsubr + endchar + + + -101 callsubr + -102 callsubr + endchar + + + -101 callsubr + -101 callsubr + endchar + + + -101 callsubr + -100 callsubr + endchar + + + -101 callsubr + -99 callsubr + endchar + + + -101 callsubr + -98 callsubr + endchar + + + -107 callsubr + -100 callsubr + endchar + + + -100 callsubr + -107 callsubr + endchar + + + -100 callsubr + -106 callsubr + endchar + + + -100 callsubr + -105 callsubr + endchar + + + -100 callsubr + -104 callsubr + endchar + + + -100 callsubr + -103 callsubr + endchar + + + -100 callsubr + -102 callsubr + endchar + + + -100 callsubr + -101 callsubr + endchar + + + -100 callsubr + -100 callsubr + endchar + + + -100 callsubr + -99 callsubr + endchar + + + -100 callsubr + -98 callsubr + endchar + + + -107 callsubr + -99 callsubr + endchar + + + -99 callsubr + -107 callsubr + endchar + + + -99 callsubr + -106 callsubr + endchar + + + -99 callsubr + -105 callsubr + endchar + + + -99 callsubr + -104 callsubr + endchar + + + -99 callsubr + -103 callsubr + endchar + + + -99 callsubr + -102 callsubr + endchar + + + -99 callsubr + -101 callsubr + endchar + + + -99 callsubr + -100 callsubr + endchar + + + -99 callsubr + -99 callsubr + endchar + + + -99 callsubr + -98 callsubr + endchar + + + -107 callsubr + -98 callsubr + endchar + + + -98 callsubr + -107 callsubr + endchar + + + -98 callsubr + -106 callsubr + endchar + + + -98 callsubr + -105 callsubr + endchar + + + -98 callsubr + -104 callsubr + endchar + + + -98 callsubr + -103 callsubr + endchar + + + -98 callsubr + -102 callsubr + endchar + + + -98 callsubr + -101 callsubr + endchar + + + -98 callsubr + -100 callsubr + endchar + + + -98 callsubr + -99 callsubr + endchar + + + -98 callsubr + -98 callsubr + endchar + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/base.ttx.OS_2 b/Tests/ttLib/tables/data/aots/base.ttx.OS_2 new file mode 100644 index 0000000..ffd7124 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/base.ttx.OS_2 @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/base.ttx.cmap b/Tests/ttLib/tables/data/aots/base.ttx.cmap new file mode 100644 index 0000000..0f3e822 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/base.ttx.cmap @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/base.ttx.head b/Tests/ttLib/tables/data/aots/base.ttx.head new file mode 100644 index 0000000..8ac917a --- /dev/null +++ b/Tests/ttLib/tables/data/aots/base.ttx.head @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/base.ttx.hhea b/Tests/ttLib/tables/data/aots/base.ttx.hhea new file mode 100644 index 0000000..ce8b339 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/base.ttx.hhea @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/base.ttx.hmtx b/Tests/ttLib/tables/data/aots/base.ttx.hmtx new file mode 100644 index 0000000..0efe1f9 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/base.ttx.hmtx @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/base.ttx.maxp b/Tests/ttLib/tables/data/aots/base.ttx.maxp new file mode 100644 index 0000000..408b67e --- /dev/null +++ b/Tests/ttLib/tables/data/aots/base.ttx.maxp @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/base.ttx.name b/Tests/ttLib/tables/data/aots/base.ttx.name new file mode 100644 index 0000000..6e54516 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/base.ttx.name @@ -0,0 +1,25 @@ + + + + + + base + + + Regular + + + base + + + base + + + Version1.0 + + + base + + + + diff --git a/Tests/ttLib/tables/data/aots/base.ttx.post b/Tests/ttLib/tables/data/aots/base.ttx.post new file mode 100644 index 0000000..b042c3a --- /dev/null +++ b/Tests/ttLib/tables/data/aots/base.ttx.post @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/classdef1_font1.otf b/Tests/ttLib/tables/data/aots/classdef1_font1.otf new file mode 100644 index 0000000000000000000000000000000000000000..f0add6983936d966c680e9a86c6e8c34453aa0dd GIT binary patch literal 6004 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/classdef1_font2.otf b/Tests/ttLib/tables/data/aots/classdef1_font2.otf new file mode 100644 index 0000000000000000000000000000000000000000..f01876dd19e81cb241e90f9b249b8b46129b5763 GIT binary patch literal 6020 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 h#8AwT2{zl1L65 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/classdef1_font3.otf b/Tests/ttLib/tables/data/aots/classdef1_font3.otf new file mode 100644 index 0000000000000000000000000000000000000000..2a0f9cc8fbea43ddb902429d72bf85354cac6ec7 GIT binary patch literal 6060 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/classdef1_font3.ttx.GSUB b/Tests/ttLib/tables/data/aots/classdef1_font3.ttx.GSUB new file mode 100644 index 0000000..9bc1e43 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/classdef1_font3.ttx.GSUB @@ -0,0 +1,510 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/classdef1_font4.otf b/Tests/ttLib/tables/data/aots/classdef1_font4.otf new file mode 100644 index 0000000000000000000000000000000000000000..9c0f41c51604a67f643f34e9f249e3c71ef8d401 GIT binary patch literal 5984 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/classdef2_font1.otf b/Tests/ttLib/tables/data/aots/classdef2_font1.otf new file mode 100644 index 0000000000000000000000000000000000000000..2e2faafee002ca24442bd596e54c42e896b83b6b GIT binary patch literal 6004 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/classdef2_font2.otf b/Tests/ttLib/tables/data/aots/classdef2_font2.otf new file mode 100644 index 0000000000000000000000000000000000000000..2e2a1af788ff9759391eabc1a13494882664ee48 GIT binary patch literal 6016 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/classdef2_font3.otf b/Tests/ttLib/tables/data/aots/classdef2_font3.otf new file mode 100644 index 0000000000000000000000000000000000000000..14c91193811e714c3399b4b1b1b4fcc441d40e39 GIT binary patch literal 6052 zcmeYd3Grv(VrXDsW>9c;b5l?b>uO|RU|7SzAfVzN9O@)l$Cu2&AaH?!f%%Ppu)a~w znb5rq3?h3N7#I?ga}x^~q#2_b7(`_l7#IRFQWH}$Zh9LqFo@9WTYmF)^dGf zU=Urxz`&rAky}#XP+~QMfkE^H0|UdU+{B6khLivX1_rSy3=9lxd5O8H0^8K17#PIn zFfcH@D9A4^`9Gh*h=D=8g@J)VfsuiQg@KWUfq{V$q&%fGH@6bO_xi!l@S~pbM+4K3 z2Ie0UEWZVLnB}+_IR1BcLn$8i?rs^*BQhLmtg;LY>%Q>HGB7X~^MlB8L0JX{hCo>c z26iSPhBTNR%nS?+tPBhc>!JdJE!HI!^ z!Igo5!GnQ;!JC1B!4KpJ1_p)@1_p+31_p*G1_p*$P*5{4FeEcDFr+asFk~_?Fo1$L zpMim)h=GBjl!1Yvf`Ngdnt_3#j)8#z6n&uJYG+_z=we`C=w)DFn83imFqwgYVHyJi z!%PMS22k+MXJBAh#K6F?l!1X^1p@=aY6b>|bqov)8yOfFwlFX-Y-eC#*u}uWu$O^> z;Q#{z!(j#nhGPs23?~^F7|t*-Fq~&#V7SDSRGe|cxOgA%1H#1H*Gf6iyO*b=3H#1K+vq(3yOgA@3H#bZ-H%d1*PB%A6H#bc; zH%m7+PdB$nH@8f;Fi5vBOt&yfw=hn(FiE#CO}8*hw=hq)ut>MCOt&;hw=_(*G)lKL zPPa5kw=_++G)uQMPq(y4x3qN5FQ_caOwTA$&`8!)FfuSOQgBSkPfAq?t}HG|%`H~& z%uCKMD#$NNEJ;n#Q*g}5Q3wJlFIEUjElw>eOHI*(WG)6q#*QS06j*j(gJuB%XtI}L zU|^7kCU;E+1_nK7GPhu0V6cTIZ+8X;1|Mj$4r5?oh=wNTR0alyECvP!P%C6cCp#_%21b5pa+73WV3dO#uNqy#!LnV#ykcF#$pBr#tH@o###mj z#wG>^#&!k<#vTR+#)%9JjMEqx7-usuFfL$VU|h<;z_^NmfpI+p1LGD32F9HX42=62 z7#I&TFfg8AU|>AUz`%Hkfr0Tl0|Vn71_s853=E9V7#J8|GcYiIU|?YUYQSZ{#izif z2qKg~gffUw0THSoLJdTyg9r@}p~=Ok$fW=x6hVX%h)@O*Dj-4?M5uuXbr7KeA~d=9 zlt4O^Kz1l`DS^1kAVLL1sDcPJ5TOntG(dzV7oRdnhcZZqGDwFqmoi991w^QV2sIF) z4k9!_geDiC3P^_vNQVkYhYCoC3YQ8geDiC21thnNQVYUhXzQ821thnNQVYUhXzQ8 z2A2j%u_hOvCP;@SNQWj!hbBmeCP;@SNQWj!hbBmeCP;@SmnIiq(UPC4OMbU6`R+2~ z(30QNmau(azvR#1C6||YEn!EO<3Px9uI75cy_$#T!2}*&8O|djGMq<5!S$UmgAgol zgQ|_y3=E873=E8G85o!x7#Nt2GcYjQF)%PsW?*0mXJBCEXJBA$0#(!u3~YB87}zTr z7&urM7&vMf7&w|37&w~0tNzp#bxg?rZPn_~_*-DX?}G0d@f-~sHc#3lJ#lhhf4}^N zC>R)0@VZ%=Q}%+48|)0#QH*Z%n}%KhE>r>1Dn z@9c@cvwMDL_a`3g>)q1zJ*V}1R@e9JhINVEEomIzZ~oO3<^HbxV~yysg(o(h?z=gm zPb)jx{x|3E1tCqpZ8~$NN46e>1cw z^moo}p4&TT_IJha(+^$v&UyJepT~El-v-Iywb6wU70I=;BeqQD`2O?H8d2`&KW>Q5 zm^fq7%>IM3zx(t|vFfSosOzY0iu)~4o7l|J+1b-2-N~L?Ga-3h`|-Au?Yk20Is9h$ zEoE8mP^I=Pv9`3irE+RnZ)b03Pv_LeX&qC0IeL4$dZcH!%xam{vZi)T?Yfpvm3Ln5 zzPaz7_glO5?L8g6?Y(m*&7LxA`i&FW-)uG;?E1~#WRT2JP&_wn{UnYbI)846a({pF zlU>wF*H+(ED?2VFr8Bs9;Oq|%=GeLgK z1*siHM;g>zFmerW$S73Kb+{8K|Ty*F)YM)Pl-?BL(BX}_g1ev2LY?fhLa z@w-9ncj@mFy}$FP+|2H1HyxBDIXn{|!m%Dhkg&!@D^?3y`+_+{i59OetZ)>HS=W6=}wM? zi+Xw&$`@x`so> zn5gDxX_sD)S*}&k#P`GOuf8bv_pLuIM2)8_skfMRl{6MkDV zy1#j5E6;NJu5kO_sx=Ecw@#YZKCf@iB#zbJBSd?no1(MQax=|NH*aY;eC4~$#gfS# zlUpaX_U82G^yYGW5B@Vjl=}z&PkzzJ+0jeFC;qlwl>K}19i6lG)%|ZeIVMg4l_?7r z%$q%X&SbBCuU6motftW9TF(;OU7_=nXH-OPsMuMxqiRR>s)n`GSNDFN*!?}I{Uk@% z`nL6L>pK^C`%TTi^W71D#u2wzX{M_^$DNujskT zO|}t1VgAdmXGgh@-o3?w}-sP1OD<@R-CiW+`CUmv! z?AkeX=Y)k*=1p3_F{x`}#{{{3bC=ILT+8v>?02hZf5n8#{tAvBwvPE7^E&5q{4o9_ zF3SDG`o~YvxTPsO!YBTYUgr5ndgE__UB9&pR(@wU;b@;QsdJ+AtqB+AZ9X%5*|dd= z<}X=ZxF)l{uA!l+p|-Z7B(SVCvoWJNqp56m-n80<&fmG`e&6`z4adB>tGX9Y-dMV! zbVKRJ%B4H9mT+A8^HY@j+>bY+lc)4dk7>q%(5>wp-)CiuerMHA`JHc}mX+j_7t+Mh$KLYYHR`+b zmhVm+6WC8qJkhm3p5wdypEIJ|`+xiroo_b5s@=FVyd%9moTIh+xAUY@xoMeO+E;dQ ze15ZG;wNaytRsKtYH#np345XSTVz*MTSRBy&5oPZH#uG` z_|DP&ymDUo>Z!e}yZXBOy87B?wN9$;S6uvCuR}4HqpG#FzCo(1qr0Q0gX2fYpI@Tf z=YBGYPGIZHsmyH8ZO`th?@Hv*Wp8fish!-^`3I!t@F0@Z2HdB{aSi{)Ye#? z-r60txt-&O>t7~O?jIf9qTJjJj0_CSH<(W`a51njJ!9fwG-BWaci|ZsI2f21LKut~ zm>JXu zkwFQpKLtD>!NkDKz{0@Fz{bGNz`?-Dz{SALz{9}Hz{kMPAiyBVAjBZdAi^NZAjTlh zAi*HXAjKffAj2TbAjcrjpa3>YnL&j?l|hX`ok4>^lR=9?n?Z*`mqCv~pTU5^kim$- zn8Ad>l);R_oWX*@lEI3>n!$#_mcfp}p22~^k->?{ zm%)$0pCN!DkRga6m?4BAlp%~EoFRfCk|ByAnjwZEmLZNIo*{uDks*m885|a%aAIU& zV_;=qWEh@41I6y}^cnN;inEf`;t~c9aPDGeV9ZG@$z$MP0QFZG{{IK3RVL0Oj1w4G z8F(1KFn(e9#b5-c85qBSSPTqITx`s2tlUgYAU&WVLk1Xdba8S8=b8Xc(AdEV5c$BK z@&Et-3=Et{IFB$mGB|?Of^s!TrvU>q0~Z4Wg9rl;SR7OefEXY-5C#p-fyxsF1_lNe zaCySSkPI@7fe}K26ocK&$iT3c?c(crew(ihjI4LS;wZ%bZ=6RsKm&cu3`|f7Mv$u* z7#L1)9%0mB)Zsh=HVG;TrWjf8fMkXP2Kgs|0bKSlFt9QBF)%YQvT!mmf(KV2Y$gUi z205rWGlLw14wTKpAjjYZWwSDfFl0d4Yz%S?H4MoNISh#m#SFy^DGaF$X$(dT@eFAU z`3!jsB@D(4K@6!3=?tY{HAMu};x{RbA(f$sp_m~PY_=hT9)ke^^FYJQpb^nIps{X{ H|4}gj2-k}8 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/classdef2_font3.ttx.GSUB b/Tests/ttLib/tables/data/aots/classdef2_font3.ttx.GSUB new file mode 100644 index 0000000..7cb045f --- /dev/null +++ b/Tests/ttLib/tables/data/aots/classdef2_font3.ttx.GSUB @@ -0,0 +1,510 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/classdef2_font4.otf b/Tests/ttLib/tables/data/aots/classdef2_font4.otf new file mode 100644 index 0000000000000000000000000000000000000000..c75c883a08621852a00d9ed51b1e127484197be1 GIT binary patch literal 5984 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 J4e~!K1^~M@i1h#f literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/classdef2_font4.ttx.GSUB b/Tests/ttLib/tables/data/aots/classdef2_font4.ttx.GSUB new file mode 100644 index 0000000..90f0b93 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/classdef2_font4.ttx.GSUB @@ -0,0 +1,469 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/cmap0_font1.otf b/Tests/ttLib/tables/data/aots/cmap0_font1.otf new file mode 100644 index 0000000000000000000000000000000000000000..772f9a748de57fc36a54bfd22b36e323c1d357ec GIT binary patch literal 5196 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 + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/cmap10_font1.otf b/Tests/ttLib/tables/data/aots/cmap10_font1.otf new file mode 100644 index 0000000000000000000000000000000000000000..023e9456153819e6bc4146e8121e870b7d0ab689 GIT binary patch literal 4968 zcmeYd3Grv(WN2VuW)N_8b5l?b>uO|RV3@28M*>+{6Ng z@F|%L41x*_3=C=+sfj5WH@yuQ7z8~S7#PelGEx&oYq>r#FbFm^z_9KMuPg%tb1^@NEEkkzU|VAp--$69xu`=L`%CuNW8@-ZC&Sd|+T;_{_k- z@Qs0i;U@zF!yg6)hW`u3XjDiddj3NvSjN%Loj8Y5? zjIs<2j0y}4jLHlQjA;xEjM?c1>4xb>>Bi|M>89ys>E`Jc>6Yn+2I+=|>4rw>hQ{fJ zCh3Nz>4s+MhUV#p7U_nT=|%?WMuzD|M(IYz=|(2$MyBaTX6Z)e=|&dmMwaQu2IBdIs#>VN!Ch5ke>BeU1#^&k97U{;8=_Us0CWh%IM(HNT=_V%WCZ_2oX6Yv8=_VHG zCYI@@2I;1T>83{MrpD=}Ch4Z8>857srsnCU7U`yz>1GD$W`^lzM(Jk8>1HPBW~S+8 zX6a_;>1Gz`W|ryZ2I=O8>E=f1=EmvfCh6v;>E>qX=H}_<7U|}e=@tg*7KZ5-M(GyD z=@usG7N+SIX6Y8@=@u607MAIj2I-cD>6S+6md5FpCh3-@>6T{cmgeb}7U`Cj&iMtE zMVaXtB?=nJnhHh+21W{wDfvmM3c;1dC8@c^3Z8k%`9%f!MTsS;DS8TyIXMbJAmzmh zL8-;5MP;cedXUV;z{uE<#E=5Z4s6gYAOKDFQVa|X^3de2$-uy%2TkS{3=9mm(B$pT zz`)=GP1a!y3=GlGoz^#w83443*I2+sweg&;d=hlNcBnrbCnKLIwtgWzb~0 zfq{WxD>QlTXJBAB0!^0Z7#J8XLzClO1_p*l&}8_Afq~&8H2M98lpf$@$Hlh z6hMR`h)@C%${<1oM5uxYH4vc=A~ZmRCKsO)NQV;04ka!n5LX#QsDKDn5TOPl)Io#> zh|uKXQwHf!2I)`+=}_iU28pSF2vrcF1|rl!ga(Mv@ zp$5{S2GXGh(xJws22!F9A~ZmRCKsPNNQXK|hdM}yI!K2)NQXK|hdM}yI+r>~od$@| zCgb_&;aSs0O`;G>Cgb_&;aSs0O`=+(f}#eCgn}&;;qw1nJNO>Cgn} z&;;qw1nJNO>CoiT6xm@Af6%U1l6w@_X76w(skg{5ibj@)EBl?C5eF2szHx zTo1Tc^YA>Fz{4xUc|=5p^N1+8z7u8;g5_;cwXvFkfl-WsfpIMZ1Cs*-1JiK^24*`3 z2Ik2O3@qUc46OVN46IF{ikg9e?G6J2dnE${2MYrOM=b*bM-u}BNAq{ppSq%s3HiUR zS{)jH3oQ6u@LeOGqhZ75Nt>i6PVVdPm%ng))2iN2J=-U2Yv0!LDDr!*@s8pdc|G}) z>bsiTTU)9-t2^qNIll8nioS7M^}DM^p-v~!`Zx2M@Y%VCs&{6;?fSm@)%h7y=625N z@9F97>Ft@>Iiqu0GspMZKfgt}zdQfb6z%z)J@I#T&+qL1#DjgkTe`mIw0_U(`kvje zF0s2MjpO^xznY@l-<5x?5k0o>#HQ1IHz)LIWk=ip=KQ@Nr0KU!XU??T2{{uQC$vp% z+go#V^4^{^J!kvQ^lX^ay)|PRhhpb%h8BhX&biHVd*{sluK0cWp$p$RFMsFr_^$NZ zAUV7?x-g<5xpsEMmdPC7fBsn`%KiMu4bd4BXH1&ee{lA9pPngJJ#`&*9koqyzXfU& zn>ji=d%C1M*>h_qB(G~f-gdHmSHeAq-weN{EXy6L)Se~QmNvIkPA%*0?CtF7oZ2|8 zV`?u)Z*NzR^z4>dEwftI)UK&r*Yc_I&dc35_ucb;Yq!3=r=z#Mch02QQ)W%SaU%Ph z&1QpLzuB7%k~s>B=ccWn#PLJt&ka%T?@xZRi#qAr>bq)X$EBon2KQD^Xqnk^J>|RD z?2l7B<2&QK5+EViv!i!M?+%WM6T5pR$Zwh4dN8qrqqC!{Q@a1VbjNqesPDp^eI31> zy{!|Q`sPvBJNV;_pIZ)e{)PJ?X{Mh!X3lfdL8GC*+P58}F#nIK%-Q6Qqv?i`_ZYswQ?Z2j?+&`3m zYKW%yrcKRg{;iW8{988dw^YV&u|vO|zbhtwH;DZ%{e7bMcm9-{*&S9h3n!%47B{BX zo!rZwhvze9hsuF+hX_o@H+ zl$M!YGpBI;Q2o7Ml>6O}Z=$DWo~$|D$+2)zPwztc;;f3)(p0nGns&c6D}Fonr)_JW zzINVs%d6j2?|oNVdt}Lp!|N9;u3eek$nN%Pw0_05^Y zvHE+2Xm4~=bXHn!rrGJ{Ee(gSe3!XcGPz@N>!jA+oc^5NT#oO-e|KR`0FB&;J zdP(@i-?odge^0)nbJo7P|4k>y#3`UMWx;}ZvuDql?A7np>f4^x6q;P?Sz@~@bbj)T zipUKWJF9k7?WkVWuy*?D-p>=ezX!FSi59x@8KMidM5Wwmg`+o zF}F5_$Tcb+Upk0SUh{# z!t;lUzJ>3roL4xxWO~3Vhm6olj>4wS;%T*=ot>Rs9UNPJuN1x8yMFrR^lPc}jcj7v zlL9)OS|@Moo8Nh$b8E}CmhBwhHNNi^Jy*HOHXc2C>8ymDgYgsR@e{>0XVuC|?BJE!iPuyD$}Neeh8bxrJ;Ah&Ps@>z## zIewe{ZWZmXm{8eY!O_FkF~4J8=X{PI#(%^`xqn#y_$eBCCdxfWY*U;G&D8T)>f1RmbGR!W;ACs zmCeqZR=d#oJNMl08^65am^XJ-_u|PLOE;8mDBW1ObVt?_jw^qDigKU(@kVs=l%6Tl zdB5f7{g$2kTY;mKePVdm%sv;64)*5WmY$Zr&YAVo%ey!_I=Z_$M1?_o&l{9=!OjvgkXX-*=P7WkvHU7y4fNEnQ>M$kEl>+TALh)Y%cbwVmVp ztZdQmtlBBR^DWe}l6>+)nmGE{TfVzSeRtmS-HBrY`^kwXy7tF&e7FB|MwEO1k6)tm z%_dm28+V3xq_>B2v{wIio>VF~Eptoz$}W!2Z#GQ)1TDE0S`}k{GtQcFr0YoEk=|9k zt9w`N?cFzFFSLG(?22lO=fGfn^%yo``)rR!_!x6W_ZXWgv6IcL?}`Yq|5&ecvcTpGK!^=#|i#?iZV-j<$C z-5lt4h)VA0i2-z#0e1j zz@G8{|NjgOoJTm1FgP+eg4HrIFf)J#Ab1$KziAerHSLH-F~U`PSc3~UU3ppa$ZWMBjjs4z1yvT!mmG4L@+K*gCEBpB47Y!(Iy z20JL5l|h6d49aF>kYLDSNM^`oNMtBrFk~=bh-XM+$Y;o7C}A*U2x3TONM|Tz$YDri qD8i`#n3bl)gf5` literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/cmap10_font1.ttx.cmap b/Tests/ttLib/tables/data/aots/cmap10_font1.ttx.cmap new file mode 100644 index 0000000..dc1b5f4 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/cmap10_font1.ttx.cmap @@ -0,0 +1,12 @@ + + + + + + + 000a0000 0000001a 00000000 00109423 + 00000003 001a001b 0020 + + + + diff --git a/Tests/ttLib/tables/data/aots/cmap10_font2.otf b/Tests/ttLib/tables/data/aots/cmap10_font2.otf new file mode 100644 index 0000000000000000000000000000000000000000..5202f794606f85ed387a7861102cdbea2407df7c GIT binary patch literal 4960 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 + + + + + + 000a0000 00000014 00000000 00000000 + 00000000 + + + + diff --git a/Tests/ttLib/tables/data/aots/cmap12_font1.otf b/Tests/ttLib/tables/data/aots/cmap12_font1.otf new file mode 100644 index 0000000000000000000000000000000000000000..2d74b3a90f20e75cbaad0b03a7cef1e27ca9695d GIT binary patch literal 4980 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 + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/cmap14_font1.otf b/Tests/ttLib/tables/data/aots/cmap14_font1.otf new file mode 100644 index 0000000000000000000000000000000000000000..a8e941d8f9c7c21bb0508163a72947fb8acab58a GIT binary patch literal 5028 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/cmap14_font1.ttx.cmap b/Tests/ttLib/tables/data/aots/cmap14_font1.ttx.cmap new file mode 100644 index 0000000..ced8c8a --- /dev/null +++ b/Tests/ttLib/tables/data/aots/cmap14_font1.ttx.cmap @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/cmap2_font1.otf b/Tests/ttLib/tables/data/aots/cmap2_font1.otf new file mode 100644 index 0000000000000000000000000000000000000000..a123d9ca6c5caae757dac95fbb7f3ec13d94b16f GIT binary patch literal 6000 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/cmap2_font1.ttx.cmap b/Tests/ttLib/tables/data/aots/cmap2_font1.ttx.cmap new file mode 100644 index 0000000..813e750 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/cmap2_font1.ttx.cmap @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/cmap4_font1.otf b/Tests/ttLib/tables/data/aots/cmap4_font1.otf new file mode 100644 index 0000000000000000000000000000000000000000..516ed8e971c94aaef0b21d8131ba146a5ad81889 GIT binary patch literal 4964 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 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/cmap4_font2.otf b/Tests/ttLib/tables/data/aots/cmap4_font2.otf new file mode 100644 index 0000000000000000000000000000000000000000..0f678a3e151003c0f9824abcde99d7060df57c27 GIT binary patch literal 4956 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 + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/cmap4_font3.otf b/Tests/ttLib/tables/data/aots/cmap4_font3.otf new file mode 100644 index 0000000000000000000000000000000000000000..2034ecd43c961ca78a6364349f52706c12a2c894 GIT binary patch literal 4956 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 + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/cmap4_font4.otf b/Tests/ttLib/tables/data/aots/cmap4_font4.otf new file mode 100644 index 0000000000000000000000000000000000000000..450508e3b4a1a69b56083a83dd5d8a63c315e63f GIT binary patch literal 4972 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/cmap6_font1.otf b/Tests/ttLib/tables/data/aots/cmap6_font1.otf new file mode 100644 index 0000000000000000000000000000000000000000..10b64a782b799c299e99edfc1048d5e594ad3169 GIT binary patch literal 4948 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/cmap6_font1.ttx.cmap b/Tests/ttLib/tables/data/aots/cmap6_font1.ttx.cmap new file mode 100644 index 0000000..85a7bc9 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/cmap6_font1.ttx.cmap @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/cmap6_font2.otf b/Tests/ttLib/tables/data/aots/cmap6_font2.otf new file mode 100644 index 0000000000000000000000000000000000000000..2d2957fdaebed024772392ad478444aa7c9cce36 GIT binary patch literal 4944 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 + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/cmap8_font1.otf b/Tests/ttLib/tables/data/aots/cmap8_font1.otf new file mode 100644 index 0000000000000000000000000000000000000000..791b9e3571709d5acb3de2b739b17726cea1acd6 GIT binary patch literal 13224 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 + + + + + + 00080000 00002064 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000007 + 00000034 00000034 00000011 00000035 + 00000035 00000038 00000036 00000036 + 0000000c 00008432 00008434 00000014 + 00009232 00009234 00000017 00109423 + 00109424 0000001a 00109425 00109425 + 00000020 + + + + diff --git a/Tests/ttLib/tables/data/aots/cmap_composition_font1.otf b/Tests/ttLib/tables/data/aots/cmap_composition_font1.otf new file mode 100644 index 0000000000000000000000000000000000000000..c79071739931fd3dbddb10e756809e25648d97f8 GIT binary patch literal 5096 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/cmap_composition_font1.ttx.cmap b/Tests/ttLib/tables/data/aots/cmap_composition_font1.ttx.cmap new file mode 100644 index 0000000..07468c1 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/cmap_composition_font1.ttx.cmap @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/cmap_subtableselection_font1.otf b/Tests/ttLib/tables/data/aots/cmap_subtableselection_font1.otf new file mode 100644 index 0000000000000000000000000000000000000000..8929f8ab238d765e5c42db8c8ece32eb89f247b9 GIT binary patch literal 6412 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/cmap_subtableselection_font1.ttx.cmap b/Tests/ttLib/tables/data/aots/cmap_subtableselection_font1.ttx.cmap new file mode 100644 index 0000000..b83a043 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/cmap_subtableselection_font1.ttx.cmap @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/cmap_subtableselection_font2.otf b/Tests/ttLib/tables/data/aots/cmap_subtableselection_font2.otf new file mode 100644 index 0000000000000000000000000000000000000000..26110921f7f1ac42739adca4b0ad6973de56a865 GIT binary patch literal 6140 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 I!xx1B06eEaI{*Lx literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/cmap_subtableselection_font2.ttx.cmap b/Tests/ttLib/tables/data/aots/cmap_subtableselection_font2.ttx.cmap new file mode 100644 index 0000000..60a5cbc --- /dev/null +++ b/Tests/ttLib/tables/data/aots/cmap_subtableselection_font2.ttx.cmap @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/cmap_subtableselection_font3.otf b/Tests/ttLib/tables/data/aots/cmap_subtableselection_font3.otf new file mode 100644 index 0000000000000000000000000000000000000000..9f39331557ccdb2c533c171140c21cfb52b0e1e5 GIT binary patch literal 5872 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/cmap_subtableselection_font3.ttx.cmap b/Tests/ttLib/tables/data/aots/cmap_subtableselection_font3.ttx.cmap new file mode 100644 index 0000000..d363b8e --- /dev/null +++ b/Tests/ttLib/tables/data/aots/cmap_subtableselection_font3.ttx.cmap @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/cmap_subtableselection_font4.otf b/Tests/ttLib/tables/data/aots/cmap_subtableselection_font4.otf new file mode 100644 index 0000000000000000000000000000000000000000..83ae88efb7ac99885a666b6742743baff4bffbff GIT binary patch literal 5600 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 ah75WP24uSdG-L@HU7G_MHwA|;3IPB%1U)?f literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/cmap_subtableselection_font4.ttx.cmap b/Tests/ttLib/tables/data/aots/cmap_subtableselection_font4.ttx.cmap new file mode 100644 index 0000000..10c4b66 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/cmap_subtableselection_font4.ttx.cmap @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/cmap_subtableselection_font5.otf b/Tests/ttLib/tables/data/aots/cmap_subtableselection_font5.otf new file mode 100644 index 0000000000000000000000000000000000000000..8b614adeb6af16a928739e993e64c54e287d6737 GIT binary patch literal 5332 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 + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos1_1_lookupflag_f1.otf b/Tests/ttLib/tables/data/aots/gpos1_1_lookupflag_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..3245425dfe196a371d130350d826d3420fc6ed8d GIT binary patch literal 5208 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos1_1_lookupflag_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos1_1_lookupflag_f1.ttx.GDEF new file mode 100644 index 0000000..971a3f1 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos1_1_lookupflag_f1.ttx.GDEF @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos1_1_lookupflag_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos1_1_lookupflag_f1.ttx.GPOS new file mode 100644 index 0000000..93d3a05 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos1_1_lookupflag_f1.ttx.GPOS @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos1_1_simple_f1.otf b/Tests/ttLib/tables/data/aots/gpos1_1_simple_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..c5f888822d8a92620d1c78fd8df435533d236a9d GIT binary patch literal 5136 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos1_1_simple_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos1_1_simple_f1.ttx.GPOS new file mode 100644 index 0000000..00eacc3 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos1_1_simple_f1.ttx.GPOS @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos1_1_simple_f2.otf b/Tests/ttLib/tables/data/aots/gpos1_1_simple_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..905d0a398186941bfd2824689c5af9c331f5978b GIT binary patch literal 5136 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos1_1_simple_f2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos1_1_simple_f2.ttx.GPOS new file mode 100644 index 0000000..1eff021 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos1_1_simple_f2.ttx.GPOS @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos1_1_simple_f3.otf b/Tests/ttLib/tables/data/aots/gpos1_1_simple_f3.otf new file mode 100644 index 0000000000000000000000000000000000000000..550be87e84c77554f8910d6c22f981c2afc5e020 GIT binary patch literal 5136 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos1_1_simple_f3.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos1_1_simple_f3.ttx.GPOS new file mode 100644 index 0000000..c3850df --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos1_1_simple_f3.ttx.GPOS @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos1_1_simple_f4.otf b/Tests/ttLib/tables/data/aots/gpos1_1_simple_f4.otf new file mode 100644 index 0000000000000000000000000000000000000000..448bc8b83af8d3f98a20c224fc1586ab39852f70 GIT binary patch literal 5136 zcmeYd3Grv(VrXDsW>9c;b5l?b>uO|RU|7SzAfVzN;2->=<$)3dgTMs_1_lrRV11*U zGogDK7z8C47#I?ga}x^~q#2_b7z8UA7#IRFQWH}$Zh9LqFbJMtU|=xI$Vg2Tt>yZ} zz#zoHz`&rAky}#XP+~QMfk8-xfq~&vZem3NLrMSx1B1{91_lPUyu{p8Aw7*Q1_q%Y z3=9lE3i69f{?BJHVqg#k$ty52u&^*NvM?|(FoKk)l;-AEBKTfE_!)lGGyZ5``q9Ar zLxSbEAP=(~7X!!t?rtc>!`|I3!+Au8BaKy-fnnVjUReeP=3;&jSuQBcz`zhF%fP_S zB*c&gvxAv|fq|8Qfq|WYfq|2Ofq|QWfq|ESfq|cafkBXgfkBvofkBjkfdLd8(hLj? zatsU%iXg8tFfgbyFfeE_FfiycFfbS}FfbT{f`oyA!IFW2!G?i>!JdJE!HI!^!4(v= z3=9n33=9l@AV)ATFoZBLFoZKOFhnshFvNm_nt_2KnSp^Jje&t7lYxODhk=11pMim) zh=GBjl!1W(6#Uf;3=DM)3=E*?YhhquXlGzx=we`C=w)DFn83imFqwgYVHyJi!%PMS zhB*uj4D%Tn7#1-wFf3(YU|7Mxz_6Nufngm31H(oJ28JyR3=G>D7#MajFfi<8U|=}F zz`$^rfq~%|0|UcJ1_p*R3=9nC85kHYF)%P(Wnf^q!N9<9n}LDh9s>izLk0$hCkzY> z&lwmPUNJB*yk%fu07c(t1_p+23=9lE85kJ;FfcIuXJBAtVqjooWnf_BU|?Y6W?*3C zV_;wuWME(vVPIesXJBBIVqjpDWnf@bU|?WWW?*1UV_;y+PB%z5OgBn5PB%$6O*cz7 zPq#?7OgA)0H#AH)G)gx#PB%13H#AK*G)p%$PdBtkH?&MQGDtTvOgA!0H!@B)GD$Zw zO*b-2H!@E*vPd_wOgA=2H#ST+HcB@(PB%75H#SW-HcK})PdBzmH?~YSF-SKtOgAw~ zH!)5(F-bQuO*b)1H!)8)u}C+uOgA-1H#JN*HA*)%PB%44H#JQ+HA^=&PdBwlH?>SR zGe|cxOgA%1H#1H*Gf6iyO*b=3H#1K+vq(3yOgA@3H#bZ-H%d1*PB%A6H#bc;H%m7+ zPdB$nH@8f;Fi5vBOt&yfw=hn(FiE#CO}8*hw=hq)ut>MCOt&;hw=_(*G)lKLPPa5k zw=_++G)uQMPq(y4x3qN5FQ_caOwTA$&`8!)FfuSOQgBSkPfAq?t}HG|%`H~&%uCKM zD#$NNEJ;n#Q*g}5Q3wJlFIEUjElw>eOHI*(WG)6q#*QS06j*j(gJuB%XtI}LU|^7k zCU;E+1_nK7GPhu0V6cTIZ+8X;1|Mj$4r5?oh=wNTR0alyENC(=VPIgWgeKo+1_p)> zXtJHez`!sanp_t$Ffc5GCesZJ3=CVL$#Xvg1H%z$vOLGYz;GFw9PctPFg$`L!#4~J z3?HG%?>D6M04F;x1_nlcXmXQeU|^JkCNm8N21Z>521X+W21auR21XkO21Z8)21Yjq z21aiN2F3se2F6eZ2F54`2F7>>2F4Ty2F6SV2F5%F2F79r2F3~o2F6+j2F4}^2F7*< z2F4x+2F8gD42;tl7#L?WFfcA)U|?Lzz`(eQfq`*70|Vn01_s8R3=E9>7#J82GcYio zU|?W8%fP^RiGhLfIs*ga9R>!*hYSpi&lngOUo$W;eqdl={A$2uz{RJ)r3fOFK!h@g zPyrFDAVLj9sDlU%5TVJ%r^uxMA{0S{5{OU+5h@@;6-20k2z3yl0U|WH_>@38lt6YU zaVdeg${<1oM5uxYH4vc=A~ZmRCKsPFNQW{=hcZZqGM6$)Oa(-!f(SJbp$;N6K!he2 zp9)Ba3P^_vNQVkYhYFVpNJ14vsDTJ|5TOAgG`aXxK{`}HI#fYAR6#maK{`~qR6(+8 zAVM8PXn+V!E=fMOXUK!3KA~KvuM8Wl)FoO^*Z-c6h)eH=b zVhjw7YZ(}r92gjwjx#VY+c7XOPiA0X31?tn@l=J@{e&l*wg=Raz>!|CfZHoIXP@CAy(b?J4CEdxMTQebfUHkF2lkK|_?m7Hs_$_5w?og%n zEU~tdpUZ0yLzN&x6Epp)v~5`P3^jtPnCCG?!LM2p7&e3 z_3b?!z3shoCe5BQYx<27+23q78|?bc-ei!>QBXWLZT%#UA3A?-h;n~_@{?WEN!M22 zRVzC#C8aaCw|YX$%$DmZ-^FHsoZ1=R8Q+xv3BjHny*qk$a7>)o-7`Uc%jDLBi5(oB z9bKK${okcKzDq`Z7w+uq=jevT47iGJcC4`tAH(G4Z=W?04z! z6TQFlr`*i$u$oyoA-%S^F})_gvaoR8hg09hzO&8cSUq7?^J=J79KUD({v*2o$al?4 zTYodHF}M32`kQr)=E}TJ{m-Yg%k2=+p#}wTl@62^S)bN{jPfNyVBYtOHLeKzhH6g%Ir>#)|jZ~Xla*T zk6Er&(8Tw{?61Bk_xG(oEkuo{E2+1bc9k?1PAQ#IHo0U*`NZbQEmPVOYjO(9f^saj z{FZx?xuj`v^|GF)y{&B*cXAxx)b^dV`Do|jx`j;(o99=~s+rv~sj0uIuWv%%guaQ} z?w394c-lI@YI1Ey|W-HHf z`mS*M-l{bVJGV}n*FLXr&Loc2-y=kOqno0$(sDD+PB(99IDF;1%*B$)9g|xpwf5%p z=k(@sd=LIJL6rLk|4)9=$l1|L!YBT=U6lQM@*SPC_SOAwIyojz0hK8W7R;MHd(LF9 zey>*F_N=DR3E9BY0Dh|X}I?J>_Yu`RhVv1iGp6_*xX z;rPDrPlzb@fgc}5D}tNKJN!EgmKpXL^tp9Rtn=XL=xOiilxwS=TCuqEU7tR=@kX}{23w`j)V*~=E5KUDNBd|&0f!pSAm16Da?gjRACHgy(HtL^OU?Ck2` z*z$X&=-uA+(=VrAOPz0I6XTu~(CO4Vd0XH7&I6rWTeh`q=lHJieXr=b%1yQrL1F&Z z_4f4~UEN(>(mgHgo!wnMU2?_M6Ejw|U7NOh+TP`r6DucF^(OWwwkCA7?d;k)b?1bI zQ|3)tz%i+7V#frzeRG%3I$X=~+w6C%Xn)0o%Ki$D9=4A89rHTpbNn#=BQDDQ!}`Zh z(YU24JHjXaj$Y>ZM|$ILfnC3~3s!z-H{ocXFsXB*^sNaO=50PRd)c&wi{>v`UbrT+ zzOJF6siC&Eq9m}aHM23JIism;cHXqwh0fo(=YHS##v(#>%BT zvX*dM`SVkh``nK=qLZieOp(s}EkEzK?A+f99G&bF!@Fkoxo~u_H}|&mwDfh(te;-q z#nI8x-PIxA(B9tB(f|&CPPvYnzNTpn_wBz&oj&y7#dnoO-}(H$n=~#fnpe5d_u6mi z8k0tjuGZG>R_Ub9j?k^`9N%YUi+*R-PWhd0p_Y~8lNZv&(Z}BM-8JgF^Oo;U923}2 zPCU`IKc3^e{hu?U-1~q05}j{0!K&T3GrS|cJ)EPp`nU6>Qn_iFTiRE4aeRKWVd5uf z$*s_;81tKP)|4Y%NBWNRuIgRgyJ~Omz6pDw^;=|DR9i%6-_4Gj)i*g_EcnjR{k(Es z`Rb{?tGoKT`?~ttX0=YL?pIv=TdzYgm!qn+wZ1{BtE0Q4r-S21$e&-L+~QJJVu_g5QiKzZs_fX6mnASk%^7o!;6VwYi<+ zhwEP^QSKid-J;yw44|O_69zQ~E(S&h7BG*Ifr9~rIsRKPFfuSP2r-B-FqWhimoRWZ zhY}cb5=-(JI2b@}A%_3|!Md0@k1$SPU}fN8{KEK!;TMAum}X%70%9>RFmbUlv$1kB zF@f}e>;#Q)fU%>ClPiM*gCj!#C#YX?0z^KrXZ-*FKLZ2j5zZqFjtq`qg-i_0V4DmW zm>IYj7#Kttc);Q^U=rF*fp+{seo^frlq(K|H_B zR|ZDbI}8}$|2NJf9H2o2W(FpxBqPXG3=9k>IFB&uFzRp~0lNt*38olX?|@{60|xmg zfPo@8g_D7iL5YC@%4TBVV^D&!nHiKAK(WKf!pXqGpu`Xa6=!7-VJLyJ z*%*`A(J7Op@1QWA(bJXA&tR=A&4QBA)TR=A%`K6p@<0e ogiQ=%NM$Hu0NIq!kjG%ipvPc9gt?$0Ezs!89MHHLI2=$201(bPL;wH) literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos1_1_simple_f4.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos1_1_simple_f4.ttx.GPOS new file mode 100644 index 0000000..f80286c --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos1_1_simple_f4.ttx.GPOS @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos1_2_font1.otf b/Tests/ttLib/tables/data/aots/gpos1_2_font1.otf new file mode 100644 index 0000000000000000000000000000000000000000..3e7b7bc4228e4107bc7d763386ba728f15fa5e7b GIT binary patch literal 5108 zcmeYd3Grv(VrXDsW>9c;b5l?b>uO|RU|7SzAfVzN;2-=#`+fuigTMs_1_mGhV11*U zGogDK7zAY)7#I?ga}x^~q#2_b7zAq=7#IRFQWH}$Zh9LqFbJMuU|=xI$Vg2Tt>yZ} zz#zoJz`&rAky}#XP+~QMfk8-vfq~&vZem3NLrMSx1B1{P1_lPUyu{p8zC)^V3=Bel z7#J9C6yz6|{GZQY#K0hYg@J)VfsuiQg@F-d6eCD^N@;FxC4%qugP-9?J>!oCrXLN= zKO|Uw3-U0_aWQcG@9u_DJnY@wGMq!H23=9l`vJ4FD zOhOE4FgutT7#LU?7#P?Y7#KJi7#O%27#MgN7#R2&7#IW@7#M^Z7#Ktu7#JiN7#O4( z7#QRj7#I{mUS(ilP-kFZ&|+X<&}Cp?FkoO{Fa`w)0|SF40|SE%0|SFS0|SE-0|SFA z0|SEx0|SFM0|SE}$Po+-3?U2*4B-q63{eaW46&e~W?*1QW?*1QV_;y&WME*(VPIg$ zXJB9`Vqjn>Wnf^aU|?XVW?*2bV_;waMPCa814BCl149=B14AzZ1H%Lc28PKD3=Gp4 z7#LzU|{&nz`*d0fq~&C0|NsndjB&pFfuVPFtRc*Fmf<3Fmf|6 zF!C`lFbXm-Fp4lRFp4uUFiJ5nFv>D8Fe)%GFe)=JFs3muFlMJ4q#LFir5mT4q?@Lj zrJJW)q+6yN8l)Q8KoN;ryH51 z8=0mXnWY<AZ>rW+fj8ylw^o1`0?rW>238=I#aTcjIXrkfa~n;52> z7^RySr<<6ho0z7Xn5CPTr<+)$n^>ls8l;;Vrkfh2n;NH^nxvbWrkk3jo0_MaTBMs= zrkfe0n;E8?8Ks*Ur<<9io0+DYnWdYVr<+-%n^~rt8>E{Xrkfk3n;WN_o1~kYrkk6k zo13SbTcn#?rdt@KTNtKW7^Pbnr(2k$TbQO>n5A2or(0O0TUe%B8l+nqrdt}NTN^7bTXYrsyd+=Hw^@fs_|31f>?I7L}!@=s_|U10!Qc5UsN{eq>o5iehG=MVPGw+V$bu&05(Wl_N@((JW?*3G zfF|2X3=9m@p~-b20|Ub{XfoZvz`(E-nmqS2Ffbf}Cd+dS3=Ef{$?+}&1H&U|GJM0p z!0-{8{C+bqfJzT=vg2Z4VC07;H%SHtMmcCQ(_mm=)Ma2`G-6<2G-qI7v|(UibYx&) zbYoy(^k!gS3}9ej3}s+ojACG5jAvkAOkrSP%w%9-%wu3+EM{O}tYBbZtYu(eY+_(w zY-eC#>|tPFoXEhyIE{gUaW(@3;{pZ-#-$7ljH?(J7}ql}Fm7RBVBE>Tz_^crf$=Z{ z1LFw>2F9}t42+i;7#OcJFfiU>U|@X6z`*#7fr0Tg0|Vm+1_s8j23!VQd?LzPPv zB&!A@)Io#>h|uKXQv>Nx1L;r$=}-gdPy^{u1L;uXQUfVb2N4<|LX(S69i&4Yq(dE~ zLmi|;9i&4Yq(dE~L!C<#!uE%~Xs_;XEQD!+As$T;B;pYGzP|096~S z85kJF7#JAWGB7YXFfcG3XJBBqV_;yO%)r1B&cMLR&%nUi1gfYR7})MGFtArLFmSLi zFmTi|FmN<6FmN<~SN*9g>X?xK+p5)}@wdQ&-v!?_;yD^NY@W19dgA21{(kuj$2YC& z{nWF4!nXEpEsr9<=Nj)Qo{`s+KdHW}xxKZey0f~Yu9@RIU!>?8w^hHpY82{p60LtT zuL+->d#HM6_S>%St6!a;F=cM&tp1*!-k#o`nVmB_r!{kYul@5|l>58$PfgLD-`Nv? zXZQTh?oT|}*Sn?bdrs^3tgi3b4eJuSThchb-~6j7%KcsW#~RUN3r}o1-FI_BpH_CX z{cq0S3qqQH+jQnk%bk!jp>aan)V94fM%JE znf(W6fA{H`V%1aEQP)x16!%-8HnEwbv$Lm5x|2P(WkX1a`g6g^+?Zdnbk6@Wlimx+I20TD(}49eRJPE z@3(gA+j}~C+k59snmuLK^cyF#zu9ay*!7#e$sn1dpm=WD`biu=bpG5B<^KNUC%dSV zuC2bSR(4!UN@s9y^@Nt0E!R`Ni_QKxwKKjmzAFI|f;~HWcl7Sym^iV!XM+5e$*l(y zJ2*N!x;mx%ze{&~myG%@+}YRB+u7SXvAMsgi=(x*qrF|evkL?`epmg`5uGw~;>;;C zW-s2bZ_dKqr}v)Rdvb+R&u^yo-;5?rzs2h!=X3m!`<*Y!z2&EvXisNPSC>>vZ_9)> zxmj*5N+;qjMSjmZzWX=Fl=8mfj?%i{A`uzCd0JDueA^1w<}J+1pPjWS;#&8y-g^_S zFW7T|W6ApN-P5+tyPU8safk2u-^NAuOUt)yKiqou!_1FupSmE?_?xlkH`9dQ3{@Om zJ>A_sQblXx3g@PB{LubuD$4ys`KN|xdT-j)jOO1u*}=bM(|${3{1!X(+xfd<;&+4C z@6z8VdVl9nxtZNzHM4L+dTnuIdQE<1Vd1M|A&@ z@0ypk{$^TZZudL%H|rYBm3g1~pHFF-*)?+t#}C!t`$f6m{rDz&YUatB)14d(7xnZm zlrPSzNG(k@`>kpBTeIS~V}IJV_UUWqeYd>&UG?5~rL{+voH)FG!Q$GL*_|A%F;UIY z(k{Iovs|m7iSLKmUwu*T?^}Obh#F5x$Wj#-OTiY(~m?K^Ap(ayzn3!4@;&##dvkHKloJ<&4yc`HNK&bbs^A zR-WbbUE%hCNT%9{gv5DEANkpZubcv!j=UPyB7WDEs&1J343WtNY({a!i~8DpM9Lm^XX& zoXKAOUah|ESxuqIwVoxmyF%wD&!~vpP_eUWN7at%RSj#WukQUkvHN>a`$>+j^=<3h z)^{#yU)-^1#=ffg^Y%@c-9Dpf@~mmyi^^wnIGc<1X7*=H$mpnOuWoIrC@n27E356P z?XKwwJDPcH?)?c1X3d#8cfq6uvln)3Y1>$_xt61=v$LzKv$n6MslANjd+49tqTJu3 zehP^8H1{<1Hn+!i)mHh&|2DV!tx@sY4ic4j4Ld!>F4%>EwEF{x*A&t$pYB^A@FI;wgrYx{~g*8C0-o#8&)W1eSXTXJJ!&yq_k zE-k#m@qOW+5K-;}KR$|91UHp;_;(mAGwd_ybL*H`=fTm@)85l5*H%5XVsYomiQlz* z4h?d>MWjC+u7OK z+10_Z<@ZX_yS?kDUrxW4I^W19#yu&Z)2Vgxw!ZnD2RgU5Y-`!h@m=HlUeR-vn`|S3 z!u+l4?dv(Zy1Tlhds^5#ySsY2P3UUd*|l@( z&It>r%$u};V^Y_|jtO%6<}ROgxR&F$+3!}-{)!2e{S_QNY#sAE=5@~J_+k7{l4+b8;*H%S9LF*ys>mc>4wsc zl}mSIE#bKG=cg$5xgT#tCr{~_BAxeJe%^1{xxW=SI@u?Ncg^f`;pkv*?rrI5>Fb)mw6E;q`21$W z#81$YTcK4k<~QT4DMz}F^d0G4)w{ZP)!yEH6ZS&ux5%!jwusKYn;kc+Z*sg?@SUUk zdF8zF)l+*{clCAmb@jE)YMoTwuekWPUWZ~XM^$TUeS=h2M|Ve02gi?)Kfgq|&;4W) zoxs+YQ<>SG+n(K1-<8Or%ii43Q#-k-_c!ANP{GRxsaCqawtnmUc74{(>YH;`&8^>( z-sxQJG{dE_Yg^B@-fbMcTjy=*+4P;M{cFp2ro|8izZpw@Gfe%>)L*@@sI9R&y|p`P zb34Zm*S}1n+&?Ghk=oS>A%IlDty0Q!Z?9}m4S!x3*#4tUkpZInt|~Ph{eFb z#Kp$U#>&ma1kwYtmjMPGU7TDQ92guK0ysf^lM^8Ffj#5@|Nj{nIFE21VQ^${1gm9Y zUs$O3Cs*kPzgqms~8vkh64uqCxC$=1w=EjG5CREfQ6HRkpa}3WoBSx;bdT9;A4=2iZe4vF=#;9EDTZ% zPEa;0g9t+ml+DH<#ZbVI&QQRR&rr-@$Pmw9#1PMr#*ojD$56sx$PmPk%8<@b%8XG(!DbpV=rI`JF$Xkk#lXPue-3Eu4D3%70ssO`HAw&f literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos1_2_font1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos1_2_font1.ttx.GPOS new file mode 100644 index 0000000..11351a6 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos1_2_font1.ttx.GPOS @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos1_2_font2.otf b/Tests/ttLib/tables/data/aots/gpos1_2_font2.otf new file mode 100644 index 0000000000000000000000000000000000000000..ba9d224f705ed0b39df0c6fac9bc1916a6ed195c GIT binary patch literal 5148 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 + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos1_2_font2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos1_2_font2.ttx.GPOS new file mode 100644 index 0000000..88257ac --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos1_2_font2.ttx.GPOS @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos2_1_font6.otf b/Tests/ttLib/tables/data/aots/gpos2_1_font6.otf new file mode 100644 index 0000000000000000000000000000000000000000..cd4ea94c6e53f52176e5b1097cc4528985da82f0 GIT binary patch literal 5120 zcmeYd3Grv(VrXDsW>9c;b5l?b>uO|RU|7SzAfVzN;2-?_-`$%G3<4Jz7#Je_gY}Ja z&V=q|U=Y+`U|>i{&P^;}kYofq}s+BO^6Yw3h1= z1A~wN0|SFfMs7)kLy6T41_mJ$1_p*xxrr483@HH&3=F~y3=9lxd5O8He8*LF85o2) z7#J9C6yz6|{GZQY#K0i@fPsNQfsuiQg@KWUfq{V$q&%fGH@6bO_xi!l@S~pbM+4K3 z2Ie0UEWZVLnB}+_IR1BcLn$8i?rs^*BQhLmtg;LY>%Q>HGB7X~^MlB8L0JX{hCo>c z26iSPhBTNR%nS?+tPBhc>3vhGB7YmFfcGkGcYj7 zF)%PFg1pMWz@W~+z@Wvzz@W>(z+k|@z+emt5(WkaO9lo88wLgjdjhkk7!t zP{hE%P|Co-P{F{!P|d)=P{+W)0E)gA1_p+91_p*M1_p*+1_p)+3=9mD85kI*F)%R9 zWME*J!@$5WpMim45d#CmQU(Tw6$}gvs~H#=)-f+S8Fq~vyU^v6Tz;K>{f#DJZ1H)BNR536x+-6{4xW~Z2@Q{Il;Ryo+ z!*d1(hF1&>3~w127(OsCFnnfUVED$s!0?lSf#DAW1H*p?21X_Z21Zr}21X7B21afM z21Y&x21Y>!21XGE21aoP21Y3c21Z#121W%221aED2F5f72FC1kgLK1mqjckRlXTN` zvvl)xi*(C$LxXff!*oNVbVK8GLz8qv({w|#bVKuWLyL4n%XA}ybR)xbBcpU9<8&jF zbR*MrBeQfP^K>JNbR)}jV}o>K!*pY#bYtUmW0Q1a({y9AbYt^$V~ccS%XAZibQ8mL z6Qgt!<8%{~bQ9Bb6SH&^^K=u7bQ8;TQ-gF(dbTi9zbAxnq!*p|_baUf$bCYy)({yvQ zbaV4`bBlCy%XABabPL0D3!`)k<8%v?bPLmT3$t_!^K=V~bPLOLOM`Sv!*oldbW7uO zOOtd<({xL-bW8JeON(?%OXvK8%A(Blj1mQnWK9Jl0|O%k$CUh}RE6Nm;*!+dVg=8< zQlTXJBAB0!^0Z7#J8XLzClO1_p*l z&}8_Afq~&8H2M98lpf$@$Hlh6hMR`h)@C%${<1oM5uxYH4vc=A~ZmRCKsO) zNQV;04ka!n5LX#QsDKDn5TOPl)Io#>h|uKXQwHf!2I)`+=}_iU28pSF2vrcF1|rl! zga(Mv@p$5{S2GXGh(xJws22!F9A~ZmRCKsPNNQXK| zhdM}yI!K2)NQXK|hdM}yI+r>~od$@|Cgb_&;aSs0O`;G>Cgb_&;aSs0O`=+ z(f}#eCgn}&;;qw1nJNO>Cgn}&;;qw1nJNO>CoiT6xm@Af6%U1l6w z@_X76w(skg{5ibj@)EBl?C5eF2szHxTo1Tc^YA>Fz{4xUc|=5p^N1+8z7u8;g5_;c zwXvFkfl-WsfpIMZ1Cs*-1JiK^24*`32Ik2O3@qUc46OVN46IF{ikg9e?G6J2dnE${ z2MYrOM=b*bM-u}BNAq{ppSq%s3HiURS{)jH3oQ6u@LeOGqhZ75Nt>i6PVVdPm%ng) z)2iN2J=-U2Yv0!LDDr!*@s8pdc|G})>bsiTTU)9-t2^qNIll8nioS7M^}DM^p-v~! z`Zx2M@Y%VCs&{6;?fSm@)%h7y=625N@9F97>Ft@>Iiqu0GspMZKfgt}zdQfb6z%z) zJ@I#T&+qL1#DjgkTe`mIw0_U(`kvjeF0s2MjpO^xznY@l-<5x?5k0o>#HQ1IHz)LI zWk=ip=KQ@Nr0KU!XU??T2{{uQC$vp%+go#V^4^{^J!kvQ^lX^ay)|PRhhpb%h8BhX z&biHVd*{sluK0cWp$p$RFMsFr_^$NZAUV7?x-g<5xpsEMmdPC7fBsn`%KiMu4bd4B zXH1&ee{lA9pPngJJ#`&*9koqyzXfU&n>ji=d%C1M*>h_qB(G~f-gdHmSHeAq-weN{ zEXy6L)Se~QmNvIkPA%*0?CtF7oZ2|8V`?u)Z*NzR^z4>dEwftI)UK&r*Yc_I&dc35 z_ucb;Yq!3=r=z#Mch02QQ)W%SaU%Ph&1QpLzuB7%k~s>B=ccWn#PLJt&ka%T?@xZR zi#qAr>bq)X$EBon2KQD^Xqnk^J>|RD?2l7B<2&QK5+EViv!i!M?+%WM6T5pR$Zwh4 zdN8qrqqC!{Q@a1VbjNqesPDp^eI31>y{!|Q`sPvBJNV;_pIZ)e{)PJ?X{Mh!X3lfdL8GC*+P58}F z#nIK%-Q6Qqv?i`_ZYswQ?Z2j?+&`3mYKW%yrcKRg{;iW8{988dw^YV&u|vO|zbhtw zH;DZ%{e7bMcm9-{*&S9h3n!%47B{BXo!rZwhvze9hsuF+hX_o@H+l$M!YGpBI;Q2o7Ml>6O}Z=$DWo~$|D$+2)z zPwztc;;f3)(p0nGns&c6D}Fonr)_JWzINVs%d6j2?|oNVdt}Lp!|N9;u3eek$nN%Pw0_05^YvHE+2Xm4~=bXHn!rrGJ{Ee(gSe3!XcGPz@N z>!jA+oc^5NT#oO-e|KR`0FB&;JdP(@i-?odge^0)nbJo7P|4k>y#3`UMWx;}Z zvuDql?A7np>f4^x6q;P?Sz@~@bbj)TipUKWJF9k7?WkVWuy*?D-p>=ezX!FSi59x@8KMidM5Wwmg`+oF}F5_$Tcb+Upk0SUh{#!t;lUzJ>3roL4xxWO~3Vhm6olj>4wS;%T*= zot>Rs9UNPJuN1x8yMFrR^lPc}jcj7vlL9)OS|@Moo8Nh$b8E}CmhBwhHNNi^Jy*HO zHXc2C>8ymDgYgsR@e{>0XVuC|?B zJE!iPuyD$}Neeh8bxrJ;Ah&Ps@>z##Iewe{ZWZmXm{8eY!O_FkF~4J8=X{PI#(%^` zxqn#y_$eB zCCdxfWY*U;G&D8T)>f1RmbGR!W;ACsmCeqZR=d#oJNMl08^65am^XJ-_u|PLOE;8m zDBW1ObVt?_jw^qDigKU(@kVs=l%6TldB5f7{g$2kTY;mKePVdm%sv;64)*5WmY$Zr z&YAVo%ey!_I=Z_$M1?_o&l{9=!OjvgkXX-*=P7WkvHU z7y4fNEnQ>M$kEl>+TALh)Y%cbwVmVptZdQmtlBBR^DWe}l6>+)nmGE{TfVzSeRtmS z-HBrY`^kwXy7tF&e7FB|MwEO1k6)tm%_dm28+V3xq_>B2v{wIio>VF~Eptoz$}W!2 zZ#GQ)1TDE0S`}k{GtQcFr0YoEk=|9kt9w`N?cFzFFSLG(?22lO=fGfn^%yo``)rR!_!x6W_ZXWgv6IcL?} z`Yq|5&ecvcTpGK!^=#|i#?iZV-j<$C-VMf+FAerHSL0$@AU`PSc3~UU3pcr7`1g91TD4U6ak3kB` zW@eCL(15a87^E1SplntK5r!Bjn~gz=p@1Qsp@1Qup_svlA)djIA)X!)LCkg=o D6OK7X literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos2_1_font6.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos2_1_font6.ttx.GPOS new file mode 100644 index 0000000..db1315b --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos2_1_font6.ttx.GPOS @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos2_1_font7.otf b/Tests/ttLib/tables/data/aots/gpos2_1_font7.otf new file mode 100644 index 0000000000000000000000000000000000000000..2871acc01b9c2e9c6023a865958da0de5c0fd309 GIT binary patch literal 5132 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 V13cz{hO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f1.otf b/Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..600378243f7e08d2b9d1ddd182c5899d19e2c0f1 GIT binary patch literal 5220 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f1.ttx.GDEF new file mode 100644 index 0000000..971a3f1 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f1.ttx.GDEF @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f1.ttx.GPOS new file mode 100644 index 0000000..06b0691 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f1.ttx.GPOS @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f2.otf b/Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..9d0a273c0318bf44b520656a0e230b4efa84e440 GIT binary patch literal 5220 zcmeYd3Grv(W@unwW-xGeb5l?b>uO|RVA#XJAfV#z;_Akr#30PTAn<^JfkDhYz(4o_ z$BRq`27w<83=Bd3!TLryXF~TfFbMiEFfb$}=Oz{~NHaz=FbJ+;U|N_)*XJqk-v11M?3FmfwOr%yL`|9RIt!p%f2$cef1Z5gCp&R#^sybzgX8 z85o$0`9Wm4pezFeL!c}J13QxtLmJEuW(EcZRt5$Jb_NCpP6h@BZUzPhUIqpReg*~x zL6Ex{7#Ktu7#JiN7#O4(7#QRj7#I{mUS(ilP-kFZ&|+X<&}Cp?FkoO{Fa`w)0|SF4 z0|SE%0|SFS0|SE-0|SFA0|SEx0|SFM0|SE}$Po+-3?U2*4B-q63{eaW46&e~W?*1Q zW?*1QV_;y&WME*(VPIg$XJB9`Vqjn>Wnf^aU|?XVW?*2bV_;waMP~~G14BCl149=B z14AzZ1H%Lc28PKD3=Gp47#L7#_V*1bi;I`bmMfBbklURbn|qJbjx%@gLFf~bVH+bL*sNqlXOGVbVIXrL-TY) zi*!TFbR&awBg1qfqjV$VbR&~=Bhz#vvvec#bR&y&Bg=GSgLGrVbYr7*W8-vVlXPR# zbYru0WAk)li*#elbQ6Pg6T@^9qjVGFbQ6lbQ6no6U%f{gLG5FbW@{r zQ{!|~lXO$lbW^i*Q}c9Fi*!@VbTfl=GsAQy zgLHGlbaSJ0bK`V#lXP>_baS(GbMtg_i*$3#bPI!Y3&V5^qjU@7bPJPo3)6H9vvdpd zbPJ1g3(Is%gLF&7bW5XjOXGA)lXOedbW5{zOY?L~i*!p%=lp`oqRjM+5(SN9O$8$Z z10w~;l>DSrh2YBKlGNN{1<$sUoE(K9kn&=Mpw!~jqO#N!JxJza zU}Wq_Vn~5y2R3LH5P&9oDFy}xd1!LiWME*>gC=te1_lOOX!3SvU|;~17J&>53}Flm z4AIc!oXWt!kOfV~B@7Gz`$t4z`$tEz`$t3z`*Fpz`*Fnz`*Fuz`z*5z`z*Fz`z*Az`z*Kz`&Toz`&Tv zz`&Ttz`$6{z`$6+z`$6`z`)qVz`)qfz`)qUz`!_>fq`)v0|Vo11_s6j3=E7*85kH> zF)%Q$XJBC5!oa||lYxP89|Hs9VFm`q6ATQDXBik6FEKDMUT0unyu-l2_>h5t@fiaH z<7);6#t#e(j9(4747m6dxD-Kz5{OU+5h@@;6-20k2z3yl0U|WH_!PMmK!hTQPy!Lk zAVLL1sDcPJ5TOntG(dzV7oQSHhZ4vRB`zfpR~ba8fCyC(p#~z@p$5{S2GXGh(xC>@ zp~j^KQlbtbG(dzV7oR#vhdM}yI!K2)NQXK|hdM}yI!K2)mpVwD28ht);?n@>&;aSs z0O`;G>Cgb_&;aSs0O`;G>CoWP04dhw;?o4_&;;qw1nJNO>Cgn}&;;qw1nJNO>Cgn} z(B#tO;wxJ6Q+3Jj_9fq4W*l1bd)gAV@9UTRIlScZ60arf=yDtgInLEw54czJ@I08n z!z;siL_~)3h$y(e6J`*CFMq1?U~s* zqjOp_$M@PlzeTyfJO9)a?fIQO@ppF5@9h4>gMGbQy1wVMe$VRqp53r6vAZRW9TXS^s-kvi( zXZz0dY?#!&HDelwV&`v$7KQ%Kxy^HX=gj`D_ z?3P(Avs%{FuBlzu@~QI9%iTBk-Sd8Hx4ylnqqn_x&ZOB>W=+3wBKw=oW`kY7*_#ZK zISPvBrmdgE@k8g&4N>mzPkyqCI_cW#yJ}^}rKEHQ_f}76nb~qZ<-6GIk5fD2JL9_& zAR*YZqjyK|4vvWvyL%?cZ<*YBFtLN9v!kn1y8pX$$9Ku7@4}sZ9lf2strMI3o4Pn! zTRYm@()3%rE^%7Ye+Y)#9p8suJWWTh0+xEk) zXFtsR*!HOl5{tRK{TI*R~8n|`*7;J z*mt(M9IGd+YF-VsisSd}-+x5+ANj6%Y3pyMHRg7|Lw~cb(OjALssH(umYH2Mr*QmG z{k>n5``wRkqNiq_tU2Auv2amO??UlZApU76j<(Haxg94+nA>oLo<3Yz$SnEllk<^I0)r-i8TbS3o`)2@=n z!YQRw$|jf0D4*Coxn)XQVogqASx}C}mfvzuGM6+hu3pyjw70eG;!cj^o7%p!HXrR= zT(_`kVe|aTSv9j;CN=dp_4Q5Yo6t9L+x@af9Zy^5S52<%Xq?h8xvqz!{tts_Z)a~; zU)S{Z8C_GFmsZY5otVE^B|-N$&urycPTv)7-&?h2VdvIK^V;Y2&6&ip`g??EZ*)_1 zR$6YR+3Dsj4TrCMm$_Inxnpwcq}JY?{+!-ij_<*LCWvzX;Qz@l8aX?9N%+Lywu`cV zPrjpb*1o#`O()01DWEcC!Gd|SXV01J)$i5n+n&`Fnq2EyV!JDJe)5cp$PE=ct9DfF zs9x2ucKYhx&l9`92eqH%=vv>lzHNQyqV~ldi)QSrnm=#fgxT#gnkLVh*1f2FHixsh zXm4hJ#)OQHiuUT(mWtBS^0Knpp4#r3p0J~t$L8Ljuwd4lsdE=hS}=QI$CkE@6`N~0 zx;i_%x;kt7YMR>1IKGGe*)7WbJ?f``XisxbQ*U#7Y*%fSZ~Sj_tKS+GzwIDVIqCSU zqjL`*YTrNeVE^3E`Jqd~=B4eOSGSqN;=5Ps_rUD$;T)5CCihI1>s?YYy{eSd53?8!7{@>gFd&8 ziFFlV#eJbT%~^M{JQh3~7J zS2($3dcZ1&jL=Gs!lusRX|`6usNKe){F~YpL^%Y+~G#0y>>qCvWSU z-+7>OYs zP}yI>(Zkj;zhhqKe2yQ+f5b((e^~$cDH^vlWk>kL-_grF|448AEwJmicEQT;>?R!T z6DD;|l)g3L!o1CAW-ptzaMAoF%L~_J*4H&OG&R)LR+I#mwPrSEG-ouG&CZ)vyU_VN z_uTIrzr5j?H+NO{;>jCJHqy^`-c`M;dspr4-8W$` zw0?{1ifW7K?7P`J6%l>6LICeaCOeL0nx?YZsQJ@s9Q9J=hy4L!A!n|gmUP5>3W zjF4)j>uc+`&TrRe-K@SjXVu*LE$N-k)lM^98oRdjZ0p^|(YtltmYz-DncBa$d}mq= zQSh6w5<10w?q0}}&Cj)Q@bK?KZWWZ(g@|6BZ@!@$VE$RNzXSdv;?!oUF?Y+%eu zEXiZwU;wq282=zRL*^`M{p>|Ns9C44g+ek1#kgID!>3F))MeGGJh4 z;9_845MhA$O$ICr?aDw02SEN(U|@jyhlwE>WEuk_gaoSxxq*>^;T+GSBk}w;Ul|x# z?|{Woi2vU>k8pqn8kiZFpc0HAS1~X!oZvjdsKcnkc?9exs3e$TWW58D84eiap8y7i z6cEk8#^48v0TxaMM(|(`GXoPLYY&Hfh zh6N1i3 + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f2.ttx.GPOS new file mode 100644 index 0000000..03e9f8b --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos2_1_lookupflag_f2.ttx.GPOS @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos2_1_next_glyph_f1.otf b/Tests/ttLib/tables/data/aots/gpos2_1_next_glyph_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..64d6c2c97c7a7f98cb5c79d71605f94c54d83a0e GIT binary patch literal 5180 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos2_1_next_glyph_f2.otf b/Tests/ttLib/tables/data/aots/gpos2_1_next_glyph_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..3d8c37ae724e82e0cc025a6dc2b2a7868f803916 GIT binary patch literal 5176 zcmeYd3Grv(VrXDsW>9c;b5l?b>uO|RU|7SzAfVzN;2-?5>iz}>27wC<3=9GO!TLry zXF~TfFbFCzFfb$}=Oz{~NHaz=FbFn)+m$StXGD6yKsz#ycO z$ipnh#lZ2uyBkXJuy=RMa2}E2NMn^{U|9EsSC)Z+xtJeBmJ7- z>|kbKU|?lnU|?rpVBln6VBlt8VBlq7VBlw9U=U=0xKosYfdS+^X$A%cIR*v>MUYn+ z7#P$U7#Oq|7#MUJ7#Iu~7#NHh7#PeL7#J)W7#M6A7#Qps7#N%w7#Lg`7#KVl7#O@6 z7#RFOj$mM52w`Ag2xnkmh+<%1hy?{T0|P@c0|P@E0|P@Q0|P@20|P@o0|P@50|P@T z0|P?^0|P@f0|P@H0|Nsn`dSzm7}^;a7`hl37ZZI$~+-6{4xW~Z2@Q{Il;Ryo+!*d1( zhF1&>3~w127(OsCFnnfUVED$s!0?lSf#DAW1H*p?21X_Z21Zr}21X7B21afM21Y&x z21Y>!21XGE21aoP21Y3c21Z#121W%221aED2F5f72FC1kgLK1mqjckRlXTN`vvl)x zi*(C$LxXff!*oNVbVK8GLz8qv({w|#bVKuWLyL4n%XA}ybR)xbBcpU9<8&jFbR*Mr zBeQfP^K>JNbR)}jV}o>K!*pY#bYtUmW0Q1a({y9AbYt^$V~ccS%XAZibQ8mL6Qgt! z<8%{~bQ9Bb6SH&^^K=u7bQ8;TQ-gF(dbTi9zbAxnq!*p|_baUf$bCYy)({yvQbaV4` zbBlCy%XABabPL0D3!`)k<8%v?bPLmT3$t_!^K=V~bPLOLOM`Sv!*oldbW7uOOOtd< z({xL-bW8JeON(?%OXvK8%A(Blj1mQnWK9Jl0|O%k$CUh}RE6Nm;*!+dVg=8<gC=te1_lOOX!3SvU|{fpChIT;28L*8a!zGnV90_d;}Ql2hDvDiZDwF# z=zu2MNem1O)1k?AAp--$GH5d0z`($;6`DNvGcYh5fhNmy3=9mHp~>+s0|Ub&Xfk}m zz`*bkn*4qvPU|`H-U|`H+U|=j}U|_6ZU|_6eU|?)w zU|?)#U|{TFU|^icz`!_-fq`*00|Vm%1_s8Z3=E8`7#JAWGcYi2VPIg~$-uz4kAZ>l zFarbQ2?hqnvkVN3mlzlruQM<(-eF*1e8|AS_>6&p@ihYj;|B%?#;*ok23&j!T#6t< z2}CG^2o(^a3L?}%ggS`O01=v8e2QEOAVLvDD1iuN5TODhR6&Fqh)@R+8X!WGi%$up zLkVPu5|gLEi^bSQ&#D03-;#8g0pDu_@65$Yg9 z14L+Y@u`4xsDN~+fOM#Ubf|EtfFx8wgc^uY2N4<|LX(S66{JHIq(c>?LlvY$6{JIz zOBE!m1|rl!ga(Mv@(K)S|<9qF&-=f^#oquYI_WaJC z_&dAjcXof`!M@%tUEgzBzh`xQ&u&!~SNd&`99|n;7*Ua2J3C^_WRCAY|Ev+^e*WWz=!}Uo zCe7?WIQzR#&lIbkx{kVz+NQYQ0=0?F9G#s#UDBQGxiu4#*R>ySJK4S~;hw{9hTl?_ znpTi@Q((c9iTXVUB`v!>rTk^RkPv%#+4>`eyA90kR5)7DSo_@VRXhA8*kPz(I(YvE}2gk&T-8~cJw@hw5 znApM5+0oT0-Tz&>8KKZHYU4&;K?qvR_)hZTsQYvma)DZ2Qy&iN@cIJ-?YI{AQ@) z=<4b2?vW~56IVDlmE(u@UsF-;AId*9MALiIre-w%*2xb3Et~dRD&x1$feK_@9>^s|Bj@1)ZHLr$R#qoRg??0mZ zk9^m>wDmXB8gskfp}$$zXs*or)c<@+%gnBsQ#gL8{@yRj{qDy%(Ni-|)|~F-Sh%RC zccFZ7Rz+%Qs@ZQ%yWg4>za9J2wzW@RJMX*Y)$gkJzALRgvgE|!^$Ql)uFUS_XpM<# zj+S=m^_b;a1xL?&a)00Y(?ZmEx{`W}X;(>O;gr%TWs^&0luvA)+%lytu_mXm zEGWlf%Wt_SnM;}$S1;>%+S}T8aVN*|O>N&~!;%hQn9B%Ump(+%dUz zQfqHce@<^M$M@hr6GXXx@c-l&jhr35Bz)p;+eO*GC*RRIYhT^}rjujh6i}J6V8OiE zv*%3q>i25(ZO>{7O|JDUvE3CqKY2z)t0koo5R^$v^TRqV?stpMSFE?OGRmEd0AO) zPi=QiPuS7SV{`9MSTJkO)VT{LEttKqV@un{ip{kgU7ej>U7fXkHBIeh9N$C#>=xzz z9`#c|w5PeJskga3wyUx}CwPjn&c8>2F-}j21tK4K8 z5ftWcU2k8{(be76CEe4)-r3#N(|? zQ+G~SIAz|X1ss#QCU#7a+c$Ulti!b&zs-KPiuPAbsO+!c=wa)a-!ZRqKF1H^KjNa? zKdgWJ6pdS&vLk%r@91Tof22457TEP$yI|#ab`y^F36nY}O5d7rVczC5vzJX8 z<%Mf9>+2dCni^_rD@p>(S~D9nnlqZpX6H?-UFiIsd+zs*U*2%co4cxe@#KxA8%j5n zZme9oBWnrAl|Mg4xzGK0BRY9X&lKsr-}3W*%g+6+z|qM*F}!PLp9@C^dvk9~PfK6t z%=+o&T^t=9-CZ5>4ejk6Ee+rR=#=ZI>1&$SaNquW)agSHUVK+s^qtS|yGi4+qIs1I zeXsqNt}$uk=xS~4Zk0~z>2?wha|TE9hhMYTnA_TB8bS$&h^#e(k~ z-OnrMm9L)KySl5dyRWOSZC2}~>VCz=zx6s4b2+M7Tk9L7x;nZ$dOA3Mg#7s>%6;x9 zljsDtzMRU;_T2XDp8Bpt4qf)Ik0g+* zQJdR2ez^W+66OBU(Jji&%>ZiL+c4-ca4|44urM%zn)(bJ42%pyU>QaR9uWKg90o=Z z&A?caT3o^a8l3>?W6Vh`$z$MP0JVu2{{IK-VB$Q&IDvtcfrs%6;}?cs3`Ss@f$&oX{}~uKk8mDgaAa@< zt7T$f2HRx7z|6qKz`!8FzylVS0h7?~3UmMf}T3``8kAk!EaAtYEm$PJ7P z3`}j_|Kj;=zA`Yf-T{lF5dXh%9^n8DCNP6T2qeb{auovu!wJqKj5>@uoJYWJ0?EKI zBkLWI%y7UUF9k3#q=0A!HU>XX46txAFoFkcm>C#ZI2o82_!zXH;>-+M3>Hu}3xgIz z5|qu#Ai~fCWwSA8F)Uz6XDDFEXDDVcVu)ujWQb?TV@PGFU?^dTXGmwrVW?y%V8~#I zXGmi(VhCbLWk_czWyoPjWGEs*AMrMXF{CmSF%&aoGUPMlF&HxFF&L0w4`}EMG)glE LH2ww-Clmqz39~^w literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos2_1_next_glyph_f2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos2_1_next_glyph_f2.ttx.GPOS new file mode 100644 index 0000000..cf71f47 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos2_1_next_glyph_f2.ttx.GPOS @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos2_1_simple_f1.otf b/Tests/ttLib/tables/data/aots/gpos2_1_simple_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..c947776aa88d97f9307d19278739369f8dc34ec6 GIT binary patch literal 5148 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 P!0>+#Xp9XU4k!cwe7QSB literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos2_1_simple_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos2_1_simple_f1.ttx.GPOS new file mode 100644 index 0000000..81b1720 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos2_1_simple_f1.ttx.GPOS @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos2_2_font1.otf b/Tests/ttLib/tables/data/aots/gpos2_2_font1.otf new file mode 100644 index 0000000000000000000000000000000000000000..dde370a0e3df8417c0fcda6c31bef8acb12d9dc2 GIT binary patch literal 5148 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos2_2_font1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos2_2_font1.ttx.GPOS new file mode 100644 index 0000000..5595b99 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos2_2_font1.ttx.GPOS @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos2_2_font2.otf b/Tests/ttLib/tables/data/aots/gpos2_2_font2.otf new file mode 100644 index 0000000000000000000000000000000000000000..63d874a2632f5cb1cad78c51b60ea6ba32317e0d GIT binary patch literal 5188 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 + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos2_2_font2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos2_2_font2.ttx.GPOS new file mode 100644 index 0000000..7896be3 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos2_2_font2.ttx.GPOS @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos2_2_font3.otf b/Tests/ttLib/tables/data/aots/gpos2_2_font3.otf new file mode 100644 index 0000000000000000000000000000000000000000..b5306764d14cb73a6fe4608af8598ab6cc0d2dfe GIT binary patch literal 5188 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 + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos2_2_font3.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos2_2_font3.ttx.GPOS new file mode 100644 index 0000000..216a591 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos2_2_font3.ttx.GPOS @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos2_2_font4.otf b/Tests/ttLib/tables/data/aots/gpos2_2_font4.otf new file mode 100644 index 0000000000000000000000000000000000000000..b549e0239817bf90da6bb27b18f38b78408134ac GIT binary patch literal 5148 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos2_2_font4.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos2_2_font4.ttx.GPOS new file mode 100644 index 0000000..a2e6017 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos2_2_font4.ttx.GPOS @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos2_2_font5.otf b/Tests/ttLib/tables/data/aots/gpos2_2_font5.otf new file mode 100644 index 0000000000000000000000000000000000000000..64c40bba952a4dbe36c4b0fd18ff43369efa908e GIT binary patch literal 5140 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos2_2_font5.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos2_2_font5.ttx.GPOS new file mode 100644 index 0000000..d269735 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos2_2_font5.ttx.GPOS @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos3_font1.otf b/Tests/ttLib/tables/data/aots/gpos3_font1.otf new file mode 100644 index 0000000000000000000000000000000000000000..9b6d39acdf2eaf9aecb8444438cb4c087f9d5934 GIT binary patch literal 5120 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 Q0BE#j4rm+=>@O4o07y$UNdN!< literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos3_font1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos3_font1.ttx.GPOS new file mode 100644 index 0000000..8cbdbc7 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos3_font1.ttx.GPOS @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos3_font2.otf b/Tests/ttLib/tables/data/aots/gpos3_font2.otf new file mode 100644 index 0000000000000000000000000000000000000000..dee57855ec01c19cd83b3c1181553d9d0a7e3d15 GIT binary patch literal 5160 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 + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos3_font2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos3_font2.ttx.GPOS new file mode 100644 index 0000000..3ded3a7 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos3_font2.ttx.GPOS @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos3_font3.otf b/Tests/ttLib/tables/data/aots/gpos3_font3.otf new file mode 100644 index 0000000000000000000000000000000000000000..7522660d9f3cd19bab83aedcfc524ffd3d3750ab GIT binary patch literal 5164 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 + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos3_font3.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos3_font3.ttx.GPOS new file mode 100644 index 0000000..e6a1c04 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos3_font3.ttx.GPOS @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos4_lookupflag_f1.otf b/Tests/ttLib/tables/data/aots/gpos4_lookupflag_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..b141116af150b28f7ee6566515e8a4c3cf06eb6f GIT binary patch literal 5256 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 Z20aD?BFzU4oqI003n!M>qfg literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos4_lookupflag_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos4_lookupflag_f1.ttx.GDEF new file mode 100644 index 0000000..5118ad8 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos4_lookupflag_f1.ttx.GDEF @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos4_lookupflag_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos4_lookupflag_f1.ttx.GPOS new file mode 100644 index 0000000..744eaaa --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos4_lookupflag_f1.ttx.GPOS @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos4_lookupflag_f2.otf b/Tests/ttLib/tables/data/aots/gpos4_lookupflag_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..84e48437c7cf066fe975dd53550f344debc262f9 GIT binary patch literal 5240 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 + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos4_lookupflag_f2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos4_lookupflag_f2.ttx.GPOS new file mode 100644 index 0000000..7817336 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos4_lookupflag_f2.ttx.GPOS @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos4_multiple_anchors_1.otf b/Tests/ttLib/tables/data/aots/gpos4_multiple_anchors_1.otf new file mode 100644 index 0000000000000000000000000000000000000000..025f69b3188611c61eb280c7bbee28a555dd0b4a GIT binary patch literal 5352 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos4_multiple_anchors_1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos4_multiple_anchors_1.ttx.GDEF new file mode 100644 index 0000000..4f9f64a --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos4_multiple_anchors_1.ttx.GDEF @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos4_multiple_anchors_1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos4_multiple_anchors_1.ttx.GPOS new file mode 100644 index 0000000..afe7e6a --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos4_multiple_anchors_1.ttx.GPOS @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos4_simple_1.otf b/Tests/ttLib/tables/data/aots/gpos4_simple_1.otf new file mode 100644 index 0000000000000000000000000000000000000000..da54a1fd11d892b858b43361e934a964d5919ae9 GIT binary patch literal 5200 zcmeYd3Grv(W@unwW-xGeb5l?b>uO|RVA#XJAfV#z;_Ai_%MirCAn<^JfkDGPz(4rm zu~$DC7z8;O7#Pz0gY}Ja&V=q|U=S=|U|>i{&P^;}kY&tSyBAY#D4z@Wg$z{0}7$il$Dzz9;FQkt7v ziQs$v;Ai+z&-kN(=|=U+uBp4VNq!}0(*ZU|`T|bqov)8yOfFwlFX- zY-eC#*u}uWu$O^>;Q#{z!(j#nhGPs23?~^F7|t*-Fq~&#V7SDSRGe|cxOgA%1H#1H*Gf6iyO*b=3H#1K+vq(3yOgA@3H#bZ- zH%d1*PB%A6H#bc;H%m7+PdB$nH@8f;Fi5vBOt&yfw=hn(FiE#CO}8*hw=hq)ut>MC zOt&;hw=_(*G)lKLPPa5kw=_++G)uQMPq(y4x3qN5FQ_caOwTA$&`8!)FfuSOQgBSk zPfAq?t}HG|%`H~&%uCKMD#$NNEJ;n#Q*g}5Q3wJlFIEUjElw>eOHI*(WG)6q#*QS0 z6j*j(gJuB%XtI}LU|^7kCU;E+1_nI_1_n?vw_spku!SaXcLoLqA84`;V_;y2h9>7! z1_p*KXfiHgU|^_(Cf{ZT28Ir3vYo`hz%U(}To*DhFf4;6(+vy^3|pbeb3X$E!x3n* zJjcMma2c8$?=mnjJc1^}Hw+96AEC+bH>C6cCp#_%21b5pa+73WV3dO#uNqy#!LnV#ykcF z#$pBr#tH@o###mj#wG>^#&!k<#vTR+#)%9JjMEqx7-usuFfL$VU|h<;z_^NmfpI+p z1LGD32F9HX42=627#I&TFfg8AU|>AUz`%Hkfr0Tl0|Vn71_s853=E9V7#J8|GcYiI zU|?YUYQSZ{#izif2qKg~gffUw0THSoLJdTyg9r@}p~=Ok$fW=x6hVX%h)@O*Dj-4? zM5uuXbr7KeA~d=9lt4O^Kz1l`DS^1kAVLL1sDcPJ5TOntG(dzV7oRdnhcZZqGDwFq zmoi991w^QV2sIF)4k9!_geDiC3P^_vNQVkYhYCoC3YQ8geDiC21thnNQVYUhXzQ8 z21thnNQVYUhXzQ82A2j%u_hOvCP;@SNQWj!hbBmeCP;@SNQWj!hbBmeCP;@SmnIiq z(UPC4OMbU6`R+2~(30QNmau(azvR#1C6||YEn!EO<3Px9uI75cy_$#T!2}*&8O|dj zGMq<5!S$UmgAgolgQ|_y3=E873=E8G85o!x7#Nt2GcYjQF)%PsW?*0mXJBCEXJBA$ z0#(!u3~YB87}zTr7&urM7&vMf7&w|37&w~0tNzp#bxg?rZPn_~_*-DX?}G0d@f-~s zHc#3lJ#lhhf4}^NC>R)0@VZ%=Q}%+48|)0#QH z*Z%n}%KhE>r>1Dn@9c@cvwMDL_a`3g>)q1zJ*V}1R@e9JhINVEEomIzZ~oO3<^Hbx zV~yysg(o(h?z=gmPb)jx{x|3E1tCqpZ8~$NN46e>1cw^moo}p4&TT_IJha(+^$v&UyJepT~El-v-Iywb6wU70I=;BeqQD z`2O?H8d2`&KW>Q5m^fq7%>IM3zx(t|vFfSosOzY0iu)~4o7l|J+1b-2-N~L?Ga-3h z`|-Au?Yk20Is9h$EoE8mP^I=Pv9`3irE+RnZ)b03Pv_LeX&qC0IeL4$dZcH!%xam{ zvZi)T?Yfpvm3Ln5zPaz7_glO5?L8g6?Y(m*&7LxA`i&FW-)uG;?E1~#WRT2JP&_wn z{UnYbI)846a({pFlU>wF*H+(ED?2VFr8Bs9;Oq|%=GeLgK1*siHM;g>zFmerW$S73Kb+{8K|Ty*F)YM)Pl-?BL(B zX}_g1ev2LY?fhLa@w-9ncj@mFy}$FP+|2H1HyxBDIXn{|!m%Dhkg&!@D^?3y`+_+{i59O zetZ)>HS=W6=}wM?i+Xw&$`@x`so>n5gDxX_sD)S*}&k#P`GOuf8bv_pLuIM2)8_skfMRl{6MkDVy1#j5E6;NJu5kO_sx=Ecw@#YZKCf@iB#zbJBSd?no1(MQax=|N zH*aY;eC4~$#gfS#lUpaX_U82G^yYGW5B@Vjl=}z&PkzzJ+0jeFC;qlwl>K}19i6lG z)%|ZeIVMg4l_?7r%$q%X&SbBCuU6motftW9TF(;OU7_=nXH-OPsMuMxqiRR>s)n`G zSNDFN*!?}I{Uk@%`nL6L>pK^C`%TTi^W71D#u2 zwzX{M_^$DNujskTO|}t1VgAdmXGgh@-o3?w}-sP1O zD<@R-CiW+`CUmv!?AkeX=Y)k*=1p3_F{x`}#{{{3bC=ILT+8v>?02hZf5n8#{tAvB zwvPE7^E&5q{4o9_F3SDG`o~YvxTPsO!YBTYUgr5ndgE__UB9&pR(@wU;b@;QsdJ+A ztqB+AZ9X%5*|dd=<}X=ZxF)l{uA!l+p|-Z7B(SVCvoWJNqp56m-n80<&fmG`e&6`z z4adB>tGX9Y-dMV!bVKRJ%B4H9mT+A8^HY@j+>bY+lc)4dk7>q%(5>wp-)CiuerMHA`JHc}mX+j_ z7t+Mh$KLYYHR`+bmhVm+6WC8qJkhm3p5wdypEIJ|`+xiroo_b5s@=FVyd%9moTIh+ zxAUY@xoMeO+E;dQe15ZG;wNaytRsKtYH#np345XSTVz*M zTSRBy&5oPZH#uG`_|DP&ymDUo>Z!e}yZXBOy87B?wN9$;S6uvCuR}4HqpG#FzCo(1 zqr0Q0gX2fYpI@Tf=YBGYPGIZHsmyH8ZO`th?@Hv*Wp8fish!-^`3I!t@F0@Z2HdB{aSi{)Ye#?-r60txt-&O>t7~O?jIf9qTJjJprHXC1_)qcU||qs5M*Fv5MmHw zU}O+x5N2Rz5MdBuUvL)1LGGEi-CcOi;bC$m79qP;sS`f!K9;$lPiM*gCj!#C#ZjO0z^KrXZ-*F zKLZ2j5zZqFjtq`qg-lT27%(t1a4|42h%i9>F9Vi^c6Xoy2q3?L#%5R;m>3utm>7~l zrZF%=NU(aaPZ$`k&3fh#&u{aUfsyqNSR94;|BdqqM;ZeI12Y2?RDu!YDh39I6P!mF zbr^Lxk3jtol?Dyku-*a53-*(3|de&3xf=U8 + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos4_simple_1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos4_simple_1.ttx.GPOS new file mode 100644 index 0000000..3a2e623 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos4_simple_1.ttx.GPOS @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos5_font1.otf b/Tests/ttLib/tables/data/aots/gpos5_font1.otf new file mode 100644 index 0000000000000000000000000000000000000000..8c48fb679fff3e62893c84a36c9eb3f258897359 GIT binary patch literal 5284 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 + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos5_font1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos5_font1.ttx.GPOS new file mode 100644 index 0000000..b5017b3 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos5_font1.ttx.GPOS @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos5_font1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gpos5_font1.ttx.GSUB new file mode 100644 index 0000000..85d3308 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos5_font1.ttx.GSUB @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos6_font1.otf b/Tests/ttLib/tables/data/aots/gpos6_font1.otf new file mode 100644 index 0000000000000000000000000000000000000000..f7f92cc37fbad48d6537f023e38f16aadeaa226c GIT binary patch literal 5176 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 ANB{r; literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos6_font1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos6_font1.ttx.GDEF new file mode 100644 index 0000000..640f3eb --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos6_font1.ttx.GDEF @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos6_font1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos6_font1.ttx.GPOS new file mode 100644 index 0000000..d4c4da5 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos6_font1.ttx.GPOS @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos7_1_font1.otf b/Tests/ttLib/tables/data/aots/gpos7_1_font1.otf new file mode 100644 index 0000000000000000000000000000000000000000..ced8907e90f9f8f3cb316a802a31a0417f3f6a65 GIT binary patch literal 5160 zcmeYd3Grv(VrXDsW>9c;b5l?b>uO|RU|7SzAfVzN;2-?z>?3Oi27wC<3=9qa!TLry zXF~TfFbD=PFfb$}=Oz{~NHaz=FbHm7U|&tSyBAi}}Gz@Wg$z{0}7$il$Dzz9;FQkt7viQs$v;Ai+z&-kN(=|=1_nitR~Z->)EO8Uv=|r|bQu^JK;AJ11qlNKgCzq4gAD@%gFOQSgA)S-gDV3A zg9ifxgEs>MgCEEd3=9k*3=9n63=9lW3=9mhprB@8U`S?QU`S(NV8~=(V8~%$V8~}+ zU?^f>U?^o^V5neVV5nwbV5nnYU;ss53j+f~I|Bnl7Xt%BF9QR^1O^6%$qWn((-;^S zW->4^%wb?)n9sn#u!w$U|{&kz`*c_fq~&a0|O%y0|O%~0|O%m0|O&B z0|O%;0|TQV0|TQ70|TQt0|TQJ0|TQh0|TQ10|TQn0|R3k0|R4rx34ux=Ffe zx>>q;x<$HWx}ib3p<%kAQM#dVx}izBp=r9IS-PQlx}in7p=G*}LAsG)x{*=3k#V|_ zNxG3~x{+DBk$Jk2MY@q?y0JmJv0=KgQM$2ly0J;Rv1z)oS-P=#y0JyNv1Pi6LAr@y zx`|P`iE+A#NxF$?x`|o3iFvw-MY@S)x~W0BsbRXQQM##dx~WOJscE{YS-Potx~WCF zsb#vELAse?x|vbBnQ^+ANxGS7x|vzJnR&XIMY@?~y17BRxna7wQM$Qty17ZZxoNt& zS-QD-y17NVxn;VALAr%ux`k1?g>kxtNxFq;x`kP~g?YM#MY@G$x}`z7rD3|IQM#pZ zx}{0FrD?jQS-Pcpx}`L4HwUNotCof@4mOLJ&xKu|iO4acWUnYKk5tb1^V7b|f*Rz_J4yGz$nolf4uJ z1A{y?xoa{oFz7KbFo2RdD7V`}leaqq1A`AVS%)z&FhoO>b1DM^Ll!g{moP9eR6>() zGXn!d2Q=AEVqjpH4o$8L85kIrL6a${wAl(xp8FXX7>+=bikAFTUGlqq$#<6-hnD=F zwuJ5b`Xzr3FS)$LYY98L90x*NZd3a;;j8H8YY8&qwq zW?*0xV_;xh%fP_oz`($CoPmMaj)8%BG6MrkI0FMKKLZ166R4tQU|_q$z`$O~z`()6 zz`#+v^^#@_-9eiwY#i05e7uzAuZ>4}s3`upWC9N)C6 z_fyaI3ESGYwLFUao@>0Lct&1N{-pY@=JwW>>dxwpx@L~=e37DW+*bYWs!^!ZNwogW zye52h?xE_P*>AhPuYPrY#+138v-*2_dV6|%W_Hf#oYu_oz4p&rerHeo zo!#?0yFc+@U+VjCv%0=#H>^wSZb{?#e)F%UDED{eA8SO9Ej+R5bl=SheOlSk z_P;rQF9>P+ZPS@EEq6lBgvJSNQ``2|9G$$k=SWW{WeGruZ=E@s7S7z9kFFH$M>Iq)`)UH|8YZf#>5$u zX7(SP{oSW$id9ctM_os4Q`~QX+Qep#&d#1L=}z|CnhDA4+K;!LY~Ph|&*3-2Zz;=i zhbpyaiM6H8EtONtdOLeNdpf5!PV1Q3%hB80)gwK-Wme0qmNm6&YS*=Vs=V`Z_sxCx zyx-cbZ|~{oZSS2kY4(&^({G%}{${h;VApTnCyi(D`#il>7UWpX{Pe zy0-eRTG??aDV@Q+)e~A~wp>s7E;jq))XwCbu3; z?BM9^=<1a2|1RC}T{7ysaA#jfZ)b1o#OD5{E{@jLj`nu>&Mpw(_+9l!M|8@}i8H6n zn7w$zzBvnbpWb_N@5vQPJ-?aSe>0ji{T8o_oX_z??svW@_m-bxqCK5GU0qTwy)6^k z7;{c!8q4>Lcued>Zl<8Q{E-%JyJGgNVO z^>lalNENM#E1a9k@k9HssVMgk<)0d&>Ah)FGn#+vWC#D2P5UjC@muWBZ|CodiQf%k zze|6g=>45PP>9xg;={5P4g@yAzocb>Iooz11>ItiwS3|Ah_&xjgAJP3s zzH46E`kQHux!v#3->hpiSLS`{e?FyUX4lLq96wZl?-%8M_v4%BshKBhPIq!FT-4LM zP`)^;BDFNt?6;=fZ_SF|j{Rxd+NZCb_uca9ch!5}mDV0va^mp%1&eD}W_NP5#zZwo zOS|-X%yO-QCcYnLfAvMVzi<6%A!`kSEo@rYJil^Q&Fq#*P5n)MeG~d7 z^iAA$zwA-R)7JS_lWRK~r!-8i>*1*X!yww*+1u6EHNAaC*Oca^l`~Q&<}X%B(EZIb zTX~k#cZJ*cR;^jsxpmUK_IZ7CCULC(9wFKr-4vabmYZpIx_L{(;Va)|E|yI0nA|$4 zwKu0fr#F}5d+?tLqTE0DfAWh)&W>IZKJmBhqU_(3@93PhukL@-$uV&Xs7zU~VBYN6 zb0&NBd$szuXElW;*Ls%N?h2isJfk9VL&eUj9aTH3S2e7izPk7G#P07w?I$_9*0-&1 zTi>~;eR0R48T+c{&)YX)cKeK`$+M<)FDjqS;cPD2o7tZ+A)}+By}GrfqO`QUtgN=D zw!5Y$>}ck(x%Ve5m^EkW+y#>s%wE{BrEO!y=30)f&d#o`&f313ruH(9@1cKoi*kRD z`Y9mV)7;b4+uR=8Ra@m7|J&T^w?@TpJ4jScIzH>@+{1_3_s=}oKR0xK=#sE`X?y3@ zZRW7}?v?sIF#CHr$E2RgJ(J~nmsCuz>Zt0itnDk}So1qTbcXwEk9nSnZOM&^JxeaF zxU}#J$M=PQLPWU_{P-wZ5!_VX;oo7f%&^a(&#hx(od-upPkT?NTwC?jip8BLCw|xN zJ$!J%f+cfjEjj*4`-S$pMKc!9UbgW3p`vf$`zq%ZPA-`qu*xALw34H+sk3-mZD(g^ zXIBTumftHy@Aj^remVVG>U<-c826-rPN&w%+xq5r9_ZZKvaMx1$9IkIdqvMxZnBLC z3iG$Fx3A~u>h9{2?rCA~?C$F6k}IyBn6awu+O*x%_AalSSUI7pH?cplHKD6*XV=cD zJ0~ohGH=oXj!9h;J0{5Oo4b70;aZO0X1`lS`zt0?_E&K9uyxGunAbU<*2}k>cNu3j=Z%w!`Z}XYi%cd<{G=ItR z!Zn%obqx(o4YjouC4ptFnT;9E8BJxg^QP4(+ro!u5CTrdbe@(Zk@NKXVZ74_OC79nHED7{AMiq%`o*hQ-AfsqPE8B^w#dE z&FvgNT>mnOa{uV)7Ukw<01XY~FvKx%F)%ThFtCBU?u-l^42%p43=o=yfti7sL6|`V zG)Mtf&jcP)U}O*i^FXSY82A_%8JHPT7)~(ifLTId9gHQZ#U%`&(FjHcMh3>5#F9J) z4hB#gisAo%ux=*KBa9OmSQ&U2zc7Ab_{CrZrWqK&fLII+Ok8ZtY^>Z&OdvfVw}Kr3 zA{xFDY2<|_ju z>m9H-3i1CN=MfIj-~%%QD6Bwoj38GrFfg3pJi@5MsKa>#>?V*53^TIc0m%#p4DwO{ z149akW?*CR1H}LfCj%n`s0Ylo4X(irj?@)$}O3>ktLQW?@2N*Qt(5*do{sKaeS7(*&U g5koOUCfH0v20aD?Jm!Fgx + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos9_font1.otf b/Tests/ttLib/tables/data/aots/gpos9_font1.otf new file mode 100644 index 0000000000000000000000000000000000000000..e99c25a04898254347767ad136d08fec412def8c GIT binary patch literal 5096 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos9_font2.otf b/Tests/ttLib/tables/data/aots/gpos9_font2.otf new file mode 100644 index 0000000000000000000000000000000000000000..9ae824baeca8b24db28d877d4575db7eb46bf93d GIT binary patch literal 5124 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos9_font2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos9_font2.ttx.GPOS new file mode 100644 index 0000000..ac6d6af --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos9_font2.ttx.GPOS @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f1.otf b/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..44c4117a0d317c607a696480f23f61f6966f86a3 GIT binary patch literal 5496 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f1.ttx.GPOS new file mode 100644 index 0000000..ea85d9f --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f1.ttx.GPOS @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f2.otf b/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..431b08fed7732e3fd60555e2a6b5c1336d9ad097 GIT binary patch literal 5500 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f2.ttx.GPOS new file mode 100644 index 0000000..bd176ba --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f2.ttx.GPOS @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f3.otf b/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f3.otf new file mode 100644 index 0000000000000000000000000000000000000000..1bac49ab9b5d764028dc703857c196ba8ec4fe10 GIT binary patch literal 5496 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f3.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f3.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f3.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f3.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f3.ttx.GPOS new file mode 100644 index 0000000..b189067 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f3.ttx.GPOS @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f4.otf b/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f4.otf new file mode 100644 index 0000000000000000000000000000000000000000..3d377829c090147b03a75049cab0083586a3671d GIT binary patch literal 5496 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f4.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f4.ttx.GPOS new file mode 100644 index 0000000..f750652 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining1_boundary_f4.ttx.GPOS @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_lookupflag_f1.otf b/Tests/ttLib/tables/data/aots/gpos_chaining1_lookupflag_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..a83342a12a172735fbf084a9405ef1040b5c7931 GIT binary patch literal 5520 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_lookupflag_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining1_lookupflag_f1.ttx.GPOS new file mode 100644 index 0000000..b07a9ba --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining1_lookupflag_f1.ttx.GPOS @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f1.otf b/Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..07bf55c41fc66fe0b97f52985e26b6bf6d3d9764 GIT binary patch literal 5592 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 Gg#ZA2&2bF? literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f1.ttx.GPOS new file mode 100644 index 0000000..09b5bd8 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f1.ttx.GPOS @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f2.otf b/Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..dc3754b56c5bbd5d237c4c034aa94da74834d207 GIT binary patch literal 5592 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 Gg#ZAu6mkpz literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f2.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f2.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f2.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f2.ttx.GPOS new file mode 100644 index 0000000..e8fb516 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining1_multiple_subrules_f2.ttx.GPOS @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_next_glyph_f1.otf b/Tests/ttLib/tables/data/aots/gpos_chaining1_next_glyph_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..17852c2d0c4f92b65f20b06331cd7f098d7f845f GIT binary patch literal 5540 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_next_glyph_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_chaining1_next_glyph_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining1_next_glyph_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_next_glyph_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining1_next_glyph_f1.ttx.GPOS new file mode 100644 index 0000000..1f7539e --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining1_next_glyph_f1.ttx.GPOS @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f1.otf b/Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..31cbe77d133752daf251cc19088096a164261d35 GIT binary patch literal 5488 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f1.ttx.GPOS new file mode 100644 index 0000000..1780fda --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f1.ttx.GPOS @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f2.otf b/Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..3293ad8c28a40e73aa0a42256ce7d8a4628bd7a8 GIT binary patch literal 5488 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f2.ttx.GPOS new file mode 100644 index 0000000..c2d3411 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining1_simple_f2.ttx.GPOS @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_successive_f1.otf b/Tests/ttLib/tables/data/aots/gpos_chaining1_successive_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..4c8666321025cc932179924f4562a45722fc08d6 GIT binary patch literal 5524 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining1_successive_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining1_successive_f1.ttx.GPOS new file mode 100644 index 0000000..3960d8a --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining1_successive_f1.ttx.GPOS @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f1.otf b/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..49210fb954d223b8ecf9d54f895979a948ba8817 GIT binary patch literal 5704 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f1.ttx.GPOS new file mode 100644 index 0000000..fe06077 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f1.ttx.GPOS @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f2.otf b/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..456fc9b64ab8eb8888c71e0125f7ac4abe6d5d58 GIT binary patch literal 5708 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f2.ttx.GPOS new file mode 100644 index 0000000..c529371 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f2.ttx.GPOS @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f3.otf b/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f3.otf new file mode 100644 index 0000000000000000000000000000000000000000..768492a893a1906d1d8f899289149656df79ec86 GIT binary patch literal 5704 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 XF&L2LdeG1(Xq0XaX#5o%o+tzW90h3= literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f3.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f3.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f3.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f3.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f3.ttx.GPOS new file mode 100644 index 0000000..fa55cf1 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f3.ttx.GPOS @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f4.otf b/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f4.otf new file mode 100644 index 0000000000000000000000000000000000000000..2670da652467451551ff572405b311c8a0c87c36 GIT binary patch literal 5704 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f4.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f4.ttx.GPOS new file mode 100644 index 0000000..f943402 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining2_boundary_f4.ttx.GPOS @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_lookupflag_f1.otf b/Tests/ttLib/tables/data/aots/gpos_chaining2_lookupflag_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..e8cce567e62459b46bf82846104a68ec26491769 GIT binary patch literal 5728 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 rLayy$455E8oiqX8kYr!FA4zwU_NXT literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_lookupflag_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_chaining2_lookupflag_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining2_lookupflag_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_lookupflag_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining2_lookupflag_f1.ttx.GPOS new file mode 100644 index 0000000..b26d93b --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining2_lookupflag_f1.ttx.GPOS @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_multiple_subrules_f1.otf b/Tests/ttLib/tables/data/aots/gpos_chaining2_multiple_subrules_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..f182c7f3cb9350d0bb4871a5ebbe494bced6e497 GIT binary patch literal 5800 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_multiple_subrules_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining2_multiple_subrules_f1.ttx.GPOS new file mode 100644 index 0000000..33399ba --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining2_multiple_subrules_f1.ttx.GPOS @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_multiple_subrules_f2.otf b/Tests/ttLib/tables/data/aots/gpos_chaining2_multiple_subrules_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..d24896a13013b0cbabd4330895a7f77361376676 GIT binary patch literal 5800 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_multiple_subrules_f2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining2_multiple_subrules_f2.ttx.GPOS new file mode 100644 index 0000000..dc10c2f --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining2_multiple_subrules_f2.ttx.GPOS @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_next_glyph_f1.otf b/Tests/ttLib/tables/data/aots/gpos_chaining2_next_glyph_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..f6bbda472d078aea489fb3b87f1c0ba9e4864be6 GIT binary patch literal 5744 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_next_glyph_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining2_next_glyph_f1.ttx.GPOS new file mode 100644 index 0000000..9c18a9b --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining2_next_glyph_f1.ttx.GPOS @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f1.otf b/Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..1805a034bb298b4b2b045ee0af6f4d40f4685826 GIT binary patch literal 5696 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f1.ttx.GPOS new file mode 100644 index 0000000..decc575 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f1.ttx.GPOS @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f2.otf b/Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..1df12f5285c75ab5feca7ce5398b566a161f6642 GIT binary patch literal 5696 zcmeYd3Grv(W@unwW-xGeb5l?b>uO|RVA#XJAfV#z;_Ak(%C5t}An<^JfuX}az(4qN z+Djh>2Ehyl2F4Zs!TLryXF~TfFbE4UFfb$}=Oz{~NHaz=FbEegFfasUq$Z|h-1IhJ zU=TjQz`$UZk&&7xTFdo`fkF5O0|SFfMs7)kLy6T41_lus1_p*xxrr483@HH&3=ASK z7#JAX@)C1XC3flkV_*>Zz`($0P>^3-@_#;q5d(vm0s{ks0wV(p3j-qy0|NsiNO?+W zZf+%l@AZS9;YU5=j|Qe64a`3zSbhugFw1cC!tSQ!`?*cliYI2jlixEUB2co`TN_!$@& z1Q{3@gc%qZL>U+uBp4VNq!}0(*ZU|`TMgCEEd3=9k*3=9n63=9lW3=9mhprB@8 zU`S?QU`S(NV8~=(V8~%$V8~}+U?^f>U?^o^V5neVV5nwbV5nnYU;ssD3j+f~I|Bnl z7Xt%BF9QR^1O^6%$qWn((-;^SW->4^%wb?)n9sn#u!wK?@9AjW$ILW}kaE5__;XDHa!zBg=hN}z= z3^y1U7;ZB#Fx+EcV0g&D!0?2Df#Ep=1H&r@28Oo`3=AI_7#Kb?Ffe>$U|{&kz`*c_ zfq~&a0|O%y0|O%~0|O%m0|O&B0|O%;0|TQV0|TQ70|TQt0|TQJ0|TQh0|TQ10|TQn z0|R3k0|R4rx34ux=Ffex>>q;x<$HWx}ib3p<%kAQM#dVx}izBp=r9IS-PQl zx}in7p=G*}LAsG)x{*=3k#V|_NxG3~x{+DBk$Jk2MY@q?y0JmJv0=KgQM$2ly0J;R zv1z)oS-P=#y0JyNv1Pi6LAr@yx`|P`iE+A#NxF$?x`|o3iFvw-MY@S)x~W0BsbRXQ zQM##dx~WOJscE{YS-Potx~WCFsb#vELAse?x|vbBnQ^+ANxGS7x|vzJnR&XIMY@?~ zy17BRxna7wQM$Qty17ZZxoNt&S-QD-y17NVxn;VALAr%ux`k1?g>kxtNxFq;x`kP~ zg?YM#MY@G$x}`z7rD3|IQM#pZx}{0FrD?jQS-Pcpx}`L4HwUNotCof@4mOLJ&xKu|iO4acWUnYKk5t zb1^V7b|f*Rz_J4yGz)-oKPcHtF)%R5LzBBE0|SE|G?`m4FfiCcleaqq1A`AVS%)z& zFhoO>b1DM^Ll!g{moP9eR6>()GpOW&Cfi923=Gqu$#o$E1H&?CGTp$yz_1mXJohs& zFdTs<%X172440wF@h$@c!y{-ie8a%N@DZB)enUzRaI)iKU|{5jCO1h221Yq(GSgsS zVAN$`U^HT2U^Hi7V6BPzDhy zAVL*HsDTJ|5TOAgG`aYcKsuB_b|`Tvfw;;bLIp&qf(SJbp$;N6K!he2pE5{?GDwFq zNQW|)GDu7XM5uxYH4vc=A~ZmRCKsOyNQVkYhYCoC3P^_vmkLNi6-20k2z3yl0U|WH z_*6kUR6#maK{`}HI#fYARJl|^vT7hg9Ykn=2u&_NHINQ9kPbDF4mFStHINQ9kPbC2 zHINc@5TOAgG`aZHK|0hyI@Ccr)ImDbK|0hyI@Ccr)Vb6_>NG%vCKsOuNQVYUhXzQ8 z21thnNQVYUhXzQ821thnmj+0&CKsP3NQWj!hbBmeCP;@SNQWj!hbBmeCP;@SNQWku zCKq4PlAo$eez!0A?lR-hlHb#ouzg>@(HmzQ`gVMmwaK*({f=6b-rnuq7X1Rh=) z&Lbi+oJT~#^_?(-5G-$ls*TkQ42)t742)|T7?>Ow7?_STFfiLOFfdPMU|e)VFTl=<_N0Hxijdv8!$m_|URNvLy-r7>#S=~|B z%<-KsQuK}6s^48T3UxY(*1wt8gwM`BRJ}9%ZP)kJug=eyGPiS9e@{`}9n)>Z$9f>!@vt`z=tL*v!${+0!N6$(~y? zA$eW<@wSugyAtj>{ATzqWm)b}rS>ebwzRpWa%x#`XK!au=hVh&9aDQbdV9Niq-VFx zYMIrtrgly3x|UCscV6zkx$mC$Tf6n`JsrL6y>lkbo-%9tjT715Y&IM0`pw>Ckjzm~ zJU4CqB#s|Ce{P6!e}D3mUDQd}R^L@CJ1!-qGq|^ULd(pS>nY#GW`CU88Q&S-l>iCB zo*lhAdUtS4oY>tnL4M2R)`N*19Gx9qoznf^r8~Y$Mtv9V?Ca?5>}{Rc+~3s2(c0S4 z-Y(zS1p*wutN!SSPMJAz=9C$;7jM`%XW{PCdr$5?xk9PuH&gp>Mw6!B;&qYpIey6f z&KKq0@>5K-r?aQ4ORA-}WkQ?WEH@XW6LFU!zh@oa{hMP-d0%lyY29y;h>YJnt*KqU zZG~&|7G~wo&e{}lt@~K-y$RPB?76_PWPSJUXX!}t7e<0AW|<=eI&Zaw>9 z=Et^AU65$}&DissX~J)YDvqw6?(QC`qBU`ab5l8fX#X`8<^G}kQ$sYpH*IQ0^KYH( z;NP-ozojyMiyivy{9Q5eyFu)C>F*Q0zw@Wu%s+ko~OO7Z5MZP9N*OTowfOB=i<7B zO$(dnSI(-L-7=}Ezp1ZpLf?eGiQDd%J?ePcI=^aiZAasjhRJn39QA(~M0-1XyZXAO zx6kOB(!8{CM(V`;#VQH9zj%aQoh>H48hpPMX&~uW!yIj@92IM0=x~qO;O+ zGtEvnZ)rGu<-5$qlF1#DTPL;l=Je~BZWUqd&R^RrlrqJYC&l1~Rq4SexR77s5*jcrsYDe{| zhPBgI_kNz({XMAtBuCf!w)Ji6I~TPt?pQQqU)B71`zFk8pV2gV*0k5)%MhO*Yt!P%{(^u{)7dy=1iTtVA6uw3p=*7ZLHW_%hA=@ z+11rq+gH=nUdHh~^v`Zl?(b1Q1w?zAdzyNi+hefqS&d!^{z-u2Tjr(a8*Z)6kWo)pmO)H-=v-~7%4 zom*SBwQT43uJL`Z=();Gwh=*L{?_&O^&DN@U0u>WE$p4$T|HfL#nlrtR<&K5wtL#% z<&_gFCsg$&_9wO`bhYj5+BtRSgoRV)O=VPgX7;&obg(z~w)C|0bYv6-qF$k4uDR%j+(xvX$|-7zek-u z^x(yJl||qA{JxtsE-RW>xzP98Z|NG7Mvku5*6vp6q|T1et?eA&XJw0iXVp&moo}I* zmE@Bb(!|ln-tyfw>bvun?@k;O*iTM8(X~IGM+FLot`5sn{fiD;AMnV zD_vh(zjc1QKI>-n%{i;))^ADgbgp)q;nLW(t!G>BHjdt{^S1PC`p(q;wdFh0Vu*s@ zj3vJrrv7H?uU=Tx)>xh1+8wpIo#Th=UnWuRA06GI+}sSHp#dHS2w-9mVF+LdU|?hj zWC&znWC&siVqjzlW(a0rWC&phVPIqkWe8kVPIy6Wr$^9W{6{mV_;^8XNYHDVMt&|U|?ZLWJqLSVMt;~ zVqjrNW=LjW0l9;53S%1s7XvF}2%`z32qPZ@4+95y@PdPZiQxsqE`}b4IPkCm#I?kb zpaBDjIz|Q-1||kk1`!5k25|-n1||kc1}TuO3=9lR4D1XdU?|D}f?^CHD9!+a5)2?H z$pC^<3?N8L)r?@5h%$(Q-H7Z$4hBXB5EcTvnUR5qfssLsL4<*kfssL!fe9K;j11W2 zL9qAL!i;F>o0ofjD1{nrf2F8-q;u2`6voJ8`B$nhca4>+{ybS;UgKcKwJi<7E zft7)W@eAV@hF=UuV48vP3y8(Qz{JJI%*M*i#01g5lt4h)VA0i2-z z=Lrz`z@G8{|NjgOoJTm1FgP+ef)z6{FoSI}U|?q80u6RCK+-2@3=E_IWGV=Q26#ZR z3L5JI#VsQP6GJk{GzLZp2~rF;iIIU}@`62L@%%Pl85mjbfW=XW|KB)|aDWDjn86_l zl4At9ih+UQ1m_V(9Y!6_BVdz2GBC`@dIuyk95BdB0SpW&Aew=V!4DJzESwCC;DI=1 z21XW61||kR22hNE*vt$D40cd8EDQz=c~CYhg9yVsD4UJJfMF9uIzs_NK0`4>JVP== z216o4CPN+=r!yEa#4{8#WHRJ36fop4q%y=aq%jyV1TmyCq%)K< + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f2.ttx.GPOS new file mode 100644 index 0000000..a35678d --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining2_simple_f2.ttx.GPOS @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_successive_f1.otf b/Tests/ttLib/tables/data/aots/gpos_chaining2_successive_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..a3aadafb4aaed09fd3c1ddc1c61060778abefab1 GIT binary patch literal 5732 zcmeYd3Grv(W@unwW-xGeb5l?b>uO|RVA#XJAfV#z;_Ak(%C5t}An<^JfuX}az(4rq zg3p2s41yU942-M%gY}Ja&V=q|U=S8zU|>i{&P^;}kY%Q>H zGB7X~^MlB8L0JX{hCo>c26iSPhBTNR%nS?+tPBhc>!JdJE!HI!^!Igo5!GnQ;!JC1B!4KpJ1_p)@1_p+31_p*G1_p*$P*5{4 zFeEcDFr+asFk~_?Fyt^WFyu2ZFcdK`FqASdFjO!wFjO-zFw`+HFo2@7g@J*goq>U& zi-Ccmmw|y{0s{lXWCjL?X$%YuGZ`2d<}ffY%x7R=Sj51z637`8AlFl=XFVA#dLz_6Erf#CoH1H)kk28Lq{3=Ah37#PklFfg2FU|_h!z`$^o zfq~%$0|Uct1_p+E3=9kp85kIzFfcGYXJBA>#lXPumVtrc0|NuYX9fm_Zww3!KN%Pp zK*`}h0|O%y0|O%~0|O%m0|O&B0|O%;0|TQV0|TQ70|TQt0|TQJ0|TQh0|TQ10|TQn z0|R3k0|R4rx34ux=Ffex>>q;x<$HWx}ib3p<%kAQM#dVx}izBp=r9IS-PQl zx}in7p=G*}LAsG)x{*=3k#V|_NxG3~x{+DBk$Jk2MY@q?y0JmJv0=KgQM$2ly0J;R zv1z)oS-P=#y0JyNv1Pi6LAr@yx`|P`iE+A#NxF$?x`|o3iFvw-MY@S)x~W0BsbRXQ zQM##dx~WOJscE{YS-Potx~WCFsb#vELAse?x|vbBnQ^+ANxGS7x|vzJnR&XIMY@?~ zy17BRxna7wQM$Qty17ZZxoNt&S-QD-y17NVxn;VALAr%ux`k1?g>kxtNxFq;x`kP~ zg?YM#MY@G$x}`z7rD3|IQM#pZx}{0FrD?jQS-Pcpx}`L4HwUNotCof@4mOLJ&xKu|iO4acWUnYKk5t zb1^V7b|f*Rz_J4yGz$nolf4uJ1A{y?xoa{oFz7*(xdj6QgDo_9yE8B__&}3&7y|=C zG&DJ zz`$t4z`$tEz`$t3z`*Fpz`*Fnz`*DYDw!A<7(*Et7^4^%7~>fj7*iM+7&93d81ony z7>gMg7%LbU7;70A7@HUv7~2^b7<(8P7$-6?FivA&V4Tgsz_@^cfpICQL}g%LT+hJ3 zxP^g%aVG-<<30uk#={H@j3*cv7|${=FkWI{V7$)2z<7s&f$<>&1LHFW2FBM642&Nb z7#P1Aa2as%DR3!*2qh4q3?fuOger(o0}<*VLIXr-a`7p0DS!w?5TOJjltF|Fh)@L) zY9K-#L}-8rO)fqqkPan~9ZFnEAg(fqPyrFDAVLj9sDlU%5TVJ%rwr1e4AP+t(xJ?y z3=&fT5vm|U4MeDe2n`US$;GDv(xC#*r1rcf>LLEeCfCx=4K2?wo zRgext^$;GDu(xCy;p#jpN0n(uX z(xCy;p#jpN0n(wtr2$f`$;GD$(xC~`p$XEV3DThn(xC~`p$XEV3DThn(xJ(v$;DT+ z649t@m7+As?7+Cok7+9M? z6*U6`+Z_f5_DTi@4i*Lmj#>r=jwS{Mj^^*GKXpYN6Y_sswK_EZ7Fh7R;JZdVN5h8A zlQv0DoZQ#nFMr|qrd7S4dbUs4*1oOfQRMer;~m8_@_OUUR-LY+>c^>5}i;j?oORqxDx+x30*tMfCa%Fw9zbpS( zBYJG%iA|^bZcgaa%8s`G&G~ylNYihd&YWqv6LKarPH3CjwzuZ!De%; zduzrt4#m#j3@r-%opYP#_Rg98UGe+$Ll?etUjEMK@m=Y+L2`I)bYVnAa_#JhEt5IE z|NOH?l>7OQ8=^BN&X_c_|KRNJK0Q;cdg?mrI%=EZehbtlHgj}#_H;>ivgg)JNM6@| zyzONBu7rCIzZrf@S(ZCgsXa@qEp2Y8oLbh~+1uIEIkjDeu_T4uGZ zsa;dMuH{qZotL|B?z`vx)^2@!Pe*Ti@0>}qr_7pu<3#p1o6QEhezP|jBy$uL&rMrD ziQ|XPpBtjw-=F+s7j@FL)pym(j!Q}D4DPL-&@!{-ddhdP*&nBN#&^bdB|t*3XGiaj z-W?nhCwBKtkl!-7^5lJ`QQw6-`#O3%ds`Fnw1l4|K~nb0OT%gsgUMBJsw?^(xp|K^xd-dEgFTK8KdBI7qtYigHoTjAQg zg<1Ktvo=Ls>ps?dZ^HEjdoFM+S>L^T+SYlO6SgJp@IC+AxX6BK`L^wcThD%&`LXR& z7bF^gGxq#un(&*UileKiySqoKXiZ$<+*FPq+J8+&xqm4C)DTVYO`Dq0{97kG__u7@ zZ>fymVuyY^e^*TWZV>xj`ujxh@BAq@vpcM27EVa7EpAM&$*(LdocH0>cd_qmb2(N| zSk=55Y8A)t*}wmY?mzNf^U~JeOl!>Teuw^MU8A`&?^FNtDJ?U*W=`Swq56BjDEGS` z-$YN%JXv$PlVjncp5BG>#aR`prKx7WHSKrV?&V&aawW+tE0sVRBs$NBtiL(caG9uD-76?K8Tj zG%u~3kvcJdu}XsOZ=Tu8vz)#w+`hMJ&BD&DljgP0>zgx)WA*n4(cb8$=&ZEdOtaI? zTN(~u`7U#@WOB#k)=90sIsG}kxg6hv|4b0&{=xr~Uo>)d^pfz2zik&~|DJqD=d68o z|C>&ZiBmvj%7O*+X3w58*{k2H)weyXDKxp(v&42+==|gv6_Fb%c2@1E+EKl#VeRzQ zy`Lv`e-COu$tt}O$rR8O1wLP`nH9cWRGmp)^KViYFIaB8@n6zN_!j3I%8!I-~a&&cec6D{u z_SH1CmvMX#{j*z?`+L++0nwi3o~GXB_Sml4D&P3u=2pKoDt_BRqH@ylSx4s{KGeQ{ z=E45Cq4PtRgw0FaJFjjthsAfV)bD}W-@`d3^-S)WEZ4iFVtQ3aRc~c&UlGTe-vOdC z+-G~t^Gs|@ZcOZ1a%sh-g;zMfFZ>fC%6;I+N70Jlrt%K|4ufTeeFlAQ9TV$3I68XT zdphOXs;5>g?mRj1yLRv4g9{ccnLBIA@lV<>wAU?~v3T~fh35|yeGA`LIj?YX$@G9# z4jG}99EDAt#nWm#J3BkOIykocUMYIFcm4Fs>DN-{8`;FTCk1pmwNBpFH^1{h=hl{O zE!#Q1Ykc1;daiPlZA4I*zjeKRJx5n}SC@283wvjGS5KE*arMNERc+U%?Vh%GdF8~) z301v`{fVs!U2QwNc23YCUwL2lpN<+Bdga{M;?-74B&F`=@*f}@A6 zV}8fH&iNcajQ@y>a{sXY@l!NzY08f9iNB+ldH#{!_*-DtZ|#DW-`Pz#+9yovoG5*3 z!i9O8&&*ynZQ-K%OO_X|$*iwyXlQDvt*s~tENjhd%xKPNDw~}*t#+aFcka30H-34; zF>mgw?!}WgmToBBP`a^l>5i-=99RDQ6y-km{CHdrq zG;#E?w|sYv`tH2tyA#I*_LCD&bnTDl_-_B_j41d1AHPKBn@zB4H|`AYNN*44Xs!P3 zJgHP}TIQDam0cX4-)xxp30iV1v?|8@W}G$UNY|0RBfYD7SNE>k+q-YVUTFOm*%j3m z(b;#i<7V|uju#8Qb96tioL9bjYVYc)F=3jiYz#ye&POzB9FdZTZf$7^2`eW65uZ zslS=}s}~lvHCCs$c1LY)=lJ3Jmr0cSM@P3PH#Y-lXn=83Gv? z8G;ys7#JCX8G;!Y8A2FB7#JBs8A2JD7{VCB7?>Es8NwNu7$O)V7?>C$86p{&7@`=W z7?>HN8KN1O8Dbb>7?>Gi8Dbfj8R8h?7?>I28R8jO7!nu~7+4q*84?*-7?K#07+4sR z8Il=TK<;3i#@NBY#lXrK#%RVU#wftR!@vO^yx?G9VtB={hoO%l0X%E~aV;SdG+2O8 z!w9xRl7WRmltGL^oPm)+f`N&Fk%5tciGhKEiGiI#1PnzPKv0YU1jQLZP=WyjB^f|a ziU9;^shSb&5{Mfm7$g~_Kp_KmAqN8^0|*O&MHm@)7#JDE7(~E+5C!`JpFAj@A@U$I zK)SdXM4({-u}7LghC!Bru_U#)1RCxv42(I6C3y@S44_so!~g$an;96GIFB$+U|?n7 zVf@1Qh2a;25twFR`~qSzFfehkF|)C9Gckd5f$W9Hf}@L*D}w`rBSQcus1JGqL_V-* z{Qv(y0|Vy~&La$t431#MObpCmy9^kZ8Mr_LUJQ^lDg%~=MC + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining2_successive_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining2_successive_f1.ttx.GPOS new file mode 100644 index 0000000..6775e5b --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining2_successive_f1.ttx.GPOS @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f1.otf b/Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..4f13bdd6be9e938759473ade28fe62c6a497fd16 GIT binary patch literal 5504 zcmeYd3Grv(W@unwW-xGeb5l?b>uO|RVA#XJAfV#z;_Ak(%C5t}An<^JfuX}az(4qn z^P>U=2Ehyl28J8{!TLryXF~TfFbFv?Ffb$}=Oz{~NHaz=FbFMRU|F))ZIFfcGW6yz6|{GZQY#K0h$z`($uz{tSD!obMFz`(!=Ql3(p zn_G$Cd;Q>N_)*XJqk-v11M?3FmfwOr%yL`|9RIt!p%f2$cef1Z5gCp&R#^sybzgX8 z85o$0`9Wm4pezFeL!c}J13QxtLmJEuW(EcZRt5$Jb_NCpP6h@BZUzPhUIqpReg*~x zK?Vi}VFm^UQ3eJEkn^P(7#QRj7#I{mUS(ilP-kFZ&|+X<&}Cp?FkoO{Fa`w)0|SF4 z0|SE%0|SFS0|SE-0|SFA0|SEx0|SFM0|SE}$Po+-3?U2*4B-q63{eaW46&e~W?*1Q zW?*1QV_;y&WME*(VPIg$XJB9`Vqjn>Wnf^aU|?XVW?*2bV_;waMP~~G14BCl149=B z14AzZ1H%Lc28PKD3=Gp47#L17jKk z17mi&LAqhOQMz%uNxEseS-N?;MY?6Wp+UN#VY;DFx}kBpp-H--X}Y0Vx}kZxp+&l( zWxA0;x{+bJkx{ylak`O7x{+zRky*NtdAgBBx{+nNu|c}AVY;zVy0LM(u}QkIX}Yml zy0Lk>u|>MEWx9z$x`|=BiBYxkrE`8kWl?5&Mu~z(vZjKOfq{{N zV@iHfszPvOaY<@!v4Uq_a(+=keoD6M04F;x1_nlcXmXQeU|^JEU|<9#GYtj? zMqLI5Mk59WMso%RMjHkOMn?t)MmGiqMsEfN#sCHe#!v#uNqy#!LnV z#ykcF#$pBr#tH@o###mj#wG>^#&!k<#vTR+#)%9JjMEqx7-usuFfL$VU|h<;z_^Nm zfpI+p1LGD32F9HX42=627#I&TFfg8AU|>AUz`%Hkfr0Tl0|Vn71_s853=E9V7#J8| zGcYiIU|?YUYQSZ{#izif2qKg~gffUw0THSoLJdTyg9r@}p~=Ok$fW=x6hVX%h)@O* zDj-4?M5uuXbr7KeA~d=9lt4O^Kz1l`DS^1kAVLL1sDcPJ5TOntG(dzV7oRdnhcZZq zGDwFqmoi991w^QV2sIF)4k9!_geDiC3P^_vNQVkYhYCoC3YQ8geDiC21thnNQVYU zhXzQ821thnNQVYUhXzQ82A2j%u_hOvCP;@SNQWj!hbBmeCP;@SNQWj!hbBmeCP;@S zmnIiq(UPC4OMbU6`R+2~(30QNmau(azvR#1C6||YEn!EO<3Px9uI75cy_$#T!2}*& z8O|djGMq<5!S$UmgAgolgQ|_y3=E873=E8G85o!x7#Nt2GcYjQF)%PsW?*0mXJBCE zXJBA$0#(!u3~YB87}zTr7&urM7&vMf7&w|37&w~0tNzp#bxg?rZPn_~_*-DX?}G0d z@f-~sHc#3lJ#lhhf4}^NC>R)0@VZ%=Q}%+48| z)0#QH*Z%n}%KhE>r>1Dn@9c@cvwMDL_a`3g>)q1zJ*V}1R@e9JhINVEEomIzZ~oO3 z<^HbxV~yysg(o(h?z=gmPb)jx{x|3E1tCqpZ8~$NN46e>1cw^moo}p4&TT_IJha(+^$v&UyJepT~El-v-Iywb6wU70I=; zBeqQD`2O?H8d2`&KW>Q5m^fq7%>IM3zx(t|vFfSosOzY0iu)~4o7l|J+1b-2-N~L? zGa-3h`|-Au?Yk20Is9h$EoE8mP^I=Pv9`3irE+RnZ)b03Pv_LeX&qC0IeL4$dZcH! z%xam{vZi)T?Yfpvm3Ln5zPaz7_glO5?L8g6?Y(m*&7LxA`i&FW-)uG;?E1~#WRT2J zP&_wn{UnYbI)846a({pFlU>wF*H+(ED?2VFr8Bs9;Oq|%=GeLgK1*siHM;g>zFmerW$S73Kb+{8K|Ty*F)YM)Pl- z?BL(BX}_g1ev2LY?fhLa@w-9ncj@mFy}$FP+|2H1HyxBDIXn{|!m%Dhkg&!@D^?3y`+_+ z{i59OetZ)>HS=W6=}wM?i+Xw&$`@x`so>n5gDxX_sD)S*}&k#P`GOuf8bv_pLuIM2)8_skfMRl{6Mk zDVy1#j5E6;NJu5kO_sx=Ecw@#YZKCf@iB#zbJBSd?no1(MQ zax=|NH*aY;eC4~$#gfS#lUpaX_U82G^yYGW5B@Vjl=}z&PkzzJ+0jeFC;qlwl>K}1 z9i6lG)%|ZeIVMg4l_?7r%$q%X&SbBCuU6motftW9TF(;OU7_=nXH-OPsMuMxqiRR> zs)n`GSNDFN*!?}I{Uk@%`nL6L>pK^C`%TTi^W7 z1D#u2wzX{M_^$DNujskTO|}t1VgAdmXGgh@-o3?w} z-sP1OD<@R-CiW+`CUmv!?AkeX=Y)k*=1p3_F{x`}#{{{3bC=ILT+8v>?02hZf5n8# z{tAvBwvPE7^E&5q{4o9_F3SDG`o~YvxTPsO!YBTYUgr5ndgE__UB9&pR(@wU;b@;Q zsdJ+AtqB+AZ9X%5*|dd=<}X=ZxF)l{uA!l+p|-Z7B(SVCvoWJNqp56m-n80<&fmG` ze&6`z4adB>tGX9Y-dMV!bVKRJ%B4H9mT+A8^HY@j+>bY+lc)4dk7>q%(5>wp-)CiuerMHA`JHc} zmX+j_7t+Mh$KLYYHR`+bmhVm+6WC8qJkhm3p5wdypEIJ|`+xiroo_b5s@=FVyd%9m zoTIh+xAUY@xoMeO+E;dQe15ZG;wNaytRsKtYH#np345XS zTVz*MTSRBy&5oPZH#uG`_|DP&ymDUo>Z!e}yZXBOy87B?wN9$;S6uvCuR}4HqpG#F zzCo(1qr0Q0gX2fYpI@Tf=YBGYPGIZHsmyH8ZO`th?@Hv*Wp8fish!-^`3I!t@F0@Z2HdB{aSi{)Ye#?-r60txt-&O>t7~O?jIf9qTJjJprHXC1_)qc5Mc;l2w-4j z2xJIkU}Okl2x4Gl2xbUoU}Okk2w`Ak2xSOmU}6Yk2xDMk2xkaqU}A`1h+tr1h-8Rl zU}A`3h+<%7h-QdpU}lJ6h+$x6h-HXnU}lJ8h+|-8h-ZjrU|~pLNMK-LNMuN4U|~pN zNMc}NNM=Z8U;(@12*Wl8E(TVH7KRiC7X~{99tIBZ-~|T*GXo=o3Ih{^9D@V{BZClF zl#xN4fsuicK@3caf=LmuISdR;3>*xMU@Qa{VPxO|s|3k2GKexTflUCZ!7dL96^MS2 z8jvn71`!5P1~CS41_=g91}O$<1{nrf2F8-q;u5GUSr`~|5=-(JI2b@}M27$W!8S8- z9$}ooz{m>3utm>7~lrZF%=NU(a48yFcF=KedoES}%yD+43z9k4hG@&6m=5f0Gc z3Ntt)L2`^BS1~X!oZvjdsKcnkc?9fxkPHknvfcs73JVP== z216o4CPN+=r!yEc#4{u@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f1.ttx.GPOS new file mode 100644 index 0000000..c362427 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f1.ttx.GPOS @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f2.otf b/Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..48be5dda3392737e7df10ed078a9efd4ee31e27a GIT binary patch literal 5508 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f2.ttx.GPOS new file mode 100644 index 0000000..7b27f90 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f2.ttx.GPOS @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f3.otf b/Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f3.otf new file mode 100644 index 0000000000000000000000000000000000000000..a10068bb587e202e854d03e5eb7baf89e330bdbd GIT binary patch literal 5500 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 ehJ1!R215ot1_QEO56ab`k*ztPF-vfGq7VRu + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f3.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f3.ttx.GPOS new file mode 100644 index 0000000..73df34c --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f3.ttx.GPOS @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f4.otf b/Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f4.otf new file mode 100644 index 0000000000000000000000000000000000000000..8030ac0eb455272fcd0bf321f6deacc1816fa818 GIT binary patch literal 5500 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 f8S)wO7z`Qo7!1gAJ!rTPG_o}ZG-e47PZR + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f4.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f4.ttx.GPOS new file mode 100644 index 0000000..67bfc0e --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining3_boundary_f4.ttx.GPOS @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining3_lookupflag_f1.otf b/Tests/ttLib/tables/data/aots/gpos_chaining3_lookupflag_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..7864ce02e82dd1a83bfdbb1aa875b04fee8e8fed GIT binary patch literal 5548 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 H@I@g29ExQb literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining3_lookupflag_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_chaining3_lookupflag_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining3_lookupflag_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining3_lookupflag_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining3_lookupflag_f1.ttx.GPOS new file mode 100644 index 0000000..b12b665 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining3_lookupflag_f1.ttx.GPOS @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining3_next_glyph_f1.otf b/Tests/ttLib/tables/data/aots/gpos_chaining3_next_glyph_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..20a7966c5fc4539af76c34d5d8dbbd2436b8f238 GIT binary patch literal 5524 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining3_next_glyph_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_chaining3_next_glyph_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining3_next_glyph_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining3_next_glyph_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining3_next_glyph_f1.ttx.GPOS new file mode 100644 index 0000000..d5b0c1f --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining3_next_glyph_f1.ttx.GPOS @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f1.otf b/Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..2c633280ebcc71c37543f5504c76bb4f490da266 GIT binary patch literal 5496 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f1.ttx.GPOS new file mode 100644 index 0000000..b9b0ce5 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f1.ttx.GPOS @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f2.otf b/Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..6ef8fb118201178636106301a74c6284ce2d3438 GIT binary patch literal 5516 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 E0GHcdAOHXW literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f2.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f2.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f2.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f2.ttx.GPOS new file mode 100644 index 0000000..6d023ec --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining3_simple_f2.ttx.GPOS @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining3_successive_f1.otf b/Tests/ttLib/tables/data/aots/gpos_chaining3_successive_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..8e8439d9e7b7c8299f25ade12c71fa2e0726359d GIT binary patch literal 5544 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_chaining3_successive_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_chaining3_successive_f1.ttx.GPOS new file mode 100644 index 0000000..f7c85b6 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_chaining3_successive_f1.ttx.GPOS @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_boundary_f1.otf b/Tests/ttLib/tables/data/aots/gpos_context1_boundary_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..22bb3eacda800510dafc003d53d8d9aaf10ec58b GIT binary patch literal 5480 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 MIp8n@2P8590KkD=A^-pY literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_boundary_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context1_boundary_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context1_boundary_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_boundary_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context1_boundary_f1.ttx.GPOS new file mode 100644 index 0000000..662ae54 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context1_boundary_f1.ttx.GPOS @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_boundary_f2.otf b/Tests/ttLib/tables/data/aots/gpos_context1_boundary_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..7e4c0e198a79eb69acd091122d8828bd4a42662f GIT binary patch literal 5480 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_boundary_f2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context1_boundary_f2.ttx.GPOS new file mode 100644 index 0000000..56a4f7b --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context1_boundary_f2.ttx.GPOS @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_expansion_f1.otf b/Tests/ttLib/tables/data/aots/gpos_context1_expansion_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..f7c60f68b94ad5d386f412ba531077afcbf3d218 GIT binary patch literal 5492 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_expansion_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context1_expansion_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context1_expansion_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_expansion_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context1_expansion_f1.ttx.GPOS new file mode 100644 index 0000000..77d52eb --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context1_expansion_f1.ttx.GPOS @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f1.otf b/Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..e9b03ae13c06a38295779d199504671fd3859b97 GIT binary patch literal 5508 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 a8ZziH7?5c{XxI=m!Zimpb_oh!R15%ijba=C literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f1.ttx.GPOS new file mode 100644 index 0000000..2d5f796 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f1.ttx.GPOS @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f2.otf b/Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..b9998cd05c67a601f6a95d233794d989dca8eb84 GIT binary patch literal 5500 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f2.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f2.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f2.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f2.ttx.GPOS new file mode 100644 index 0000000..ef419ff --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context1_lookupflag_f2.ttx.GPOS @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f1.otf b/Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..6f1aafaa213d3851d5de40b4c4c0d88c00e0c021 GIT binary patch literal 5568 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f1.ttx.GPOS new file mode 100644 index 0000000..82750d5 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f1.ttx.GPOS @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f2.otf b/Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..ed9a387640dce4dfb497d4d67cfdc998a5626238 GIT binary patch literal 5568 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f2.ttx.GPOS new file mode 100644 index 0000000..764703b --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context1_multiple_subrules_f2.ttx.GPOS @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_next_glyph_f1.otf b/Tests/ttLib/tables/data/aots/gpos_context1_next_glyph_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..6007d54cfaecfb869f54986b8e5a85ab5e40bcb7 GIT binary patch literal 5500 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_next_glyph_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context1_next_glyph_f1.ttx.GPOS new file mode 100644 index 0000000..ac00f86 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context1_next_glyph_f1.ttx.GPOS @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_simple_f1.otf b/Tests/ttLib/tables/data/aots/gpos_context1_simple_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..5e6cd9ed3abd67b24262532a7f411d909526a01e GIT binary patch literal 5476 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_simple_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context1_simple_f1.ttx.GPOS new file mode 100644 index 0000000..031f56f --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context1_simple_f1.ttx.GPOS @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_simple_f2.otf b/Tests/ttLib/tables/data/aots/gpos_context1_simple_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..b4fddb33bebfa6821cd67bcd41b70bb5c607317a GIT binary patch literal 5468 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 aLk2wt0}|~64F`fouI7NoB*Ec^LI3~&mRckL literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_simple_f2.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context1_simple_f2.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context1_simple_f2.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_simple_f2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context1_simple_f2.ttx.GPOS new file mode 100644 index 0000000..bb3d01d --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context1_simple_f2.ttx.GPOS @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_successive_f1.otf b/Tests/ttLib/tables/data/aots/gpos_context1_successive_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..eb0f962e12474749d0d5a9e46b27602b9e2db9d7 GIT binary patch literal 5508 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 X9)kgy_Jf8EK_gsqKx3Dn@I}P{F(YCf literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_successive_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context1_successive_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context1_successive_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context1_successive_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context1_successive_f1.ttx.GPOS new file mode 100644 index 0000000..2d17ca5 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context1_successive_f1.ttx.GPOS @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_boundary_f1.otf b/Tests/ttLib/tables/data/aots/gpos_context2_boundary_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..3fce49709ceac13a3124b48957e00f68b5146768 GIT binary patch literal 5492 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 Ta5rd}5HzAS2OMVLfJ7z$BLQ6@ literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_boundary_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context2_boundary_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_boundary_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_boundary_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context2_boundary_f1.ttx.GPOS new file mode 100644 index 0000000..b830138 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_boundary_f1.ttx.GPOS @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_boundary_f2.otf b/Tests/ttLib/tables/data/aots/gpos_context2_boundary_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..5b4e012335179620a56fde051ec1b8d224f0b6e8 GIT binary patch literal 5496 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_boundary_f2.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context2_boundary_f2.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_boundary_f2.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_boundary_f2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context2_boundary_f2.ttx.GPOS new file mode 100644 index 0000000..a48dc6a --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_boundary_f2.ttx.GPOS @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_classes_f1.otf b/Tests/ttLib/tables/data/aots/gpos_context2_classes_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..585b511c46d0c874e839cbd772c9f8b2abc090cb GIT binary patch literal 5540 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 YgC2tc$##N<6+t6lb3kL8;P68s0Cg^59RL6T literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_classes_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context2_classes_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_classes_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_classes_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context2_classes_f1.ttx.GPOS new file mode 100644 index 0000000..7573e48 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_classes_f1.ttx.GPOS @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_classes_f2.otf b/Tests/ttLib/tables/data/aots/gpos_context2_classes_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..411d58e6da6620847ab1dab80747d9a785880aab GIT binary patch literal 5564 zcmeYd3Grv(W@unwW-xGeb5l?b>uO|RVA#XJAfV#z;_Ak(%C5t}An<^JfuX}az(4rO zwYPg17z8sI7#NlOgY}Ja&V=q|U=ZqIU|>i{&P^;}kY%Q>H zGB7X~^MlB8L0JX{hCo>c26iSPhBTNR%nS?+tPBhc>u28Qhn3=F#%7#Q|4FfbfoU|=}Rz`$^ffq~&90|Ubu1_p-n3=9mH7#J9?GB7aQ zU|?Xl&A`BLkAZ>VAp--$69xu`=L`%CuNW8@-ZC&Sd|+T;_{_k-@Qs0i;U@zF!yg6) zhW`u3XjDiddj3NvSjN%Loj8Y5?jIs<2j0y}4jLHlQ zjA;xEjM?c1>4xb>>Bi|M>89ys>E`Jc>6Yn+2I+=|>4rw>hQ{fJCh3Nz>4s+MhUV#p z7U_nT=|%?WMuzD|M(IYz=|(2$MyBaTX6Z)e=|&dmMwaQu2IBdIs#>VN!Ch5ke z>BeU1#^&k97U{;8=_Us0CWh%IM(HNT=_V%WCZ_2oX6Yv8=_VHGCYI@@2I;1T>83{M zrpD=}Ch4Z8>857srsnCU7U`yz>1GD$W`^lzM(Jk8>1HPBW~S+8X6a_;>1Gz`W|ryZ z2I=O8>E=f1=EmvfCh6v;>E>qX=H}_<7U|}e=@tg*7KZ5-M(GyD=@usG7N+SIX6Y8@ z=@u607MAIj2I-cD>6S+6md5FpCh3-@>6T{cmgeb}7U`Cj&iMtEMVaXtB?=nJnhHh+ z21W{wDfvmM3c;1dC8@c^3Z8k%`9%f!MTsS;DS8TyIXMbJAmzmhL8-;5MP;cedXUV; zz{uE<#E=5Z4s6gYAOKDFQVa|X^3de2$-uy%2TkS{3=9mm(B$pTz`)=GP1a!y3=GlG zoz^#-P%n5}JIQK_w3~*-m0$V3-a~t_v9$7?wek=>`S{hON-#xu1c7;RrNY zo?~EOxC~8>cNrKM9zl~KsC4=WO@6;2r3X0KaWOD3@lqjr zw=gg;?qpzK+{eJcc$k5K@dN_{<5>m<#!CzgjMo_$81FDJFg|2pV0^~F!1$Vhf$;+a z1LIc%E(0z;1ujJpp#&n7L4*p3Pz4ccAVM8PXn+V!EoR6r7{AVLj9sDlU%5TVJ%rwY=c z3euqp(xD2{p$gKW%B2dDRRa;~AVLE~XmatXfpn;Wbf|%JsDX5-fpn;Wbf|Huft09& z2n`US$;GD*(xDF0p$^ia4$`3x(xDF0p$^ia&ZQ1grvV}~x%f0dIy68!G(b8uKsq!) zIy68!G(b8uKsq$IG(d_qx%f0eIy6B#G(kEvK{_-+Iy6B#G(kEvK{_-+IyAX7x%i5f z{8U}?yM4)bml=na{GPUi?fd#Ae-1CXyu@n>JGvYPLXLAa*8}d=JUkC3@bJoT9ubk@ zJR%CN?}QnIV0jx>}DLjG^7R)@yl0tW;c*j_-Vt zqHo+*{qCw!sMATb{>{85e0J`k>YdqdyS}e}b$-T_xt+86dwP0%dV6Me&gh)h%<;YU z&u>xg@6JCpMSFf{PyC(T^E3e{zGpYAOYCk*)PhCe{M{QHwZ-LsxW{%Fzo-XN5_S~8Y$?MvW zx1DU?m2l7DH^Xl!%W{V*wP%U7rOhpsQ_FfgdpmnNr#4ROnA*$H+uPM6J-cOA%dD0) zwQFkEwS20)^K$piefPZI+O2Qz>F90moil0nlv&enoXGxWv)N$RZ}ujGWR8O3xoPVs zas1Hvb3>H-`;(vSqE5QD`mS2paVaUC!M)WJT4uIfPx&r3`{UHk_|EvQ1V{+>?C9Om zyMtrm#O|I6@>?di9!%`u=br1fUq^3eZ|lV7{-!RD*4B>pcKOaO z5a9S-^+!i^%FKx~r_7kWc*DLq3wNL1dvfo|6-qt7nc9Cdnl$|uuZx_|@k8!+z9{#W zpJJjtojqM$QZ2nL6WZivxw$Bvh`SW|J?r@H-yBoQ`-(eC>wb$wWc=o7P3`h+D_onm zFe`s{)~1MS-N$$`VP+dA)Z!nVX6zUO}%7uhc@-?sg5>)8)8Kem19 zf<)tQ#-86y6Mi#Padh=`clSsYt%)m~o67M+`>&}e_YdWt8lvgFX;U+rf9qri|CUYr zEtTJfc8Ar>!U^fM#f|AT`IUu*^FEyVF7};mF30K# ztD09st>XAS`}ZHw{YSoQUfTMbX^pwv@6g|@YcyBped>QcrDbN<%qbi{RDbUm<$m|$ zo9L;TCu>f3ax7fb)4Nca#k8xWv2aT1l(NYs zGs-76Pi~pgmROThSQeCHvE{eilguSei>sIQJne05ySS6%_@=h+tj$L|7uPLpTG%|l za#qdkmPt+hO?`b6`X=;E+;+e0QODEP`Bjr^I~u1nOs?zUsQ<$t+S}RN)z>w>eMZ-m z=B1T0QYYpwR!PwP%`;nhmeY5I+xJ$jS=hOC(!BP0eRC#pto|M$+8f;zot2iGX?D7K zOT*zS-(@bAOzxQ6I;picr$47Rm*aczp9!MeKlp$0i$>0lUJ^d>x9y_r-;?j?oVBm+ zf78h^aSEtRS+HQ*(CWhuZhg zJlH=sbbjcPuz6{F=hbcIu=wtk`aLlFdpO6Wp2aDEpE8RohK)L*X})haKVBlb7w6%{z?0V_PRwg7SCR`@cf~oZ{hnY=M_#anI5pp zAtSVsqp+#7cv@{|XJ=XPnhVejnj>gkdzuAZ2&s_oje-P86iubfyp zp{h5rKe08Tt8Hi3&Z#>mESxfL(gKc2T@yPd$nBfEeAeMwj^Ad#TSfaTCRFxUaP+Ws z%N;g(6-I29~+0CdWA)buq?Yq)R!J?iwK2QR*> zEc(vp_uZs%S<$@8g}&E*OV^k*a&)z}cDG6=b#{bqZRhwtD_itCt9Huod<(U#B%i#H zCXPP#mhY}n-<`L7cjB19esbc8uKn>G-|hdL5#`?hFwbht<}Gs zCzZ-g%iPkwvWw&Mn++2`K}&9hR>hd#jI*X3={nMPq<2;C>fTj*d-qM)3$5QGyQ116 zI{R*R+^oLI@nXSuj_&7`^U7CG?Ool~*WK6E*EXwlQgy%L;@^55in$zBt*!M9Qe7S0 z9X%Z!KSKWe66HSklSy;}TVGCPW_xaXc29j*B8M(}b3;$<H6CG zt@GRUSvRY1&RI3LeoK0%bG6e9m&UGbJ==P>arADTx20#(cc%8QE#H|ILlpdGEcwka z^*2+0^}?dI#_IIe?x@Y}96wzDGKq5k=;#*Z=4Jp54e&5P026} z1||kZ1}5+j2SkL4fr)_`WEuk#0|$c$g9rm7gD8V210#bNgBSxNg9L*F12cmZgA@Z3 zgEWIQ0~3P`gA4-`gDitA*en(X5e88PF$M{+84O@kKrUcp5CV%ZGVp-S0+|ak8DcjF z11@<`_(Jr9)PQtxL2VFckYJExkYbQ#kYSKzU@S>3E@9w+`h+nju_TXy1MFjl|Np@@ zGcYi59$}ooz{Ea30}2!r;i@2v!UWI|Bx01}@M*69Xh|$be;`T|wyJ z3MdW~7#J8>z~RQkkPI@7fe}K2)q~u?$iT3nXWNB%ew(ihjI4LS;wZ%bZ=6RsKm#?P zxP-_sf?UPGz;J@|2%`?84(Ac5{~@v3=C`xexTIL!pXo0 z9t2}%U}WKBU}E570L2K1&CFoHUowo3`n*UG~5Up8Jhzd^8|$>Dh2@2Oko}X literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_classes_f2.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context2_classes_f2.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_classes_f2.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_classes_f2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context2_classes_f2.ttx.GPOS new file mode 100644 index 0000000..4435c02 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_classes_f2.ttx.GPOS @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_expansion_f1.otf b/Tests/ttLib/tables/data/aots/gpos_context2_expansion_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..4b2d36ae7f945a0a0a57cf4021a3dc1484d34ce7 GIT binary patch literal 5524 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_expansion_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context2_expansion_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_expansion_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_expansion_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context2_expansion_f1.ttx.GPOS new file mode 100644 index 0000000..584892f --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_expansion_f1.ttx.GPOS @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f1.otf b/Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..1c0c48086127525304af510aa2bc6a1bd2eba757 GIT binary patch literal 5540 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 N|8qcNo8WLoApmKKWE%hg literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f1.ttx.GPOS new file mode 100644 index 0000000..99546b5 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f1.ttx.GPOS @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f2.otf b/Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..bf20d8460352d1ca583ad5fa9ee8879302f6511f GIT binary patch literal 5532 zcmeYd3Grv(W@unwW-xGeb5l?b>uO|RVA#XJAfV#z;_Ak(%C5t}An<^JfuX}az(4r0 z&Knm72Ehyl28LJu!TLryXF~TfFbIV(Ffb$}=Oz{~NHaz=FbHj7U|Enif85l%N7#J8m3i69f{?BJHVqg%hVPIfTU}RunVPFIq%?MJSQkt7v ziQs$v;Ai+z&-kN(=|=1_nitR~Z->)EO8Uv=|r|bQu^J3>X*~j6p%dz`$V1 zz`y_s4toX$1}6pv23H0K1`h@X25$xi20xG^7#J8p!5hxNz!1g2zz_=xY6b>|WCjL? zGzJESOa=yq90mr4d|ItB&?P;|C1Ffg<;Ffep6FfjBo zFfdGDU|^Wcz`!t#fq`Kr0|Ubx1_p-t3=9m57#J9qGB7Z#U|?Wa&A`C0j)8$;BLf2i zC_1+@Ffi<5U|`tGz`$^Tfq~&L0|Ub`1_p+c3=9ls7#JANGcYh*Vqjpn%D}*IgMoqJ zHUk60Jq8AbhYSo1PZ$^&o`aGM0|Ucb1_p)?3=9mP85kJ8F)%RvWME+U!@$7spMimq ziGhKUm4ShggMopOn}LCmkAZ)GTq!D-P|zU z+$i1LINjVN-P|`S{hON-#xu1c7;RrNYo?~EO zxC~8>cNrKM9zm1g8wLi3kI>}zn}GpTdVrH17Xt$$KQy^XGB7a8L6eyV0|TQj0|TQG z0|TQu0|TQC0|TQY0|TQQ0|TQs0|R3K0|R3y0|R3e0|R3`0|R3U0|R3w0|R3o0|R3* z0|R3P0|R3%0|R3d0|R3_0|R3Z0|Vnk1_s7y3=E9385kHBFfcGKWnf@j#lXP0o`HdJ z3j+h=P6h_XeGCkYhZz_cPcSero@HQQyu`r3c%6ZP@eTt6<3k1p#%Bx+jIS9O7(XyD zFn%@QGT`D<;8Fw;N+3cRM5urWRS=;DBGf^I28ht);#1^O01=8HLJ34Dg9sH6p$a0@ zK!iGo&;SvdTzpC(9ZDcOl(>{YTxAfU0wPpFgc^uY2N4<|LX(S68KgrQq(d2`Lzzn% zB&Gr)R6&Fqh)@R+8X!WGi%$ilLj|Nm1*Ag-q(g;E1tg&gBGf>HI*8B!5t>|lsvsSz zARVe89jYK5svsSzT&f^hH4vc=A~ZmRCKsO?NQW9ohZ;zS8c2s4NQW9ohZ>g}NQpX# z&;SvdTzu*v9qJ$*>L4BJARX!;9qJ$*>L4BJT^z?`ccezOP^M=kSutOT3n_qswt1Xu=!}DMQ53da85fK^A zBckB?PMARmmbXFG#%cxzMll8k#WVrh|8?x<_#_|6w8 z`o?Y5@2(n!I-Nx8-^^>mXXhTO-kJTj>-*|g=Vwfr+c~Shr>D24w`XSOjLvDz9N%mI z{1)Z@?)+0zwC8vB#NXLHzq9)j5BBwL>H412`aP@bdv?RR#O{_fj_)`BYKn4ySN^d^ z^w`1^n@;!LoY1G09c}-c^Y?;~rr$Q5In#0{5}eb&#jq|ysrIt z+sXD_3HKa+GyImaEO)3ZEI{@2Zs@my*&M+*>`NWoFCul<#7*KThq8?~LzCfP`Srj@}); zJ2)mz?CzN$zh!dk!Nd-Z&W^55>HhE19p5FRz6*Eub@X=jwoYvBZ|dS`ZS81pm+$NX z0gm5Qe{@8r%$zuL%8c2IH|(3UaQErGC-3a;%=P zs(CfkDvsZ?fBzBPf8@L7rLDi2)|lJ<4*ku#MssD}r~c7VEpBAFV)0Na)OuI@N3#XJ$DVtm} zqkLlXTYk$u$z0O3xO!R7)85v$i#s`vZ)*F_+I+NgaoxhEh0XIT zXVuJZnbg$Z)YmtmZ$jV1ZTHI_bv$jIUp2Y5qj5^ZhBSvz0pn4S!ubMW~ZCC zG#tM2UFKrRSHD-QZ+li#XmYJ*iS4e?`N=aXA~#g*tlCkvqk2`t+Ucu% zKTqub9@Ku4qicQJ`nL6*i`o}=ESj;eYW}=^6K1#1Xqr50TKA&z*&NR1qP>~@851%( zD%z`CTPjLR%gf4YduqFDdcuxo9-DiA!h%_Irp{e3X~FD;9b4KqR&1{2=<4k3>gufR zt7&R4XSXQ#_o$x&qCL$$O})+Sv0b%QzVW}!t$u4%{I-Kc<)q`Yj?O)NsD1y; zgZ*e-8 zGD0gk3Y$8Mr`2|Lc6N4kaBTU#QuJ=``stU`ucgj6vWam|3g~odoxH7Ye&>PCtu5PH zwsU;f_`X;4T;(R)h@dck>w5coj;`*mF6o{Y_Rj9Eo-VoK>WLYv+OAF8J#Fvu%88W| zs(KUq6I&Cy+IDvBoVs(u!YT76E#R2cHL+uY+`hTXXC1EP_-*#PRkXiiLS=siM-N-a z{Em5@^ErMP{}C7E{$c&&r)b>LlpWy{e@8F#{3E^bx4^F7+661Wvzu_VPngs>QTo<| z3-dOgnZ0bSzp)C(9}>{TTv2N)|%Ov(VWp#Hal-x?Lz18+;hKg{PKol z-rQB)izja^-B7xrbYtbx9a&2_uKf8a%6;y~8_~&AdZtL{{g$8iTXybm1&&VkiQ!!{ z`&>9W*qeJ>dRqEAXVy3}vPHkMYN!0pw@}MU^2rNn z;^<><`R*F^-FeG*Cyoj1Cnuih+8@vH-Tu!RQSSXeeu>UEn_$&$+!@}H-X6};TK(I3 zQmNdu%q{IJyEs0-*)Z`FwB%N3RgC$~IBUw0t|NU%dRO(X?p?LFci)7)(E2U1E2=G` zv+rid&FY&RFBW{~=zd-~uYC2?-ql@w-F;nsZL?Y@Rrf0{{;k)cn9EVs+FIWr)z#76 z(bK{4BjnF7QSNg;nM5bB_2pD%w&%8I_tbYKa_F)*H}up_ZtDHbI001fGD51AuCJ}% zI=@|?b+h{BoK3%0FDz5JD0vG}q7#RW?0vQ+? zf*67r7#V^Yf*BYYLKs3A7#TttLK&DC!WhCBm>9wt!WoztA{ZhVm>41%A{m$%q8OqW zm>HrOq8XSOVi;l=m>FUjVi}kj;uzu>m>J?3;u%;N5*QK~SQru+5*b(+k{FU0SQwHS zk{MXQ?zqNqf`N;Hm0=n~8ABLD00R#LI|By;BLfEm6N4Fp0t2Y+1?o$Ix+E+N%nXbS zOi&U;i!g{VFfxcTh%zuSh%tzPWf;L?Am1@TO=4sa0^7yNz{9}EAO@BP>0)94nF_KE zyF4hIKrVq`kWE|+A`GGoVhrL85)6_IQVh}zG7Pc|j3ueXB@7&3(^wc7a}rDP7&yQ# zXZZggY%>D`6Xy}e2@I?ZJd9r$zcBn_FapyIj9)-31_mZBHfAe?j(w+z-Z% zE>5lt4h)VA0i2*d+zAl*z@G8{|NjgOoJTm1FgP+ef)#?o1EkY{fti5|G+@L4iDMbC zEXb!23>qY1WME-XU|?WiVPFD>Xfnt&21W=8Ru3|Xk%3|DuWi%f`E9;3FtXkOi=z<# zzi}Sn01db>Gl0SpB*zGH6$1mq3C<&oI*dA;N1*-($w4q9>m88HaKIoB1u!tAfM^Cb z20u{xW8q|AWH4i3fU=nw_!!KfY-R>C1{Wxsg~5!W3d&|>5MkH=WwSAuF&tq?XDDFE zXDDWfXGmtqXUJnHVMt}DU?^cQVu)wRVaR95XUJwKWhh`sW5{7hWJqU-XGmi(VhCbL xWk_cz1&bAtX$E;VhcTow6fqPtWP)95$e_nyK&Jhm;Y85L*BsE8COBMC2mr&QWE%hg literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f2.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f2.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f2.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f2.ttx.GPOS new file mode 100644 index 0000000..4a0fcb8 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_lookupflag_f2.ttx.GPOS @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f1.otf b/Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..05b6b73cb252c64f04efdbf844e10396b09faada GIT binary patch literal 5600 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 TaT}yeVE8`=G%gB`4-^6b^dfN% literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f1.ttx.GPOS new file mode 100644 index 0000000..d2f38c3 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f1.ttx.GPOS @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f2.otf b/Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..f79712cc46ec193b40e84bb218c97672a7afd126 GIT binary patch literal 5600 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f2.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f2.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f2.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f2.ttx.GPOS new file mode 100644 index 0000000..e5d0a63 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_multiple_subrules_f2.ttx.GPOS @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_next_glyph_f1.otf b/Tests/ttLib/tables/data/aots/gpos_context2_next_glyph_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..1b5a256fdafb2f32f2faa45f96680e762cad69c0 GIT binary patch literal 5512 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_next_glyph_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context2_next_glyph_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_next_glyph_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_next_glyph_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context2_next_glyph_f1.ttx.GPOS new file mode 100644 index 0000000..c171781 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_next_glyph_f1.ttx.GPOS @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_simple_f1.otf b/Tests/ttLib/tables/data/aots/gpos_context2_simple_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..9aaec44ff09ec0ebb84e8af8c54bf9b27fd2d4e4 GIT binary patch literal 5508 zcmeYd3Grv(W@unwW-xGeb5l?b>uO|RVA#XJAfV#z;_Ak(%C5t}An<^JfuX}az(4r0 z%bPR?2Ehyl28K`m!TLryXF~TfFbKskFfb$}=Oz{~NHaz=FbM5oU|V}6$K0_0SpWbA|?zB z3~YIcxv3I-db1c9L~Ixs7&Qv=i%b5`XE0)55KUoVU{GLWU}0fkWMN=nU<4^oDb3BT zMDV?S@H70VXZ+E?^rM0KhXl)SK^|r~E(VVO-Q7@%hrPR7hVzIFM;fav1H-y6ys``o z%*Fg5vRqJ>fq@}VmVtqtNr)j0W(P9^0|P4q0|Pq)0|O@m0|Pe$0|PGu0|P$;1A`y~ z1A{OF1A{071A_zu1A{aJ1A`m`1A`*Ss|*Yb>I@7FS_}*fx(o~q1`G@g#-Jc!U|_Ih zU|_IeU|_IkU|?`!U|?`%U|{fIU|{fOU|{eAIf8+KA%uZ}A)JAMA&P;4Ar=(W3=9m( z3=9lu3=9mJ3=9l83=9nU3=9lK3=9mV3=9kv3=9m_3=9l)3=9mQ=xkwNU}$GxVCZ6C zVCZFFV3@$bz%ZGCfngd01H()P28KBd3=H!b7#J2YFfc4-U|?9mz`(GYfq`Ki0|UcG z1_p*L3=9n085kILF)%RfWnf@9z`($8n1O-e7y|>tNd^XnGYkw2=NT9nE-^4LTxDQj zxWT}{aGQaF;T{76!$SrJh9?XR49^)D7+x_jFuY}8VEDkm!0?%Yf#Dki1H(@S28KTj z3=IDn7#Nut7#LX@7#KMi7#O)37#R5&7#IZ^7#Kwu7#PJF7#O7(7#L+47#I~87#Niq z7#PzS7#OqD4blzMjna+NP0~%%&C<=&Ez&L14Gq!_4bu&c(hZH%4NcMwP16m{(hbei z4K30QEz^w*(v1w$jf~QbjMI%w(v3{hjm*-G%+rl5(v2+BjSbR`4bzQ{(v6MNjZM;x zP1B9d(v8j2jV;oREz?a5(oGE0O^nh_jMGg_(oIa$P0Z3w%+pOQ(oHPWO%2jb4bx4H z(oK!iO-<5GP18-y(oN0NO)b(*Ez`{m(#;Ih&5Y8`jML3b(#=fM&CJrx%+t**(# z%?;Ac4b#ny(#?(2%}vtHP1DWI(#_4&%`MW+Ez>Ow(k%?rEsWAFjMFVl(k)EWEzHs_ z%+oC_(k(30Ee+Bw4bv@+(k+eCEltubP17yS(k;!?EiKY5EuHfVDvL7HGfEURk~I~K z3=E7E98>a>QWb(Li%U{-ixoWclJkoS@{1BnQd9I49CLCMfVD2b!$I7#J9$ zp~*Rwfq@|lnv6>r7#J#{$+sC)@<5aABnAeC>Coi5kb!|=88n%0U|?X_3QeB-85kIj zK$GP;1_p-9(BycRfq~%>G#S2OU|{$NO@6;2r3X0KaWOD3@lqjrw=gg;?qpzK+{eJcc$k5K@dN_{<5>m<#!CzgjMo_$81FDJFg|2pV0^~F!1$Vh zf$;+a1LIc%E(0z;1ujJpp#&n7L4*p3Pz4ccAVM8PXn+V!EoR6r7{AVLj9sDlU%5TVJ% zrwY=c3euqp(xD2{p$gKW%B2dDRRa;~AVLE~XmatXfpn;Wbf|%JsDX5-fpn;Wbf|Hu zft09&2n`US$;GD*(xDF0p$^ia4$`3x(xDF0p$^ia&ZQ1grvV}~x%f0dIy68!G(b8u zKsq!)Iy68!G(b8uKsq$IG(d_qx%f0eIy6B#G(kEvK{_-+Iy6B#G(kEvK{_-+IyAX7 zx%i5f{8U}?yM4)bml=na{GPUi?fd#Ae-1CXyu@n>JGvYPLXLAa*8}d=JUkC3@bJoT z9ubk@JR%CN?}QnIV0jx>}DLjG^7R)@yl0tW;c* zj_-VtqHo+*{qCw!sMATb{>{85e0J`k>YdqdyS}e}b$-T_xt+86dwP0%dV6Me&gh)h z%<;YU&u>xg@6JCpMSFf{PyC(T^E3e{zGpYAOYCk*)PhCe{M{QHwZ-LsxW{%Fzo-XN5_S~8Y z$?MvWx1DU?m2l7DH^Xl!%W{V*wP%U7rOhpsQ_FfgdpmnNr#4ROnA*$H+uPM6J-cOA z%dD0)wQFkEwS20)^K$piefPZI+O2Qz>F90moil0nlv&enoXGxWv)N$RZ}ujGWR8O3 zxoPVsas1Hvb3>H-`;(vSqE5QD`mS2paVaUC!M)WJT4uIfPx&r3`{UHk_|EvQ1V{+> z?C9OmyMtrm#O|I6@>?di9!%`u=br1fUq^3eZ|lV7{-!RD*4B>p zcKOaO5a9S-^+!i^%FKx~r_7kWc*DLq3wNL1dvfo|6-qt7nc9Cdnl$|uuZx_|@k8!+ zz9{#WpJJjtojqM$QZ2nL6WZivxw$Bvh`SW|J?r@H-yBoQ`-(eC>wb$wWc=o7P3`h+ zD_onmFe`s{)~1MS-N$$`VP+dA)Z!nVX6zUO}%7uhc@-?sg5>)8)8 zKem19f<)tQ#-86y6Mi#Padh=`clSsYt%)m~o67M+`>&}e_YdWt8lvgFX;U+rf9qri z|CUYrEtTJfc8Ar>!U^fM#f|AT`IUu*^FEyVF7};m zF30K#tD09st>XAS`}ZHw{YSoQUfTMbX^pwv@6g|@YcyBped>QcrDbN<%qbi{RDbUm z<$m|$o9L;TCu>f3ax7fb)4Nca#k8xWv2aT1 zl(NYsGs-76Pi~pgmROThSQeCHvE{eilguSei>sIQJne05ySS6%_@=h+tj$L|7uPLp zTG%|la#qdkmPt+hO?`b6`X=;E+;+e0QODEP`Bjr^I~u1nOs?zUsQ<$t+S}RN)z>w> zeMZ-m=B1T0QYYpwR!PwP%`;nhmeY5I+xJ$jS=hOC(!BP0eRC#pto|M$+8f;zot2iG zX?D7KOT*zS-(@bAOzxQ6I;picr$47Rm*aczp9!MeKlp$0i$>0lUJ^d>x9y_r-;?j? zoVBm+f78h^aSEtRS+HQ*(CW zhuZhgJlH=sbbjcPuz6{F=hbcIu=wtk`aLlFdpO6Wp2aDEpE8RohK)L*X})haKVBlb7w6%{z?0V_PRwg7SCR`@cf~oZ{hnY=M_#a znI5ppAtSVsqp+#7cv@{|XJ=XPnhVejnj>gkdzuAZ2&s_oje-P86i zubfypp{h5rKe08Tt8Hi3&Z#>mESxfL(gKc2T@yPd$nBfEeAeMwj^Ad#TSfaTCRFxU zaP+Ws%N;g(6-I29~+0CdWA)buq?Yq)R!J?iwK z2QR*>Ec(vp_uZs%S<$@8g}&E*OV^k*a&)z}cDG6=b#{bqZRhwtD_itCt9Huod<(U# zB%i#HCXPP#mhY}n-<`L7cjB19esbc8uKn>G-|hdL5#`?hFwbh zt<}GsCzZ-g%iPkwvWw&Mn++2`K}&9hR>hd#jI*X3={nMPq<2;C>fTj*d-qM)3$5QG zyQ116I{R*R+^oLI@nXSuj_&7`^U7CG?Ool~*WK6E*EXwlQgy%L;@^55in$zBt*!M9 zQe7S09X%Z!KSKWe66HSklSy;}TVGCPW_xaXc29j*B8M(}b3;$<H6CGt@GRUSvRY1&RI3LeoK0%bG6e9m&UGbJ==P>arADTx20#(cc%8QE#H|ILlpdG zEcwka^*2+0^}?dI#_IIe?x@Y}96wzDGKq5k=;#*Z=4Jp54S+@;7#J8Bm>5JD0vG}q z7#RW?0vQ+?f*67r7#V^Yf*BYYLKs3A7#TttLK&DC!WhCBm>9wt!WoztA{ZhVm>41% zA{m$%q8OqWm>HrOq8XSOVi;l=m>FUjVi}kj;uzu>m>J?3;u%;N5*QK~SQru+5*b(+ zk{FU0SQwHSk{MXQ?zqQrfq{#Gm0=!39YY*L1OpFvAc2vAgMo>`jzNO~)b;}Pr9fR0 z76xVpW(Fn((2xU&4{|*SGc$-Vh%hiRh%$&WFfoWRh%qoTFfuTL#UOsTvJ8wR zsl_D>9AMK}7#MRBOY#^vz&>F3{~v5K0|OK15ylA&tPDJiUl_kI{9-Ty(+rGXKr99Z zCN4H+HdbyXCXl~D_JZ6G#*Qvdt_%(gjtl{upuXG*5c$BK@&Et-3=Et{IFB$mGB|=2 zg2D%+(}01QfeSQn!~ltF8L%uQ+(7c6!4gIW76t_d1_l-eCI&_ZCWd5?X$*`I609Di zl97R7tIb}4cz&C&42-OIz~U&x|8JZ}I6wn0pxA-PFoImgz`$^V^9Z93qYmd0sQ)3d zAd->w4oGG=V32nL7#Kh!a10D=41S>W$HK|L2=47OGcdAnGB7dlG3Y_XnHlsLY@low z20ex>D4Uf*gkc7h&Bma|u!bR>p@1Qup_n0_A(Aiq}de4kjhX5b^*v$h75WP1|-@C$-@l) M=YYm8LE(pr0j3#SBme*a literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_simple_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context2_simple_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_simple_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_simple_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context2_simple_f1.ttx.GPOS new file mode 100644 index 0000000..266f31b --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_simple_f1.ttx.GPOS @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_simple_f2.otf b/Tests/ttLib/tables/data/aots/gpos_context2_simple_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..c789b2ef28bb7666f7321e03107281e78f4198d6 GIT binary patch literal 5484 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_simple_f2.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context2_simple_f2.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_simple_f2.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_simple_f2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context2_simple_f2.ttx.GPOS new file mode 100644 index 0000000..8001658 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_simple_f2.ttx.GPOS @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_successive_f1.otf b/Tests/ttLib/tables/data/aots/gpos_context2_successive_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..b89bfb7b81f9bc7d48617e90b30439ac1bb03690 GIT binary patch literal 5544 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_successive_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context2_successive_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_successive_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context2_successive_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context2_successive_f1.ttx.GPOS new file mode 100644 index 0000000..2de586f --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context2_successive_f1.ttx.GPOS @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context3_boundary_f1.otf b/Tests/ttLib/tables/data/aots/gpos_context3_boundary_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..f8949d475c7c9c6bda290a13c1da5937f88b7ad0 GIT binary patch literal 5476 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 R4GV%su;zfn3>=Wi1OT- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context3_boundary_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context3_boundary_f1.ttx.GPOS new file mode 100644 index 0000000..fd85d19 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context3_boundary_f1.ttx.GPOS @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context3_boundary_f2.otf b/Tests/ttLib/tables/data/aots/gpos_context3_boundary_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..ceb7452a8e6045a7815137f4c2dd7f74e2c00b31 GIT binary patch literal 5472 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 Spdmrf=+zu>n1KTlnE(I-G+iM8 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context3_boundary_f2.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context3_boundary_f2.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context3_boundary_f2.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context3_boundary_f2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context3_boundary_f2.ttx.GPOS new file mode 100644 index 0000000..bee96ff --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context3_boundary_f2.ttx.GPOS @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f1.otf b/Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..40b55ee0ef58169ff9dfec7c5c6b43fedb81b675 GIT binary patch literal 5512 zcmeYd3Grv(W@unwW-xGeb5l?b>uO|RVA#XJAfV#z;_Ak(%C5t}An<^JfuX}az(4qn z>mz>#2Ehyl28J8{!TLryXF~TfFbFv?Ffb$}=Oz{~NHaz=FbFMRU|tXv3=FIc3=HfH3=EtM3=G^13=F&s3=I4X3=Dz{ z3=F~y3=Ev(q zz>vnkz>vwnz>vehz>v?tz)-}%z);G-z)-=!z);P=z);7)zyONQ76t}}b_NE9E(QjM zUIqq+2@DJjlNlHorZF%u%w%9-n8U!pFrR^eVG#oZ!%_wYh7}A9467L!7}hZ`Fl=OC zVA#UIz_6WxfngT|1H)bh28II+3=D@E7#NN*Ffg2CU|=}Iz`$^xfq~%?0|UcVP|{#v zV7Se|z;KU&f#D$o1H%&r28QPh3=FRr7#Q9%Fff3U!)FEthHnfE3_lqd82&IYF#Km= zU}R!oU}R-rVB}z6VB}_CVB}+9U=(CvU=(3sU=(LyV3cBDV3cKGU<4%xWd;VuGzJF7 z>~w>4!*ru`<8+gB({!_R^K^@J%XCA7bVI{*L!)#<<8(ulbVJj0L$h>4^K?UtbVJK@ zBZG7!!*nB~bR*+*Ba?I^({v-VbR+Y0Ba3t+%XDLdbYsJGW21Cq<8)(_bYs(WW3zN) z^K@g2bYshO6N7XU!*mm)bQ9xr6O(ik({vNFbQAM*6N_{c%XCwNbW_80Q=@cK<8)J# zbW_uGQ?qna^K?^-bW_W8GlO(9!*nyFbTi|0Gm~^P({wYlbTjjGGmCUH%XD*tbaTUW zbE9-~<8*VAbaT^mbF*}F^K^5IbaTse3xjkE!*mOybPMBj3zKvU({u~7bPMxz3yX9M z%XCYFbW6i@OQUp4<8(`tbW788OS5!K^K?s#bW2O;{DR7&%=C;B1&w4)1tS9kBL&Bl z{G?Qc;L75X)ZAhP&%EUPqJsRQ#FEq$Jq5>{9EBi|@?wRc)Z)~lveXnkNakW-Wb8;{ zNP%SsHfR1_lOsXmZzNU|`ULCUXl01_oPb@^)unU;yRzKn4beFa`#OXlQay zWnf^)f+ph<1_p*oX!30al|0a7JBfjTVLCLqE@WU}SO!g|8yFZEwnCHVeg+1HBhX}d zj)8&UGBi2fWnf@<1Wkr-7#J8nLX+QbNa+Dic3cb$jQr5#Cdt6SD96CS2ufxe3=E9A z3=E7$3=E9s3=E7m3=E8p3=E8J3=E9k3=E6`3=E8+3=E7>3=E9%3=E7Z3=E8!3=E8U z3=E9L3=E7F3=E953=E7-3=E9z3=E7t3=E7D85kI+F)%RBW?*1kz`($`l!1Y96$1m~ zdIko@Ees5dI~f=l_c1Uq9%f)*Ji)-gc$R^I@e%_A<8=lG#yboQj1L(Y7@sjPFurDB zVEn+q!1&dG%YchdflCoYD1iuN5TODhR6&Fqh)@R+8X!WGi%*eD0YoT*2qh4q3?fuO zger(o0}<*VLIXr-a`7pFbSQ!BP~uVoag{-Y3W!hz5o#bp9Ykn=2u&_NWsnYKkPc;# z4rMN7keCXHPz4ccAVM8PXn+V!EL5Y`L}+sH zse*K`@Vk3pTkQoFY#K!jxNW6kmFp<^?-Xd56^=MJiIcT zM?_>ekBEZnJ7ESPSl$Lz8><-@7{wSE7}qi|FgY+VFdb)LV76mmV4lptz!J{Dz{=0S zz}f_=s2Ld8?l3U0S28egurM%i)G{z|G%+x6G=Eq9sVnN3kpJ7N)uHjXz=Gce-!Mvwgz0_H8YXBERPv?U0vVe>1NMpPhTCdS~|AuJ5Z~ou4sfZs)B2o}S*G-kzDAGdia= zb9}G;^IMeryYo*?(VpMg6Mtv-{Lb!AJlNN}rR#f6>-Vg#@7WFO61!W{IKJQft0~I; zUHQiv(PIlwY&zX{b3&h1cC`I(&fg0{ntt1K=1j|-kTaoiLfh1~y){QC@9jC$bGGkH z&xT3eTQjC{D0cp4Xi@0zoZCFNch2nZir=Rny6~Oz@^?Ou?@GT7lEZ7G3nMC$YiCDn znauJ1=btsA+|Pg95S=k`#-y432WNlx>6v2HQ`b?~QQH*vTc9?vnWM9_r%Sq%J-22; z^1AlpZ717zCERoP&G1{wvfQCc?O9@NX>&{E)Uw{r-p-!Rsg2V*ruK65_ICA1&u*F3 zGOJ}x?V8$kEuSjyyxe_r-#zcQcI(@FI(plC=S-SCW!Cf?C$hiUY&O{So4v^(nWLb1 zZrb`u96xma+z{pd{^TdSsFSX(zN=PtTuMr3aBua5mYFTrQ@)GM{y4QWzB9fn0TO~e zJ9>BY?%vAbu2{FceB2NOFuIy<^LrTf22cYK$O`Yznr*U{VA+d8qizp0C(wY8(Y zUB0sm1UPmuiK{E+*d zFUq~;r%*vmgwJG9S_p#o46Rt1VbAe;Y`tIG+w$8hpuq|YOJ)2PJM`Q6yJF&ZgV^uV-zR#1=TEtr-C;Gea6)=*abtQ-eq~|dybq_oi+yLC z%dvXGs^-;Dt2lno{{2UE|B>&Sm$v?9T4QeaJM=f}8qJk?pZcFqX_?tIa|*`~)!+L? zx!?WxCVFb-$(qxh919oq^e&Vy&Zy&kh%tDuSRhuL3!QSR?se_DtdPghcJG3_d8ESyq0 zrEGG^jPi-ilUt^=CD!B=mIdWlZ22wsBy&mA;_78RPkUS2F7D(wzNzgyYxB|0#dQmt z7Bahslk0jo>i;l^_ICDm^>s~e zpV2j?d1>X0)QS0vRT6Z6^UPMB<@8CfrS<@g@_XM!mA5B{J0qLH(smxNFJZM!J@_vAY| zXYH%|-*j?JoB}FS7A%-Id-j~kUj1IJzU^5}p~>)Y0ME^1%gv1rD=s`>NwO_<$2qiOQ2Y2Ay;XLC54i}q&r zXH3ZGsA#WlZK)_NEiWsp?Wygq=?OcUd2H_e2@7V;nL2mDqy@7Vc5G?eSh2a5qpP#C ztE;oNucoQJjN^OgpWUL|-=lsCi1sx1H1#&O$9C0L`Nsb?xB9J7@!Jj(m6MLoIy(38 zq4xbV5BARuogca+Y+l;ld3BpPEWUfCeh8Kr#j}?!Jb$R@Tll`pd4-cp zrU$HY$Ox_EC~WF1o>tr0+1c6E!LjA{O3}N$>!)8%zm_`R$R@@;DWKD-b@H~p`JD$k zx3+9++0OA@8s&gb}H{6}1r`-k<9pQ3R~Q+9+;{2jf_^N;k#-vYaSYZt8i&ThidK4DVl zMCn@-F3j6}X7;ja3m45_vb=CjW_?{lLsLU-ZAD37S!-rvMsr3}+3dV&wF{lUbI<+0 z@yi>Id2?5FFP^-ybVKQe(v6i%cVsQ$xbo+xDEGM^Z$u|g>6s#(_gjA6Z`rxO6*xNC zCx&;;>~rDhU~lei>1pZfoLN7;yo;lwqr0m^zM;Lnqon~H0G)CjHGNIf8t&VFk2-zm z!He%Ii@x*ueK%=bRy412q3^Zd(lsWH99^xg-L29|ogJZD+d00^$`<|3s-5yX-$E@b z$tN$QiKCCb<-2RtcjqnNoj4}2pPYE2Ykxe)cl$qQM7j6>_$4~uY=Tw0ac6i(dV4rW zYxQsENu_erGPks^?Be+RX2Zl!(2`rBRWar_0Q;kx_8yy-hC7HLhHB4 zuBf(%&c2%+H>+=Qyjbv^qx*T~yz-=_o*3Ighb5_l*-;&K5XTV5z|0WO5YNEEkid|@z`~HokjTKoki?M0 zz`~Hskj%gWcE=HhZ46uttPCv-DGV+Qb__fW>m2_=VvYgAtf!VEh7NF)%Q3u`#o;ax*c3^nh%I!~_EagQJU+D}w`rBSQcusK0gs zL_V-*{Qv(y0|Vy~&La$t431#MObpCmn+zD38Mr`$Lky7klmW{^!U|+EXh4LKfrUYV zfq{XAfr){Ufr%j*WEuk_gaoSxsbpken0?~3Q9QrRR|ZDbJ794X;{P|!BOIW?6=nud zNP^@TL9Sw8U^u~fgi(i4hw}*7O&}QhEj$ChBSs8hD3&RhIocF21AA*hE#@hhElLt5t(L? nXLA@sDnk)NF+(QUwT29O3r*rSfAN literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f1.ttx.GPOS new file mode 100644 index 0000000..e9c854c --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f1.ttx.GPOS @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f2.otf b/Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..bcf4d17ecdeddb4d41d4a640342894c601ab5bf5 GIT binary patch literal 5504 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f2.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f2.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f2.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f2.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f2.ttx.GPOS new file mode 100644 index 0000000..e2b4a23 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context3_lookupflag_f2.ttx.GPOS @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context3_next_glyph_f1.otf b/Tests/ttLib/tables/data/aots/gpos_context3_next_glyph_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..6f9d9197ff2435063eafcddbcf863d0b0e1532f3 GIT binary patch literal 5496 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 WU_hq*prJz0sMZ|NcqKSoQ3wE+gkl^3 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context3_next_glyph_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context3_next_glyph_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context3_next_glyph_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context3_next_glyph_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context3_next_glyph_f1.ttx.GPOS new file mode 100644 index 0000000..44ce072 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context3_next_glyph_f1.ttx.GPOS @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context3_simple_f1.otf b/Tests/ttLib/tables/data/aots/gpos_context3_simple_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..470c2ed3e9100e53b3b0d3cb3eec965b1cfa6bc0 GIT binary patch literal 5480 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context3_simple_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context3_simple_f1.ttx.GPOS new file mode 100644 index 0000000..c559a7d --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context3_simple_f1.ttx.GPOS @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context3_successive_f1.otf b/Tests/ttLib/tables/data/aots/gpos_context3_successive_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..aeb9bbdfa3e299c746da8a1d57c989d16234ee09 GIT binary patch literal 5516 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 uki(G3P(-E~XIg#ZBbLSq{M literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gpos_context3_successive_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gpos_context3_successive_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context3_successive_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gpos_context3_successive_f1.ttx.GPOS b/Tests/ttLib/tables/data/aots/gpos_context3_successive_f1.ttx.GPOS new file mode 100644 index 0000000..8ff7eea --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gpos_context3_successive_f1.ttx.GPOS @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub1_1_lookupflag_f1.otf b/Tests/ttLib/tables/data/aots/gsub1_1_lookupflag_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..a539b95d6fd3b626be94d9c9778e82f04ad34c0f GIT binary patch literal 5208 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 + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub1_1_lookupflag_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub1_1_lookupflag_f1.ttx.GSUB new file mode 100644 index 0000000..ba7b68f --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub1_1_lookupflag_f1.ttx.GSUB @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub1_1_modulo_f1.otf b/Tests/ttLib/tables/data/aots/gsub1_1_modulo_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..7ba237920ce894666b1deb361e80066ba417a52d GIT binary patch literal 5216 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub1_1_simple_f1.otf b/Tests/ttLib/tables/data/aots/gsub1_1_simple_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..c21fcd3f0220cac15289dbc58e93936b5309a162 GIT binary patch literal 5136 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub1_1_simple_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub1_1_simple_f1.ttx.GSUB new file mode 100644 index 0000000..e76ba74 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub1_1_simple_f1.ttx.GSUB @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub1_2_lookupflag_f1.otf b/Tests/ttLib/tables/data/aots/gsub1_2_lookupflag_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..b13af6e7c64b0abb05fd277115259d25034b7e19 GIT binary patch literal 5212 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 + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub1_2_lookupflag_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub1_2_lookupflag_f1.ttx.GSUB new file mode 100644 index 0000000..51bfbb1 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub1_2_lookupflag_f1.ttx.GSUB @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub1_2_simple_f1.otf b/Tests/ttLib/tables/data/aots/gsub1_2_simple_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..d3851b3327bcb23196372a2031a88f4cd8bd5599 GIT binary patch literal 5140 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 E03jqfMF0Q* literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub1_2_simple_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub1_2_simple_f1.ttx.GSUB new file mode 100644 index 0000000..55649d2 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub1_2_simple_f1.ttx.GSUB @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub2_1_lookupflag_f1.otf b/Tests/ttLib/tables/data/aots/gsub2_1_lookupflag_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..8330ad4cffc86ce636d4dfcad8b32a6af665c0c9 GIT binary patch literal 5224 zcmeYd3Grv(W@unwW-xGeb5l?b>uO|RVA#XJAfV#z;_Akr#30PTAn<^JfkDhYIMnIE zyqB2_3<5tG7#PC*gY}Ja&V=q|U=R#oU|>i{&P^;}kYb5lhRXun`!5H?_7V3a7xFE05%pTUTMLBxZBfkAtXv3=FIc3=HfH3=EtM3=G^13=F&s3=I4X3=Dz{ z44|N55M^LskYHe7kY-?DkYiwAPy~6Efq_Atfq_Abfq_Anfq}t*fq}so6eJ7`43-QG z3^oi54E78R3{DIT46Y0e3?T1$GcYjtfgHiWz!1W~z!1*Bz!1g2zz_=xY6b>|WCjL? zGzJESOa=yq90mr4d|ItB&?P;|C1Ffg<;Ffep6FfjBo zFfdGDU|^Wcz`!t#fq`Kr0|Ubx1_p-t3=9m57#J9qGB7Z#U|?Wa&A`C0j)8$;BLf4& z76t}}?FzU|{&nz`*d0fq~&C0|Ub!1_p-z z3=E7+3=E8{3=E7M3=E9i3=E8X3=E8d3=E7S3=E9o3=E7?3=E923=E743=E9Q3=E8E z3=E9f=?3YB=|<_s=_cu>>1OHX=@#jh>4paBhKA{eM(Kve>4qlhhNkI;X6c6J>4p~R zhL-6@2I)qI=|)EBM#kwzCh10|=|*PhM&{{87U@Qo>Ba`>#)j#}M(M`J>Bc7M#-{1U zX6eS}>Bbi6#+K81whriSUJM(L); z>82*>rl#qpX6dHp>82Lxrk3eu2I*#o>1IahX2$7eCh2CT>1Jl>X6ET;7U^b|>E;IM z=7#C!M(O6p>EE;&c=9cLe2I&@t=@v%m7RKooCg~QY=@w?`7Ut;| z7U>q2>6QlRmWJt;M(LKu>6RwxmZs^JX6csZ>6RAhmX^->1(ija=@}&o8p)apMg|5( z3XUoHNvR6KmBl5gxy1^edCB=j1^GpZC8;TT3XVBB3PB*{#R@^G#i>PQsVRDp%*DXS z*pbAL0?Q6;&@3RpzyMD6QVa|X^3de2$-uy%2TkUn+-?g^-tG(x3_j3g9mc@G5DiVv zsSFGZS zXvDz4XwJaEXv4t3=*Ymp=*Gan=*_^u7{I{57|OuF7{$QA7|+1Kn8Luon90Dvn8(1t zSj@n{Si!)+Sj)h`*u=oV*v`Pf*u%iUIFW&YaT)^y<7@^7#sv%vj7u397*{bcFs^4{ zVBEsMz_^ovfpH%L1LI)^2F4Q%42)+P7#J@xFfd+cU|_t%z`*#Bfr0TE0|Vo01_s6t z3=E844Y&-r_!PJlL4*>BPzDhyAVL*HsDTJ|5TOAgG`aW`xfDQzB8X4|5y~J!1w^QV z2sIF)4k9!_geDiC5=e&<$POhgB@kB`M5urWRS=;DBGf^I28ht);!_6cPzLEx2I)}d zQU-~sfCyC(p#~z@p$5{S2GXI%r3O-> z4k9!_geDiCI!K2)NQXK|hdM}yI!K2)NQXK|hdP%!NSy|V(B$IN0O`;G>Cgb_&;aSs z0O`;G>Cgb_&;aSs;L-po*5u;T1nJNO>Cgn}&;;qw1nJNO>Cgn}&;;qw1nJP^(&XYR zTJlqM$?x_h-(6-LTJn3^61MN_m;5=r1_ow31_tKI3=Ayc3=FLN3=FJI zpo*G-f$a_h1A8R{0|yHO14k_b14k1B14r|B)t|bejtTj{ty&!#e+w-5UGQBao}*#I z=1H5RCr<9`@0Y)DeABAlPd(cwY-``v@+k6quJMlI8F@YVlj^&g+gn?zJF7eDnmNAn zMT)+0TlKrEMxjn8(fT*@n(*1VhpKmGzwP?I`qlXvQ|5Ng>hJ04?dk2A**T+gS~JJ@ z+CRTVxxYLA)D-Rcojvh)cF*tZ{=|cQy<57z=d^y$>iVADur9H?C5_|z&A*zW+~1Xd ztPwr7@WiImeK#lcX=O*-|K|L?Af)NHO=r%u+zB}o8Yi?(ZQEOObn@PwGd*Yf&h%`U z)V(!h8i!)%Z-y3y{?570b9?8^{;v3a`k@QoIWK?b^Z2gx+aNi-Ho7pPBDr>U#Foh% z-+%sDBg*~!#|_aL6K71C*?(~Mcb}dqRy}ncbse=$alZv>6Pr0YJA1mMJK1w73d)tz&90M{jRekM!)8SuL|# z*3_=4UDxud^3KcMH}~E1ervbBy{DtMy?4%}*;8gszi}e_o6Tl}UBB6z43aqtisz=S zpTzM)=g$pM?(a{2vWq(D+UmP%WyhtYbO!fUPiUFhay{j{*zAu}JL5a!yAmKF*t4T| zNAC`fi4(hfCdhA@+oxQCSoBNx(I9gjf+S}zj zyFh^Bchw&q(J3=0&YUu1_Tmlu<}BQOdhf}-Cs!!-{AOzZ&1ll}Tf8oEKF1Ha-}$24 zTYid(_H_1ibxF1KwoGV~o8{)BbRzCj`Deo)pD6RV~5|Qznr!}?9x2*;$(+u5};ly*J_df;|^FmaOmIJ#Fi}%L&^Ocle(FZCqr(w0zt4!>wmO%>3B) zsS6T~zZrXeGfnu-P{q;J)7{-8RkS9qaBeEc5ADCEqTD}}e`<)P_ohwFX#TB}9sFB1 z?YC6MZ?Qwaoxdw4em98yF8zI?_jmr3o7o*!GYcoA*A_RX*W_0g7S8){>bux?wz(Xu zC#-5-4Yi8n_w3()ME4*0u6b$eZ>BZocE3Y^v#!xxnfIyx`IMHKT{EX}{80V9UzGdZ zk8h%VSWa!)dsG%c=P*7LNtwe8|gj^mr!zOyzT?Oa^9uxVlQ z{K{E1vs)%L^*8nPP3W7@H*wqjvPT_HTjy6zuI*@?(lEKMhok-vgJ^GOZ&zQ}^!6ED zQ<|4n&Pbh@zgQ(f_czaMi^KwPs=G)=Bf)=k?8*#IgE&glKPcQ*>5ZZl>Ak z<}D3}uY8xeSTea|a_gkl-kkoN-dv9F!G9)*a{u7}$uAl?J9seyED|CMHjEcw&6+5eTRPCr<)v$K@ z>fX;2yT1pupXBIT-?qMOednU~#T|=g?5mnTZ{LL3?K7Gt&zjb~sC+huv$<$*W`D+n zjE;)->eiNu($ey>vf7^7?wX#kqnXF%-k-2w)|{zx7ff0(dtt|xwv82=YdN|)JG;6% zYx`=N+RHe;hyK|u%Kbg+r+{crb5B!mb9-!8ZIy5QZ*!~P8Wq3oAW=E#_^hLI4JcSXT^$@-ey-)2omwYv>zm(spmS@> zwwCQ2-!;DP6+KtE$u=S=%-_1+zMiA2yQ@pOr-i+{=r?($iOYdL$i5n%J1wZ9PJY(bxxGN zHQ~a%&1YsWo3?P#{3Xi^*JRe$H8eCe)Yev%1eUdCHfA(uG?mTHn^wEf`8)UA?;F3o z;g~mfRrlh_8%sBoZYbSYxpYU?5{@f>eu{FR`|(C}@|2z_(s{q-=lzzQ`&)sdlYL@% z*UUZ_jt=(b-j<%0zRsET)62U!Iy$<$I^-ML+dEnszyZ)H*HP2gG_B#j{r9NThaSB6 zuCnMmpWk8#(#X-(+S=VJoz&S8y0x9-`>bry@2uJ>zw<5BvXXrA zLYg@G*jv84MtyhQ^4*DJ0{h8{C%X2>b9}e|b4HYV|Bqjy^UWq$wHtSacciz6bF^0f zcAiu!H!X8Z`^qkk&u=zN`~)qz6-;v%`y{mgy?d{z+VK20Pi|mSO zi|FjT*>SV_CdZ2f-#NOUSI#S6J+*grS6_EuS6|z#)=Aa~ibtvX?RJFF&H%N7L zba(W0aQq1Q^GlTb+)pOa32c2im6`3i?b$u`U5OmJ?9B~5wUe8Ae=|-16}*g)YNhLI z>$lEt*Js_VzBy;r-1;r)ozB%xGh75bqP2ZW?zqWj5S`1O}o3Z3K z!_?nQ{nZPL+8V3VTf3t+w{!e({mUfE{iCB>l$)CYG&H~i8h&74U|?ckWDsHy0*isf zofu3QxEL52SQwbVavThd3}OsS3<3;X3``7S4B`w-3?dAo42%p+3_=XT42&hI#U%_J z&;bX=oWznm1`Y;Li;3a?f3T?x3{0Fy7$-2WGVm~dVf@1Ii@^v?GcbMuu^1SbxY(H4 zSh<;)K)OI~fI5wV!O_LZmBE3*ks*K+)PFevA|Kc@{{R1d>QicMCG=?08M22*Rc!o3vLxv!R zREBhhQm|MN3Hpe)A&eoFp@^ZFA(J7WA& + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub2_1_lookupflag_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub2_1_lookupflag_f1.ttx.GSUB new file mode 100644 index 0000000..2c59793 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub2_1_lookupflag_f1.ttx.GSUB @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub2_1_multiple_sequences_f1.otf b/Tests/ttLib/tables/data/aots/gsub2_1_multiple_sequences_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..c912937e6a808696cb82050e44806024d572a4b0 GIT binary patch literal 5248 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 kB8FmyOon`hJO)DsJq7~`+yNRw1C92~0gdB + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub2_1_simple_f1.otf b/Tests/ttLib/tables/data/aots/gsub2_1_simple_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..584a7f555392e7c35f29ddac046b6dc00f316d82 GIT binary patch literal 5144 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub3_1_lookupflag_f1.otf b/Tests/ttLib/tables/data/aots/gsub3_1_lookupflag_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..4ccf55f4a8cac6761cf8d552cb3a08fa17b582e9 GIT binary patch literal 5224 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 BLp%Tg literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub3_1_lookupflag_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub3_1_lookupflag_f1.ttx.GDEF new file mode 100644 index 0000000..971a3f1 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub3_1_lookupflag_f1.ttx.GDEF @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub3_1_lookupflag_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub3_1_lookupflag_f1.ttx.GSUB new file mode 100644 index 0000000..63c53b4 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub3_1_lookupflag_f1.ttx.GSUB @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub3_1_multiple_f1.otf b/Tests/ttLib/tables/data/aots/gsub3_1_multiple_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..075f1962d97f09bd9ef9a823d8073ea72a3ac2ef GIT binary patch literal 5168 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub3_1_simple_f1.otf b/Tests/ttLib/tables/data/aots/gsub3_1_simple_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..201f0f23752fd3123e5780b84477eaa959eee8b7 GIT binary patch literal 5144 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub4_1_lookupflag_f1.otf b/Tests/ttLib/tables/data/aots/gsub4_1_lookupflag_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..08ec01a7282705ad844fd60e6c32a62170abf8bb GIT binary patch literal 5220 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub4_1_lookupflag_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub4_1_lookupflag_f1.ttx.GDEF new file mode 100644 index 0000000..ffcc7a1 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub4_1_lookupflag_f1.ttx.GDEF @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub4_1_lookupflag_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub4_1_lookupflag_f1.ttx.GSUB new file mode 100644 index 0000000..0982ef6 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub4_1_lookupflag_f1.ttx.GSUB @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub4_1_multiple_ligatures_f1.otf b/Tests/ttLib/tables/data/aots/gsub4_1_multiple_ligatures_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..90da33160c6dacb45a6ca41de7721cc623a581ae GIT binary patch literal 5252 zcmeYd3Grv(VrXDsW>9c;b5l?b>uO|RU|7SzAfVzN9P0F7?aNFC27wC<3=C2J!TLry zXF~TfFbL`}Ffb$}=Oz{~NHaz=FbMWAFfasUq$Z|h-1IhJU=X~+z`$UZk&&7xTFdo` zfk8-wfq_9KBe$f&p~Pwi1A~wS0|UdU+{B6khLivX1_ogk1_lPUyu{p8*<_=W3=G0N z3=E7B1^LA#|K~FpF))avFfcGEFfy>PFff9YGJ=$+l;-AEBKTfE_!)lGGyZ5``q9Ar zLxSbEAP=(~7X!!t?rtc>!`|I3!+Au8BaKy-fnnVjUReeP=3;&jSuQBcz`zhF%fP_S zB*c&gvxAv|fq|8Qfq|WYfq|2Ofq|QWfq|ESfq|cafk6=DZUzPhQ3eJE2?hoRX$A%c zIR*v>MUYn+7#P$U7#Oq|7#MUJ7#Iu~7#NH}LBhbmV9CJ1V8g(`V9&t7;Kabd;L5U27Xt&sUIqq+ z0}KodhZz_cjxjJWoMd2NIK#leaGrsI;SvJ_!&L?bh8qkF47V8=816AJFg#>nV0gm7 z!0?=bf#DSc1H)Se28Is|3=E$c7#O}WFfjaNU|{&ez`*dIfq{{Ufq{{gfq{{Ofq{{m zfq{{afq_wwfq_wkfq_w+fq_wqfq_w$fq_whfq_w(fq^lNfq^kQ-5}jC-6-8S-6Y*K z-7MWa-6GvG-OwQ2&@kQ5DBaLF-Owc6&@|o9EZxvN-OwW4&@$b~Al=9?-N-22$T;1| zB;Cj~-N-E6$UNQ1BHhR`-Pj=A*f8DLDBajN-Pk1E*fibPEZx{V-Pj`C*fQP3Al<|; z-NY!}#5mo=B;CX`-NY>2#5~=^BHhF?-P9o6)G*!DDBaXJ-P9!A)HL1HEZx*R-P9u8 z)H2=7Al=L`-OMQ6%sAc5B;Cw3-OMcA%sk!9BHhd~-P|DE+%VnTDBavR-P|PI+%(5wfq}sXnykYZ7#O0V$vKsQfguZ;j7u087%HL3x0!)~ zp#z$1CowQEOot}dg$xV~%b>}00|NuYR%r6v&%nTN1ez?*F)%P(h9<|m3=9m9pvmwJ z0|UcHX!85bzyK;ez{!q_fq{`9n%pEA7#QWC$xMTRfl-%%fzgP8fzh0SfzgJ6fzgqH zfzgeDfzg|RfiZxAfiaYUfia4Kfia$efiZ=FfiaVTfiaJPfw7o@fw6*tfw7i>fw75! zfw7%|fw6~yfpH=O1LHIX2FBS842%mH7#No_FfguSU|?L&z`(eLfq`)+0|Vne1_s8% z3=E7X7#JAOGB7Y+Vqjpr&cMKUhk=3dAp-;BGX@66*9;7d9~c-IzZ!5EaPcW{DS`+k z5TOhrR6v9(h)@F&>L5Y`L}+sHDRL=*2t^Q~1R|6{gbIjI1rcf>LLEeCfCx=4J|&P2 zC6FCTTuLCWGKf$C5vm|U4MeDe2n`US$;GD((xD8}p$yWY%%uzxQvngGAVLj9sDlU% z5TVJ%rvlQU0@9%Z(xC#)wEfCx=4K6Q`|b&w8q zkPdZ_4t0XP5>OTN3zIJD&V zv?Xlc*Dv{Vc**4@UQ5`~oU6GWaIfa!c`$*8SBCS5hz#cuQE+`H%pe5I+n{P= zH3I{q7y|?2S_TFt2L=YF;|vVUb_@*6lNlIT!WkG?`572kn?Myc0|VO~1_t&@1_llm z1_q8=1_q8M1_qAi@2Wp_MI96Je_ORWH2xM?@VnrI*WWLH;rOOi zy`OrvPuSMJt>sbV_gv#0#WV7H@+Z}IHMh66RCiW))HQQ_=Zh45=d@;y@3nt^i*kQ={;4V2^E-Ru z@9du6+5L$J`+B!@ea~tAp4Ig|yJ1~ocS{<__nUt;MY+E#|5zh>Y~hJbr~7VB=+nxM zw*SrfdqGIkZ=24XX}J?}CNxfHo7%Rw=IG?TJ!g8(_MPe3FsXZM#xxGa&fg3z3jLjP zo9FhzH?sw&gb!6>9;|0cx`lHL`8D#?1(LsIllk=vqqHr`Hvf-GbYZM zG_(KU?C(B3Q>=RGI_f%Vo8o>8)Fw7_bawW1Nq4g6)=WrV*M7Y1Wc#j!dk()DeoI-F zJ5;GXOROzzZmFDF*4x?J+0!|-aazaJUXI@0t{&;xEwfr?wXCUKQ@gI^Q{|nPyKnBh z=l#}heS1$wZ+q{YNwcTSnttO%_BWf&2D^Tz6co=*TR(~8ht8iHqTJt~{A3q( z(zVrh)yj@bN$Cvkt)9>_v*miqcd^+Yr*_77#&;z^La=8??~dLb91|yY_e_xAGP(6& zVh2ZOM^~qG|99z*?~+m9g**E?dOLetCpPytb#b(|cC@$4cXojQ$M32?I-*l%PMkSq z#_Yu#_RU$i`}E$Edrz)V>iNyo{+rRH>9=@YFSbd>1~ewg{O?Nb*d8h;fyc%j1$M4y{|A_8C z@?G=N*56EP%QX?{eA0C3sK|gO6o19T_uf$Q%a|lO)i;HKCyXn%apdnnw-M2 zpd5=WzvZ4}E@@g^y{zYHZ)@AdogBwEwS8x8KH9msZei2H=J}PgYG$`gYU*$5>zmLw zp>N{0`(=+hp0>`fnq1q_IHh57T@Oe79|qCh&fc!RuIcSFx~4QQt(=iMF@Ld2g6?mg z*~+t=zAN0mw`$G8&aIQ?wa@FDGl^sM_XyG6=%(nbwA@Uy)6H8N4qy2$bFpM{$K=*Y zt-U$@IlZ|Y--G{55as^C|C3)da(48R@QJ@|7iIsRd`IW3eRcnvPL7FFKxN8;1@mUl zo-^62->cQPJ*z1+xz@A9c30^9-PzJKPy{<)#^LzjfjOWQlI zZZn6)cdyj%f!W`~IVSZ??wKstyQE@zRYz5CWo=&($C}>(qBGoQd(87pY)fuT>{)VY z#ifN;IKD6Z6C%oe;KxVNir}X54*w2=WrlqQeQq5S>pVC*dfIzB<=U#JRxIv3Iq|!8 z@8N?B7A%=NYsv9X+Ap-%Et;`-_OgZN4;6h2-&Z-WaB|7?fK?6|p_LqkO`XNlYCAhS zJG(kKw)|cxdbfA|^vmhjQs*1l#JDE~bUL+8-qtt2^FZg;mTfKDIlgOr-z$2qa+7UD zP?*1Uy?s4LS9e#JbWaO=XLnammt1l6#EeyK*QV{Bws(2u#L5X(y@~yatqEOiJG*vH z-8o_5lzEdDa7^l&*fBwF-`wT14%c%0Hv8Qw+FvoDvcH0(hpl6N$Gpz@96yZzh>LRn zu>SE=G;V3ij_`@UqnCO9k>2=QVApT$f|cLdO*q;oOzNB{eQUynd7ID7UN&vvqWMdf z7p}>yuWM*%YN)NPCKKJ8|=;SFqQ>62L%g_5QJNLH&M<@Hl@UEGCE*u@~&Alx>Eq$Fc z>!+7@addQacXh}&w6}M(G=Kx3Q?8??uW4Gtef#fGrw=`N@m*!ncRs)GCXLIA=2b5A zz4lwW#-x#>tF^VeRXVA&BXny!$M;#;qTgAyQ-0@LsAVPjmca8e)yyd$S z#{~A16Hj#QkLUPq|L2S-_x>NhMCY4LuxdB%4DU#959esD{_Q-eRBl@4miCoh9G~B8 znD_}=ax1hd#{6cSHRVXxk-j6nt9n=WuG-tXZ^B+^{TA63)fUm&ceCSW^-Yc!3%+x7 zKd+ouzItl!>aM=-zOKHuS*??*`xO`e*6UEr<)~_Ht#6R(>gew1>EQSg^5>T*_qm@; zq7&Hqaw;?1bKA3f>bnv-blIC5dTJ*(_5Nm@04jJHA=OIP*Vb>H->%QPS$%WPs=4)B z(mS22oo2W+c5Um~*1L_Pck8?@J)6EWwSR5-&a@b!;5TE*Z-%MAnfj|27PU22r?+-T zZEolI;rf?Jl>0|Vw&oX z{}~uKk8mDgaAa@Z@WL&u{aUfsqw7E(4ZA5dXh%9^n8DEHE=LK}8usu3}(d zIKg>@QHN27^9a~YP)RVw$a)7PGaN9;KLHF3pb;hp1~vviPzcObmPs zR!}xGgB61Zl+D6m#n1$0voeS0FFk~{MGbA#UFqASBF{CmSGsH8bF&HugF{Co2gH + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub4_1_multiple_ligatures_f2.otf b/Tests/ttLib/tables/data/aots/gsub4_1_multiple_ligatures_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..4383ba93822e5fc5c9fae06ac3cdae7d5154aa93 GIT binary patch literal 5252 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub4_1_multiple_ligatures_f2.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub4_1_multiple_ligatures_f2.ttx.GSUB new file mode 100644 index 0000000..bc25e84 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub4_1_multiple_ligatures_f2.ttx.GSUB @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub4_1_multiple_ligsets_f1.otf b/Tests/ttLib/tables/data/aots/gsub4_1_multiple_ligsets_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..cea1b1aee12c30626da984e5aa59aa3d8f37292b GIT binary patch literal 5240 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 VGVKQqoq + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub4_1_simple_f1.otf b/Tests/ttLib/tables/data/aots/gsub4_1_simple_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..50c713a85280369f470f5c83b99c110fdb23986f GIT binary patch literal 5148 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub7_font1.otf b/Tests/ttLib/tables/data/aots/gsub7_font1.otf new file mode 100644 index 0000000000000000000000000000000000000000..b920398a52f8d10dac4fce9b3463276d2978a4e6 GIT binary patch literal 5096 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub7_font2.otf b/Tests/ttLib/tables/data/aots/gsub7_font2.otf new file mode 100644 index 0000000000000000000000000000000000000000..c98bafb51d4992a07f16935687896b85e148c0f7 GIT binary patch literal 5116 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub7_font2.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub7_font2.ttx.GSUB new file mode 100644 index 0000000..98338ed --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub7_font2.ttx.GSUB @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f1.otf b/Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..444d93174a1bd745633bf6f73c97244e0cbbd94f GIT binary patch literal 5516 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f1.ttx.GSUB new file mode 100644 index 0000000..49ed83d --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f1.ttx.GSUB @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f2.otf b/Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..2268647d75fdaa8064c65ca49509b61a658a826c GIT binary patch literal 5520 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f2.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f2.ttx.GSUB new file mode 100644 index 0000000..7790bf9 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f2.ttx.GSUB @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f3.otf b/Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f3.otf new file mode 100644 index 0000000000000000000000000000000000000000..a5929474902ea64b7e6cb1ca6ef3f7736cb3828a GIT binary patch literal 5520 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f3.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f3.ttx.GSUB new file mode 100644 index 0000000..c58e3d5 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f3.ttx.GSUB @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f4.otf b/Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f4.otf new file mode 100644 index 0000000000000000000000000000000000000000..f3f6b8c28e52ee07dfdeb242abfed23ef19199ba GIT binary patch literal 5520 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f4.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f4.ttx.GSUB new file mode 100644 index 0000000..43a1181 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining1_boundary_f4.ttx.GSUB @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_lookupflag_f1.otf b/Tests/ttLib/tables/data/aots/gsub_chaining1_lookupflag_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..47e4c64dd9c68e31fd2fb456d8570d35163711f7 GIT binary patch literal 5544 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_lookupflag_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining1_lookupflag_f1.ttx.GSUB new file mode 100644 index 0000000..e3bdf7f --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining1_lookupflag_f1.ttx.GSUB @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_multiple_subrules_f1.otf b/Tests/ttLib/tables/data/aots/gsub_chaining1_multiple_subrules_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..741362c8ce3ea36282e5f944705f23e36354d361 GIT binary patch literal 5616 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 T0}WAvM$_hi#!12Pf + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_multiple_subrules_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining1_multiple_subrules_f1.ttx.GSUB new file mode 100644 index 0000000..a8ceb2b --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining1_multiple_subrules_f1.ttx.GSUB @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_multiple_subrules_f2.otf b/Tests/ttLib/tables/data/aots/gsub_chaining1_multiple_subrules_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..67801f210648822d3c44a7da04919e31d648f445 GIT binary patch literal 5616 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_multiple_subrules_f2.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining1_multiple_subrules_f2.ttx.GSUB new file mode 100644 index 0000000..f73b4ec --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining1_multiple_subrules_f2.ttx.GSUB @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_next_glyph_f1.otf b/Tests/ttLib/tables/data/aots/gsub_chaining1_next_glyph_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..655000aad2d2a85a9dddfde0f76fc0b9f860d93f GIT binary patch literal 5560 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_next_glyph_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining1_next_glyph_f1.ttx.GSUB new file mode 100644 index 0000000..3858160 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining1_next_glyph_f1.ttx.GSUB @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f1.otf b/Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..c7709656a61ff07cedf46c3c0260824f7b570917 GIT binary patch literal 5508 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f1.ttx.GSUB new file mode 100644 index 0000000..d7dbd6b --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f1.ttx.GSUB @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f2.otf b/Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..8999e3f7c18ad8a60dd090428305cc88ede64965 GIT binary patch literal 5512 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 F0|2~lU>^Vg literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f2.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f2.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f2.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f2.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f2.ttx.GSUB new file mode 100644 index 0000000..e97a6b1 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining1_simple_f2.ttx.GSUB @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_successive_f1.otf b/Tests/ttLib/tables/data/aots/gsub_chaining1_successive_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..ad472ece265bb5e6be2dfcb575606a980e95f203 GIT binary patch literal 5544 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining1_successive_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining1_successive_f1.ttx.GSUB new file mode 100644 index 0000000..146dd2a --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining1_successive_f1.ttx.GSUB @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f1.otf b/Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..845c2560acd6b913c49591947273cdb07aadfdb6 GIT binary patch literal 5724 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f1.ttx.GSUB new file mode 100644 index 0000000..ecd0a6b --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f1.ttx.GSUB @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f2.otf b/Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..af0ad1f78ec1576d4905dc85fa597ac9dd9a8050 GIT binary patch literal 5728 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f2.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f2.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f2.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f2.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f2.ttx.GSUB new file mode 100644 index 0000000..076cb30 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f2.ttx.GSUB @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f3.otf b/Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f3.otf new file mode 100644 index 0000000000000000000000000000000000000000..28679c86763584d87ad82b63697ddd90edb16459 GIT binary patch literal 5728 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f3.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f3.ttx.GSUB new file mode 100644 index 0000000..c6fc1dc --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f3.ttx.GSUB @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f4.otf b/Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f4.otf new file mode 100644 index 0000000000000000000000000000000000000000..14746c6e834b6a923622a6b417cd3ebb87bc4476 GIT binary patch literal 5728 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f4.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f4.ttx.GSUB new file mode 100644 index 0000000..4221c6c --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining2_boundary_f4.ttx.GSUB @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_lookupflag_f1.otf b/Tests/ttLib/tables/data/aots/gsub_chaining2_lookupflag_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..2f4feedf17517ef5eaf38b38171d7e7b2a30399b GIT binary patch literal 5752 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 K*Mh?rg#ZA*b8Qj; literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_lookupflag_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_chaining2_lookupflag_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining2_lookupflag_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_lookupflag_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining2_lookupflag_f1.ttx.GSUB new file mode 100644 index 0000000..eaccba0 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining2_lookupflag_f1.ttx.GSUB @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f1.otf b/Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..6edeb6bb04acea328d1ca59401ad82cc2c55c0b5 GIT binary patch literal 5824 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f1.ttx.GSUB new file mode 100644 index 0000000..c0447c2 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f1.ttx.GSUB @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f2.otf b/Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..ae0298fd26aa08b015b268b43f76ba2b91c059e4 GIT binary patch literal 5824 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 aA%h-+0hQbb8gd1V4$c9M`-0;Ig#Z9|C42(_ literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f2.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f2.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f2.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f2.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f2.ttx.GSUB new file mode 100644 index 0000000..6067dfd --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining2_multiple_subrules_f2.ttx.GSUB @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_next_glyph_f1.otf b/Tests/ttLib/tables/data/aots/gsub_chaining2_next_glyph_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..878666f5c10c0c10096ecbed969cf53307d3660e GIT binary patch literal 5764 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 X7!1gE18CS3G{QFrG + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_next_glyph_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining2_next_glyph_f1.ttx.GSUB new file mode 100644 index 0000000..f3dcb64 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining2_next_glyph_f1.ttx.GSUB @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f1.otf b/Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..e8609309d582d0b0299beed0ae35e130a2106441 GIT binary patch literal 5716 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f1.ttx.GSUB new file mode 100644 index 0000000..7997e3a --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f1.ttx.GSUB @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f2.otf b/Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..4fdde3323656ac9ab7b7f13d6e3d5c7798979b93 GIT binary patch literal 5720 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 BXBYqg literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f2.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f2.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f2.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f2.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f2.ttx.GSUB new file mode 100644 index 0000000..bd3645a --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining2_simple_f2.ttx.GSUB @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_successive_f1.otf b/Tests/ttLib/tables/data/aots/gsub_chaining2_successive_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..90f9f731c468d396ef8d40137620f814e040c7d6 GIT binary patch literal 5752 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining2_successive_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining2_successive_f1.ttx.GSUB new file mode 100644 index 0000000..fd6ead4 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining2_successive_f1.ttx.GSUB @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f1.otf b/Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..1aea8bee2c011008e43a630a11e48940ddad1b60 GIT binary patch literal 5528 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 I3RhGN08=+&9RL6T literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f1.ttx.GSUB new file mode 100644 index 0000000..579d6ce --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f1.ttx.GSUB @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f2.otf b/Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..97c92c38ac0968123b41cd3f573a3d40271ef9a4 GIT binary patch literal 5532 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f2.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f2.ttx.GSUB new file mode 100644 index 0000000..5098ceb --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f2.ttx.GSUB @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f3.otf b/Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f3.otf new file mode 100644 index 0000000000000000000000000000000000000000..3b8513eb5c36c0421c03168e2c00f6158cd0acd7 GIT binary patch literal 5524 zcmeYd3Grv(W@unwW-xGeb5l?b>uO|RVA#XJAfV#z;_Ak(%C5t}An<^JfuX}aIMnHJ zi{&P^;}kY1_nitR~Z->)EO8Uv=|r|bQu^J3>X*~j6p%dz`$V1 zz`y_s4toX$1}6pv23H0K1`h@X25$xi20xG^7#J8p!5hxNz!1g2zz_=xY6b>|WCjL? zGzJESOa=yq90mr4d|ItB&?P;|C1Ffg<;Ffep6FfjBo zFfdGDU|^Wcz`!t#fq`Kr0|Ubx1_p-t3=9m57#J9qGB7Z#U|?Wa&A`C0j)8$;BLf2i zC_1+@Ffi<5U|`tGz`$^Tfq~&L0|Ub`1_p+c3=9ls7#JANGcYh*Vqjpn%D}*IgMoqJ zHUk60Jq8AbhYSo1PZ$^&o`aGM0|Ucb1_p)?3=9mP85kJ8F)%RvWME+U!@$7spMimq ziGhKUm4ShggMopOn}LCmkAZ)GTq!D-P|zU z+$i1LINjVN-P|Coi5kb!|=88n%0U|?X_3QeB-85kIjK$GP;1_p-9 z(BycRfq~%>G#S2OU|{$NO@6-_7(k^5IN5PAFfj5%lba+11EU-?nQ1UEFzPZeFd8v1 zFq$(kFxoIMFgh|YFuE}?FnTjEFa|I%ForTPFh(&jFvc@5Fs3jtFlI6^Fy=8ZFcvc~ zFjg=yFxE0KFg7tTFt#%=F!nGoFivD(V4TLlz&M+MfpGx?1LIN#2F6tk42ikAFT zUGlqq$#<6-hnD=FwuJ5b`Xzr3FS)$LYY98L90x*NZd z3a;;j8H8YY8&qwqW?*0xV_;xh%fP_oz`($CoPmMaj)8%BG6MrkI0FMKKLZ166R4tQ zU|_q$z`$O~z`()6z`#+v^^#@_-9eiwY#i05e7uzAuZ z>4}s3`upWC9N)C6_fyaI3ESGYwLFUao@>0Lct&1N{-pY@=JwW>>dxwpx@L~=e37DW z+*bYWs!^!ZNwogWye52h?xE_P*>AhPuYPrY#+138v-*2_dV6|%W_Hf#oYu_oz4p&< zQSR@~KQ%>rerHeoo!#?0yFc+@U+VjCv%0=#H>^wSZb{?#e)F%UDED{eA8SO9 zEj+R5bl=SheOlSk_P;rQF9>P+ZPS@EEq6lBgvJSNQ``2|9G$$k=SWW{WeGruZ=E@s7S7z9kFFH$M>Iq z)`)UH|8YZf#>5$uX7(SP{oSW$id9ctM_os4Q`~QX+Qep#&d#1L=}z|CnhDA4+K;!L zY~Ph|&*3-2Zz;=ihbpyaiM6H8EtONtdOLeNdpf5!PV1Q3%hB80)gwK-Wme0qmNm6& zYS*=Vs=V`Z_sxCxyx-cbZ|~{oZSS2kY4(&^({G%}{${h;VApTnCyi z(D`#il>7UWpX{Pey0-eRTG??aDV@Q+)e~A~wp>s7E;jq))XwCbu3;?BM9^=<1a2|1RC}T{7ysaA#jfZ)b1o#OD5{E{@jLj`nu>&Mpw( z_+9l!M|8@}i8H6nn7w$zzBvnbpWb_N@5vQPJ-?aSe>0ji{T8o_oX_z??svW@_m-bx zqCK5GU0qTwy)6^k7;{c!8q4>Lcued>Zl z<8Q{E-%JyJGgNVO^>lalNENM#E1a9k@k9HssVMgk<)0d&>Ah)FGn#+vWC#D2P5UjC z@muWBZ|CodiQf%kze|6g=>45PP>9xg;={5P4g@yAzocb>Iooz11>Itiw zS3|Ah_&xjgAJP3szH46E`kQHux!v#3->hpiSLS`{e?FyUX4lLq96wZl?-%8M_v4%B zshKBhPIq!FT-4LMP`)^;BDFNt?6;=fZ_SF|j{Rxd+NZCb_uca9ch!5}mDV0va^mp% z1&eD}W_NP5#zZwoOS|-X%yO-QCcYnLfAvMVzi<6%A!`kSEo@rYJil^Q z&Fq#*P5n)MeG~d7^iAA$zwA-R)7JS_lWRK~r!-8i>*1*X!yww*+1u6EHNAaC*Oca^ zl`~Q&<}X%B(EZIbTX~k#cZJ*cR;^jsxpmUK_IZ7CCULC(9wFKr-4vabmYZpIx_L{( z;Va)|E|yI0nA|$4wKu0fr#F}5d+?tLqTE0DfAWh)&W>IZKJmBhqU_(3@93PhukL@- z$uV&Xs7zU~VBYN6b0&NBd$szuXElW;*Ls%N?h2isJfk9VL&eUj9aTH3S2e7izPk7G z#P07w?I$_9*0-&1Ti>~;eR0R48T+c{&)YX)cKeK`$+M<)FDjqS;cPD2o7tZ+A)}+B zy}GrfqO`QUtgN=Dw!5Y$>}ck(x%Ve5m^EkW+y#>s%wE{BrEO!y=30)f&d#o`&f313 zruH(9@1cKoi*kRD`Y9mV)7;b4+uR=8Ra@m7|J&T^w?@TpJ4jScIzH>@+{1_3_s=}o zKR0xK=#sE`X?y3@ZRW7}?v?sIF#CHr$E2RgJ(J~nmsCuz>Zt0itnDk}So1qTbcXwE zk9nSnZOM&^JxeaFxU}#J$M=PQLPWU_{P-wZ5!_VX;oo7f%&^a(&#hx(od-upPkT?N zTwC?jip8BLCw|xNJ$!J%f+cfjEjj*4`-S$pMKc!9UbgW3p`vf$`zq%ZPA-`qu*xAL zw34H+sk3-mZD(g^XIBTumftHy@Aj^remVVG>U<-c826-rPN&w%+xq5r9_ZZKvaMx1 z$9IkIdqvMxZnBLC3iG$Fx3A~u>h9{2?rCA~?C$F6k}IyBn6awu+O*x%_AalSSUI7p zH?cplHKD6*XV=cDJ0~ohGH=oXj!9h;J0{5Oo4b70;aZO0X1`lS`zt0?_E&K9uyxGu znAbU<*2}k>cNu3j=Z%w!` zZ}XYi%cd<{G=ItR!Zn%obqx(o4YjouC4ptFnT;9E8BJxg^QP4(+ro!u5CTrdbe@(Zk@NKXVZ74_OC79nHED7{AMiq%`o*h zQ-AfsqPE8B^w#dE&FvgNT>mnOa{uV)7Ukw<05$G;7$AU&L4+ZIA%KCAA&?=EfsrAI zA&7yIA($bUfsrAEA%uaEA(SDMfr%lEA&h~EA)Fzcfr%l4A%cO4A(A1Ifr%lCA&P;S zA(|nYftewOA%=mOA(kPQftewWA&!BWA)Xc*544{DpMg|22CI%@65wKcNw}g>FoPm*n zkwFYhiZU=UfJ8YM7#Rc@Ai8WB>=^8!vMdZB84wl%%Q7;sFxWCM!F5AgcDUt1Aq6s( zk%5gtgMpEOi$R1zltGL^oI!#?l0k|=nn8v^mVvP(wYY?V1MCtO2F9Gkk~{_u29Vhd z|Nlc>&cu0yaRLJ?0}taD#xD%N7>vL)1LGGEi-CcOi;bC$m79qPqzhy#Bz71W7#v-k zTp1h~92o*QL4CLrAo77dgW~}t#|Uy20|Ube&LfOEj5?f0z-|J`z%V209gxg$z#uOLFfgQmXa+V0 zKTr&?a56A5m@qIv*-Q+43?@)EGlL0(6O_%uV8T!aWwSDfFsy>I*%(Y1_A#V06f=}E zBr(J@Br{|%Br;?& + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f3.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f3.ttx.GSUB new file mode 100644 index 0000000..3d6b966 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f3.ttx.GSUB @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f4.otf b/Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f4.otf new file mode 100644 index 0000000000000000000000000000000000000000..e81d00ed5e8138bdbd5018b309dda1e24b436af9 GIT binary patch literal 5524 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 wkOLMa#{_cpgfXNt6fqPtWHRJ4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f4.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f4.ttx.GSUB new file mode 100644 index 0000000..9cf6009 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining3_boundary_f4.ttx.GSUB @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining3_lookupflag_f1.otf b/Tests/ttLib/tables/data/aots/gsub_chaining3_lookupflag_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..47c1007c067134eac205dda5dcaff6a53107f35a GIT binary patch literal 5572 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining3_lookupflag_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining3_lookupflag_f1.ttx.GSUB new file mode 100644 index 0000000..9cc951e --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining3_lookupflag_f1.ttx.GSUB @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining3_next_glyph_f1.otf b/Tests/ttLib/tables/data/aots/gsub_chaining3_next_glyph_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..9160eda2b8cf38d5c7c8dde469d3ade6f9897d85 GIT binary patch literal 5548 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining3_next_glyph_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_chaining3_next_glyph_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining3_next_glyph_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining3_next_glyph_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining3_next_glyph_f1.ttx.GSUB new file mode 100644 index 0000000..00ff357 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining3_next_glyph_f1.ttx.GSUB @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f1.otf b/Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..5982eb512c190d6f631b823dc2b874d088e6e6e0 GIT binary patch literal 5520 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 HKNJE0`ruz6 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f1.ttx.GSUB new file mode 100644 index 0000000..6cbf5f9 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f1.ttx.GSUB @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f2.otf b/Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..359b12602f129701256659e10319a31777389269 GIT binary patch literal 5540 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f2.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f2.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f2.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f2.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f2.ttx.GSUB new file mode 100644 index 0000000..65fea5d --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining3_simple_f2.ttx.GSUB @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining3_successive_f1.otf b/Tests/ttLib/tables/data/aots/gsub_chaining3_successive_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..ae39d9207b3ee4df3dbf45669943ffedc602555b GIT binary patch literal 5568 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 R + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_chaining3_successive_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_chaining3_successive_f1.ttx.GSUB new file mode 100644 index 0000000..6e8918f --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_chaining3_successive_f1.ttx.GSUB @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_boundary_f1.otf b/Tests/ttLib/tables/data/aots/gsub_context1_boundary_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..0267cabfb4b68fcfd819f3251e4fdbb597edb824 GIT binary patch literal 5500 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_boundary_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_context1_boundary_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context1_boundary_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_boundary_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context1_boundary_f1.ttx.GSUB new file mode 100644 index 0000000..5ce2dbc --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context1_boundary_f1.ttx.GSUB @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_boundary_f2.otf b/Tests/ttLib/tables/data/aots/gsub_context1_boundary_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..24b1716fd0520fec32d76b1365386888816503ba GIT binary patch literal 5504 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_boundary_f2.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_context1_boundary_f2.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context1_boundary_f2.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_boundary_f2.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context1_boundary_f2.ttx.GSUB new file mode 100644 index 0000000..f4df5df --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context1_boundary_f2.ttx.GSUB @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_expansion_f1.otf b/Tests/ttLib/tables/data/aots/gsub_context1_expansion_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..c1dda8078f3ff78baa25276e72fdfdc61a824e70 GIT binary patch literal 5516 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_expansion_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_context1_expansion_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context1_expansion_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_expansion_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context1_expansion_f1.ttx.GSUB new file mode 100644 index 0000000..e03d5c3 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context1_expansion_f1.ttx.GSUB @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f1.otf b/Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..60676ab50fc6e50e2b25ffd496087d00a313b97c GIT binary patch literal 5532 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f1.ttx.GSUB new file mode 100644 index 0000000..7f7dbae --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f1.ttx.GSUB @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f2.otf b/Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..bcb56e4194a237626e2fa3f32242d5ec5d28efd5 GIT binary patch literal 5524 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 tD + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f2.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f2.ttx.GSUB new file mode 100644 index 0000000..921576c --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context1_lookupflag_f2.ttx.GSUB @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f1.otf b/Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..52cd86161be825d038ca9cb04050141442895860 GIT binary patch literal 5592 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 OM$P7c#zR5zf{FpS1aS@k literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f1.ttx.GSUB new file mode 100644 index 0000000..3bcb882 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f1.ttx.GSUB @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f2.otf b/Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..891356a083d5d1741e044aab938f001699021f0f GIT binary patch literal 5592 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f2.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f2.ttx.GSUB new file mode 100644 index 0000000..04e9696 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context1_multiple_subrules_f2.ttx.GSUB @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_next_glyph_f1.otf b/Tests/ttLib/tables/data/aots/gsub_context1_next_glyph_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..2786dedc6a5f45ad1dc7f2bb80b8a3fdb2884692 GIT binary patch literal 5520 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_next_glyph_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context1_next_glyph_f1.ttx.GSUB new file mode 100644 index 0000000..1276fb9 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context1_next_glyph_f1.ttx.GSUB @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_simple_f1.otf b/Tests/ttLib/tables/data/aots/gsub_context1_simple_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..ebdaf22a7db92c1e494a6f9fb40e0d4d80c42120 GIT binary patch literal 5500 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_simple_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_context1_simple_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context1_simple_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_simple_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context1_simple_f1.ttx.GSUB new file mode 100644 index 0000000..4e135b5 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context1_simple_f1.ttx.GSUB @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_simple_f2.otf b/Tests/ttLib/tables/data/aots/gsub_context1_simple_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..d4a3fbfceb6c478b3b1f5b04a42dc22a10a7cb80 GIT binary patch literal 5492 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_simple_f2.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context1_simple_f2.ttx.GSUB new file mode 100644 index 0000000..787c216 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context1_simple_f2.ttx.GSUB @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_successive_f1.otf b/Tests/ttLib/tables/data/aots/gsub_context1_successive_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..ce2dce5d41fce799c36efecbd2ca35182fcf4213 GIT binary patch literal 5528 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_successive_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_context1_successive_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context1_successive_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context1_successive_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context1_successive_f1.ttx.GSUB new file mode 100644 index 0000000..e0b1c54 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context1_successive_f1.ttx.GSUB @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_boundary_f1.otf b/Tests/ttLib/tables/data/aots/gsub_context2_boundary_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..6f46192dca5b360086977a17434648e5b274e7fe GIT binary patch literal 5516 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_boundary_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context2_boundary_f1.ttx.GSUB new file mode 100644 index 0000000..705389b --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context2_boundary_f1.ttx.GSUB @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_boundary_f2.otf b/Tests/ttLib/tables/data/aots/gsub_context2_boundary_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..ef19d87746b261cf4b3e1acd9d36e238447a1552 GIT binary patch literal 5516 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 A9smFU literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_boundary_f2.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_context2_boundary_f2.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context2_boundary_f2.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_boundary_f2.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context2_boundary_f2.ttx.GSUB new file mode 100644 index 0000000..3a35d50 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context2_boundary_f2.ttx.GSUB @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_classes_f1.otf b/Tests/ttLib/tables/data/aots/gsub_context2_classes_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..85077601a36eabecddeba49f22541463bdb6bfc9 GIT binary patch literal 5564 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_classes_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_context2_classes_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context2_classes_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_classes_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context2_classes_f1.ttx.GSUB new file mode 100644 index 0000000..6716203 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context2_classes_f1.ttx.GSUB @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_classes_f2.otf b/Tests/ttLib/tables/data/aots/gsub_context2_classes_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..ceb74b2cb4fccaf97996291ee054b6be9a9fcfcc GIT binary patch literal 5584 zcmeYd3Grv(W@unwW-xGeb5l?b>uO|RVA#XJAfV#z;_Ak(%C5t}An<^JfuX}aIMnHV z!IN(c41yU942%~3!TLryXF~TfFbFMRU|>i{&P^;}kYd?v7!3;Yi%b5`XE0)55Z%DQz@Wg$z{0}7$il$Dzz9;FQkt7v ziQs$v;Ai+z&-kN(=|=Wnf?c1z$A-14A7H0|O{JTNoG^+8G!ax)>N3dKnlP zCNMBCOlDwUn8v`sFq46SVGaWW!+Zt?hD8hv3`-dp7*;SaFsx=^U|7e%z_5{lfnf^+ z1H*O(28LY>3=Df27#I#PFfbfuU|=}Lz`$^lfq~%+0|Uc(1_p*p3=9lc85kIDFfcIO zW?*2r$H2hwkb!~W2?GPea|Q;6R}2gcZy6XEJ}@vad}d%^_{PA%@RNao;SU1?!+!<_ zMkWRZMpgy}Mh*rBMs5ZMMm`1xMnMJ!MiB-EMsWrPMkxjcMp*_1Mg;~2Mr8&D#xw>7 z#_V*1bi;I`bmMfBbklURbn|qJbjx%@gLFf~bVH+bL*sNqlXOGVbVIXrL-TY)i*!TF zbR&awBg1qfqjV$VbR&~=Bhz#vvvec#bR&y&Bg=GSgLGrVbYr7*W8-vVlXPR#bYru0 zWAk)li*#elbQ6Pg6T@^9qjVGFbQ6lbQ6no6U%f{gLG5FbW@{rQ{!|~ zlXO$lbW^i*Q}c9Fi*!@VbTfl=GsAQygLHGl zbaSJ0bK`V#lXP>_baS(GbMtg_i*$3#bPI!Y3&V5^qjU@7bPJPo3)6H9vvdpdbPJ1g z3(Is%gLF&7bW5XjOXGA)lXOedbW5{zOY?L~i*!p%=lp`oqRjM+5(SN9O$8$Z10w~; zl>DSrh2YBKlGNN{1<$sUoE(K9kn&=Mpw!~jqO#N!JxJzaU}Wq_ zVn~5y2R3LH5MW?n03~}V1_lOsXmZzNU|`ULCUXl01_oPb@^)unVDNz^>o5iehG=MV zPGw+V$bu&05(Wl_N@((J29-R}WIKt0fnho{xh`a2U|0rCrl8VbD>QlTXJBAB0!^0Z z7#J8XLzClO1_p*l&}8_Afq~&8H2M98lpf$@$Hlh6hMR`h)@C%${<1oM5uxY zH4vc=A~ZmRCKsO)NQV;04ka!n5LX#QsDKDn5TOPl)Io#>h|uKXQwHf!2I)`+=}_iU z28pSF2vrcF1|rl!ga(Mv@p$5{S2GXGh(xJws22!F9 zA~ZmRCKsPNNQXK|hdM}yI!K2)NQXK|hdM}yI+r>~od$@|Cgb_&;aSs0O`;G z>Cgb_&;aSs0O`=+(f}#eCgn}&;;qw1nJNO>Cgn}&;;qw1nJNO>CoiT6xm@Af6%U1l6w@_X76w(skg{5ibj@)EBl?C5eF2szHxTo1Tc^YA>Fz{4xUc|=5p z^N1+8z7u8;g5_;cwXvFkfl-WsfpIMZ1Cs*-1JiK^24*`32Ik2O3@qUc46OVN46IF{ zikg9e?G6J2dnE${2MYrOM=b*bM-u}BNAq{ppSq%s3HiURS{)jH3oQ6u@LeOGqhZ75 zNt>i6PVVdPm%ng))2iN2J=-U2Yv0!LDDr!*@s8pdc|G})>bsiTTU)9-t2^qNIll8n zioS7M^}DM^p-v~!`Zx2M@Y%VCs&{6;?fSm@)%h7y=625N@9F97>Ft@>Iiqu0GspMZ zKfgt}zdQfb6z%z)J@I#T&+qL1#DjgkTe`mIw0_U(`kvjeF0s2MjpO^xznY@l-<5x? z5k0o>#HQ1IHz)LIWk=ip=KQ@Nr0KU!XU??T2{{uQC$vp%+go#V^4^{^J!kvQ^lX^a zy)|PRhhpb%h8BhX&biHVd*{sluK0cWp$p$RFMsFr_^$NZAUV7?x-g<5xpsEMmdPC7 zfBsn`%KiMu4bd4BXH1&ee{lA9pPngJJ#`&*9koqyzXfU&n>ji=d%C1M*>h_qB(G~f z-gdHmSHeAq-weN{EXy6L)Se~QmNvIkPA%*0?CtF7oZ2|8V`?u)Z*NzR^z4>dEwftI z)UK&r*Yc_I&dc35_ucb;Yq!3=r=z#Mch02QQ)W%SaU%Ph&1QpLzuB7%k~s>B=ccWn z#PLJt&ka%T?@xZRi#qAr>bq)X$EBon2KQD^Xqnk^J>|RD?2l7B<2&QK5+EViv!i!M z?+%WM6T5pR$Zwh4dN8qrqqC!{Q@a1VbjNqesPDp^eI31>y{!|Q`sPvBJNV;_pIZ)e{)PJ?X{Mh!X z3lfdL8GC*+P58}F#nIK%-Q6Qqv?i`_ZYswQ?Z2j?+&`3mYKW%yrcKRg{;iW8{988d zw^YV&u|vO|zbhtwH;DZ%{e7bMcm9-{*&S9h3n!%47B{BXo!rZwhvze9hsuF+hX_o@H+l$M!YGpBI;Q2o7Ml>6O} zZ=$DWo~$|D$+2)zPwztc;;f3)(p0nGns&c6D}Fonr)_JWzINVs%d6j2?|oNVdt}Lp z!|N9;u3eek$nN%Pw0_05^YvHE+2Xm4~=bXHn!rrGJ{ zEe(gSe3!XcGPz@N>!jA+oc^5NT#oO-e|KR`0FB&;JdP(@i-?odge^0)nbJo7P z|4k>y#3`UMWx;}ZvuDql?A7np>f4^x6q;P?Sz@~@bbj)TipUKWJF9k7?WkVWuy*?D z-p>=ezX!FSi59x@8KMidM5Wwmg`+oF}F5_$Tcb+Upk0SUh{#!t;lUzJ>3roL4xxWO~3V zhm6olj>4wS;%T*=ot>Rs9UNPJuN1x8yMFrR^lPc}jcj7vlL9)OS|@Moo8Nh$b8E}C zmhBwhHNNi^Jy*HOHXc2C>8ymDgY zgsR@e{>0XVuC|?BJE!iPuyD$}Neeh8bxrJ;Ah&Ps@>z##Iewe{ZWZmXm{8eY!O_Fk zF~4J8=X{PI#(%^`xqn#y_$eBCCdxfWY*U;G&D8T)>f1RmbGR!W;ACsmCeqZR=d#oJNMl08^65a zm^XJ-_u|PLOE;8mDBW1ObVt?_jw^qDigKU(@kVs=l%6TldB5f7{g$2kTY;mKePVdm z%sv;64)*5WmY$Zr&YAVo%ey!_I=Z_$M1?_o&l{9=!Oj zvgkXX-*=P7WkvHU7y4fNEnQ>M$kEl>+TALh)Y%cbwVmVptZdQmtlBBR^DWe}l6>+) znmGE{TfVzSeRtmS-HBrY`^kwXy7tF&e7FB|MwEO1k6)tm%_dm28+V3xq_>B2v{wIi zo>VF~Eptoz$}W!2Z#GQ)1TDE0S`}k{GtQcFr0YoEk=|9kt9w`N?cFzFFSLG(?22lO z=fGfn^%yo``)rR!_! zx6W_ZXWgv6IcL?}`Yq|5&ecvcTpGK!^=#|i#?iZV-j<$C-FhU}lJBh-P4Bh+&9fU}lJAh-F}Ah+~LjU}lJCh-YA7NMJ}{U|~pPNMvAPNMcB0 zU|~pRNM>LGxr0%KQH+6$ftBGI!!d?s3>^$S;K2k&1`Y-$h71N122k5eiGhJZfPs;L z1+0gefr){UfeAds0TE$hU}9hfna04xz`-EGAi}`NAj%-hz{nuRAjZJRAi*HPz|0`U zAjQDMAk84nz{DWKAj81KAj=>NHj9Npgh7-+j6niwDhC52*gg&hW(HdZI|h3OMzD!2 z3?La07GeOo5ac3T1}3l_AR9q$g@|L92ZcLCo{d3+fsug=YOgqh1cM}l6oWK_41+8K zV@Yap2?Gbze~dYaC3y@S;Ba90{~zK@&LfNy7+4v27{4%nVfe*h1g04nzkpZ_3`|^X z%xtXOOiU2>KqDGNIl4HxGB_|eG6Zmf`hzDxJi_3};0RI&4^IOI zW(F<>1_luZNP3Y0%R;+{&;b`vC@3&6FtC8bl8GT1WEuk_gaoUH_?uz2@4^4^{5D@1 z7+LRt#Zid=-#CwOfCg_s@e7e*1i6ZVf#C$_5k?(G9nK?Q-$UdeBqQq`kj!wvAP)sF zFrN>ZWMBjjj4?AXvT!mmG4L^fVg$ryW-wr|gQ{U+Fkr}ovRN5K80JCQ zYzzhrn;6m=iWy27k{IF{k{R+D@)$}OQW+{3N*Ij5A~_6+48;t^450gZ!#!w-c302K~m A8~^|S literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_classes_f2.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_context2_classes_f2.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context2_classes_f2.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_classes_f2.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context2_classes_f2.ttx.GSUB new file mode 100644 index 0000000..deeea52 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context2_classes_f2.ttx.GSUB @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_expansion_f1.otf b/Tests/ttLib/tables/data/aots/gsub_context2_expansion_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..c12f0ac7a3eba9939ee62d1ab525ff80877b8a72 GIT binary patch literal 5544 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 F1OQP;V;cYf literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_expansion_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_context2_expansion_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context2_expansion_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_expansion_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context2_expansion_f1.ttx.GSUB new file mode 100644 index 0000000..3952840 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context2_expansion_f1.ttx.GSUB @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f1.otf b/Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..a0fada4f4d79b0488cc8a564bc2afa125b1394ae GIT binary patch literal 5560 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 ckO_9JA%h-+0h#tg@;$@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f1.ttx.GSUB new file mode 100644 index 0000000..80a6237 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f1.ttx.GSUB @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f2.otf b/Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..cd40a5d0a8ed78ddc360ce9ef4901f69db0bcbdb GIT binary patch literal 5552 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 PM#JWS#yP>^ib4PYzA9uI literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f2.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f2.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f2.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f2.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f2.ttx.GSUB new file mode 100644 index 0000000..715a1cd --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context2_lookupflag_f2.ttx.GSUB @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f1.otf b/Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..53be20d56d5a4a8f7571bd52e49dc3862145709f GIT binary patch literal 5620 zcmeYd3Grv(W@unwW-xGeb5l?b>uO|RVA#XJAfV#z;_Ak(%C5t}An<^JfuX}aIMnIw zpU2x67z8sI7#Ml{gY}Ja&V=q|U=S){U|>i{&P^;}kYfq}s+BO^6Yw3h1=1A}l30|SFfMs7)kLy6T41_t2<1_p*xxrr483@HH&3=ASZ z3=9lxd5O8HO3oI63=AS63=E771^LA#|K~FpF))aN%vWG!U}0fkWMN=nU<4^oDb3BT zMDV?S@H70VXZ+E?^rM0KhXl)SK^|r~E(VVO-Q7@%hrPR7hVzIFM;fav1H-y6ys``o z%*Fg5vRqJ>fq@}VmVtqtNr)j0W(P9^0|P4q0|Pq)0|O@m0|Pe$0|PGu0|P$;1A`y~ z1A{OF1A{071A_zu1A{aJ1A`m`1A`*Ss|*Yb>I@7FS_}*fx(o~q1`G@g#-Jc!U|_Ih zU|_IeU|_IkU|?`!U|?`%U|{fIU|{fOU|{eAIf8+KA%uZ}A)JAMA&P;4Ar=(W3=9m( z3=9lu3=9mJ3=9l83=9nU3=9lK3=9mV3=9kv3=9m_3=9l)3=9mQ=xkwNU}$GxVCZ6C zVCZFFV3@$bz%ZGCfngd01H()P28KBd3=H!b7#J2YFfc4-U|?9mz`(GYfq`Ki0|UcG z1_p*L3=9n085kILF)%RfWnf@9z`($8n1O-e7y|>tNd^XnGYkw2=NT9nE-^4LTxDQj zxWT}{aGQaF;T{76!$SrJh9?XR49^)D7+x_jFuY}8VEDkm!0?%Yf#Dki1H(@S28KTj z3=IDn7#Nut7#LX@7#KMi7#O)37#R5&7#IZ^7#Kwu7#PJF7#O7(7#L+47#I~87#Niq z7#PzS7#OqD4blzMjna+NP0~%%&C<=&Ez&L14Gq!_4bu&c(hZH%4NcMwP16m{(hbei z4K30QEz^w*(v1w$jf~QbjMI%w(v3{hjm*-G%+rl5(v2+BjSbR`4bzQ{(v6MNjZM;x zP1B9d(v8j2jV;oREz?a5(oGE0O^nh_jMGg_(oIa$P0Z3w%+pOQ(oHPWO%2jb4bx4H z(oK!iO-<5GP18-y(oN0NO)b(*Ez`{m(#;Ih&5Y8`jML3b(#=fM&CJrx%+t**(# z%?;Ac4b#ny(#?(2%}vtHP1DWI(#_4&%`MW+Ez>Ow(k%?rEsWAFjMFVl(k)EWEzHs_ z%+oC_(k(30Ee+Bw4bv@+(k+eCEltubP17yS(k;!?EiKY5EuHfVDvL7HGfEURk~I~K z3=E7E98>a>QWb(Li%U{-ixoWclJkoS@{1BnQd9I49CLCMfS?l)D2N7#PAB z7#O0V$vKsQfguZ;j6tPIB{caqgGwG~vYo`hz%U(}To*DhFf4;6(+vy^3|pbeb3X$E z!x3n*JjcMma2c8$?=mnjJc1^}Hw+96AEC+bH>C6cCp#_%21b5pa+73WV3dO#uNqy#!LnV z#ykcF#$pBr#tH@o###mj#wG>^#&!k<#vTR+#)%9JjMEqx7-usuFfL$VU|h<;z_^Nm zfpI+p1LGD32F9HX42=627#I&TFfg8AU|>AUz`%Hkfr0Tl0|Vn71_s853=E9V7#J8| zGcYiIU|?YUYQSZ{#izif2qKg~gffUw0THSoLJdTyg9r@}p~=Ok$fW=x6hVX%h)@O* zDj-4?M5uuXbr7KeA~d=9lt4O^Kz1l`DS^1kAVLL1sDcPJ5TOntG(dzV7oRdnhcZZq zGDwFqmoi991w^QV2sIF)4k9!_geDiC3P^_vNQVkYhYCoC3YQ8geDiC21thnNQVYU zhXzQ821thnNQVYUhXzQ82A2j%u_hOvCP;@SNQWj!hbBmeCP;@SNQWj!hbBmeCP;@S zmnIiq(UPC4OMbU6`R+2~(30QNmau(azvR#1C6||YEn!EO<3Px9uI75cy_$#T!2}*& z8O|djGMq<5!S$UmgAgolgQ|_y3=E873=E8G85o!x7#Nt2GcYjQF)%PsW?*0mXJBCE zXJBA$0#(!u3~YB87}zTr7&urM7&vMf7&w|37&w~0tNzp#bxg?rZPn_~_*-DX?}G0d z@f-~sHc#3lJ#lhhf4}^NC>R)0@VZ%=Q}%+48| z)0#QH*Z%n}%KhE>r>1Dn@9c@cvwMDL_a`3g>)q1zJ*V}1R@e9JhINVEEomIzZ~oO3 z<^HbxV~yysg(o(h?z=gmPb)jx{x|3E1tCqpZ8~$NN46e>1cw^moo}p4&TT_IJha(+^$v&UyJepT~El-v-Iywb6wU70I=; zBeqQD`2O?H8d2`&KW>Q5m^fq7%>IM3zx(t|vFfSosOzY0iu)~4o7l|J+1b-2-N~L? zGa-3h`|-Au?Yk20Is9h$EoE8mP^I=Pv9`3irE+RnZ)b03Pv_LeX&qC0IeL4$dZcH! z%xam{vZi)T?Yfpvm3Ln5zPaz7_glO5?L8g6?Y(m*&7LxA`i&FW-)uG;?E1~#WRT2J zP&_wn{UnYbI)846a({pFlU>wF*H+(ED?2VFr8Bs9;Oq|%=GeLgK1*siHM;g>zFmerW$S73Kb+{8K|Ty*F)YM)Pl- z?BL(BX}_g1ev2LY?fhLa@w-9ncj@mFy}$FP+|2H1HyxBDIXn{|!m%Dhkg&!@D^?3y`+_+ z{i59OetZ)>HS=W6=}wM?i+Xw&$`@x`so>n5gDxX_sD)S*}&k#P`GOuf8bv_pLuIM2)8_skfMRl{6Mk zDVy1#j5E6;NJu5kO_sx=Ecw@#YZKCf@iB#zbJBSd?no1(MQ zax=|NH*aY;eC4~$#gfS#lUpaX_U82G^yYGW5B@Vjl=}z&PkzzJ+0jeFC;qlwl>K}1 z9i6lG)%|ZeIVMg4l_?7r%$q%X&SbBCuU6motftW9TF(;OU7_=nXH-OPsMuMxqiRR> zs)n`GSNDFN*!?}I{Uk@%`nL6L>pK^C`%TTi^W7 z1D#u2wzX{M_^$DNujskTO|}t1VgAdmXGgh@-o3?w} z-sP1OD<@R-CiW+`CUmv!?AkeX=Y)k*=1p3_F{x`}#{{{3bC=ILT+8v>?02hZf5n8# z{tAvBwvPE7^E&5q{4o9_F3SDG`o~YvxTPsO!YBTYUgr5ndgE__UB9&pR(@wU;b@;Q zsdJ+AtqB+AZ9X%5*|dd=<}X=ZxF)l{uA!l+p|-Z7B(SVCvoWJNqp56m-n80<&fmG` ze&6`z4adB>tGX9Y-dMV!bVKRJ%B4H9mT+A8^HY@j+>bY+lc)4dk7>q%(5>wp-)CiuerMHA`JHc} zmX+j_7t+Mh$KLYYHR`+bmhVm+6WC8qJkhm3p5wdypEIJ|`+xiroo_b5s@=FVyd%9m zoTIh+xAUY@xoMeO+E;dQe15ZG;wNaytRsKtYH#np345XS zTVz*MTSRBy&5oPZH#uG`_|DP&ymDUo>Z!e}yZXBOy87B?wN9$;S6uvCuR}4HqpG#F zzCo(1qr0Q0gX2fYpI@Tf=YBGYPGIZHsmyH8ZO`th?@Hv*Wp8fish!-^`3I!t@F0@Z2HdB{aSi{)Ye#?-r60txt-&O>t7~O?jIf9qTJjJprHXC1_)qc5Mc;l2w-4j z2xJIkU}Okl2x4Gl2xbUoU}Okk2w`Ak2xSOmU}6Yk2xDMk2xkaqU}A`1h+tr1h-8Rl zU}A`3h+<%7h-QdpU}lJ6h+$x6h-HXnU}lJ8h+|-8h-ZjrU|~pLNMK-LNMuN4U|~pN zNMc}NNM=Z8U;(@12g54{E(TVHT@14rsu*Gzc))`Rj0_wMObkv8It-w;7pO1Az`(>H zz`(}9#J~u~3=GU*7HIeZB*)Ak!XU!H$RNrf%D}`R#vlfkXJimzU;+<@a4;}32rz)m zWoEEtuw$@iU<9jXVF1a1un<_5k%5K57Hk4c9@H7aEe{Gykg1FeYz!IrwW#_<0?I2@Qbk1$SPU}fN8 z{KEK!;TMAum}X%70%9>RFmbUlv$1kBF@eGZWG5u{7#J8FU7TDQ92guK0ysf^yb~bu zfj#5@|Nj{nIFE21VQ^${1S@6&hX`n(g_(g1G@!%)Ndq!qS&%Ow7&M3i3IzoQ1_l;z zSTivsgG^&!gpgqMAU7~FFm%Un?u_TR`O3h^dIv0yLj3>6d4vNrAj1rf1&|yg$W;ss z3@12`FzPVsa2^5s9wY<9jI4J+GQ$CbycEE|kOHC^*ckjkF~Gvfz{udj08YOwoD571 zd<-s7ab^Y=h9D@Lg~5ek8kEh-Ai{79%4TD5VR*-o&QQ!y%8+P7Efg;W{77#6j8-Q nYPc$lA(f$sp_m~P>@PzGJq81+xD8THF#Mkb8Y>0I2MPfI{#$bk literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f1.ttx.GSUB new file mode 100644 index 0000000..827db06 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f1.ttx.GSUB @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f2.otf b/Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..6bcc0cb301e918c7b55eb39d1ce2152b6a30efe3 GIT binary patch literal 5620 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f2.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f2.ttx.GSUB new file mode 100644 index 0000000..83d6af5 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context2_multiple_subrules_f2.ttx.GSUB @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_next_glyph_f1.otf b/Tests/ttLib/tables/data/aots/gsub_context2_next_glyph_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..4ca4e4897bc730766ff97c5e11a9e96408e28807 GIT binary patch literal 5536 zcmeYd3Grv(W@unwW-xGeb5l?b>uO|RVA#XJAfV#z;_Ak(%C5t}An<^JfuX}aIMnHJ z$D1Pz41yU93=Hr5gY}Ja&V=q|U=WI6U|>i{&P^;}kY7#J8m3i69f{?BJHVqg$$U|?WSU}RunVPIroU|?VbDNiZQ z&8P4WyIY3yhzv&>t1JVfq}uEfq}t^fq}u5fq}t;fq}uBfq}sfGB7aYFfcIWGcYg|F)%QcGB7YyFfcGwGcYjJF)%QIqO*m8fuWs&fuW0m zfuWayfnfpz1H)tn28L-23=A_F7#QX-Ffhz#U|?9pz`(GSfq`KK0|Ucq1_p+83=9k# z85kJ0FfcG|XJBC1#lXO@mw|!d00RTVVFm_{WU|>{c zU|>vRU|`HnH%K>3H%d27H%T{5H%m89w@9~4H#A5$G)y-%N;fo4H#A8%G)*@&OE)x6 zH?&AMv`jZLNH;P}H!?~$GEO%#NjEZ0H!@2%GEXGNwM;iNNH;S~H#15%Gfp=%NjEc1H#18&Gfy|ONH?=g zH#bN(H%vD-N;fx7H#bQ)H%&J;OE))9H@8SPw@kM%NVhOdw=hb#Fiy8HNw+Xfw=he$ zFi*FzNVl*|w=_t%G)%WNO1Cslw=_w&G)=cOOSd#nx3oyNv~vE1EG|jSEmrW%OU^GU$S+DPNlnpHaLmb32m&cDRtQQhPAw`+P0@p7 zE(S)%jwFT@Sax88W&r_cvX^3DV33Cz`$t4z`$tEz`$t3z`*Fpz`*Fnz`*Fuz`z*5z`z*Fz`z*Az`z*Kz`&Toz`&Tv zz`&Ttz`$6{z`$6+z`$6`z`)qVz`)qfz`)qUz`!_>fq`)v0|Vo11_s6j3=E7*85kH> zF)%Q$XJBC5!oa||lYxP89|Hs9VFm`q6ATQDXBik6FEKDMUT0unyu-l2_>h5t@fiaH z<7);6#t#e(j9(4747m6dxD-Kz5{OU+5h@@;6-20k2z3yl0U|WH_!PMmK!hTQPy!Lk zAVLL1sDcPJ5TOntG(dzV7oQSHhZ4vRB`zfpR~ba8fCyC(p#~z@p$5{S2GXGh(xC>@ zp~j^KQlbtbG(dzV7oR#vhdM}yI!K2)NQXK|hdM}yI!K2)mpVwD28ht);?n@>&;aSs z0O`;G>Cgb_&;aSs0O`;G>CoWP04dhw;?o4_&;;qw1nJNO>Cgn}&;;qw1nJNO>Cgn} z(B#tO;wxJ6Q+3Jj_9fq4W*l1bd)gAV@9UTRIlScZ60arf=yDtgInLEw54czJ@I08n z!z;siL_~)3h$y(e6J`*CFMq1?U~s* zqjOp_$M@PlzeTyfJO9)a?fIQO@ppF5@9h4>gMGbQy1wVMe$VRqp53r6vAZRW9TXS^s-kvi( zXZz0dY?#!&HDelwV&`v$7KQ%Kxy^HX=gj`D_ z?3P(Avs%{FuBlzu@~QI9%iTBk-Sd8Hx4ylnqqn_x&ZOB>W=+3wBKw=oW`kY7*_#ZK zISPvBrmdgE@k8g&4N>mzPkyqCI_cW#yJ}^}rKEHQ_f}76nb~qZ<-6GIk5fD2JL9_& zAR*YZqjyK|4vvWvyL%?cZ<*YBFtLN9v!kn1y8pX$$9Ku7@4}sZ9lf2strMI3o4Pn! zTRYm@()3%rE^%7Ye+Y)#9p8suJWWTh0+xEk) zXFtsR*!HOl5{tRK{TI*R~8n|`*7;J z*mt(M9IGd+YF-VsisSd}-+x5+ANj6%Y3pyMHRg7|Lw~cb(OjALssH(umYH2Mr*QmG z{k>n5``wRkqNiq_tU2Auv2amO??UlZApU76j<(Haxg94+nA>oLo<3Yz$SnEllk<^I0)r-i8TbS3o`)2@=n z!YQRw$|jf0D4*Coxn)XQVogqASx}C}mfvzuGM6+hu3pyjw70eG;!cj^o7%p!HXrR= zT(_`kVe|aTSv9j;CN=dp_4Q5Yo6t9L+x@af9Zy^5S52<%Xq?h8xvqz!{tts_Z)a~; zU)S{Z8C_GFmsZY5otVE^B|-N$&urycPTv)7-&?h2VdvIK^V;Y2&6&ip`g??EZ*)_1 zR$6YR+3Dsj4TrCMm$_Inxnpwcq}JY?{+!-ij_<*LCWvzX;Qz@l8aX?9N%+Lywu`cV zPrjpb*1o#`O()01DWEcC!Gd|SXV01J)$i5n+n&`Fnq2EyV!JDJe)5cp$PE=ct9DfF zs9x2ucKYhx&l9`92eqH%=vv>lzHNQyqV~ldi)QSrnm=#fgxT#gnkLVh*1f2FHixsh zXm4hJ#)OQHiuUT(mWtBS^0Knpp4#r3p0J~t$L8Ljuwd4lsdE=hS}=QI$CkE@6`N~0 zx;i_%x;kt7YMR>1IKGGe*)7WbJ?f``XisxbQ*U#7Y*%fSZ~Sj_tKS+GzwIDVIqCSU zqjL`*YTrNeVE^3E`Jqd~=B4eOSGSqN;=5Ps_rUD$;T)5CCihI1>s?YYy{eSd53?8!7{@>gFd&8 ziFFlV#eJbT%~^M{JQh3~7J zS2($3dcZ1&jL=Gs!lusRX|`6usNKe){F~YpL^%Y+~G#0y>>qCvWSU z-+7>OYs zP}yI>(Zkj;zhhqKe2yQ+f5b((e^~$cDH^vlWk>kL-_grF|448AEwJmicEQT;>?R!T z6DD;|l)g3L!o1CAW-ptzaMAoF%L~_J*4H&OG&R)LR+I#mwPrSEG-ouG&CZ)vyU_VN z_uTIrzr5j?H+NO{;>jCJHqy^`-c`M;dspr4-8W$` zw0?{1ifW7K?7P`J6%l>6LICeaCOeL0nx?YZsQJ@s9Q9J=hy4L!A!n|gmUP5>3W zjF4)j>uc+`&TrRe-K@SjXVu*LE$N-k)lM^98oRdjZ0p^|(YtltmYz-DncBa$d}mq= zQSh6wNHAq*i5j0~X+p$tq6VGLmmObp=+;S5X+5eyLwObn3> zkqk@>Q4CQG%nZ>C(G1KCF$^&b%nY#%u?)-%aSU+`%nb1i@eC{s2@DAgEDVVZi3}_Z zNeoF0EDXsE$qX!Dcidt)!@$MB$}oqah9QQ*j)4a}n83)u!NA0z!XU-K#K6G7!@$VE z!obA92-d*>W{EI}fccCJB49Nj9gGYD3?Q1B!Ir^}!5%Cx%D}?F0TyLs5Q5Sy47LnR z3}SG3P~cv_PgZGO#gdFfcN3F^DjTGKevVGe|Hy<>Ul@Kd7=dX9#xEci0|OHm8#5a# zHxm;`7sysf3^6b;IJ!8wGB_|eG6Zmf`f(>fJi_3};0RXC#J~); z%YcEIfeSQf!~ltB8L%wKpAZZhC;_=sfq?-UvP=xgAk!EaAtYEm$PJ7P3~OW8+s5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_next_glyph_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context2_next_glyph_f1.ttx.GSUB new file mode 100644 index 0000000..09e9dc9 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context2_next_glyph_f1.ttx.GSUB @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_simple_f1.otf b/Tests/ttLib/tables/data/aots/gsub_context2_simple_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..16aae9d2e6baa2573e77d33c7e3dfcb04edef352 GIT binary patch literal 5528 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_simple_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_context2_simple_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context2_simple_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_simple_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context2_simple_f1.ttx.GSUB new file mode 100644 index 0000000..6bc8a46 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context2_simple_f1.ttx.GSUB @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_simple_f2.otf b/Tests/ttLib/tables/data/aots/gsub_context2_simple_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..fc31262e6605eb836129a2ba9892fb790e225309 GIT binary patch literal 5504 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 FF#wn@T_FGf literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_simple_f2.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_context2_simple_f2.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context2_simple_f2.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_simple_f2.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context2_simple_f2.ttx.GSUB new file mode 100644 index 0000000..f65bf4d --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context2_simple_f2.ttx.GSUB @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_successive_f1.otf b/Tests/ttLib/tables/data/aots/gsub_context2_successive_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..cf1a89c1eefcb49593f95beb45c71f91366c1963 GIT binary patch literal 5568 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 aLk2wt12XLg4LO2F$L4^>J;C9MLI42p*=894 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_successive_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_context2_successive_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context2_successive_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context2_successive_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context2_successive_f1.ttx.GSUB new file mode 100644 index 0000000..3c7e0f9 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context2_successive_f1.ttx.GSUB @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context3_boundary_f1.otf b/Tests/ttLib/tables/data/aots/gsub_context3_boundary_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..01cd29dba670fa2950005975e4fe92c3406eef6c GIT binary patch literal 5500 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context3_boundary_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context3_boundary_f1.ttx.GSUB new file mode 100644 index 0000000..843e88d --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context3_boundary_f1.ttx.GSUB @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context3_boundary_f2.otf b/Tests/ttLib/tables/data/aots/gsub_context3_boundary_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..6fa5f05a38705e9c889a75c1412377662fa7b579 GIT binary patch literal 5496 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context3_boundary_f2.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context3_boundary_f2.ttx.GSUB new file mode 100644 index 0000000..cba8c31 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context3_boundary_f2.ttx.GSUB @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context3_lookupflag_f1.otf b/Tests/ttLib/tables/data/aots/gsub_context3_lookupflag_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..94371b4d1c86541c881214221bee3cce1cec7b9b GIT binary patch literal 5536 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context3_lookupflag_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context3_lookupflag_f1.ttx.GSUB new file mode 100644 index 0000000..d485a04 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context3_lookupflag_f1.ttx.GSUB @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context3_lookupflag_f2.otf b/Tests/ttLib/tables/data/aots/gsub_context3_lookupflag_f2.otf new file mode 100644 index 0000000000000000000000000000000000000000..d8150df42e16631efc4a27e079a2d9adbb195daa GIT binary patch literal 5528 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context3_lookupflag_f2.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context3_lookupflag_f2.ttx.GSUB new file mode 100644 index 0000000..016350f --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context3_lookupflag_f2.ttx.GSUB @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context3_next_glyph_f1.otf b/Tests/ttLib/tables/data/aots/gsub_context3_next_glyph_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..93533b81c657aaa9b89396dd1bc56882f13a18bb GIT binary patch literal 5520 zcmeYd3Grv(W@unwW-xGeb5l?b>uO|RVA#XJAfV#z;_Ak(%C5t}An<^JfuX}aIMnH} z>6;=32Ehyl28Mh7!TLryXF~TfFbH`tFfb$}=Oz{~NHaz=FbJ(+U|P4WyIY3yhzv&>t1JVfq}uEfq}t^fq}u5fq?-OoZbu!41OR-FfcHHf+w7Tfgy^4fgu(Y)C>#^$qWn( zX$%YunG6gJISdR8`3wvUMGOoKr3?%V6$}gv)eH;_bqov)py+I2U|?uxU|{HCU|{HF zU|^WQz`!t>fq`Kf0|UcM1_p*X3=9nO85kHAF)%PJWnf?cMc--$28MME3=A6?7#OxN zFfeRqU|`tAz`(GVfq~%w0|Uch1_p*>3=9k>85kJOFfcHjXJBBs#K6FCm4Sib1_J}b zZBPo`PdejzSPfd9gxJYH?~&S!#+NBy%w^GIk^} zq`7&cQ zz-Yw4z-Z3Ez-Yt3!05=p!05)n!064uz!<>5z!=KFz!=5Az!=ZKz?j0oz?jLvz?jFt zz*x+{z*xb+z*x(`z}UpVz}U{fz}UmUz&MeCfpHoG1LJH42F3*p42(+|7#LSEFfguX zU|`(Bz`(eZfq`)!0|Vn>1_s6x3=E8C85kHZF)%P*XJBBw!@$7!kb!~m83P03YX%0! z4-5>9Uk$hnxcC&f6hVX%h)@O*Dj-4?M5uuXbr7KeA~d=96uA^Ygd&Jg0ujm}LIp&q zf(SJbp$;N6K!he2pAtxi637lEE+r6G8APan2vrcF1|rl!ga(Mvh|uKXQvvBv0qIZy=}-abP~lPmNvMJdH4vc=A~ZmRCKsP7 zNQWv&hbl;iDoBSaNQWwyDo9ohM5u!Z4G^Kp#is_+p$5{S2GXGh(xC>@p$5{S#-#>Q zq7EW7K!he2pE^i~I!K2)NQXK|hdM}yI!K2)NQXL?I!K)ch|uKX(*Wtv0O`;G>Cgb_ z&;aSs0O`;G>Cgb_(BRSlDc0oT(*)_z1nJNO>Cgn}&;;qw1nJNO>Cgn}&;;qwzDjFyyWr{uO;m0avTUb&edEGxL5P=Jea`4E5mt2 zM27Q-D7d~8W)OnqZBVtbnt_2)jDdl1Edv9S0|NuoaRvrvI|c^k$qWoE;S3C{{0t1N zO`wXJfr0G~0|R>{0|N&O0|Q4b0|Q4B0|Q6%ch#S|qK*mqzpYvw8h;Bc_+9W_Bc7vS z!{$kwq$f`9>+hGpaD3CM-cLQ-Cv0oq*77Lwd#>@0;u(28`IG9qn%i4jsynMY>Y6#e z^F@ljaa;Aft45(tC(-&h^P2G4xreHEX20$FzWUYq8B^wV&g$>!>Fw$5nb|p`b6PXU z_u4B+yCbLy&$CNw@qixwA=|f6B;M9O>Ns-b9D0Fo-;jX`_A-i znAE*BV;YBI=Wm7J(Uefpsb-#IUT=kxfk^xGggyf(Tpq9VC=cEpy+ z9N&NbStH8*{KpN^853trn%RGF_IID2DONpo9d#YGO>w^kY7?6|Iy-y1q&wMjYbGSG zYd_w0vVB*=J%`^6zojh89jerxCDxWUw^U9o>+S6A?CG4^IIUx9FGp{0SC91UmRT*c zTGrIAsa@Cdsq)Ut-8c8$^L}f$zP+cTx4n1Hq}fwuO}}v>`DubMYGuczq;v-NR!?Y|*>XMQyV&fHQ#<23+x;R=}JKEdj zJG(%D<9F2`9nmQ>C(fKQWA@?=`{pd%eR}W7y(d>F_55aP|IKL9^jo|xaz4iox!?Js z+*^K%iS~5%bahF!^tMcBlbhw{qI4qeQsnon%BMO`hqu;tt=61hBf3vR9T$%T&|M`@bnO!rdaQsmHy#ix-*QhfmozP|Ue@!px3%r!PLAW7+P#hLH9S$Y~@)_-xY4(TeW6k=hjK{+UNDnnZ&XBdxU6jbW?OzT5hJ< z>ExmYr}V{+@H*4~`{oZeiH@4ZgEcPjgRGZ*zNWS8bJV{BLus-x?La?I2M(>G-Upa}OVC z-#_zU|J=~|p-aN%rR|+px0%D@yI1P>!0hkg9Fuw`_e_@ST~aZ9&V%(DgI-Oc4Z|j@id7yJ^ z%eI#79N#s*?-e~)xyd#nD9qow-oBortGla9x~GM`v%9OOORl(jV#cbrYtwd5+q=AS zV&#OY-o*aI)`YIMon1Sp?wqi2%DhPnI3{&X?3f_8Z|?G0hif^0oBeJT?XQ?n*(yv=83FPpY-(flRL3)f`U*EKXWHPqHtlmwQwW;SLtXEc?~&YM=d(D^&}-0vH| zyy2KPcUAY|$s0>Ilx`^9Sh;jZ))I~@e}0N`pZoDfbn=v*Dbjhr<>&pDo%>sXqmzAN zc-PE67mg10=H8Z`mcGuJ_0!9{I66AIyE^0>+S@x?8o&Y2Dc4cc*EFr+zWw*8(}y0s z_^z_(JD=Zolg4F5^C}nmUi&RwW75dc)!N$KDxK8X5xTXVlhi6^@D$8&tQ|8qu^d;gDLqVvrrShX8>hIgd5hjX-6 z|8|~KDmN{2OZ&<$j?Zs4O#B2bxfNO!V}3KvnsTJ;NZ*m(RlTcwSMBZHH(@Wdev9mi zYK!RXyV-HG`XZTkpI6Q+Up=*Vbyr_^Usqq-tky}@{fdi!>vbsRa#Xdp);CCX zb#!<1ba4C#`SVMZ``k|^(FtsQIhC31x$W6K^<9Y^y6nvjJ++gYdVe!c02RE9kZPst zYwNepZ`WtttiCyC)!h0m>7CBiPBUB@ySDXg>)poDyLH}{o=xAG+P}7ZXIczV@SCyZ zH^bE5O#Rghi`p8i(_6cvHn(&9aQ(|9%Kf9GTa=rd0W>tg!vFzH3?d8x3;_&`41o-R z42%px3_%Qx48aV+42%pR3?U4R451963``7R3}FmR4B-sn3``6W3=s@W43P|x3``7B z3{ecs4ABhH49pBM3^5GM46zKc49pC13~>z14Dk%{3@i)@3<(S@42cYh3@i*u3`q&6xK&CP>urX*b zFfwp4h%ksUh%tyWNH9n;NHIt=$S}wEC!FCxiFf(w028kFT@hStB1^E+# zK?5V8Z~%=Vv4F#si6I$e8UrJQ1gi(Rfsuh>9pi@Vcz&C&42-OIz~U&x|8JZ}I6#9e zp!k8vFoImgz`$^V^9Z93qYmd0u$v%q5R#Gg4oGG=V33Cb7#LDOGy@xhA1DS`I2jlj z%orG;Y$gUi1~Vv|nZb;~1iWy27k{IF{k{R+D z@)$}OQW+{3N*Ih8;u-Rw?0ANBh8%`Uh608RhIocF21AA*hE#@hhEj$chD3%UGR+{* m<}ij-h9ZVyhD@+)4H@(p49K(}G=vBm?V1A`#{`Ef3IPC)?_(SQ literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_context3_next_glyph_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_context3_next_glyph_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context3_next_glyph_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context3_next_glyph_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context3_next_glyph_f1.ttx.GSUB new file mode 100644 index 0000000..10fcba5 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context3_next_glyph_f1.ttx.GSUB @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context3_simple_f1.otf b/Tests/ttLib/tables/data/aots/gsub_context3_simple_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..a1cd98c43b9a16155d185265e11b74690234a332 GIT binary patch literal 5504 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 Dn4DfA literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/gsub_context3_simple_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/gsub_context3_simple_f1.ttx.GDEF new file mode 100644 index 0000000..dba5550 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context3_simple_f1.ttx.GDEF @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context3_simple_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context3_simple_f1.ttx.GSUB new file mode 100644 index 0000000..93f4bdd --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context3_simple_f1.ttx.GSUB @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context3_successive_f1.otf b/Tests/ttLib/tables/data/aots/gsub_context3_successive_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..d8b3d5ccb2f0e39e2548a96897bb37b4bd869840 GIT binary patch literal 5540 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/gsub_context3_successive_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/gsub_context3_successive_f1.ttx.GSUB new file mode 100644 index 0000000..73e3567 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/gsub_context3_successive_f1.ttx.GSUB @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/lookupflag_ignore_attach_f1.otf b/Tests/ttLib/tables/data/aots/lookupflag_ignore_attach_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..80651f1d66e7e4a144218d22def1dbc7e553ed17 GIT binary patch literal 5416 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/lookupflag_ignore_attach_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/lookupflag_ignore_attach_f1.ttx.GSUB new file mode 100644 index 0000000..f8064cb --- /dev/null +++ b/Tests/ttLib/tables/data/aots/lookupflag_ignore_attach_f1.ttx.GSUB @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/lookupflag_ignore_base_f1.otf b/Tests/ttLib/tables/data/aots/lookupflag_ignore_base_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..3c242b0d1aa008ff5bf838ab875695937baa5ae3 GIT binary patch literal 5256 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 literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/lookupflag_ignore_base_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/lookupflag_ignore_base_f1.ttx.GDEF new file mode 100644 index 0000000..2b0a686 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/lookupflag_ignore_base_f1.ttx.GDEF @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/lookupflag_ignore_base_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/lookupflag_ignore_base_f1.ttx.GSUB new file mode 100644 index 0000000..0982ef6 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/lookupflag_ignore_base_f1.ttx.GSUB @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/lookupflag_ignore_combination_f1.otf b/Tests/ttLib/tables/data/aots/lookupflag_ignore_combination_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..b88359a8fcd75ddb1c5580ffaf0a713a20994d7d GIT binary patch literal 5408 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/lookupflag_ignore_combination_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/lookupflag_ignore_combination_f1.ttx.GSUB new file mode 100644 index 0000000..95047b3 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/lookupflag_ignore_combination_f1.ttx.GSUB @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/lookupflag_ignore_ligatures_f1.otf b/Tests/ttLib/tables/data/aots/lookupflag_ignore_ligatures_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..1dc0c237c17ae4741fac96c7b32dd47aba542410 GIT binary patch literal 5320 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 + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/lookupflag_ignore_ligatures_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/lookupflag_ignore_ligatures_f1.ttx.GSUB new file mode 100644 index 0000000..3bc1e8d --- /dev/null +++ b/Tests/ttLib/tables/data/aots/lookupflag_ignore_ligatures_f1.ttx.GSUB @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/lookupflag_ignore_marks_f1.otf b/Tests/ttLib/tables/data/aots/lookupflag_ignore_marks_f1.otf new file mode 100644 index 0000000000000000000000000000000000000000..aa429de3aa288cbd93f7f816118cd85ded0e5743 GIT binary patch literal 5288 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 a!H_|Z!GJ8gK|^bxQJ^`X@jY;Oq7VRny-zRz literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/aots/lookupflag_ignore_marks_f1.ttx.GDEF b/Tests/ttLib/tables/data/aots/lookupflag_ignore_marks_f1.ttx.GDEF new file mode 100644 index 0000000..19aa7aa --- /dev/null +++ b/Tests/ttLib/tables/data/aots/lookupflag_ignore_marks_f1.ttx.GDEF @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/aots/lookupflag_ignore_marks_f1.ttx.GSUB b/Tests/ttLib/tables/data/aots/lookupflag_ignore_marks_f1.ttx.GSUB new file mode 100644 index 0000000..28b4682 --- /dev/null +++ b/Tests/ttLib/tables/data/aots/lookupflag_ignore_marks_f1.ttx.GSUB @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/graphite/graphite_tests.ttf b/Tests/ttLib/tables/data/graphite/graphite_tests.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ab6a79b5758660e56bbf2d23fce99b4ede597e5c GIT binary patch literal 2260 zcmZQzWME+6XJ}wxW+-q=O)N>j|011%f$atZ1A~uyPGSjD0CNNb1G@?X1A~ftPJS|j zDT5ya1G@(U1A~Zvu)a|cLyQ3f17igP14CGFW=@((?yKhv4D2xs42&VcnK?OrK5k45 z3>+Ik`jc}L3m7#SQy3T+pD-{milygNrZIp4GY5#uNKH(6s+r=$z`*c_fq}s+BO^7D zc_X7P0|TQ8NPk9dNd?hN`fg?um~%d4I-ErI2arlxEL51 zwHeNX1Q-|@&ofS7U}a!n+QZbvz`)QCp&5@d7%?(~RIq>u2nNLwgvG$X;Opod#Nfc7 zz>v%gb_4TzhKF!TFoRKtfr0THgDMlp|38f97?c?p7{DenFfhn~-NnG*!oUn>$uO`m zXfQA^uraVOFfuTKO#S~K6m|@W|NsAg_nMhz&NnIVJG49aFf zQp3t{gK-{IoQ*+^sSL_yXJBBO17R~VFz~?Hj7V%I1_7oOP&Ld9B1}7=Y!(JKrZZ4B zD+3GDB`BMXL5S%ZLl{FULlHwULncE$LmqQ4E)0nbISe@riBJ<#m@z0Ygfav( zID&m+$zaZ41on|Rg8_pDgD!(Pg9(ErgDKd=a0XX~V1^Qg6tGV`8GIOA81flP8FCo% z8HyQ#7*fG52H8}EN1XzLFGC_jGD8vArKt=l44Djx3mBWUdFGAJ;VfL#cRRZv(cFt{@0F(fnOGZcYi66POt(~B4s7z`Ld_8T#n zGw4HO(~!ZK!HU6z!GOUE9P*HG4rTzEj60P581fnX7(gi?mBEK05t?Qa8PXY286p`{ z8HyPc7;?elrQlRh2^Px(>&j<<#6%tgM2-Pg{{F9K5P{}dR0>qyGBda^xH7nb)xt_W zMEPvfz`)3qm!GQumSC4EnfssKDT268>urP2j2r)=7$T6sZ&4iS8JPhn0r!p}xGX4jZJWR|i z5D5qgE`b>s92gitr7R<;b_FR1xto!JgMo{Ig@KhB* z52OdA667k7Ynd2Wpk*DXdn@|067#S4GLBU237_c z21OaZ~=GwZN3D6>c!f>nZ?$-uynR+O5; Tz{DT`jz2~Q25`JGFfafBCW-aw literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Feat b/Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Feat new file mode 100644 index 0000000..9c23f4d --- /dev/null +++ b/Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Feat @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Glat b/Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Glat new file mode 100644 index 0000000..9788530 --- /dev/null +++ b/Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Glat @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Glat.setup b/Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Glat.setup new file mode 100644 index 0000000..8eac7d3 --- /dev/null +++ b/Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Glat.setup @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Silf b/Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Silf new file mode 100644 index 0000000..4b82d1c --- /dev/null +++ b/Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Silf @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + c + + + + + + + + + + + .notdef=0 space=0 a=1 b=2 c=0 + + + + + + + + + PUT_GLYPH_8BIT_OBS(0) + ASSOC(0, 1) + NEXT + DELETE + NEXT + RET_ZERO + + + + + PUT_GLYPH_8BIT_OBS(0) + NEXT + RET_ZERO + + + + + 0 1 + 1 1 2 + 0 3 0 + 0 4 0 + 0 0 5 + 0 0 5 + + + + + + + a=0 c=1 + + + + + + + + COPY_NEXT + PUT_COPY(0) + PUSH_BYTE(-1) + ATTR_SET_SLOT(2) + PUSH_BYTE(0) + ATTR_SET(17) + PUSH_GLYPH_ATTR_OBS(6, 0) + ATTR_SET(8) + PUSH_GLYPH_ATTR_OBS(7, 0) + ATTR_SET(9) + PUSH_ATT_TO_GATTR_OBS(6, 0) + ATTR_SET(3) + PUSH_ATT_TO_GATTR_OBS(7, 0) + ATTR_SET(4) + NEXT + RET_ZERO + + + + + 0 + 1 0 + 0 2 + + + + + + + diff --git a/Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Silf.setup b/Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Silf.setup new file mode 100644 index 0000000..8eac7d3 --- /dev/null +++ b/Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Silf.setup @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Sill b/Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Sill new file mode 100644 index 0000000..4107059 --- /dev/null +++ b/Tests/ttLib/tables/data/graphite/graphite_tests.ttx.Sill @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/Tests/ttLib/tables/data/ttProgram.ttx b/Tests/ttLib/tables/data/ttProgram.ttx new file mode 100644 index 0000000..7a99efd --- /dev/null +++ b/Tests/ttLib/tables/data/ttProgram.ttx @@ -0,0 +1,1627 @@ + + + NPUSHB[ ] /* 59 values pushed */ + 58 57 56 55 54 53 52 51 50 49 48 47 46 45 44 43 42 41 40 39 38 37 36 35 34 + 33 32 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 + 8 7 6 5 4 3 2 1 0 + FDEF[ ] /* FunctionDefinition */ + SVTCA[1] /* SetFPVectorToAxis */ + PUSHB[ ] /* 1 value pushed */ + 24 + RS[ ] /* ReadStore */ + IF[ ] /* If */ + RCVT[ ] /* ReadCVT */ + ROUND[10] /* Round */ + PUSHB[ ] /* 1 value pushed */ + 25 + RS[ ] /* ReadStore */ + ADD[ ] /* Add */ + PUSHB[ ] /* 1 value pushed */ + 70 + SWAP[ ] /* SwapTopStack */ + WCVTP[ ] /* WriteCVTInPixels */ + SWAP[ ] /* SwapTopStack */ + SRP0[ ] /* SetRefPoint0 */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 70 + FLIPOFF[ ] /* SetAutoFlipOff */ + MIRP[10000] /* MoveIndirectRelPt */ + FLIPON[ ] /* SetAutoFlipOn */ + MDAP[1] /* MoveDirectAbsPt */ + PUSHB[ ] /* 1 value pushed */ + 0 + SRP2[ ] /* SetRefPoint2 */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + SWAP[ ] /* SwapTopStack */ + SRP1[ ] /* SetRefPoint1 */ + SHP[1] /* ShiftPointByLastPoint */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[1] /* SetFPVectorToAxis */ + PUSHB[ ] /* 1 value pushed */ + 24 + RS[ ] /* ReadStore */ + IF[ ] /* If */ + PUSHB[ ] /* 1 value pushed */ + 5 + CALL[ ] /* CallFunction */ + PUSHB[ ] /* 1 value pushed */ + 0 + SZP0[ ] /* SetZonePointer0 */ + MPPEM[ ] /* MeasurePixelPerEm */ + PUSHB[ ] /* 1 value pushed */ + 20 + LT[ ] /* LessThan */ + IF[ ] /* If */ + PUSHB[ ] /* 2 values pushed */ + 0 64 + SHPIX[ ] /* ShiftZoneByPixel */ + EIF[ ] /* EndIf */ + PUSHB[ ] /* 1 value pushed */ + 6 + CALL[ ] /* CallFunction */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + SWAP[ ] /* SwapTopStack */ + SRP1[ ] /* SetRefPoint1 */ + SHP[1] /* ShiftPointByLastPoint */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[1] /* SetFPVectorToAxis */ + PUSHB[ ] /* 1 value pushed */ + 24 + RS[ ] /* ReadStore */ + IF[ ] /* If */ + FLIPOFF[ ] /* SetAutoFlipOff */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + SRP0[ ] /* SetRefPoint0 */ + MIRP[10010] /* MoveIndirectRelPt */ + POP[ ] /* PopTopStack */ + PUSHB[ ] /* 1 value pushed */ + 0 + SRP2[ ] /* SetRefPoint2 */ + FLIPON[ ] /* SetAutoFlipOn */ + ELSE[ ] /* Else */ + DUP[ ] /* DuplicateTopStack */ + RCVT[ ] /* ReadCVT */ + PUSHB[ ] /* 1 value pushed */ + 4 + CINDEX[ ] /* CopyXToTopStack */ + PUSHB[ ] /* 1 value pushed */ + 4 + CINDEX[ ] /* CopyXToTopStack */ + SWAP[ ] /* SwapTopStack */ + MD[1] /* MeasureDistance */ + SUB[ ] /* Subtract */ + ABS[ ] /* Absolute */ + PUSHB[ ] /* 1 value pushed */ + 40 + GT[ ] /* GreaterThan */ + IF[ ] /* If */ + POP[ ] /* PopTopStack */ + SWAP[ ] /* SwapTopStack */ + SRP0[ ] /* SetRefPoint0 */ + MDRP[10110] /* MoveDirectRelPt */ + ELSE[ ] /* Else */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + SRP0[ ] /* SetRefPoint0 */ + MIRP[10010] /* MoveIndirectRelPt */ + POP[ ] /* PopTopStack */ + PUSHB[ ] /* 1 value pushed */ + 0 + SRP2[ ] /* SetRefPoint2 */ + EIF[ ] /* EndIf */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 26 + RS[ ] /* ReadStore */ + IF[ ] /* If */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + ELSE[ ] /* Else */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + MD[0] /* MeasureDistance */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + MD[1] /* MeasureDistance */ + SUB[ ] /* Subtract */ + DUP[ ] /* DuplicateTopStack */ + ABS[ ] /* Absolute */ + PUSHB[ ] /* 1 value pushed */ + 16 + LT[ ] /* LessThan */ + IF[ ] /* If */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + ELSE[ ] /* Else */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + MD[0] /* MeasureDistance */ + PUSHB[ ] /* 1 value pushed */ + 0 + LT[ ] /* LessThan */ + IF[ ] /* If */ + PUSHB[ ] /* 1 value pushed */ + 0 + LT[ ] /* LessThan */ + IF[ ] /* If */ + PUSHW[ ] /* 1 value pushed */ + -30 + SHPIX[ ] /* ShiftZoneByPixel */ + POP[ ] /* PopTopStack */ + ELSE[ ] /* Else */ + PUSHB[ ] /* 1 value pushed */ + 16 + SHPIX[ ] /* ShiftZoneByPixel */ + POP[ ] /* PopTopStack */ + EIF[ ] /* EndIf */ + ELSE[ ] /* Else */ + PUSHB[ ] /* 1 value pushed */ + 0 + GT[ ] /* GreaterThan */ + IF[ ] /* If */ + PUSHB[ ] /* 1 value pushed */ + 30 + SHPIX[ ] /* ShiftZoneByPixel */ + POP[ ] /* PopTopStack */ + ELSE[ ] /* Else */ + PUSHW[ ] /* 1 value pushed */ + -16 + SHPIX[ ] /* ShiftZoneByPixel */ + POP[ ] /* PopTopStack */ + EIF[ ] /* EndIf */ + EIF[ ] /* EndIf */ + EIF[ ] /* EndIf */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[1] /* SetFPVectorToAxis */ + PUSHB[ ] /* 1 value pushed */ + 24 + RS[ ] /* ReadStore */ + IF[ ] /* If */ + PUSHB[ ] /* 1 value pushed */ + 5 + CALL[ ] /* CallFunction */ + PUSHB[ ] /* 1 value pushed */ + 0 + SZP0[ ] /* SetZonePointer0 */ + MPPEM[ ] /* MeasurePixelPerEm */ + PUSHB[ ] /* 1 value pushed */ + 20 + LT[ ] /* LessThan */ + IF[ ] /* If */ + PUSHW[ ] /* 2 values pushed */ + 0 -64 + SHPIX[ ] /* ShiftZoneByPixel */ + EIF[ ] /* EndIf */ + PUSHB[ ] /* 1 value pushed */ + 6 + CALL[ ] /* CallFunction */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + SWAP[ ] /* SwapTopStack */ + SRP1[ ] /* SetRefPoint1 */ + SHP[1] /* ShiftPointByLastPoint */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + FLIPOFF[ ] /* SetAutoFlipOff */ + SVTCA[1] /* SetFPVectorToAxis */ + ROLL[ ] /* RollTopThreeStack */ + SRP0[ ] /* SetRefPoint0 */ + PUSHB[ ] /* 2 values pushed */ + 70 25 + RS[ ] /* ReadStore */ + WCVTP[ ] /* WriteCVTInPixels */ + PUSHB[ ] /* 1 value pushed */ + 0 + SZP1[ ] /* SetZonePointer1 */ + PUSHB[ ] /* 2 values pushed */ + 0 70 + MIRP[00010] /* MoveIndirectRelPt */ + PUSHB[ ] /* 1 value pushed */ + 0 + SZP2[ ] /* SetZonePointer2 */ + PUSHW[ ] /* 2 values pushed */ + 0 -16 + SHPIX[ ] /* ShiftZoneByPixel */ + SVTCA[0] /* SetFPVectorToAxis */ + PUSHB[ ] /* 1 value pushed */ + 0 + ALIGNRP[ ] /* AlignRelativePt */ + PUSHB[ ] /* 1 value pushed */ + 40 + CALL[ ] /* CallFunction */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + SRP0[ ] /* SetRefPoint0 */ + PUSHB[ ] /* 1 value pushed */ + 0 + ALIGNRP[ ] /* AlignRelativePt */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[1] /* SetFPVectorToAxis */ + RTG[ ] /* RoundToGrid */ + PUSHB[ ] /* 1 value pushed */ + 0 + MDAP[1] /* MoveDirectAbsPt */ + PUSHB[ ] /* 1 value pushed */ + 1 + SZP1[ ] /* SetZonePointer1 */ + MIRP[10010] /* MoveIndirectRelPt */ + PUSHB[ ] /* 1 value pushed */ + 1 + SZP0[ ] /* SetZonePointer0 */ + PUSHB[ ] /* 1 value pushed */ + 1 + SZP2[ ] /* SetZonePointer2 */ + FLIPON[ ] /* SetAutoFlipOn */ + PUSHB[ ] /* 1 value pushed */ + 0 + SRP2[ ] /* SetRefPoint2 */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[1] /* SetFPVectorToAxis */ + PUSHB[ ] /* 1 value pushed */ + 24 + RS[ ] /* ReadStore */ + IF[ ] /* If */ + PUSHB[ ] /* 1 value pushed */ + 5 + CALL[ ] /* CallFunction */ + PUSHB[ ] /* 1 value pushed */ + 0 + SZP0[ ] /* SetZonePointer0 */ + PUSHW[ ] /* 2 values pushed */ + 0 -32 + SHPIX[ ] /* ShiftZoneByPixel */ + PUSHB[ ] /* 1 value pushed */ + 6 + CALL[ ] /* CallFunction */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + SWAP[ ] /* SwapTopStack */ + SRP1[ ] /* SetRefPoint1 */ + SHP[1] /* ShiftPointByLastPoint */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[1] /* SetFPVectorToAxis */ + PUSHB[ ] /* 1 value pushed */ + 24 + RS[ ] /* ReadStore */ + IF[ ] /* If */ + RCVT[ ] /* ReadCVT */ + ABS[ ] /* Absolute */ + ROUND[10] /* Round */ + SWAP[ ] /* SwapTopStack */ + RCVT[ ] /* ReadCVT */ + ABS[ ] /* Absolute */ + ROUND[01] /* Round */ + PUSHB[ ] /* 1 value pushed */ + 25 + RS[ ] /* ReadStore */ + ABS[ ] /* Absolute */ + ADD[ ] /* Add */ + ADD[ ] /* Add */ + PUSHB[ ] /* 1 value pushed */ + 70 + SWAP[ ] /* SwapTopStack */ + WCVTP[ ] /* WriteCVTInPixels */ + SWAP[ ] /* SwapTopStack */ + SRP0[ ] /* SetRefPoint0 */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 70 + MIRP[10000] /* MoveIndirectRelPt */ + MDAP[1] /* MoveDirectAbsPt */ + PUSHB[ ] /* 1 value pushed */ + 0 + SRP2[ ] /* SetRefPoint2 */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + DUP[ ] /* DuplicateTopStack */ + ROLL[ ] /* RollTopThreeStack */ + DUP[ ] /* DuplicateTopStack */ + ROLL[ ] /* RollTopThreeStack */ + GT[ ] /* GreaterThan */ + IF[ ] /* If */ + SRP1[ ] /* SetRefPoint1 */ + SHP[1] /* ShiftPointByLastPoint */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + EIF[ ] /* EndIf */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[1] /* SetFPVectorToAxis */ + PUSHB[ ] /* 2 values pushed */ + 11 10 + RS[ ] /* ReadStore */ + SWAP[ ] /* SwapTopStack */ + RS[ ] /* ReadStore */ + NEG[ ] /* Negate */ + SPVFS[ ] /* SetPVectorFromStack */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[0] /* SetFPVectorToAxis */ + PUSHB[ ] /* 2 values pushed */ + 10 11 + RS[ ] /* ReadStore */ + SWAP[ ] /* SwapTopStack */ + RS[ ] /* ReadStore */ + SFVFS[ ] /* SetFVectorFromStack */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[0] /* SetFPVectorToAxis */ + PUSHB[ ] /* 1 value pushed */ + 70 + SWAP[ ] /* SwapTopStack */ + WCVTF[ ] /* WriteCVTInFUnits */ + PUSHB[ ] /* 2 values pushed */ + 1 70 + MIAP[0] /* MoveIndirectAbsPt */ + SVTCA[1] /* SetFPVectorToAxis */ + PUSHB[ ] /* 1 value pushed */ + 70 + SWAP[ ] /* SwapTopStack */ + WCVTF[ ] /* WriteCVTInFUnits */ + PUSHB[ ] /* 2 values pushed */ + 2 70 + RCVT[ ] /* ReadCVT */ + MSIRP[0] /* MoveStackIndirRelPt */ + PUSHB[ ] /* 2 values pushed */ + 2 0 + SFVTL[0] /* SetFVectorToLine */ + GFV[ ] /* GetFVector */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 18 + CALL[ ] /* CallFunction */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + ROUND[10] /* Round */ + PUSHB[ ] /* 1 value pushed */ + 64 + MAX[ ] /* Maximum */ + ADD[ ] /* Add */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + SWAP[ ] /* SwapTopStack */ + WCVTP[ ] /* WriteCVTInPixels */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 19 + CALL[ ] /* CallFunction */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + ROUND[10] /* Round */ + PUSHW[ ] /* 1 value pushed */ + -64 + MIN[ ] /* Minimum */ + ADD[ ] /* Add */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + SWAP[ ] /* SwapTopStack */ + WCVTP[ ] /* WriteCVTInPixels */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 0 + PUSHB[ ] /* 1 value pushed */ + 18 + CALL[ ] /* CallFunction */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 0 + PUSHB[ ] /* 1 value pushed */ + 19 + CALL[ ] /* CallFunction */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[1] /* SetFPVectorToAxis */ + PUSHB[ ] /* 1 value pushed */ + 6 + RS[ ] /* ReadStore */ + PUSHB[ ] /* 1 value pushed */ + 7 + RS[ ] /* ReadStore */ + NEG[ ] /* Negate */ + SPVFS[ ] /* SetPVectorFromStack */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + DUP[ ] /* DuplicateTopStack */ + ROUND[01] /* Round */ + PUSHB[ ] /* 1 value pushed */ + 64 + SUB[ ] /* Subtract */ + PUSHB[ ] /* 1 value pushed */ + 0 + MAX[ ] /* Maximum */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 2 values pushed */ + 44 192 + ROLL[ ] /* RollTopThreeStack */ + MIN[ ] /* Minimum */ + PUSHW[ ] /* 1 value pushed */ + 4096 + DIV[ ] /* Divide */ + ADD[ ] /* Add */ + CALL[ ] /* CallFunction */ + GPV[ ] /* GetPVector */ + ABS[ ] /* Absolute */ + SWAP[ ] /* SwapTopStack */ + ABS[ ] /* Absolute */ + SUB[ ] /* Subtract */ + NOT[ ] /* LogicalNot */ + IF[ ] /* If */ + PUSHB[ ] /* 1 value pushed */ + 3 + SUB[ ] /* Subtract */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + PUSHB[ ] /* 2 values pushed */ + 0 3 + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + ROUND[00] /* Round */ + EQ[ ] /* Equal */ + PUSHB[ ] /* 1 value pushed */ + 28 + MPPEM[ ] /* MeasurePixelPerEm */ + LT[ ] /* LessThan */ + AND[ ] /* LogicalAnd */ + IF[ ] /* If */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + ADD[ ] /* Add */ + ROUND[00] /* Round */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 4 + CINDEX[ ] /* CopyXToTopStack */ + SWAP[ ] /* SwapTopStack */ + WCVTP[ ] /* WriteCVTInPixels */ + PUSHB[ ] /* 1 value pushed */ + 4 + CINDEX[ ] /* CopyXToTopStack */ + SWAP[ ] /* SwapTopStack */ + WCVTP[ ] /* WriteCVTInPixels */ + ELSE[ ] /* Else */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + DUP[ ] /* DuplicateTopStack */ + RCVT[ ] /* ReadCVT */ + ROUND[00] /* Round */ + DUP[ ] /* DuplicateTopStack */ + ROLL[ ] /* RollTopThreeStack */ + SWAP[ ] /* SwapTopStack */ + WCVTP[ ] /* WriteCVTInPixels */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + ROUND[00] /* Round */ + ADD[ ] /* Add */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + SWAP[ ] /* SwapTopStack */ + WCVTP[ ] /* WriteCVTInPixels */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + DUP[ ] /* DuplicateTopStack */ + RCVT[ ] /* ReadCVT */ + ROUND[00] /* Round */ + DUP[ ] /* DuplicateTopStack */ + ROLL[ ] /* RollTopThreeStack */ + SWAP[ ] /* SwapTopStack */ + WCVTP[ ] /* WriteCVTInPixels */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + ABS[ ] /* Absolute */ + ROUND[00] /* Round */ + NEG[ ] /* Negate */ + ADD[ ] /* Add */ + PUSHB[ ] /* 1 value pushed */ + 4 + CINDEX[ ] /* CopyXToTopStack */ + PUSHB[ ] /* 1 value pushed */ + 1 + ADD[ ] /* Add */ + SWAP[ ] /* SwapTopStack */ + WCVTP[ ] /* WriteCVTInPixels */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 9 + RS[ ] /* ReadStore */ + IF[ ] /* If */ + SDPVTL[1] /* SetDualPVectorToLine */ + POP[ ] /* PopTopStack */ + MDRP[00000] /* MoveDirectRelPt */ + ELSE[ ] /* Else */ + PUSHB[ ] /* 1 value pushed */ + 18 + RS[ ] /* ReadStore */ + IF[ ] /* If */ + SDPVTL[1] /* SetDualPVectorToLine */ + RCVT[ ] /* ReadCVT */ + PUSHB[ ] /* 1 value pushed */ + 17 + CALL[ ] /* CallFunction */ + PUSHB[ ] /* 1 value pushed */ + 71 + SWAP[ ] /* SwapTopStack */ + WCVTP[ ] /* WriteCVTInPixels */ + PUSHB[ ] /* 1 value pushed */ + 71 + ROFF[ ] /* RoundOff */ + MIRP[00100] /* MoveIndirectRelPt */ + ELSE[ ] /* Else */ + SPVTCA[1] /* SetPVectorToAxis */ + ROLL[ ] /* RollTopThreeStack */ + RCVT[ ] /* ReadCVT */ + RTG[ ] /* RoundToGrid */ + ROUND[01] /* Round */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 71 + SWAP[ ] /* SwapTopStack */ + WCVTP[ ] /* WriteCVTInPixels */ + ROLL[ ] /* RollTopThreeStack */ + ROLL[ ] /* RollTopThreeStack */ + SDPVTL[1] /* SetDualPVectorToLine */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 160 + LTEQ[ ] /* LessThenOrEqual */ + IF[ ] /* If */ + PUSHB[ ] /* 1 value pushed */ + 17 + CALL[ ] /* CallFunction */ + PUSHB[ ] /* 1 value pushed */ + 71 + SWAP[ ] /* SwapTopStack */ + WCVTP[ ] /* WriteCVTInPixels */ + PUSHB[ ] /* 1 value pushed */ + 71 + ROFF[ ] /* RoundOff */ + MIRP[00100] /* MoveIndirectRelPt */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + PUSHB[ ] /* 1 value pushed */ + 71 + ROFF[ ] /* RoundOff */ + MIRP[00100] /* MoveIndirectRelPt */ + EIF[ ] /* EndIf */ + EIF[ ] /* EndIf */ + EIF[ ] /* EndIf */ + RTG[ ] /* RoundToGrid */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + DUP[ ] /* DuplicateTopStack */ + ROLL[ ] /* RollTopThreeStack */ + RCVT[ ] /* ReadCVT */ + SWAP[ ] /* SwapTopStack */ + RCVT[ ] /* ReadCVT */ + ROUND[00] /* Round */ + ADD[ ] /* Add */ + WCVTP[ ] /* WriteCVTInPixels */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + RCVT[ ] /* ReadCVT */ + ROUND[10] /* Round */ + WS[ ] /* WriteStore */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[1] /* SetFPVectorToAxis */ + RTG[ ] /* RoundToGrid */ + MDAP[1] /* MoveDirectAbsPt */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[1] /* SetFPVectorToAxis */ + PUSHB[ ] /* 1 value pushed */ + 24 + RS[ ] /* ReadStore */ + IF[ ] /* If */ + PUSHB[ ] /* 1 value pushed */ + 4 + CINDEX[ ] /* CopyXToTopStack */ + PUSHB[ ] /* 1 value pushed */ + 4 + CINDEX[ ] /* CopyXToTopStack */ + MD[0] /* MeasureDistance */ + ABS[ ] /* Absolute */ + SWAP[ ] /* SwapTopStack */ + RCVT[ ] /* ReadCVT */ + ABS[ ] /* Absolute */ + ROUND[01] /* Round */ + PUSHB[ ] /* 1 value pushed */ + 64 + MAX[ ] /* Maximum */ + SUB[ ] /* Subtract */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 128 + DIV[ ] /* Divide */ + ROUND[10] /* Round */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + SUB[ ] /* Subtract */ + MIN[ ] /* Minimum */ + PUSHB[ ] /* 1 value pushed */ + 25 + RS[ ] /* ReadStore */ + ADD[ ] /* Add */ + PUSHB[ ] /* 1 value pushed */ + 70 + SWAP[ ] /* SwapTopStack */ + WCVTP[ ] /* WriteCVTInPixels */ + POP[ ] /* PopTopStack */ + ROLL[ ] /* RollTopThreeStack */ + SRP0[ ] /* SetRefPoint0 */ + PUSHB[ ] /* 1 value pushed */ + 70 + MIRP[10110] /* MoveIndirectRelPt */ + POP[ ] /* PopTopStack */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[1] /* SetFPVectorToAxis */ + PUSHB[ ] /* 1 value pushed */ + 24 + RS[ ] /* ReadStore */ + IF[ ] /* If */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + ABS[ ] /* Absolute */ + ADD[ ] /* Add */ + ROUND[10] /* Round */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + ROUND[10] /* Round */ + SUB[ ] /* Subtract */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 4 + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + ROUND[10] /* Round */ + DUP[ ] /* DuplicateTopStack */ + ROLL[ ] /* RollTopThreeStack */ + MAX[ ] /* Maximum */ + NEG[ ] /* Negate */ + PUSHB[ ] /* 1 value pushed */ + 4 + CINDEX[ ] /* CopyXToTopStack */ + SWAP[ ] /* SwapTopStack */ + WCVTP[ ] /* WriteCVTInPixels */ + MIN[ ] /* Minimum */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + SWAP[ ] /* SwapTopStack */ + WCVTP[ ] /* WriteCVTInPixels */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + ELSE[ ] /* Else */ + DUP[ ] /* DuplicateTopStack */ + RCVT[ ] /* ReadCVT */ + ROUND[10] /* Round */ + WCVTP[ ] /* WriteCVTInPixels */ + DUP[ ] /* DuplicateTopStack */ + RCVT[ ] /* ReadCVT */ + ROUND[10] /* Round */ + WCVTP[ ] /* WriteCVTInPixels */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[1] /* SetFPVectorToAxis */ + DUP[ ] /* DuplicateTopStack */ + RCVT[ ] /* ReadCVT */ + PUSHB[ ] /* 1 value pushed */ + 0 + NEQ[ ] /* NotEqual */ + PUSHB[ ] /* 1 value pushed */ + 24 + RS[ ] /* ReadStore */ + AND[ ] /* LogicalAnd */ + IF[ ] /* If */ + RCVT[ ] /* ReadCVT */ + ROUND[00] /* Round */ + SWAP[ ] /* SwapTopStack */ + RCVT[ ] /* ReadCVT */ + ROUND[01] /* Round */ + PUSHB[ ] /* 1 value pushed */ + 64 + MAX[ ] /* Maximum */ + SUB[ ] /* Subtract */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 128 + DIV[ ] /* Divide */ + ROUND[10] /* Round */ + DUP[ ] /* DuplicateTopStack */ + ROLL[ ] /* RollTopThreeStack */ + SWAP[ ] /* SwapTopStack */ + SUB[ ] /* Subtract */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + MAX[ ] /* Maximum */ + NEG[ ] /* Negate */ + PUSHB[ ] /* 1 value pushed */ + 4 + CINDEX[ ] /* CopyXToTopStack */ + SWAP[ ] /* SwapTopStack */ + WCVTP[ ] /* WriteCVTInPixels */ + MIN[ ] /* Minimum */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + SWAP[ ] /* SwapTopStack */ + WCVTP[ ] /* WriteCVTInPixels */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + PUSHB[ ] /* 1 value pushed */ + 25 + CALL[ ] /* CallFunction */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[1] /* SetFPVectorToAxis */ + ROLL[ ] /* RollTopThreeStack */ + ROLL[ ] /* RollTopThreeStack */ + RCVT[ ] /* ReadCVT */ + ABS[ ] /* Absolute */ + SWAP[ ] /* SwapTopStack */ + RCVT[ ] /* ReadCVT */ + ABS[ ] /* Absolute */ + SUB[ ] /* Subtract */ + ABS[ ] /* Absolute */ + WS[ ] /* WriteStore */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 4 + CINDEX[ ] /* CopyXToTopStack */ + PUSHB[ ] /* 1 value pushed */ + 4 + CINDEX[ ] /* CopyXToTopStack */ + PUSHB[ ] /* 1 value pushed */ + 25 + CALL[ ] /* CallFunction */ + PUSHB[ ] /* 1 value pushed */ + 24 + RS[ ] /* ReadStore */ + IF[ ] /* If */ + PUSHB[ ] /* 1 value pushed */ + 4 + CINDEX[ ] /* CopyXToTopStack */ + PUSHB[ ] /* 1 value pushed */ + 4 + CINDEX[ ] /* CopyXToTopStack */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + PUSHB[ ] /* 1 value pushed */ + 27 + CALL[ ] /* CallFunction */ + SVTCA[1] /* SetFPVectorToAxis */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + RS[ ] /* ReadStore */ + PUSHB[ ] /* 1 value pushed */ + 64 + EQ[ ] /* Equal */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + RS[ ] /* ReadStore */ + PUSHB[ ] /* 1 value pushed */ + 0 + EQ[ ] /* Equal */ + AND[ ] /* LogicalAnd */ + IF[ ] /* If */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + DUP[ ] /* DuplicateTopStack */ + RCVT[ ] /* ReadCVT */ + PUSHB[ ] /* 1 value pushed */ + 64 + SUB[ ] /* Subtract */ + WCVTP[ ] /* WriteCVTInPixels */ + EIF[ ] /* EndIf */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + RS[ ] /* ReadStore */ + PUSHB[ ] /* 1 value pushed */ + 0 + EQ[ ] /* Equal */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + RS[ ] /* ReadStore */ + PUSHB[ ] /* 1 value pushed */ + 64 + EQ[ ] /* Equal */ + AND[ ] /* LogicalAnd */ + IF[ ] /* If */ + PUSHB[ ] /* 1 value pushed */ + 4 + CINDEX[ ] /* CopyXToTopStack */ + DUP[ ] /* DuplicateTopStack */ + RCVT[ ] /* ReadCVT */ + PUSHB[ ] /* 1 value pushed */ + 64 + ADD[ ] /* Add */ + WCVTP[ ] /* WriteCVTInPixels */ + EIF[ ] /* EndIf */ + EIF[ ] /* EndIf */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[1] /* SetFPVectorToAxis */ + MPPEM[ ] /* MeasurePixelPerEm */ + GT[ ] /* GreaterThan */ + IF[ ] /* If */ + RS[ ] /* ReadStore */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + SWAP[ ] /* SwapTopStack */ + SUB[ ] /* Subtract */ + WCVTP[ ] /* WriteCVTInPixels */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[1] /* SetFPVectorToAxis */ + MPPEM[ ] /* MeasurePixelPerEm */ + GT[ ] /* GreaterThan */ + IF[ ] /* If */ + RS[ ] /* ReadStore */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + SWAP[ ] /* SwapTopStack */ + ADD[ ] /* Add */ + WCVTP[ ] /* WriteCVTInPixels */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + MPPEM[ ] /* MeasurePixelPerEm */ + GT[ ] /* GreaterThan */ + IF[ ] /* If */ + RCVT[ ] /* ReadCVT */ + WCVTP[ ] /* WriteCVTInPixels */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[1] /* SetFPVectorToAxis */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + SWAP[ ] /* SwapTopStack */ + MD[0] /* MeasureDistance */ + PUSHB[ ] /* 1 value pushed */ + 64 + ADD[ ] /* Add */ + PUSHB[ ] /* 1 value pushed */ + 32 + MUL[ ] /* Multiply */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 0 + GT[ ] /* GreaterThan */ + IF[ ] /* If */ + SWAP[ ] /* SwapTopStack */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + SHPIX[ ] /* ShiftZoneByPixel */ + SWAP[ ] /* SwapTopStack */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + NEG[ ] /* Negate */ + SHPIX[ ] /* ShiftZoneByPixel */ + SVTCA[0] /* SetFPVectorToAxis */ + ROLL[ ] /* RollTopThreeStack */ + MUL[ ] /* Multiply */ + SHPIX[ ] /* ShiftZoneByPixel */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + EIF[ ] /* EndIf */ + SVTCA[1] /* SetFPVectorToAxis */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + MPPEM[ ] /* MeasurePixelPerEm */ + LT[ ] /* LessThan */ + IF[ ] /* If */ + RCVT[ ] /* ReadCVT */ + WCVTP[ ] /* WriteCVTInPixels */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[1] /* SetFPVectorToAxis */ + PUSHB[ ] /* 1 value pushed */ + 5 + CINDEX[ ] /* CopyXToTopStack */ + SRP0[ ] /* SetRefPoint0 */ + SWAP[ ] /* SwapTopStack */ + DUP[ ] /* DuplicateTopStack */ + ROLL[ ] /* RollTopThreeStack */ + MIRP[10101] /* MoveIndirectRelPt */ + SVTCA[0] /* SetFPVectorToAxis */ + PUSHB[ ] /* 1 value pushed */ + 1 + ADD[ ] /* Add */ + SWAP[ ] /* SwapTopStack */ + MIRP[01101] /* MoveIndirectRelPt */ + MIRP[01100] /* MoveIndirectRelPt */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[1] /* SetFPVectorToAxis */ + PUSHB[ ] /* 1 value pushed */ + 5 + CINDEX[ ] /* CopyXToTopStack */ + SRP0[ ] /* SetRefPoint0 */ + SWAP[ ] /* SwapTopStack */ + DUP[ ] /* DuplicateTopStack */ + ROLL[ ] /* RollTopThreeStack */ + MIRP[10101] /* MoveIndirectRelPt */ + SVTCA[0] /* SetFPVectorToAxis */ + PUSHB[ ] /* 1 value pushed */ + 1 + SUB[ ] /* Subtract */ + SWAP[ ] /* SwapTopStack */ + MIRP[01101] /* MoveIndirectRelPt */ + MIRP[01100] /* MoveIndirectRelPt */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[1] /* SetFPVectorToAxis */ + PUSHB[ ] /* 1 value pushed */ + 6 + CINDEX[ ] /* CopyXToTopStack */ + SRP0[ ] /* SetRefPoint0 */ + MIRP[10101] /* MoveIndirectRelPt */ + SVTCA[0] /* SetFPVectorToAxis */ + MIRP[01101] /* MoveIndirectRelPt */ + MIRP[01100] /* MoveIndirectRelPt */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + GC[0] /* GetCoordOnPVector */ + SWAP[ ] /* SwapTopStack */ + GC[0] /* GetCoordOnPVector */ + ADD[ ] /* Add */ + ROLL[ ] /* RollTopThreeStack */ + ROLL[ ] /* RollTopThreeStack */ + GC[0] /* GetCoordOnPVector */ + SWAP[ ] /* SwapTopStack */ + DUP[ ] /* DuplicateTopStack */ + GC[0] /* GetCoordOnPVector */ + ROLL[ ] /* RollTopThreeStack */ + ADD[ ] /* Add */ + ROLL[ ] /* RollTopThreeStack */ + SUB[ ] /* Subtract */ + PUSHW[ ] /* 1 value pushed */ + -128 + DIV[ ] /* Divide */ + SWAP[ ] /* SwapTopStack */ + DUP[ ] /* DuplicateTopStack */ + SRP0[ ] /* SetRefPoint0 */ + SWAP[ ] /* SwapTopStack */ + ROLL[ ] /* RollTopThreeStack */ + PUSHB[ ] /* 2 values pushed */ + 75 75 + ROLL[ ] /* RollTopThreeStack */ + WCVTF[ ] /* WriteCVTInFUnits */ + RCVT[ ] /* ReadCVT */ + ADD[ ] /* Add */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 0 + LT[ ] /* LessThan */ + IF[ ] /* If */ + PUSHB[ ] /* 1 value pushed */ + 1 + SUB[ ] /* Subtract */ + PUSHW[ ] /* 1 value pushed */ + -70 + MAX[ ] /* Maximum */ + ELSE[ ] /* Else */ + PUSHB[ ] /* 1 value pushed */ + 70 + MIN[ ] /* Minimum */ + EIF[ ] /* EndIf */ + PUSHB[ ] /* 1 value pushed */ + 16 + ADD[ ] /* Add */ + ROUND[00] /* Round */ + SVTCA[1] /* SetFPVectorToAxis */ + MSIRP[0] /* MoveStackIndirRelPt */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + DUP[ ] /* DuplicateTopStack */ + RCVT[ ] /* ReadCVT */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + GC[0] /* GetCoordOnPVector */ + GT[ ] /* GreaterThan */ + IF[ ] /* If */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + GC[0] /* GetCoordOnPVector */ + DUP[ ] /* DuplicateTopStack */ + ROUND[00] /* Round */ + SUB[ ] /* Subtract */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + MIAP[1] /* MoveIndirectAbsPt */ + SWAP[ ] /* SwapTopStack */ + POP[ ] /* PopTopStack */ + SHPIX[ ] /* ShiftZoneByPixel */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + SRP1[ ] /* SetRefPoint1 */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + DUP[ ] /* DuplicateTopStack */ + RCVT[ ] /* ReadCVT */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + GC[0] /* GetCoordOnPVector */ + LT[ ] /* LessThan */ + IF[ ] /* If */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + GC[0] /* GetCoordOnPVector */ + DUP[ ] /* DuplicateTopStack */ + ROUND[00] /* Round */ + SUB[ ] /* Subtract */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + MIAP[1] /* MoveIndirectAbsPt */ + SWAP[ ] /* SwapTopStack */ + POP[ ] /* PopTopStack */ + SHPIX[ ] /* ShiftZoneByPixel */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + SRP1[ ] /* SetRefPoint1 */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[0] /* SetFPVectorToAxis */ + PUSHB[ ] /* 1 value pushed */ + 7 + RS[ ] /* ReadStore */ + PUSHB[ ] /* 1 value pushed */ + 6 + RS[ ] /* ReadStore */ + SFVFS[ ] /* SetFVectorFromStack */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + ROLL[ ] /* RollTopThreeStack */ + SRP0[ ] /* SetRefPoint0 */ + MIRP[01100] /* MoveIndirectRelPt */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 12 + RS[ ] /* ReadStore */ + IF[ ] /* If */ + POP[ ] /* PopTopStack */ + ELSE[ ] /* Else */ + DUP[ ] /* DuplicateTopStack */ + GC[0] /* GetCoordOnPVector */ + PUSHB[ ] /* 1 value pushed */ + 0 + GT[ ] /* GreaterThan */ + IF[ ] /* If */ + PUSHW[ ] /* 1 value pushed */ + -16 + SHPIX[ ] /* ShiftZoneByPixel */ + ELSE[ ] /* Else */ + PUSHB[ ] /* 1 value pushed */ + 16 + SHPIX[ ] /* ShiftZoneByPixel */ + EIF[ ] /* EndIf */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 0 + NEQ[ ] /* NotEqual */ + IF[ ] /* If */ + PUSHW[ ] /* 1 value pushed */ + 4096 + MUL[ ] /* Multiply */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + ABS[ ] /* Absolute */ + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + ABS[ ] /* Absolute */ + SUB[ ] /* Subtract */ + PUSHB[ ] /* 1 value pushed */ + 0 + GTEQ[ ] /* GreaterThanOrEqual */ + IF[ ] /* If */ + PUSHB[ ] /* 1 value pushed */ + 2 + ELSE[ ] /* Else */ + PUSHB[ ] /* 1 value pushed */ + 64 + SUB[ ] /* Subtract */ + PUSHB[ ] /* 1 value pushed */ + 3 + EIF[ ] /* EndIf */ + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + ROUND[01] /* Round */ + GTEQ[ ] /* GreaterThanOrEqual */ + IF[ ] /* If */ + RCVT[ ] /* ReadCVT */ + WCVTP[ ] /* WriteCVTInPixels */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + EIF[ ] /* EndIf */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + RCVT[ ] /* ReadCVT */ + SUB[ ] /* Subtract */ + ABS[ ] /* Absolute */ + PUSHB[ ] /* 1 value pushed */ + 40 + LTEQ[ ] /* LessThenOrEqual */ + IF[ ] /* If */ + RCVT[ ] /* ReadCVT */ + WCVTP[ ] /* WriteCVTInPixels */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + EIF[ ] /* EndIf */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + GPV[ ] /* GetPVector */ + ABS[ ] /* Absolute */ + SWAP[ ] /* SwapTopStack */ + ABS[ ] /* Absolute */ + MAX[ ] /* Maximum */ + PUSHW[ ] /* 1 value pushed */ + 16384 + DIV[ ] /* Divide */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + POP[ ] /* PopTopStack */ + PUSHB[ ] /* 1 value pushed */ + 128 + LTEQ[ ] /* LessThenOrEqual */ + IF[ ] /* If */ + GPV[ ] /* GetPVector */ + ABS[ ] /* Absolute */ + SWAP[ ] /* SwapTopStack */ + ABS[ ] /* Absolute */ + MAX[ ] /* Maximum */ + PUSHW[ ] /* 1 value pushed */ + 8192 + DIV[ ] /* Divide */ + ELSE[ ] /* Else */ + PUSHB[ ] /* 3 values pushed */ + 0 64 47 + CALL[ ] /* CallFunction */ + EIF[ ] /* EndIf */ + PUSHB[ ] /* 1 value pushed */ + 2 + ADD[ ] /* Add */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + POP[ ] /* PopTopStack */ + PUSHB[ ] /* 1 value pushed */ + 192 + LTEQ[ ] /* LessThenOrEqual */ + IF[ ] /* If */ + GPV[ ] /* GetPVector */ + ABS[ ] /* Absolute */ + SWAP[ ] /* SwapTopStack */ + ABS[ ] /* Absolute */ + MAX[ ] /* Maximum */ + PUSHW[ ] /* 1 value pushed */ + 5461 + DIV[ ] /* Divide */ + ELSE[ ] /* Else */ + PUSHB[ ] /* 3 values pushed */ + 0 128 47 + CALL[ ] /* CallFunction */ + EIF[ ] /* EndIf */ + PUSHB[ ] /* 1 value pushed */ + 2 + ADD[ ] /* Add */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + GPV[ ] /* GetPVector */ + ABS[ ] /* Absolute */ + SWAP[ ] /* SwapTopStack */ + ABS[ ] /* Absolute */ + MAX[ ] /* Maximum */ + PUSHW[ ] /* 1 value pushed */ + 16384 + DIV[ ] /* Divide */ + ADD[ ] /* Add */ + SWAP[ ] /* SwapTopStack */ + POP[ ] /* PopTopStack */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + MPPEM[ ] /* MeasurePixelPerEm */ + GTEQ[ ] /* GreaterThanOrEqual */ + IF[ ] /* If */ + PUSHB[ ] /* 1 value pushed */ + 4 + CINDEX[ ] /* CopyXToTopStack */ + PUSHB[ ] /* 1 value pushed */ + 4 + CINDEX[ ] /* CopyXToTopStack */ + MD[0] /* MeasureDistance */ + ABS[ ] /* Absolute */ + SWAP[ ] /* SwapTopStack */ + RCVT[ ] /* ReadCVT */ + ABS[ ] /* Absolute */ + ROUND[01] /* Round */ + PUSHB[ ] /* 1 value pushed */ + 64 + MAX[ ] /* Maximum */ + SUB[ ] /* Subtract */ + DUP[ ] /* DuplicateTopStack */ + PUSHB[ ] /* 1 value pushed */ + 128 + DIV[ ] /* Divide */ + ROUND[10] /* Round */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + SUB[ ] /* Subtract */ + MIN[ ] /* Minimum */ + PUSHB[ ] /* 1 value pushed */ + 70 + SWAP[ ] /* SwapTopStack */ + WCVTP[ ] /* WriteCVTInPixels */ + POP[ ] /* PopTopStack */ + ROLL[ ] /* RollTopThreeStack */ + SRP0[ ] /* SetRefPoint0 */ + PUSHB[ ] /* 1 value pushed */ + 70 + MIRP[10110] /* MoveIndirectRelPt */ + POP[ ] /* PopTopStack */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + ROLL[ ] /* RollTopThreeStack */ + SRP1[ ] /* SetRefPoint1 */ + SWAP[ ] /* SwapTopStack */ + SRP2[ ] /* SetRefPoint2 */ + DUP[ ] /* DuplicateTopStack */ + IP[ ] /* InterpolatePts */ + MDAP[1] /* MoveDirectAbsPt */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + MD[0] /* MeasureDistance */ + ABS[ ] /* Absolute */ + PUSHB[ ] /* 1 value pushed */ + 192 + EQ[ ] /* Equal */ + IF[ ] /* If */ + PUSHW[ ] /* 1 value pushed */ + -8 + SHPIX[ ] /* ShiftZoneByPixel */ + PUSHB[ ] /* 1 value pushed */ + 8 + SHPIX[ ] /* ShiftZoneByPixel */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 19 + RS[ ] /* ReadStore */ + IF[ ] /* If */ + SPVTCA[1] /* SetPVectorToAxis */ + ELSE[ ] /* Else */ + SPVTCA[0] /* SetPVectorToAxis */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 19 + RS[ ] /* ReadStore */ + IF[ ] /* If */ + SPVTCA[0] /* SetPVectorToAxis */ + ELSE[ ] /* Else */ + SPVTCA[1] /* SetPVectorToAxis */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 10 + CALL[ ] /* CallFunction */ + SWAP[ ] /* SwapTopStack */ + SRP0[ ] /* SetRefPoint0 */ + DUP[ ] /* DuplicateTopStack */ + ALIGNRP[ ] /* AlignRelativePt */ + PUSHB[ ] /* 1 value pushed */ + 23 + CALL[ ] /* CallFunction */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + PUSHW[ ] /* 1 value pushed */ + -16 + SHPIX[ ] /* ShiftZoneByPixel */ + PUSHB[ ] /* 1 value pushed */ + 40 + CALL[ ] /* CallFunction */ + ROLL[ ] /* RollTopThreeStack */ + SRP0[ ] /* SetRefPoint0 */ + SWAP[ ] /* SwapTopStack */ + DUP[ ] /* DuplicateTopStack */ + MDRP[10000] /* MoveDirectRelPt */ + SWAP[ ] /* SwapTopStack */ + PUSHB[ ] /* 1 value pushed */ + 16 + CALL[ ] /* CallFunction */ + PUSHB[ ] /* 1 value pushed */ + 5 + RS[ ] /* ReadStore */ + IF[ ] /* If */ + MDRP[00000] /* MoveDirectRelPt */ + ELSE[ ] /* Else */ + ALIGNRP[ ] /* AlignRelativePt */ + EIF[ ] /* EndIf */ + DUP[ ] /* DuplicateTopStack */ + SRP0[ ] /* SetRefPoint0 */ + SRP1[ ] /* SetRefPoint1 */ + PUSHB[ ] /* 1 value pushed */ + 0 + SRP2[ ] /* SetRefPoint2 */ + SVTCA[1] /* SetFPVectorToAxis */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + MPPEM[ ] /* MeasurePixelPerEm */ + GTEQ[ ] /* GreaterThanOrEqual */ + SWAP[ ] /* SwapTopStack */ + MPPEM[ ] /* MeasurePixelPerEm */ + LTEQ[ ] /* LessThenOrEqual */ + AND[ ] /* LogicalAnd */ + IF[ ] /* If */ + SHPIX[ ] /* ShiftZoneByPixel */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[1] /* SetFPVectorToAxis */ + PUSHB[ ] /* 1 value pushed */ + 2 + CINDEX[ ] /* CopyXToTopStack */ + SRP0[ ] /* SetRefPoint0 */ + MDRP[10000] /* MoveDirectRelPt */ + SWAP[ ] /* SwapTopStack */ + MDRP[01001] /* MoveDirectRelPt */ + SVTCA[1] /* SetFPVectorToAxis */ + PUSHB[ ] /* 1 value pushed */ + 1 + SZP0[ ] /* SetZonePointer0 */ + PUSHB[ ] /* 1 value pushed */ + 0 + SZP1[ ] /* SetZonePointer1 */ + SRP0[ ] /* SetRefPoint0 */ + PUSHB[ ] /* 1 value pushed */ + 1 + ALIGNRP[ ] /* AlignRelativePt */ + PUSHB[ ] /* 1 value pushed */ + 1 + SZPS[ ] /* SetZonePointerS */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + SVTCA[1] /* SetFPVectorToAxis */ + PUSHB[ ] /* 1 value pushed */ + 0 + SZP0[ ] /* SetZonePointer0 */ + PUSHB[ ] /* 1 value pushed */ + 1 + PUSHB[ ] /* 1 value pushed */ + 3 + CINDEX[ ] /* CopyXToTopStack */ + MD[0] /* MeasureDistance */ + PUSHB[ ] /* 1 value pushed */ + 3 + SLOOP[ ] /* SetLoopVariable */ + SHPIX[ ] /* ShiftZoneByPixel */ + PUSHB[ ] /* 1 value pushed */ + 1 + SZP0[ ] /* SetZonePointer0 */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + MPPEM[ ] /* MeasurePixelPerEm */ + GTEQ[ ] /* GreaterThanOrEqual */ + SWAP[ ] /* SwapTopStack */ + MPPEM[ ] /* MeasurePixelPerEm */ + LTEQ[ ] /* LessThenOrEqual */ + AND[ ] /* LogicalAnd */ + IF[ ] /* If */ + DUP[ ] /* DuplicateTopStack */ + RCVT[ ] /* ReadCVT */ + ROLL[ ] /* RollTopThreeStack */ + ADD[ ] /* Add */ + WCVTP[ ] /* WriteCVTInPixels */ + ELSE[ ] /* Else */ + POP[ ] /* PopTopStack */ + POP[ ] /* PopTopStack */ + EIF[ ] /* EndIf */ + ENDF[ ] /* EndFunctionDefinition */ + FDEF[ ] /* FunctionDefinition */ + DUP[ ] /* DuplicateTopStack */ + IP[ ] /* InterpolatePts */ + MDAP[1] /* MoveDirectAbsPt */ + ENDF[ ] /* EndFunctionDefinition */ + diff --git a/Tests/ttLib/tables/otBase_test.py b/Tests/ttLib/tables/otBase_test.py new file mode 100644 index 0000000..f1fa5b0 --- /dev/null +++ b/Tests/ttLib/tables/otBase_test.py @@ -0,0 +1,96 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.textTools import deHexStr +from fontTools.ttLib.tables.otBase import OTTableReader, OTTableWriter +import unittest + + +class OTTableReaderTest(unittest.TestCase): + def test_readShort(self): + reader = OTTableReader(deHexStr("CA FE")) + self.assertEqual(reader.readShort(), -13570) + self.assertEqual(reader.pos, 2) + + def test_readLong(self): + reader = OTTableReader(deHexStr("CA FE BE EF")) + self.assertEqual(reader.readLong(), -889274641) + self.assertEqual(reader.pos, 4) + + def test_readUInt8(self): + reader = OTTableReader(deHexStr("C3")) + self.assertEqual(reader.readUInt8(), 0xC3) + self.assertEqual(reader.pos, 1) + + def test_readUShort(self): + reader = OTTableReader(deHexStr("CA FE")) + self.assertEqual(reader.readUShort(), 0xCAFE) + self.assertEqual(reader.pos, 2) + + def test_readUShortArray(self): + reader = OTTableReader(deHexStr("DE AD BE EF CA FE")) + self.assertEqual(list(reader.readUShortArray(3)), + [0xDEAD, 0xBEEF, 0xCAFE]) + self.assertEqual(reader.pos, 6) + + def test_readUInt24(self): + reader = OTTableReader(deHexStr("C3 13 37")) + self.assertEqual(reader.readUInt24(), 0xC31337) + self.assertEqual(reader.pos, 3) + + def test_readULong(self): + reader = OTTableReader(deHexStr("CA FE BE EF")) + self.assertEqual(reader.readULong(), 0xCAFEBEEF) + self.assertEqual(reader.pos, 4) + + def test_readTag(self): + reader = OTTableReader(deHexStr("46 6F 6F 64")) + self.assertEqual(reader.readTag(), "Food") + self.assertEqual(reader.pos, 4) + + def test_readData(self): + reader = OTTableReader(deHexStr("48 65 6C 6C 6F")) + self.assertEqual(reader.readData(5), b"Hello") + self.assertEqual(reader.pos, 5) + + def test_getSubReader(self): + reader = OTTableReader(deHexStr("CAFE F00D")) + sub = reader.getSubReader(2) + self.assertEqual(sub.readUShort(), 0xF00D) + self.assertEqual(reader.readUShort(), 0xCAFE) + + +class OTTableWriterTest(unittest.TestCase): + def test_writeShort(self): + writer = OTTableWriter() + writer.writeShort(-12345) + self.assertEqual(writer.getData(), deHexStr("CF C7")) + + def test_writeLong(self): + writer = OTTableWriter() + writer.writeLong(-12345678) + self.assertEqual(writer.getData(), deHexStr("FF 43 9E B2")) + + def test_writeUInt8(self): + writer = OTTableWriter() + writer.writeUInt8(0xBE) + self.assertEqual(writer.getData(), deHexStr("BE")) + + def test_writeUShort(self): + writer = OTTableWriter() + writer.writeUShort(0xBEEF) + self.assertEqual(writer.getData(), deHexStr("BE EF")) + + def test_writeUInt24(self): + writer = OTTableWriter() + writer.writeUInt24(0xBEEF77) + self.assertEqual(writer.getData(), deHexStr("BE EF 77")) + + def test_writeULong(self): + writer = OTTableWriter() + writer.writeULong(0xBEEFCAFE) + self.assertEqual(writer.getData(), deHexStr("BE EF CA FE")) + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/otConverters_test.py b/Tests/ttLib/tables/otConverters_test.py new file mode 100644 index 0000000..3b9d5e9 --- /dev/null +++ b/Tests/ttLib/tables/otConverters_test.py @@ -0,0 +1,433 @@ +# coding: utf-8 +from __future__ import print_function, division, absolute_import, \ + unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc.loggingTools import CapturingLogHandler +from fontTools.misc.testTools import FakeFont, makeXMLWriter +from fontTools.misc.textTools import deHexStr +import fontTools.ttLib.tables.otConverters as otConverters +from fontTools.ttLib import newTable +from fontTools.ttLib.tables.otBase import OTTableReader, OTTableWriter +import unittest + + +class Char64Test(unittest.TestCase): + font = FakeFont([]) + converter = otConverters.Char64("char64", 0, None, None) + + def test_read(self): + reader = OTTableReader(b"Hello\0junk after zero byte" + 100 * b"\0") + self.assertEqual(self.converter.read(reader, self.font, {}), "Hello") + self.assertEqual(reader.pos, 64) + + def test_read_replace_not_ascii(self): + reader = OTTableReader(b"Hello \xE4 world" + 100 * b"\0") + with CapturingLogHandler(otConverters.log, "WARNING") as captor: + data = self.converter.read(reader, self.font, {}) + self.assertEqual(data, "Hello � world") + self.assertEqual(reader.pos, 64) + self.assertIn('replaced non-ASCII characters in "Hello � world"', + [r.msg for r in captor.records]) + + def test_write(self): + writer = OTTableWriter() + self.converter.write(writer, self.font, {}, "Hello world") + self.assertEqual(writer.getData(), b"Hello world" + 53 * b"\0") + + def test_write_replace_not_ascii(self): + writer = OTTableWriter() + with CapturingLogHandler(otConverters.log, "WARNING") as captor: + self.converter.write(writer, self.font, {}, "Hello ☃") + self.assertEqual(writer.getData(), b"Hello ?" + 57 * b"\0") + self.assertIn('replacing non-ASCII characters in "Hello ☃"', + [r.msg for r in captor.records]) + + def test_write_truncated(self): + writer = OTTableWriter() + with CapturingLogHandler(otConverters.log, "WARNING") as captor: + self.converter.write(writer, self.font, {}, "A" * 80) + self.assertEqual(writer.getData(), b"A" * 64) + self.assertIn('truncating overlong "' + "A" * 80 + '" to 64 bytes', + [r.msg for r in captor.records]) + + def test_xmlRead(self): + value = self.converter.xmlRead({"value": "Foo"}, [], self.font) + self.assertEqual(value, "Foo") + + def test_xmlWrite(self): + writer = makeXMLWriter() + self.converter.xmlWrite(writer, self.font, "Hello world", "Element", + [("attr", "v")]) + xml = writer.file.getvalue().decode("utf-8").rstrip() + self.assertEqual(xml, '') + + +class GlyphIDTest(unittest.TestCase): + font = FakeFont(".notdef A B C".split()) + converter = otConverters.GlyphID('GlyphID', 0, None, None) + + def test_readArray(self): + reader = OTTableReader(deHexStr("0002 0001 DEAD 0002")) + self.assertEqual(self.converter.readArray(reader, self.font, {}, 4), + ["B", "A", "glyph57005", "B"]) + self.assertEqual(reader.pos, 8) + + def test_read(self): + reader = OTTableReader(deHexStr("0003")) + self.assertEqual(self.converter.read(reader, self.font, {}), "C") + self.assertEqual(reader.pos, 2) + + def test_write(self): + writer = OTTableWriter() + self.converter.write(writer, self.font, {}, "B") + self.assertEqual(writer.getData(), deHexStr("0002")) + + +class LongTest(unittest.TestCase): + font = FakeFont([]) + converter = otConverters.Long('Long', 0, None, None) + + def test_read(self): + reader = OTTableReader(deHexStr("FF0000EE")) + self.assertEqual(self.converter.read(reader, self.font, {}), -16776978) + self.assertEqual(reader.pos, 4) + + def test_write(self): + writer = OTTableWriter() + self.converter.write(writer, self.font, {}, -16777213) + self.assertEqual(writer.getData(), deHexStr("FF000003")) + + def test_xmlRead(self): + value = self.converter.xmlRead({"value": "314159"}, [], self.font) + self.assertEqual(value, 314159) + + def test_xmlWrite(self): + writer = makeXMLWriter() + self.converter.xmlWrite(writer, self.font, 291, "Foo", [("attr", "v")]) + xml = writer.file.getvalue().decode("utf-8").rstrip() + self.assertEqual(xml, '') + + +class NameIDTest(unittest.TestCase): + converter = otConverters.NameID('NameID', 0, None, None) + + def makeFont(self): + nameTable = newTable('name') + nameTable.setName(u"Demibold Condensed", 0x123, 3, 0, 0x409) + nameTable.setName(u"Copyright 2018", 0, 3, 0, 0x409) + return {"name": nameTable} + + def test_read(self): + font = self.makeFont() + reader = OTTableReader(deHexStr("0123")) + self.assertEqual(self.converter.read(reader, font, {}), 0x123) + + def test_write(self): + writer = OTTableWriter() + self.converter.write(writer, self.makeFont(), {}, 0x123) + self.assertEqual(writer.getData(), deHexStr("0123")) + + def test_xmlWrite(self): + writer = makeXMLWriter() + self.converter.xmlWrite(writer, self.makeFont(), 291, + "FooNameID", [("attr", "val")]) + xml = writer.file.getvalue().decode("utf-8").rstrip() + self.assertEqual( + xml, + ' ') + + def test_xmlWrite_missingID(self): + writer = makeXMLWriter() + with CapturingLogHandler(otConverters.log, "WARNING") as captor: + self.converter.xmlWrite(writer, self.makeFont(), 666, + "Entity", [("attrib", "val")]) + self.assertIn("name id 666 missing from name table", + [r.msg for r in captor.records]) + xml = writer.file.getvalue().decode("utf-8").rstrip() + self.assertEqual( + xml, + ' ') + + def test_xmlWrite_NULL(self): + writer = makeXMLWriter() + self.converter.xmlWrite(writer, self.makeFont(), 0, + "FooNameID", [("attr", "val")]) + xml = writer.file.getvalue().decode("utf-8").rstrip() + self.assertEqual( + xml, '') + + +class UInt8Test(unittest.TestCase): + font = FakeFont([]) + converter = otConverters.UInt8("UInt8", 0, None, None) + + def test_read(self): + reader = OTTableReader(deHexStr("FE")) + self.assertEqual(self.converter.read(reader, self.font, {}), 254) + self.assertEqual(reader.pos, 1) + + def test_write(self): + writer = OTTableWriter() + self.converter.write(writer, self.font, {}, 253) + self.assertEqual(writer.getData(), deHexStr("FD")) + + def test_xmlRead(self): + value = self.converter.xmlRead({"value": "254"}, [], self.font) + self.assertEqual(value, 254) + + def test_xmlWrite(self): + writer = makeXMLWriter() + self.converter.xmlWrite(writer, self.font, 251, "Foo", [("attr", "v")]) + xml = writer.file.getvalue().decode("utf-8").rstrip() + self.assertEqual(xml, '') + + +class AATLookupTest(unittest.TestCase): + font = FakeFont(".notdef A B C D E F G H A.alt B.alt".split()) + converter = otConverters.AATLookup("AATLookup", 0, None, + tableClass=otConverters.GlyphID) + + def __init__(self, methodName): + unittest.TestCase.__init__(self, methodName) + # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, + # and fires deprecation warnings if a program uses the old name. + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + + def test_readFormat0(self): + reader = OTTableReader(deHexStr("0000 0000 0001 0002 0000 7D00 0001")) + self.assertEqual(self.converter.read(reader, self.font, None), { + ".notdef": ".notdef", + "A": "A", + "B": "B", + "C": ".notdef", + "D": "glyph32000", + "E": "A" + }) + + def test_readFormat2(self): + reader = OTTableReader(deHexStr( + "0002 0006 0002 000C 0001 0006 " + "0002 0001 0003 " # glyph A..B: map to C + "0007 0005 0008 " # glyph E..G: map to H + "FFFF FFFF FFFF")) # end of search table + self.assertEqual(self.converter.read(reader, self.font, None), { + "A": "C", + "B": "C", + "E": "H", + "F": "H", + "G": "H", + }) + + def test_readFormat4(self): + reader = OTTableReader(deHexStr( + "0004 0006 0003 000C 0001 0006 " + "0002 0001 001E " # glyph 1..2: mapping at offset 0x1E + "0005 0004 001E " # glyph 4..5: mapping at offset 0x1E + "FFFF FFFF FFFF " # end of search table + "0007 0008")) # offset 0x18: glyphs [7, 8] = [G, H] + self.assertEqual(self.converter.read(reader, self.font, None), { + "A": "G", + "B": "H", + "D": "G", + "E": "H", + }) + + def test_readFormat6(self): + reader = OTTableReader(deHexStr( + "0006 0004 0002 0008 0001 0004 " + "0003 0001 " # C --> A + "0005 0002 " # E --> B + "FFFF FFFF")) # end of search table + self.assertEqual(self.converter.read(reader, self.font, None), { + "C": "A", + "E": "B", + }) + + def test_readFormat8(self): + reader = OTTableReader(deHexStr( + "0008 " + "0003 0003 " # first: C, count: 3 + "0007 0001 0002")) # [G, A, B] + self.assertEqual(self.converter.read(reader, self.font, None), { + "C": "G", + "D": "A", + "E": "B", + }) + + def test_readUnknownFormat(self): + reader = OTTableReader(deHexStr("0009")) + self.assertRaisesRegex( + AssertionError, + "unsupported lookup format: 9", + self.converter.read, reader, self.font, None) + + def test_writeFormat0(self): + writer = OTTableWriter() + font = FakeFont(".notdef A B C".split()) + self.converter.write(writer, font, {}, { + ".notdef": ".notdef", + "A": "C", + "B": "C", + "C": "A" + }) + self.assertEqual(writer.getData(), deHexStr("0000 0000 0003 0003 0001")) + + def test_writeFormat2(self): + writer = OTTableWriter() + font = FakeFont(".notdef A B C D E F G H".split()) + self.converter.write(writer, font, {}, { + "B": "C", + "C": "C", + "D": "C", + "E": "C", + "G": "A", + "H": "A", + }) + self.assertEqual(writer.getData(), deHexStr( + "0002 " # format=2 + "0006 " # binSrchHeader.unitSize=6 + "0002 " # binSrchHeader.nUnits=2 + "000C " # binSrchHeader.searchRange=12 + "0001 " # binSrchHeader.entrySelector=1 + "0000 " # binSrchHeader.rangeShift=0 + "0005 0002 0003 " # segments[0].lastGlyph=E, firstGlyph=B, value=C + "0008 0007 0001 " # segments[1].lastGlyph=H, firstGlyph=G, value=A + "FFFF FFFF 0000 " # segments[2]= + )) + + def test_writeFormat6(self): + writer = OTTableWriter() + font = FakeFont(".notdef A B C D E".split()) + self.converter.write(writer, font, {}, { + "A": "C", + "C": "B", + "D": "D", + "E": "E", + }) + self.assertEqual(writer.getData(), deHexStr( + "0006 " # format=6 + "0004 " # binSrchHeader.unitSize=4 + "0004 " # binSrchHeader.nUnits=4 + "0010 " # binSrchHeader.searchRange=16 + "0002 " # binSrchHeader.entrySelector=2 + "0000 " # binSrchHeader.rangeShift=0 + "0001 0003 " # entries[0].glyph=A, .value=C + "0003 0002 " # entries[1].glyph=C, .value=B + "0004 0004 " # entries[2].glyph=D, .value=D + "0005 0005 " # entries[3].glyph=E, .value=E + "FFFF 0000 " # entries[4]= + )) + + def test_writeFormat8(self): + writer = OTTableWriter() + font = FakeFont(".notdef A B C D E F G H".split()) + self.converter.write(writer, font, {}, { + "B": "B", + "C": "A", + "D": "B", + "E": "C", + "F": "B", + "G": "A", + }) + self.assertEqual(writer.getData(), deHexStr( + "0008 " # format=8 + "0002 " # firstGlyph=B + "0006 " # glyphCount=6 + "0002 0001 0002 0003 0002 0001" # valueArray=[B, A, B, C, B, A] + )) + + def test_xmlRead(self): + value = self.converter.xmlRead({}, [ + ("Lookup", {"glyph": "A", "value": "A.alt"}, []), + ("Lookup", {"glyph": "B", "value": "B.alt"}, []), + ], self.font) + self.assertEqual(value, {"A": "A.alt", "B": "B.alt"}) + + def test_xmlWrite(self): + writer = makeXMLWriter() + self.converter.xmlWrite(writer, self.font, + value={"A": "A.alt", "B": "B.alt"}, + name="Foo", attrs=[("attr", "val")]) + xml = writer.file.getvalue().decode("utf-8").splitlines() + self.assertEqual(xml, [ + '', + ' ', + ' ', + '', + ]) + + +class LazyListTest(unittest.TestCase): + + def test_slice(self): + ll = otConverters._LazyList([10, 11, 12, 13]) + sl = ll[:] + + self.assertIsNot(sl, ll) + self.assertIsInstance(sl, list) + self.assertEqual([10, 11, 12, 13], sl) + + self.assertEqual([11, 12], ll[1:3]) + + def test_getitem(self): + count = 2 + reader = OTTableReader(b"\x00\xFE\xFF\x00\x00\x00", offset=1) + converter = otConverters.UInt8("UInt8", 0, None, None) + recordSize = converter.staticSize + l = otConverters._LazyList() + l.reader = reader + l.pos = l.reader.pos + l.font = None + l.conv = converter + l.recordSize = recordSize + l.extend(otConverters._MissingItem([i]) for i in range(count)) + reader.advance(count * recordSize) + + self.assertEqual(l[0], 254) + self.assertEqual(l[1], 255) + + def test_add_both_LazyList(self): + ll1 = otConverters._LazyList([1]) + ll2 = otConverters._LazyList([2]) + + l3 = ll1 + ll2 + + self.assertIsInstance(l3, list) + self.assertEqual([1, 2], l3) + + def test_add_LazyList_and_list(self): + ll1 = otConverters._LazyList([1]) + l2 = [2] + + l3 = ll1 + l2 + + self.assertIsInstance(l3, list) + self.assertEqual([1, 2], l3) + + def test_add_not_implemented(self): + with self.assertRaises(TypeError): + otConverters._LazyList() + 0 + with self.assertRaises(TypeError): + otConverters._LazyList() + tuple() + + def test_radd_list_and_LazyList(self): + l1 = [1] + ll2 = otConverters._LazyList([2]) + + l3 = l1 + ll2 + + self.assertIsInstance(l3, list) + self.assertEqual([1, 2], l3) + + def test_radd_not_implemented(self): + with self.assertRaises(TypeError): + 0 + otConverters._LazyList() + with self.assertRaises(TypeError): + tuple() + otConverters._LazyList() + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/otTables_test.py b/Tests/ttLib/tables/otTables_test.py new file mode 100644 index 0000000..e02b22f --- /dev/null +++ b/Tests/ttLib/tables/otTables_test.py @@ -0,0 +1,490 @@ +# coding: utf-8 +from __future__ import print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +from fontTools.misc.testTools import getXML, parseXML, FakeFont +from fontTools.misc.textTools import deHexStr, hexStr +from fontTools.misc.xmlWriter import XMLWriter +from fontTools.ttLib.tables.otBase import OTTableReader, OTTableWriter +import fontTools.ttLib.tables.otTables as otTables +import unittest + + +def makeCoverage(glyphs): + coverage = otTables.Coverage() + coverage.glyphs = glyphs + return coverage + + +class SingleSubstTest(unittest.TestCase): + def setUp(self): + self.glyphs = ".notdef A B C D E a b c d e".split() + self.font = FakeFont(self.glyphs) + + def test_postRead_format1(self): + table = otTables.SingleSubst() + table.Format = 1 + rawTable = { + "Coverage": makeCoverage(["A", "B", "C"]), + "DeltaGlyphID": 5 + } + table.postRead(rawTable, self.font) + self.assertEqual(table.mapping, {"A": "a", "B": "b", "C": "c"}) + + def test_postRead_format2(self): + table = otTables.SingleSubst() + table.Format = 2 + rawTable = { + "Coverage": makeCoverage(["A", "B", "C"]), + "GlyphCount": 3, + "Substitute": ["c", "b", "a"] + } + table.postRead(rawTable, self.font) + self.assertEqual(table.mapping, {"A": "c", "B": "b", "C": "a"}) + + def test_postRead_formatUnknown(self): + table = otTables.SingleSubst() + table.Format = 987 + rawTable = {"Coverage": makeCoverage(["A", "B", "C"])} + self.assertRaises(AssertionError, table.postRead, rawTable, self.font) + + def test_preWrite_format1(self): + table = otTables.SingleSubst() + table.mapping = {"A": "a", "B": "b", "C": "c"} + rawTable = table.preWrite(self.font) + self.assertEqual(table.Format, 1) + self.assertEqual(rawTable["Coverage"].glyphs, ["A", "B", "C"]) + self.assertEqual(rawTable["DeltaGlyphID"], 5) + + def test_preWrite_format2(self): + table = otTables.SingleSubst() + table.mapping = {"A": "c", "B": "b", "C": "a"} + rawTable = table.preWrite(self.font) + self.assertEqual(table.Format, 2) + self.assertEqual(rawTable["Coverage"].glyphs, ["A", "B", "C"]) + self.assertEqual(rawTable["Substitute"], ["c", "b", "a"]) + + def test_preWrite_emptyMapping(self): + table = otTables.SingleSubst() + table.mapping = {} + rawTable = table.preWrite(self.font) + self.assertEqual(table.Format, 2) + self.assertEqual(rawTable["Coverage"].glyphs, []) + self.assertEqual(rawTable["Substitute"], []) + + def test_toXML2(self): + writer = XMLWriter(StringIO()) + table = otTables.SingleSubst() + table.mapping = {"A": "a", "B": "b", "C": "c"} + table.toXML2(writer, self.font) + self.assertEqual(writer.file.getvalue().splitlines()[1:], [ + '', + '', + '', + ]) + + def test_fromXML(self): + table = otTables.SingleSubst() + for name, attrs, content in parseXML( + '' + '' + ''): + table.fromXML(name, attrs, content, self.font) + self.assertEqual(table.mapping, {"A": "a", "B": "b", "C": "c"}) + + +class MultipleSubstTest(unittest.TestCase): + def setUp(self): + self.glyphs = ".notdef c f i t c_t f_f_i".split() + self.font = FakeFont(self.glyphs) + + def test_postRead_format1(self): + makeSequence = otTables.MultipleSubst.makeSequence_ + table = otTables.MultipleSubst() + table.Format = 1 + rawTable = { + "Coverage": makeCoverage(["c_t", "f_f_i"]), + "Sequence": [ + makeSequence(["c", "t"]), + makeSequence(["f", "f", "i"]) + ] + } + table.postRead(rawTable, self.font) + self.assertEqual(table.mapping, { + "c_t": ["c", "t"], + "f_f_i": ["f", "f", "i"] + }) + + def test_postRead_formatUnknown(self): + table = otTables.MultipleSubst() + table.Format = 987 + self.assertRaises(AssertionError, table.postRead, {}, self.font) + + def test_preWrite_format1(self): + table = otTables.MultipleSubst() + table.mapping = {"c_t": ["c", "t"], "f_f_i": ["f", "f", "i"]} + rawTable = table.preWrite(self.font) + self.assertEqual(table.Format, 1) + self.assertEqual(rawTable["Coverage"].glyphs, ["c_t", "f_f_i"]) + + def test_toXML2(self): + writer = XMLWriter(StringIO()) + table = otTables.MultipleSubst() + table.mapping = {"c_t": ["c", "t"], "f_f_i": ["f", "f", "i"]} + table.toXML2(writer, self.font) + self.assertEqual(writer.file.getvalue().splitlines()[1:], [ + '', + '', + ]) + + def test_fromXML(self): + table = otTables.MultipleSubst() + for name, attrs, content in parseXML( + '' + ''): + table.fromXML(name, attrs, content, self.font) + self.assertEqual(table.mapping, + {'c_t': ['c', 't'], 'f_f_i': ['f', 'f', 'i']}) + + def test_fromXML_oldFormat(self): + table = otTables.MultipleSubst() + for name, attrs, content in parseXML( + '' + ' ' + ' ' + '' + '' + ' ' + ' ' + '' + '' + ' ' + ' ' + ' ' + ''): + table.fromXML(name, attrs, content, self.font) + self.assertEqual(table.mapping, + {'c_t': ['c', 't'], 'f_f_i': ['f', 'f', 'i']}) + + def test_fromXML_oldFormat_bug385(self): + # https://github.com/behdad/fonttools/issues/385 + table = otTables.MultipleSubst() + table.Format = 1 + for name, attrs, content in parseXML( + '' + ' ' + ' ' + '' + '' + ' ' + ' ' + ' ' + '' + '' + ' ' + ''): + table.fromXML(name, attrs, content, self.font) + self.assertEqual(table.mapping, + {'o': ['o', 'l', 'o'], 'l': ['o']}) + + +class LigatureSubstTest(unittest.TestCase): + def setUp(self): + self.glyphs = ".notdef c f i t c_t f_f f_i f_f_i".split() + self.font = FakeFont(self.glyphs) + + def makeLigature(self, s): + """'ffi' --> Ligature(LigGlyph='f_f_i', Component=['f', 'f', 'i'])""" + lig = otTables.Ligature() + lig.Component = list(s) + lig.LigGlyph = "_".join(lig.Component) + return lig + + def makeLigatures(self, s): + """'ffi fi' --> [otTables.Ligature, otTables.Ligature]""" + return [self.makeLigature(lig) for lig in s.split()] + + def test_postRead_format1(self): + table = otTables.LigatureSubst() + table.Format = 1 + ligs_c = otTables.LigatureSet() + ligs_c.Ligature = self.makeLigatures("ct") + ligs_f = otTables.LigatureSet() + ligs_f.Ligature = self.makeLigatures("ffi ff fi") + rawTable = { + "Coverage": makeCoverage(["c", "f"]), + "LigatureSet": [ligs_c, ligs_f] + } + table.postRead(rawTable, self.font) + self.assertEqual(set(table.ligatures.keys()), {"c", "f"}) + self.assertEqual(len(table.ligatures["c"]), 1) + self.assertEqual(table.ligatures["c"][0].LigGlyph, "c_t") + self.assertEqual(table.ligatures["c"][0].Component, ["c", "t"]) + self.assertEqual(len(table.ligatures["f"]), 3) + self.assertEqual(table.ligatures["f"][0].LigGlyph, "f_f_i") + self.assertEqual(table.ligatures["f"][0].Component, ["f", "f", "i"]) + self.assertEqual(table.ligatures["f"][1].LigGlyph, "f_f") + self.assertEqual(table.ligatures["f"][1].Component, ["f", "f"]) + self.assertEqual(table.ligatures["f"][2].LigGlyph, "f_i") + self.assertEqual(table.ligatures["f"][2].Component, ["f", "i"]) + + def test_postRead_formatUnknown(self): + table = otTables.LigatureSubst() + table.Format = 987 + rawTable = {"Coverage": makeCoverage(["f"])} + self.assertRaises(AssertionError, table.postRead, rawTable, self.font) + + def test_preWrite_format1(self): + table = otTables.LigatureSubst() + table.ligatures = { + "c": self.makeLigatures("ct"), + "f": self.makeLigatures("ffi ff fi") + } + rawTable = table.preWrite(self.font) + self.assertEqual(table.Format, 1) + self.assertEqual(rawTable["Coverage"].glyphs, ["c", "f"]) + [c, f] = rawTable["LigatureSet"] + self.assertIsInstance(c, otTables.LigatureSet) + self.assertIsInstance(f, otTables.LigatureSet) + [ct] = c.Ligature + self.assertIsInstance(ct, otTables.Ligature) + self.assertEqual(ct.LigGlyph, "c_t") + self.assertEqual(ct.Component, ["c", "t"]) + [ffi, ff, fi] = f.Ligature + self.assertIsInstance(ffi, otTables.Ligature) + self.assertEqual(ffi.LigGlyph, "f_f_i") + self.assertEqual(ffi.Component, ["f", "f", "i"]) + self.assertIsInstance(ff, otTables.Ligature) + self.assertEqual(ff.LigGlyph, "f_f") + self.assertEqual(ff.Component, ["f", "f"]) + self.assertIsInstance(fi, otTables.Ligature) + self.assertEqual(fi.LigGlyph, "f_i") + self.assertEqual(fi.Component, ["f", "i"]) + + def test_toXML2(self): + writer = XMLWriter(StringIO()) + table = otTables.LigatureSubst() + table.ligatures = { + "c": self.makeLigatures("ct"), + "f": self.makeLigatures("ffi ff fi") + } + table.toXML2(writer, self.font) + self.assertEqual(writer.file.getvalue().splitlines()[1:], [ + '', + ' ', + '', + '', + ' ', + ' ', + ' ', + '' + ]) + + def test_fromXML(self): + table = otTables.LigatureSubst() + for name, attrs, content in parseXML( + '' + ' ' + ' ' + ''): + table.fromXML(name, attrs, content, self.font) + self.assertEqual(set(table.ligatures.keys()), {"f"}) + [ffi, ff] = table.ligatures["f"] + self.assertEqual(ffi.LigGlyph, "f_f_i") + self.assertEqual(ffi.Component, ["f", "f", "i"]) + self.assertEqual(ff.LigGlyph, "f_f") + self.assertEqual(ff.Component, ["f", "f"]) + + +class AlternateSubstTest(unittest.TestCase): + def setUp(self): + self.glyphs = ".notdef G G.alt1 G.alt2 Z Z.fina".split() + self.font = FakeFont(self.glyphs) + + def makeAlternateSet(self, s): + result = otTables.AlternateSet() + result.Alternate = s.split() + return result + + def test_postRead_format1(self): + table = otTables.AlternateSubst() + table.Format = 1 + rawTable = { + "Coverage": makeCoverage(["G", "Z"]), + "AlternateSet": [ + self.makeAlternateSet("G.alt2 G.alt1"), + self.makeAlternateSet("Z.fina") + ] + } + table.postRead(rawTable, self.font) + self.assertEqual(table.alternates, { + "G": ["G.alt2", "G.alt1"], + "Z": ["Z.fina"] + }) + + def test_postRead_formatUnknown(self): + table = otTables.AlternateSubst() + table.Format = 987 + self.assertRaises(AssertionError, table.postRead, {}, self.font) + + def test_preWrite_format1(self): + table = otTables.AlternateSubst() + table.alternates = {"G": ["G.alt2", "G.alt1"], "Z": ["Z.fina"]} + rawTable = table.preWrite(self.font) + self.assertEqual(table.Format, 1) + self.assertEqual(rawTable["Coverage"].glyphs, ["G", "Z"]) + [g, z] = rawTable["AlternateSet"] + self.assertIsInstance(g, otTables.AlternateSet) + self.assertEqual(g.Alternate, ["G.alt2", "G.alt1"]) + self.assertIsInstance(z, otTables.AlternateSet) + self.assertEqual(z.Alternate, ["Z.fina"]) + + def test_toXML2(self): + writer = XMLWriter(StringIO()) + table = otTables.AlternateSubst() + table.alternates = {"G": ["G.alt2", "G.alt1"], "Z": ["Z.fina"]} + table.toXML2(writer, self.font) + self.assertEqual(writer.file.getvalue().splitlines()[1:], [ + '', + ' ', + ' ', + '', + '', + ' ', + '' + ]) + + def test_fromXML(self): + table = otTables.AlternateSubst() + for name, attrs, content in parseXML( + '' + ' ' + ' ' + '' + '' + ' ' + ''): + table.fromXML(name, attrs, content, self.font) + self.assertEqual(table.alternates, { + "G": ["G.alt2", "G.alt1"], + "Z": ["Z.fina"] + }) + + +class RearrangementMorphActionTest(unittest.TestCase): + def setUp(self): + self.font = FakeFont(['.notdef', 'A', 'B', 'C']) + + def testCompile(self): + r = otTables.RearrangementMorphAction() + r.NewState = 0x1234 + r.MarkFirst = r.DontAdvance = r.MarkLast = True + r.ReservedFlags, r.Verb = 0x1FF0, 0xD + writer = OTTableWriter() + r.compile(writer, self.font, actionIndex=None) + self.assertEqual(hexStr(writer.getAllData()), "1234fffd") + + def testDecompileToXML(self): + r = otTables.RearrangementMorphAction() + r.decompile(OTTableReader(deHexStr("1234fffd")), + self.font, actionReader=None) + toXML = lambda w, f: r.toXML(w, f, {"Test": "Foo"}, "Transition") + self.assertEqual(getXML(toXML, self.font), [ + '', + ' ', # 0x1234 = 4660 + ' ', + ' ', + ' ', + '', + ]) + + +class ContextualMorphActionTest(unittest.TestCase): + def setUp(self): + self.font = FakeFont(['.notdef', 'A', 'B', 'C']) + + def testCompile(self): + a = otTables.ContextualMorphAction() + a.NewState = 0x1234 + a.SetMark, a.DontAdvance, a.ReservedFlags = True, True, 0x3117 + a.MarkIndex, a.CurrentIndex = 0xDEAD, 0xBEEF + writer = OTTableWriter() + a.compile(writer, self.font, actionIndex=None) + self.assertEqual(hexStr(writer.getAllData()), "1234f117deadbeef") + + def testDecompileToXML(self): + a = otTables.ContextualMorphAction() + a.decompile(OTTableReader(deHexStr("1234f117deadbeef")), + self.font, actionReader=None) + toXML = lambda w, f: a.toXML(w, f, {"Test": "Foo"}, "Transition") + self.assertEqual(getXML(toXML, self.font), [ + '', + ' ', # 0x1234 = 4660 + ' ', + ' ', + ' ', # 0xDEAD = 57005 + ' ', # 0xBEEF = 48879 + '', + ]) + + +class LigatureMorphActionTest(unittest.TestCase): + def setUp(self): + self.font = FakeFont(['.notdef', 'A', 'B', 'C']) + + def testDecompileToXML(self): + a = otTables.LigatureMorphAction() + actionReader = OTTableReader(deHexStr("DEADBEEF 7FFFFFFE 80000003")) + a.decompile(OTTableReader(deHexStr("1234FAB30001")), + self.font, actionReader) + toXML = lambda w, f: a.toXML(w, f, {"Test": "Foo"}, "Transition") + self.assertEqual(getXML(toXML, self.font), [ + '', + ' ', # 0x1234 = 4660 + ' ', + ' ', + ' ', + ' ', + '', + ]) + + +class InsertionMorphActionTest(unittest.TestCase): + MORPH_ACTION_XML = [ + '', + ' ', # 0x1234 = 4660 + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '' + ] + + def setUp(self): + self.font = FakeFont(['.notdef', 'A', 'B', 'C', 'D']) + self.maxDiff = None + + def testDecompileToXML(self): + a = otTables.InsertionMorphAction() + actionReader = OTTableReader( + deHexStr("DEAD BEEF 0002 0001 0004 0002 0003 DEAD BEEF")) + a.decompile(OTTableReader(deHexStr("1234 FC43 0005 0002")), + self.font, actionReader) + toXML = lambda w, f: a.toXML(w, f, {"Test": "Foo"}, "Transition") + self.assertEqual(getXML(toXML, self.font), self.MORPH_ACTION_XML) + + def testCompileFromXML(self): + a = otTables.InsertionMorphAction() + for name, attrs, content in parseXML(self.MORPH_ACTION_XML): + a.fromXML(name, attrs, content, self.font) + writer = OTTableWriter() + a.compile(writer, self.font, + actionIndex={('B', 'C'): 9, ('B', 'A', 'D'): 7}) + self.assertEqual(hexStr(writer.getAllData()), "1234fc4300090007") + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/tables/tables_test.py b/Tests/ttLib/tables/tables_test.py new file mode 100644 index 0000000..9d03814 --- /dev/null +++ b/Tests/ttLib/tables/tables_test.py @@ -0,0 +1,329 @@ +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.misc.py23 import * +from fontTools.ttLib import TTFont, tagToXML +import os +import sys +import re +import contextlib +import pytest + +try: + import unicodedata2 +except ImportError: + if sys.version_info[:2] < (3, 6): + unicodedata2 = None + else: + # on 3.6 the built-in unicodedata is the same as unicodedata2 backport + import unicodedata + unicodedata2 = unicodedata + + +# Font files in data/*.{o,t}tf; output gets compared to data/*.ttx.* +TESTS = { + "aots/base.otf": ('CFF ', 'cmap', 'head', + 'hhea', 'hmtx', 'maxp', + 'name', 'OS/2', 'post'), + "aots/classdef1_font1.otf": ('GSUB',), + "aots/classdef1_font2.otf": ('GSUB',), + "aots/classdef1_font3.otf": ('GSUB',), + "aots/classdef1_font4.otf": ('GSUB',), + "aots/classdef2_font1.otf": ('GSUB',), + "aots/classdef2_font2.otf": ('GSUB',), + "aots/classdef2_font3.otf": ('GSUB',), + "aots/classdef2_font4.otf": ('GSUB',), + "aots/cmap0_font1.otf": ('cmap',), + "aots/cmap10_font1.otf": ('cmap',), + "aots/cmap10_font2.otf": ('cmap',), + "aots/cmap12_font1.otf": ('cmap',), + "aots/cmap14_font1.otf": ('cmap',), + "aots/cmap2_font1.otf": ('cmap',), + "aots/cmap4_font1.otf": ('cmap',), + "aots/cmap4_font2.otf": ('cmap',), + "aots/cmap4_font3.otf": ('cmap',), + "aots/cmap4_font4.otf": ('cmap',), + "aots/cmap6_font1.otf": ('cmap',), + "aots/cmap6_font2.otf": ('cmap',), + "aots/cmap8_font1.otf": ('cmap',), + "aots/cmap_composition_font1.otf": ('cmap',), + "aots/cmap_subtableselection_font1.otf": ('cmap',), + "aots/cmap_subtableselection_font2.otf": ('cmap',), + "aots/cmap_subtableselection_font3.otf": ('cmap',), + "aots/cmap_subtableselection_font4.otf": ('cmap',), + "aots/cmap_subtableselection_font5.otf": ('cmap',), + "aots/gpos1_1_lookupflag_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos1_1_simple_f1.otf": ('GPOS',), + "aots/gpos1_1_simple_f2.otf": ('GPOS',), + "aots/gpos1_1_simple_f3.otf": ('GPOS',), + "aots/gpos1_1_simple_f4.otf": ('GPOS',), + "aots/gpos1_2_font1.otf": ('GPOS',), + "aots/gpos1_2_font2.otf": ('GDEF', 'GPOS'), + "aots/gpos2_1_font6.otf": ('GPOS',), + "aots/gpos2_1_font7.otf": ('GPOS',), + "aots/gpos2_1_lookupflag_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos2_1_lookupflag_f2.otf": ('GDEF', 'GPOS'), + "aots/gpos2_1_next_glyph_f1.otf": ('GPOS',), + "aots/gpos2_1_next_glyph_f2.otf": ('GPOS',), + "aots/gpos2_1_simple_f1.otf": ('GPOS',), + "aots/gpos2_2_font1.otf": ('GPOS',), + "aots/gpos2_2_font2.otf": ('GDEF', 'GPOS'), + "aots/gpos2_2_font3.otf": ('GDEF', 'GPOS'), + "aots/gpos2_2_font4.otf": ('GPOS',), + "aots/gpos2_2_font5.otf": ('GPOS',), + "aots/gpos3_font1.otf": ('GPOS',), + "aots/gpos3_font2.otf": ('GDEF', 'GPOS'), + "aots/gpos3_font3.otf": ('GDEF', 'GPOS'), + "aots/gpos4_lookupflag_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos4_lookupflag_f2.otf": ('GDEF', 'GPOS'), + "aots/gpos4_multiple_anchors_1.otf": ('GDEF', 'GPOS'), + "aots/gpos4_simple_1.otf": ('GDEF', 'GPOS'), + "aots/gpos5_font1.otf": ('GDEF', 'GPOS', 'GSUB'), + "aots/gpos6_font1.otf": ('GDEF', 'GPOS'), + "aots/gpos7_1_font1.otf": ('GPOS',), + "aots/gpos9_font1.otf": ('GPOS',), + "aots/gpos9_font2.otf": ('GPOS',), + "aots/gpos_chaining1_boundary_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining1_boundary_f2.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining1_boundary_f3.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining1_boundary_f4.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining1_lookupflag_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining1_multiple_subrules_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining1_multiple_subrules_f2.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining1_next_glyph_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining1_simple_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining1_simple_f2.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining1_successive_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining2_boundary_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining2_boundary_f2.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining2_boundary_f3.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining2_boundary_f4.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining2_lookupflag_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining2_multiple_subrules_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining2_multiple_subrules_f2.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining2_next_glyph_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining2_simple_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining2_simple_f2.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining2_successive_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining3_boundary_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining3_boundary_f2.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining3_boundary_f3.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining3_boundary_f4.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining3_lookupflag_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining3_next_glyph_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining3_simple_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining3_simple_f2.otf": ('GDEF', 'GPOS'), + "aots/gpos_chaining3_successive_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_context1_boundary_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_context1_boundary_f2.otf": ('GDEF', 'GPOS'), + "aots/gpos_context1_expansion_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_context1_lookupflag_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_context1_lookupflag_f2.otf": ('GDEF', 'GPOS'), + "aots/gpos_context1_multiple_subrules_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_context1_multiple_subrules_f2.otf": ('GDEF', 'GPOS'), + "aots/gpos_context1_next_glyph_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_context1_simple_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_context1_simple_f2.otf": ('GDEF', 'GPOS'), + "aots/gpos_context1_successive_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_context2_boundary_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_context2_boundary_f2.otf": ('GDEF', 'GPOS'), + "aots/gpos_context2_classes_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_context2_classes_f2.otf": ('GDEF', 'GPOS'), + "aots/gpos_context2_expansion_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_context2_lookupflag_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_context2_lookupflag_f2.otf": ('GDEF', 'GPOS'), + "aots/gpos_context2_multiple_subrules_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_context2_multiple_subrules_f2.otf": ('GDEF', 'GPOS'), + "aots/gpos_context2_next_glyph_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_context2_simple_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_context2_simple_f2.otf": ('GDEF', 'GPOS'), + "aots/gpos_context2_successive_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_context3_boundary_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_context3_boundary_f2.otf": ('GDEF', 'GPOS'), + "aots/gpos_context3_lookupflag_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_context3_lookupflag_f2.otf": ('GDEF', 'GPOS'), + "aots/gpos_context3_next_glyph_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_context3_simple_f1.otf": ('GDEF', 'GPOS'), + "aots/gpos_context3_successive_f1.otf": ('GDEF', 'GPOS'), + "aots/gsub1_1_lookupflag_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub1_1_modulo_f1.otf": ('GSUB',), + "aots/gsub1_1_simple_f1.otf": ('GSUB',), + "aots/gsub1_2_lookupflag_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub1_2_simple_f1.otf": ('GSUB',), + "aots/gsub2_1_lookupflag_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub2_1_multiple_sequences_f1.otf": ('GSUB',), + "aots/gsub2_1_simple_f1.otf": ('GSUB',), + "aots/gsub3_1_lookupflag_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub3_1_multiple_f1.otf": ('GSUB',), + "aots/gsub3_1_simple_f1.otf": ('GSUB',), + "aots/gsub4_1_lookupflag_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub4_1_multiple_ligatures_f1.otf": ('GSUB',), + "aots/gsub4_1_multiple_ligatures_f2.otf": ('GSUB',), + "aots/gsub4_1_multiple_ligsets_f1.otf": ('GSUB',), + "aots/gsub4_1_simple_f1.otf": ('GSUB',), + "aots/gsub7_font1.otf": ('GSUB',), + "aots/gsub7_font2.otf": ('GSUB',), + "aots/gsub_chaining1_boundary_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining1_boundary_f2.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining1_boundary_f3.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining1_boundary_f4.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining1_lookupflag_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining1_multiple_subrules_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining1_multiple_subrules_f2.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining1_next_glyph_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining1_simple_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining1_simple_f2.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining1_successive_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining2_boundary_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining2_boundary_f2.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining2_boundary_f3.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining2_boundary_f4.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining2_lookupflag_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining2_multiple_subrules_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining2_multiple_subrules_f2.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining2_next_glyph_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining2_simple_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining2_simple_f2.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining2_successive_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining3_boundary_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining3_boundary_f2.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining3_boundary_f3.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining3_boundary_f4.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining3_lookupflag_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining3_next_glyph_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining3_simple_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining3_simple_f2.otf": ('GDEF', 'GSUB'), + "aots/gsub_chaining3_successive_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_context1_boundary_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_context1_boundary_f2.otf": ('GDEF', 'GSUB'), + "aots/gsub_context1_expansion_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_context1_lookupflag_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_context1_lookupflag_f2.otf": ('GDEF', 'GSUB'), + "aots/gsub_context1_multiple_subrules_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_context1_multiple_subrules_f2.otf": ('GDEF', 'GSUB'), + "aots/gsub_context1_next_glyph_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_context1_simple_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_context1_simple_f2.otf": ('GDEF', 'GSUB'), + "aots/gsub_context1_successive_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_context2_boundary_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_context2_boundary_f2.otf": ('GDEF', 'GSUB'), + "aots/gsub_context2_classes_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_context2_classes_f2.otf": ('GDEF', 'GSUB'), + "aots/gsub_context2_expansion_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_context2_lookupflag_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_context2_lookupflag_f2.otf": ('GDEF', 'GSUB'), + "aots/gsub_context2_multiple_subrules_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_context2_multiple_subrules_f2.otf": ('GDEF', 'GSUB'), + "aots/gsub_context2_next_glyph_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_context2_simple_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_context2_simple_f2.otf": ('GDEF', 'GSUB'), + "aots/gsub_context2_successive_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_context3_boundary_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_context3_boundary_f2.otf": ('GDEF', 'GSUB'), + "aots/gsub_context3_lookupflag_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_context3_lookupflag_f2.otf": ('GDEF', 'GSUB'), + "aots/gsub_context3_next_glyph_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_context3_simple_f1.otf": ('GDEF', 'GSUB'), + "aots/gsub_context3_successive_f1.otf": ('GDEF', 'GSUB'), + "aots/lookupflag_ignore_attach_f1.otf": ('GDEF', 'GSUB'), + "aots/lookupflag_ignore_base_f1.otf": ('GDEF', 'GSUB'), + "aots/lookupflag_ignore_combination_f1.otf": ('GDEF', 'GSUB'), + "aots/lookupflag_ignore_ligatures_f1.otf": ('GDEF', 'GSUB'), + "aots/lookupflag_ignore_marks_f1.otf": ('GDEF', 'GSUB'), + "graphite/graphite_tests.ttf": ('Silf', 'Glat', 'Feat', 'Sill'), +} + + +TEST_REQUIREMENTS = { + "aots/cmap4_font4.otf": ("unicodedata2",), +} + + +ttLibVersion_RE = re.compile(r' ttLibVersion=".*"') + + +def getpath(testfile): + path = os.path.dirname(__file__) + return os.path.join(path, "data", testfile) + + +def read_expected_ttx(testfile, tableTag): + name = os.path.splitext(testfile)[0] + xml_expected_path = getpath("%s.ttx.%s" % (name, tagToXML(tableTag))) + with open(xml_expected_path, 'r', encoding="utf-8") as xml_file: + xml_expected = ttLibVersion_RE.sub('', xml_file.read()) + return xml_expected + + +def dump_ttx(font, tableTag): + f = UnicodeIO() + font.saveXML(f, newlinestr='\n', tables=[tableTag]) + return ttLibVersion_RE.sub('', f.getvalue()) + + +def load_ttx(ttx): + f = UnicodeIO() + f.write(ttx) + f.seek(0) + font = TTFont() + font.importXML(f) + return font + + +@contextlib.contextmanager +def open_font(testfile): + font = TTFont(getpath(testfile)) + try: + yield font + finally: + font.close() + + +def _skip_if_requirement_missing(testfile): + if testfile in TEST_REQUIREMENTS: + for req in TEST_REQUIREMENTS[testfile]: + if globals()[req] is None: + pytest.skip('%s not installed' % req) + + +def test_xml_from_binary(testfile, tableTag): + """Check XML from decompiled object.""" + _skip_if_requirement_missing(testfile) + + xml_expected = read_expected_ttx(testfile, tableTag) + + with open_font(testfile) as font: + xml_from_binary = dump_ttx(font, tableTag) + + assert xml_expected == xml_from_binary + + +def test_xml_from_xml(testfile, tableTag): + """Check XML from object read from XML.""" + _skip_if_requirement_missing(testfile) + + xml_expected = read_expected_ttx(testfile, tableTag) + + font = load_ttx(xml_expected) + name = os.path.splitext(testfile)[0] + setupfile = getpath("%s.ttx.%s.setup" % (name, tagToXML(tableTag))) + if os.path.exists(setupfile): +# import pdb; pdb.set_trace() + font.importXML(setupfile) + xml_from_xml = dump_ttx(font, tableTag) + + assert xml_expected == xml_from_xml + + +def pytest_generate_tests(metafunc): + # http://doc.pytest.org/en/latest/parametrize.html#basic-pytest-generate-tests-example + fixturenames = metafunc.fixturenames + argnames = ("testfile", "tableTag") + if all(fn in fixturenames for fn in argnames): + argvalues = [(testfile, tableTag) + for testfile, tableTags in sorted(TESTS.items()) + for tableTag in tableTags] + metafunc.parametrize(argnames, argvalues) + + +if __name__ == '__main__': + sys.exit(pytest.main(sys.argv)) diff --git a/Tests/ttLib/tables/ttProgram_test.py b/Tests/ttLib/tables/ttProgram_test.py new file mode 100644 index 0000000..9a7d232 --- /dev/null +++ b/Tests/ttLib/tables/ttProgram_test.py @@ -0,0 +1,119 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.xmlWriter import XMLWriter +from fontTools.ttLib.tables.ttProgram import Program +from fontTools.misc.textTools import deHexStr +import array +import os +import re +import unittest + +CURR_DIR = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) +DATA_DIR = os.path.join(CURR_DIR, 'data') + +TTPROGRAM_TTX = os.path.join(DATA_DIR, "ttProgram.ttx") +#TTPROGRAM_BIN = os.path.join(DATA_DIR, "ttProgram.bin") + +BYTECODE = deHexStr( + '403b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a' + '191817161514131211100f0e0d0c0b0a090807060504030201002c01b0184358456ab0' + '194360b0462344231020b0464ef04d2fb000121b21231133592d2c01b0184358b0052b' + 'b000134bb0145058b100403859b0062b1b21231133592d2c01b01843584eb0032510f2' + '21b000124d1b2045b00425b00425234a6164b0285258212310d61bb0032510f221b000' + '1259592d2cb01a435821211bb00225b0022549b00325b003254a612064b01050582121' + '211bb00325b0032549b0005058b0005058b8ffe238211bb0103821591bb0005258b01e' + '38211bb8fff03821595959592d2c01b0184358b0052bb000134bb0145058b90000ffc0' + '3859b0062b1b21231133592d2c4e018a10b146194344b00014b10046e2b00015b90000' + 'fff03800b0003cb0282bb0022510b0003c2d2c0118b0002fb00114f2b00113b001154d' + 'b000122d2c01b0184358b0052bb00013b90000ffe038b0062b1b21231133592d2c01b0' + '18435845646a23456469b01943646060b0462344231020b046f02fb000121b2121208a' + '208a525811331b212159592d2c01b10b0a432343650a2d2c00b10a0b4323430b2d2c00' + 'b0462370b101463e01b0462370b10246453ab10200080d2d2cb0122bb0022545b00225' + '456ab0408b60b0022523442121212d2cb0132bb0022545b00225456ab8ffc08c60b002' + '2523442121212d2cb000b0122b2121212d2cb000b0132b2121212d2c01b00643b00743' + '650a2d2c2069b04061b0008b20b12cc08a8cb8100062602b0c642364615c58b0036159' + '2d2cb1000325456854b01c4b505a58b0032545b0032545606820b004252344b0042523' + '441bb00325204568208a2344b00325456860b003252344592d2cb00325204568208a23' + '44b003254564686560b00425b0016023442d2cb00943588721c01bb01243588745b011' + '2bb0472344b0477ae41b038a45186920b04723448a8a8720b0a05158b0112bb0472344' + 'b0477ae41b21b0477ae4595959182d2c208a4523456860442d2c456a422d2c01182f2d' + '2c01b0184358b00425b00425496423456469b0408b6120b080626ab00225b00225618c' + 'b0194360b0462344218a10b046f6211b21212121592d2c01b0184358b0022545b00225' + '4564606ab00325456a6120b00425456a208a8b65b0042523448cb00325234421211b20' + '456a4420456a44592d2c012045b00055b018435a584568234569b0408b6120b080626a' + '208a236120b003258b65b0042523448cb00325234421211b2121b0192b592d2c018a8a' + '45642345646164422d2cb00425b00425b0192bb0184358b00425b00425b00325b01b2b' + '01b0022543b04054b0022543b000545a58b003252045b040614459b0022543b00054b0' + '022543b040545a58b004252045b04060445959212121212d2c014b525843b002254523' + '61441b2121592d2c014b525843b00225452360441b2121592d2c4b525845441b212159' + '2d2c0120b003252349b04060b0206320b000525823b002253823b002256538008a6338' + '1b212121212159012d2c4b505845441b2121592d2c01b005251023208af500b0016023' + 'edec2d2c01b005251023208af500b0016123edec2d2c01b0062510f500edec2d2c4623' + '46608a8a462320468a608a61b8ff8062232010238ab14b4b8a70456020b0005058b001' + '61b8ffba8b1bb0468c59b0106068013a2d2c2045b00325465258b0022546206861b003' + '25b003253f2321381b2111592d2c2045b00325465058b0022546206861b00325b00325' + '3f2321381b2111592d2c00b00743b006430b2d2c8a10ec2d2cb00c4358211b2046b000' + '5258b8fff0381bb0103859592d2c20b0005558b8100063b003254564b00325456461b0' + '005358b0021bb04061b00359254569535845441b2121591b21b0022545b00225456164' + 'b028515845441b212159592d2c21210c6423648bb84000622d2c21b08051580c642364' + '8bb82000621bb200402f2b59b002602d2c21b0c051580c6423648bb81555621bb20080' + '2f2b59b002602d2c0c6423648bb84000626023212d2c4b5358b00425b0042549642345' + '6469b0408b6120b080626ab00225b00225618cb0462344218a10b046f6211b218a1123' + '1220392f592d2cb00225b002254964b0c05458b8fff838b008381b2121592d2cb01343' + '58031b02592d2cb0134358021b03592d2cb00a2b2310203cb0172b2d2cb00225b8fff0' + '38b0282b8a102320d023b0102bb0054358c01b3c59201011b00012012d2c4b53234b51' + '5a58381b2121592d2c01b0022510d023c901b00113b0001410b0013cb001162d2c01b0' + '0013b001b0032549b0031738b001132d2c4b53234b515a5820458a60441b2121592d2c' + '20392f2d') + + +class TestFont(object): + disassembleInstructions = True + + +class ProgramTest(unittest.TestCase): + + def test__bool__(self): + p = Program() + assert not bool(p) + + bc = array.array("B", [0]) + p.fromBytecode(bc) + assert bool(p) + + assert p.bytecode.pop() == 0 + assert not bool(p) + + p = Program() + asm = ['SVTCA[0]'] + p.fromAssembly(asm) + assert bool(p) + + assert p.assembly.pop() == 'SVTCA[0]' + assert not bool(p) + + def test_roundtrip(self): + p = Program() + p.fromBytecode(BYTECODE) + asm = p.getAssembly(preserve=True) + p.fromAssembly(asm) + assert BYTECODE == p.getBytecode() + + def test_xml_indentation(self): + with open(TTPROGRAM_TTX, 'r', encoding='utf-8') as f: + ttProgramXML = f.read() + p = Program() + p.fromBytecode(BYTECODE) + ttfont = TestFont() + buf = UnicodeIO() + writer = XMLWriter(buf, newlinestr='\n') + try: + p.toXML(writer, ttfont) + finally: + output_string = buf.getvalue() + assert output_string == ttProgramXML + + +if __name__ == '__main__': + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttLib/woff2_test.py b/Tests/ttLib/woff2_test.py new file mode 100644 index 0000000..b3ec6b2 --- /dev/null +++ b/Tests/ttLib/woff2_test.py @@ -0,0 +1,840 @@ +from __future__ import print_function, division, absolute_import, unicode_literals +from fontTools.misc.py23 import * +from fontTools import ttLib +from fontTools.ttLib.woff2 import ( + WOFF2Reader, woff2DirectorySize, woff2DirectoryFormat, + woff2FlagsSize, woff2UnknownTagSize, woff2Base128MaxSize, WOFF2DirectoryEntry, + getKnownTagIndex, packBase128, base128Size, woff2UnknownTagIndex, + WOFF2FlavorData, woff2TransformedTableTags, WOFF2GlyfTable, WOFF2LocaTable, + WOFF2Writer, unpackBase128, unpack255UShort, pack255UShort) +import unittest +from fontTools.misc import sstruct +import struct +import os +import random +import copy +from collections import OrderedDict + +haveBrotli = False +try: + import brotli + haveBrotli = True +except ImportError: + pass + + +# Python 3 renamed 'assertRaisesRegexp' to 'assertRaisesRegex', and fires +# deprecation warnings if a program uses the old name. +if not hasattr(unittest.TestCase, 'assertRaisesRegex'): + unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp + + +current_dir = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) +data_dir = os.path.join(current_dir, 'data') +TTX = os.path.join(data_dir, 'TestTTF-Regular.ttx') +OTX = os.path.join(data_dir, 'TestOTF-Regular.otx') +METADATA = os.path.join(data_dir, 'test_woff2_metadata.xml') + +TT_WOFF2 = BytesIO() +CFF_WOFF2 = BytesIO() + + +def setUpModule(): + if not haveBrotli: + raise unittest.SkipTest("No module named brotli") + assert os.path.exists(TTX) + assert os.path.exists(OTX) + # import TT-flavoured test font and save it as WOFF2 + ttf = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False) + ttf.importXML(TTX) + ttf.flavor = "woff2" + ttf.save(TT_WOFF2, reorderTables=None) + # import CFF-flavoured test font and save it as WOFF2 + otf = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False) + otf.importXML(OTX) + otf.flavor = "woff2" + otf.save(CFF_WOFF2, reorderTables=None) + + +class WOFF2ReaderTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.file = BytesIO(CFF_WOFF2.getvalue()) + cls.font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False) + cls.font.importXML(OTX) + + def setUp(self): + self.file.seek(0) + + def test_bad_signature(self): + with self.assertRaisesRegex(ttLib.TTLibError, 'bad signature'): + WOFF2Reader(BytesIO(b"wOFF")) + + def test_not_enough_data_header(self): + incomplete_header = self.file.read(woff2DirectorySize - 1) + with self.assertRaisesRegex(ttLib.TTLibError, 'not enough data'): + WOFF2Reader(BytesIO(incomplete_header)) + + def test_incorrect_compressed_size(self): + data = self.file.read(woff2DirectorySize) + header = sstruct.unpack(woff2DirectoryFormat, data) + header['totalCompressedSize'] = 0 + data = sstruct.pack(woff2DirectoryFormat, header) + with self.assertRaises((brotli.error, ttLib.TTLibError)): + WOFF2Reader(BytesIO(data + self.file.read())) + + def test_incorrect_uncompressed_size(self): + decompress_backup = brotli.decompress + brotli.decompress = lambda data: b"" # return empty byte string + with self.assertRaisesRegex(ttLib.TTLibError, 'unexpected size for decompressed'): + WOFF2Reader(self.file) + brotli.decompress = decompress_backup + + def test_incorrect_file_size(self): + data = self.file.read(woff2DirectorySize) + header = sstruct.unpack(woff2DirectoryFormat, data) + header['length'] -= 1 + data = sstruct.pack(woff2DirectoryFormat, header) + with self.assertRaisesRegex( + ttLib.TTLibError, "doesn't match the actual file size"): + WOFF2Reader(BytesIO(data + self.file.read())) + + def test_num_tables(self): + tags = [t for t in self.font.keys() if t not in ('GlyphOrder', 'DSIG')] + data = self.file.read(woff2DirectorySize) + header = sstruct.unpack(woff2DirectoryFormat, data) + self.assertEqual(header['numTables'], len(tags)) + + def test_table_tags(self): + tags = set([t for t in self.font.keys() if t not in ('GlyphOrder', 'DSIG')]) + reader = WOFF2Reader(self.file) + self.assertEqual(set(reader.keys()), tags) + + def test_get_normal_tables(self): + woff2Reader = WOFF2Reader(self.file) + specialTags = woff2TransformedTableTags + ('head', 'GlyphOrder', 'DSIG') + for tag in [t for t in self.font.keys() if t not in specialTags]: + origData = self.font.getTableData(tag) + decompressedData = woff2Reader[tag] + self.assertEqual(origData, decompressedData) + + def test_reconstruct_unknown(self): + reader = WOFF2Reader(self.file) + with self.assertRaisesRegex(ttLib.TTLibError, 'transform for table .* unknown'): + reader.reconstructTable('ZZZZ') + + +class WOFF2ReaderTTFTest(WOFF2ReaderTest): + """ Tests specific to TT-flavored fonts. """ + + @classmethod + def setUpClass(cls): + cls.file = BytesIO(TT_WOFF2.getvalue()) + cls.font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False) + cls.font.importXML(TTX) + + def setUp(self): + self.file.seek(0) + + def test_reconstruct_glyf(self): + woff2Reader = WOFF2Reader(self.file) + reconstructedData = woff2Reader['glyf'] + self.assertEqual(self.font.getTableData('glyf'), reconstructedData) + + def test_reconstruct_loca(self): + woff2Reader = WOFF2Reader(self.file) + reconstructedData = woff2Reader['loca'] + self.assertEqual(self.font.getTableData('loca'), reconstructedData) + self.assertTrue(hasattr(woff2Reader.tables['glyf'], 'data')) + + def test_reconstruct_loca_not_match_orig_size(self): + reader = WOFF2Reader(self.file) + reader.tables['loca'].origLength -= 1 + with self.assertRaisesRegex( + ttLib.TTLibError, "'loca' table doesn't match original size"): + reader.reconstructTable('loca') + + +def normalise_table(font, tag, padding=4): + """ Return normalised table data. Keep 'font' instance unmodified. """ + assert tag in ('glyf', 'loca', 'head') + assert tag in font + if tag == 'head': + origHeadFlags = font['head'].flags + font['head'].flags |= (1 << 11) + tableData = font['head'].compile(font) + if font.sfntVersion in ("\x00\x01\x00\x00", "true"): + assert {'glyf', 'loca', 'head'}.issubset(font.keys()) + origIndexFormat = font['head'].indexToLocFormat + if hasattr(font['loca'], 'locations'): + origLocations = font['loca'].locations[:] + else: + origLocations = [] + glyfTable = ttLib.newTable('glyf') + glyfTable.decompile(font.getTableData('glyf'), font) + glyfTable.padding = padding + if tag == 'glyf': + tableData = glyfTable.compile(font) + elif tag == 'loca': + glyfTable.compile(font) + tableData = font['loca'].compile(font) + if tag == 'head': + glyfTable.compile(font) + font['loca'].compile(font) + tableData = font['head'].compile(font) + font['head'].indexToLocFormat = origIndexFormat + font['loca'].set(origLocations) + if tag == 'head': + font['head'].flags = origHeadFlags + return tableData + + +def normalise_font(font, padding=4): + """ Return normalised font data. Keep 'font' instance unmodified. """ + # drop DSIG but keep a copy + DSIG_copy = copy.deepcopy(font['DSIG']) + del font['DSIG'] + # ovverride TTFont attributes + origFlavor = font.flavor + origRecalcBBoxes = font.recalcBBoxes + origRecalcTimestamp = font.recalcTimestamp + origLazy = font.lazy + font.flavor = None + font.recalcBBoxes = False + font.recalcTimestamp = False + font.lazy = True + # save font to temporary stream + infile = BytesIO() + font.save(infile) + infile.seek(0) + # reorder tables alphabetically + outfile = BytesIO() + reader = ttLib.sfnt.SFNTReader(infile) + writer = ttLib.sfnt.SFNTWriter( + outfile, len(reader.tables), reader.sfntVersion, reader.flavor, reader.flavorData) + for tag in sorted(reader.keys()): + if tag in woff2TransformedTableTags + ('head',): + writer[tag] = normalise_table(font, tag, padding) + else: + writer[tag] = reader[tag] + writer.close() + # restore font attributes + font['DSIG'] = DSIG_copy + font.flavor = origFlavor + font.recalcBBoxes = origRecalcBBoxes + font.recalcTimestamp = origRecalcTimestamp + font.lazy = origLazy + return outfile.getvalue() + + +class WOFF2DirectoryEntryTest(unittest.TestCase): + + def setUp(self): + self.entry = WOFF2DirectoryEntry() + + def test_not_enough_data_table_flags(self): + with self.assertRaisesRegex(ttLib.TTLibError, "can't read table 'flags'"): + self.entry.fromString(b"") + + def test_not_enough_data_table_tag(self): + incompleteData = bytearray([0x3F, 0, 0, 0]) + with self.assertRaisesRegex(ttLib.TTLibError, "can't read table 'tag'"): + self.entry.fromString(bytes(incompleteData)) + + def test_table_reserved_flags(self): + with self.assertRaisesRegex(ttLib.TTLibError, "bits 6-7 are reserved"): + self.entry.fromString(bytechr(0xC0)) + + def test_loca_zero_transformLength(self): + data = bytechr(getKnownTagIndex('loca')) # flags + data += packBase128(random.randint(1, 100)) # origLength + data += packBase128(1) # non-zero transformLength + with self.assertRaisesRegex( + ttLib.TTLibError, "transformLength of the 'loca' table must be 0"): + self.entry.fromString(data) + + def test_fromFile(self): + unknownTag = Tag('ZZZZ') + data = bytechr(getKnownTagIndex(unknownTag)) + data += unknownTag.tobytes() + data += packBase128(random.randint(1, 100)) + expectedPos = len(data) + f = BytesIO(data + b'\0'*100) + self.entry.fromFile(f) + self.assertEqual(f.tell(), expectedPos) + + def test_transformed_toString(self): + self.entry.tag = Tag('glyf') + self.entry.flags = getKnownTagIndex(self.entry.tag) + self.entry.origLength = random.randint(101, 200) + self.entry.length = random.randint(1, 100) + expectedSize = (woff2FlagsSize + base128Size(self.entry.origLength) + + base128Size(self.entry.length)) + data = self.entry.toString() + self.assertEqual(len(data), expectedSize) + + def test_known_toString(self): + self.entry.tag = Tag('head') + self.entry.flags = getKnownTagIndex(self.entry.tag) + self.entry.origLength = 54 + expectedSize = (woff2FlagsSize + base128Size(self.entry.origLength)) + data = self.entry.toString() + self.assertEqual(len(data), expectedSize) + + def test_unknown_toString(self): + self.entry.tag = Tag('ZZZZ') + self.entry.flags = woff2UnknownTagIndex + self.entry.origLength = random.randint(1, 100) + expectedSize = (woff2FlagsSize + woff2UnknownTagSize + + base128Size(self.entry.origLength)) + data = self.entry.toString() + self.assertEqual(len(data), expectedSize) + + +class DummyReader(WOFF2Reader): + + def __init__(self, file, checkChecksums=1, fontNumber=-1): + self.file = file + for attr in ('majorVersion', 'minorVersion', 'metaOffset', 'metaLength', + 'metaOrigLength', 'privLength', 'privOffset'): + setattr(self, attr, 0) + + +class WOFF2FlavorDataTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + assert os.path.exists(METADATA) + with open(METADATA, 'rb') as f: + cls.xml_metadata = f.read() + cls.compressed_metadata = brotli.compress(cls.xml_metadata, mode=brotli.MODE_TEXT) + # make random byte strings; font data must be 4-byte aligned + cls.fontdata = bytes(bytearray(random.sample(range(0, 256), 80))) + cls.privData = bytes(bytearray(random.sample(range(0, 256), 20))) + + def setUp(self): + self.file = BytesIO(self.fontdata) + self.file.seek(0, 2) + + def test_get_metaData_no_privData(self): + self.file.write(self.compressed_metadata) + reader = DummyReader(self.file) + reader.metaOffset = len(self.fontdata) + reader.metaLength = len(self.compressed_metadata) + reader.metaOrigLength = len(self.xml_metadata) + flavorData = WOFF2FlavorData(reader) + self.assertEqual(self.xml_metadata, flavorData.metaData) + + def test_get_privData_no_metaData(self): + self.file.write(self.privData) + reader = DummyReader(self.file) + reader.privOffset = len(self.fontdata) + reader.privLength = len(self.privData) + flavorData = WOFF2FlavorData(reader) + self.assertEqual(self.privData, flavorData.privData) + + def test_get_metaData_and_privData(self): + self.file.write(self.compressed_metadata + self.privData) + reader = DummyReader(self.file) + reader.metaOffset = len(self.fontdata) + reader.metaLength = len(self.compressed_metadata) + reader.metaOrigLength = len(self.xml_metadata) + reader.privOffset = reader.metaOffset + reader.metaLength + reader.privLength = len(self.privData) + flavorData = WOFF2FlavorData(reader) + self.assertEqual(self.xml_metadata, flavorData.metaData) + self.assertEqual(self.privData, flavorData.privData) + + def test_get_major_minorVersion(self): + reader = DummyReader(self.file) + reader.majorVersion = reader.minorVersion = 1 + flavorData = WOFF2FlavorData(reader) + self.assertEqual(flavorData.majorVersion, 1) + self.assertEqual(flavorData.minorVersion, 1) + + +class WOFF2WriterTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False, flavor="woff2") + cls.font.importXML(OTX) + cls.tags = [t for t in cls.font.keys() if t != 'GlyphOrder'] + cls.numTables = len(cls.tags) + cls.file = BytesIO(CFF_WOFF2.getvalue()) + cls.file.seek(0, 2) + cls.length = (cls.file.tell() + 3) & ~3 + cls.setUpFlavorData() + + @classmethod + def setUpFlavorData(cls): + assert os.path.exists(METADATA) + with open(METADATA, 'rb') as f: + cls.xml_metadata = f.read() + cls.compressed_metadata = brotli.compress(cls.xml_metadata, mode=brotli.MODE_TEXT) + cls.privData = bytes(bytearray(random.sample(range(0, 256), 20))) + + def setUp(self): + self.file.seek(0) + self.writer = WOFF2Writer(BytesIO(), self.numTables, self.font.sfntVersion) + + def test_DSIG_dropped(self): + self.writer['DSIG'] = b"\0" + self.assertEqual(len(self.writer.tables), 0) + self.assertEqual(self.writer.numTables, self.numTables-1) + + def test_no_rewrite_table(self): + self.writer['ZZZZ'] = b"\0" + with self.assertRaisesRegex(ttLib.TTLibError, "cannot rewrite"): + self.writer['ZZZZ'] = b"\0" + + def test_num_tables(self): + self.writer['ABCD'] = b"\0" + with self.assertRaisesRegex(ttLib.TTLibError, "wrong number of tables"): + self.writer.close() + + def test_required_tables(self): + font = ttLib.TTFont(flavor="woff2") + with self.assertRaisesRegex(ttLib.TTLibError, "missing required table"): + font.save(BytesIO()) + + def test_head_transform_flag(self): + headData = self.font.getTableData('head') + origFlags = byteord(headData[16]) + woff2font = ttLib.TTFont(self.file) + newHeadData = woff2font.getTableData('head') + modifiedFlags = byteord(newHeadData[16]) + self.assertNotEqual(origFlags, modifiedFlags) + restoredFlags = modifiedFlags & ~0x08 # turn off bit 11 + self.assertEqual(origFlags, restoredFlags) + + def test_tables_sorted_alphabetically(self): + expected = sorted([t for t in self.tags if t != 'DSIG']) + woff2font = ttLib.TTFont(self.file) + self.assertEqual(expected, list(woff2font.reader.keys())) + + def test_checksums(self): + normFile = BytesIO(normalise_font(self.font, padding=4)) + normFile.seek(0) + normFont = ttLib.TTFont(normFile, checkChecksums=2) + w2font = ttLib.TTFont(self.file) + # force reconstructing glyf table using 4-byte padding + w2font.reader.padding = 4 + for tag in [t for t in self.tags if t != 'DSIG']: + w2data = w2font.reader[tag] + normData = normFont.reader[tag] + if tag == "head": + w2data = w2data[:8] + b'\0\0\0\0' + w2data[12:] + normData = normData[:8] + b'\0\0\0\0' + normData[12:] + w2CheckSum = ttLib.sfnt.calcChecksum(w2data) + normCheckSum = ttLib.sfnt.calcChecksum(normData) + self.assertEqual(w2CheckSum, normCheckSum) + normCheckSumAdjustment = normFont['head'].checkSumAdjustment + self.assertEqual(normCheckSumAdjustment, w2font['head'].checkSumAdjustment) + + def test_calcSFNTChecksumsLengthsAndOffsets(self): + normFont = ttLib.TTFont(BytesIO(normalise_font(self.font, padding=4))) + for tag in self.tags: + self.writer[tag] = self.font.getTableData(tag) + self.writer._normaliseGlyfAndLoca(padding=4) + self.writer._setHeadTransformFlag() + self.writer.tables = OrderedDict(sorted(self.writer.tables.items())) + self.writer._calcSFNTChecksumsLengthsAndOffsets() + for tag, entry in normFont.reader.tables.items(): + self.assertEqual(entry.offset, self.writer.tables[tag].origOffset) + self.assertEqual(entry.length, self.writer.tables[tag].origLength) + self.assertEqual(entry.checkSum, self.writer.tables[tag].checkSum) + + def test_bad_sfntVersion(self): + for i in range(self.numTables): + self.writer[bytechr(65 + i)*4] = b"\0" + self.writer.sfntVersion = 'ZZZZ' + with self.assertRaisesRegex(ttLib.TTLibError, "bad sfntVersion"): + self.writer.close() + + def test_calcTotalSize_no_flavorData(self): + expected = self.length + self.writer.file = BytesIO() + for tag in self.tags: + self.writer[tag] = self.font.getTableData(tag) + self.writer.close() + self.assertEqual(expected, self.writer.length) + self.assertEqual(expected, self.writer.file.tell()) + + def test_calcTotalSize_with_metaData(self): + expected = self.length + len(self.compressed_metadata) + flavorData = self.writer.flavorData = WOFF2FlavorData() + flavorData.metaData = self.xml_metadata + self.writer.file = BytesIO() + for tag in self.tags: + self.writer[tag] = self.font.getTableData(tag) + self.writer.close() + self.assertEqual(expected, self.writer.length) + self.assertEqual(expected, self.writer.file.tell()) + + def test_calcTotalSize_with_privData(self): + expected = self.length + len(self.privData) + flavorData = self.writer.flavorData = WOFF2FlavorData() + flavorData.privData = self.privData + self.writer.file = BytesIO() + for tag in self.tags: + self.writer[tag] = self.font.getTableData(tag) + self.writer.close() + self.assertEqual(expected, self.writer.length) + self.assertEqual(expected, self.writer.file.tell()) + + def test_calcTotalSize_with_metaData_and_privData(self): + metaDataLength = (len(self.compressed_metadata) + 3) & ~3 + expected = self.length + metaDataLength + len(self.privData) + flavorData = self.writer.flavorData = WOFF2FlavorData() + flavorData.metaData = self.xml_metadata + flavorData.privData = self.privData + self.writer.file = BytesIO() + for tag in self.tags: + self.writer[tag] = self.font.getTableData(tag) + self.writer.close() + self.assertEqual(expected, self.writer.length) + self.assertEqual(expected, self.writer.file.tell()) + + def test_getVersion(self): + # no version + self.assertEqual((0, 0), self.writer._getVersion()) + # version from head.fontRevision + fontRevision = self.font['head'].fontRevision + versionTuple = tuple(int(i) for i in str(fontRevision).split(".")) + entry = self.writer.tables['head'] = ttLib.newTable('head') + entry.data = self.font.getTableData('head') + self.assertEqual(versionTuple, self.writer._getVersion()) + # version from writer.flavorData + flavorData = self.writer.flavorData = WOFF2FlavorData() + flavorData.majorVersion, flavorData.minorVersion = (10, 11) + self.assertEqual((10, 11), self.writer._getVersion()) + + +class WOFF2WriterTTFTest(WOFF2WriterTest): + + @classmethod + def setUpClass(cls): + cls.font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False, flavor="woff2") + cls.font.importXML(TTX) + cls.tags = [t for t in cls.font.keys() if t != 'GlyphOrder'] + cls.numTables = len(cls.tags) + cls.file = BytesIO(TT_WOFF2.getvalue()) + cls.file.seek(0, 2) + cls.length = (cls.file.tell() + 3) & ~3 + cls.setUpFlavorData() + + def test_normaliseGlyfAndLoca(self): + normTables = {} + for tag in ('head', 'loca', 'glyf'): + normTables[tag] = normalise_table(self.font, tag, padding=4) + for tag in self.tags: + tableData = self.font.getTableData(tag) + self.writer[tag] = tableData + if tag in normTables: + self.assertNotEqual(tableData, normTables[tag]) + self.writer._normaliseGlyfAndLoca(padding=4) + self.writer._setHeadTransformFlag() + for tag in normTables: + self.assertEqual(self.writer.tables[tag].data, normTables[tag]) + + +class WOFF2LocaTableTest(unittest.TestCase): + + def setUp(self): + self.font = font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False) + font['head'] = ttLib.newTable('head') + font['loca'] = WOFF2LocaTable() + font['glyf'] = WOFF2GlyfTable() + + def test_compile_short_loca(self): + locaTable = self.font['loca'] + locaTable.set(list(range(0, 0x20000, 2))) + self.font['glyf'].indexFormat = 0 + locaData = locaTable.compile(self.font) + self.assertEqual(len(locaData), 0x20000) + + def test_compile_short_loca_overflow(self): + locaTable = self.font['loca'] + locaTable.set(list(range(0x20000 + 1))) + self.font['glyf'].indexFormat = 0 + with self.assertRaisesRegex( + ttLib.TTLibError, "indexFormat is 0 but local offsets > 0x20000"): + locaTable.compile(self.font) + + def test_compile_short_loca_not_multiples_of_2(self): + locaTable = self.font['loca'] + locaTable.set([1, 3, 5, 7]) + self.font['glyf'].indexFormat = 0 + with self.assertRaisesRegex(ttLib.TTLibError, "offsets not multiples of 2"): + locaTable.compile(self.font) + + def test_compile_long_loca(self): + locaTable = self.font['loca'] + locaTable.set(list(range(0x20001))) + self.font['glyf'].indexFormat = 1 + locaData = locaTable.compile(self.font) + self.assertEqual(len(locaData), 0x20001 * 4) + + def test_compile_set_indexToLocFormat_0(self): + locaTable = self.font['loca'] + # offsets are all multiples of 2 and max length is < 0x10000 + locaTable.set(list(range(0, 0x20000, 2))) + locaTable.compile(self.font) + newIndexFormat = self.font['head'].indexToLocFormat + self.assertEqual(0, newIndexFormat) + + def test_compile_set_indexToLocFormat_1(self): + locaTable = self.font['loca'] + # offsets are not multiples of 2 + locaTable.set(list(range(10))) + locaTable.compile(self.font) + newIndexFormat = self.font['head'].indexToLocFormat + self.assertEqual(1, newIndexFormat) + # max length is >= 0x10000 + locaTable.set(list(range(0, 0x20000 + 1, 2))) + locaTable.compile(self.font) + newIndexFormat = self.font['head'].indexToLocFormat + self.assertEqual(1, newIndexFormat) + + +class WOFF2GlyfTableTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False) + font.importXML(TTX) + cls.tables = {} + cls.transformedTags = ('maxp', 'head', 'loca', 'glyf') + for tag in reversed(cls.transformedTags): # compile in inverse order + cls.tables[tag] = font.getTableData(tag) + infile = BytesIO(TT_WOFF2.getvalue()) + reader = WOFF2Reader(infile) + cls.transformedGlyfData = reader.tables['glyf'].loadData( + reader.transformBuffer) + cls.glyphOrder = ['.notdef'] + ["glyph%.5d" % i for i in range(1, font['maxp'].numGlyphs)] + + def setUp(self): + self.font = font = ttLib.TTFont(recalcBBoxes=False, recalcTimestamp=False) + font.setGlyphOrder(self.glyphOrder) + font['head'] = ttLib.newTable('head') + font['maxp'] = ttLib.newTable('maxp') + font['loca'] = WOFF2LocaTable() + font['glyf'] = WOFF2GlyfTable() + for tag in self.transformedTags: + font[tag].decompile(self.tables[tag], font) + + def test_reconstruct_glyf_padded_4(self): + glyfTable = WOFF2GlyfTable() + glyfTable.reconstruct(self.transformedGlyfData, self.font) + glyfTable.padding = 4 + data = glyfTable.compile(self.font) + normGlyfData = normalise_table(self.font, 'glyf', glyfTable.padding) + self.assertEqual(normGlyfData, data) + + def test_reconstruct_glyf_padded_2(self): + glyfTable = WOFF2GlyfTable() + glyfTable.reconstruct(self.transformedGlyfData, self.font) + glyfTable.padding = 2 + data = glyfTable.compile(self.font) + normGlyfData = normalise_table(self.font, 'glyf', glyfTable.padding) + self.assertEqual(normGlyfData, data) + + def test_reconstruct_glyf_unpadded(self): + glyfTable = WOFF2GlyfTable() + glyfTable.reconstruct(self.transformedGlyfData, self.font) + data = glyfTable.compile(self.font) + self.assertEqual(self.tables['glyf'], data) + + def test_reconstruct_glyf_incorrect_glyphOrder(self): + glyfTable = WOFF2GlyfTable() + badGlyphOrder = self.font.getGlyphOrder()[:-1] + self.font.setGlyphOrder(badGlyphOrder) + with self.assertRaisesRegex(ttLib.TTLibError, "incorrect glyphOrder"): + glyfTable.reconstruct(self.transformedGlyfData, self.font) + + def test_reconstruct_glyf_missing_glyphOrder(self): + glyfTable = WOFF2GlyfTable() + del self.font.glyphOrder + numGlyphs = self.font['maxp'].numGlyphs + del self.font['maxp'] + glyfTable.reconstruct(self.transformedGlyfData, self.font) + expected = [".notdef"] + expected.extend(["glyph%.5d" % i for i in range(1, numGlyphs)]) + self.assertEqual(expected, glyfTable.glyphOrder) + + def test_reconstruct_loca_padded_4(self): + locaTable = self.font['loca'] = WOFF2LocaTable() + glyfTable = self.font['glyf'] = WOFF2GlyfTable() + glyfTable.reconstruct(self.transformedGlyfData, self.font) + glyfTable.padding = 4 + glyfTable.compile(self.font) + data = locaTable.compile(self.font) + normLocaData = normalise_table(self.font, 'loca', glyfTable.padding) + self.assertEqual(normLocaData, data) + + def test_reconstruct_loca_padded_2(self): + locaTable = self.font['loca'] = WOFF2LocaTable() + glyfTable = self.font['glyf'] = WOFF2GlyfTable() + glyfTable.reconstruct(self.transformedGlyfData, self.font) + glyfTable.padding = 2 + glyfTable.compile(self.font) + data = locaTable.compile(self.font) + normLocaData = normalise_table(self.font, 'loca', glyfTable.padding) + self.assertEqual(normLocaData, data) + + def test_reconstruct_loca_unpadded(self): + locaTable = self.font['loca'] = WOFF2LocaTable() + glyfTable = self.font['glyf'] = WOFF2GlyfTable() + glyfTable.reconstruct(self.transformedGlyfData, self.font) + glyfTable.compile(self.font) + data = locaTable.compile(self.font) + self.assertEqual(self.tables['loca'], data) + + def test_reconstruct_glyf_header_not_enough_data(self): + with self.assertRaisesRegex(ttLib.TTLibError, "not enough 'glyf' data"): + WOFF2GlyfTable().reconstruct(b"", self.font) + + def test_reconstruct_glyf_table_incorrect_size(self): + msg = "incorrect size of transformed 'glyf'" + with self.assertRaisesRegex(ttLib.TTLibError, msg): + WOFF2GlyfTable().reconstruct(self.transformedGlyfData + b"\x00", self.font) + with self.assertRaisesRegex(ttLib.TTLibError, msg): + WOFF2GlyfTable().reconstruct(self.transformedGlyfData[:-1], self.font) + + def test_transform_glyf(self): + glyfTable = self.font['glyf'] + data = glyfTable.transform(self.font) + self.assertEqual(self.transformedGlyfData, data) + + def test_transform_glyf_incorrect_glyphOrder(self): + glyfTable = self.font['glyf'] + badGlyphOrder = self.font.getGlyphOrder()[:-1] + del glyfTable.glyphOrder + self.font.setGlyphOrder(badGlyphOrder) + with self.assertRaisesRegex(ttLib.TTLibError, "incorrect glyphOrder"): + glyfTable.transform(self.font) + glyfTable.glyphOrder = badGlyphOrder + with self.assertRaisesRegex(ttLib.TTLibError, "incorrect glyphOrder"): + glyfTable.transform(self.font) + + def test_transform_glyf_missing_glyphOrder(self): + glyfTable = self.font['glyf'] + del glyfTable.glyphOrder + del self.font.glyphOrder + numGlyphs = self.font['maxp'].numGlyphs + del self.font['maxp'] + glyfTable.transform(self.font) + expected = [".notdef"] + expected.extend(["glyph%.5d" % i for i in range(1, numGlyphs)]) + self.assertEqual(expected, glyfTable.glyphOrder) + + def test_roundtrip_glyf_reconstruct_and_transform(self): + glyfTable = WOFF2GlyfTable() + glyfTable.reconstruct(self.transformedGlyfData, self.font) + data = glyfTable.transform(self.font) + self.assertEqual(self.transformedGlyfData, data) + + def test_roundtrip_glyf_transform_and_reconstruct(self): + glyfTable = self.font['glyf'] + transformedData = glyfTable.transform(self.font) + newGlyfTable = WOFF2GlyfTable() + newGlyfTable.reconstruct(transformedData, self.font) + newGlyfTable.padding = 4 + reconstructedData = newGlyfTable.compile(self.font) + normGlyfData = normalise_table(self.font, 'glyf', newGlyfTable.padding) + self.assertEqual(normGlyfData, reconstructedData) + + +class Base128Test(unittest.TestCase): + + def test_unpackBase128(self): + self.assertEqual(unpackBase128(b'\x3f\x00\x00'), (63, b"\x00\x00")) + self.assertEqual(unpackBase128(b'\x8f\xff\xff\xff\x7f')[0], 4294967295) + + self.assertRaisesRegex( + ttLib.TTLibError, + "UIntBase128 value must not start with leading zeros", + unpackBase128, b'\x80\x80\x3f') + + self.assertRaisesRegex( + ttLib.TTLibError, + "UIntBase128-encoded sequence is longer than 5 bytes", + unpackBase128, b'\x8f\xff\xff\xff\xff\x7f') + + self.assertRaisesRegex( + ttLib.TTLibError, + "UIntBase128 value exceeds 2\*\*32-1", + unpackBase128, b'\x90\x80\x80\x80\x00') + + self.assertRaisesRegex( + ttLib.TTLibError, + "not enough data to unpack UIntBase128", + unpackBase128, b'') + + def test_base128Size(self): + self.assertEqual(base128Size(0), 1) + self.assertEqual(base128Size(24567), 3) + self.assertEqual(base128Size(2**32-1), 5) + + def test_packBase128(self): + self.assertEqual(packBase128(63), b"\x3f") + self.assertEqual(packBase128(2**32-1), b'\x8f\xff\xff\xff\x7f') + self.assertRaisesRegex( + ttLib.TTLibError, + "UIntBase128 format requires 0 <= integer <= 2\*\*32-1", + packBase128, 2**32+1) + self.assertRaisesRegex( + ttLib.TTLibError, + "UIntBase128 format requires 0 <= integer <= 2\*\*32-1", + packBase128, -1) + + +class UShort255Test(unittest.TestCase): + + def test_unpack255UShort(self): + self.assertEqual(unpack255UShort(bytechr(252))[0], 252) + # some numbers (e.g. 506) can have multiple encodings + self.assertEqual( + unpack255UShort(struct.pack(b"BB", 254, 0))[0], 506) + self.assertEqual( + unpack255UShort(struct.pack(b"BB", 255, 253))[0], 506) + self.assertEqual( + unpack255UShort(struct.pack(b"BBB", 253, 1, 250))[0], 506) + + self.assertRaisesRegex( + ttLib.TTLibError, + "not enough data to unpack 255UInt16", + unpack255UShort, struct.pack(b"BB", 253, 0)) + + self.assertRaisesRegex( + ttLib.TTLibError, + "not enough data to unpack 255UInt16", + unpack255UShort, struct.pack(b"B", 254)) + + self.assertRaisesRegex( + ttLib.TTLibError, + "not enough data to unpack 255UInt16", + unpack255UShort, struct.pack(b"B", 255)) + + def test_pack255UShort(self): + self.assertEqual(pack255UShort(252), b'\xfc') + self.assertEqual(pack255UShort(505), b'\xff\xfc') + self.assertEqual(pack255UShort(506), b'\xfe\x00') + self.assertEqual(pack255UShort(762), b'\xfd\x02\xfa') + + self.assertRaisesRegex( + ttLib.TTLibError, + "255UInt16 format requires 0 <= integer <= 65535", + pack255UShort, -1) + + self.assertRaisesRegex( + ttLib.TTLibError, + "255UInt16 format requires 0 <= integer <= 65535", + pack255UShort, 0xFFFF+1) + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/ttx/data/TestBOM.ttx b/Tests/ttx/data/TestBOM.ttx new file mode 100644 index 0000000..7597133 --- /dev/null +++ b/Tests/ttx/data/TestBOM.ttx @@ -0,0 +1,3 @@ + + + diff --git a/Tests/ttx/data/TestDFONT.dfont b/Tests/ttx/data/TestDFONT.dfont new file mode 100644 index 0000000000000000000000000000000000000000..a6ea7009be3ed3c1083254effaf54bc6fbfed685 GIT binary patch literal 3505 zcmZQzU}Rum;7wv+;7MX&VDM)cae#9NI0yt785md?7~I@Ke5;;r{mH<JEMQ<@d;xM_PJVJCLjXes0|Rpj z0|SFVZem3Ng8+jB0|O(-K83u*+|+7@*}E7Rm_INuu=*6_7nd+S4tm7Ez-hz4z+hZZ zlnQbO0|Ntd4#<7ruwZ0hh_`WLis!fa${^1SvWMZ~G(mHaKd$_Jzm&mn$R z1`Y-$kSJIu19Qv2D+~-wmq8*-bHFBnm@v!^7GVeL;bC9~^I&pJU_J|D9^(WCRt5&9 zJxpB;3=BOGn(;1!5yLMICMIq+78WLU4n~#+NV0|LX7Ka(b7eTdz^kCf^zJ_!!wKdV zMjeQo7#JA8FzsPrU{GLSWc&hhIRgWuF9QR^FDU-VV9s=z0pvGkP#}Q96zZ}Vu$ved zWEfZ&92giFc)%`a0c95k1qM9@wg3OY4&a6J82^9!|K$Jm`wyTdfTckMBOAiOjGzz$ z*#(k-VMyYG@fa9Dfq=vtR7^H1xkiiu22~i$7??nD!+4149Ro81I|CB~qp_f(pt_)< zpb_H)qkmVJ-u)FaV(R)^2C|$H9ACQ_su-9-D$R{WmCc)*tEw0n826;Ad=wve_9l8DB!#91NUH>QFW(11FO!l+Dec%rqU!=3!7~ zhUEu725aW|P;ns!4(7L@RLH`~z`y{?&&&*rESwCC3<3=Eq2f#odsr8$X33|L%g z#h`#;5J(K>KL)rftQ278TY;U3*8yQ*4;C|If`dhY!H_|Z!2k>y!cvQhGxPHl4D}2Q z3=oDu{Gf~Obda4PA#S>GFQEHcfgzIt6s&m+`QQjjWKduzVJKoqWJqC1Wyl2wKsJK{ zLq0LSBA}LZU)RQDRDJZemfkLVg-jJYWkzL;w+z1tp*oh609S1}g@A z27QKfhD?SMh75*Mh9m|(hGd3(hFk`Hh9rhmh75)ju>18H(!k+Y!cYQFRty;>B?ZM+ z`ugdaB^jkjddc~@`bnu7DTyiiY5932C7=WY%32^jL&+|X&vO{^8IlQcix7er{rMNotY4kEgS%U$CoQNks`F z)qxT}THXQGW}tGn8B~6Qic>Vq4z7op7(q3>Aj4`V=O9p3$i%?G!1ez>13$Q=1GRL- zpk*^71E{10)v{2N!73Q81a6f9nT*m7VPIM$z+kQdvYv + diff --git a/Tests/ttx/data/TestNoXML.ttx b/Tests/ttx/data/TestNoXML.ttx new file mode 100644 index 0000000..8dac507 --- /dev/null +++ b/Tests/ttx/data/TestNoXML.ttx @@ -0,0 +1,2 @@ + + diff --git a/Tests/ttx/data/TestOTF.otf b/Tests/ttx/data/TestOTF.otf new file mode 100644 index 0000000000000000000000000000000000000000..cd0bdf67d24e99164a89a4bdd920496f346b5bff GIT binary patch literal 2308 zcmeYd3Grv(VrXDsW>9c;b5l_Jb4Y-Jf&B*q1H)^VU{7}j1_nk3297@r3=ACp!TLt& z%KSeV7#IZ@7#I?ga}x^~4V7*)FtFZXU|{@}k(!voQQ;BKz`(GEfq}s+BO^7DErjs_ z0|Ubo1_lO|jNFn6_B9L(7#KJ}e2Lt|iUJ0<0FV_N3=9lxd5O8HEj=|a7#J7}7#LWx z3i69f{_kKgVqjpu!N9+oD57LQIPQr49qS6t}rk#U1l(1Fk+emG6gCJq1YH00vH(B7#J8>7&{mzFt9Q( zFzsRLVqjqCfzXV18H^ZyaWFA)v$3!+v2!r8G=M_}q8&u~`TMyt9AIEjP-A-cpN-)J za|@#m#BK%##xG2Jz&-#elxFmT+iwC>!NADC!C(RvXJTMyuz|9f86+8;p==f;HLMK0 z3^7n~HU@r%OemY3L6e~Z%I09;WLOAgb0V?18I&1TLB)BH*nAAu3}>O@LJS;?91P41 zj4YfC3=A2J0#G(1g8*X%l+DDz$Jha7Gcy=5PJ^;pkkqg;NHLy(inB4uF+PH_*%^YE z7@%wp1|g;(D4P?B&CTG(lmZp!L1ObUlrgPx&M&Ae%1qBFQP4=%R4_6yG*w8dRB+4B zD+$TZ&nebZ@XJ>K%NHvYr52|am8GWWg`^gjDENoC1*N8!<|G!u6(snm}BT848)j3VHb@3W*9OMTse?xrs&D3i)YB4uG4UQBqP+ zY^ATCo>`JnnxvPUpR1pgnvs&2qMw$ZS5g9UEfGrelXCKt^m7x7OHzyUeLS6A{eoTf zN-9dg?gF$6KBGNdzBS70U^gi+7&7QF7=R(n zCWt+{Xg0xQ6&N7$3JjSHAQ$H`4G9)qRF(fnOGvqSpGbAyjGGs8MfNj!eNCW$$ zgrNi;w$w2NWKRx5K0^|NKG-+K3?<-@(`WEu@MLgiaAojg2xf3)&|@fJs9-1o6*UaZ z44^#5z#s$7)iI#l!~o*6FgP$UFz_&NFfcK&FfcIiGAJR!w*@;A974T5t0aCicC1MSseDKk!$Mov)kkdmk9J9uiaF;`^BnqQ1BO<`Y%mf>M0F!XRI8ei#4F g_gkEen}P8QGz@+*Fff4dM+S4I%M74410yJ806r{tod5s; literal 0 HcmV?d00001 diff --git a/Tests/ttx/data/TestOTF.ttx b/Tests/ttx/data/TestOTF.ttx new file mode 100644 index 0000000..852dacf --- /dev/null +++ b/Tests/ttx/data/TestOTF.ttx @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright (c) 2015 by FontTools. No rights reserved. + + + Test OTF + + + Regular + + + FontTools: Test OTF: 2015 + + + Test OTF + + + Version 1.000 + + + TestOTF-Regular + + + Test OTF is not a trademark of FontTools. + + + FontTools + + + FontTools + + + https://github.com/behdad/fonttools + + + https://github.com/behdad/fonttools + + + https://github.com/behdad/fonttools/blob/master/LICENSE.txt + + + Test TTF + + + Copyright (c) 2015 by FontTools. No rights reserved. + + + Test OTF + + + Regular + + + FontTools: Test OTF: 2015 + + + Test OTF + + + Version 1.000 + + + TestOTF-Regular + + + Test OTF is not a trademark of FontTools. + + + FontTools + + + FontTools + + + https://github.com/behdad/fonttools + + + https://github.com/behdad/fonttools + + + https://github.com/behdad/fonttools/blob/master/LICENSE.txt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 131 122 -131 hlineto + return + + + + + + 500 450 hmoveto + 750 -400 -750 vlineto + 50 50 rmoveto + 650 300 -650 vlineto + endchar + + + 0 endchar + + + 250 endchar + + + 723 55 hmoveto + -107 callsubr + 241 -122 rmoveto + -107 callsubr + 241 -122 rmoveto + -107 callsubr + endchar + + + 241 55 hmoveto + -107 callsubr + endchar + + + 250 endchar + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttx/data/TestTTC.ttc b/Tests/ttx/data/TestTTC.ttc new file mode 100644 index 0000000000000000000000000000000000000000..a21fe89dc4c4b44093e403aedc8034d88d5c5003 GIT binary patch literal 2608 zcmXRZNls&6WB>ss1_lNZ1_nj}FrS~Hfq|K!z$Mtzoq+))!oRRd zfk}jcfgvF|H?e@xQ0X=U15*hD1LLpcvJwT5VGIl`1q=)fOlbw_xln!y$ei@V;sOQ^ z25ANcmI?+22A=es$~3tkMiT}GmKFvEhK!8V#1xJSmkkUIEN>VX7|cKdY$1#f7#LVp z7#J8-GIC2Q*w-*DU|?YNU|?X7$jMJmWQb$%VPIfQU|?Y2%T25(VBlvEV_;wf*{_h7 zn47wLJ%=3w1M36^29~6P{Nj=#)guoW7&yN$Fff=G6s3aHF)%Q2>44k^@+ree^*cD8 z85kH?7&{mzFt9Q(FzsRLVqjqCfzXV18H^ZyaWFA)v$3!+v2!r8G{AH~qL{(Y-_Mod z00V=98q>S~Yz!xuTNojs3JTgUOnVp@7$B*GnSp@;gk`|t&A<@Dzzk-|Ft9KiSy-N^Ry03=F#%su-9->dcKrmCc)*tEw3O|7S2~U|^cV zP{p7IPB-cdEDRqR7#P_ZSQsCHN+JeEaEf4LV9?ldGcTUs<|_j;^9zuRE>05!naXhG z@B5`7moUv?ddI-Pz{vnhCyWdX%q{<}FfcG(W-wwfVwwXs38WQ<*%-fobTTk7{DR_- z4CYLiL2hCYg+e6;6Hqo_U|?Var*0l-nunPI$`PQFSenrbZaz#0$mbjkCQxxE26hG; zD4Ur;-(AL1mc3sP{=G+$jdKLNK_~( zN=!-3O)Sb*$WKFZ0NnJ9l9GaAD}DX+%#w`KB)#PPT>YffjFiL_{j~hNk`j<>iBO`S zl#`#NpPN`*l3JwivgH$)LdC#*ojD$56r$!jR99&yd4V%%I1hz~IM_&!B*$zZlF*WhiDy zWhi1OV@PF4VbEg;VE~DMOb=lQVQ^yzVn}63XDDUJVMt^s!eX)&g93&gkQmHOVPMmX z88X3cQeZG-&|@$FLzqnvdvwukg2{qR2f0ImA(H{*;yi|Yu-_9I6c|bviWm|ZQW#Ph za>1_4W>8?rXGp^l0+^u!3dItJ0)}D+D+YZAeTH;~OokGM42DvMBnCZ(WQKf(Tn2rH zB!*Om42Bf2P5KOJV1JY_l)%H5I;Mc^$zjN6NMg_j`=*$o1RQeu3_c8=49*O$41Ns3 z46Y1%3?&Q|44_sRv|RhY6!?U`i`U&joWCnBRcRNlz>;VBlboW?tQ+KcDuw!6gDPUk= zNh-)ME-6wy@_>PX;|c=rRw!l(mr69WU|7p6U+kN~kjE@xn1_yxru8O)h3Gl2ZX z%)r0^!ZJ`-#X#LB!@$De0E#EDt63Nr7{{IKtjNmc;|N8&&|7-W}Lrnln zGcYi)G0p=sPoar}1)&5i453^c6v%xzq!>Uh!zDK=jw=LMF(gD8AW(_H1eAM0>4otS z(>n%c26hG}21a8+ML~5zML{FR2}b{}FunULWW?0_P3?CU77}*(E7$1S{0F^e*(?kQHLMK03^7n~HU@r%OemY3L6e~Z%I09;WLOAgb0V?18I&1T zLB)BH*nAAu3}>O@LSVa?85mhOL1h&q2Pl`Ya56A52rw2v*-Q+4jG&Seq=uQnh_MeU z&VrFNgT&@zC}UdW zoL^8`l$oAUqM(tisbFMaXsVD@so<8MR}zw+pHr--;FqremM>N)N-a(;Doah#3rQ_5 zQ3wff3rbBd%}FeRE4EUA$yk9+3QH|2&dkqKFw`?JFaSw~gt+NKHG#MwGZZq574q^+ z6cQCmiV{;&a}$fQ74p-N8~`^xqokyu*h*hNJ+maEG)XTxKUY5~H6tZ4ML#V+ucQRz zS|XI_C*|ZP>E|XEm!uZy`*=FL`USh{l~j~4I5Xrk6fjgW6ftBnq%&kNlrShTXfPx* zXfh};7%>^3R5BmgfQeY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SVTCA[0] /* SetFPVectorToAxis */ + + + + + + SVTCA[0] /* SetFPVectorToAxis */ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SVTCA[0] /* SetFPVectorToAxis */ + SVTCA[1] /* SetFPVectorToAxis */ + + + + + + + + + + + + + SVTCA[0] /* SetFPVectorToAxis */ + SVTCA[1] /* SetFPVectorToAxis */ + + + + + + + + + + + + SVTCA[0] /* SetFPVectorToAxis */ + SVTCA[1] /* SetFPVectorToAxis */ + + + + + + + + + + Copyright (c) 2015 by FontTools. No rights reserved. + + + Test TTF + + + Regular + + + FontTools: Test TTF: 2015 + + + Test TTF + + + Version 1.000 + + + TestTTF-Regular + + + Test TTF is not a trademark of FontTools. + + + FontTools + + + FontTools + + + https://github.com/behdad/fonttools + + + https://github.com/behdad/fonttools + + + https://github.com/behdad/fonttools/blob/master/LICENSE.txt + + + Test TTF + + + Copyright (c) 2015 by FontTools. No rights reserved. + + + Test TTF + + + Regular + + + FontTools: Test TTF: 2015 + + + Test TTF + + + Version 1.000 + + + TestTTF-Regular + + + Test TTF is not a trademark of FontTools. + + + FontTools + + + FontTools + + + https://github.com/behdad/fonttools + + + https://github.com/behdad/fonttools + + + https://github.com/behdad/fonttools/blob/master/LICENSE.txt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/ttx/data/TestWOFF.woff b/Tests/ttx/data/TestWOFF.woff new file mode 100644 index 0000000000000000000000000000000000000000..64cd78dc86433fa450098f671dc2e012b06d7d70 GIT binary patch literal 1328 zcmXT-cXRU(3GruOU^QUiVqjok;ACN71ksq-+09LXfq}(J0mqQ zg@J+L3j+g#83=P!oR!STNKIs5VANq?U{GLSU{GNTVSJF0TT;Qmz$(DNz#sv_>}wbn zR#0PR93A4U&EwJ$U5E*<*(e9XWUK#3cri6bGide|774*d-)HuFSaiH$B~ynzJ4s;j0w%Xk#}91PO?S)RPJXj#`i;qNiVZ?tOf%b6z5aZ@fB*mHzuOu2 z6z@B5@Y~N5`KS4$-sIkNm7T9E|A4n@TDIs}#khy7pDOP;cBDvFJR&-8L8tt**`jvG z*li;1Ru*v2|8V+~q?BLtX6vnL)iaB;ZXR5@dE1*sPir>UG-yBH{WQw8xMTY{|I_Xj z@BeIzIeww_DciX_3!kR7_6zRld^(wH`-Y$j-y+~F}%PZNAx=gw!4Vt;g!o?xi{+}$DXo|kB6S83ek zK9^<0nk?sjX7}k^&tl%4-SS@ZMM9X(^8QY@KPIJu;kt&}#d4Ct691Pn=_fDaniXog z`@_#=bDWKLrO4V|3%ht--TT<2BDMMNqQ(EtkL^kF>hnx9PkX;K39+27wg`!+C|9ji?EB{IvxDB+N1GtZ2VCpHSC9@21NUBmD~k6#sH zb{a$T|7gh`i46=4I)@lvf$~5~LSjlnN{R@Fr>@>d{=&<=UcUNgJ$0BmHU9}D{7^jo z!@2KeyUb^P&S`t^+Vj?#ZLAA_@cwU{!k5@+ffdsy_8vaxtH0hv{kY``%l!*BX&Bos zc2RUwwsZ?Vc;di?5XF^22X*b&tEkx9FOTp)s2|U`W>LgKjja}2SLm3!@~3QMx{$`k z#&)di7$2J(TV0zMo0*#%r)DH;>1=sf`$I?S%>D-cynSR}`T7_BJKwYaJGJq1>F3(_ z%!{u%CvnBp*k5+M^8MQVnneX&>%%3UaG!hs=>E^T?#+eF4699i_A)Sj0p)xK28Lfy Q{E@+&=`sVTTmYAf0I{SKEdT%j literal 0 HcmV?d00001 diff --git a/Tests/ttx/data/TestWOFF2.woff2 b/Tests/ttx/data/TestWOFF2.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..a345573f7f4431265b3079d84394a8a0416239d2 GIT binary patch literal 808 zcmXT-cQayOWME)mVAf#Z1JRs33=B*+AYvdsDrQSyXs+U5;$#%$;mRpzG-Fa>mf+@N zU{PT0NMbkVVU*_P5DcC3{KP&BgKb-v>|H%)%4630MGBt;^Hh#*Xg&Y&!~MGRb@O+A z<_YaqTkWiz8RV;CBecma(D#RUE#rmw`5U)>+W)UQc$>vCP32o67enuD`Cff=LqYC_ z!n*m-i>lP*UpTz`DSJq!cjMP8yY_+y4`)aPD6-Akdi2u-=5KAe%vVy+cia&CYU9vn zTjzrhdAoPgR>&eS%$8fxLtsu;P)f*y3V}XOVg`vK237xadP_ zh{grahO1nSew#8gmS~;KSTkGk?yoBgcXTY;S1$L>i=m-t4nsI=U!l|H52fi3#HT2z zgz~;JVNJcj!78;-^40>uQ$h-_iq7$R_kKPUw0O^&g>1_EWPV<%xxb(`^USihv$ro_ z(%-Xh(|Z@!zU?Uz-d1Uw75;UzJl9w8RsS2N+jYL+dB*3B1^@rwUMjdG`6YLj5J#8n z|NGP5NhZ2jsWC7-{gKM@L%RO)?u{?!ZDdLf^;B+UU}$wX9-+XnSU@49S4d&y3>Alk zduKAGWKS{KCY9>D*44FMIxw_&*LJmq3+J5+l3v)^nfF3>QDj(0%;8xpoLbU0@_A0a z(R8T8;>v8pDJ31=8n1=?12-D{a|!L z{n_vTtu|o!$}M*_uQSVpze>zEyP1~2+4jpwU}J5VB!{NzNu|y2or8dVlWL9c=aj)~K}zhmQ~%X^>9kcisG z>uforVdr6H*+{{+s~h;4)>nnT`F?eJcezvMxn9SW?eg;Ht7Dl|r}fVLt~c5F-{+-i z>O3t^RQZ|A7RY-(e(=n3U4hCDe`fKwe;8+1vWpnq_o=>hq-K@(kr|eAdpDFgJf3{` zk;Db{LWVg{uilw_uzB4Z?tQlF&E)q>A2iQn`r3Y{&uHSJfQ*0fyIy-P+xaV#0RZ-? BYb5{x literal 0 HcmV?d00001 diff --git a/Tests/ttx/ttx_test.py b/Tests/ttx/ttx_test.py new file mode 100644 index 0000000..ceb7abd --- /dev/null +++ b/Tests/ttx/ttx_test.py @@ -0,0 +1,1014 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.misc.testTools import parseXML +from fontTools.misc.timeTools import timestampSinceEpoch +from fontTools.ttLib import TTFont, TTLibError +from fontTools import ttx +import getopt +import logging +import os +import shutil +import sys +import tempfile +import unittest + +import pytest + +try: + import zopfli +except ImportError: + zopfli = None +try: + import brotli +except ImportError: + brotli = None + + +class TTXTest(unittest.TestCase): + + def __init__(self, methodName): + unittest.TestCase.__init__(self, methodName) + # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, + # and fires deprecation warnings if a program uses the old name. + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + + def setUp(self): + self.tempdir = None + self.num_tempfiles = 0 + + def tearDown(self): + if self.tempdir: + shutil.rmtree(self.tempdir) + + @staticmethod + def getpath(testfile): + path, _ = os.path.split(__file__) + return os.path.join(path, "data", testfile) + + def temp_dir(self): + if not self.tempdir: + self.tempdir = tempfile.mkdtemp() + + def temp_font(self, font_path, file_name): + self.temp_dir() + temppath = os.path.join(self.tempdir, file_name) + shutil.copy2(font_path, temppath) + return temppath + + @staticmethod + def read_file(file_path): + with open(file_path, "r", encoding="utf-8") as f: + return f.readlines() + + # ----- + # Tests + # ----- + + def test_parseOptions_no_args(self): + with self.assertRaises(getopt.GetoptError) as cm: + ttx.parseOptions([]) + self.assertTrue( + "Must specify at least one input file" in str(cm.exception) + ) + + def test_parseOptions_invalid_path(self): + file_path = "invalid_font_path" + with self.assertRaises(getopt.GetoptError) as cm: + ttx.parseOptions([file_path]) + self.assertTrue('File not found: "%s"' % file_path in str(cm.exception)) + + def test_parseOptions_font2ttx_1st_time(self): + file_name = "TestOTF.otf" + font_path = self.getpath(file_name) + temp_path = self.temp_font(font_path, file_name) + jobs, _ = ttx.parseOptions([temp_path]) + self.assertEqual(jobs[0][0].__name__, "ttDump") + self.assertEqual( + jobs[0][1:], + ( + os.path.join(self.tempdir, file_name), + os.path.join(self.tempdir, file_name.split(".")[0] + ".ttx"), + ), + ) + + def test_parseOptions_font2ttx_2nd_time(self): + file_name = "TestTTF.ttf" + font_path = self.getpath(file_name) + temp_path = self.temp_font(font_path, file_name) + _, _ = ttx.parseOptions([temp_path]) # this is NOT a mistake + jobs, _ = ttx.parseOptions([temp_path]) + self.assertEqual(jobs[0][0].__name__, "ttDump") + self.assertEqual( + jobs[0][1:], + ( + os.path.join(self.tempdir, file_name), + os.path.join(self.tempdir, file_name.split(".")[0] + "#1.ttx"), + ), + ) + + def test_parseOptions_ttx2font_1st_time(self): + file_name = "TestTTF.ttx" + font_path = self.getpath(file_name) + temp_path = self.temp_font(font_path, file_name) + jobs, _ = ttx.parseOptions([temp_path]) + self.assertEqual(jobs[0][0].__name__, "ttCompile") + self.assertEqual( + jobs[0][1:], + ( + os.path.join(self.tempdir, file_name), + os.path.join(self.tempdir, file_name.split(".")[0] + ".ttf"), + ), + ) + + def test_parseOptions_ttx2font_2nd_time(self): + file_name = "TestOTF.ttx" + font_path = self.getpath(file_name) + temp_path = self.temp_font(font_path, file_name) + _, _ = ttx.parseOptions([temp_path]) # this is NOT a mistake + jobs, _ = ttx.parseOptions([temp_path]) + self.assertEqual(jobs[0][0].__name__, "ttCompile") + self.assertEqual( + jobs[0][1:], + ( + os.path.join(self.tempdir, file_name), + os.path.join(self.tempdir, file_name.split(".")[0] + "#1.otf"), + ), + ) + + def test_parseOptions_multiple_fonts(self): + file_names = ["TestOTF.otf", "TestTTF.ttf"] + font_paths = [self.getpath(file_name) for file_name in file_names] + temp_paths = [ + self.temp_font(font_path, file_name) + for font_path, file_name in zip(font_paths, file_names) + ] + jobs, _ = ttx.parseOptions(temp_paths) + for i in range(len(jobs)): + self.assertEqual(jobs[i][0].__name__, "ttDump") + self.assertEqual( + jobs[i][1:], + ( + os.path.join(self.tempdir, file_names[i]), + os.path.join( + self.tempdir, file_names[i].split(".")[0] + ".ttx" + ), + ), + ) + + def test_parseOptions_mixed_files(self): + operations = ["ttDump", "ttCompile"] + extensions = [".ttx", ".ttf"] + file_names = ["TestOTF.otf", "TestTTF.ttx"] + font_paths = [self.getpath(file_name) for file_name in file_names] + temp_paths = [ + self.temp_font(font_path, file_name) + for font_path, file_name in zip(font_paths, file_names) + ] + jobs, _ = ttx.parseOptions(temp_paths) + for i in range(len(jobs)): + self.assertEqual(jobs[i][0].__name__, operations[i]) + self.assertEqual( + jobs[i][1:], + ( + os.path.join(self.tempdir, file_names[i]), + os.path.join( + self.tempdir, + file_names[i].split(".")[0] + extensions[i], + ), + ), + ) + + def test_parseOptions_splitTables(self): + file_name = "TestTTF.ttf" + font_path = self.getpath(file_name) + temp_path = self.temp_font(font_path, file_name) + args = ["-s", temp_path] + + jobs, options = ttx.parseOptions(args) + + ttx_file_path = jobs[0][2] + temp_folder = os.path.dirname(ttx_file_path) + self.assertTrue(options.splitTables) + self.assertTrue(os.path.exists(ttx_file_path)) + + ttx.process(jobs, options) + + # Read the TTX file but strip the first two and the last lines: + # + # + # ... + # + parsed_xml = parseXML(self.read_file(ttx_file_path)[2:-1]) + for item in parsed_xml: + if not isinstance(item, tuple): + continue + # the tuple looks like this: + # (u'head', {u'src': u'TestTTF._h_e_a_d.ttx'}, []) + table_file_name = item[1].get("src") + table_file_path = os.path.join(temp_folder, table_file_name) + self.assertTrue(os.path.exists(table_file_path)) + + def test_parseOptions_splitGlyphs(self): + file_name = "TestTTF.ttf" + font_path = self.getpath(file_name) + temp_path = self.temp_font(font_path, file_name) + args = ["-g", temp_path] + + jobs, options = ttx.parseOptions(args) + + ttx_file_path = jobs[0][2] + temp_folder = os.path.dirname(ttx_file_path) + self.assertTrue(options.splitGlyphs) + # splitGlyphs also forces splitTables + self.assertTrue(options.splitTables) + self.assertTrue(os.path.exists(ttx_file_path)) + + ttx.process(jobs, options) + + # Read the TTX file but strip the first two and the last lines: + # + # + # ... + # + for item in parseXML(self.read_file(ttx_file_path)[2:-1]): + if not isinstance(item, tuple): + continue + # the tuple looks like this: + # (u'head', {u'src': u'TestTTF._h_e_a_d.ttx'}, []) + table_tag = item[0] + table_file_name = item[1].get("src") + table_file_path = os.path.join(temp_folder, table_file_name) + self.assertTrue(os.path.exists(table_file_path)) + if table_tag != "glyf": + continue + # also strip the enclosing 'glyf' element + for item in parseXML(self.read_file(table_file_path)[4:-3]): + if not isinstance(item, tuple): + continue + # glyphs without outline data only have 'name' attribute + glyph_file_name = item[1].get("src") + if glyph_file_name is not None: + glyph_file_path = os.path.join(temp_folder, glyph_file_name) + self.assertTrue(os.path.exists(glyph_file_path)) + + def test_guessFileType_ttf(self): + file_name = "TestTTF.ttf" + font_path = self.getpath(file_name) + self.assertEqual(ttx.guessFileType(font_path), "TTF") + + def test_guessFileType_otf(self): + file_name = "TestOTF.otf" + font_path = self.getpath(file_name) + self.assertEqual(ttx.guessFileType(font_path), "OTF") + + def test_guessFileType_woff(self): + file_name = "TestWOFF.woff" + font_path = self.getpath(file_name) + self.assertEqual(ttx.guessFileType(font_path), "WOFF") + + def test_guessFileType_woff2(self): + file_name = "TestWOFF2.woff2" + font_path = self.getpath(file_name) + self.assertEqual(ttx.guessFileType(font_path), "WOFF2") + + def test_guessFileType_ttc(self): + file_name = "TestTTC.ttc" + font_path = self.getpath(file_name) + self.assertEqual(ttx.guessFileType(font_path), "TTC") + + def test_guessFileType_dfont(self): + file_name = "TestDFONT.dfont" + font_path = self.getpath(file_name) + self.assertEqual(ttx.guessFileType(font_path), "TTF") + + def test_guessFileType_ttx_ttf(self): + file_name = "TestTTF.ttx" + font_path = self.getpath(file_name) + self.assertEqual(ttx.guessFileType(font_path), "TTX") + + def test_guessFileType_ttx_otf(self): + file_name = "TestOTF.ttx" + font_path = self.getpath(file_name) + self.assertEqual(ttx.guessFileType(font_path), "OTX") + + def test_guessFileType_ttx_bom(self): + file_name = "TestBOM.ttx" + font_path = self.getpath(file_name) + self.assertEqual(ttx.guessFileType(font_path), "TTX") + + def test_guessFileType_ttx_no_sfntVersion(self): + file_name = "TestNoSFNT.ttx" + font_path = self.getpath(file_name) + self.assertEqual(ttx.guessFileType(font_path), "TTX") + + def test_guessFileType_ttx_no_xml(self): + file_name = "TestNoXML.ttx" + font_path = self.getpath(file_name) + self.assertIsNone(ttx.guessFileType(font_path)) + + def test_guessFileType_invalid_path(self): + font_path = "invalid_font_path" + self.assertIsNone(ttx.guessFileType(font_path)) + + +# ----------------------- +# ttx.Options class tests +# ----------------------- + + +def test_options_flag_h(capsys): + with pytest.raises(SystemExit): + ttx.Options([("-h", None)], 1) + + out, err = capsys.readouterr() + assert "TTX -- From OpenType To XML And Back" in out + + +def test_options_flag_version(capsys): + with pytest.raises(SystemExit): + ttx.Options([("--version", None)], 1) + + out, err = capsys.readouterr() + version_list = out.split(".") + assert len(version_list) >= 3 + assert version_list[0].isdigit() + assert version_list[1].isdigit() + assert version_list[2].strip().isdigit() + + +def test_options_d_goodpath(tmpdir): + temp_dir_path = str(tmpdir) + tto = ttx.Options([("-d", temp_dir_path)], 1) + assert tto.outputDir == temp_dir_path + + +def test_options_d_badpath(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("-d", "bogusdir")], 1) + + +def test_options_o(): + tto = ttx.Options([("-o", "testfile.ttx")], 1) + assert tto.outputFile == "testfile.ttx" + + +def test_options_f(): + tto = ttx.Options([("-f", "")], 1) + assert tto.overWrite is True + + +def test_options_v(): + tto = ttx.Options([("-v", "")], 1) + assert tto.verbose is True + assert tto.logLevel == logging.DEBUG + + +def test_options_q(): + tto = ttx.Options([("-q", "")], 1) + assert tto.quiet is True + assert tto.logLevel == logging.WARNING + + +def test_options_l(): + tto = ttx.Options([("-l", "")], 1) + assert tto.listTables is True + + +def test_options_t_nopadding(): + tto = ttx.Options([("-t", "CFF2")], 1) + assert len(tto.onlyTables) == 1 + assert tto.onlyTables[0] == "CFF2" + + +def test_options_t_withpadding(): + tto = ttx.Options([("-t", "CFF")], 1) + assert len(tto.onlyTables) == 1 + assert tto.onlyTables[0] == "CFF " + + +def test_options_s(): + tto = ttx.Options([("-s", "")], 1) + assert tto.splitTables is True + assert tto.splitGlyphs is False + + +def test_options_g(): + tto = ttx.Options([("-g", "")], 1) + assert tto.splitGlyphs is True + assert tto.splitTables is True + + +def test_options_i(): + tto = ttx.Options([("-i", "")], 1) + assert tto.disassembleInstructions is False + + +def test_options_z_validoptions(): + valid_options = ("raw", "row", "bitwise", "extfile") + for option in valid_options: + tto = ttx.Options([("-z", option)], 1) + assert tto.bitmapGlyphDataFormat == option + + +def test_options_z_invalidoption(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("-z", "bogus")], 1) + + +def test_options_y_validvalue(): + tto = ttx.Options([("-y", "1")], 1) + assert tto.fontNumber == 1 + + +def test_options_y_invalidvalue(): + with pytest.raises(ValueError): + ttx.Options([("-y", "A")], 1) + + +def test_options_m(): + tto = ttx.Options([("-m", "testfont.ttf")], 1) + assert tto.mergeFile == "testfont.ttf" + + +def test_options_b(): + tto = ttx.Options([("-b", "")], 1) + assert tto.recalcBBoxes is False + + +def test_options_a(): + tto = ttx.Options([("-a", "")], 1) + assert tto.allowVID is True + + +def test_options_e(): + tto = ttx.Options([("-e", "")], 1) + assert tto.ignoreDecompileErrors is False + + +def test_options_unicodedata(): + tto = ttx.Options([("--unicodedata", "UnicodeData.txt")], 1) + assert tto.unicodedata == "UnicodeData.txt" + + +def test_options_newline_lf(): + tto = ttx.Options([("--newline", "LF")], 1) + assert tto.newlinestr == "\n" + + +def test_options_newline_cr(): + tto = ttx.Options([("--newline", "CR")], 1) + assert tto.newlinestr == "\r" + + +def test_options_newline_crlf(): + tto = ttx.Options([("--newline", "CRLF")], 1) + assert tto.newlinestr == "\r\n" + + +def test_options_newline_invalid(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("--newline", "BOGUS")], 1) + + +def test_options_recalc_timestamp(): + tto = ttx.Options([("--recalc-timestamp", "")], 1) + assert tto.recalcTimestamp is True + + +def test_options_flavor(): + tto = ttx.Options([("--flavor", "woff")], 1) + assert tto.flavor == "woff" + + +def test_options_with_zopfli(): + tto = ttx.Options([("--with-zopfli", ""), ("--flavor", "woff")], 1) + assert tto.useZopfli is True + + +def test_options_with_zopfli_fails_without_woff_flavor(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("--with-zopfli", "")], 1) + + +def test_options_quiet_and_verbose_shouldfail(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("-q", ""), ("-v", "")], 1) + + +def test_options_mergefile_and_flavor_shouldfail(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("-m", "testfont.ttf"), ("--flavor", "woff")], 1) + + +def test_options_onlytables_and_skiptables_shouldfail(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("-t", "CFF"), ("-x", "CFF2")], 1) + + +def test_options_mergefile_and_multiplefiles_shouldfail(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("-m", "testfont.ttf")], 2) + + +def test_options_woff2_and_zopfli_shouldfail(): + with pytest.raises(getopt.GetoptError): + ttx.Options([("--with-zopfli", ""), ("--flavor", "woff2")], 1) + + +# ---------------------------- +# ttx.ttCompile function tests +# ---------------------------- + + +def test_ttcompile_otf_compile_default(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestOTF.ttx") + # outotf = os.path.join(str(tmpdir), "TestOTF.otf") + outotf = tmpdir.join("TestOTF.ttx") + default_options = ttx.Options([], 1) + ttx.ttCompile(inttx, str(outotf), default_options) + # confirm that font was built + assert outotf.check(file=True) + # confirm that it is valid OTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(str(outotf)) + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "post", + "CFF ", + "hmtx", + "DSIG", + ) + for table in expected_tables: + assert table in ttf + + +def test_ttcompile_otf_to_woff_without_zopfli(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestOTF.ttx") + outwoff = tmpdir.join("TestOTF.woff") + options = ttx.Options([], 1) + options.flavor = "woff" + ttx.ttCompile(inttx, str(outwoff), options) + # confirm that font was built + assert outwoff.check(file=True) + # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(str(outwoff)) + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "post", + "CFF ", + "hmtx", + "DSIG", + ) + for table in expected_tables: + assert table in ttf + + +@pytest.mark.skipif(zopfli is None, reason="zopfli not installed") +def test_ttcompile_otf_to_woff_with_zopfli(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestOTF.ttx") + outwoff = tmpdir.join("TestOTF.woff") + options = ttx.Options([], 1) + options.flavor = "woff" + options.useZopfli = True + ttx.ttCompile(inttx, str(outwoff), options) + # confirm that font was built + assert outwoff.check(file=True) + # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(str(outwoff)) + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "post", + "CFF ", + "hmtx", + "DSIG", + ) + for table in expected_tables: + assert table in ttf + + +@pytest.mark.skipif(brotli is None, reason="brotli not installed") +def test_ttcompile_otf_to_woff2(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestOTF.ttx") + outwoff2 = tmpdir.join("TestTTF.woff2") + options = ttx.Options([], 1) + options.flavor = "woff2" + ttx.ttCompile(inttx, str(outwoff2), options) + # confirm that font was built + assert outwoff2.check(file=True) + # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(str(outwoff2)) + # DSIG should not be included from original ttx as per woff2 spec (https://dev.w3.org/webfonts/WOFF2/spec/) + assert "DSIG" not in ttf + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "post", + "CFF ", + "hmtx", + ) + for table in expected_tables: + assert table in ttf + + +def test_ttcompile_ttf_compile_default(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outttf = tmpdir.join("TestTTF.ttf") + default_options = ttx.Options([], 1) + ttx.ttCompile(inttx, str(outttf), default_options) + # confirm that font was built + assert outttf.check(file=True) + # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(str(outttf)) + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "hmtx", + "fpgm", + "prep", + "cvt ", + "loca", + "glyf", + "post", + "gasp", + "DSIG", + ) + for table in expected_tables: + assert table in ttf + + +def test_ttcompile_ttf_to_woff_without_zopfli(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outwoff = tmpdir.join("TestTTF.woff") + options = ttx.Options([], 1) + options.flavor = "woff" + ttx.ttCompile(inttx, str(outwoff), options) + # confirm that font was built + assert outwoff.check(file=True) + # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(str(outwoff)) + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "hmtx", + "fpgm", + "prep", + "cvt ", + "loca", + "glyf", + "post", + "gasp", + "DSIG", + ) + for table in expected_tables: + assert table in ttf + + +@pytest.mark.skipif(zopfli is None, reason="zopfli not installed") +def test_ttcompile_ttf_to_woff_with_zopfli(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outwoff = tmpdir.join("TestTTF.woff") + options = ttx.Options([], 1) + options.flavor = "woff" + options.useZopfli = True + ttx.ttCompile(inttx, str(outwoff), options) + # confirm that font was built + assert outwoff.check(file=True) + # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(str(outwoff)) + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "hmtx", + "fpgm", + "prep", + "cvt ", + "loca", + "glyf", + "post", + "gasp", + "DSIG", + ) + for table in expected_tables: + assert table in ttf + + +@pytest.mark.skipif(brotli is None, reason="brotli not installed") +def test_ttcompile_ttf_to_woff2(tmpdir): + inttx = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outwoff2 = tmpdir.join("TestTTF.woff2") + options = ttx.Options([], 1) + options.flavor = "woff2" + ttx.ttCompile(inttx, str(outwoff2), options) + # confirm that font was built + assert outwoff2.check(file=True) + # confirm that it is valid TTF file, can instantiate a TTFont, has expected OpenType tables + ttf = TTFont(str(outwoff2)) + # DSIG should not be included from original ttx as per woff2 spec (https://dev.w3.org/webfonts/WOFF2/spec/) + assert "DSIG" not in ttf + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "hmtx", + "fpgm", + "prep", + "cvt ", + "loca", + "glyf", + "post", + "gasp", + ) + for table in expected_tables: + assert table in ttf + + +@pytest.mark.parametrize( + "inpath, outpath1, outpath2", + [ + ("TestTTF.ttx", "TestTTF1.ttf", "TestTTF2.ttf"), + ("TestOTF.ttx", "TestOTF1.otf", "TestOTF2.otf"), + ], +) +def test_ttcompile_timestamp_calcs(inpath, outpath1, outpath2, tmpdir): + inttx = os.path.join("Tests", "ttx", "data", inpath) + outttf1 = tmpdir.join(outpath1) + outttf2 = tmpdir.join(outpath2) + options = ttx.Options([], 1) + # build with default options = do not recalculate timestamp + ttx.ttCompile(inttx, str(outttf1), options) + # confirm that font was built + assert outttf1.check(file=True) + # confirm that timestamp is same as modified time on ttx file + mtime = os.path.getmtime(inttx) + epochtime = timestampSinceEpoch(mtime) + ttf = TTFont(str(outttf1)) + assert ttf["head"].modified == epochtime + + # reset options to recalculate the timestamp and compile new font + options.recalcTimestamp = True + ttx.ttCompile(inttx, str(outttf2), options) + # confirm that font was built + assert outttf2.check(file=True) + # confirm that timestamp is more recent than modified time on ttx file + mtime = os.path.getmtime(inttx) + epochtime = timestampSinceEpoch(mtime) + ttf = TTFont(str(outttf2)) + assert ttf["head"].modified > epochtime + + +# ------------------------- +# ttx.ttList function tests +# ------------------------- + + +def test_ttlist_ttf(capsys, tmpdir): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttf") + fakeoutpath = tmpdir.join("TestTTF.ttx") + options = ttx.Options([], 1) + options.listTables = True + ttx.ttList(inpath, str(fakeoutpath), options) + out, err = capsys.readouterr() + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "hmtx", + "fpgm", + "prep", + "cvt ", + "loca", + "glyf", + "post", + "gasp", + "DSIG", + ) + # confirm that expected tables are printed to stdout + for table in expected_tables: + assert table in out + # test for one of the expected tag/checksum/length/offset strings + assert "OS/2 0x67230FF8 96 376" in out + + +def test_ttlist_otf(capsys, tmpdir): + inpath = os.path.join("Tests", "ttx", "data", "TestOTF.otf") + fakeoutpath = tmpdir.join("TestOTF.ttx") + options = ttx.Options([], 1) + options.listTables = True + ttx.ttList(inpath, str(fakeoutpath), options) + out, err = capsys.readouterr() + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "post", + "CFF ", + "hmtx", + "DSIG", + ) + # confirm that expected tables are printed to stdout + for table in expected_tables: + assert table in out + # test for one of the expected tag/checksum/length/offset strings + assert "OS/2 0x67230FF8 96 272" in out + + +def test_ttlist_woff(capsys, tmpdir): + inpath = os.path.join("Tests", "ttx", "data", "TestWOFF.woff") + fakeoutpath = tmpdir.join("TestWOFF.ttx") + options = ttx.Options([], 1) + options.listTables = True + options.flavor = "woff" + ttx.ttList(inpath, str(fakeoutpath), options) + out, err = capsys.readouterr() + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "post", + "CFF ", + "hmtx", + "DSIG", + ) + # confirm that expected tables are printed to stdout + for table in expected_tables: + assert table in out + # test for one of the expected tag/checksum/length/offset strings + assert "OS/2 0x67230FF8 84 340" in out + + +@pytest.mark.skipif(brotli is None, reason="brotli not installed") +def test_ttlist_woff2(capsys, tmpdir): + inpath = os.path.join("Tests", "ttx", "data", "TestWOFF2.woff2") + fakeoutpath = tmpdir.join("TestWOFF2.ttx") + options = ttx.Options([], 1) + options.listTables = True + options.flavor = "woff2" + ttx.ttList(inpath, str(fakeoutpath), options) + out, err = capsys.readouterr() + expected_tables = ( + "head", + "hhea", + "maxp", + "OS/2", + "name", + "cmap", + "hmtx", + "fpgm", + "prep", + "cvt ", + "loca", + "glyf", + "post", + "gasp", + ) + # confirm that expected tables are printed to stdout + for table in expected_tables: + assert table in out + # test for one of the expected tag/checksum/length/offset strings + assert "OS/2 0x67230FF8 96 0" in out + + +# ------------------- +# main function tests +# ------------------- + + +def test_main_default_ttf_dump_to_ttx(tmpdir): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttf") + outpath = tmpdir.join("TestTTF.ttx") + args = ["-o", str(outpath), inpath] + ttx.main(args) + assert outpath.check(file=True) + + +def test_main_default_ttx_compile_to_ttf(tmpdir): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outpath = tmpdir.join("TestTTF.ttf") + args = ["-o", str(outpath), inpath] + ttx.main(args) + assert outpath.check(file=True) + + +def test_main_getopterror_missing_directory(): + with pytest.raises(SystemExit): + with pytest.raises(getopt.GetoptError): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttf") + args = ["-d", "bogusdir", inpath] + ttx.main(args) + + +def test_main_keyboard_interrupt(tmpdir, monkeypatch, capsys): + with pytest.raises(SystemExit): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outpath = tmpdir.join("TestTTF.ttf") + args = ["-o", str(outpath), inpath] + monkeypatch.setattr( + ttx, "process", (lambda x, y: raise_exception(KeyboardInterrupt)) + ) + ttx.main(args) + + out, err = capsys.readouterr() + assert "(Cancelled.)" in err + + +@pytest.mark.skipif( + sys.platform == "win32", + reason="waitForKeyPress function causes test to hang on Windows platform", +) +def test_main_system_exit(tmpdir, monkeypatch): + with pytest.raises(SystemExit): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outpath = tmpdir.join("TestTTF.ttf") + args = ["-o", str(outpath), inpath] + monkeypatch.setattr( + ttx, "process", (lambda x, y: raise_exception(SystemExit)) + ) + ttx.main(args) + + +def test_main_ttlib_error(tmpdir, monkeypatch, capsys): + with pytest.raises(SystemExit): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outpath = tmpdir.join("TestTTF.ttf") + args = ["-o", str(outpath), inpath] + monkeypatch.setattr( + ttx, + "process", + (lambda x, y: raise_exception(TTLibError("Test error"))), + ) + ttx.main(args) + + out, err = capsys.readouterr() + assert "Test error" in err + + +@pytest.mark.skipif( + sys.platform == "win32", + reason="waitForKeyPress function causes test to hang on Windows platform", +) +def test_main_base_exception(tmpdir, monkeypatch, capsys): + with pytest.raises(SystemExit): + inpath = os.path.join("Tests", "ttx", "data", "TestTTF.ttx") + outpath = tmpdir.join("TestTTF.ttf") + args = ["-o", str(outpath), inpath] + monkeypatch.setattr( + ttx, + "process", + (lambda x, y: raise_exception(Exception("Test error"))), + ) + ttx.main(args) + + out, err = capsys.readouterr() + assert "Unhandled exception has occurred" in err + + +# --------------------------- +# support functions for tests +# --------------------------- + + +def raise_exception(exception): + raise exception diff --git a/Tests/unicodedata_test.py b/Tests/unicodedata_test.py new file mode 100644 index 0000000..96c0f01 --- /dev/null +++ b/Tests/unicodedata_test.py @@ -0,0 +1,254 @@ +from __future__ import ( + print_function, division, absolute_import, unicode_literals) +from fontTools.misc.py23 import * + +from fontTools import unicodedata + +import pytest + + +def test_script(): + assert unicodedata.script("a") == "Latn" + assert unicodedata.script(unichr(0)) == "Zyyy" + assert unicodedata.script(unichr(0x0378)) == "Zzzz" + assert unicodedata.script(unichr(0x10FFFF)) == "Zzzz" + + # these were randomly sampled, one character per script + assert unicodedata.script(unichr(0x1E918)) == 'Adlm' + assert unicodedata.script(unichr(0x1170D)) == 'Ahom' + assert unicodedata.script(unichr(0x145A0)) == 'Hluw' + assert unicodedata.script(unichr(0x0607)) == 'Arab' + assert unicodedata.script(unichr(0x056C)) == 'Armn' + assert unicodedata.script(unichr(0x10B27)) == 'Avst' + assert unicodedata.script(unichr(0x1B41)) == 'Bali' + assert unicodedata.script(unichr(0x168AD)) == 'Bamu' + assert unicodedata.script(unichr(0x16ADD)) == 'Bass' + assert unicodedata.script(unichr(0x1BE5)) == 'Batk' + assert unicodedata.script(unichr(0x09F3)) == 'Beng' + assert unicodedata.script(unichr(0x11C5B)) == 'Bhks' + assert unicodedata.script(unichr(0x3126)) == 'Bopo' + assert unicodedata.script(unichr(0x1103B)) == 'Brah' + assert unicodedata.script(unichr(0x2849)) == 'Brai' + assert unicodedata.script(unichr(0x1A0A)) == 'Bugi' + assert unicodedata.script(unichr(0x174E)) == 'Buhd' + assert unicodedata.script(unichr(0x18EE)) == 'Cans' + assert unicodedata.script(unichr(0x102B7)) == 'Cari' + assert unicodedata.script(unichr(0x1053D)) == 'Aghb' + assert unicodedata.script(unichr(0x11123)) == 'Cakm' + assert unicodedata.script(unichr(0xAA1F)) == 'Cham' + assert unicodedata.script(unichr(0xAB95)) == 'Cher' + assert unicodedata.script(unichr(0x1F0C7)) == 'Zyyy' + assert unicodedata.script(unichr(0x2C85)) == 'Copt' + assert unicodedata.script(unichr(0x12014)) == 'Xsux' + assert unicodedata.script(unichr(0x1082E)) == 'Cprt' + assert unicodedata.script(unichr(0xA686)) == 'Cyrl' + assert unicodedata.script(unichr(0x10417)) == 'Dsrt' + assert unicodedata.script(unichr(0x093E)) == 'Deva' + assert unicodedata.script(unichr(0x1BC4B)) == 'Dupl' + assert unicodedata.script(unichr(0x1310C)) == 'Egyp' + assert unicodedata.script(unichr(0x1051C)) == 'Elba' + assert unicodedata.script(unichr(0x2DA6)) == 'Ethi' + assert unicodedata.script(unichr(0x10AD)) == 'Geor' + assert unicodedata.script(unichr(0x2C52)) == 'Glag' + assert unicodedata.script(unichr(0x10343)) == 'Goth' + assert unicodedata.script(unichr(0x11371)) == 'Gran' + assert unicodedata.script(unichr(0x03D0)) == 'Grek' + assert unicodedata.script(unichr(0x0AAA)) == 'Gujr' + assert unicodedata.script(unichr(0x0A4C)) == 'Guru' + assert unicodedata.script(unichr(0x23C9F)) == 'Hani' + assert unicodedata.script(unichr(0xC259)) == 'Hang' + assert unicodedata.script(unichr(0x1722)) == 'Hano' + assert unicodedata.script(unichr(0x108F5)) == 'Hatr' + assert unicodedata.script(unichr(0x05C2)) == 'Hebr' + assert unicodedata.script(unichr(0x1B072)) == 'Hira' + assert unicodedata.script(unichr(0x10847)) == 'Armi' + assert unicodedata.script(unichr(0x033A)) == 'Zinh' + assert unicodedata.script(unichr(0x10B66)) == 'Phli' + assert unicodedata.script(unichr(0x10B4B)) == 'Prti' + assert unicodedata.script(unichr(0xA98A)) == 'Java' + assert unicodedata.script(unichr(0x110B2)) == 'Kthi' + assert unicodedata.script(unichr(0x0CC6)) == 'Knda' + assert unicodedata.script(unichr(0x3337)) == 'Kana' + assert unicodedata.script(unichr(0xA915)) == 'Kali' + assert unicodedata.script(unichr(0x10A2E)) == 'Khar' + assert unicodedata.script(unichr(0x17AA)) == 'Khmr' + assert unicodedata.script(unichr(0x11225)) == 'Khoj' + assert unicodedata.script(unichr(0x112B6)) == 'Sind' + assert unicodedata.script(unichr(0x0ED7)) == 'Laoo' + assert unicodedata.script(unichr(0xAB3C)) == 'Latn' + assert unicodedata.script(unichr(0x1C48)) == 'Lepc' + assert unicodedata.script(unichr(0x1923)) == 'Limb' + assert unicodedata.script(unichr(0x1071D)) == 'Lina' + assert unicodedata.script(unichr(0x100EC)) == 'Linb' + assert unicodedata.script(unichr(0xA4E9)) == 'Lisu' + assert unicodedata.script(unichr(0x10284)) == 'Lyci' + assert unicodedata.script(unichr(0x10926)) == 'Lydi' + assert unicodedata.script(unichr(0x11161)) == 'Mahj' + assert unicodedata.script(unichr(0x0D56)) == 'Mlym' + assert unicodedata.script(unichr(0x0856)) == 'Mand' + assert unicodedata.script(unichr(0x10AF0)) == 'Mani' + assert unicodedata.script(unichr(0x11CB0)) == 'Marc' + assert unicodedata.script(unichr(0x11D28)) == 'Gonm' + assert unicodedata.script(unichr(0xABDD)) == 'Mtei' + assert unicodedata.script(unichr(0x1E897)) == 'Mend' + assert unicodedata.script(unichr(0x109B0)) == 'Merc' + assert unicodedata.script(unichr(0x10993)) == 'Mero' + assert unicodedata.script(unichr(0x16F5D)) == 'Plrd' + assert unicodedata.script(unichr(0x1160B)) == 'Modi' + assert unicodedata.script(unichr(0x18A8)) == 'Mong' + assert unicodedata.script(unichr(0x16A48)) == 'Mroo' + assert unicodedata.script(unichr(0x1128C)) == 'Mult' + assert unicodedata.script(unichr(0x105B)) == 'Mymr' + assert unicodedata.script(unichr(0x108AF)) == 'Nbat' + assert unicodedata.script(unichr(0x19B3)) == 'Talu' + assert unicodedata.script(unichr(0x1143D)) == 'Newa' + assert unicodedata.script(unichr(0x07F4)) == 'Nkoo' + assert unicodedata.script(unichr(0x1B192)) == 'Nshu' + assert unicodedata.script(unichr(0x169C)) == 'Ogam' + assert unicodedata.script(unichr(0x1C56)) == 'Olck' + assert unicodedata.script(unichr(0x10CE9)) == 'Hung' + assert unicodedata.script(unichr(0x10316)) == 'Ital' + assert unicodedata.script(unichr(0x10A93)) == 'Narb' + assert unicodedata.script(unichr(0x1035A)) == 'Perm' + assert unicodedata.script(unichr(0x103D5)) == 'Xpeo' + assert unicodedata.script(unichr(0x10A65)) == 'Sarb' + assert unicodedata.script(unichr(0x10C09)) == 'Orkh' + assert unicodedata.script(unichr(0x0B60)) == 'Orya' + assert unicodedata.script(unichr(0x104CF)) == 'Osge' + assert unicodedata.script(unichr(0x104A8)) == 'Osma' + assert unicodedata.script(unichr(0x16B12)) == 'Hmng' + assert unicodedata.script(unichr(0x10879)) == 'Palm' + assert unicodedata.script(unichr(0x11AF1)) == 'Pauc' + assert unicodedata.script(unichr(0xA869)) == 'Phag' + assert unicodedata.script(unichr(0x10909)) == 'Phnx' + assert unicodedata.script(unichr(0x10B81)) == 'Phlp' + assert unicodedata.script(unichr(0xA941)) == 'Rjng' + assert unicodedata.script(unichr(0x16C3)) == 'Runr' + assert unicodedata.script(unichr(0x0814)) == 'Samr' + assert unicodedata.script(unichr(0xA88C)) == 'Saur' + assert unicodedata.script(unichr(0x111C8)) == 'Shrd' + assert unicodedata.script(unichr(0x1045F)) == 'Shaw' + assert unicodedata.script(unichr(0x115AD)) == 'Sidd' + assert unicodedata.script(unichr(0x1D8C0)) == 'Sgnw' + assert unicodedata.script(unichr(0x0DB9)) == 'Sinh' + assert unicodedata.script(unichr(0x110F9)) == 'Sora' + assert unicodedata.script(unichr(0x11A60)) == 'Soyo' + assert unicodedata.script(unichr(0x1B94)) == 'Sund' + assert unicodedata.script(unichr(0xA81F)) == 'Sylo' + assert unicodedata.script(unichr(0x0740)) == 'Syrc' + assert unicodedata.script(unichr(0x1714)) == 'Tglg' + assert unicodedata.script(unichr(0x1761)) == 'Tagb' + assert unicodedata.script(unichr(0x1965)) == 'Tale' + assert unicodedata.script(unichr(0x1A32)) == 'Lana' + assert unicodedata.script(unichr(0xAA86)) == 'Tavt' + assert unicodedata.script(unichr(0x116A5)) == 'Takr' + assert unicodedata.script(unichr(0x0B8E)) == 'Taml' + assert unicodedata.script(unichr(0x1754D)) == 'Tang' + assert unicodedata.script(unichr(0x0C40)) == 'Telu' + assert unicodedata.script(unichr(0x07A4)) == 'Thaa' + assert unicodedata.script(unichr(0x0E42)) == 'Thai' + assert unicodedata.script(unichr(0x0F09)) == 'Tibt' + assert unicodedata.script(unichr(0x2D3A)) == 'Tfng' + assert unicodedata.script(unichr(0x114B0)) == 'Tirh' + assert unicodedata.script(unichr(0x1038B)) == 'Ugar' + assert unicodedata.script(unichr(0xA585)) == 'Vaii' + assert unicodedata.script(unichr(0x118CF)) == 'Wara' + assert unicodedata.script(unichr(0xA066)) == 'Yiii' + assert unicodedata.script(unichr(0x11A31)) == 'Zanb' + + +def test_script_extension(): + assert unicodedata.script_extension("a") == {"Latn"} + assert unicodedata.script_extension(unichr(0)) == {"Zyyy"} + assert unicodedata.script_extension(unichr(0x0378)) == {"Zzzz"} + assert unicodedata.script_extension(unichr(0x10FFFF)) == {"Zzzz"} + + assert unicodedata.script_extension("\u0660") == {'Arab', 'Thaa'} + assert unicodedata.script_extension("\u0964") == { + 'Beng', 'Deva', 'Gran', 'Gujr', 'Guru', 'Knda', 'Mahj', 'Mlym', + 'Orya', 'Sind', 'Sinh', 'Sylo', 'Takr', 'Taml', 'Telu', 'Tirh'} + + +def test_script_name(): + assert unicodedata.script_name("Latn") == "Latin" + assert unicodedata.script_name("Zyyy") == "Common" + assert unicodedata.script_name("Zzzz") == "Unknown" + # underscores in long names are replaced by spaces + assert unicodedata.script_name("Egyp") == "Egyptian Hieroglyphs" + + with pytest.raises(KeyError): + unicodedata.script_name("QQQQ") + assert unicodedata.script_name("QQQQ", default="Unknown") + + +def test_script_code(): + assert unicodedata.script_code("Latin") == "Latn" + assert unicodedata.script_code("Common") == "Zyyy" + assert unicodedata.script_code("Unknown") == "Zzzz" + # case, whitespace, underscores and hyphens are ignored + assert unicodedata.script_code("Egyptian Hieroglyphs") == "Egyp" + assert unicodedata.script_code("Egyptian_Hieroglyphs") == "Egyp" + assert unicodedata.script_code("egyptianhieroglyphs") == "Egyp" + assert unicodedata.script_code("Egyptian-Hieroglyphs") == "Egyp" + + with pytest.raises(KeyError): + unicodedata.script_code("Does not exist") + assert unicodedata.script_code("Does not exist", default="Zzzz") == "Zzzz" + + +def test_block(): + assert unicodedata.block("\x00") == "Basic Latin" + assert unicodedata.block("\x7F") == "Basic Latin" + assert unicodedata.block("\x80") == "Latin-1 Supplement" + assert unicodedata.block("\u1c90") == "No_Block" + + +def test_ot_tags_from_script(): + # simple + assert unicodedata.ot_tags_from_script("Latn") == ["latn"] + # script mapped to multiple new and old script tags + assert unicodedata.ot_tags_from_script("Deva") == ["dev2", "deva"] + # exceptions + assert unicodedata.ot_tags_from_script("Hira") == ["kana"] + # special script codes map to DFLT + assert unicodedata.ot_tags_from_script("Zinh") == ["DFLT"] + assert unicodedata.ot_tags_from_script("Zyyy") == ["DFLT"] + assert unicodedata.ot_tags_from_script("Zzzz") == ["DFLT"] + # this is invalid or unknown + assert unicodedata.ot_tags_from_script("Aaaa") == ["DFLT"] + + +def test_ot_tag_to_script(): + assert unicodedata.ot_tag_to_script("latn") == "Latn" + assert unicodedata.ot_tag_to_script("kana") == "Kana" + assert unicodedata.ot_tag_to_script("DFLT") == None + assert unicodedata.ot_tag_to_script("aaaa") == None + assert unicodedata.ot_tag_to_script("beng") == "Beng" + assert unicodedata.ot_tag_to_script("bng2") == "Beng" + assert unicodedata.ot_tag_to_script("dev2") == "Deva" + assert unicodedata.ot_tag_to_script("gjr2") == "Gujr" + assert unicodedata.ot_tag_to_script("yi ") == "Yiii" + assert unicodedata.ot_tag_to_script("nko ") == "Nkoo" + assert unicodedata.ot_tag_to_script("vai ") == "Vaii" + assert unicodedata.ot_tag_to_script("lao ") == "Laoo" + assert unicodedata.ot_tag_to_script("yi") == "Yiii" + + for invalid_value in ("", " ", "z zz", "zzzzz"): + with pytest.raises(ValueError, match="invalid OpenType tag"): + unicodedata.ot_tag_to_script(invalid_value) + + +def test_script_horizontal_direction(): + assert unicodedata.script_horizontal_direction("Latn") == "LTR" + assert unicodedata.script_horizontal_direction("Arab") == "RTL" + assert unicodedata.script_horizontal_direction("Thaa") == "RTL" + + with pytest.raises(KeyError): + unicodedata.script_horizontal_direction("Azzz") + assert unicodedata.script_horizontal_direction("Azzz", + default="LTR") == "LTR" + + +if __name__ == "__main__": + import sys + sys.exit(pytest.main(sys.argv)) diff --git a/Tests/varLib/__init__.py b/Tests/varLib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Tests/varLib/builder_test.py b/Tests/varLib/builder_test.py new file mode 100644 index 0000000..09fd290 --- /dev/null +++ b/Tests/varLib/builder_test.py @@ -0,0 +1,67 @@ +from __future__ import print_function, division, absolute_import +from fontTools.varLib.builder import buildVarData +import pytest + + +@pytest.mark.parametrize("region_indices, items, expected_num_shorts", [ + ([], [], 0), + ([0], [[1]], 0), + ([0], [[128]], 1), + ([0, 1, 2], [[128, 1, 2], [3, -129, 5], [6, 7, 8]], 2), + ([0, 1, 2], [[0, 128, 2], [3, 4, 5], [6, 7, -129]], 3), +], ids=[ + "0_regions_0_deltas", + "1_region_1_uint8", + "1_region_1_short", + "3_regions_2_shorts_ordered", + "3_regions_2_shorts_unordered", +]) +def test_buildVarData_no_optimize(region_indices, items, expected_num_shorts): + data = buildVarData(region_indices, items, optimize=False) + + assert data.ItemCount == len(items) + assert data.NumShorts == expected_num_shorts + assert data.VarRegionCount == len(region_indices) + assert data.VarRegionIndex == region_indices + assert data.Item == items + + +@pytest.mark.parametrize([ + "region_indices", "items", "expected_num_shorts", + "expected_regions", "expected_items" +], [ + ([0, 1, 2], [[0, 1, 2], [3, 4, 5], [6, 7, 8]], 0, + [0, 1, 2], [[0, 1, 2], [3, 4, 5], [6, 7, 8]]), + ([0, 1, 2], [[0, 128, 2], [3, 4, 5], [6, 7, 8]], 1, + [1, 0, 2], [[128, 0, 2], [4, 3, 5], [7, 6, 8]]), + ([0, 1, 2], [[0, 1, 128], [3, 4, 5], [6, -129, 8]], 2, + [1, 2, 0], [[1, 128, 0], [4, 5, 3], [-129, 8, 6]]), + ([0, 1, 2], [[128, 1, -129], [3, 4, 5], [6, 7, 8]], 2, + [0, 2, 1], [[128, -129, 1], [3, 5, 4], [6, 8, 7]]), + ([0, 1, 2], [[0, 1, 128], [3, -129, 5], [256, 7, 8]], 3, + [0, 1, 2], [[0, 1, 128], [3, -129, 5], [256, 7, 8]]), + ([0, 1, 2], [[0, 128, 2], [0, 4, 5], [0, 7, 8]], 1, + [1, 2], [[128, 2], [4, 5], [7, 8]]), +], ids=[ + "0/3_shorts_no_reorder", + "1/3_shorts_reorder", + "2/3_shorts_reorder", + "2/3_shorts_same_row_reorder", + "3/3_shorts_no_reorder", + "1/3_shorts_1/3_zeroes", +]) +def test_buildVarData_optimize( + region_indices, items, expected_num_shorts, expected_regions, + expected_items): + data = buildVarData(region_indices, items, optimize=True) + + assert data.ItemCount == len(items) + assert data.NumShorts == expected_num_shorts + assert data.VarRegionCount == len(expected_regions) + assert data.VarRegionIndex == expected_regions + assert data.Item == expected_items + + +if __name__ == "__main__": + import sys + sys.exit(pytest.main(sys.argv)) diff --git a/Tests/varLib/data/Build.designspace b/Tests/varLib/data/Build.designspace new file mode 100644 index 0000000..e0bf58d --- /dev/null +++ b/Tests/varLib/data/Build.designspace @@ -0,0 +1,300 @@ + + + + + + Contrast + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/BuildAvarEmptyAxis.designspace b/Tests/varLib/data/BuildAvarEmptyAxis.designspace new file mode 100644 index 0000000..6f0d84e --- /dev/null +++ b/Tests/varLib/data/BuildAvarEmptyAxis.designspace @@ -0,0 +1,59 @@ + + + + + + + + + Weight + + + Width + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/BuildAvarIdentityMaps.designspace b/Tests/varLib/data/BuildAvarIdentityMaps.designspace new file mode 100644 index 0000000..a9f30bf --- /dev/null +++ b/Tests/varLib/data/BuildAvarIdentityMaps.designspace @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + Weight + + + + + + + Width + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/BuildAvarSingleAxis.designspace b/Tests/varLib/data/BuildAvarSingleAxis.designspace new file mode 100644 index 0000000..8895007 --- /dev/null +++ b/Tests/varLib/data/BuildAvarSingleAxis.designspace @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + Weight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/Designspace.designspace b/Tests/varLib/data/Designspace.designspace new file mode 100644 index 0000000..df1036e --- /dev/null +++ b/Tests/varLib/data/Designspace.designspace @@ -0,0 +1,39 @@ + + + + + + + + + + + Contrast + Kontrast + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/Designspace2.designspace b/Tests/varLib/data/Designspace2.designspace new file mode 100644 index 0000000..ac7d403 --- /dev/null +++ b/Tests/varLib/data/Designspace2.designspace @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Tests/varLib/data/InterpolateLayout.designspace b/Tests/varLib/data/InterpolateLayout.designspace new file mode 100644 index 0000000..18f118d --- /dev/null +++ b/Tests/varLib/data/InterpolateLayout.designspace @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/InterpolateLayout2.designspace b/Tests/varLib/data/InterpolateLayout2.designspace new file mode 100644 index 0000000..b686c08 --- /dev/null +++ b/Tests/varLib/data/InterpolateLayout2.designspace @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/InterpolateLayout3.designspace b/Tests/varLib/data/InterpolateLayout3.designspace new file mode 100644 index 0000000..3764529 --- /dev/null +++ b/Tests/varLib/data/InterpolateLayout3.designspace @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ttx_interpolatable_otf/TestFamily2-Master0.ttx b/Tests/varLib/data/master_ttx_interpolatable_otf/TestFamily2-Master0.ttx new file mode 100644 index 0000000..a6a8e00 --- /dev/null +++ b/Tests/varLib/data/master_ttx_interpolatable_otf/TestFamily2-Master0.ttx @@ -0,0 +1,855 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Paul D. Hunt + + + Alternate a + + + Test Family 2 + + + Regular + + + Version 2.020;ADBO;Test Family 2 Regular + + + Test Family 2 + + + Version 2.020 + + + TestFamily2-Master0 + + + Paul D. Hunt + + + Master 0 + + + Alternate a + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + rmoveto + 18 0 14 14 0 18 rrcurveto + 0 18 -14 14 -18 0 rrcurveto + -18 0 -14 -14 0 -18 rrcurveto + 0 -18 14 -14 18 0 rrcurveto + return + + + rmoveto + 64 0 60 36 50 40 rrcurveto + 2 0 rlineto + 4 -64 rlineto + 26 0 rlineto + 0 310 rlineto + 0 96 -34 84 -112 0 rrcurveto + -78 0 -66 -40 -30 -22 rrcurveto + 16 -24 rlineto + 30 24 58 34 68 0 rrcurveto + 100 0 20 -86 -2 -78 rrcurveto + -216 -24 -98 -50 0 -107 rrcurveto + 0 -91 64 -38 74 0 rrcurveto + 2 28 rmoveto + -58 0 -50 28 0 74 rrcurveto + 0 82 72 48 210 24 rrcurveto + 0 -174 rlineto + -64 -54 -52 -28 -58 0 rrcurveto + endchar + + + -40 22 -58 0 rrcurveto + -116 0 -98 -98 0 -154 rrcurveto + 0 -162 78 -88 120 0 rrcurveto + 2 28 rmoveto + -108 0 -60 90 0 132 rrcurveto + 0 124 78 100 102 0 rrcurveto + 50 0 44 -18 54 -48 rrcurveto + 0 -296 rlineto + -54 -54 -50 -30 -56 0 rrcurveto + endchar + + + 0 rlineto + 0 114 rlineto + 0 72 24 42 54 0 rrcurveto + 18 0 20 -4 20 -10 rrcurveto + 10 26 rlineto + -22 10 -24 6 -20 0 rrcurveto + -68 0 -42 -44 0 -94 rrcurveto + 0 -118 rlineto + -66 -4 rlineto + 0 -24 rlineto + 66 0 rlineto + 0 -450 rlineto + 30 0 rlineto + 0 450 rlineto + return + + + 580 rmoveto + 63 0 16 66 4 56 rrcurveto + -26 2 rlineto + -2 -52 -16 -46 -37 0 rrcurveto + -59 0 -20 100 -76 0 rrcurveto + -64 0 -16 -65 -4 -57 rrcurveto + 26 -2 rlineto + 2 54 16 44 38 0 rrcurveto + 58 0 20 -100 77 0 rrcurveto + return + + + -10 26 rlineto + -18 -8 -26 -8 -18 0 rrcurveto + -70 0 -14 44 0 62 rrcurveto + 0 328 rlineto + 142 0 rlineto + 0 28 rlineto + -142 0 rlineto + 0 140 rlineto + -26 0 rlineto + -4 -140 rlineto + return + + + 540 252 -12 rmoveto + 66 0 54 36 40 40 rrcurveto + 2 0 rlineto + 4 -64 rlineto + 26 0 rlineto + 0 return + + + 0 21 rrcurveto + 0 20 -13 11 -18 0 rrcurveto + -16 0 -13 -11 0 -20 rrcurveto + 0 -21 13 return + + + + + + 624 96 0 rmoveto + 432 0 rlineto + 0 660 rlineto + -432 0 rlineto + 214 -294 rmoveto + -56 92 rlineto + -94 168 rlineto + 302 0 rlineto + -94 -168 rlineto + -54 -92 rlineto + -180 -292 rmoveto + 0 536 rlineto + 154 -270 rlineto + 200 -266 rmoveto + -152 266 rlineto + 152 270 rlineto + -344 -578 rmoveto + 102 176 rlineto + 64 106 rlineto + 4 0 rlineto + 62 -106 rlineto + 100 -176 rlineto + endchar + + + 520 476 0 rmoveto + 34 0 rlineto + -236 660 rlineto + -28 0 rlineto + -236 -660 rlineto + 32 0 rlineto + 83 236 rlineto + 269 0 rlineto + -212 160 rmoveto + 28 80 24 68 24 82 rrcurveto + 4 0 rlineto + 24 -82 24 -68 28 -80 rrcurveto + 46 -132 rlineto + -249 0 rlineto + endchar + + + 444 400 0 rmoveto + 34 0 rlineto + -198 510 rlineto + -29 0 rlineto + -197 -510 rlineto + 32 0 rlineto + 67 176 rlineto + 225 0 rlineto + -176 128 rmoveto + 23 62 18 48 21 61 rrcurveto + 4 0 rlineto + 21 -60 18 -48 23 -63 rrcurveto + 38 -100 rlineto + -204 0 rlineto + endchar + + + 486 198 -12 -106 callsubr + + + -101 callsubr + 478 rlineto + -28 0 rlineto + -2 -46 rlineto + -2 0 rlineto + -46 36 -105 callsubr + + + 562 550 16 rmoveto + -39 15 -44 27 -47 39 rrcurveto + 53 67 39 86 26 92 rrcurveto + -30 0 rlineto + -24 -88 -35 -77 -50 -62 rrcurveto + -70 64 -72 88 -47 90 rrcurveto + 76 58 78 57 0 84 rrcurveto + 0 66 -36 50 -68 0 rrcurveto + -76 0 -54 -60 0 -84 rrcurveto + 0 -52 17 -57 28 -57 rrcurveto + -70 -53 -67 -58 0 -85 rrcurveto + 0 -110 86 -68 100 0 rrcurveto + 73 0 56 35 48 51 rrcurveto + 51 -43 46 -28 40 -15 rrcurveto + -378 542 rmoveto + 0 62 36 52 62 0 rrcurveto + 56 0 20 -46 0 -44 rrcurveto + 0 -72 -66 -50 -69 -52 rrcurveto + -24 52 -15 51 0 47 rrcurveto + -90 -362 rmoveto + 0 71 54 51 63 49 rrcurveto + 48 -91 73 -88 72 -67 rrcurveto + -43 -45 -53 -32 -58 0 rrcurveto + -84 0 -72 60 0 92 rrcurveto + endchar + + + 486 319 -103 callsubr + -121 -592 -106 callsubr + + + -101 callsubr + 722 rlineto + -30 0 rlineto + 0 -202 rlineto + 2 -90 rlineto + -50 38 -105 callsubr + + + 252 244 450 rmoveto + 0 28 rlineto + -114 -104 callsubr + endchar + + + 518 508 6 rmoveto + -102 callsubr + -192 -104 callsubr + 192 0 rlineto + 0 -324 rlineto + 0 -82 24 -56 88 0 rrcurveto + 16 0 30 8 28 10 rrcurveto + endchar + + + 526 96 0 rmoveto + 30 0 rlineto + 0 366 rlineto + 62 64 44 32 60 0 rrcurveto + 82 0 34 -52 0 -106 rrcurveto + 0 -304 rlineto + 30 0 rlineto + 0 308 rlineto + 0 124 -46 58 -98 0 rrcurveto + -66 0 -50 -38 -50 -50 rrcurveto + -2 0 rlineto + -4 76 rlineto + -26 0 rlineto + endchar + + + 200 endchar + + + 302 218 -12 rmoveto + 16 0 30 8 28 10 rrcurveto + -102 callsubr + -76 -4 rlineto + 0 -24 rlineto + 76 0 rlineto + 0 -324 rlineto + 0 -82 24 -56 88 0 rrcurveto + endchar + + + 0 77 -103 callsubr + endchar + + + 0 -86 602 -107 callsubr + 172 0 -107 callsubr + endchar + + + 0 -86 -188 -107 callsubr + 172 0 -107 callsubr + endchar + + + 0 77 -220 rmoveto + 63 0 16 66 4 56 rrcurveto + -26 2 rlineto + -2 -52 -16 -46 -37 0 rrcurveto + -59 0 -20 100 -76 0 rrcurveto + -64 0 -16 -65 -4 -57 rrcurveto + 26 -2 rlineto + 2 54 16 44 38 0 rrcurveto + 58 0 20 -100 77 0 rrcurveto + endchar + + + 592 295 426 rmoveto + 18 0 13 12 0 20 rrcurveto + 0 20 -13 12 -18 0 rrcurveto + -16 0 -13 -12 0 -20 rrcurveto + 0 -20 13 -12 16 0 rrcurveto + -106 -26 rmoveto + 18 0 12 12 0 19 rrcurveto + 0 22 -13 10 -17 0 rrcurveto + -16 0 -13 -10 0 -22 rrcurveto + 0 -19 13 -12 16 0 rrcurveto + 212 -1 rmoveto + 19 0 11 13 0 19 rrcurveto + 0 21 -13 10 -17 0 rrcurveto + -15 0 -13 -10 0 -21 rrcurveto + 0 -19 13 -13 15 0 rrcurveto + -291 -81 rmoveto + 19 0 12 12 -100 callsubr + -12 16 0 rrcurveto + 370 -1 rmoveto + 19 0 11 12 0 20 rrcurveto + 0 20 -13 11 -17 0 rrcurveto + -15 0 -14 -11 0 -20 rrcurveto + 0 -20 14 -12 15 0 rrcurveto + -398 -110 rmoveto + 19 0 13 12 0 20 rrcurveto + 0 21 -13 10 -19 0 rrcurveto + -15 0 -13 -10 0 -21 rrcurveto + 0 -20 13 -12 15 0 rrcurveto + 426 0 rmoveto + 18 0 12 12 0 20 rrcurveto + 0 21 -15 10 -15 0 rrcurveto + -17 0 -13 -10 0 -21 rrcurveto + 0 -20 13 -12 17 0 rrcurveto + -398 -110 rmoveto + 19 0 12 13 0 19 rrcurveto + 0 21 -13 11 -18 0 rrcurveto + -16 0 -13 -11 0 -21 rrcurveto + 0 -19 13 -13 16 0 rrcurveto + 370 0 rmoveto + 19 0 11 13 0 19 rrcurveto + 0 21 -13 11 -17 0 rrcurveto + -15 0 -14 -11 0 -21 rrcurveto + 0 -19 14 -13 15 0 rrcurveto + -291 -82 rmoveto + 18 0 12 12 0 22 rrcurveto + 0 19 -13 10 -17 0 rrcurveto + -16 0 -13 -10 0 -19 rrcurveto + 0 -22 13 -12 16 0 rrcurveto + 212 0 rmoveto + 19 0 11 12 0 22 rrcurveto + 0 19 -13 10 -17 0 rrcurveto + -15 0 -13 -10 0 -19 rrcurveto + 0 -22 13 -12 15 0 rrcurveto + -106 -27 rmoveto + 18 0 13 11 -100 callsubr + -11 16 0 rrcurveto + endchar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ttx_interpolatable_otf/TestFamily2-Master1.ttx b/Tests/varLib/data/master_ttx_interpolatable_otf/TestFamily2-Master1.ttx new file mode 100644 index 0000000..b7ca3a9 --- /dev/null +++ b/Tests/varLib/data/master_ttx_interpolatable_otf/TestFamily2-Master1.ttx @@ -0,0 +1,693 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Paul D. Hunt + + + Test Family 2 + + + Regular + + + Version 2.020;ADBO;Test Family 2 Regular + + + Test Family 2 + + + Version 2.020 + + + TestFamily2-Master1 + + + Paul D. Hunt + + + Master 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + rmoveto + 47 0 33 35 0 45 rrcurveto + 0 45 -33 35 -47 0 rrcurveto + -47 0 -33 -35 0 -45 rrcurveto + 0 -45 33 -35 47 0 rrcurveto + return + + + rmoveto + 54 0 44 24 40 36 rrcurveto + 4 0 rlineto + 12 -48 rlineto + 140 0 rlineto + 0 278 rlineto + 0 164 -78 70 -130 0 rrcurveto + -78 0 -72 -24 -70 -42 rrcurveto + 60 -112 rlineto + 52 28 38 14 36 0 rrcurveto + 44 0 22 -16 4 -36 rrcurveto + -192 -20 -80 -58 0 -104 rrcurveto + 0 -82 56 -72 94 0 rrcurveto + 60 132 rmoveto + -30 0 -16 13 0 23 rrcurveto + 0 28 26 26 82 12 rrcurveto + 0 -68 rlineto + -18 -20 -16 -14 -28 0 rrcurveto + endchar + + + 0 rlineto + 0 12 rlineto + 0 47 20 15 32 0 rrcurveto + 16 0 18 -4 16 -6 rrcurveto + 30 126 rlineto + -22 8 -38 10 -46 0 rrcurveto + -148 0 -50 -95 0 -107 rrcurveto + 0 -7 rlineto + -66 -5 rlineto + 0 -128 rlineto + 66 0 rlineto + 0 -366 rlineto + 172 0 rlineto + 0 366 rlineto + return + + + -98 0 -164 rrcurveto + 0 -162 80 -100 124 0 rrcurveto + 46 140 rmoveto + -46 0 -28 34 0 90 rrcurveto + 0 88 34 32 36 0 rrcurveto + 22 0 26 -6 20 -18 rrcurveto + 0 -184 rlineto + -18 -28 -20 -8 -26 0 rrcurveto + endchar + + + -26 124 rlineto + -12 -4 -16 -4 -16 0 rrcurveto + -32 0 -28 18 0 55 rrcurveto + 0 171 rlineto + 114 0 rlineto + 0 134 rlineto + -114 0 rlineto + 0 130 rlineto + -142 0 rlineto + -20 -130 rlineto + return + + + 113 rrcurveto + -106 6 rlineto + -4 -36 -10 -10 -16 0 rrcurveto + -26 0 -38 56 -60 0 rrcurveto + -80 0 -50 -45 -2 -113 rrcurveto + 106 -6 rlineto + 4 36 10 10 16 0 rrcurveto + 26 0 38 -56 60 0 rrcurveto + return + + + 580 240 -12 rmoveto + 44 0 48 24 34 34 rrcurveto + 4 0 rlineto + 12 -46 rlineto + 140 0 rlineto + 0 return + + + 0 rrcurveto + -23 0 -21 -16 0 -28 rrcurveto + 0 -30 21 -17 23 0 rrcurveto + return + + + rrcurveto + 0 28 -19 16 -26 0 rrcurveto + -23 0 -20 -16 0 -28 rrcurveto + 0 return + + + 0 rlineto + 0 -174 rlineto + 0 -122 54 -82 130 0 rrcurveto + return + + + rmoveto + 26 0 19 17 0 30 rrcurveto + 0 28 -21 16 -24 0 rrcurveto + return + + + + + + 704 76 0 rmoveto + 552 0 rlineto + 0 660 rlineto + -552 0 rlineto + 274 -236 rmoveto + -40 96 rlineto + -18 36 rlineto + 120 0 rlineto + -18 -36 rlineto + -40 -96 rlineto + -166 -252 rmoveto + 0 336 rlineto + 82 -168 rlineto + 246 -168 rmoveto + -82 168 rlineto + 82 168 rlineto + -228 -404 rmoveto + 26 56 rlineto + 36 96 rlineto + 4 0 rlineto + 36 -96 rlineto + 26 -56 rlineto + endchar + + + 584 412 0 rmoveto + 182 0 rlineto + -198 650 rlineto + -208 0 rlineto + -198 -650 rlineto + 176 0 rlineto + 32 138 rlineto + 182 0 rlineto + -140 178 rmoveto + 16 62 16 78 14 66 rrcurveto + 4 0 rlineto + 16 -65 16 -79 16 -62 rrcurveto + 11 -45 rlineto + -120 0 rlineto + endchar + + + 516 346 0 rmoveto + 180 0 rlineto + -165 532 rlineto + -206 0 rlineto + -165 -532 rlineto + 174 0 rlineto + 21 94 rlineto + 140 0 rlineto + -106 150 rmoveto + 11 48 11 66 11 51 rrcurveto + 4 0 rlineto + 13 -50 11 -67 11 -48 rrcurveto + 6 -28 rlineto + -84 0 rlineto + endchar + + + 536 188 -12 -106 callsubr + + + -101 callsubr + 500 rlineto + -134 0 rlineto + -14 -50 rlineto + -4 0 rlineto + -38 44 -40 18 -48 0 rrcurveto + -102 0 -106 -104 callsubr + + + 690 670 126 rmoveto + -31 4 -38 12 -39 19 rrcurveto + 49 66 34 71 23 76 rrcurveto + -156 0 rlineto + -15 -56 -25 -48 -30 -40 rrcurveto + -41 29 -40 33 -33 35 rrcurveto + 66 43 64 53 0 85 rrcurveto + 0 94 -68 60 -104 0 rrcurveto + -116 0 -72 -82 0 -94 rrcurveto + 0 -39 14 -45 25 -45 rrcurveto + -62 -38 -53 -52 0 -91 rrcurveto + 0 -98 73 -90 151 0 rrcurveto + 83 0 70 24 57 39 rrcurveto + 58 -31 59 -22 57 -10 rrcurveto + -391 498 rmoveto + 0 42 24 22 27 0 rrcurveto + 25 0 13 -14 0 -28 rrcurveto + 0 -38 -30 -25 -41 -24 rrcurveto + -12 23 -6 22 0 20 rrcurveto + -55 -300 rmoveto + 0 23 12 19 18 19 rrcurveto + 34 -40 40 -38 44 -35 rrcurveto + -22 -10 -21 -6 -21 0 rrcurveto + -52 0 -32 28 0 40 rrcurveto + endchar + + + 536 330 572 rmoveto + 80 0 50 45 2 -102 callsubr + -142 -584 -106 callsubr + + + -101 callsubr + 696 rlineto + -172 0 rlineto + 0 -162 rlineto + 6 -72 rlineto + -30 30 -32 20 -54 0 rrcurveto + -102 0 -102 -104 callsubr + + + 360 344 366 rmoveto + 0 134 rlineto + -84 -105 callsubr + endchar + + + 724 706 6 rmoveto + -103 callsubr + -154 -105 callsubr + 144 -98 callsubr + 55 0 37 10 26 8 rrcurveto + endchar + + + 582 58 0 rmoveto + 172 0 rlineto + 0 328 rlineto + 26 24 18 14 32 0 rrcurveto + 34 0 16 -16 0 -64 rrcurveto + 0 -286 rlineto + 172 0 rlineto + 0 308 rlineto + 0 124 -46 80 -110 0 rrcurveto + -68 0 -50 -34 -40 -38 rrcurveto + -4 0 rlineto + -12 60 rlineto + -140 0 rlineto + endchar + + + 200 endchar + + + 400 264 -12 rmoveto + 55 0 37 10 26 8 rrcurveto + -103 callsubr + -76 -6 rlineto + 0 -128 rlineto + 66 -98 callsubr + endchar + + + 0 64 572 rmoveto + 80 0 50 45 2 -102 callsubr + endchar + + + 0 -114 562 -107 callsubr + 228 0 -107 callsubr + endchar + + + 0 -114 -224 -107 callsubr + 228 0 -107 callsubr + endchar + + + 0 64 -228 rmoveto + 80 0 50 45 2 -102 callsubr + endchar + + + 574 287 421 rmoveto + 27 0 18 18 0 29 -99 callsubr + -29 20 -18 23 0 rrcurveto + -105 -26 rmoveto + 26 0 19 17 0 28 rrcurveto + 0 30 -21 16 -24 0 rrcurveto + -24 0 -21 -16 0 -30 rrcurveto + 0 -28 21 -17 24 0 rrcurveto + 210 -1 rmoveto + 28 0 18 17 0 29 rrcurveto + 0 29 -21 17 -25 0 rrcurveto + -23 0 -21 -17 0 -29 rrcurveto + 0 -29 21 -17 23 0 rrcurveto + -288 -81 rmoveto + 27 0 18 18 0 29 rrcurveto + 0 30 -19 15 -26 0 rrcurveto + -22 0 -21 -15 0 -30 rrcurveto + 0 -29 21 -18 22 0 rrcurveto + 368 0 rmoveto + 26 0 18 17 0 29 rrcurveto + 0 29 -20 16 -24 0 rrcurveto + -25 0 -21 -16 0 -29 rrcurveto + 0 -29 21 -17 25 0 rrcurveto + -396 -109 rmoveto + 28 0 18 17 0 30 rrcurveto + 0 28 -20 16 -26 -100 callsubr + 422 0 -97 callsubr + -25 0 -20 -16 0 -28 rrcurveto + 0 -30 20 -17 25 0 rrcurveto + -394 -108 rmoveto + 27 0 18 16 0 29 rrcurveto + 0 29 -19 17 -26 0 rrcurveto + -22 0 -21 -17 0 -29 rrcurveto + 0 -29 21 -16 22 0 rrcurveto + 368 0 rmoveto + 26 0 18 16 0 29 rrcurveto + 0 29 -20 17 -24 0 rrcurveto + -25 0 -21 -17 0 -29 rrcurveto + 0 -29 21 -16 25 0 rrcurveto + -290 -82 -97 callsubr + -24 0 -21 -16 0 -28 rrcurveto + 0 -30 21 -17 24 0 rrcurveto + 210 0 rmoveto + 28 0 18 17 0 30 rrcurveto + 0 28 -21 16 -25 -100 callsubr + -105 -27 rmoveto + 27 0 18 17 0 30 -99 callsubr + -30 20 -17 23 0 rrcurveto + endchar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master0.ttx b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master0.ttx new file mode 100644 index 0000000..f14f89d --- /dev/null +++ b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master0.ttx @@ -0,0 +1,520 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PUSHB[ ] /* 1 value pushed */ + 0 + FDEF[ ] /* FunctionDefinition */ + POP[ ] /* PopTopStack */ + ENDF[ ] /* EndFunctionDefinition */ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PUSHB[ ] /* 1 value pushed */ + 0 + MDAP[0] /* MoveDirectAbsPt */ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test Family + + + Regular + + + Version 1.001;ADBO;Test Family Regular + + + Test Family + + + Version 1.001 + + + TestFamily-Master0 + + + Frank Grießhammer + + + Master 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master1.ttx b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master1.ttx new file mode 100644 index 0000000..8d5bace --- /dev/null +++ b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master1.ttx @@ -0,0 +1,520 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PUSHB[ ] /* 1 value pushed */ + 0 + FDEF[ ] /* FunctionDefinition */ + POP[ ] /* PopTopStack */ + ENDF[ ] /* EndFunctionDefinition */ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PUSHB[ ] /* 1 value pushed */ + 0 + MDAP[0] /* MoveDirectAbsPt */ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test Family + + + Regular + + + Version 1.001;ADBO;Test Family Regular + + + Test Family + + + Version 1.001 + + + TestFamily-Master1 + + + Frank Grießhammer + + + Master 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master2.ttx b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master2.ttx new file mode 100644 index 0000000..3e20fac --- /dev/null +++ b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master2.ttx @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test Family + + + Regular + + + Version 1.001;ADBO;Test Family Regular + + + Test Family + + + Version 1.001 + + + TestFamily-Master2 + + + Frank Grießhammer + + + Master 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master3.ttx b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master3.ttx new file mode 100644 index 0000000..e1d0375 --- /dev/null +++ b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master3.ttx @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test Family + + + Regular + + + Version 1.001;ADBO;Test Family Regular + + + Test Family + + + Version 1.001 + + + TestFamily-Master3 + + + Frank Grießhammer + + + Master 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master4.ttx b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master4.ttx new file mode 100644 index 0000000..c0946fd --- /dev/null +++ b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily-Master4.ttx @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test Family + + + Regular + + + Version 1.001;ADBO;Test Family Regular + + + Test Family + + + Version 1.001 + + + TestFamily-Master4 + + + Frank Grießhammer + + + Master 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily2-Master0.ttx b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily2-Master0.ttx new file mode 100644 index 0000000..13d48e7 --- /dev/null +++ b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily2-Master0.ttx @@ -0,0 +1,1149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Paul D. Hunt + + + Alternate a + + + Test Family 2 + + + Regular + + + Version 2.020;ADBO;Test Family 2 Regular + + + Test Family 2 + + + Version 2.020 + + + TestFamily2-Master0 + + + Paul D. Hunt + + + Master 0 + + + Alternate a + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily2-Master1.ttx b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily2-Master1.ttx new file mode 100644 index 0000000..92157bb --- /dev/null +++ b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily2-Master1.ttx @@ -0,0 +1,986 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Paul D. Hunt + + + Test Family 2 + + + Regular + + + Version 2.020;ADBO;Test Family 2 Regular + + + Test Family 2 + + + Version 2.020 + + + TestFamily2-Master1 + + + Paul D. Hunt + + + Master 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Bold.ttx b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Bold.ttx new file mode 100644 index 0000000..a752bc1 --- /dev/null +++ b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Bold.ttx @@ -0,0 +1,520 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright 2015 Google Inc. All Rights Reserved. + + + Test Family 3 + + + Bold + + + 1.902;GOOG;TestFamily3-Bold + + + Test Family 3 Bold + + + Version 1.902 + + + TestFamily3-Bold + + + Noto is a trademark of Google Inc. + + + Monotype Imaging Inc. + + + Monotype Design Team + + + Designed by Monotype design team. + + + http://www.google.com/get/noto/ + + + http://www.monotype.com/studio + + + This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the SIL Open Font License for the specific language, permissions and limitations governing your use of this Font Software. + + + http://scripts.sil.org/OFL + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Condensed.ttx b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Condensed.ttx new file mode 100644 index 0000000..db0372a --- /dev/null +++ b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Condensed.ttx @@ -0,0 +1,526 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright 2015 Google Inc. All Rights Reserved. + + + Test Family 3 Condensed + + + Regular + + + 1.902;GOOG;TestFamily3-Condensed + + + Test Family 3 Condensed + + + Version 1.902 + + + TestFamily3-Condensed + + + Noto is a trademark of Google Inc. + + + Monotype Imaging Inc. + + + Monotype Design Team + + + Designed by Monotype design team. + + + http://www.google.com/get/noto/ + + + http://www.monotype.com/studio + + + This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the SIL Open Font License for the specific language, permissions and limitations governing your use of this Font Software. + + + http://scripts.sil.org/OFL + + + Test Family 3 + + + Condensed + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-CondensedBold.ttx b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-CondensedBold.ttx new file mode 100644 index 0000000..3313ce6 --- /dev/null +++ b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-CondensedBold.ttx @@ -0,0 +1,526 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright 2015 Google Inc. All Rights Reserved. + + + Test Family 3 Condensed Bold + + + Regular + + + 1.902;GOOG;TestFamily3-CondensedBold + + + Test Family 3 Condensed Bold + + + Version 1.902 + + + TestFamily3-CondensedBold + + + Noto is a trademark of Google Inc. + + + Monotype Imaging Inc. + + + Monotype Design Team + + + Designed by Monotype design team. + + + http://www.google.com/get/noto/ + + + http://www.monotype.com/studio + + + This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the SIL Open Font License for the specific language, permissions and limitations governing your use of this Font Software. + + + http://scripts.sil.org/OFL + + + Test Family 3 + + + Condensed Bold + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-CondensedLight.ttx b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-CondensedLight.ttx new file mode 100644 index 0000000..c83912b --- /dev/null +++ b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-CondensedLight.ttx @@ -0,0 +1,526 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright 2015 Google Inc. All Rights Reserved. + + + Test Family 3 Condensed Light + + + Regular + + + 1.902;GOOG;TestFamily3-CondensedLight + + + Test Family 3 Condensed Light + + + Version 1.902 + + + TestFamily3-CondensedLight + + + Noto is a trademark of Google Inc. + + + Monotype Imaging Inc. + + + Monotype Design Team + + + Designed by Monotype design team. + + + http://www.google.com/get/noto/ + + + http://www.monotype.com/studio + + + This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the SIL Open Font License for the specific language, permissions and limitations governing your use of this Font Software. + + + http://scripts.sil.org/OFL + + + Test Family 3 + + + Condensed Light + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-CondensedSemiBold.ttx b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-CondensedSemiBold.ttx new file mode 100644 index 0000000..7b0c88f --- /dev/null +++ b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-CondensedSemiBold.ttx @@ -0,0 +1,526 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright 2015 Google Inc. All Rights Reserved. + + + Test Family 3 Condensed SemiBold + + + Regular + + + 1.902;GOOG;TestFamily3-CondensedSemiBold + + + Test Family 3 Condensed SemiBold + + + Version 1.902 + + + TestFamily3-CondensedSemiBold + + + Noto is a trademark of Google Inc. + + + Monotype Imaging Inc. + + + Monotype Design Team + + + Designed by Monotype design team. + + + http://www.google.com/get/noto/ + + + http://www.monotype.com/studio + + + This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the SIL Open Font License for the specific language, permissions and limitations governing your use of this Font Software. + + + http://scripts.sil.org/OFL + + + Test Family 3 + + + Condensed SemiBold + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Light.ttx b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Light.ttx new file mode 100644 index 0000000..0e84719 --- /dev/null +++ b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Light.ttx @@ -0,0 +1,526 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright 2015 Google Inc. All Rights Reserved. + + + Test Family 3 Light + + + Regular + + + 1.902;GOOG;TestFamily3-Light + + + Test Family 3 Light + + + Version 1.902 + + + TestFamily3-Light + + + Noto is a trademark of Google Inc. + + + Monotype Imaging Inc. + + + Monotype Design Team + + + Designed by Monotype design team. + + + http://www.google.com/get/noto/ + + + http://www.monotype.com/studio + + + This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the SIL Open Font License for the specific language, permissions and limitations governing your use of this Font Software. + + + http://scripts.sil.org/OFL + + + Test Family 3 + + + Light + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Regular.ttx b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Regular.ttx new file mode 100644 index 0000000..44848db --- /dev/null +++ b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-Regular.ttx @@ -0,0 +1,520 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright 2015 Google Inc. All Rights Reserved. + + + Test Family 3 + + + Regular + + + 1.902;GOOG;TestFamily3-Regular + + + Test Family 3 Regular + + + Version 1.902 + + + TestFamily3-Regular + + + Noto is a trademark of Google Inc. + + + Monotype Imaging Inc. + + + Monotype Design Team + + + Designed by Monotype design team. + + + http://www.google.com/get/noto/ + + + http://www.monotype.com/studio + + + This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the SIL Open Font License for the specific language, permissions and limitations governing your use of this Font Software. + + + http://scripts.sil.org/OFL + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-SemiBold.ttx b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-SemiBold.ttx new file mode 100644 index 0000000..7e6a0b3 --- /dev/null +++ b/Tests/varLib/data/master_ttx_interpolatable_ttf/TestFamily3-SemiBold.ttx @@ -0,0 +1,526 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright 2015 Google Inc. All Rights Reserved. + + + Test Family 3 SemiBold + + + Regular + + + 1.902;GOOG;TestFamily3-SemiBold + + + Test Family 3 SemiBold + + + Version 1.902 + + + TestFamily3-SemiBold + + + Noto is a trademark of Google Inc. + + + Monotype Imaging Inc. + + + Monotype Design Team + + + Designed by Monotype design team. + + + http://www.google.com/get/noto/ + + + http://www.monotype.com/studio + + + This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the SIL Open Font License for the specific language, permissions and limitations governing your use of this Font Software. + + + http://scripts.sil.org/OFL + + + Test Family 3 + + + SemiBold + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ttx_varfont_ttf/Mutator_IUP.ttx b/Tests/varLib/data/master_ttx_varfont_ttf/Mutator_IUP.ttx new file mode 100755 index 0000000..23c240e --- /dev/null +++ b/Tests/varLib/data/master_ttx_varfont_ttf/Mutator_IUP.ttx @@ -0,0 +1,551 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + VarFont + + + Regular + + + VarFont Regular: 2017 + + + VarFont Regular + + + VarFont-Regular + + + Width + + + Ascender + + + Regular + + + VarFont + + + Regular + + + VarFont Regular: 2017 + + + VarFont Regular + + + VarFont-Regular + + + Width + + + Ascender + + + Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + wdth + 60.0 + 100.0 + 100.0 + 256 + + + + + ASCN + 608.0 + 608.0 + 648.0 + 257 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/features.fea b/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/features.fea new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/features.fea @@ -0,0 +1 @@ + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/fontinfo.plist b/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/fontinfo.plist new file mode 100644 index 0000000..15db2ee --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/fontinfo.plist @@ -0,0 +1,140 @@ + + + + + ascender + 738 + capHeight + 677 + descender + -245 + familyName + Test Family + openTypeHheaAscender + 918 + openTypeHheaDescender + -335 + openTypeHheaLineGap + 0 + openTypeNameDesigner + Frank Grießhammer + openTypeOS2CodePageRanges + + 0 + 1 + 29 + + openTypeOS2Panose + + 2 + 4 + 2 + 3 + 5 + 4 + 5 + 2 + 2 + 4 + + openTypeOS2TypoAscender + 730 + openTypeOS2TypoDescender + -270 + openTypeOS2TypoLineGap + 0 + openTypeOS2UnicodeRanges + + 0 + 1 + + openTypeOS2VendorID + ADBO + openTypeOS2WeightClass + 200 + openTypeOS2WinAscent + 918 + openTypeOS2WinDescent + 335 + postscriptBlueFuzz + 0 + postscriptBlueScale + 0.0375 + postscriptBlueShift + 7 + postscriptBlueValues + + -13 + 0 + 470 + 483 + 534 + 547 + 556 + 569 + 654 + 667 + 677 + 690 + 738 + 758 + + postscriptFamilyBlues + + -20 + 0 + 473 + 491 + 525 + 540 + 549 + 562 + 644 + 659 + 669 + 689 + 729 + 749 + + postscriptFamilyOtherBlues + + -249 + -239 + + postscriptFontName + TestFamily-Master0 + postscriptForceBold + + postscriptOtherBlues + + -255 + -245 + + postscriptStemSnapH + + 26 + 20 + + postscriptStemSnapV + + 28 + 32 + + postscriptUnderlinePosition + -75 + postscriptUnderlineThickness + 50 + styleMapFamilyName + Test Family + styleName + Master 0 + unitsPerEm + 1000 + versionMajor + 1 + versionMinor + 1 + xHeight + 470 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/A_.glif b/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/A_.glif new file mode 100644 index 0000000..0861ae7 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/A_.glif @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/_notdef.glif b/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/_notdef.glif new file mode 100644 index 0000000..0c5a60d --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/_notdef.glif @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/a.glif b/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/a.glif new file mode 100644 index 0000000..ed47bff --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/a.glif @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/contents.plist b/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/contents.plist new file mode 100644 index 0000000..ec975ca --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/contents.plist @@ -0,0 +1,18 @@ + + + + + .notdef + _notdef.glif + A + A_.glif + a + a.glif + dollar + dollar.glif + dollar.nostroke + dollar.nostroke.glif + space + space.glif + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/dollar.glif b/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/dollar.glif new file mode 100644 index 0000000..fd3d67f --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/dollar.glif @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/dollar.nostroke.glif b/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/dollar.nostroke.glif new file mode 100644 index 0000000..9703f85 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/dollar.nostroke.glif @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/space.glif b/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/space.glif new file mode 100644 index 0000000..58d78af --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/glyphs/space.glif @@ -0,0 +1,5 @@ + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/lib.plist b/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/lib.plist new file mode 100644 index 0000000..a379c4b --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/lib.plist @@ -0,0 +1,15 @@ + + + + + public.glyphOrder + + .notdef + space + A + a + dollar + dollar.nostroke + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/metainfo.plist b/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/metainfo.plist new file mode 100644 index 0000000..9c654d4 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master0.ufo/metainfo.plist @@ -0,0 +1,10 @@ + + + + + creator + org.robofab.ufoLib + formatVersion + 2 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/features.fea b/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/features.fea new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/features.fea @@ -0,0 +1 @@ + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/fontinfo.plist b/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/fontinfo.plist new file mode 100644 index 0000000..24c1bbf --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/fontinfo.plist @@ -0,0 +1,140 @@ + + + + + ascender + 738 + capHeight + 677 + descender + -245 + familyName + Test Family + openTypeHheaAscender + 918 + openTypeHheaDescender + -335 + openTypeHheaLineGap + 0 + openTypeNameDesigner + Frank Grießhammer + openTypeOS2CodePageRanges + + 0 + 1 + 29 + + openTypeOS2Panose + + 2 + 4 + 6 + 3 + 5 + 4 + 5 + 2 + 2 + 4 + + openTypeOS2TypoAscender + 730 + openTypeOS2TypoDescender + -270 + openTypeOS2TypoLineGap + 0 + openTypeOS2UnicodeRanges + + 0 + 1 + + openTypeOS2VendorID + ADBO + openTypeOS2WeightClass + 400 + openTypeOS2WinAscent + 918 + openTypeOS2WinDescent + 335 + postscriptBlueFuzz + 0 + postscriptBlueScale + 0.0375 + postscriptBlueShift + 7 + postscriptBlueValues + + -15 + 0 + 474 + 487 + 527 + 540 + 550 + 563 + 647 + 660 + 670 + 685 + 730 + 750 + + postscriptFamilyBlues + + -20 + 0 + 473 + 491 + 525 + 540 + 549 + 562 + 644 + 659 + 669 + 689 + 729 + 749 + + postscriptFamilyOtherBlues + + -249 + -239 + + postscriptFontName + TestFamily-Master1 + postscriptForceBold + + postscriptOtherBlues + + -250 + -240 + + postscriptStemSnapH + + 55 + 40 + + postscriptStemSnapV + + 80 + 90 + + postscriptUnderlinePosition + -75 + postscriptUnderlineThickness + 50 + styleMapFamilyName + Test Family + styleName + Master 1 + unitsPerEm + 1000 + versionMajor + 1 + versionMinor + 1 + xHeight + 474 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/A_.glif b/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/A_.glif new file mode 100644 index 0000000..1cf8995 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/A_.glif @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/_notdef.glif b/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/_notdef.glif new file mode 100644 index 0000000..27591b4 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/_notdef.glif @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/a.glif b/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/a.glif new file mode 100644 index 0000000..b9ed8a5 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/a.glif @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/contents.plist b/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/contents.plist new file mode 100644 index 0000000..ec975ca --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/contents.plist @@ -0,0 +1,18 @@ + + + + + .notdef + _notdef.glif + A + A_.glif + a + a.glif + dollar + dollar.glif + dollar.nostroke + dollar.nostroke.glif + space + space.glif + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/dollar.glif b/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/dollar.glif new file mode 100644 index 0000000..3ce19bf --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/dollar.glif @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/dollar.nostroke.glif b/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/dollar.nostroke.glif new file mode 100644 index 0000000..fb86b06 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/dollar.nostroke.glif @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/space.glif b/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/space.glif new file mode 100644 index 0000000..d1fb461 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/glyphs/space.glif @@ -0,0 +1,5 @@ + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/lib.plist b/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/lib.plist new file mode 100644 index 0000000..a379c4b --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/lib.plist @@ -0,0 +1,15 @@ + + + + + public.glyphOrder + + .notdef + space + A + a + dollar + dollar.nostroke + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/metainfo.plist b/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/metainfo.plist new file mode 100644 index 0000000..9c654d4 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master1.ufo/metainfo.plist @@ -0,0 +1,10 @@ + + + + + creator + org.robofab.ufoLib + formatVersion + 2 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/features.fea b/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/features.fea new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/features.fea @@ -0,0 +1 @@ + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/fontinfo.plist b/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/fontinfo.plist new file mode 100644 index 0000000..dab014f --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/fontinfo.plist @@ -0,0 +1,140 @@ + + + + + ascender + 738 + capHeight + 677 + descender + -245 + familyName + Test Family + openTypeHheaAscender + 918 + openTypeHheaDescender + -335 + openTypeHheaLineGap + 0 + openTypeNameDesigner + Frank Grießhammer + openTypeOS2CodePageRanges + + 0 + 1 + 29 + + openTypeOS2Panose + + 2 + 4 + 9 + 3 + 5 + 4 + 5 + 2 + 2 + 4 + + openTypeOS2TypoAscender + 730 + openTypeOS2TypoDescender + -270 + openTypeOS2TypoLineGap + 0 + openTypeOS2UnicodeRanges + + 0 + 1 + + openTypeOS2VendorID + ADBO + openTypeOS2WeightClass + 900 + openTypeOS2WinAscent + 918 + openTypeOS2WinDescent + 335 + postscriptBlueFuzz + 0 + postscriptBlueScale + 0.0375 + postscriptBlueShift + 7 + postscriptBlueValues + + -20 + 0 + 487 + 503 + 515 + 531 + 536 + 552 + 624 + 640 + 652 + 672 + 711 + 731 + + postscriptFamilyBlues + + -20 + 0 + 473 + 491 + 525 + 540 + 549 + 562 + 644 + 659 + 669 + 689 + 729 + 749 + + postscriptFamilyOtherBlues + + -249 + -239 + + postscriptFontName + TestFamily-Master2 + postscriptForceBold + + postscriptOtherBlues + + -232 + -222 + + postscriptStemSnapH + + 74 + 60 + + postscriptStemSnapV + + 190 + 200 + + postscriptUnderlinePosition + -75 + postscriptUnderlineThickness + 50 + styleMapFamilyName + Test Family + styleName + Master 2 + unitsPerEm + 1000 + versionMajor + 1 + versionMinor + 1 + xHeight + 487 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/A_.glif b/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/A_.glif new file mode 100644 index 0000000..792544c --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/A_.glif @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/_notdef.glif b/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/_notdef.glif new file mode 100644 index 0000000..3aeffb1 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/_notdef.glif @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/a.glif b/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/a.glif new file mode 100644 index 0000000..458dfea --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/a.glif @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/contents.plist b/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/contents.plist new file mode 100644 index 0000000..ec975ca --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/contents.plist @@ -0,0 +1,18 @@ + + + + + .notdef + _notdef.glif + A + A_.glif + a + a.glif + dollar + dollar.glif + dollar.nostroke + dollar.nostroke.glif + space + space.glif + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/dollar.glif b/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/dollar.glif new file mode 100644 index 0000000..a15b1d7 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/dollar.glif @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/dollar.nostroke.glif b/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/dollar.nostroke.glif new file mode 100644 index 0000000..6bf5b54 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/dollar.nostroke.glif @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/space.glif b/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/space.glif new file mode 100644 index 0000000..32dd00c --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/glyphs/space.glif @@ -0,0 +1,5 @@ + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/lib.plist b/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/lib.plist new file mode 100644 index 0000000..a379c4b --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/lib.plist @@ -0,0 +1,15 @@ + + + + + public.glyphOrder + + .notdef + space + A + a + dollar + dollar.nostroke + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/metainfo.plist b/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/metainfo.plist new file mode 100644 index 0000000..9c654d4 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master2.ufo/metainfo.plist @@ -0,0 +1,10 @@ + + + + + creator + org.robofab.ufoLib + formatVersion + 2 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/features.fea b/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/features.fea new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/features.fea @@ -0,0 +1 @@ + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/fontinfo.plist b/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/fontinfo.plist new file mode 100644 index 0000000..6a94c39 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/fontinfo.plist @@ -0,0 +1,140 @@ + + + + + ascender + 738 + capHeight + 677 + descender + -245 + familyName + Test Family + openTypeHheaAscender + 918 + openTypeHheaDescender + -335 + openTypeHheaLineGap + 0 + openTypeNameDesigner + Frank Grießhammer + openTypeOS2CodePageRanges + + 0 + 1 + 29 + + openTypeOS2Panose + + 2 + 4 + 9 + 3 + 5 + 4 + 5 + 2 + 2 + 4 + + openTypeOS2TypoAscender + 730 + openTypeOS2TypoDescender + -270 + openTypeOS2TypoLineGap + 0 + openTypeOS2UnicodeRanges + + 0 + 1 + + openTypeOS2VendorID + ADBO + openTypeOS2WeightClass + 900 + openTypeOS2WinAscent + 918 + openTypeOS2WinDescent + 335 + postscriptBlueFuzz + 0 + postscriptBlueScale + 0.0375 + postscriptBlueShift + 7 + postscriptBlueValues + + -20 + 0 + 487 + 503 + 515 + 531 + 536 + 552 + 624 + 640 + 652 + 672 + 711 + 731 + + postscriptFamilyBlues + + -20 + 0 + 473 + 491 + 525 + 540 + 549 + 562 + 644 + 659 + 669 + 689 + 729 + 749 + + postscriptFamilyOtherBlues + + -249 + -239 + + postscriptFontName + TestFamily-Master3 + postscriptForceBold + + postscriptOtherBlues + + -232 + -222 + + postscriptStemSnapH + + 50 + 38 + + postscriptStemSnapV + + 190 + 200 + + postscriptUnderlinePosition + -75 + postscriptUnderlineThickness + 50 + styleMapFamilyName + Test Family + styleName + Master 3 + unitsPerEm + 1000 + versionMajor + 1 + versionMinor + 1 + xHeight + 487 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/A_.glif b/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/A_.glif new file mode 100644 index 0000000..0422ab9 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/A_.glif @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/_notdef.glif b/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/_notdef.glif new file mode 100644 index 0000000..c6f960c --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/_notdef.glif @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/a.glif b/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/a.glif new file mode 100644 index 0000000..0cdd98b --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/a.glif @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/contents.plist b/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/contents.plist new file mode 100644 index 0000000..ec975ca --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/contents.plist @@ -0,0 +1,18 @@ + + + + + .notdef + _notdef.glif + A + A_.glif + a + a.glif + dollar + dollar.glif + dollar.nostroke + dollar.nostroke.glif + space + space.glif + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/dollar.glif b/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/dollar.glif new file mode 100644 index 0000000..9f75ced --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/dollar.glif @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/dollar.nostroke.glif b/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/dollar.nostroke.glif new file mode 100644 index 0000000..d009a05 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/dollar.nostroke.glif @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/space.glif b/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/space.glif new file mode 100644 index 0000000..32dd00c --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/glyphs/space.glif @@ -0,0 +1,5 @@ + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/lib.plist b/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/lib.plist new file mode 100644 index 0000000..a379c4b --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/lib.plist @@ -0,0 +1,15 @@ + + + + + public.glyphOrder + + .notdef + space + A + a + dollar + dollar.nostroke + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/metainfo.plist b/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/metainfo.plist new file mode 100644 index 0000000..9c654d4 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master3.ufo/metainfo.plist @@ -0,0 +1,10 @@ + + + + + creator + org.robofab.ufoLib + formatVersion + 2 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/features.fea b/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/features.fea new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/features.fea @@ -0,0 +1 @@ + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/fontinfo.plist b/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/fontinfo.plist new file mode 100644 index 0000000..f8b307d --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/fontinfo.plist @@ -0,0 +1,140 @@ + + + + + ascender + 738 + capHeight + 677 + descender + -245 + familyName + Test Family + openTypeHheaAscender + 918 + openTypeHheaDescender + -335 + openTypeHheaLineGap + 0 + openTypeNameDesigner + Frank Grießhammer + openTypeOS2CodePageRanges + + 0 + 1 + 29 + + openTypeOS2Panose + + 2 + 4 + 6 + 3 + 5 + 4 + 5 + 2 + 2 + 4 + + openTypeOS2TypoAscender + 730 + openTypeOS2TypoDescender + -270 + openTypeOS2TypoLineGap + 0 + openTypeOS2UnicodeRanges + + 0 + 1 + + openTypeOS2VendorID + ADBO + openTypeOS2WeightClass + 400 + openTypeOS2WinAscent + 918 + openTypeOS2WinDescent + 335 + postscriptBlueFuzz + 0 + postscriptBlueScale + 0.0375 + postscriptBlueShift + 7 + postscriptBlueValues + + -15 + 0 + 474 + 487 + 527 + 540 + 550 + 563 + 647 + 660 + 670 + 685 + 730 + 750 + + postscriptFamilyBlues + + -20 + 0 + 473 + 491 + 525 + 540 + 549 + 562 + 644 + 659 + 669 + 689 + 729 + 749 + + postscriptFamilyOtherBlues + + -249 + -239 + + postscriptFontName + TestFamily-Master4 + postscriptForceBold + + postscriptOtherBlues + + -250 + -240 + + postscriptStemSnapH + + 55 + 40 + + postscriptStemSnapV + + 80 + 90 + + postscriptUnderlinePosition + -75 + postscriptUnderlineThickness + 50 + styleMapFamilyName + Test Family + styleName + Master 4 + unitsPerEm + 1000 + versionMajor + 1 + versionMinor + 1 + xHeight + 474 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/A_.glif b/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/A_.glif new file mode 100644 index 0000000..1061198 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/A_.glif @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/_notdef.glif b/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/_notdef.glif new file mode 100644 index 0000000..fe1f506 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/_notdef.glif @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/a.glif b/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/a.glif new file mode 100644 index 0000000..b62fb75 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/a.glif @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/contents.plist b/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/contents.plist new file mode 100644 index 0000000..ec975ca --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/contents.plist @@ -0,0 +1,18 @@ + + + + + .notdef + _notdef.glif + A + A_.glif + a + a.glif + dollar + dollar.glif + dollar.nostroke + dollar.nostroke.glif + space + space.glif + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/dollar.glif b/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/dollar.glif new file mode 100644 index 0000000..75c00e4 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/dollar.glif @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/dollar.nostroke.glif b/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/dollar.nostroke.glif new file mode 100644 index 0000000..53dd438 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/dollar.nostroke.glif @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/space.glif b/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/space.glif new file mode 100644 index 0000000..d1fb461 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/glyphs/space.glif @@ -0,0 +1,5 @@ + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/lib.plist b/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/lib.plist new file mode 100644 index 0000000..a379c4b --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/lib.plist @@ -0,0 +1,15 @@ + + + + + public.glyphOrder + + .notdef + space + A + a + dollar + dollar.nostroke + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/metainfo.plist b/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/metainfo.plist new file mode 100644 index 0000000..9c654d4 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily-Master4.ufo/metainfo.plist @@ -0,0 +1,10 @@ + + + + + creator + org.robofab.ufoLib + formatVersion + 2 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/features.fea b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/features.fea new file mode 100644 index 0000000..5d84635 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/features.fea @@ -0,0 +1,81 @@ +table head { + FontRevision 2.020; +} head; + + +table name { + nameid 9 "Paul D. Hunt"; + nameid 9 1 "Paul D. Hunt"; +} name; + + +table hhea { + Ascender 984; + Descender -273; + LineGap 0; +} hhea; + + +table BASE { + HorizAxis.BaseTagList ideo romn; + HorizAxis.BaseScriptList + latn romn -170 0, + grek romn -170 0, + cyrl romn -170 0, + DFLT romn -170 0; +} BASE; + + +table OS/2 { + Panose 2 11 3 3 3 4 3 2 2 4; + XHeight 478; + WeightClass 200; + + TypoAscender 750; + TypoDescender -250; + TypoLineGap 0; + winAscent 984; + winDescent 273; + + CapHeight 660; + WidthClass 5; + Vendor "ADBO"; + FSType 0; +} OS/2; + + +languagesystem DFLT dflt; +languagesystem latn dflt; + +# GSUB ========================================= +# Merging of GSUB is not performed. The variable +# font will inherit the GSUB table from the +# base master. + +feature c2sc { + sub A by A.sc; # GSUB LookupType 1 +} c2sc; + +feature ss01 { + featureNames { + name "Alternate a"; + name 1 0 0 "Alternate a";}; + sub a by a.alt; +} ss01; + +feature ccmp { + sub ampersand by a n d; # GSUB LookupType 2 +} ccmp; + +feature salt { + sub a from [a.alt A.sc]; # GSUB LookupType 3 +} salt; + +feature liga { + sub f t by f_t; # GSUB LookupType 4 +} liga; + +feature calt { + sub a' t by a.alt; # GSUB LookupType 6 +} calt; + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/fontinfo.plist b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/fontinfo.plist new file mode 100644 index 0000000..2775576 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/fontinfo.plist @@ -0,0 +1,140 @@ + + + + + ascender + 722 + capHeight + 660 + descender + -222 + familyName + Test Family 2 + italicAngle + 0 + openTypeHheaAscender + 984 + openTypeHheaDescender + -273 + openTypeHheaLineGap + 0 + openTypeNameDesigner + Paul D. Hunt + openTypeOS2CodePageRanges + + 0 + 1 + 29 + + openTypeOS2Panose + + 2 + 11 + 5 + 3 + 3 + 4 + 3 + 2 + 2 + 4 + + openTypeOS2TypoAscender + 750 + openTypeOS2TypoDescender + -250 + openTypeOS2TypoLineGap + 0 + openTypeOS2UnicodeRanges + + 0 + 1 + + openTypeOS2VendorID + ADBO + openTypeOS2WeightClass + 200 + openTypeOS2WinAscent + 984 + openTypeOS2WinDescent + 273 + postscriptBlueFuzz + 0 + postscriptBlueScale + 0.0625 + postscriptBlueValues + + -12 + 0 + 478 + 490 + 510 + 522 + 570 + 582 + 640 + 652 + 660 + 672 + 722 + 734 + + postscriptFamilyBlues + + -12 + 0 + 486 + 498 + 518 + 530 + 574 + 586 + 638 + 650 + 656 + 668 + 712 + 724 + + postscriptFamilyOtherBlues + + -217 + -205 + + postscriptFontName + TestFamily2-Master0 + postscriptForceBold + + postscriptOtherBlues + + -234 + -222 + + postscriptStemSnapH + + 28 + 40 + + postscriptStemSnapV + + 32 + 48 + + postscriptUnderlinePosition + -75 + postscriptUnderlineThickness + 50 + styleMapFamilyName + Test Family 2 + styleName + Master 0 + unitsPerEm + 1000 + versionMajor + 2 + versionMinor + 20 + xHeight + 478 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/A_.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/A_.glif new file mode 100644 index 0000000..6712970 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/A_.glif @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/A_.sc.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/A_.sc.glif new file mode 100644 index 0000000..7152360 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/A_.sc.glif @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/_notdef.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/_notdef.glif new file mode 100644 index 0000000..5f6c8ec --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/_notdef.glif @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/a.alt.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/a.alt.glif new file mode 100644 index 0000000..ecc308e --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/a.alt.glif @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/a.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/a.glif new file mode 100644 index 0000000..7a91021 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/a.glif @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/ampersand.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/ampersand.glif new file mode 100644 index 0000000..8844902 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/ampersand.glif @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/atilde.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/atilde.glif new file mode 100644 index 0000000..37411a1 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/atilde.glif @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/circledotted.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/circledotted.glif new file mode 100644 index 0000000..c6337ce --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/circledotted.glif @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/contents.plist b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/contents.plist new file mode 100644 index 0000000..2c5ee6b --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/contents.plist @@ -0,0 +1,42 @@ + + + + + .notdef + _notdef.glif + A + A_.glif + A.sc + A_.sc.glif + a + a.glif + a.alt + a.alt.glif + ampersand + ampersand.glif + atilde + atilde.glif + circledotted + circledotted.glif + d + d.glif + dieresisbelowcmb + dieresisbelowcmb.glif + dieresiscmb + dieresiscmb.glif + f + f.glif + f_t + f_t.glif + n + n.glif + space + space.glif + t + t.glif + tildebelowcmb + tildebelowcmb.glif + tildecmb + tildecmb.glif + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/d.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/d.glif new file mode 100644 index 0000000..1dc211e --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/d.glif @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/dieresisbelowcmb.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/dieresisbelowcmb.glif new file mode 100644 index 0000000..c63f156 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/dieresisbelowcmb.glif @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/dieresiscmb.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/dieresiscmb.glif new file mode 100644 index 0000000..33f264f --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/dieresiscmb.glif @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/f.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/f.glif new file mode 100644 index 0000000..b87de41 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/f.glif @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/f_t.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/f_t.glif new file mode 100644 index 0000000..7c1f3d3 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/f_t.glif @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/n.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/n.glif new file mode 100644 index 0000000..d2486e8 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/n.glif @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/space.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/space.glif new file mode 100644 index 0000000..eea7af4 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/space.glif @@ -0,0 +1,5 @@ + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/t.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/t.glif new file mode 100644 index 0000000..1a6d330 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/t.glif @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/tildebelowcmb.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/tildebelowcmb.glif new file mode 100644 index 0000000..29589ac --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/tildebelowcmb.glif @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/tildecmb.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/tildecmb.glif new file mode 100644 index 0000000..966bd5c --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/glyphs/tildecmb.glif @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/lib.plist b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/lib.plist new file mode 100644 index 0000000..54eab2b --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/lib.plist @@ -0,0 +1,40 @@ + + + + + public.glyphOrder + + .notdef + space + A + a + d + f + n + t + f_t + a.alt + A.sc + atilde + ampersand + circledotted + tildecmb + dieresiscmb + tildebelowcmb + dieresisbelowcmb + + public.postscriptNames + + circledotted + uni25CC + dieresisbelowcmb + uni0324 + dieresiscmb + uni0308 + tildebelowcmb + uni0330 + tildecmb + uni0303 + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/metainfo.plist b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/metainfo.plist new file mode 100644 index 0000000..9c654d4 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master0.ufo/metainfo.plist @@ -0,0 +1,10 @@ + + + + + creator + org.robofab.ufoLib + formatVersion + 2 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/features.fea b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/features.fea new file mode 100644 index 0000000..487b2ec --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/features.fea @@ -0,0 +1,54 @@ +table head { + FontRevision 2.020; +} head; + + +table name { + nameid 9 "Paul D. Hunt"; + nameid 9 1 "Paul D. Hunt"; +} name; + + +table hhea { + Ascender 984; + Descender -273; + LineGap 0; +} hhea; + + +table BASE { + HorizAxis.BaseTagList ideo romn; + HorizAxis.BaseScriptList + latn romn -170 0, + grek romn -170 0, + cyrl romn -170 0, + DFLT romn -170 0; +} BASE; + + +table OS/2 { + Panose 2 11 8 3 3 4 3 2 2 4; + XHeight 500; + WeightClass 900; + + TypoAscender 750; + TypoDescender -250; + TypoLineGap 0; + winAscent 984; + winDescent 273; + + CapHeight 660; + WidthClass 5; + Vendor "ADBO"; + FSType 0; +} OS/2; + + +languagesystem DFLT dflt; +languagesystem latn dflt; + +# GSUB ========================================= +# No merging of GSUB is performed. The variable +# font will inherit the GSUB table from the +# base master. + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/fontinfo.plist b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/fontinfo.plist new file mode 100644 index 0000000..43f7e75 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/fontinfo.plist @@ -0,0 +1,140 @@ + + + + + ascender + 696 + capHeight + 650 + descender + -176 + familyName + Test Family 2 + italicAngle + 0 + openTypeHheaAscender + 984 + openTypeHheaDescender + -273 + openTypeHheaLineGap + 0 + openTypeNameDesigner + Paul D. Hunt + openTypeOS2CodePageRanges + + 0 + 1 + 29 + + openTypeOS2Panose + + 2 + 11 + 5 + 3 + 3 + 4 + 3 + 2 + 2 + 4 + + openTypeOS2TypoAscender + 750 + openTypeOS2TypoDescender + -250 + openTypeOS2TypoLineGap + 0 + openTypeOS2UnicodeRanges + + 0 + 1 + + openTypeOS2VendorID + ADBO + openTypeOS2WeightClass + 900 + openTypeOS2WinAscent + 984 + openTypeOS2WinDescent + 273 + postscriptBlueFuzz + 0 + postscriptBlueScale + 0.0625 + postscriptBlueValues + + -12 + 0 + 500 + 512 + 532 + 544 + 580 + 592 + 634 + 646 + 650 + 662 + 696 + 708 + + postscriptFamilyBlues + + -12 + 0 + 486 + 498 + 518 + 530 + 574 + 586 + 638 + 650 + 656 + 668 + 712 + 724 + + postscriptFamilyOtherBlues + + -217 + -205 + + postscriptFontName + TestFamily2-Master1 + postscriptForceBold + + postscriptOtherBlues + + -188 + -176 + + postscriptStemSnapH + + 134 + 144 + + postscriptStemSnapV + + 172 + 176 + + postscriptUnderlinePosition + -75 + postscriptUnderlineThickness + 50 + styleMapFamilyName + Test Family 2 + styleName + Master 1 + unitsPerEm + 1000 + versionMajor + 2 + versionMinor + 20 + xHeight + 500 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/A_.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/A_.glif new file mode 100644 index 0000000..e5afc57 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/A_.glif @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/A_.sc.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/A_.sc.glif new file mode 100644 index 0000000..4b4ac31 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/A_.sc.glif @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/_notdef.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/_notdef.glif new file mode 100644 index 0000000..930ed6d --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/_notdef.glif @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/a.alt.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/a.alt.glif new file mode 100644 index 0000000..db719a0 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/a.alt.glif @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/a.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/a.glif new file mode 100644 index 0000000..e49d9ac --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/a.glif @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/ampersand.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/ampersand.glif new file mode 100644 index 0000000..622075f --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/ampersand.glif @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/atilde.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/atilde.glif new file mode 100644 index 0000000..b53079d --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/atilde.glif @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/circledotted.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/circledotted.glif new file mode 100644 index 0000000..9a24018 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/circledotted.glif @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/contents.plist b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/contents.plist new file mode 100644 index 0000000..2c5ee6b --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/contents.plist @@ -0,0 +1,42 @@ + + + + + .notdef + _notdef.glif + A + A_.glif + A.sc + A_.sc.glif + a + a.glif + a.alt + a.alt.glif + ampersand + ampersand.glif + atilde + atilde.glif + circledotted + circledotted.glif + d + d.glif + dieresisbelowcmb + dieresisbelowcmb.glif + dieresiscmb + dieresiscmb.glif + f + f.glif + f_t + f_t.glif + n + n.glif + space + space.glif + t + t.glif + tildebelowcmb + tildebelowcmb.glif + tildecmb + tildecmb.glif + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/d.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/d.glif new file mode 100644 index 0000000..9caa625 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/d.glif @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/dieresisbelowcmb.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/dieresisbelowcmb.glif new file mode 100644 index 0000000..a8b69d5 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/dieresisbelowcmb.glif @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/dieresiscmb.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/dieresiscmb.glif new file mode 100644 index 0000000..8c78d78 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/dieresiscmb.glif @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/f.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/f.glif new file mode 100644 index 0000000..35765a2 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/f.glif @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/f_t.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/f_t.glif new file mode 100644 index 0000000..58378a8 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/f_t.glif @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/n.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/n.glif new file mode 100644 index 0000000..ccf3277 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/n.glif @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/space.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/space.glif new file mode 100644 index 0000000..eea7af4 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/space.glif @@ -0,0 +1,5 @@ + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/t.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/t.glif new file mode 100644 index 0000000..e5c75c2 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/t.glif @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/tildebelowcmb.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/tildebelowcmb.glif new file mode 100644 index 0000000..29589ac --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/tildebelowcmb.glif @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/tildecmb.glif b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/tildecmb.glif new file mode 100644 index 0000000..e5d36b2 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/glyphs/tildecmb.glif @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/lib.plist b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/lib.plist new file mode 100644 index 0000000..54eab2b --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/lib.plist @@ -0,0 +1,40 @@ + + + + + public.glyphOrder + + .notdef + space + A + a + d + f + n + t + f_t + a.alt + A.sc + atilde + ampersand + circledotted + tildecmb + dieresiscmb + tildebelowcmb + dieresisbelowcmb + + public.postscriptNames + + circledotted + uni25CC + dieresisbelowcmb + uni0324 + dieresiscmb + uni0308 + tildebelowcmb + uni0330 + tildecmb + uni0303 + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/metainfo.plist b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/metainfo.plist new file mode 100644 index 0000000..9c654d4 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily2-Master1.ufo/metainfo.plist @@ -0,0 +1,10 @@ + + + + + creator + org.robofab.ufoLib + formatVersion + 2 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/fontinfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/fontinfo.plist new file mode 100644 index 0000000..003488d --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/fontinfo.plist @@ -0,0 +1,153 @@ + + + + + ascender + 760 + capHeight + 714 + copyright + Copyright 2015 Google Inc. All Rights Reserved. + descender + -240 + familyName + Test Family 3 + guidelines + + openTypeHeadCreated + 2016/03/15 19:50:39 + openTypeHheaAscender + 1069 + openTypeHheaDescender + -293 + openTypeHheaLineGap + 0 + openTypeNameDescription + Designed by Monotype design team. + openTypeNameDesigner + Monotype Design Team + openTypeNameDesignerURL + http://www.monotype.com/studio + openTypeNameLicense + This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the SIL Open Font License for the specific language, permissions and limitations governing your use of this Font Software. + openTypeNameLicenseURL + http://scripts.sil.org/OFL + openTypeNameManufacturer + Monotype Imaging Inc. + openTypeNameManufacturerURL + http://www.google.com/get/noto/ + openTypeNameVersion + Version 1.902 + openTypeOS2Panose + + 2 + 11 + 10 + 2 + 4 + 5 + 4 + 2 + 2 + 4 + + openTypeOS2Selection + + 8 + + openTypeOS2Type + + openTypeOS2TypoAscender + 1069 + openTypeOS2TypoDescender + -293 + openTypeOS2TypoLineGap + 0 + openTypeOS2UnicodeRanges + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 9 + 29 + 30 + 31 + 32 + 33 + 34 + 35 + 36 + 62 + 64 + 67 + 69 + 91 + 116 + + openTypeOS2VendorID + GOOG + openTypeOS2WinAscent + 1069 + openTypeOS2WinDescent + 293 + postscriptBlueValues + + -15 + 0 + 553 + 563 + 591 + 601 + 714 + 725 + 760 + 766 + + postscriptFamilyBlues + + postscriptFamilyOtherBlues + + postscriptOtherBlues + + -256 + -240 + + postscriptStemSnapH + + 115 + 143 + 158 + + postscriptStemSnapV + + 132 + 176 + 194 + + postscriptUnderlinePosition + -100 + postscriptUnderlineThickness + 50 + styleMapFamilyName + Test Family 3 + styleMapStyleName + bold + styleName + Bold + trademark + Noto is a trademark of Google Inc. + unitsPerEm + 1000 + versionMajor + 1 + versionMinor + 902 + xHeight + 553 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/F_.glif b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/F_.glif new file mode 100644 index 0000000..11b34cc --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/F_.glif @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/T_.glif b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/T_.glif new file mode 100644 index 0000000..80de0d1 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/T_.glif @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/contents.plist b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/contents.plist new file mode 100644 index 0000000..d50a055 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/contents.plist @@ -0,0 +1,20 @@ + + + + + F + F_.glif + T + T_.glif + l + l.glif + n + n.glif + o + o.glif + s + s.glif + t + t.glif + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/l.glif b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/l.glif new file mode 100644 index 0000000..7c63850 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/l.glif @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/layerinfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/layerinfo.plist new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/layerinfo.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/n.glif b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/n.glif new file mode 100644 index 0000000..cdce9c2 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/n.glif @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/o.glif b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/o.glif new file mode 100644 index 0000000..1a5405b --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/o.glif @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/s.glif b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/s.glif new file mode 100644 index 0000000..019e3d9 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/s.glif @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/t.glif b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/t.glif new file mode 100644 index 0000000..54dde26 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/glyphs/t.glif @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/groups.plist b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/groups.plist new file mode 100644 index 0000000..02bf8c6 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/groups.plist @@ -0,0 +1,50 @@ + + + + + public.kern1.T.left + + T + + public.kern1.d + + l + + public.kern1.n.left + + n + + public.kern1.o.left + + o + + public.kern1.t.left + + t + + public.kern2.T.right + + T + + public.kern2.b + + l + + public.kern2.n.right + + n + + public.kern2.o.right + + o + + public.kern2.s.right + + s + + public.kern2.t.right + + t + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/kerning.plist b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/kerning.plist new file mode 100644 index 0000000..2000fcc --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/kerning.plist @@ -0,0 +1,17 @@ + + + + + public.kern1.T.left + + public.kern2.T.right + 20 + public.kern2.n.right + -50 + public.kern2.o.right + -70 + public.kern2.s.right + -60 + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/layercontents.plist b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/layercontents.plist new file mode 100644 index 0000000..cf95d35 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/layercontents.plist @@ -0,0 +1,10 @@ + + + + + + public.default + glyphs + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/lib.plist b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/lib.plist new file mode 100644 index 0000000..42d19d9 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/lib.plist @@ -0,0 +1,16 @@ + + + + + public.glyphOrder + + F + T + l + n + o + s + t + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/metainfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/metainfo.plist new file mode 100644 index 0000000..632695b --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Bold.ufo/metainfo.plist @@ -0,0 +1,10 @@ + + + + + creator + org.robofab.ufoLib + formatVersion + 3 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/fontinfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/fontinfo.plist new file mode 100644 index 0000000..52de30a --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/fontinfo.plist @@ -0,0 +1,153 @@ + + + + + ascender + 760 + capHeight + 714 + copyright + Copyright 2015 Google Inc. All Rights Reserved. + descender + -240 + familyName + Test Family 3 + guidelines + + openTypeHeadCreated + 2016/03/15 19:50:39 + openTypeHheaAscender + 1069 + openTypeHheaDescender + -293 + openTypeHheaLineGap + 0 + openTypeNameDescription + Designed by Monotype design team. + openTypeNameDesigner + Monotype Design Team + openTypeNameDesignerURL + http://www.monotype.com/studio + openTypeNameLicense + This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the SIL Open Font License for the specific language, permissions and limitations governing your use of this Font Software. + openTypeNameLicenseURL + http://scripts.sil.org/OFL + openTypeNameManufacturer + Monotype Imaging Inc. + openTypeNameManufacturerURL + http://www.google.com/get/noto/ + openTypeNameVersion + Version 1.902 + openTypeOS2Panose + + 2 + 11 + 5 + 6 + 4 + 5 + 4 + 2 + 2 + 4 + + openTypeOS2Selection + + 8 + + openTypeOS2Type + + openTypeOS2TypoAscender + 1069 + openTypeOS2TypoDescender + -293 + openTypeOS2TypoLineGap + 0 + openTypeOS2UnicodeRanges + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 9 + 29 + 30 + 31 + 32 + 33 + 34 + 35 + 36 + 62 + 64 + 67 + 69 + 91 + 116 + + openTypeOS2VendorID + GOOG + openTypeOS2WinAscent + 1069 + openTypeOS2WinDescent + 293 + postscriptBlueValues + + -15 + 0 + 537 + 548 + 571 + 581 + 714 + 725 + 760 + 766 + + postscriptFamilyBlues + + postscriptFamilyOtherBlues + + postscriptOtherBlues + + -256 + -240 + + postscriptStemSnapH + + 60 + 68 + 76 + + postscriptStemSnapV + + 64 + 75 + 84 + + postscriptUnderlinePosition + -100 + postscriptUnderlineThickness + 50 + styleMapFamilyName + Test Family 3 Condensed + styleMapStyleName + regular + styleName + Condensed + trademark + Noto is a trademark of Google Inc. + unitsPerEm + 1000 + versionMajor + 1 + versionMinor + 902 + xHeight + 537 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/F_.glif b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/F_.glif new file mode 100644 index 0000000..aa27dad --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/F_.glif @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/T_.glif b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/T_.glif new file mode 100644 index 0000000..fe0bff7 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/T_.glif @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/contents.plist b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/contents.plist new file mode 100644 index 0000000..d50a055 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/contents.plist @@ -0,0 +1,20 @@ + + + + + F + F_.glif + T + T_.glif + l + l.glif + n + n.glif + o + o.glif + s + s.glif + t + t.glif + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/l.glif b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/l.glif new file mode 100644 index 0000000..4a3fdac --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/l.glif @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/layerinfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/layerinfo.plist new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/layerinfo.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/n.glif b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/n.glif new file mode 100644 index 0000000..e1e638b --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/n.glif @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/o.glif b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/o.glif new file mode 100644 index 0000000..aa7f5c6 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/o.glif @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/s.glif b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/s.glif new file mode 100644 index 0000000..6f2a82f --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/s.glif @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/t.glif b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/t.glif new file mode 100644 index 0000000..42e05ce --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/glyphs/t.glif @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/groups.plist b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/groups.plist new file mode 100644 index 0000000..02bf8c6 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/groups.plist @@ -0,0 +1,50 @@ + + + + + public.kern1.T.left + + T + + public.kern1.d + + l + + public.kern1.n.left + + n + + public.kern1.o.left + + o + + public.kern1.t.left + + t + + public.kern2.T.right + + T + + public.kern2.b + + l + + public.kern2.n.right + + n + + public.kern2.o.right + + o + + public.kern2.s.right + + s + + public.kern2.t.right + + t + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/kerning.plist b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/kerning.plist new file mode 100644 index 0000000..0548061 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/kerning.plist @@ -0,0 +1,17 @@ + + + + + public.kern1.T.left + + public.kern2.T.right + 12 + public.kern2.n.right + -30 + public.kern2.o.right + -42 + public.kern2.s.right + -36 + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/layercontents.plist b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/layercontents.plist new file mode 100644 index 0000000..cf95d35 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/layercontents.plist @@ -0,0 +1,10 @@ + + + + + + public.default + glyphs + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/lib.plist b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/lib.plist new file mode 100644 index 0000000..42d19d9 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/lib.plist @@ -0,0 +1,16 @@ + + + + + public.glyphOrder + + F + T + l + n + o + s + t + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/metainfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/metainfo.plist new file mode 100644 index 0000000..632695b --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Condensed.ufo/metainfo.plist @@ -0,0 +1,10 @@ + + + + + creator + org.robofab.ufoLib + formatVersion + 3 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/fontinfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/fontinfo.plist new file mode 100644 index 0000000..d74fc13 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/fontinfo.plist @@ -0,0 +1,153 @@ + + + + + ascender + 760 + capHeight + 714 + copyright + Copyright 2015 Google Inc. All Rights Reserved. + descender + -240 + familyName + Test Family 3 + guidelines + + openTypeHeadCreated + 2016/03/15 19:50:39 + openTypeHheaAscender + 1069 + openTypeHheaDescender + -293 + openTypeHheaLineGap + 0 + openTypeNameDescription + Designed by Monotype design team. + openTypeNameDesigner + Monotype Design Team + openTypeNameDesignerURL + http://www.monotype.com/studio + openTypeNameLicense + This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the SIL Open Font License for the specific language, permissions and limitations governing your use of this Font Software. + openTypeNameLicenseURL + http://scripts.sil.org/OFL + openTypeNameManufacturer + Monotype Imaging Inc. + openTypeNameManufacturerURL + http://www.google.com/get/noto/ + openTypeNameVersion + Version 1.902 + openTypeOS2Panose + + 2 + 11 + 10 + 6 + 4 + 5 + 4 + 2 + 2 + 4 + + openTypeOS2Selection + + 8 + + openTypeOS2Type + + openTypeOS2TypoAscender + 1069 + openTypeOS2TypoDescender + -293 + openTypeOS2TypoLineGap + 0 + openTypeOS2UnicodeRanges + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 9 + 29 + 30 + 31 + 32 + 33 + 34 + 35 + 36 + 62 + 64 + 67 + 69 + 91 + 116 + + openTypeOS2VendorID + GOOG + openTypeOS2WinAscent + 1069 + openTypeOS2WinDescent + 293 + postscriptBlueValues + + -15 + 0 + 553 + 564 + 589 + 599 + 714 + 726 + 760 + 766 + + postscriptFamilyBlues + + postscriptFamilyOtherBlues + + postscriptOtherBlues + + -256 + -240 + + postscriptStemSnapH + + 115 + 133 + 146 + + postscriptStemSnapV + + 117 + 150 + 170 + + postscriptUnderlinePosition + -100 + postscriptUnderlineThickness + 50 + styleMapFamilyName + Test Family 3 Condensed Bold + styleMapStyleName + regular + styleName + Condensed Bold + trademark + Noto is a trademark of Google Inc. + unitsPerEm + 1000 + versionMajor + 1 + versionMinor + 902 + xHeight + 553 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/F_.glif b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/F_.glif new file mode 100644 index 0000000..f3c1c89 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/F_.glif @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/T_.glif b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/T_.glif new file mode 100644 index 0000000..603c147 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/T_.glif @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/contents.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/contents.plist new file mode 100644 index 0000000..d50a055 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/contents.plist @@ -0,0 +1,20 @@ + + + + + F + F_.glif + T + T_.glif + l + l.glif + n + n.glif + o + o.glif + s + s.glif + t + t.glif + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/l.glif b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/l.glif new file mode 100644 index 0000000..9beb251 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/l.glif @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/layerinfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/layerinfo.plist new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/layerinfo.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/n.glif b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/n.glif new file mode 100644 index 0000000..d914b64 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/n.glif @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/o.glif b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/o.glif new file mode 100644 index 0000000..78ebeae --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/o.glif @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/s.glif b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/s.glif new file mode 100644 index 0000000..2bc0153 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/s.glif @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/t.glif b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/t.glif new file mode 100644 index 0000000..0c0765c --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/glyphs/t.glif @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/groups.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/groups.plist new file mode 100644 index 0000000..02bf8c6 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/groups.plist @@ -0,0 +1,50 @@ + + + + + public.kern1.T.left + + T + + public.kern1.d + + l + + public.kern1.n.left + + n + + public.kern1.o.left + + o + + public.kern1.t.left + + t + + public.kern2.T.right + + T + + public.kern2.b + + l + + public.kern2.n.right + + n + + public.kern2.o.right + + o + + public.kern2.s.right + + s + + public.kern2.t.right + + t + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/kerning.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/kerning.plist new file mode 100644 index 0000000..0548061 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/kerning.plist @@ -0,0 +1,17 @@ + + + + + public.kern1.T.left + + public.kern2.T.right + 12 + public.kern2.n.right + -30 + public.kern2.o.right + -42 + public.kern2.s.right + -36 + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/layercontents.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/layercontents.plist new file mode 100644 index 0000000..cf95d35 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/layercontents.plist @@ -0,0 +1,10 @@ + + + + + + public.default + glyphs + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/lib.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/lib.plist new file mode 100644 index 0000000..42d19d9 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/lib.plist @@ -0,0 +1,16 @@ + + + + + public.glyphOrder + + F + T + l + n + o + s + t + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/metainfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/metainfo.plist new file mode 100644 index 0000000..632695b --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedBold.ufo/metainfo.plist @@ -0,0 +1,10 @@ + + + + + creator + org.robofab.ufoLib + formatVersion + 3 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/fontinfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/fontinfo.plist new file mode 100644 index 0000000..268b885 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/fontinfo.plist @@ -0,0 +1,149 @@ + + + + + ascender + 760 + capHeight + 714 + copyright + Copyright 2015 Google Inc. All Rights Reserved. + descender + -232 + familyName + Test Family 3 + guidelines + + openTypeHeadCreated + 2016/03/15 19:50:39 + openTypeHheaAscender + 1069 + openTypeHheaDescender + -293 + openTypeHheaLineGap + 0 + openTypeNameDescription + Designed by Monotype design team. + openTypeNameDesigner + Monotype Design Team + openTypeNameDesignerURL + http://www.monotype.com/studio + openTypeNameLicense + This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the SIL Open Font License for the specific language, permissions and limitations governing your use of this Font Software. + openTypeNameLicenseURL + http://scripts.sil.org/OFL + openTypeNameManufacturer + Monotype Imaging Inc. + openTypeNameManufacturerURL + http://www.google.com/get/noto/ + openTypeNameVersion + Version 1.902 + openTypeOS2Panose + + 2 + 11 + 2 + 6 + 4 + 5 + 4 + 2 + 2 + 4 + + openTypeOS2Selection + + 8 + + openTypeOS2Type + + openTypeOS2TypoAscender + 1069 + openTypeOS2TypoDescender + -293 + openTypeOS2TypoLineGap + 0 + openTypeOS2UnicodeRanges + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 9 + 29 + 30 + 31 + 32 + 33 + 34 + 35 + 36 + 62 + 64 + 67 + 69 + 91 + 116 + + openTypeOS2VendorID + GOOG + openTypeOS2WinAscent + 1069 + openTypeOS2WinDescent + 293 + postscriptBlueValues + + -15 + 0 + 527 + 537 + 559 + 569 + 714 + 724 + 760 + 765 + + postscriptFamilyBlues + + postscriptFamilyOtherBlues + + postscriptOtherBlues + + -242 + -232 + + postscriptStemSnapH + + 26 + + postscriptStemSnapV + + 26 + + postscriptUnderlinePosition + -100 + postscriptUnderlineThickness + 50 + styleMapFamilyName + Test Family 3 Condensed Light + styleMapStyleName + regular + styleName + Condensed Light + trademark + Noto is a trademark of Google Inc. + unitsPerEm + 1000 + versionMajor + 1 + versionMinor + 902 + xHeight + 527 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/F_.glif b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/F_.glif new file mode 100644 index 0000000..43c930e --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/F_.glif @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/T_.glif b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/T_.glif new file mode 100644 index 0000000..cc943a3 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/T_.glif @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/contents.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/contents.plist new file mode 100644 index 0000000..d50a055 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/contents.plist @@ -0,0 +1,20 @@ + + + + + F + F_.glif + T + T_.glif + l + l.glif + n + n.glif + o + o.glif + s + s.glif + t + t.glif + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/l.glif b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/l.glif new file mode 100644 index 0000000..6689b25 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/l.glif @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/layerinfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/layerinfo.plist new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/layerinfo.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/n.glif b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/n.glif new file mode 100644 index 0000000..b8b3116 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/n.glif @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/o.glif b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/o.glif new file mode 100644 index 0000000..a58d0c0 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/o.glif @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/s.glif b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/s.glif new file mode 100644 index 0000000..de8071b --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/s.glif @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/t.glif b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/t.glif new file mode 100644 index 0000000..ac82427 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/glyphs/t.glif @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/groups.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/groups.plist new file mode 100644 index 0000000..02bf8c6 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/groups.plist @@ -0,0 +1,50 @@ + + + + + public.kern1.T.left + + T + + public.kern1.d + + l + + public.kern1.n.left + + n + + public.kern1.o.left + + o + + public.kern1.t.left + + t + + public.kern2.T.right + + T + + public.kern2.b + + l + + public.kern2.n.right + + n + + public.kern2.o.right + + o + + public.kern2.s.right + + s + + public.kern2.t.right + + t + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/kerning.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/kerning.plist new file mode 100644 index 0000000..0548061 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/kerning.plist @@ -0,0 +1,17 @@ + + + + + public.kern1.T.left + + public.kern2.T.right + 12 + public.kern2.n.right + -30 + public.kern2.o.right + -42 + public.kern2.s.right + -36 + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/layercontents.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/layercontents.plist new file mode 100644 index 0000000..cf95d35 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/layercontents.plist @@ -0,0 +1,10 @@ + + + + + + public.default + glyphs + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/lib.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/lib.plist new file mode 100644 index 0000000..42d19d9 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/lib.plist @@ -0,0 +1,16 @@ + + + + + public.glyphOrder + + F + T + l + n + o + s + t + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/metainfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/metainfo.plist new file mode 100644 index 0000000..632695b --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedLight.ufo/metainfo.plist @@ -0,0 +1,10 @@ + + + + + creator + org.robofab.ufoLib + formatVersion + 3 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/fontinfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/fontinfo.plist new file mode 100644 index 0000000..4f8649f --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/fontinfo.plist @@ -0,0 +1,153 @@ + + + + + ascender + 760 + capHeight + 714 + copyright + Copyright 2015 Google Inc. All Rights Reserved. + descender + -240 + familyName + Test Family 3 + guidelines + + openTypeHeadCreated + 2016/03/15 19:50:39 + openTypeHheaAscender + 1069 + openTypeHheaDescender + -293 + openTypeHheaLineGap + 0 + openTypeNameDescription + Designed by Monotype design team. + openTypeNameDesigner + Monotype Design Team + openTypeNameDesignerURL + http://www.monotype.com/studio + openTypeNameLicense + This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the SIL Open Font License for the specific language, permissions and limitations governing your use of this Font Software. + openTypeNameLicenseURL + http://scripts.sil.org/OFL + openTypeNameManufacturer + Monotype Imaging Inc. + openTypeNameManufacturerURL + http://www.google.com/get/noto/ + openTypeNameVersion + Version 1.902 + openTypeOS2Panose + + 2 + 11 + 8 + 6 + 4 + 5 + 4 + 2 + 2 + 4 + + openTypeOS2Selection + + 8 + + openTypeOS2Type + + openTypeOS2TypoAscender + 1069 + openTypeOS2TypoDescender + -293 + openTypeOS2TypoLineGap + 0 + openTypeOS2UnicodeRanges + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 9 + 29 + 30 + 31 + 32 + 33 + 34 + 35 + 36 + 62 + 64 + 67 + 69 + 91 + 116 + + openTypeOS2VendorID + GOOG + openTypeOS2WinAscent + 1069 + openTypeOS2WinDescent + 293 + postscriptBlueValues + + -15 + 0 + 547 + 558 + 582 + 592 + 714 + 726 + 760 + 766 + + postscriptFamilyBlues + + postscriptFamilyOtherBlues + + postscriptOtherBlues + + -256 + -240 + + postscriptStemSnapH + + 95 + 108 + 119 + + postscriptStemSnapV + + 97 + 121 + 137 + + postscriptUnderlinePosition + -100 + postscriptUnderlineThickness + 50 + styleMapFamilyName + Test Family 3 Condensed SemiBold + styleMapStyleName + regular + styleName + Condensed SemiBold + trademark + Noto is a trademark of Google Inc. + unitsPerEm + 1000 + versionMajor + 1 + versionMinor + 902 + xHeight + 547 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/F_.glif b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/F_.glif new file mode 100644 index 0000000..c3578a3 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/F_.glif @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/T_.glif b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/T_.glif new file mode 100644 index 0000000..32e1ec3 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/T_.glif @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/contents.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/contents.plist new file mode 100644 index 0000000..d50a055 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/contents.plist @@ -0,0 +1,20 @@ + + + + + F + F_.glif + T + T_.glif + l + l.glif + n + n.glif + o + o.glif + s + s.glif + t + t.glif + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/l.glif b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/l.glif new file mode 100644 index 0000000..02b0e36 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/l.glif @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/layerinfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/layerinfo.plist new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/layerinfo.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/n.glif b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/n.glif new file mode 100644 index 0000000..10c02f2 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/n.glif @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/o.glif b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/o.glif new file mode 100644 index 0000000..c4b03fc --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/o.glif @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/s.glif b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/s.glif new file mode 100644 index 0000000..9ab6232 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/s.glif @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/t.glif b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/t.glif new file mode 100644 index 0000000..d43759a --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/glyphs/t.glif @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/groups.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/groups.plist new file mode 100644 index 0000000..02bf8c6 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/groups.plist @@ -0,0 +1,50 @@ + + + + + public.kern1.T.left + + T + + public.kern1.d + + l + + public.kern1.n.left + + n + + public.kern1.o.left + + o + + public.kern1.t.left + + t + + public.kern2.T.right + + T + + public.kern2.b + + l + + public.kern2.n.right + + n + + public.kern2.o.right + + o + + public.kern2.s.right + + s + + public.kern2.t.right + + t + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/kerning.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/kerning.plist new file mode 100644 index 0000000..0548061 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/kerning.plist @@ -0,0 +1,17 @@ + + + + + public.kern1.T.left + + public.kern2.T.right + 12 + public.kern2.n.right + -30 + public.kern2.o.right + -42 + public.kern2.s.right + -36 + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/layercontents.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/layercontents.plist new file mode 100644 index 0000000..cf95d35 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/layercontents.plist @@ -0,0 +1,10 @@ + + + + + + public.default + glyphs + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/lib.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/lib.plist new file mode 100644 index 0000000..42d19d9 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/lib.plist @@ -0,0 +1,16 @@ + + + + + public.glyphOrder + + F + T + l + n + o + s + t + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/metainfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/metainfo.plist new file mode 100644 index 0000000..632695b --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-CondensedSemiBold.ufo/metainfo.plist @@ -0,0 +1,10 @@ + + + + + creator + org.robofab.ufoLib + formatVersion + 3 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/fontinfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/fontinfo.plist new file mode 100644 index 0000000..575f306 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/fontinfo.plist @@ -0,0 +1,149 @@ + + + + + ascender + 760 + capHeight + 714 + copyright + Copyright 2015 Google Inc. All Rights Reserved. + descender + -240 + familyName + Test Family 3 + guidelines + + openTypeHeadCreated + 2016/03/15 19:50:39 + openTypeHheaAscender + 1069 + openTypeHheaDescender + -293 + openTypeHheaLineGap + 0 + openTypeNameDescription + Designed by Monotype design team. + openTypeNameDesigner + Monotype Design Team + openTypeNameDesignerURL + http://www.monotype.com/studio + openTypeNameLicense + This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the SIL Open Font License for the specific language, permissions and limitations governing your use of this Font Software. + openTypeNameLicenseURL + http://scripts.sil.org/OFL + openTypeNameManufacturer + Monotype Imaging Inc. + openTypeNameManufacturerURL + http://www.google.com/get/noto/ + openTypeNameVersion + Version 1.902 + openTypeOS2Panose + + 2 + 11 + 2 + 2 + 4 + 5 + 4 + 2 + 2 + 4 + + openTypeOS2Selection + + 8 + + openTypeOS2Type + + openTypeOS2TypoAscender + 1069 + openTypeOS2TypoDescender + -293 + openTypeOS2TypoLineGap + 0 + openTypeOS2UnicodeRanges + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 9 + 29 + 30 + 31 + 32 + 33 + 34 + 35 + 36 + 62 + 64 + 67 + 69 + 91 + 116 + + openTypeOS2VendorID + GOOG + openTypeOS2WinAscent + 1069 + openTypeOS2WinDescent + 293 + postscriptBlueValues + + -15 + 0 + 528 + 537 + 559 + 569 + 714 + 724 + 760 + 766 + + postscriptFamilyBlues + + postscriptFamilyOtherBlues + + postscriptOtherBlues + + -256 + -240 + + postscriptStemSnapH + + 25 + + postscriptStemSnapV + + 26 + + postscriptUnderlinePosition + -100 + postscriptUnderlineThickness + 50 + styleMapFamilyName + Test Family 3 Light + styleMapStyleName + regular + styleName + Light + trademark + Noto is a trademark of Google Inc. + unitsPerEm + 1000 + versionMajor + 1 + versionMinor + 902 + xHeight + 528 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/F_.glif b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/F_.glif new file mode 100644 index 0000000..6cc64b0 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/F_.glif @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/T_.glif b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/T_.glif new file mode 100644 index 0000000..2d18271 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/T_.glif @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/contents.plist b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/contents.plist new file mode 100644 index 0000000..d50a055 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/contents.plist @@ -0,0 +1,20 @@ + + + + + F + F_.glif + T + T_.glif + l + l.glif + n + n.glif + o + o.glif + s + s.glif + t + t.glif + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/l.glif b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/l.glif new file mode 100644 index 0000000..a7c1e80 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/l.glif @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/layerinfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/layerinfo.plist new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/layerinfo.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/n.glif b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/n.glif new file mode 100644 index 0000000..65ea31d --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/n.glif @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/o.glif b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/o.glif new file mode 100644 index 0000000..410754c --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/o.glif @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/s.glif b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/s.glif new file mode 100644 index 0000000..34cd9d4 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/s.glif @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/t.glif b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/t.glif new file mode 100644 index 0000000..9874dc9 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/glyphs/t.glif @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/groups.plist b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/groups.plist new file mode 100644 index 0000000..02bf8c6 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/groups.plist @@ -0,0 +1,50 @@ + + + + + public.kern1.T.left + + T + + public.kern1.d + + l + + public.kern1.n.left + + n + + public.kern1.o.left + + o + + public.kern1.t.left + + t + + public.kern2.T.right + + T + + public.kern2.b + + l + + public.kern2.n.right + + n + + public.kern2.o.right + + o + + public.kern2.s.right + + s + + public.kern2.t.right + + t + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/kerning.plist b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/kerning.plist new file mode 100644 index 0000000..2000fcc --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/kerning.plist @@ -0,0 +1,17 @@ + + + + + public.kern1.T.left + + public.kern2.T.right + 20 + public.kern2.n.right + -50 + public.kern2.o.right + -70 + public.kern2.s.right + -60 + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/layercontents.plist b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/layercontents.plist new file mode 100644 index 0000000..cf95d35 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/layercontents.plist @@ -0,0 +1,10 @@ + + + + + + public.default + glyphs + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/lib.plist b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/lib.plist new file mode 100644 index 0000000..42d19d9 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/lib.plist @@ -0,0 +1,16 @@ + + + + + public.glyphOrder + + F + T + l + n + o + s + t + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/metainfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/metainfo.plist new file mode 100644 index 0000000..632695b --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Light.ufo/metainfo.plist @@ -0,0 +1,10 @@ + + + + + creator + org.robofab.ufoLib + formatVersion + 3 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/fontinfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/fontinfo.plist new file mode 100644 index 0000000..c843436 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/fontinfo.plist @@ -0,0 +1,153 @@ + + + + + ascender + 760 + capHeight + 714 + copyright + Copyright 2015 Google Inc. All Rights Reserved. + descender + -240 + familyName + Test Family 3 + guidelines + + openTypeHeadCreated + 2016/03/15 19:50:39 + openTypeHheaAscender + 1069 + openTypeHheaDescender + -293 + openTypeHheaLineGap + 0 + openTypeNameDescription + Designed by Monotype design team. + openTypeNameDesigner + Monotype Design Team + openTypeNameDesignerURL + http://www.monotype.com/studio + openTypeNameLicense + This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the SIL Open Font License for the specific language, permissions and limitations governing your use of this Font Software. + openTypeNameLicenseURL + http://scripts.sil.org/OFL + openTypeNameManufacturer + Monotype Imaging Inc. + openTypeNameManufacturerURL + http://www.google.com/get/noto/ + openTypeNameVersion + Version 1.902 + openTypeOS2Panose + + 2 + 11 + 5 + 2 + 4 + 5 + 4 + 2 + 2 + 4 + + openTypeOS2Selection + + 8 + + openTypeOS2Type + + openTypeOS2TypoAscender + 1069 + openTypeOS2TypoDescender + -293 + openTypeOS2TypoLineGap + 0 + openTypeOS2UnicodeRanges + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 9 + 29 + 30 + 31 + 32 + 33 + 34 + 35 + 36 + 62 + 64 + 67 + 69 + 91 + 116 + + openTypeOS2VendorID + GOOG + openTypeOS2WinAscent + 1069 + openTypeOS2WinDescent + 293 + postscriptBlueValues + + -15 + 0 + 536 + 547 + 572 + 582 + 714 + 726 + 760 + 766 + + postscriptFamilyBlues + + postscriptFamilyOtherBlues + + postscriptOtherBlues + + -256 + -240 + + postscriptStemSnapH + + 60 + 68 + 79 + + postscriptStemSnapV + + 64 + 75 + 90 + + postscriptUnderlinePosition + -100 + postscriptUnderlineThickness + 50 + styleMapFamilyName + Test Family 3 + styleMapStyleName + regular + styleName + Regular + trademark + Noto is a trademark of Google Inc. + unitsPerEm + 1000 + versionMajor + 1 + versionMinor + 902 + xHeight + 536 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/F_.glif b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/F_.glif new file mode 100644 index 0000000..1dfb60b --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/F_.glif @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/T_.glif b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/T_.glif new file mode 100644 index 0000000..a92aaba --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/T_.glif @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/contents.plist b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/contents.plist new file mode 100644 index 0000000..d50a055 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/contents.plist @@ -0,0 +1,20 @@ + + + + + F + F_.glif + T + T_.glif + l + l.glif + n + n.glif + o + o.glif + s + s.glif + t + t.glif + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/l.glif b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/l.glif new file mode 100644 index 0000000..c8cf0fd --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/l.glif @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/layerinfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/layerinfo.plist new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/layerinfo.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/n.glif b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/n.glif new file mode 100644 index 0000000..018de2b --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/n.glif @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/o.glif b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/o.glif new file mode 100644 index 0000000..13b0675 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/o.glif @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/s.glif b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/s.glif new file mode 100644 index 0000000..3a58fb8 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/s.glif @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/t.glif b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/t.glif new file mode 100644 index 0000000..2e74d20 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/glyphs/t.glif @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/groups.plist b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/groups.plist new file mode 100644 index 0000000..02bf8c6 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/groups.plist @@ -0,0 +1,50 @@ + + + + + public.kern1.T.left + + T + + public.kern1.d + + l + + public.kern1.n.left + + n + + public.kern1.o.left + + o + + public.kern1.t.left + + t + + public.kern2.T.right + + T + + public.kern2.b + + l + + public.kern2.n.right + + n + + public.kern2.o.right + + o + + public.kern2.s.right + + s + + public.kern2.t.right + + t + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/kerning.plist b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/kerning.plist new file mode 100644 index 0000000..2000fcc --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/kerning.plist @@ -0,0 +1,17 @@ + + + + + public.kern1.T.left + + public.kern2.T.right + 20 + public.kern2.n.right + -50 + public.kern2.o.right + -70 + public.kern2.s.right + -60 + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/layercontents.plist b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/layercontents.plist new file mode 100644 index 0000000..cf95d35 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/layercontents.plist @@ -0,0 +1,10 @@ + + + + + + public.default + glyphs + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/lib.plist b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/lib.plist new file mode 100644 index 0000000..42d19d9 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/lib.plist @@ -0,0 +1,16 @@ + + + + + public.glyphOrder + + F + T + l + n + o + s + t + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/metainfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/metainfo.plist new file mode 100644 index 0000000..632695b --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-Regular.ufo/metainfo.plist @@ -0,0 +1,10 @@ + + + + + creator + org.robofab.ufoLib + formatVersion + 3 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/fontinfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/fontinfo.plist new file mode 100644 index 0000000..b819589 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/fontinfo.plist @@ -0,0 +1,153 @@ + + + + + ascender + 760 + capHeight + 714 + copyright + Copyright 2015 Google Inc. All Rights Reserved. + descender + -240 + familyName + Test Family 3 + guidelines + + openTypeHeadCreated + 2016/03/15 19:50:39 + openTypeHheaAscender + 1069 + openTypeHheaDescender + -293 + openTypeHheaLineGap + 0 + openTypeNameDescription + Designed by Monotype design team. + openTypeNameDesigner + Monotype Design Team + openTypeNameDesignerURL + http://www.monotype.com/studio + openTypeNameLicense + This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the SIL Open Font License for the specific language, permissions and limitations governing your use of this Font Software. + openTypeNameLicenseURL + http://scripts.sil.org/OFL + openTypeNameManufacturer + Monotype Imaging Inc. + openTypeNameManufacturerURL + http://www.google.com/get/noto/ + openTypeNameVersion + Version 1.902 + openTypeOS2Panose + + 2 + 11 + 8 + 2 + 4 + 5 + 4 + 2 + 2 + 4 + + openTypeOS2Selection + + 8 + + openTypeOS2Type + + openTypeOS2TypoAscender + 1069 + openTypeOS2TypoDescender + -293 + openTypeOS2TypoLineGap + 0 + openTypeOS2UnicodeRanges + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 9 + 29 + 30 + 31 + 32 + 33 + 34 + 35 + 36 + 62 + 64 + 67 + 69 + 91 + 116 + + openTypeOS2VendorID + GOOG + openTypeOS2WinAscent + 1069 + openTypeOS2WinDescent + 293 + postscriptBlueValues + + -15 + 0 + 546 + 557 + 582 + 593 + 714 + 726 + 760 + 766 + + postscriptFamilyBlues + + postscriptFamilyOtherBlues + + postscriptOtherBlues + + -256 + -240 + + postscriptStemSnapH + + 103 + 112 + 124 + + postscriptStemSnapV + + 117 + 144 + 151 + + postscriptUnderlinePosition + -100 + postscriptUnderlineThickness + 50 + styleMapFamilyName + Test Family 3 SemiBold + styleMapStyleName + regular + styleName + SemiBold + trademark + Noto is a trademark of Google Inc. + unitsPerEm + 1000 + versionMajor + 1 + versionMinor + 902 + xHeight + 546 + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/F_.glif b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/F_.glif new file mode 100644 index 0000000..6a2b861 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/F_.glif @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/T_.glif b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/T_.glif new file mode 100644 index 0000000..93aed48 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/T_.glif @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/contents.plist b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/contents.plist new file mode 100644 index 0000000..d50a055 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/contents.plist @@ -0,0 +1,20 @@ + + + + + F + F_.glif + T + T_.glif + l + l.glif + n + n.glif + o + o.glif + s + s.glif + t + t.glif + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/l.glif b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/l.glif new file mode 100644 index 0000000..7e6cdad --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/l.glif @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/layerinfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/layerinfo.plist new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/layerinfo.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/n.glif b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/n.glif new file mode 100644 index 0000000..74a3878 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/n.glif @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/o.glif b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/o.glif new file mode 100644 index 0000000..e78de65 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/o.glif @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/s.glif b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/s.glif new file mode 100644 index 0000000..e027b48 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/s.glif @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/t.glif b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/t.glif new file mode 100644 index 0000000..da81c00 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/glyphs/t.glif @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/groups.plist b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/groups.plist new file mode 100644 index 0000000..02bf8c6 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/groups.plist @@ -0,0 +1,50 @@ + + + + + public.kern1.T.left + + T + + public.kern1.d + + l + + public.kern1.n.left + + n + + public.kern1.o.left + + o + + public.kern1.t.left + + t + + public.kern2.T.right + + T + + public.kern2.b + + l + + public.kern2.n.right + + n + + public.kern2.o.right + + o + + public.kern2.s.right + + s + + public.kern2.t.right + + t + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/kerning.plist b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/kerning.plist new file mode 100644 index 0000000..2000fcc --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/kerning.plist @@ -0,0 +1,17 @@ + + + + + public.kern1.T.left + + public.kern2.T.right + 20 + public.kern2.n.right + -50 + public.kern2.o.right + -70 + public.kern2.s.right + -60 + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/layercontents.plist b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/layercontents.plist new file mode 100644 index 0000000..cf95d35 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/layercontents.plist @@ -0,0 +1,10 @@ + + + + + + public.default + glyphs + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/lib.plist b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/lib.plist new file mode 100644 index 0000000..42d19d9 --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/lib.plist @@ -0,0 +1,16 @@ + + + + + public.glyphOrder + + F + T + l + n + o + s + t + + + diff --git a/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/metainfo.plist b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/metainfo.plist new file mode 100644 index 0000000..632695b --- /dev/null +++ b/Tests/varLib/data/master_ufo/TestFamily3-SemiBold.ufo/metainfo.plist @@ -0,0 +1,10 @@ + + + + + creator + org.robofab.ufoLib + formatVersion + 3 + + diff --git a/Tests/varLib/data/test_results/Build.ttx b/Tests/varLib/data/test_results/Build.ttx new file mode 100644 index 0000000..5d665e3 --- /dev/null +++ b/Tests/varLib/data/test_results/Build.ttx @@ -0,0 +1,1601 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + wght + 0x0 + 0.0 + 368.0 + 1000.0 + 256 + + + + + cntr + 0x0 + 0.0 + 0.0 + 100.0 + 257 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/Build3.ttx b/Tests/varLib/data/test_results/Build3.ttx new file mode 100644 index 0000000..a6ae23e --- /dev/null +++ b/Tests/varLib/data/test_results/Build3.ttx @@ -0,0 +1,725 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + wght + 0x0 + 0.0 + 0.0 + 1000.0 + 257 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/BuildAvarEmptyAxis.ttx b/Tests/varLib/data/test_results/BuildAvarEmptyAxis.ttx new file mode 100644 index 0000000..aee6f5a --- /dev/null +++ b/Tests/varLib/data/test_results/BuildAvarEmptyAxis.ttx @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/BuildAvarIdentityMaps.ttx b/Tests/varLib/data/test_results/BuildAvarIdentityMaps.ttx new file mode 100644 index 0000000..799d68f --- /dev/null +++ b/Tests/varLib/data/test_results/BuildAvarIdentityMaps.ttx @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/BuildAvarSingleAxis.ttx b/Tests/varLib/data/test_results/BuildAvarSingleAxis.ttx new file mode 100644 index 0000000..9daa330 --- /dev/null +++ b/Tests/varLib/data/test_results/BuildAvarSingleAxis.ttx @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/BuildMain.ttx b/Tests/varLib/data/test_results/BuildMain.ttx new file mode 100644 index 0000000..33ebbd4 --- /dev/null +++ b/Tests/varLib/data/test_results/BuildMain.ttx @@ -0,0 +1,2238 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PUSHB[ ] /* 1 value pushed */ + 0 + FDEF[ ] /* FunctionDefinition */ + POP[ ] /* PopTopStack */ + ENDF[ ] /* EndFunctionDefinition */ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PUSHB[ ] /* 1 value pushed */ + 0 + MDAP[0] /* MoveDirectAbsPt */ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Weight + + + Contrast + + + ExtraLight + + + TestFamily-ExtraLight + + + Light + + + TestFamily-Light + + + Regular + + + TestFamily-Regular + + + Semibold + + + TestFamily-Semibold + + + Bold + + + TestFamily-Bold + + + Black + + + TestFamily-Black + + + Black Medium Contrast + + + TestFamily-BlackMediumContrast + + + Black High Contrast + + + TestFamily-BlackHighContrast + + + Test Family + + + Regular + + + Version 1.001;ADBO;Test Family Regular + + + Test Family + + + Version 1.001 + + + TestFamily-Master1 + + + Frank Grießhammer + + + Master 1 + + + Weight + + + Contrast + + + ExtraLight + + + TestFamily-ExtraLight + + + Light + + + TestFamily-Light + + + Regular + + + TestFamily-Regular + + + Semibold + + + TestFamily-Semibold + + + Bold + + + TestFamily-Bold + + + Black + + + TestFamily-Black + + + Black Medium Contrast + + + TestFamily-BlackMediumContrast + + + Black High Contrast + + + TestFamily-BlackHighContrast + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + wght + 0x0 + 0.0 + 368.0 + 1000.0 + 256 + + + + + cntr + 0x0 + 0.0 + 0.0 + 100.0 + 257 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/InterpolateLayout.ttx b/Tests/varLib/data/test_results/InterpolateLayout.ttx new file mode 100644 index 0000000..b1ea1e9 --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateLayout.ttx @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/InterpolateLayout2.ttx b/Tests/varLib/data/test_results/InterpolateLayout2.ttx new file mode 100644 index 0000000..37461c4 --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateLayout2.ttx @@ -0,0 +1,4 @@ + + + + diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_diff.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_diff.ttx new file mode 100644 index 0000000..74e9cc5 --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_diff.ttx @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_diff2.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_diff2.ttx new file mode 100644 index 0000000..2e21b26 --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_diff2.ttx @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_same.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_same.ttx new file mode 100644 index 0000000..a61e75f --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_1_same.ttx @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_class_diff.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_class_diff.ttx new file mode 100644 index 0000000..4f94c37 --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_class_diff.ttx @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_class_diff2.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_class_diff2.ttx new file mode 100644 index 0000000..811ed58 --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_class_diff2.ttx @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_class_same.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_class_same.ttx new file mode 100644 index 0000000..9872533 --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_class_same.ttx @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_spec_diff.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_spec_diff.ttx new file mode 100644 index 0000000..113bd0b --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_spec_diff.ttx @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_spec_diff2.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_spec_diff2.ttx new file mode 100644 index 0000000..efc5ee5 --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_spec_diff2.ttx @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_spec_same.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_spec_same.ttx new file mode 100644 index 0000000..014c1ec --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_2_spec_same.ttx @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_3_diff.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_3_diff.ttx new file mode 100644 index 0000000..65d77f9 --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_3_diff.ttx @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_3_same.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_3_same.ttx new file mode 100644 index 0000000..b7c8a25 --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_3_same.ttx @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_4_diff.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_4_diff.ttx new file mode 100644 index 0000000..72a8ccf --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_4_diff.ttx @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_4_same.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_4_same.ttx new file mode 100644 index 0000000..9b41519 --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_4_same.ttx @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_5_diff.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_5_diff.ttx new file mode 100644 index 0000000..28480e7 --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_5_diff.ttx @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_5_same.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_5_same.ttx new file mode 100644 index 0000000..4830f9a --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_5_same.ttx @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_6_diff.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_6_diff.ttx new file mode 100644 index 0000000..38d6437 --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_6_diff.ttx @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_6_same.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_6_same.ttx new file mode 100644 index 0000000..05e4b51 --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_6_same.ttx @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx new file mode 100644 index 0000000..12f4269 --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_same.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_same.ttx new file mode 100644 index 0000000..b7e86ba --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_same.ttx @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_size_feat_same.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_size_feat_same.ttx new file mode 100644 index 0000000..773dc59 --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_size_feat_same.ttx @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/InterpolateLayoutMain.ttx b/Tests/varLib/data/test_results/InterpolateLayoutMain.ttx new file mode 100644 index 0000000..0c0af32 --- /dev/null +++ b/Tests/varLib/data/test_results/InterpolateLayoutMain.ttx @@ -0,0 +1,499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PUSHB[ ] /* 1 value pushed */ + 0 + FDEF[ ] /* FunctionDefinition */ + POP[ ] /* PopTopStack */ + ENDF[ ] /* EndFunctionDefinition */ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PUSHB[ ] /* 1 value pushed */ + 0 + MDAP[0] /* MoveDirectAbsPt */ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test Family + + + Regular + + + Version 1.001;ADBO;Test Family Regular + + + Test Family + + + Version 1.001 + + + TestFamily-Master1 + + + Frank Grießhammer + + + Master 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/Mutator.ttx b/Tests/varLib/data/test_results/Mutator.ttx new file mode 100644 index 0000000..a106d21 --- /dev/null +++ b/Tests/varLib/data/test_results/Mutator.ttx @@ -0,0 +1,499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PUSHB[ ] /* 1 value pushed */ + 0 + FDEF[ ] /* FunctionDefinition */ + POP[ ] /* PopTopStack */ + ENDF[ ] /* EndFunctionDefinition */ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PUSHB[ ] /* 1 value pushed */ + 0 + MDAP[0] /* MoveDirectAbsPt */ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test Family + + + Regular + + + Version 1.001;ADBO;Test Family Regular + + + Test Family + + + Version 1.001 + + + TestFamily-Master1 + + + Frank Grießhammer + + + Master 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/Mutator_IUP-instance.ttx b/Tests/varLib/data/test_results/Mutator_IUP-instance.ttx new file mode 100755 index 0000000..1800479 --- /dev/null +++ b/Tests/varLib/data/test_results/Mutator_IUP-instance.ttx @@ -0,0 +1,282 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + VarFont + + + Regular + + + VarFont Regular: 2017 + + + VarFont Regular + + + VarFont-Regular + + + VarFont + + + Regular + + + VarFont Regular: 2017 + + + VarFont Regular + + + VarFont-Regular + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/designspace_test.py b/Tests/varLib/designspace_test.py new file mode 100644 index 0000000..fbdaab3 --- /dev/null +++ b/Tests/varLib/designspace_test.py @@ -0,0 +1,69 @@ +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.varLib import designspace +import os +import unittest + + +class DesignspaceTest(unittest.TestCase): + def test_load(self): + self.maxDiff = None + self.assertEqual( + designspace.load(_getpath("Designspace.designspace")), + + {'sources': + [{'location': {'weight': 0.0}, + 'groups': {'copy': True}, + 'filename': 'DesignspaceTest-Light.ufo', + 'info': {'copy': True}, + 'name': 'master_1', + 'lib': {'copy': True}}, + {'location': {'weight': 1.0}, + 'name': 'master_2', + 'filename': 'DesignspaceTest-Bold.ufo'}], + + 'instances': + [{'location': {'weight': 0.5}, + 'familyname': 'DesignspaceTest', + 'filename': 'instance/DesignspaceTest-Medium.ufo', + 'kerning': {}, + 'info': {}, + 'stylename': 'Medium'}], + + 'axes': + [{'name': 'weight', + 'map': [{'input': 0.0, 'output': 10.0}, + {'input': 401.0, 'output': 66.0}, + {'input': 1000.0, 'output': 990.0}], + 'tag': 'wght', + 'maximum': 1000.0, + 'minimum': 0.0, + 'default': 0.0}, + {'maximum': 1000.0, + 'default': 250.0, + 'minimum': 0.0, + 'name': 'width', + 'tag': 'wdth'}, + {'name': 'contrast', + 'tag': 'cntr', + 'maximum': 100.0, + 'minimum': 0.0, + 'default': 0.0, + 'labelname': {'de': 'Kontrast', 'en': 'Contrast'}}] + } + ) + + def test_load2(self): + self.assertEqual( + designspace.load(_getpath("Designspace2.designspace")), + {'sources': [], 'instances': [{}]}) + + +def _getpath(testfile): + path, _ = os.path.split(__file__) + return os.path.join(path, "data", testfile) + + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/varLib/interpolatable_test.py b/Tests/varLib/interpolatable_test.py new file mode 100644 index 0000000..4900d52 --- /dev/null +++ b/Tests/varLib/interpolatable_test.py @@ -0,0 +1,102 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.ttLib import TTFont +from fontTools.varLib.interpolatable import main as interpolatable_main +import os +import shutil +import sys +import tempfile +import unittest + +try: + import scipy +except: + scipy = None + +try: + import munkres +except ImportError: + munkres = None + + +@unittest.skipUnless(scipy or munkres, "scipy or munkres not installed") +class InterpolatableTest(unittest.TestCase): + def __init__(self, methodName): + unittest.TestCase.__init__(self, methodName) + # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, + # and fires deprecation warnings if a program uses the old name. + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + + def setUp(self): + self.tempdir = None + self.num_tempfiles = 0 + + def tearDown(self): + if self.tempdir: + shutil.rmtree(self.tempdir) + + @staticmethod + def get_test_input(test_file_or_folder): + path, _ = os.path.split(__file__) + return os.path.join(path, "data", test_file_or_folder) + + @staticmethod + def get_file_list(folder, suffix, prefix=''): + all_files = os.listdir(folder) + file_list = [] + for p in all_files: + if p.startswith(prefix) and p.endswith(suffix): + file_list.append(os.path.abspath(os.path.join(folder, p))) + return file_list + + def temp_path(self, suffix): + self.temp_dir() + self.num_tempfiles += 1 + return os.path.join(self.tempdir, + "tmp%d%s" % (self.num_tempfiles, suffix)) + + def temp_dir(self): + if not self.tempdir: + self.tempdir = tempfile.mkdtemp() + + def compile_font(self, path, suffix, temp_dir): + ttx_filename = os.path.basename(path) + savepath = os.path.join(temp_dir, ttx_filename.replace('.ttx', suffix)) + font = TTFont(recalcBBoxes=False, recalcTimestamp=False) + font.importXML(path) + font.save(savepath, reorderTables=None) + return font, savepath + +# ----- +# Tests +# ----- + + def test_interpolatable_ttf(self): + suffix = '.ttf' + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for path in ttx_paths: + self.compile_font(path, suffix, self.tempdir) + + ttf_paths = self.get_file_list(self.tempdir, suffix) + self.assertIsNone(interpolatable_main(ttf_paths)) + + + def test_interpolatable_otf(self): + suffix = '.otf' + ttx_dir = self.get_test_input('master_ttx_interpolatable_otf') + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for path in ttx_paths: + self.compile_font(path, suffix, self.tempdir) + + otf_paths = self.get_file_list(self.tempdir, suffix) + self.assertIsNone(interpolatable_main(otf_paths)) + + +if __name__ == "__main__": + sys.exit(unittest.main()) diff --git a/Tests/varLib/interpolate_layout_test.py b/Tests/varLib/interpolate_layout_test.py new file mode 100644 index 0000000..6f6efe0 --- /dev/null +++ b/Tests/varLib/interpolate_layout_test.py @@ -0,0 +1,890 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.ttLib import TTFont +from fontTools.varLib import build +from fontTools.varLib.interpolate_layout import interpolate_layout +from fontTools.varLib.interpolate_layout import main as interpolate_layout_main +from fontTools.feaLib.builder import addOpenTypeFeaturesFromString +import difflib +import os +import shutil +import sys +import tempfile +import unittest + + +class InterpolateLayoutTest(unittest.TestCase): + def __init__(self, methodName): + unittest.TestCase.__init__(self, methodName) + # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, + # and fires deprecation warnings if a program uses the old name. + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + + def setUp(self): + self.tempdir = None + self.num_tempfiles = 0 + + def tearDown(self): + if self.tempdir: + shutil.rmtree(self.tempdir) + + @staticmethod + def get_test_input(test_file_or_folder): + path, _ = os.path.split(__file__) + return os.path.join(path, "data", test_file_or_folder) + + @staticmethod + def get_test_output(test_file_or_folder): + path, _ = os.path.split(__file__) + return os.path.join(path, "data", "test_results", test_file_or_folder) + + @staticmethod + def get_file_list(folder, suffix, prefix=''): + all_files = os.listdir(folder) + file_list = [] + for p in all_files: + if p.startswith(prefix) and p.endswith(suffix): + file_list.append(os.path.abspath(os.path.join(folder, p))) + return file_list + + def temp_path(self, suffix): + self.temp_dir() + self.num_tempfiles += 1 + return os.path.join(self.tempdir, + "tmp%d%s" % (self.num_tempfiles, suffix)) + + def temp_dir(self): + if not self.tempdir: + self.tempdir = tempfile.mkdtemp() + + def read_ttx(self, path): + lines = [] + with open(path, "r", encoding="utf-8") as ttx: + for line in ttx.readlines(): + # Elide ttFont attributes because ttLibVersion may change, + # and use os-native line separators so we can run difflib. + if line.startswith("" + os.linesep) + else: + lines.append(line.rstrip() + os.linesep) + return lines + + def expect_ttx(self, font, expected_ttx, tables): + path = self.temp_path(suffix=".ttx") + font.saveXML(path, tables=tables) + actual = self.read_ttx(path) + expected = self.read_ttx(expected_ttx) + if actual != expected: + for line in difflib.unified_diff( + expected, actual, fromfile=expected_ttx, tofile=path): + sys.stdout.write(line) + self.fail("TTX output is different from expected") + + def check_ttx_dump(self, font, expected_ttx, tables, suffix): + """Ensure the TTX dump is the same after saving and reloading the font.""" + path = self.temp_path(suffix=suffix) + font.save(path) + self.expect_ttx(TTFont(path), expected_ttx, tables) + + def compile_font(self, path, suffix, temp_dir, features=None): + ttx_filename = os.path.basename(path) + savepath = os.path.join(temp_dir, ttx_filename.replace('.ttx', suffix)) + font = TTFont(recalcBBoxes=False, recalcTimestamp=False) + font.importXML(path) + if features: + addOpenTypeFeaturesFromString(font, features) + font.save(savepath, reorderTables=None) + return font, savepath + +# ----- +# Tests +# ----- + + def test_varlib_interpolate_layout_GSUB_only_ttf(self): + """Only GSUB, and only in the base master. + + The variable font will inherit the GSUB table from the + base master. + """ + suffix = '.ttf' + ds_path = self.get_test_input('InterpolateLayout.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for path in ttx_paths: + self.compile_font(path, suffix, self.tempdir) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + instfont = interpolate_layout(ds_path, {'weight': 500}, finder) + + tables = ['GSUB'] + expected_ttx_path = self.get_test_output('InterpolateLayout.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix) + + + def test_varlib_interpolate_layout_no_GSUB_ttf(self): + """The base master has no GSUB table. + + The variable font will end up without a GSUB table. + """ + suffix = '.ttf' + ds_path = self.get_test_input('InterpolateLayout2.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for path in ttx_paths: + self.compile_font(path, suffix, self.tempdir) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + instfont = interpolate_layout(ds_path, {'weight': 500}, finder) + + tables = ['GSUB'] + expected_ttx_path = self.get_test_output('InterpolateLayout2.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix) + + + def test_varlib_interpolate_layout_GSUB_only_no_axes_ttf(self): + """Only GSUB, and only in the base master. + Designspace file has no element. + + The variable font will inherit the GSUB table from the + base master. + """ + suffix = '.ttf' + ds_path = self.get_test_input('InterpolateLayout3.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for path in ttx_paths: + self.compile_font(path, suffix, self.tempdir) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + instfont = interpolate_layout(ds_path, {'weight': 500}, finder) + + tables = ['GSUB'] + expected_ttx_path = self.get_test_output('InterpolateLayout.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix) + + + def test_varlib_interpolate_layout_GPOS_only_size_feat_same_val_ttf(self): + """Only GPOS; 'size' feature; same values in all masters. + """ + suffix = '.ttf' + ds_path = self.get_test_input('InterpolateLayout.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + fea_str = """ + feature size { + parameters 10.0 0; + } size; + """ + features = [fea_str] * 2 + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for i, path in enumerate(ttx_paths): + self.compile_font(path, suffix, self.tempdir, features[i]) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + instfont = interpolate_layout(ds_path, {'weight': 500}, finder) + + tables = ['GPOS'] + expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_size_feat_same.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix) + + + def test_varlib_interpolate_layout_GPOS_only_LookupType_1_same_val_ttf(self): + """Only GPOS; LookupType 1; same values in all masters. + """ + suffix = '.ttf' + ds_path = self.get_test_input('InterpolateLayout.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + fea_str = """ + feature xxxx { + pos A <-80 0 -160 0>; + } xxxx; + """ + features = [fea_str] * 2 + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for i, path in enumerate(ttx_paths): + self.compile_font(path, suffix, self.tempdir, features[i]) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + instfont = interpolate_layout(ds_path, {'weight': 500}, finder) + + tables = ['GPOS'] + expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_1_same.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix) + + + def test_varlib_interpolate_layout_GPOS_only_LookupType_1_diff_val_ttf(self): + """Only GPOS; LookupType 1; different values in each master. + """ + suffix = '.ttf' + ds_path = self.get_test_input('InterpolateLayout.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + fea_str_0 = """ + feature xxxx { + pos A <-80 0 -160 0>; + } xxxx; + """ + fea_str_1 = """ + feature xxxx { + pos A <-97 0 -195 0>; + } xxxx; + """ + features = [fea_str_0, fea_str_1] + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for i, path in enumerate(ttx_paths): + self.compile_font(path, suffix, self.tempdir, features[i]) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + instfont = interpolate_layout(ds_path, {'weight': 500}, finder) + + tables = ['GPOS'] + expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_1_diff.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix) + + + def test_varlib_interpolate_layout_GPOS_only_LookupType_1_diff2_val_ttf(self): + """Only GPOS; LookupType 1; different values and items in each master. + """ + suffix = '.ttf' + ds_path = self.get_test_input('InterpolateLayout.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + fea_str_0 = """ + feature xxxx { + pos A <-80 0 -160 0>; + pos a <-55 0 -105 0>; + } xxxx; + """ + fea_str_1 = """ + feature xxxx { + pos A <-97 0 -195 0>; + } xxxx; + """ + features = [fea_str_0, fea_str_1] + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for i, path in enumerate(ttx_paths): + self.compile_font(path, suffix, self.tempdir, features[i]) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + instfont = interpolate_layout(ds_path, {'weight': 500}, finder) + + tables = ['GPOS'] + expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_1_diff2.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix) + + + def test_varlib_interpolate_layout_GPOS_only_LookupType_2_spec_pairs_same_val_ttf(self): + """Only GPOS; LookupType 2 specific pairs; same values in all masters. + """ + suffix = '.ttf' + ds_path = self.get_test_input('InterpolateLayout.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + fea_str = """ + feature xxxx { + pos A a -53; + } xxxx; + """ + features = [fea_str] * 2 + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for i, path in enumerate(ttx_paths): + self.compile_font(path, suffix, self.tempdir, features[i]) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + instfont = interpolate_layout(ds_path, {'weight': 500}, finder) + + tables = ['GPOS'] + expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_2_spec_same.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix) + + + def test_varlib_interpolate_layout_GPOS_only_LookupType_2_spec_pairs_diff_val_ttf(self): + """Only GPOS; LookupType 2 specific pairs; different values in each master. + """ + suffix = '.ttf' + ds_path = self.get_test_input('InterpolateLayout.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + fea_str_0 = """ + feature xxxx { + pos A a -53; + } xxxx; + """ + fea_str_1 = """ + feature xxxx { + pos A a -27; + } xxxx; + """ + features = [fea_str_0, fea_str_1] + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for i, path in enumerate(ttx_paths): + self.compile_font(path, suffix, self.tempdir, features[i]) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + instfont = interpolate_layout(ds_path, {'weight': 500}, finder) + + tables = ['GPOS'] + expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_2_spec_diff.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix) + + + def test_varlib_interpolate_layout_GPOS_only_LookupType_2_spec_pairs_diff2_val_ttf(self): + """Only GPOS; LookupType 2 specific pairs; different values and items in each master. + """ + suffix = '.ttf' + ds_path = self.get_test_input('InterpolateLayout.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + fea_str_0 = """ + feature xxxx { + pos A a -53; + } xxxx; + """ + fea_str_1 = """ + feature xxxx { + pos A a -27; + pos a a 19; + } xxxx; + """ + features = [fea_str_0, fea_str_1] + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for i, path in enumerate(ttx_paths): + self.compile_font(path, suffix, self.tempdir, features[i]) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + instfont = interpolate_layout(ds_path, {'weight': 500}, finder) + + tables = ['GPOS'] + expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_2_spec_diff2.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix) + + + def test_varlib_interpolate_layout_GPOS_only_LookupType_2_class_pairs_same_val_ttf(self): + """Only GPOS; LookupType 2 class pairs; same values in all masters. + """ + suffix = '.ttf' + ds_path = self.get_test_input('InterpolateLayout.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + fea_str = """ + feature xxxx { + pos [A] [a] -53; + } xxxx; + """ + features = [fea_str] * 2 + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for i, path in enumerate(ttx_paths): + self.compile_font(path, suffix, self.tempdir, features[i]) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + instfont = interpolate_layout(ds_path, {'weight': 500}, finder) + + tables = ['GPOS'] + expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_2_class_same.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix) + + + def test_varlib_interpolate_layout_GPOS_only_LookupType_2_class_pairs_diff_val_ttf(self): + """Only GPOS; LookupType 2 class pairs; different values in each master. + """ + suffix = '.ttf' + ds_path = self.get_test_input('InterpolateLayout.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + fea_str_0 = """ + feature xxxx { + pos [A] [a] -53; + } xxxx; + """ + fea_str_1 = """ + feature xxxx { + pos [A] [a] -27; + } xxxx; + """ + features = [fea_str_0, fea_str_1] + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for i, path in enumerate(ttx_paths): + self.compile_font(path, suffix, self.tempdir, features[i]) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + instfont = interpolate_layout(ds_path, {'weight': 500}, finder) + + tables = ['GPOS'] + expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_2_class_diff.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix) + + + def test_varlib_interpolate_layout_GPOS_only_LookupType_2_class_pairs_diff2_val_ttf(self): + """Only GPOS; LookupType 2 class pairs; different values and items in each master. + """ + suffix = '.ttf' + ds_path = self.get_test_input('InterpolateLayout.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + fea_str_0 = """ + feature xxxx { + pos [A] [a] -53; + } xxxx; + """ + fea_str_1 = """ + feature xxxx { + pos [A] [a] -27; + pos [a] [a] 19; + } xxxx; + """ + features = [fea_str_0, fea_str_1] + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for i, path in enumerate(ttx_paths): + self.compile_font(path, suffix, self.tempdir, features[i]) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + instfont = interpolate_layout(ds_path, {'weight': 500}, finder) + + tables = ['GPOS'] + expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_2_class_diff2.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix) + + + def test_varlib_interpolate_layout_GPOS_only_LookupType_3_same_val_ttf(self): + """Only GPOS; LookupType 3; same values in all masters. + """ + suffix = '.ttf' + ds_path = self.get_test_input('InterpolateLayout.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + fea_str = """ + feature xxxx { + pos cursive a ; + } xxxx; + """ + features = [fea_str] * 2 + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for i, path in enumerate(ttx_paths): + self.compile_font(path, suffix, self.tempdir, features[i]) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + instfont = interpolate_layout(ds_path, {'weight': 500}, finder) + + tables = ['GPOS'] + expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_3_same.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix) + + + def test_varlib_interpolate_layout_GPOS_only_LookupType_3_diff_val_ttf(self): + """Only GPOS; LookupType 3; different values in each master. + """ + suffix = '.ttf' + ds_path = self.get_test_input('InterpolateLayout.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + fea_str_0 = """ + feature xxxx { + pos cursive a ; + } xxxx; + """ + fea_str_1 = """ + feature xxxx { + pos cursive a ; + } xxxx; + """ + features = [fea_str_0, fea_str_1] + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for i, path in enumerate(ttx_paths): + self.compile_font(path, suffix, self.tempdir, features[i]) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + instfont = interpolate_layout(ds_path, {'weight': 500}, finder) + + tables = ['GPOS'] + expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_3_diff.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix) + + + def test_varlib_interpolate_layout_GPOS_only_LookupType_4_same_val_ttf(self): + """Only GPOS; LookupType 4; same values in all masters. + """ + suffix = '.ttf' + ds_path = self.get_test_input('InterpolateLayout.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + fea_str = """ + markClass uni0303 @MARKS_ABOVE; + feature xxxx { + pos base a mark @MARKS_ABOVE; + } xxxx; + """ + features = [fea_str] * 2 + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for i, path in enumerate(ttx_paths): + self.compile_font(path, suffix, self.tempdir, features[i]) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + instfont = interpolate_layout(ds_path, {'weight': 500}, finder) + + tables = ['GPOS'] + expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_4_same.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix) + + + def test_varlib_interpolate_layout_GPOS_only_LookupType_4_diff_val_ttf(self): + """Only GPOS; LookupType 4; different values in each master. + """ + suffix = '.ttf' + ds_path = self.get_test_input('InterpolateLayout.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + fea_str_0 = """ + markClass uni0303 @MARKS_ABOVE; + feature xxxx { + pos base a mark @MARKS_ABOVE; + } xxxx; + """ + fea_str_1 = """ + markClass uni0303 @MARKS_ABOVE; + feature xxxx { + pos base a mark @MARKS_ABOVE; + } xxxx; + """ + features = [fea_str_0, fea_str_1] + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for i, path in enumerate(ttx_paths): + self.compile_font(path, suffix, self.tempdir, features[i]) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + instfont = interpolate_layout(ds_path, {'weight': 500}, finder) + + tables = ['GPOS'] + expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_4_diff.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix) + + + def test_varlib_interpolate_layout_GPOS_only_LookupType_5_same_val_ttf(self): + """Only GPOS; LookupType 5; same values in all masters. + """ + suffix = '.ttf' + ds_path = self.get_test_input('InterpolateLayout.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + fea_str = """ + markClass uni0330 @MARKS_BELOW; + feature xxxx { + pos ligature f_t mark @MARKS_BELOW + ligComponent mark @MARKS_BELOW; + } xxxx; + """ + features = [fea_str] * 2 + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for i, path in enumerate(ttx_paths): + self.compile_font(path, suffix, self.tempdir, features[i]) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + instfont = interpolate_layout(ds_path, {'weight': 500}, finder) + + tables = ['GPOS'] + expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_5_same.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix) + + + def test_varlib_interpolate_layout_GPOS_only_LookupType_5_diff_val_ttf(self): + """Only GPOS; LookupType 5; different values in each master. + """ + suffix = '.ttf' + ds_path = self.get_test_input('InterpolateLayout.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + fea_str_0 = """ + markClass uni0330 @MARKS_BELOW; + feature xxxx { + pos ligature f_t mark @MARKS_BELOW + ligComponent mark @MARKS_BELOW; + } xxxx; + """ + fea_str_1 = """ + markClass uni0330 @MARKS_BELOW; + feature xxxx { + pos ligature f_t mark @MARKS_BELOW + ligComponent mark @MARKS_BELOW; + } xxxx; + """ + features = [fea_str_0, fea_str_1] + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for i, path in enumerate(ttx_paths): + self.compile_font(path, suffix, self.tempdir, features[i]) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + instfont = interpolate_layout(ds_path, {'weight': 500}, finder) + + tables = ['GPOS'] + expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_5_diff.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix) + + + def test_varlib_interpolate_layout_GPOS_only_LookupType_6_same_val_ttf(self): + """Only GPOS; LookupType 6; same values in all masters. + """ + suffix = '.ttf' + ds_path = self.get_test_input('InterpolateLayout.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + fea_str = """ + markClass uni0303 @MARKS_ABOVE; + feature xxxx { + pos mark uni0308 mark @MARKS_ABOVE; + } xxxx; + """ + features = [fea_str] * 2 + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for i, path in enumerate(ttx_paths): + self.compile_font(path, suffix, self.tempdir, features[i]) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + instfont = interpolate_layout(ds_path, {'weight': 500}, finder) + + tables = ['GPOS'] + expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_6_same.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix) + + + def test_varlib_interpolate_layout_GPOS_only_LookupType_6_diff_val_ttf(self): + """Only GPOS; LookupType 6; different values in each master. + """ + suffix = '.ttf' + ds_path = self.get_test_input('InterpolateLayout.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + fea_str_0 = """ + markClass uni0303 @MARKS_ABOVE; + feature xxxx { + pos mark uni0308 mark @MARKS_ABOVE; + } xxxx; + """ + fea_str_1 = """ + markClass uni0303 @MARKS_ABOVE; + feature xxxx { + pos mark uni0308 mark @MARKS_ABOVE; + } xxxx; + """ + features = [fea_str_0, fea_str_1] + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for i, path in enumerate(ttx_paths): + self.compile_font(path, suffix, self.tempdir, features[i]) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + instfont = interpolate_layout(ds_path, {'weight': 500}, finder) + + tables = ['GPOS'] + expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_6_diff.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix) + + + def test_varlib_interpolate_layout_GPOS_only_LookupType_8_same_val_ttf(self): + """Only GPOS; LookupType 8; same values in all masters. + """ + suffix = '.ttf' + ds_path = self.get_test_input('InterpolateLayout.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + fea_str = """ + markClass uni0303 @MARKS_ABOVE; + lookup CNTXT_PAIR_POS { + pos A a -23; + } CNTXT_PAIR_POS; + + lookup CNTXT_MARK_TO_BASE { + pos base a mark @MARKS_ABOVE; + } CNTXT_MARK_TO_BASE; + + feature xxxx { + pos A' lookup CNTXT_PAIR_POS a' @MARKS_ABOVE' lookup CNTXT_MARK_TO_BASE; + } xxxx; + """ + features = [fea_str] * 2 + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for i, path in enumerate(ttx_paths): + self.compile_font(path, suffix, self.tempdir, features[i]) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + instfont = interpolate_layout(ds_path, {'weight': 500}, finder) + + tables = ['GPOS'] + expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_8_same.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix) + + + def test_varlib_interpolate_layout_GPOS_only_LookupType_8_diff_val_ttf(self): + """Only GPOS; LookupType 8; different values in each master. + """ + suffix = '.ttf' + ds_path = self.get_test_input('InterpolateLayout.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + fea_str_0 = """ + markClass uni0303 @MARKS_ABOVE; + lookup CNTXT_PAIR_POS { + pos A a -23; + } CNTXT_PAIR_POS; + + lookup CNTXT_MARK_TO_BASE { + pos base a mark @MARKS_ABOVE; + } CNTXT_MARK_TO_BASE; + + feature xxxx { + pos A' lookup CNTXT_PAIR_POS a' @MARKS_ABOVE' lookup CNTXT_MARK_TO_BASE; + } xxxx; + """ + fea_str_1 = """ + markClass uni0303 @MARKS_ABOVE; + lookup CNTXT_PAIR_POS { + pos A a 57; + } CNTXT_PAIR_POS; + + lookup CNTXT_MARK_TO_BASE { + pos base a mark @MARKS_ABOVE; + } CNTXT_MARK_TO_BASE; + + feature xxxx { + pos A' lookup CNTXT_PAIR_POS a' @MARKS_ABOVE' lookup CNTXT_MARK_TO_BASE; + } xxxx; + """ + features = [fea_str_0, fea_str_1] + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily2-') + for i, path in enumerate(ttx_paths): + self.compile_font(path, suffix, self.tempdir, features[i]) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + instfont = interpolate_layout(ds_path, {'weight': 500}, finder) + + tables = ['GPOS'] + expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_8_diff.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix) + + + def test_varlib_interpolate_layout_main_ttf(self): + """Mostly for testing varLib.interpolate_layout.main() + """ + suffix = '.ttf' + ds_path = self.get_test_input('Build.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + self.temp_dir() + ttf_dir = os.path.join(self.tempdir, 'master_ttf_interpolatable') + os.makedirs(ttf_dir) + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily-') + for path in ttx_paths: + self.compile_font(path, suffix, ttf_dir) + + finder = lambda s: s.replace(ufo_dir, ttf_dir).replace('.ufo', suffix) + varfont, _, _ = build(ds_path, finder) + varfont_name = 'InterpolateLayoutMain' + varfont_path = os.path.join(self.tempdir, varfont_name + suffix) + varfont.save(varfont_path) + + ds_copy = os.path.splitext(varfont_path)[0] + '.designspace' + shutil.copy2(ds_path, ds_copy) + args = [ds_copy, 'weight=500', 'contrast=50'] + interpolate_layout_main(args) + + instfont_path = os.path.splitext(varfont_path)[0] + '-instance' + suffix + instfont = TTFont(instfont_path) + tables = [table_tag for table_tag in instfont.keys() if table_tag != 'head'] + expected_ttx_path = self.get_test_output(varfont_name + '.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + + +if __name__ == "__main__": + sys.exit(unittest.main()) diff --git a/Tests/varLib/models_test.py b/Tests/varLib/models_test.py new file mode 100644 index 0000000..c5c2a9a --- /dev/null +++ b/Tests/varLib/models_test.py @@ -0,0 +1,99 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.varLib.models import ( + normalizeLocation, supportScalar, VariationModel) + + +def test_normalizeLocation(): + axes = {"wght": (100, 400, 900)} + assert normalizeLocation({"wght": 400}, axes) == {'wght': 0.0} + assert normalizeLocation({"wght": 100}, axes) == {'wght': -1.0} + assert normalizeLocation({"wght": 900}, axes) == {'wght': 1.0} + assert normalizeLocation({"wght": 650}, axes) == {'wght': 0.5} + assert normalizeLocation({"wght": 1000}, axes) == {'wght': 1.0} + assert normalizeLocation({"wght": 0}, axes) == {'wght': -1.0} + + axes = {"wght": (0, 0, 1000)} + assert normalizeLocation({"wght": 0}, axes) == {'wght': 0.0} + assert normalizeLocation({"wght": -1}, axes) == {'wght': 0.0} + assert normalizeLocation({"wght": 1000}, axes) == {'wght': 1.0} + assert normalizeLocation({"wght": 500}, axes) == {'wght': 0.5} + assert normalizeLocation({"wght": 1001}, axes) == {'wght': 1.0} + + axes = {"wght": (0, 1000, 1000)} + assert normalizeLocation({"wght": 0}, axes) == {'wght': -1.0} + assert normalizeLocation({"wght": -1}, axes) == {'wght': -1.0} + assert normalizeLocation({"wght": 500}, axes) == {'wght': -0.5} + assert normalizeLocation({"wght": 1000}, axes) == {'wght': 0.0} + assert normalizeLocation({"wght": 1001}, axes) == {'wght': 0.0} + + +def test_supportScalar(): + assert supportScalar({}, {}) == 1.0 + assert supportScalar({'wght':.2}, {}) == 1.0 + assert supportScalar({'wght':.2}, {'wght':(0,2,3)}) == 0.1 + assert supportScalar({'wght':2.5}, {'wght':(0,2,4)}) == 0.75 + + +def test_VariationModel(): + locations = [ + {'wght':100}, + {'wght':-100}, + {'wght':-180}, + {'wdth':+.3}, + {'wght':+120,'wdth':.3}, + {'wght':+120,'wdth':.2}, + {}, + {'wght':+180,'wdth':.3}, + {'wght':+180}, + ] + model = VariationModel(locations, axisOrder=['wght']) + + assert model.locations == [ + {}, + {'wght': -100}, + {'wght': -180}, + {'wght': 100}, + {'wght': 180}, + {'wdth': 0.3}, + {'wdth': 0.3, 'wght': 180}, + {'wdth': 0.3, 'wght': 120}, + {'wdth': 0.2, 'wght': 120}] + + assert model.deltaWeights == [ + {}, + {0: 1.0}, + {0: 1.0}, + {0: 1.0}, + {0: 1.0}, + {0: 1.0}, + {0: 1.0, 4: 1.0, 5: 1.0}, + {0: 1.0, 3: 0.75, 4: 0.25, 5: 1.0, 6: 0.6666666666666666}, + {0: 1.0, + 3: 0.75, + 4: 0.25, + 5: 0.6666666666666667, + 6: 0.4444444444444445, + 7: 0.6666666666666667}] + +def test_VariationModel(): + locations = [ + {}, + {'bar': 0.5}, + {'bar': 1.0}, + {'foo': 1.0}, + {'bar': 0.5, 'foo': 1.0}, + {'bar': 1.0, 'foo': 1.0}, + ] + model = VariationModel(locations) + + assert model.locations == locations + + assert model.supports == [ + {}, + {'bar': (0, 0.5, 1.0)}, + {'bar': (0.5, 1.0, 1.0)}, + {'foo': (0, 1.0, 1.0)}, + {'bar': (0, 0.5, 1.0), 'foo': (0, 1.0, 1.0)}, + {'bar': (0.5, 1.0, 1.0), 'foo': (0, 1.0, 1.0)}, + ] diff --git a/Tests/varLib/mutator_test.py b/Tests/varLib/mutator_test.py new file mode 100644 index 0000000..de794f0 --- /dev/null +++ b/Tests/varLib/mutator_test.py @@ -0,0 +1,144 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.ttLib import TTFont +from fontTools.varLib import build +from fontTools.varLib.mutator import main as mutator +import difflib +import os +import shutil +import sys +import tempfile +import unittest + + +class MutatorTest(unittest.TestCase): + def __init__(self, methodName): + unittest.TestCase.__init__(self, methodName) + # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, + # and fires deprecation warnings if a program uses the old name. + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + + def setUp(self): + self.tempdir = None + self.num_tempfiles = 0 + + def tearDown(self): + if self.tempdir: + shutil.rmtree(self.tempdir) + + @staticmethod + def get_test_input(test_file_or_folder): + path, _ = os.path.split(__file__) + return os.path.join(path, "data", test_file_or_folder) + + @staticmethod + def get_test_output(test_file_or_folder): + path, _ = os.path.split(__file__) + return os.path.join(path, "data", "test_results", test_file_or_folder) + + @staticmethod + def get_file_list(folder, suffix, prefix=''): + all_files = os.listdir(folder) + file_list = [] + for p in all_files: + if p.startswith(prefix) and p.endswith(suffix): + file_list.append(os.path.abspath(os.path.join(folder, p))) + return file_list + + def temp_path(self, suffix): + self.temp_dir() + self.num_tempfiles += 1 + return os.path.join(self.tempdir, + "tmp%d%s" % (self.num_tempfiles, suffix)) + + def temp_dir(self): + if not self.tempdir: + self.tempdir = tempfile.mkdtemp() + + def read_ttx(self, path): + lines = [] + with open(path, "r", encoding="utf-8") as ttx: + for line in ttx.readlines(): + # Elide ttFont attributes because ttLibVersion may change, + # and use os-native line separators so we can run difflib. + if line.startswith("" + os.linesep) + else: + lines.append(line.rstrip() + os.linesep) + return lines + + def expect_ttx(self, font, expected_ttx, tables): + path = self.temp_path(suffix=".ttx") + font.saveXML(path, tables=tables) + actual = self.read_ttx(path) + expected = self.read_ttx(expected_ttx) + if actual != expected: + for line in difflib.unified_diff( + expected, actual, fromfile=expected_ttx, tofile=path): + sys.stdout.write(line) + self.fail("TTX output is different from expected") + + def compile_font(self, path, suffix, temp_dir): + ttx_filename = os.path.basename(path) + savepath = os.path.join(temp_dir, ttx_filename.replace('.ttx', suffix)) + font = TTFont(recalcBBoxes=False, recalcTimestamp=False) + font.importXML(path) + font.save(savepath, reorderTables=None) + return font, savepath + +# ----- +# Tests +# ----- + + def test_varlib_mutator_ttf(self): + suffix = '.ttf' + ds_path = self.get_test_input('Build.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily-') + for path in ttx_paths: + self.compile_font(path, suffix, self.tempdir) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + varfont, _, _ = build(ds_path, finder) + varfont_name = 'Mutator' + varfont_path = os.path.join(self.tempdir, varfont_name + suffix) + varfont.save(varfont_path) + + args = [varfont_path, 'wght=500', 'cntr=50'] + mutator(args) + + instfont_path = os.path.splitext(varfont_path)[0] + '-instance' + suffix + instfont = TTFont(instfont_path) + tables = [table_tag for table_tag in instfont.keys() if table_tag != 'head'] + expected_ttx_path = self.get_test_output(varfont_name + '.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + + def test_varlib_mutator_iup_ttf(self): + suffix = '.ttf' + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_varfont_ttf') + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'Mutator_IUP') + for path in ttx_paths: + self.compile_font(path, suffix, self.tempdir) + + varfont_name = 'Mutator_IUP' + varfont_path = os.path.join(self.tempdir, varfont_name + suffix) + + args = [varfont_path, 'wdth=80', 'ASCN=628'] + mutator(args) + + instfont_path = os.path.splitext(varfont_path)[0] + '-instance' + suffix + instfont = TTFont(instfont_path) + tables = [table_tag for table_tag in instfont.keys() if table_tag != 'head'] + expected_ttx_path = self.get_test_output(varfont_name + '-instance.ttx') + self.expect_ttx(instfont, expected_ttx_path, tables) + + +if __name__ == "__main__": + sys.exit(unittest.main()) diff --git a/Tests/varLib/varLib_test.py b/Tests/varLib/varLib_test.py new file mode 100644 index 0000000..12c4874 --- /dev/null +++ b/Tests/varLib/varLib_test.py @@ -0,0 +1,236 @@ +from __future__ import print_function, division, absolute_import +from fontTools.misc.py23 import * +from fontTools.ttLib import TTFont +from fontTools.varLib import build +from fontTools.varLib import main as varLib_main +import difflib +import os +import shutil +import sys +import tempfile +import unittest + + +class BuildTest(unittest.TestCase): + def __init__(self, methodName): + unittest.TestCase.__init__(self, methodName) + # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, + # and fires deprecation warnings if a program uses the old name. + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + + def setUp(self): + self.tempdir = None + self.num_tempfiles = 0 + + def tearDown(self): + if self.tempdir: + shutil.rmtree(self.tempdir) + + @staticmethod + def get_test_input(test_file_or_folder): + path, _ = os.path.split(__file__) + return os.path.join(path, "data", test_file_or_folder) + + @staticmethod + def get_test_output(test_file_or_folder): + path, _ = os.path.split(__file__) + return os.path.join(path, "data", "test_results", test_file_or_folder) + + @staticmethod + def get_file_list(folder, suffix, prefix=''): + all_files = os.listdir(folder) + file_list = [] + for p in all_files: + if p.startswith(prefix) and p.endswith(suffix): + file_list.append(os.path.abspath(os.path.join(folder, p))) + return file_list + + def temp_path(self, suffix): + self.temp_dir() + self.num_tempfiles += 1 + return os.path.join(self.tempdir, + "tmp%d%s" % (self.num_tempfiles, suffix)) + + def temp_dir(self): + if not self.tempdir: + self.tempdir = tempfile.mkdtemp() + + def read_ttx(self, path): + lines = [] + with open(path, "r", encoding="utf-8") as ttx: + for line in ttx.readlines(): + # Elide ttFont attributes because ttLibVersion may change, + # and use os-native line separators so we can run difflib. + if line.startswith("" + os.linesep) + else: + lines.append(line.rstrip() + os.linesep) + return lines + + def expect_ttx(self, font, expected_ttx, tables): + path = self.temp_path(suffix=".ttx") + font.saveXML(path, tables=tables) + actual = self.read_ttx(path) + expected = self.read_ttx(expected_ttx) + if actual != expected: + for line in difflib.unified_diff( + expected, actual, fromfile=expected_ttx, tofile=path): + sys.stdout.write(line) + self.fail("TTX output is different from expected") + + def check_ttx_dump(self, font, expected_ttx, tables, suffix): + """Ensure the TTX dump is the same after saving and reloading the font.""" + path = self.temp_path(suffix=suffix) + font.save(path) + self.expect_ttx(TTFont(path), expected_ttx, tables) + + def compile_font(self, path, suffix, temp_dir): + ttx_filename = os.path.basename(path) + savepath = os.path.join(temp_dir, ttx_filename.replace('.ttx', suffix)) + font = TTFont(recalcBBoxes=False, recalcTimestamp=False) + font.importXML(path) + font.save(savepath, reorderTables=None) + return font, savepath + + def _run_varlib_build_test(self, designspace_name, font_name, tables, + expected_ttx_name): + suffix = '.ttf' + ds_path = self.get_test_input(designspace_name + '.designspace') + ufo_dir = self.get_test_input('master_ufo') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + self.temp_dir() + ttx_paths = self.get_file_list(ttx_dir, '.ttx', font_name + '-') + for path in ttx_paths: + self.compile_font(path, suffix, self.tempdir) + + finder = lambda s: s.replace(ufo_dir, self.tempdir).replace('.ufo', suffix) + varfont, model, _ = build(ds_path, finder) + + expected_ttx_path = self.get_test_output(expected_ttx_name + '.ttx') + self.expect_ttx(varfont, expected_ttx_path, tables) + self.check_ttx_dump(varfont, expected_ttx_path, tables, suffix) +# ----- +# Tests +# ----- + + def test_varlib_build_ttf(self): + """Designspace file contains element.""" + self._run_varlib_build_test( + designspace_name='Build', + font_name='TestFamily', + tables=['GDEF', 'HVAR', 'MVAR', 'fvar', 'gvar'], + expected_ttx_name='Build' + ) + + def test_varlib_build_no_axes_ttf(self): + """Designspace file does not contain an element.""" + self._run_varlib_build_test( + designspace_name='InterpolateLayout3', + font_name='TestFamily2', + tables=['GDEF', 'HVAR', 'MVAR', 'fvar', 'gvar'], + expected_ttx_name='Build3' + ) + + def test_varlib_avar_single_axis(self): + """Designspace file contains a 'weight' axis with elements + modifying the normalization mapping. An 'avar' table is generated. + """ + test_name = 'BuildAvarSingleAxis' + self._run_varlib_build_test( + designspace_name=test_name, + font_name='TestFamily3', + tables=['avar'], + expected_ttx_name=test_name + ) + + def test_varlib_avar_with_identity_maps(self): + """Designspace file contains two 'weight' and 'width' axes both with + elements. + + The 'width' axis only contains identity mappings, however the resulting + avar segment will not be empty but will contain the default axis value + maps: {-1.0: -1.0, 0.0: 0.0, 1.0: 1.0}. + + This is to to work around an issue with some rasterizers: + https://github.com/googlei18n/fontmake/issues/295 + https://github.com/fonttools/fonttools/issues/1011 + """ + test_name = 'BuildAvarIdentityMaps' + self._run_varlib_build_test( + designspace_name=test_name, + font_name='TestFamily3', + tables=['avar'], + expected_ttx_name=test_name + ) + + def test_varlib_avar_empty_axis(self): + """Designspace file contains two 'weight' and 'width' axes, but + only one axis ('weight') has some elements. + + Even if no elements are defined for the 'width' axis, the + resulting avar segment still contains the default axis value maps: + {-1.0: -1.0, 0.0: 0.0, 1.0: 1.0}. + + This is again to to work around an issue with some rasterizers: + https://github.com/googlei18n/fontmake/issues/295 + https://github.com/fonttools/fonttools/issues/1011 + """ + test_name = 'BuildAvarEmptyAxis' + self._run_varlib_build_test( + designspace_name=test_name, + font_name='TestFamily3', + tables=['avar'], + expected_ttx_name=test_name + ) + + def test_varlib_main_ttf(self): + """Mostly for testing varLib.main() + """ + suffix = '.ttf' + ds_path = self.get_test_input('Build.designspace') + ttx_dir = self.get_test_input('master_ttx_interpolatable_ttf') + + self.temp_dir() + ttf_dir = os.path.join(self.tempdir, 'master_ttf_interpolatable') + os.makedirs(ttf_dir) + ttx_paths = self.get_file_list(ttx_dir, '.ttx', 'TestFamily-') + for path in ttx_paths: + self.compile_font(path, suffix, ttf_dir) + + ds_copy = os.path.join(self.tempdir, 'BuildMain.designspace') + shutil.copy2(ds_path, ds_copy) + + # by default, varLib.main finds master TTFs inside a + # 'master_ttf_interpolatable' subfolder in current working dir + cwd = os.getcwd() + os.chdir(self.tempdir) + try: + varLib_main([ds_copy]) + finally: + os.chdir(cwd) + + varfont_path = os.path.splitext(ds_copy)[0] + '-VF' + suffix + self.assertTrue(os.path.exists(varfont_path)) + + # try again passing an explicit --master-finder + os.remove(varfont_path) + finder = "%s/master_ttf_interpolatable/{stem}.ttf" % self.tempdir + varLib_main([ds_copy, "--master-finder", finder]) + self.assertTrue(os.path.exists(varfont_path)) + + # and also with explicit -o output option + os.remove(varfont_path) + varfont_path = os.path.splitext(varfont_path)[0] + "-o" + suffix + varLib_main([ds_copy, "-o", varfont_path, "--master-finder", finder]) + self.assertTrue(os.path.exists(varfont_path)) + + varfont = TTFont(varfont_path) + tables = [table_tag for table_tag in varfont.keys() if table_tag != 'head'] + expected_ttx_path = self.get_test_output('BuildMain.ttx') + self.expect_ttx(varfont, expected_ttx_path, tables) + + +if __name__ == "__main__": + sys.exit(unittest.main()) diff --git a/Tests/voltLib/lexer_test.py b/Tests/voltLib/lexer_test.py new file mode 100644 index 0000000..6041cb8 --- /dev/null +++ b/Tests/voltLib/lexer_test.py @@ -0,0 +1,35 @@ +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.voltLib.error import VoltLibError +from fontTools.voltLib.lexer import Lexer +import unittest + + +def lex(s): + return [(typ, tok) for (typ, tok, _) in Lexer(s, "test.vtp")] + + +class LexerTest(unittest.TestCase): + def __init__(self, methodName): + unittest.TestCase.__init__(self, methodName) + + def test_empty(self): + self.assertEqual(lex(""), []) + self.assertEqual(lex("\t"), []) + + def test_string(self): + self.assertEqual(lex('"foo" "bar"'), + [(Lexer.STRING, "foo"), (Lexer.STRING, "bar")]) + self.assertRaises(VoltLibError, lambda: lex('"foo\n bar"')) + + def test_name(self): + self.assertEqual(lex('DEF_FOO bar.alt1'), + [(Lexer.NAME, "DEF_FOO"), (Lexer.NAME, "bar.alt1")]) + + def test_number(self): + self.assertEqual(lex("123 -456"), + [(Lexer.NUMBER, 123), (Lexer.NUMBER, -456)]) + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tests/voltLib/parser_test.py b/Tests/voltLib/parser_test.py new file mode 100644 index 0000000..6baf900 --- /dev/null +++ b/Tests/voltLib/parser_test.py @@ -0,0 +1,1034 @@ +from __future__ import print_function, division, absolute_import +from __future__ import unicode_literals +from fontTools.voltLib.error import VoltLibError +from fontTools.voltLib.parser import Parser +from io import open +import os +import shutil +import tempfile +import unittest + + +class ParserTest(unittest.TestCase): + def __init__(self, methodName): + unittest.TestCase.__init__(self, methodName) + # Python 3 renamed assertRaisesRegexp to assertRaisesRegex, + # and fires deprecation warnings if a program uses the old name. + if not hasattr(self, "assertRaisesRegex"): + self.assertRaisesRegex = self.assertRaisesRegexp + + def test_def_glyph_base(self): + [def_glyph] = self.parse( + 'DEF_GLYPH ".notdef" ID 0 TYPE BASE END_GLYPH' + ).statements + self.assertEqual((def_glyph.name, def_glyph.id, def_glyph.unicode, + def_glyph.type, def_glyph.components), + (".notdef", 0, None, "BASE", None)) + + def test_def_glyph_base_with_unicode(self): + [def_glyph] = self.parse( + 'DEF_GLYPH "space" ID 3 UNICODE 32 TYPE BASE END_GLYPH' + ).statements + self.assertEqual((def_glyph.name, def_glyph.id, def_glyph.unicode, + def_glyph.type, def_glyph.components), + ("space", 3, [0x0020], "BASE", None)) + + def test_def_glyph_base_with_unicodevalues(self): + [def_glyph] = self.parse( + 'DEF_GLYPH "CR" ID 2 UNICODEVALUES "U+0009" ' + 'TYPE BASE END_GLYPH' + ).statements + self.assertEqual((def_glyph.name, def_glyph.id, def_glyph.unicode, + def_glyph.type, def_glyph.components), + ("CR", 2, [0x0009], "BASE", None)) + + def test_def_glyph_base_with_mult_unicodevalues(self): + [def_glyph] = self.parse( + 'DEF_GLYPH "CR" ID 2 UNICODEVALUES "U+0009,U+000D" ' + 'TYPE BASE END_GLYPH' + ).statements + self.assertEqual((def_glyph.name, def_glyph.id, def_glyph.unicode, + def_glyph.type, def_glyph.components), + ("CR", 2, [0x0009, 0x000D], "BASE", None)) + + def test_def_glyph_base_with_empty_unicodevalues(self): + [def_glyph] = self.parse( + 'DEF_GLYPH "i.locl" ID 269 UNICODEVALUES "" ' + 'TYPE BASE END_GLYPH' + ).statements + self.assertEqual((def_glyph.name, def_glyph.id, def_glyph.unicode, + def_glyph.type, def_glyph.components), + ("i.locl", 269, None, "BASE", None)) + + def test_def_glyph_base_2_components(self): + [def_glyph] = self.parse( + 'DEF_GLYPH "glyphBase" ID 320 TYPE BASE COMPONENTS 2 END_GLYPH' + ).statements + self.assertEqual((def_glyph.name, def_glyph.id, def_glyph.unicode, + def_glyph.type, def_glyph.components), + ("glyphBase", 320, None, "BASE", 2)) + + def test_def_glyph_ligature_2_components(self): + [def_glyph] = self.parse( + 'DEF_GLYPH "f_f" ID 320 TYPE LIGATURE COMPONENTS 2 END_GLYPH' + ).statements + self.assertEqual((def_glyph.name, def_glyph.id, def_glyph.unicode, + def_glyph.type, def_glyph.components), + ("f_f", 320, None, "LIGATURE", 2)) + + def test_def_glyph_no_type(self): + [def_glyph] = self.parse( + 'DEF_GLYPH "glyph20" ID 20 END_GLYPH' + ).statements + self.assertEqual((def_glyph.name, def_glyph.id, def_glyph.unicode, + def_glyph.type, def_glyph.components), + ("glyph20", 20, None, None, None)) + + def test_def_glyph_case_sensitive(self): + def_glyphs = self.parse( + 'DEF_GLYPH "A" ID 3 UNICODE 65 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "a" ID 4 UNICODE 97 TYPE BASE END_GLYPH\n' + ).statements + self.assertEqual((def_glyphs[0].name, def_glyphs[0].id, + def_glyphs[0].unicode, def_glyphs[0].type, + def_glyphs[0].components), + ("A", 3, [0x41], "BASE", None)) + self.assertEqual((def_glyphs[1].name, def_glyphs[1].id, + def_glyphs[1].unicode, def_glyphs[1].type, + def_glyphs[1].components), + ("a", 4, [0x61], "BASE", None)) + + def test_def_group_glyphs(self): + [def_group] = self.parse( + 'DEF_GROUP "aaccented"\n' + 'ENUM GLYPH "aacute" GLYPH "abreve" GLYPH "acircumflex" ' + 'GLYPH "adieresis" GLYPH "ae" GLYPH "agrave" GLYPH "amacron" ' + 'GLYPH "aogonek" GLYPH "aring" GLYPH "atilde" END_ENUM\n' + 'END_GROUP\n' + ).statements + self.assertEqual((def_group.name, def_group.enum), + ("aaccented", + ("aacute", "abreve", "acircumflex", "adieresis", + "ae", "agrave", "amacron", "aogonek", "aring", + "atilde"))) + + def test_def_group_groups(self): + [group1, group2, test_group] = self.parse( + 'DEF_GROUP "Group1"\n' + 'ENUM GLYPH "a" GLYPH "b" GLYPH "c" GLYPH "d" END_ENUM\n' + 'END_GROUP\n' + 'DEF_GROUP "Group2"\n' + 'ENUM GLYPH "e" GLYPH "f" GLYPH "g" GLYPH "h" END_ENUM\n' + 'END_GROUP\n' + 'DEF_GROUP "TestGroup"\n' + 'ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n' + 'END_GROUP\n' + ).statements + self.assertEqual( + (test_group.name, test_group.enum), + ("TestGroup", + (("Group1",), ("Group2",)))) + + def test_def_group_groups_not_yet_defined(self): + [group1, test_group1, test_group2, test_group3, group2] = self.parse( + 'DEF_GROUP "Group1"\n' + 'ENUM GLYPH "a" GLYPH "b" GLYPH "c" GLYPH "d" END_ENUM\n' + 'END_GROUP\n' + 'DEF_GROUP "TestGroup1"\n' + 'ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n' + 'END_GROUP\n' + 'DEF_GROUP "TestGroup2"\n' + 'ENUM GROUP "Group2" END_ENUM\n' + 'END_GROUP\n' + 'DEF_GROUP "TestGroup3"\n' + 'ENUM GROUP "Group2" GROUP "Group1" END_ENUM\n' + 'END_GROUP\n' + 'DEF_GROUP "Group2"\n' + 'ENUM GLYPH "e" GLYPH "f" GLYPH "g" GLYPH "h" END_ENUM\n' + 'END_GROUP\n' + ).statements + self.assertEqual( + (test_group1.name, test_group1.enum), + ("TestGroup1", + (("Group1", ), ("Group2", )))) + self.assertEqual( + (test_group2.name, test_group2.enum), + ("TestGroup2", + (("Group2", ), ))) + self.assertEqual( + (test_group3.name, test_group3.enum), + ("TestGroup3", + (("Group2", ), ("Group1", )))) + + # def test_def_group_groups_undefined(self): + # with self.assertRaisesRegex( + # VoltLibError, + # r'Group "Group2" is used but undefined.'): + # [group1, test_group, group2] = self.parse( + # 'DEF_GROUP "Group1"\n' + # 'ENUM GLYPH "a" GLYPH "b" GLYPH "c" GLYPH "d" END_ENUM\n' + # 'END_GROUP\n' + # 'DEF_GROUP "TestGroup"\n' + # 'ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n' + # 'END_GROUP\n' + # ).statements + + def test_def_group_glyphs_and_group(self): + [def_group1, def_group2] = self.parse( + 'DEF_GROUP "aaccented"\n' + 'ENUM GLYPH "aacute" GLYPH "abreve" GLYPH "acircumflex" ' + 'GLYPH "adieresis" GLYPH "ae" GLYPH "agrave" GLYPH "amacron" ' + 'GLYPH "aogonek" GLYPH "aring" GLYPH "atilde" END_ENUM\n' + 'END_GROUP\n' + 'DEF_GROUP "KERN_lc_a_2ND"\n' + 'ENUM GLYPH "a" GROUP "aaccented" END_ENUM\n' + 'END_GROUP' + ).statements + self.assertEqual((def_group2.name, def_group2.enum), + ("KERN_lc_a_2ND", + ("a", ("aaccented", )))) + + def test_def_group_range(self): + [def_group] = self.parse( + 'DEF_GROUP "KERN_lc_a_2ND"\n' + 'ENUM RANGE "a" TO "atilde" GLYPH "b" RANGE "c" TO "cdotaccent" ' + 'END_ENUM\n' + 'END_GROUP' + ).statements + self.assertEqual((def_group.name, def_group.enum), + ("KERN_lc_a_2ND", + (("a", "atilde"), "b", ("c", "cdotaccent")))) + + def test_group_duplicate(self): + self.assertRaisesRegex( + VoltLibError, + 'Glyph group "dupe" already defined, ' + 'group names are case insensitive', + self.parse, 'DEF_GROUP "dupe"\n' + 'ENUM GLYPH "a" GLYPH "b" END_ENUM\n' + 'END_GROUP\n' + 'DEF_GROUP "dupe"\n' + 'ENUM GLYPH "x" END_ENUM\n' + 'END_GROUP\n' + ) + + def test_group_duplicate_case_insensitive(self): + self.assertRaisesRegex( + VoltLibError, + 'Glyph group "Dupe" already defined, ' + 'group names are case insensitive', + self.parse, 'DEF_GROUP "dupe"\n' + 'ENUM GLYPH "a" GLYPH "b" END_ENUM\n' + 'END_GROUP\n' + 'DEF_GROUP "Dupe"\n' + 'ENUM GLYPH "x" END_ENUM\n' + 'END_GROUP\n' + ) + + def test_script_without_langsys(self): + [script] = self.parse( + 'DEF_SCRIPT NAME "Latin" TAG "latn"\n' + 'END_SCRIPT' + ).statements + self.assertEqual((script.name, script.tag, script.langs), + ("Latin", "latn", [])) + + def test_langsys_normal(self): + [def_script] = self.parse( + 'DEF_SCRIPT NAME "Latin" TAG "latn"\n' + 'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n' + 'END_LANGSYS\n' + 'DEF_LANGSYS NAME "Moldavian" TAG "MOL "\n' + 'END_LANGSYS\n' + 'END_SCRIPT' + ).statements + self.assertEqual((def_script.name, def_script.tag), + ("Latin", + "latn")) + def_lang = def_script.langs[0] + self.assertEqual((def_lang.name, def_lang.tag), + ("Romanian", + "ROM ")) + def_lang = def_script.langs[1] + self.assertEqual((def_lang.name, def_lang.tag), + ("Moldavian", + "MOL ")) + + def test_langsys_no_script_name(self): + [langsys] = self.parse( + 'DEF_SCRIPT TAG "latn"\n' + 'DEF_LANGSYS NAME "Default" TAG "dflt"\n' + 'END_LANGSYS\n' + 'END_SCRIPT' + ).statements + self.assertEqual((langsys.name, langsys.tag), + (None, + "latn")) + lang = langsys.langs[0] + self.assertEqual((lang.name, lang.tag), + ("Default", + "dflt")) + + def test_langsys_no_script_tag_fails(self): + with self.assertRaisesRegex( + VoltLibError, + r'.*Expected "TAG"'): + [langsys] = self.parse( + 'DEF_SCRIPT NAME "Latin"\n' + 'DEF_LANGSYS NAME "Default" TAG "dflt"\n' + 'END_LANGSYS\n' + 'END_SCRIPT' + ).statements + + def test_langsys_duplicate_script(self): + with self.assertRaisesRegex( + VoltLibError, + 'Script "DFLT" already defined, ' + 'script tags are case insensitive'): + [langsys1, langsys2] = self.parse( + 'DEF_SCRIPT NAME "Default" TAG "DFLT"\n' + 'DEF_LANGSYS NAME "Default" TAG "dflt"\n' + 'END_LANGSYS\n' + 'END_SCRIPT\n' + 'DEF_SCRIPT TAG "DFLT"\n' + 'DEF_LANGSYS NAME "Default" TAG "dflt"\n' + 'END_LANGSYS\n' + 'END_SCRIPT' + ).statements + + def test_langsys_duplicate_lang(self): + with self.assertRaisesRegex( + VoltLibError, + 'Language "dflt" already defined in script "DFLT", ' + 'language tags are case insensitive'): + [langsys] = self.parse( + 'DEF_SCRIPT NAME "Default" TAG "DFLT"\n' + 'DEF_LANGSYS NAME "Default" TAG "dflt"\n' + 'END_LANGSYS\n' + 'DEF_LANGSYS NAME "Default" TAG "dflt"\n' + 'END_LANGSYS\n' + 'END_SCRIPT\n' + ).statements + + def test_langsys_lang_in_separate_scripts(self): + [langsys1, langsys2] = self.parse( + 'DEF_SCRIPT NAME "Default" TAG "DFLT"\n' + 'DEF_LANGSYS NAME "Default" TAG "dflt"\n' + 'END_LANGSYS\n' + 'DEF_LANGSYS NAME "Default" TAG "ROM "\n' + 'END_LANGSYS\n' + 'END_SCRIPT\n' + 'DEF_SCRIPT NAME "Latin" TAG "latn"\n' + 'DEF_LANGSYS NAME "Default" TAG "dflt"\n' + 'END_LANGSYS\n' + 'DEF_LANGSYS NAME "Default" TAG "ROM "\n' + 'END_LANGSYS\n' + 'END_SCRIPT' + ).statements + self.assertEqual((langsys1.langs[0].tag, langsys1.langs[1].tag), + ("dflt", "ROM ")) + self.assertEqual((langsys2.langs[0].tag, langsys2.langs[1].tag), + ("dflt", "ROM ")) + + def test_langsys_no_lang_name(self): + [langsys] = self.parse( + 'DEF_SCRIPT NAME "Latin" TAG "latn"\n' + 'DEF_LANGSYS TAG "dflt"\n' + 'END_LANGSYS\n' + 'END_SCRIPT' + ).statements + self.assertEqual((langsys.name, langsys.tag), + ("Latin", + "latn")) + lang = langsys.langs[0] + self.assertEqual((lang.name, lang.tag), + (None, + "dflt")) + + def test_langsys_no_langsys_tag_fails(self): + with self.assertRaisesRegex( + VoltLibError, + r'.*Expected "TAG"'): + [langsys] = self.parse( + 'DEF_SCRIPT NAME "Latin" TAG "latn"\n' + 'DEF_LANGSYS NAME "Default"\n' + 'END_LANGSYS\n' + 'END_SCRIPT' + ).statements + + def test_feature(self): + [def_script] = self.parse( + 'DEF_SCRIPT NAME "Latin" TAG "latn"\n' + 'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n' + 'DEF_FEATURE NAME "Fractions" TAG "frac"\n' + 'LOOKUP "fraclookup"\n' + 'END_FEATURE\n' + 'END_LANGSYS\n' + 'END_SCRIPT' + ).statements + def_feature = def_script.langs[0].features[0] + self.assertEqual((def_feature.name, def_feature.tag, + def_feature.lookups), + ("Fractions", + "frac", + ["fraclookup"])) + [def_script] = self.parse( + 'DEF_SCRIPT NAME "Latin" TAG "latn"\n' + 'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n' + 'DEF_FEATURE NAME "Kerning" TAG "kern"\n' + 'LOOKUP "kern1" LOOKUP "kern2"\n' + 'END_FEATURE\n' + 'END_LANGSYS\n' + 'END_SCRIPT' + ).statements + def_feature = def_script.langs[0].features[0] + self.assertEqual((def_feature.name, def_feature.tag, + def_feature.lookups), + ("Kerning", + "kern", + ["kern1", "kern2"])) + + def test_lookup_duplicate(self): + with self.assertRaisesRegex( + VoltLibError, + 'Lookup "dupe" already defined, ' + 'lookup names are case insensitive', + ): + [lookup1, lookup2] = self.parse( + 'DEF_LOOKUP "dupe"\n' + 'AS_SUBSTITUTION\n' + 'SUB GLYPH "a"\n' + 'WITH GLYPH "a.alt"\n' + 'END_SUB\n' + 'END_SUBSTITUTION\n' + 'DEF_LOOKUP "dupe"\n' + 'AS_SUBSTITUTION\n' + 'SUB GLYPH "b"\n' + 'WITH GLYPH "b.alt"\n' + 'END_SUB\n' + 'END_SUBSTITUTION\n' + ).statements + + def test_lookup_duplicate_insensitive_case(self): + with self.assertRaisesRegex( + VoltLibError, + 'Lookup "Dupe" already defined, ' + 'lookup names are case insensitive', + ): + [lookup1, lookup2] = self.parse( + 'DEF_LOOKUP "dupe"\n' + 'AS_SUBSTITUTION\n' + 'SUB GLYPH "a"\n' + 'WITH GLYPH "a.alt"\n' + 'END_SUB\n' + 'END_SUBSTITUTION\n' + 'DEF_LOOKUP "Dupe"\n' + 'AS_SUBSTITUTION\n' + 'SUB GLYPH "b"\n' + 'WITH GLYPH "b.alt"\n' + 'END_SUB\n' + 'END_SUBSTITUTION\n' + ).statements + + def test_lookup_name_starts_with_letter(self): + with self.assertRaisesRegex( + VoltLibError, + 'Lookup name "\\\lookupname" must start with a letter' + ): + [lookup] = self.parse( + 'DEF_LOOKUP "\lookupname"\n' + 'AS_SUBSTITUTION\n' + 'SUB GLYPH "a"\n' + 'WITH GLYPH "a.alt"\n' + 'END_SUB\n' + 'END_SUBSTITUTION\n' + ).statements + + def test_substitution_empty(self): + with self.assertRaisesRegex( + VoltLibError, + r'Expected SUB'): + [lookup] = self.parse( + 'DEF_LOOKUP "empty_substitution" PROCESS_BASE PROCESS_MARKS ' + 'ALL DIRECTION LTR\n' + 'IN_CONTEXT\n' + 'END_CONTEXT\n' + 'AS_SUBSTITUTION\n' + 'END_SUBSTITUTION' + ).statements + + def test_substitution_invalid_many_to_many(self): + with self.assertRaisesRegex( + VoltLibError, + r'Invalid substitution type'): + [lookup] = self.parse( + 'DEF_LOOKUP "invalid_substitution" PROCESS_BASE PROCESS_MARKS ' + 'ALL DIRECTION LTR\n' + 'IN_CONTEXT\n' + 'END_CONTEXT\n' + 'AS_SUBSTITUTION\n' + 'SUB GLYPH "f" GLYPH "i"\n' + 'WITH GLYPH "f.alt" GLYPH "i.alt"\n' + 'END_SUB\n' + 'END_SUBSTITUTION' + ).statements + + def test_substitution_invalid_reverse_chaining_single(self): + with self.assertRaisesRegex( + VoltLibError, + r'Invalid substitution type'): + [lookup] = self.parse( + 'DEF_LOOKUP "invalid_substitution" PROCESS_BASE PROCESS_MARKS ' + 'ALL DIRECTION LTR REVERSAL\n' + 'IN_CONTEXT\n' + 'END_CONTEXT\n' + 'AS_SUBSTITUTION\n' + 'SUB GLYPH "f" GLYPH "i"\n' + 'WITH GLYPH "f_i"\n' + 'END_SUB\n' + 'END_SUBSTITUTION' + ).statements + + def test_substitution_invalid_mixed(self): + with self.assertRaisesRegex( + VoltLibError, + r'Invalid substitution type'): + [lookup] = self.parse( + 'DEF_LOOKUP "invalid_substitution" PROCESS_BASE PROCESS_MARKS ' + 'ALL DIRECTION LTR\n' + 'IN_CONTEXT\n' + 'END_CONTEXT\n' + 'AS_SUBSTITUTION\n' + 'SUB GLYPH "fi"\n' + 'WITH GLYPH "f" GLYPH "i"\n' + 'END_SUB\n' + 'SUB GLYPH "f" GLYPH "l"\n' + 'WITH GLYPH "f_l"\n' + 'END_SUB\n' + 'END_SUBSTITUTION' + ).statements + + def test_substitution_single(self): + [lookup] = self.parse( + 'DEF_LOOKUP "smcp" PROCESS_BASE PROCESS_MARKS ALL ' + 'DIRECTION LTR\n' + 'IN_CONTEXT\n' + 'END_CONTEXT\n' + 'AS_SUBSTITUTION\n' + 'SUB GLYPH "a"\n' + 'WITH GLYPH "a.sc"\n' + 'END_SUB\n' + 'SUB GLYPH "b"\n' + 'WITH GLYPH "b.sc"\n' + 'END_SUB\n' + 'END_SUBSTITUTION' + ).statements + self.assertEqual((lookup.name, list(lookup.sub.mapping.items())), + ("smcp", [(("a",), ("a.sc",)), (("b",), ("b.sc",))])) + + def test_substitution_single_in_context(self): + [group, lookup] = self.parse( + 'DEF_GROUP "Denominators" ENUM GLYPH "one.dnom" GLYPH "two.dnom" ' + 'END_ENUM END_GROUP\n' + 'DEF_LOOKUP "fracdnom" PROCESS_BASE PROCESS_MARKS ALL ' + 'DIRECTION LTR\n' + 'IN_CONTEXT LEFT ENUM GROUP "Denominators" GLYPH "fraction" ' + 'END_ENUM\n' + 'END_CONTEXT\n' + 'AS_SUBSTITUTION\n' + 'SUB GLYPH "one"\n' + 'WITH GLYPH "one.dnom"\n' + 'END_SUB\n' + 'SUB GLYPH "two"\n' + 'WITH GLYPH "two.dnom"\n' + 'END_SUB\n' + 'END_SUBSTITUTION' + ).statements + context = lookup.context[0] + self.assertEqual( + (lookup.name, list(lookup.sub.mapping.items()), + context.ex_or_in, context.left, context.right), + ("fracdnom", [(("one",), ("one.dnom",)), (("two",), ("two.dnom",))], + "IN_CONTEXT", [((("Denominators",), "fraction"),)], []) + ) + + def test_substitution_single_in_contexts(self): + [group, lookup] = self.parse( + 'DEF_GROUP "Hebrew" ENUM GLYPH "uni05D0" GLYPH "uni05D1" ' + 'END_ENUM END_GROUP\n' + 'DEF_LOOKUP "HebrewCurrency" PROCESS_BASE PROCESS_MARKS ALL ' + 'DIRECTION LTR\n' + 'IN_CONTEXT\n' + 'RIGHT GROUP "Hebrew"\n' + 'RIGHT GLYPH "one.Hebr"\n' + 'END_CONTEXT\n' + 'IN_CONTEXT\n' + 'LEFT GROUP "Hebrew"\n' + 'LEFT GLYPH "one.Hebr"\n' + 'END_CONTEXT\n' + 'AS_SUBSTITUTION\n' + 'SUB GLYPH "dollar"\n' + 'WITH GLYPH "dollar.Hebr"\n' + 'END_SUB\n' + 'END_SUBSTITUTION' + ).statements + context1 = lookup.context[0] + context2 = lookup.context[1] + self.assertEqual( + (lookup.name, context1.ex_or_in, context1.left, + context1.right, context2.ex_or_in, + context2.left, context2.right), + ("HebrewCurrency", "IN_CONTEXT", [], + [(("Hebrew",),), ("one.Hebr",)], "IN_CONTEXT", + [(("Hebrew",),), ("one.Hebr",)], [])) + + def test_substitution_skip_base(self): + [group, lookup] = self.parse( + 'DEF_GROUP "SomeMarks" ENUM GLYPH "marka" GLYPH "markb" ' + 'END_ENUM END_GROUP\n' + 'DEF_LOOKUP "SomeSub" SKIP_BASE PROCESS_MARKS ALL ' + 'DIRECTION LTR\n' + 'IN_CONTEXT\n' + 'END_CONTEXT\n' + 'AS_SUBSTITUTION\n' + 'SUB GLYPH "A"\n' + 'WITH GLYPH "A.c2sc"\n' + 'END_SUB\n' + 'END_SUBSTITUTION' + ).statements + process_base = lookup.process_base + self.assertEqual( + (lookup.name, process_base), + ("SomeSub", False)) + + def test_substitution_process_base(self): + [group, lookup] = self.parse( + 'DEF_GROUP "SomeMarks" ENUM GLYPH "marka" GLYPH "markb" ' + 'END_ENUM END_GROUP\n' + 'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS ALL ' + 'DIRECTION LTR\n' + 'IN_CONTEXT\n' + 'END_CONTEXT\n' + 'AS_SUBSTITUTION\n' + 'SUB GLYPH "A"\n' + 'WITH GLYPH "A.c2sc"\n' + 'END_SUB\n' + 'END_SUBSTITUTION' + ).statements + process_base = lookup.process_base + self.assertEqual( + (lookup.name, process_base), + ("SomeSub", True)) + + def test_substitution_skip_marks(self): + [group, lookup] = self.parse( + 'DEF_GROUP "SomeMarks" ENUM GLYPH "marka" GLYPH "markb" ' + 'END_ENUM END_GROUP\n' + 'DEF_LOOKUP "SomeSub" PROCESS_BASE SKIP_MARKS ' + 'DIRECTION LTR\n' + 'IN_CONTEXT\n' + 'END_CONTEXT\n' + 'AS_SUBSTITUTION\n' + 'SUB GLYPH "A"\n' + 'WITH GLYPH "A.c2sc"\n' + 'END_SUB\n' + 'END_SUBSTITUTION' + ).statements + process_marks = lookup.process_marks + self.assertEqual( + (lookup.name, process_marks), + ("SomeSub", False)) + + def test_substitution_process_marks(self): + [group, lookup] = self.parse( + 'DEF_GROUP "SomeMarks" ENUM GLYPH "acutecmb" GLYPH "gravecmb" ' + 'END_ENUM END_GROUP\n' + 'DEF_LOOKUP "SomeSub" PROCESS_BASE ' + 'PROCESS_MARKS "SomeMarks" \n' + 'DIRECTION RTL\n' + 'AS_SUBSTITUTION\n' + 'SUB GLYPH "A"\n' + 'WITH GLYPH "A.c2sc"\n' + 'END_SUB\n' + 'END_SUBSTITUTION' + ).statements + process_marks = lookup.process_marks + self.assertEqual( + (lookup.name, process_marks), + ("SomeSub", "SomeMarks")) + + def test_substitution_process_all_marks(self): + [group, lookup] = self.parse( + 'DEF_GROUP "SomeMarks" ENUM GLYPH "acutecmb" GLYPH "gravecmb" ' + 'END_ENUM END_GROUP\n' + 'DEF_LOOKUP "SomeSub" PROCESS_BASE ' + 'PROCESS_MARKS ALL \n' + 'DIRECTION RTL\n' + 'AS_SUBSTITUTION\n' + 'SUB GLYPH "A"\n' + 'WITH GLYPH "A.c2sc"\n' + 'END_SUB\n' + 'END_SUBSTITUTION' + ).statements + process_marks = lookup.process_marks + self.assertEqual( + (lookup.name, process_marks), + ("SomeSub", True)) + + def test_substitution_no_reversal(self): + # TODO: check right context with no reversal + [lookup] = self.parse( + 'DEF_LOOKUP "Lookup" PROCESS_BASE PROCESS_MARKS ALL ' + 'DIRECTION LTR\n' + 'IN_CONTEXT\n' + 'RIGHT ENUM GLYPH "a" GLYPH "b" END_ENUM\n' + 'END_CONTEXT\n' + 'AS_SUBSTITUTION\n' + 'SUB GLYPH "a"\n' + 'WITH GLYPH "a.alt"\n' + 'END_SUB\n' + 'END_SUBSTITUTION' + ).statements + self.assertEqual( + (lookup.name, lookup.reversal), + ("Lookup", None) + ) + + def test_substitution_reversal(self): + [lookup] = self.parse( + 'DEF_LOOKUP "RevLookup" PROCESS_BASE PROCESS_MARKS ALL ' + 'DIRECTION LTR REVERSAL\n' + 'IN_CONTEXT\n' + 'RIGHT ENUM GLYPH "a" GLYPH "b" END_ENUM\n' + 'END_CONTEXT\n' + 'AS_SUBSTITUTION\n' + 'SUB GROUP "DFLT_Num_standardFigures"\n' + 'WITH GROUP "DFLT_Num_numerators"\n' + 'END_SUB\n' + 'END_SUBSTITUTION' + ).statements + self.assertEqual( + (lookup.name, lookup.reversal), + ("RevLookup", True) + ) + + def test_substitution_single_to_multiple(self): + [lookup] = self.parse( + 'DEF_LOOKUP "ccmp" PROCESS_BASE PROCESS_MARKS ALL ' + 'DIRECTION LTR\n' + 'IN_CONTEXT\n' + 'END_CONTEXT\n' + 'AS_SUBSTITUTION\n' + 'SUB GLYPH "aacute"\n' + 'WITH GLYPH "a" GLYPH "acutecomb"\n' + 'END_SUB\n' + 'SUB GLYPH "agrave"\n' + 'WITH GLYPH "a" GLYPH "gravecomb"\n' + 'END_SUB\n' + 'END_SUBSTITUTION' + ).statements + self.assertEqual((lookup.name, list(lookup.sub.mapping.items())), + ("ccmp", + [(("aacute",), ("a", "acutecomb")), + (("agrave",), ("a", "gravecomb"))] + )) + + def test_substitution_multiple_to_single(self): + [lookup] = self.parse( + 'DEF_LOOKUP "liga" PROCESS_BASE PROCESS_MARKS ALL ' + 'DIRECTION LTR\n' + 'IN_CONTEXT\n' + 'END_CONTEXT\n' + 'AS_SUBSTITUTION\n' + 'SUB GLYPH "f" GLYPH "i"\n' + 'WITH GLYPH "f_i"\n' + 'END_SUB\n' + 'SUB GLYPH "f" GLYPH "t"\n' + 'WITH GLYPH "f_t"\n' + 'END_SUB\n' + 'END_SUBSTITUTION' + ).statements + self.assertEqual((lookup.name, list(lookup.sub.mapping.items())), + ("liga", + [(("f", "i"), ("f_i",)), + (("f", "t"), ("f_t",))])) + + def test_substitution_reverse_chaining_single(self): + [lookup] = self.parse( + 'DEF_LOOKUP "numr" PROCESS_BASE PROCESS_MARKS ALL ' + 'DIRECTION LTR REVERSAL\n' + 'IN_CONTEXT\n' + 'RIGHT ENUM ' + 'GLYPH "fraction" ' + 'RANGE "zero.numr" TO "nine.numr" ' + 'END_ENUM\n' + 'END_CONTEXT\n' + 'AS_SUBSTITUTION\n' + 'SUB RANGE "zero" TO "nine"\n' + 'WITH RANGE "zero.numr" TO "nine.numr"\n' + 'END_SUB\n' + 'END_SUBSTITUTION' + ).statements + self.assertEqual( + (lookup.name, lookup.context[0].right, + list(lookup.sub.mapping.items())), + ("numr", [(("fraction", ("zero.numr", "nine.numr")),)], + [((("zero", "nine"),), (("zero.numr", "nine.numr"),))])) + + # GPOS + # ATTACH_CURSIVE + # ATTACH + # ADJUST_PAIR + # ADJUST_SINGLE + def test_position_empty(self): + with self.assertRaisesRegex( + VoltLibError, + 'Expected ATTACH, ATTACH_CURSIVE, ADJUST_PAIR, ADJUST_SINGLE'): + [lookup] = self.parse( + 'DEF_LOOKUP "empty_position" PROCESS_BASE PROCESS_MARKS ALL ' + 'DIRECTION LTR\n' + 'EXCEPT_CONTEXT\n' + 'LEFT GLYPH "glyph"\n' + 'END_CONTEXT\n' + 'AS_POSITION\n' + 'END_POSITION' + ).statements + + def test_position_attach(self): + [lookup, anchor1, anchor2, anchor3, anchor4] = self.parse( + 'DEF_LOOKUP "anchor_top" PROCESS_BASE PROCESS_MARKS ALL ' + 'DIRECTION RTL\n' + 'IN_CONTEXT\n' + 'END_CONTEXT\n' + 'AS_POSITION\n' + 'ATTACH GLYPH "a" GLYPH "e"\n' + 'TO GLYPH "acutecomb" AT ANCHOR "top" ' + 'GLYPH "gravecomb" AT ANCHOR "top"\n' + 'END_ATTACH\n' + 'END_POSITION\n' + 'DEF_ANCHOR "MARK_top" ON 120 GLYPH acutecomb COMPONENT 1 ' + 'AT POS DX 0 DY 450 END_POS END_ANCHOR\n' + 'DEF_ANCHOR "MARK_top" ON 121 GLYPH gravecomb COMPONENT 1 ' + 'AT POS DX 0 DY 450 END_POS END_ANCHOR\n' + 'DEF_ANCHOR "top" ON 31 GLYPH a COMPONENT 1 ' + 'AT POS DX 210 DY 450 END_POS END_ANCHOR\n' + 'DEF_ANCHOR "top" ON 35 GLYPH e COMPONENT 1 ' + 'AT POS DX 215 DY 450 END_POS END_ANCHOR\n' + ).statements + self.assertEqual( + (lookup.name, lookup.pos.coverage, lookup.pos.coverage_to), + ("anchor_top", ("a", "e"), [(("acutecomb",), "top"), + (("gravecomb",), "top")]) + ) + self.assertEqual( + (anchor1.name, anchor1.gid, anchor1.glyph_name, anchor1.component, + anchor1.locked, anchor1.pos), + ("MARK_top", 120, "acutecomb", 1, False, (None, 0, 450, {}, {}, + {})) + ) + self.assertEqual( + (anchor2.name, anchor2.gid, anchor2.glyph_name, anchor2.component, + anchor2.locked, anchor2.pos), + ("MARK_top", 121, "gravecomb", 1, False, (None, 0, 450, {}, {}, + {})) + ) + self.assertEqual( + (anchor3.name, anchor3.gid, anchor3.glyph_name, anchor3.component, + anchor3.locked, anchor3.pos), + ("top", 31, "a", 1, False, (None, 210, 450, {}, {}, {})) + ) + self.assertEqual( + (anchor4.name, anchor4.gid, anchor4.glyph_name, anchor4.component, + anchor4.locked, anchor4.pos), + ("top", 35, "e", 1, False, (None, 215, 450, {}, {}, {})) + ) + + def test_position_attach_cursive(self): + [lookup] = self.parse( + 'DEF_LOOKUP "SomeLookup" PROCESS_BASE PROCESS_MARKS ALL ' + 'DIRECTION RTL\n' + 'IN_CONTEXT\n' + 'END_CONTEXT\n' + 'AS_POSITION\n' + 'ATTACH_CURSIVE EXIT GLYPH "a" GLYPH "b" ENTER GLYPH "c"\n' + 'END_ATTACH\n' + 'END_POSITION\n' + ).statements + self.assertEqual( + (lookup.name, + lookup.pos.coverages_exit, lookup.pos.coverages_enter), + ("SomeLookup", + [("a", "b")], [("c",)]) + ) + + def test_position_adjust_pair(self): + [lookup] = self.parse( + 'DEF_LOOKUP "kern1" PROCESS_BASE PROCESS_MARKS ALL ' + 'DIRECTION RTL\n' + 'IN_CONTEXT\n' + 'END_CONTEXT\n' + 'AS_POSITION\n' + 'ADJUST_PAIR\n' + ' FIRST GLYPH "A"\n' + ' SECOND GLYPH "V"\n' + ' 1 2 BY POS ADV -30 END_POS POS END_POS\n' + ' 2 1 BY POS ADV -30 END_POS POS END_POS\n' + 'END_ADJUST\n' + 'END_POSITION\n' + ).statements + self.assertEqual( + (lookup.name, lookup.pos.coverages_1, lookup.pos.coverages_2, + lookup.pos.adjust_pair), + ("kern1", [("A",)], [("V",)], + {(1, 2): ((-30, None, None, {}, {}, {}), + (None, None, None, {}, {}, {})), + (2, 1): ((-30, None, None, {}, {}, {}), + (None, None, None, {}, {}, {}))}) + ) + + def test_position_adjust_single(self): + [lookup] = self.parse( + 'DEF_LOOKUP "TestLookup" PROCESS_BASE PROCESS_MARKS ALL ' + 'DIRECTION LTR\n' + 'IN_CONTEXT\n' + # 'LEFT GLYPH "leftGlyph"\n' + # 'RIGHT GLYPH "rightGlyph"\n' + 'END_CONTEXT\n' + 'AS_POSITION\n' + 'ADJUST_SINGLE' + ' GLYPH "glyph1" BY POS ADV 0 DX 123 END_POS\n' + ' GLYPH "glyph2" BY POS ADV 0 DX 456 END_POS\n' + 'END_ADJUST\n' + 'END_POSITION\n' + ).statements + self.assertEqual( + (lookup.name, lookup.pos.adjust_single), + ("TestLookup", + [(("glyph1",), (0, 123, None, {}, {}, {})), + (("glyph2",), (0, 456, None, {}, {}, {}))]) + ) + + def test_def_anchor(self): + [anchor1, anchor2, anchor3] = self.parse( + 'DEF_ANCHOR "top" ON 120 GLYPH a ' + 'COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR\n' + 'DEF_ANCHOR "MARK_top" ON 120 GLYPH acutecomb ' + 'COMPONENT 1 AT POS DX 0 DY 450 END_POS END_ANCHOR\n' + 'DEF_ANCHOR "bottom" ON 120 GLYPH a ' + 'COMPONENT 1 AT POS DX 250 DY 0 END_POS END_ANCHOR\n' + ).statements + self.assertEqual( + (anchor1.name, anchor1.gid, anchor1.glyph_name, anchor1.component, + anchor1.locked, anchor1.pos), + ("top", 120, "a", 1, + False, (None, 250, 450, {}, {}, {})) + ) + self.assertEqual( + (anchor2.name, anchor2.gid, anchor2.glyph_name, anchor2.component, + anchor2.locked, anchor2.pos), + ("MARK_top", 120, "acutecomb", 1, + False, (None, 0, 450, {}, {}, {})) + ) + self.assertEqual( + (anchor3.name, anchor3.gid, anchor3.glyph_name, anchor3.component, + anchor3.locked, anchor3.pos), + ("bottom", 120, "a", 1, + False, (None, 250, 0, {}, {}, {})) + ) + + def test_def_anchor_duplicate(self): + self.assertRaisesRegex( + VoltLibError, + 'Anchor "dupe" already defined, ' + 'anchor names are case insensitive', + self.parse, + 'DEF_ANCHOR "dupe" ON 120 GLYPH a ' + 'COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR\n' + 'DEF_ANCHOR "dupe" ON 120 GLYPH a ' + 'COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR\n' + ) + + def test_def_anchor_locked(self): + [anchor] = self.parse( + 'DEF_ANCHOR "top" ON 120 GLYPH a ' + 'COMPONENT 1 LOCKED AT POS DX 250 DY 450 END_POS END_ANCHOR\n' + ).statements + self.assertEqual( + (anchor.name, anchor.gid, anchor.glyph_name, anchor.component, + anchor.locked, anchor.pos), + ("top", 120, "a", 1, + True, (None, 250, 450, {}, {}, {})) + ) + + def test_anchor_adjust_device(self): + [anchor] = self.parse( + 'DEF_ANCHOR "MARK_top" ON 123 GLYPH diacglyph ' + 'COMPONENT 1 AT POS DX 0 DY 456 ADJUST_BY 12 AT 34 ' + 'ADJUST_BY 56 AT 78 END_POS END_ANCHOR' + ).statements + self.assertEqual( + (anchor.name, anchor.pos), + ("MARK_top", (None, 0, 456, {}, {}, {34: 12, 78: 56})) + ) + + def test_ppem(self): + [grid_ppem, pres_ppem, ppos_ppem] = self.parse( + 'GRID_PPEM 20\n' + 'PRESENTATION_PPEM 72\n' + 'PPOSITIONING_PPEM 144\n' + ).statements + self.assertEqual( + ((grid_ppem.name, grid_ppem.value), + (pres_ppem.name, pres_ppem.value), + (ppos_ppem.name, ppos_ppem.value)), + (("GRID_PPEM", 20), ("PRESENTATION_PPEM", 72), + ("PPOSITIONING_PPEM", 144)) + ) + + def test_compiler_flags(self): + [setting1, setting2] = self.parse( + 'COMPILER_USEEXTENSIONLOOKUPS\n' + 'COMPILER_USEPAIRPOSFORMAT2\n' + ).statements + self.assertEqual( + ((setting1.name, setting1.value), + (setting2.name, setting2.value)), + (("COMPILER_USEEXTENSIONLOOKUPS", True), + ("COMPILER_USEPAIRPOSFORMAT2", True)) + ) + + def test_cmap(self): + [cmap_format1, cmap_format2, cmap_format3] = self.parse( + 'CMAP_FORMAT 0 3 4\n' + 'CMAP_FORMAT 1 0 6\n' + 'CMAP_FORMAT 3 1 4\n' + ).statements + self.assertEqual( + ((cmap_format1.name, cmap_format1.value), + (cmap_format2.name, cmap_format2.value), + (cmap_format3.name, cmap_format3.value)), + (("CMAP_FORMAT", (0, 3, 4)), + ("CMAP_FORMAT", (1, 0, 6)), + ("CMAP_FORMAT", (3, 1, 4))) + ) + + def setUp(self): + self.tempdir = None + self.num_tempfiles = 0 + + def tearDown(self): + if self.tempdir: + shutil.rmtree(self.tempdir) + + def parse(self, text): + if not self.tempdir: + self.tempdir = tempfile.mkdtemp() + self.num_tempfiles += 1 + path = os.path.join(self.tempdir, "tmp%d.vtp" % self.num_tempfiles) + with open(path, "w") as outfile: + outfile.write(text) + return Parser(path).parse() + +if __name__ == "__main__": + import sys + sys.exit(unittest.main()) diff --git a/Tools/fontTools b/Tools/fontTools deleted file mode 120000 index 9a21c02..0000000 --- a/Tools/fontTools +++ /dev/null @@ -1 +0,0 @@ -../Lib/fontTools \ No newline at end of file diff --git a/Tools/pyftinspect b/Tools/pyftinspect deleted file mode 100755 index f50f81b..0000000 --- a/Tools/pyftinspect +++ /dev/null @@ -1,6 +0,0 @@ -#! /usr/bin/env python - -import sys -from fontTools import inspect - -inspect.main(sys.argv[1:]) diff --git a/Tools/pyftmerge b/Tools/pyftmerge deleted file mode 100755 index 2479258..0000000 --- a/Tools/pyftmerge +++ /dev/null @@ -1,6 +0,0 @@ -#! /usr/bin/env python - -import sys -from fontTools import merge - -merge.main(sys.argv[1:]) diff --git a/Tools/pyftsubset b/Tools/pyftsubset deleted file mode 100755 index 4bc7c7c..0000000 --- a/Tools/pyftsubset +++ /dev/null @@ -1,6 +0,0 @@ -#! /usr/bin/env python - -import sys -from fontTools import subset - -subset.main(sys.argv[1:]) diff --git a/Tools/ttx b/Tools/ttx deleted file mode 100755 index 10b9ef0..0000000 --- a/Tools/ttx +++ /dev/null @@ -1,6 +0,0 @@ -#! /usr/bin/env python - -import sys -from fontTools import ttx - -ttx.main(sys.argv[1:]) diff --git a/Windows/README.TXT b/Windows/README.TXT deleted file mode 100644 index 13f1971..0000000 --- a/Windows/README.TXT +++ /dev/null @@ -1,53 +0,0 @@ - -TTX 2.0 for Windows -------------------------- - -Creating a Windows (9x/ME/NT/2000/XP) setup executable for TTX -This file has been created by Adam Twardoch -December 14, 2004 - -Pre-compiled versions are hosted at http://www.font.org/software/ttx/ - -APPROACH I: Using py2exe and InnoSetup - -1. Install Python 2.3 for Windows: http://www.python.org/ -2. Install py2exe: http://starship.python.net/crew/theller/py2exe/ -3. Install InnoSetup 4: http://www.jrsoftware.org/ -4. Download the latest released source code of TTX/FontTools at - http://sourceforge.net/projects/fonttools/ - Or alternatively grab the sources from the VCS: - http://fonttools.sourceforge.net/ -5. Unzip the source code of TTX/FontTools into a folder. -6. In the folder where you unzipped TTX/FontTools, type: - python setup.py py2exe --icon Windows\ttx.ico --packages encodings -7. Run Inno Setup and open Windows\fonttools-win-setup.iss -8. In Inno Setup, select File/Compile, then Run/Run. - -APPROACH II: Using McMillan Installer and InnoSetup - -1. Install Python 2.3 for Windows: http://www.python.org/ -2. Download and unpack McMillan installer: - http://py.vaults.ca/apyllo2.py/22208368 - and put the Installer folder into your Python folder, - e.g. C:\Python23\Installer -3. Install InnoSetup 4: http://www.jrsoftware.org/ -4. Install Microsoft Visual C++ Toolkit 2003: - http://msdn.microsoft.com/visualc/vctoolkit2003/ -5. Put UPX somewhere within your PATH: http://upx.sourceforge.net/ -6. Download the latest released source code of TTX/FontTools at - http://sourceforge.net/projects/fonttools/ - Or alternatively grab the sources from the VCS: - http://fonttools.sourceforge.net/ -7. Unzip the source code of TTX/FontTools into a folder. -8. In the folder where you unzipped TTX/FontTools, type: - python setup.py install -f -9. Edit mcmillan.bat so the paths in the file correspond to the paths in your system, - and run it. -10.Run Inno Setup and open Windows\fonttools-win-setup.iss -11.In Inno Setup, select File/Compile, then Run/Run. - -The distributable TTX Windows setup executable has been saved -in the Output subfolder of the FontTools\Windows folder. - -For information on running TTX on Windows, see fonttools-win-setup.txt in this folder. - diff --git a/Windows/fonttools-win-setup.iss b/Windows/fonttools-win-setup.iss deleted file mode 100644 index 1227b9a..0000000 --- a/Windows/fonttools-win-setup.iss +++ /dev/null @@ -1,355 +0,0 @@ -;This file has been created by Adam Twardoch -;See README.TXT in this folder for instructions on building the setup - -[Setup] -AppName=TTX -AppVerName=TTX 2.0 r040926 for Windows -AppPublisher=Just van Rossum -AppPublisherURL=http://www.letterror.com/code/ttx/ -AppSupportURL=http://www.font.org/software/ttx/ -AppUpdatesURL=http://www.font.org/software/ttx/ -DefaultDirName={pf}\TTX -DefaultGroupName=TTX -AllowNoIcons=false -LicenseFile=..\LICENSE.txt -InfoBeforeFile=fonttools-win-setup.txt -InfoAfterFile=..\Doc\changes.txt -OutputBaseFilename=WinTTX2.0r040926 -AppCopyright=Copyright 1999-2004 by Just van Rossum, Letterror, The Netherlands. -UninstallDisplayIcon={app}\ttx.ico - -[Tasks] -Name: desktopicon; Description: Create a &desktop icon; GroupDescription: Additional icons: - -[Files] -Source: ..\dist\ttx\*.*; DestDir: {app}; Flags: ignoreversion promptifolder -Source: ..\LICENSE.txt; DestDir: {app}; Flags: ignoreversion promptifolder -Source: ..\Doc\documentation.html; DestDir: {app}; Flags: ignoreversion promptifolder -Source: ..\Doc\changes.txt; DestDir: {app}; Flags: ignoreversion promptifolder -Source: ..\Doc\bugs.txt; DestDir: {app}; Flags: ignoreversion promptifolder -Source: fonttools-win-setup.txt; DestDir: {app}; Flags: ignoreversion promptifolder -Source: ttx.ico; DestDir: {app}; Flags: ignoreversion promptifolder; AfterInstall: AddFolderToPathVariable - -[Icons] -Name: {userdesktop}\ttx.exe; Filename: {app}\ttx.exe; Tasks: desktopicon; IconFilename: {app}\ttx.ico; IconIndex: 0 -Name: {group}\TTX; Filename: {app}\ttx.exe; Tasks: desktopicon; IconFilename: {app}\ttx.ico; IconIndex: 0 -Name: {group}\TTX documentation; Filename: {app}\documentation.html; IconIndex: 0 -Name: {group}\Changes; Filename: {app}\changes.txt; IconIndex: 0 -Name: {group}\Bugs; Filename: {app}\bugs.txt; IconIndex: 0 -Name: {group}\License; Filename: {app}\LICENSE.txt; IconIndex: 0 -Name: {group}\Uninstall TTX; Filename: {uninstallexe}; IconIndex: 0 -Name: {reg:HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders,SendTo}\TTX; Filename: {app}\ttx.exe; WorkingDir: {reg:HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders,SendTo}; IconFilename: {app}\ttx.ico; IconIndex: 0; MinVersion: 0,5.00.2195 - -[_ISTool] -EnableISX=true - -[Registry] -Root: HKCR; Subkey: .ttx; ValueType: string; ValueData: {reg:HKCR\.xml,}; Flags: createvalueifdoesntexist uninsdeletekey - -[Code] - -// -// InnoSetup Extensions Knowledge Base -// Article 44 - Native ISX procedures for PATH modification -// http://www13.brinkster.com/vincenzog/isxart.asp?idart=44 -// Author: Thomas Vedel -// - -// Version log: -// 03/31/2003: Initial release (thv@lr.dk) - -const - // Modification method - pmAddToBeginning = $1; // Add dir to beginning of Path - pmAddToEnd = $2; // Add dir to end of Path - pmAddAllways = $4; // Add also if specified dir is already included in existing path - pmAddOnlyIfDirExists = $8; // Add only if specified dir actually exists - pmRemove = $10; // Remove dir from path - pmRemoveSubdirsAlso = $20; // Remove dir and all subdirs from path - - // Scope - psCurrentUser = 1; // Modify path for current user - psAllUsers = 2; // Modify path for all users - - // Error results - mpOK = 0; // No errors - mpMissingRights = -1; // User has insufficient rights - mpAutoexecNoWriteacc = -2; // Autoexec can not be written (may be readonly) - mpBothAddAndRemove = -3; // User has specified that dir should both be removed from and added to path - - -{ Helper procedure: Split a path environment variable into individual dirnames } -procedure SplitPath(Path: string; var Dirs: TStringList); -var - pos: integer; - s: string; -begin - Dirs.Clear; - s := ''; - pos := 1; - while (pos<=Length(Path)) do - begin - if (Path[pos]<>';') then - s := s + Path[pos]; - if ((Path[pos]=';') or (pos=Length(Path))) then - begin - s := Trim(s); - s := RemoveQuotes(s); - s := Trim(s); - if (s <> '') then - Dirs.Add(s); - s := ''; - end; - Pos := Pos + 1; - end; -end; // procedure SplitPath - - -{ Helper procedure: Concatenate individual dirnames into a path environment variable } -procedure ConcatPath(Dirs: TStringList; Quotes: boolean; var Path: string); -var - Index, MaxIndex: integer; - s: string; -begin - MaxIndex := Dirs.Count-1; - Path := ''; - for Index := 0 to MaxIndex do - begin - s := Dirs.Strings[Index]; - if ((Quotes) and (pos(' ',s) > 0)) then - s := AddQuotes(s); - Path := Path + s; - if (Index < MaxIndex) then - Path := Path + ';' - end; -end; // procedure ConcatPath - - -{ Helper function: Modifies path environment string } -procedure ModifyPathString(OldPath, DirName: string; Method: integer; Quotes: boolean; var ResultPath: string); -var - Dirs: TStringList; - DirNotInPath: Boolean; - i: integer; -begin - // Create Dirs variable - Dirs := TStringList.Create; - - // Remove quotes form DirName - DirName := Trim(DirName); - DirName := RemoveQuotes(DirName); - DirName := Trim(DirName); - - // Split old path in individual directory names - SplitPath(OldPath, Dirs); - - // Check if dir is allready in path - DirNotInPath := True; - for i:=Dirs.Count-1 downto 0 do - begin - if (uppercase(Dirs.Strings[i]) = uppercase(DirName)) then - DirNotInPath := False; - end; - - // Should dir be removed from existing Path? - if ((Method and (pmRemove or pmRemoveSubdirsAlso)) > 0) then - begin - for i:=Dirs.Count-1 downto 0 do - begin - if (((Method and pmRemoveSubdirsAlso) > 0) and (pos(uppercase(DirName)+'\', uppercase(Dirs.Strings[i])) = 1)) or - (((Method and (pmRemove) or (pmRemoveSubdirsAlso)) > 0) and (uppercase(DirName) = uppercase(Dirs.Strings[i]))) - then - Dirs.Delete(i); - end; - end; - - // Should dir be added to existing Path? - if ((Method and (pmAddToBeginning or pmAddToEnd)) > 0) then - begin - // Add dir to path - if (((Method and pmAddAllways) > 0) or DirNotInPath) then - begin - // Dir is not in path allready or should be added anyway - if (((Method and pmAddOnlyIfDirExists) = 0) or (DirExists(DirName))) then - begin - // Dir actually exsists or should be added anyway - if ((Method and pmAddToBeginning) > 0) then - Dirs.Insert(0, DirName) - else - Dirs.Append(DirName); - end; - end; - end; - - // Concatenate directory names into one single path variable - ConcatPath(Dirs, Quotes, ResultPath); - // Finally free Dirs object - Dirs.Free; -end; // ModifyPathString - - -{ Helper function: Modify path on Windows 9x } -function ModifyPath9x(DirName: string; Method: integer): integer; -var - AutoexecLines: TStringList; - ActualLine: String; - PathLineNos: TStringList; - FirstPathLineNo: Integer; - OldPath, ResultPath: String; - LineNo, CharNo, Index: integer; - - TempString: String; -begin - // Expect everything to be OK - result := mpOK; - - // Create stringslists - AutoexecLines := TStringList.Create; - PathLineNos := TStringList.Create; - - // Read existing path - OldPath := ''; - LoadStringFromFile('c:\Autoexec.bat', TempString); - AutoexecLines.Text := TempString; - PathLineNos.Clear; - // Read Autoexec line by line - for LineNo := 0 to AutoexecLines.Count - 1 do begin - ActualLine := AutoexecLines.Strings[LineNo]; - // Check if line starts with "PATH=" after first stripping spaces and other "fill-chars" - if Pos('=', ActualLine) > 0 then - begin - for CharNo := Pos('=', ActualLine)-1 downto 1 do - if (ActualLine[CharNo]=' ') or (ActualLine[CharNo]=#9) then - Delete(ActualLine, CharNo, 1); - if Pos('@', ActualLine) = 1 then - Delete(ActualLine, 1, 1); - if (Pos('PATH=', uppercase(ActualLine))=1) or (Pos('SETPATH=', uppercase(ActualLine))=1) then - begin - // Remove 'PATH=' and add path to "OldPath" variable - Delete(ActualLine, 1, pos('=', ActualLine)); - // Check if an earlier PATH variable is referenced, but there has been no previous PATH defined in Autoexec - if (pos('%PATH%',uppercase(ActualLine))>0) and (PathLineNos.Count=0) then - OldPath := ExpandConstant('{win}') + ';' + ExpandConstant('{win}')+'\COMMAND'; - if (pos('%PATH%',uppercase(ActualLine))>0) then - begin - ActualLine := Copy(ActualLine, 1, pos('%PATH%',uppercase(ActualLine))-1) + - OldPath + - Copy(ActualLine, pos('%PATH%',uppercase(ActualLine))+6, Length(ActualLine)); - end; - OldPath := ActualLine; - - // Update list of line numbers holding path variables - PathLineNos.Add(IntToStr(LineNo)); - end; - end; - end; - - // Save first line number in Autoexec.bat which modifies path environment variable - if PathLineNos.Count > 0 then - FirstPathLineNo := StrToInt(PathLineNos.Strings[0]) - else - FirstPathLineNo := 0; - - // Modify path - ModifyPathString(OldPath, DirName, Method, True, ResultPath); - - // Write Modified path back to Autoexec.bat - // First delete all existing path references from Autoexec.bat - Index := PathLineNos.Count-1; - while (Index>=0) do - begin - AutoexecLines.Delete(StrToInt(PathLineNos.Strings[Index])); - Index := Index-1; - end; - // Then insert new path variable into Autoexec.bat - AutoexecLines.Insert(FirstPathLineNo, '@PATH='+ResultPath); - // Delete old Autoexec.bat from disk - if not DeleteFile('c:\Autoexec.bat') then - result := mpAutoexecNoWriteAcc; - Sleep(500); - // And finally write Autoexec.bat back to disk - if not (result=mpAutoexecNoWriteAcc) then - SaveStringToFile('c:\Autoexec.bat', AutoexecLines.Text, false); - - // Free stringlists - PathLineNos.Free; - AutoexecLines.Free; -end; // ModifyPath9x - - -{ Helper function: Modify path on Windows NT, 2000 and XP } -function ModifyPathNT(DirName: string; Method, Scope: integer): integer; -var - RegRootKey: integer; - RegSubKeyName: string; - RegValueName: string; - OldPath, ResultPath: string; - OK: boolean; -begin - // Expect everything to be OK - result := mpOK; - - // Initialize registry key and value names to reflect if changes should be global or local to current user only - case Scope of - psCurrentUser: - begin - RegRootKey := HKEY_CURRENT_USER; - RegSubKeyName := 'Environment'; - RegValueName := 'Path'; - end; - psAllUsers: - begin - RegRootKey := HKEY_LOCAL_MACHINE; - RegSubKeyName := 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'; - RegValueName := 'Path'; - end; - end; - - // Read current path value from registry - OK := RegQueryStringValue(RegRootKey, RegSubKeyName, RegValueName, OldPath); - if not OK then - begin - result := mpMissingRights; - Exit; - end; - - // Modify path - ModifyPathString(OldPath, DirName, Method, False, ResultPath); - - // Write new path value to registry - if not RegWriteStringValue(RegRootKey, RegSubKeyName, RegValueName, ResultPath) then - begin - result := mpMissingRights; - Exit; - - end; -end; // ModifyPathNT - - -{ Main function: Modify path } -function ModifyPath(Path: string; Method, Scope: integer): integer; -begin - // Check if both add and remove has been specified (= error!) - if (Method and (pmAddToBeginning or pmAddToEnd) and (pmRemove or pmRemoveSubdirsAlso)) > 0 then - begin - result := mpBothAddAndRemove; - Exit; - end; - - // Perform directory constant expansion - Path := ExpandConstantEx(Path, ' ', ' '); - - // Test if Win9x - if InstallOnThisVersion('4,0','0,0') = irInstall then - ModifyPath9x(Path, Method); - - // Test if WinNT, 2000 or XP - if InstallOnThisVersion('0,4','0,0') = irInstall then - ModifyPathNT(Path, Method, Scope); -end; // ModifyPath - -procedure AddFolderToPathVariable(); -begin - ModifyPath('{app}', pmAddToBeginning, psAllUsers); - ModifyPath('{app}', pmAddToBeginning, psCurrentUser); -end; diff --git a/Windows/fonttools-win-setup.txt b/Windows/fonttools-win-setup.txt deleted file mode 100644 index 721c858..0000000 --- a/Windows/fonttools-win-setup.txt +++ /dev/null @@ -1,12 +0,0 @@ -TTX is an application to convert OpenType and TrueType files to and from an -XML-based text format, also called TTX. - -The TTX setup application can create an icon for TTX on your desktop. You will -then be able to drop .TTF or .OTF files onto the ttx.exe icon to dump the font -to a .TTX file. Dropping a .TTX file onto it builds a TTF or OTF font. - -Also, the setup puts a shortcut to TTX in your Send To context menu in Windows -Explorer. Click on any OTF, TTF or TTX file with the right mouse button, -choose Send To and then TTX. - -For more information, see documentation.html diff --git a/Windows/mcmillan.bat b/Windows/mcmillan.bat deleted file mode 100755 index c4f48c9..0000000 --- a/Windows/mcmillan.bat +++ /dev/null @@ -1,9 +0,0 @@ -@echo off -mkdir Build -mkdir ..\dist -mkdir ..\dist\ttx -C:\Python23\Installer\Configure.py -C:\Python23\Installer\Makespec.py --upx --onefile --paths "C:\Python23\Lib\encodings;C:\Python23\Lib\site-packages\FontTools\fontTools\encodings;C:\Python23\Lib\site-packages\FontTools\fontTools\misc;C:\Python23\Lib\site-packages\FontTools\fontTools\pens;C:\Python23\Lib\site-packages\FontTools\fontTools\ttLib;" --icon ttx.ico --out Build C:\Python23\Lib\site-packages\FontTools\fontTools\ttx.py -C:\Python23\Installer\Build.py Build\ttx.spec -move Build\ttx.exe ..\dist\ttx - diff --git a/Windows/ttx.ico b/Windows/ttx.ico deleted file mode 100644 index f0482b0fd5806dcd9328a8e1546070ab1a15ebdb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2238 zcmZQzU}RuqP*4ET91JTs7#PGD7#K7d7#I{77#JKFAmR)R3=9n{AQFlhA+#U^1H=FS z{}~u&{%2q?PGc~hd4_=@Z6pgU4i|&Jf&$o#WJ^m1V~`oz5Hl<-%@_;}KxQ~V%rG)yFfuYiGQ*I; z(9jTUh6>aSkUI=OW~f5UFfuZLnxO_UBRLt(9ma5X*h0-nOHO70o1qIa!^{lk4r_=R zhK6AOBK(rfU}gsKiyhbuKZd+iB?c~iErx>33Wl6w1qOXCB?db?JG>ZVo{Ft4gDox` zVF&iRib@1SLY9M_`?om-uVTe#uVNl73s!_9M z$j49)hc8SWhI`Z)1aJTj zm^c$FR=@#Mz{7+UaKIGcmWP;tM;@#IpFBtbNS=$8l?&<$eqL4<5R(G}1O#}Pn7G(E zIG90#OicV7930FbwGe3z4zN6k16IJr4Px>#F)@K?updDB!5mg5CMH%ei<1i`&k8o5 ziJgO;o1KFdCJ$nP6*58PnHiXwxR^P3nV7-C9PC_R^O<1sTwv#Ob8xeRoX5+}3N{~P zAH;l!+dp|Xt@zLc$zUKyo4om^700$_1 zpN?>>NlYfaD=AVd7>62QD)cI|nx}GaJMduslQoGY1<7 z2L~S)6DuD$khnosK;*#+AVCijg|OI=kD{{TyM-x>e_ diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..a34deb2 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,4 @@ +pytest>=3.0 +tox>=2.5 +bump2version>=0.5.6 +sphinx>=1.5.5 diff --git a/fonttools b/fonttools new file mode 100755 index 0000000..92b390e --- /dev/null +++ b/fonttools @@ -0,0 +1,12 @@ +#!/usr/bin/env python +from __future__ import print_function, division, absolute_import +import sys +import os.path + +libdir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'Lib') +sys.path.insert(0, libdir) + +from fontTools.__main__ import main + +if __name__ == '__main__': + sys.exit(main()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5592626 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +# we use the official Brotli module on CPython and the CFFI-based +# extension 'brotlipy' on PyPy +brotli==1.0.1; platform_python_implementation != "PyPy" +brotlipy==0.7.0; platform_python_implementation == "PyPy" +unicodedata2==10.0.0; python_version < '3.7' and platform_python_implementation != "PyPy" +munkres==1.0.10 +zopfli==0.1.4 diff --git a/run-tests.sh b/run-tests.sh new file mode 100755 index 0000000..0eb84de --- /dev/null +++ b/run-tests.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +# exit if any subcommand return non-zero status +set -e + +# Choose python version +if test "x$1" = x-3; then + PYTHON=python3 + shift +elif test "x$1" = x-2; then + PYTHON=python2 + shift +fi +test "x$PYTHON" = x && PYTHON=python + +# Find tests +FILTERS= +for arg in "$@"; do + test "x$FILTERS" != x && FILTERS="$FILTERS or " + FILTERS="$FILTERS$arg" +done + +# Run tests +if [ -z "$FILTERS" ]; then + $PYTHON setup.py test +else + $PYTHON setup.py test --addopts="-k \"$FILTERS\"" +fi diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..232b34a --- /dev/null +++ b/setup.cfg @@ -0,0 +1,57 @@ +[bumpversion] +current_version = 3.28.0 +commit = True +tag = False +tag_name = {new_version} +parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\.(?P[a-z]+)(?P\d+))? +serialize = + {major}.{minor}.{patch}.{release}{dev} + {major}.{minor}.{patch} + +[bumpversion:part:release] +optional_value = final +values = + dev + final + +[bumpversion:part:dev] + +[bumpversion:file:Lib/fontTools/__init__.py] +search = __version__ = "{current_version}" +replace = __version__ = "{new_version}" + +[bumpversion:file:setup.py] +search = version="{current_version}" +replace = version="{new_version}" + +[wheel] +universal = 1 + +[sdist] +formats = zip + +[aliases] +test = pytest + +[metadata] +license_file = LICENSE + +[tool:pytest] +minversion = 3.0 +testpaths = + Tests + fontTools +python_files = + *_test.py +python_classes = + *Test +addopts = + -r a + --doctest-modules + --doctest-ignore-import-errors + --pyargs + +[egg_info] +tag_build = +tag_date = 0 + diff --git a/setup.py b/setup.py index d5b6436..c843da0 100755 --- a/setup.py +++ b/setup.py @@ -1,87 +1,344 @@ #! /usr/bin/env python from __future__ import print_function -import os, sys -from distutils.core import setup, Extension -from distutils.command.build_ext import build_ext - -try: - # load py2exe distutils extension, if available - import py2exe -except ImportError: - pass - -try: - import xml.parsers.expat -except ImportError: - print("*** Warning: FontTools needs PyXML, see:") - print(" http://sourceforge.net/projects/pyxml/") - - -class build_ext_optional(build_ext): - """build_ext command which doesn't abort when it fails.""" - def build_extension(self, ext): - # Skip extensions which cannot be built - try: - build_ext.build_extension(self, ext) - except: - self.announce( - '*** WARNING: Building of extension "%s" ' - 'failed: %s' % - (ext.name, sys.exc_info()[1])) - - -if sys.version_info > (2, 3, 0, 'alpha', 1): - # Trove classifiers for PyPI - classifiers = {"classifiers": [ - "Development Status :: 4 - Beta", - "Environment :: Console", - "Environment :: Other Environment", - "Intended Audience :: Developers", - "Intended Audience :: End Users/Desktop", - "License :: OSI Approved :: BSD License", - "Natural Language :: English", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Topic :: Multimedia :: Graphics", - "Topic :: Multimedia :: Graphics :: Graphics Conversion", - ]} -else: - classifiers = {} - -long_description = """\ -FontTools/TTX is a library to manipulate font files from Python. -It supports reading and writing of TrueType/OpenType fonts, reading -and writing of AFM files, reading (and partially writing) of PS Type 1 -fonts. The package also contains a tool called "TTX" which converts -TrueType/OpenType fonts to and from an XML-based format. -""" +import io +import sys +import os +from os.path import isfile, join as pjoin +from glob import glob +from setuptools import setup, find_packages, Command +from distutils import log +from distutils.util import convert_path +import subprocess as sp +import contextlib +import re + +# Force distutils to use py_compile.compile() function with 'doraise' argument +# set to True, in order to raise an exception on compilation errors +import py_compile +orig_py_compile = py_compile.compile + +def doraise_py_compile(file, cfile=None, dfile=None, doraise=False): + orig_py_compile(file, cfile=cfile, dfile=dfile, doraise=True) + +py_compile.compile = doraise_py_compile + +needs_pytest = {'pytest', 'test'}.intersection(sys.argv) +pytest_runner = ['pytest_runner'] if needs_pytest else [] +needs_wheel = {'bdist_wheel'}.intersection(sys.argv) +wheel = ['wheel'] if needs_wheel else [] +needs_bumpversion = {'release'}.intersection(sys.argv) +bumpversion = ['bump2version'] if needs_bumpversion else [] + +# Trove classifiers for PyPI +classifiers = {"classifiers": [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Environment :: Other Environment", + "Intended Audience :: Developers", + "Intended Audience :: End Users/Desktop", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 3", + "Topic :: Text Processing :: Fonts", + "Topic :: Multimedia :: Graphics", + "Topic :: Multimedia :: Graphics :: Graphics Conversion", +]} + + +# concatenate README.rst and NEWS.rest into long_description so they are +# displayed on the FontTols project page on PyPI +with io.open("README.rst", "r", encoding="utf-8") as readme: + long_description = readme.read() +long_description += "\nChangelog\n~~~~~~~~~\n\n" +with io.open("NEWS.rst", "r", encoding="utf-8") as changelog: + long_description += changelog.read() + + +@contextlib.contextmanager +def capture_logger(name): + """ Context manager to capture a logger output with a StringIO stream. + """ + import logging + + logger = logging.getLogger(name) + try: + import StringIO + stream = StringIO.StringIO() + except ImportError: + stream = io.StringIO() + handler = logging.StreamHandler(stream) + logger.addHandler(handler) + try: + yield stream + finally: + logger.removeHandler(handler) + + +class release(Command): + """ + Tag a new release with a single command, using the 'bumpversion' tool + to update all the version strings in the source code. + The version scheme conforms to 'SemVer' and PEP 440 specifications. + + Firstly, the pre-release '.devN' suffix is dropped to signal that this is + a stable release. If '--major' or '--minor' options are passed, the + the first or second 'semver' digit is also incremented. Major is usually + for backward-incompatible API changes, while minor is used when adding + new backward-compatible functionalities. No options imply 'patch' or bug-fix + release. + + A new header is also added to the changelog file ("NEWS.rst"), containing + the new version string and the current 'YYYY-MM-DD' date. + + All changes are committed, and an annotated git tag is generated. With the + --sign option, the tag is GPG-signed with the user's default key. + + Finally, the 'patch' part of the version string is bumped again, and a + pre-release suffix '.dev0' is appended to mark the opening of a new + development cycle. + + Links: + - http://semver.org/ + - https://www.python.org/dev/peps/pep-0440/ + - https://github.com/c4urself/bump2version + """ + + description = "update version strings for release" + + user_options = [ + ("major", None, "bump the first digit (incompatible API changes)"), + ("minor", None, "bump the second digit (new backward-compatible features)"), + ("sign", "s", "make a GPG-signed tag, using the default key"), + ("allow-dirty", None, "don't abort if working directory is dirty"), + ] + + changelog_name = "NEWS.rst" + version_RE = re.compile("^[0-9]+\.[0-9]+") + date_fmt = u"%Y-%m-%d" + header_fmt = u"%s (released %s)" + commit_message = "Release {new_version}" + tag_name = "{new_version}" + version_files = [ + "setup.cfg", + "setup.py", + "Lib/fontTools/__init__.py", + ] + + def initialize_options(self): + self.minor = False + self.major = False + self.sign = False + self.allow_dirty = False + + def finalize_options(self): + if all([self.major, self.minor]): + from distutils.errors import DistutilsOptionError + raise DistutilsOptionError("--major/--minor are mutually exclusive") + self.part = "major" if self.major else "minor" if self.minor else None + + def run(self): + if self.part is not None: + log.info("bumping '%s' version" % self.part) + self.bumpversion(self.part, commit=False) + release_version = self.bumpversion( + "release", commit=False, allow_dirty=True) + else: + log.info("stripping pre-release suffix") + release_version = self.bumpversion("release") + log.info(" version = %s" % release_version) + + changes = self.format_changelog(release_version) + + self.git_commit(release_version) + self.git_tag(release_version, changes, self.sign) + + log.info("bumping 'patch' version and pre-release suffix") + next_dev_version = self.bumpversion('patch', commit=True) + log.info(" version = %s" % next_dev_version) + + def git_commit(self, version): + """ Stage and commit all relevant version files, and format the commit + message with specified 'version' string. + """ + files = self.version_files + [self.changelog_name] + + log.info("committing changes") + for f in files: + log.info(" %s" % f) + if self.dry_run: + return + sp.check_call(["git", "add"] + files) + msg = self.commit_message.format(new_version=version) + sp.check_call(["git", "commit", "-m", msg], stdout=sp.PIPE) + + def git_tag(self, version, message, sign=False): + """ Create annotated git tag with given 'version' and 'message'. + Optionally 'sign' the tag with the user's GPG key. + """ + log.info("creating %s git tag '%s'" % ( + "signed" if sign else "annotated", version)) + if self.dry_run: + return + # create an annotated (or signed) tag from the new version + tag_opt = "-s" if sign else "-a" + tag_name = self.tag_name.format(new_version=version) + proc = sp.Popen( + ["git", "tag", tag_opt, "-F", "-", tag_name], stdin=sp.PIPE) + # use the latest changes from the changelog file as the tag message + tag_message = u"%s\n\n%s" % (tag_name, message) + proc.communicate(tag_message.encode('utf-8')) + if proc.returncode != 0: + sys.exit(proc.returncode) + + def bumpversion(self, part, commit=False, message=None, allow_dirty=None): + """ Run bumpversion.main() with the specified arguments, and return the + new computed version string (cf. 'bumpversion --help' for more info) + """ + import bumpversion + + args = ( + (['--verbose'] if self.verbose > 1 else []) + + (['--dry-run'] if self.dry_run else []) + + (['--allow-dirty'] if (allow_dirty or self.allow_dirty) else []) + + (['--commit'] if commit else ['--no-commit']) + + (['--message', message] if message is not None else []) + + ['--list', part] + ) + log.debug("$ bumpversion %s" % " ".join(a.replace(" ", "\\ ") for a in args)) + + with capture_logger("bumpversion.list") as out: + bumpversion.main(args) + + last_line = out.getvalue().splitlines()[-1] + new_version = last_line.replace("new_version=", "") + return new_version + + def format_changelog(self, version): + """ Write new header at beginning of changelog file with the specified + 'version' and the current date. + Return the changelog content for the current release. + """ + from datetime import datetime + + log.info("formatting changelog") + + changes = [] + with io.open(self.changelog_name, "r+", encoding="utf-8") as f: + for ln in f: + if self.version_RE.match(ln): + break + else: + changes.append(ln) + if not self.dry_run: + f.seek(0) + content = f.read() + date = datetime.today().strftime(self.date_fmt) + f.seek(0) + header = self.header_fmt % (version, date) + f.write(header + u"\n" + u"-"*len(header) + u"\n\n" + content) + + return u"".join(changes) + + +class PassCommand(Command): + """ This is used with Travis `dpl` tool so that it skips creating sdist + and wheel packages, but simply uploads to PyPI the files found in ./dist + folder, that were previously built inside the tox 'bdist' environment. + This ensures that the same files are uploaded to Github Releases and PyPI. + """ + + description = "do nothing" + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + pass + + +def find_data_files(manpath="share/man"): + """ Find FontTools's data_files (just man pages at this point). + + By default, we install man pages to "share/man" directory relative to the + base installation directory for data_files. The latter can be changed with + the --install-data option of 'setup.py install' sub-command. + + E.g., if the data files installation directory is "/usr", the default man + page installation directory will be "/usr/share/man". + + You can override this via the $FONTTOOLS_MANPATH environment variable. + + E.g., on some BSD systems man pages are installed to 'man' instead of + 'share/man'; you can export $FONTTOOLS_MANPATH variable just before + installing: + + $ FONTTOOLS_MANPATH="man" pip install -v . + [...] + running install_data + copying Doc/man/ttx.1 -> /usr/man/man1 + + When installing from PyPI, for this variable to have effect you need to + force pip to install from the source distribution instead of the wheel + package (otherwise setup.py is not run), by using the --no-binary option: + + $ FONTTOOLS_MANPATH="man" pip install --no-binary=fonttools fonttools + + Note that you can only override the base man path, i.e. without the + section number (man1, man3, etc.). The latter is always implied to be 1, + for "general commands". + """ + + # get base installation directory for man pages + manpagebase = os.environ.get('FONTTOOLS_MANPATH', convert_path(manpath)) + # all our man pages go to section 1 + manpagedir = pjoin(manpagebase, 'man1') + + manpages = [f for f in glob(pjoin('Doc', 'man', 'man1', '*.1')) if isfile(f)] + + data_files = [(manpagedir, manpages)] + return data_files + setup( - name = "fonttools", - version = "2.4", - description = "Tools to manipulate font files", - author = "Just van Rossum", - author_email = "just@letterror.com", - maintainer = "Just van Rossum", - maintainer_email = "just@letterror.com", - url = "http://fonttools.sourceforge.net/", - license = "OpenSource, BSD-style", - platforms = ["Any"], - long_description = long_description, - - packages = [ - "fontTools", - "fontTools.encodings", - "fontTools.misc", - "fontTools.pens", - "fontTools.ttLib", - "fontTools.ttLib.tables", - ], - package_dir = {'': 'Lib'}, - extra_path = 'FontTools', - scripts = ["Tools/ttx", "Tools/pyftsubset", "Tools/pyftinspect", "Tools/pyftmerge"], - cmdclass = {"build_ext": build_ext_optional}, - data_files = [('share/man/man1', ["Doc/ttx.1"])], - **classifiers - ) + name="fonttools", + version="3.28.0", + description="Tools to manipulate font files", + author="Just van Rossum", + author_email="just@letterror.com", + maintainer="Behdad Esfahbod", + maintainer_email="behdad@behdad.org", + url="http://github.com/fonttools/fonttools", + license="MIT", + platforms=["Any"], + long_description=long_description, + package_dir={'': 'Lib'}, + packages=find_packages("Lib"), + include_package_data=True, + data_files=find_data_files(), + setup_requires=pytest_runner + wheel + bumpversion, + tests_require=[ + 'pytest>=3.0', + ], + entry_points={ + 'console_scripts': [ + "fonttools = fontTools.__main__:main", + "ttx = fontTools.ttx:main", + "pyftsubset = fontTools.subset:main", + "pyftmerge = fontTools.merge:main", + "pyftinspect = fontTools.inspect:main" + ] + }, + cmdclass={ + "release": release, + 'pass': PassCommand, + }, + **classifiers +) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..cb1ab91 --- /dev/null +++ b/tox.ini @@ -0,0 +1,68 @@ +[tox] +envlist = py{27,36}-cov, htmlcov + +[testenv] +basepython = + py27: {env:TOXPYTHON:python2.7} + pypy: {env:TOXPYTHON:pypy} + py34: {env:TOXPYTHON:python3.4} + py35: {env:TOXPYTHON:python3.5} + py36: {env:TOXPYTHON:python3.6} +deps = + cov: coverage>=4.3 + pytest + -rrequirements.txt +install_command = + pip install -v {opts} {packages} +commands = + # run the test suite against the package installed inside tox env. + # We use parallel mode and then combine later so that coverage.py will take + # paths like .tox/py36/lib/python3.6/site-packages/fontTools and collapse + # them into Lib/fontTools. + cov: coverage run --parallel-mode -m pytest {posargs} + nocov: pytest {posargs} + +[testenv:htmlcov] +basepython = {env:TOXPYTHON:python3.6} +deps = + coverage>=4.3 +skip_install = true +commands = + coverage combine + coverage html + +[testenv:codecov] +passenv = * +basepython = {env:TOXPYTHON:python} +deps = + coverage>=4.3 + codecov +skip_install = true +ignore_outcome = true +commands = + coverage combine + codecov --env TOXENV + +[testenv:bdist] +basepython = {env:TOXPYTHON:python3.6} +deps = + pygments + docutils + setuptools + wheel +skip_install = true +install_command = + # make sure we use the latest setuptools and wheel + pip install --upgrade {opts} {packages} +whitelist_externals = + rm +commands = + # check metadata and rst long_description + python setup.py check --restructuredtext --strict + # clean up build/ and dist/ folders + rm -rf {toxinidir}/dist + python setup.py clean --all + # build sdist + python setup.py sdist --dist-dir {toxinidir}/dist + # build wheel from sdist + pip wheel -v --no-deps --no-index --wheel-dir {toxinidir}/dist --find-links {toxinidir}/dist fonttools -- GitLab