From d584daa8fdc71030f92ee665472d6c7cddd49283 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 13 Dec 2022 11:26:36 +0000 Subject: [PATCH] Blacken code --- Doc/source/conf.py | 33 +- Lib/fontTools/__main__.py | 49 +- Lib/fontTools/afmLib.py | 640 +- Lib/fontTools/agl.py | 251 +- Lib/fontTools/cffLib/__init__.py | 5721 +++++----- Lib/fontTools/cffLib/specializer.py | 1399 +-- Lib/fontTools/cffLib/width.py | 284 +- Lib/fontTools/colorLib/errors.py | 1 - Lib/fontTools/colorLib/table_builder.py | 4 +- Lib/fontTools/cu2qu/benchmark.py | 34 +- Lib/fontTools/cu2qu/cli.py | 47 +- Lib/fontTools/cu2qu/cu2qu.py | 161 +- Lib/fontTools/cu2qu/errors.py | 1 + Lib/fontTools/cu2qu/ufo.py | 74 +- Lib/fontTools/designspaceLib/__init__.py | 937 +- Lib/fontTools/encodings/MacRoman.py | 292 +- Lib/fontTools/encodings/StandardEncoding.py | 304 +- Lib/fontTools/encodings/codecs.py | 212 +- Lib/fontTools/feaLib/builder.py | 66 +- Lib/fontTools/feaLib/lexer.py | 2 +- Lib/fontTools/feaLib/lookupDebugInfo.py | 3 +- Lib/fontTools/feaLib/parser.py | 10 +- Lib/fontTools/merge/__init__.py | 288 +- Lib/fontTools/merge/__main__.py | 2 +- Lib/fontTools/merge/base.py | 109 +- Lib/fontTools/merge/cmap.py | 204 +- Lib/fontTools/merge/layout.py | 711 +- Lib/fontTools/merge/options.py | 140 +- Lib/fontTools/merge/tables.py | 501 +- Lib/fontTools/merge/unicode.py | 137 +- Lib/fontTools/merge/util.py | 164 +- Lib/fontTools/misc/arrayTools.py | 40 +- Lib/fontTools/misc/bezierTools.py | 2 +- Lib/fontTools/misc/classifyTools.py | 267 +- Lib/fontTools/misc/cliTools.py | 7 +- Lib/fontTools/misc/cython.py | 2 + Lib/fontTools/misc/dictTools.py | 55 +- Lib/fontTools/misc/eexec.py | 146 +- Lib/fontTools/misc/encodingTools.py | 123 +- Lib/fontTools/misc/etree.py | 7 +- Lib/fontTools/misc/filenames.py | 384 +- Lib/fontTools/misc/fixedTools.py | 314 +- Lib/fontTools/misc/loggingTools.py | 893 +- Lib/fontTools/misc/macCreatorType.py | 76 +- Lib/fontTools/misc/macRes.py | 356 +- Lib/fontTools/misc/plistlib/__init__.py | 8 +- Lib/fontTools/misc/psCharStrings.py | 2396 +++-- Lib/fontTools/misc/psLib.py | 642 +- Lib/fontTools/misc/psOperators.py | 899 +- Lib/fontTools/misc/roundTools.py | 56 +- Lib/fontTools/misc/sstruct.py | 228 +- Lib/fontTools/misc/symfont.py | 253 +- Lib/fontTools/misc/testTools.py | 21 +- Lib/fontTools/misc/textTools.py | 133 +- Lib/fontTools/misc/timeTools.py | 94 +- Lib/fontTools/misc/transform.py | 553 +- Lib/fontTools/misc/xmlReader.py | 289 +- Lib/fontTools/misc/xmlWriter.py | 322 +- Lib/fontTools/mtiLib/__init__.py | 2238 ++-- Lib/fontTools/mtiLib/__main__.py | 4 +- Lib/fontTools/otlLib/optimize/__main__.py | 4 +- Lib/fontTools/pens/areaPen.py | 79 +- Lib/fontTools/pens/basePen.py | 585 +- Lib/fontTools/pens/boundsPen.py | 142 +- Lib/fontTools/pens/cocoaPen.py | 28 +- Lib/fontTools/pens/cu2quPen.py | 66 +- Lib/fontTools/pens/filterPen.py | 5 +- Lib/fontTools/pens/hashPointPen.py | 4 +- Lib/fontTools/pens/momentsPen.py | 1336 ++- Lib/fontTools/pens/perimeterPen.py | 81 +- Lib/fontTools/pens/pointInsidePen.py | 308 +- Lib/fontTools/pens/pointPen.py | 822 +- Lib/fontTools/pens/qtPen.py | 32 +- Lib/fontTools/pens/quartzPen.py | 59 +- Lib/fontTools/pens/recordingPen.py | 232 +- Lib/fontTools/pens/reportLabPen.py | 107 +- Lib/fontTools/pens/reverseContourPen.py | 23 +- Lib/fontTools/pens/statisticsPen.py | 164 +- Lib/fontTools/pens/svgPathPen.py | 45 +- Lib/fontTools/pens/t2CharStringPen.py | 25 +- Lib/fontTools/pens/teePen.py | 74 +- Lib/fontTools/pens/transformPen.py | 165 +- Lib/fontTools/pens/wxPen.py | 32 +- Lib/fontTools/subset/__init__.py | 5020 +++++---- Lib/fontTools/subset/__main__.py | 2 +- Lib/fontTools/subset/cff.py | 846 +- Lib/fontTools/svgLib/path/__init__.py | 2 +- Lib/fontTools/svgLib/path/arc.py | 1 - Lib/fontTools/svgLib/path/parser.py | 54 +- Lib/fontTools/svgLib/path/shapes.py | 102 +- Lib/fontTools/t1Lib/__init__.py | 916 +- Lib/fontTools/ttLib/__init__.py | 137 +- Lib/fontTools/ttLib/macUtils.py | 78 +- Lib/fontTools/ttLib/sfnt.py | 901 +- Lib/fontTools/ttLib/standardGlyphOrder.py | 516 +- Lib/fontTools/ttLib/tables/B_A_S_E_.py | 2 +- .../ttLib/tables/BitmapGlyphMetrics.py | 49 +- Lib/fontTools/ttLib/tables/C_B_D_T_.py | 119 +- Lib/fontTools/ttLib/tables/C_B_L_C_.py | 3 +- Lib/fontTools/ttLib/tables/C_F_F_.py | 64 +- Lib/fontTools/ttLib/tables/C_F_F__2.py | 1 - Lib/fontTools/ttLib/tables/C_O_L_R_.py | 254 +- Lib/fontTools/ttLib/tables/C_P_A_L_.py | 491 +- Lib/fontTools/ttLib/tables/D_S_I_G_.py | 176 +- Lib/fontTools/ttLib/tables/DefaultTable.py | 72 +- Lib/fontTools/ttLib/tables/E_B_D_T_.py | 1253 +-- Lib/fontTools/ttLib/tables/E_B_L_C_.py | 1018 +- Lib/fontTools/ttLib/tables/F_F_T_M_.py | 48 +- Lib/fontTools/ttLib/tables/F__e_a_t.py | 97 +- Lib/fontTools/ttLib/tables/G_D_E_F_.py | 2 +- Lib/fontTools/ttLib/tables/G_M_A_P_.py | 190 +- Lib/fontTools/ttLib/tables/G_P_K_G_.py | 195 +- 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 | 128 +- Lib/fontTools/ttLib/tables/G__l_o_c.py | 51 +- Lib/fontTools/ttLib/tables/H_V_A_R_.py | 2 +- Lib/fontTools/ttLib/tables/J_S_T_F_.py | 2 +- Lib/fontTools/ttLib/tables/L_T_S_H_.py | 70 +- Lib/fontTools/ttLib/tables/M_A_T_H_.py | 2 +- Lib/fontTools/ttLib/tables/M_E_T_A_.py | 479 +- Lib/fontTools/ttLib/tables/M_V_A_R_.py | 2 +- Lib/fontTools/ttLib/tables/O_S_2f_2.py | 885 +- Lib/fontTools/ttLib/tables/S_I_N_G_.py | 126 +- Lib/fontTools/ttLib/tables/S_V_G_.py | 275 +- Lib/fontTools/ttLib/tables/S__i_l_f.py | 672 +- Lib/fontTools/ttLib/tables/S__i_l_l.py | 57 +- Lib/fontTools/ttLib/tables/T_S_I_B_.py | 3 +- Lib/fontTools/ttLib/tables/T_S_I_D_.py | 3 +- Lib/fontTools/ttLib/tables/T_S_I_J_.py | 3 +- Lib/fontTools/ttLib/tables/T_S_I_P_.py | 3 +- Lib/fontTools/ttLib/tables/T_S_I_S_.py | 3 +- Lib/fontTools/ttLib/tables/T_S_I_V_.py | 30 +- Lib/fontTools/ttLib/tables/T_S_I__0.py | 71 +- Lib/fontTools/ttLib/tables/T_S_I__1.py | 280 +- Lib/fontTools/ttLib/tables/T_S_I__2.py | 3 +- Lib/fontTools/ttLib/tables/T_S_I__3.py | 10 +- Lib/fontTools/ttLib/tables/T_S_I__5.py | 59 +- Lib/fontTools/ttLib/tables/T_T_F_A_.py | 3 +- Lib/fontTools/ttLib/tables/TupleVariation.py | 1354 +-- Lib/fontTools/ttLib/tables/V_D_M_X_.py | 385 +- Lib/fontTools/ttLib/tables/V_O_R_G_.py | 248 +- Lib/fontTools/ttLib/tables/V_V_A_R_.py | 2 +- Lib/fontTools/ttLib/tables/__init__.py | 181 +- Lib/fontTools/ttLib/tables/_a_n_k_r.py | 2 + Lib/fontTools/ttLib/tables/_a_v_a_r.py | 11 +- Lib/fontTools/ttLib/tables/_c_i_d_g.py | 20 +- Lib/fontTools/ttLib/tables/_c_m_a_p.py | 2557 ++--- Lib/fontTools/ttLib/tables/_c_v_a_r.py | 30 +- Lib/fontTools/ttLib/tables/_c_v_t.py | 66 +- Lib/fontTools/ttLib/tables/_f_e_a_t.py | 15 +- Lib/fontTools/ttLib/tables/_f_p_g_m.py | 73 +- Lib/fontTools/ttLib/tables/_f_v_a_r.py | 67 +- Lib/fontTools/ttLib/tables/_g_a_s_p.py | 76 +- Lib/fontTools/ttLib/tables/_g_l_y_f.py | 3279 +++--- Lib/fontTools/ttLib/tables/_g_v_a_r.py | 403 +- Lib/fontTools/ttLib/tables/_h_d_m_x.py | 185 +- Lib/fontTools/ttLib/tables/_h_e_a_d.py | 165 +- Lib/fontTools/ttLib/tables/_h_h_e_a.py | 169 +- Lib/fontTools/ttLib/tables/_h_m_t_x.py | 240 +- Lib/fontTools/ttLib/tables/_k_e_r_n.py | 475 +- Lib/fontTools/ttLib/tables/_l_c_a_r.py | 2 +- Lib/fontTools/ttLib/tables/_l_o_c_a.py | 93 +- Lib/fontTools/ttLib/tables/_l_t_a_g.py | 101 +- Lib/fontTools/ttLib/tables/_m_a_x_p.py | 206 +- Lib/fontTools/ttLib/tables/_m_e_t_a.py | 29 +- Lib/fontTools/ttLib/tables/_n_a_m_e.py | 1703 +-- Lib/fontTools/ttLib/tables/_p_o_s_t.py | 490 +- Lib/fontTools/ttLib/tables/_p_r_e_p.py | 3 +- Lib/fontTools/ttLib/tables/_s_b_i_x.py | 144 +- Lib/fontTools/ttLib/tables/_t_r_a_k.py | 443 +- Lib/fontTools/ttLib/tables/_v_h_e_a.py | 162 +- Lib/fontTools/ttLib/tables/_v_m_t_x.py | 9 +- Lib/fontTools/ttLib/tables/asciiTable.py | 29 +- Lib/fontTools/ttLib/tables/grUtils.py | 39 +- Lib/fontTools/ttLib/tables/otBase.py | 2441 ++--- Lib/fontTools/ttLib/tables/otConverters.py | 3136 +++--- Lib/fontTools/ttLib/tables/otData.py | 8113 ++++++++++---- Lib/fontTools/ttLib/tables/otTables.py | 3585 ++++--- Lib/fontTools/ttLib/tables/otTraverse.py | 1 - Lib/fontTools/ttLib/tables/sbixGlyph.py | 198 +- Lib/fontTools/ttLib/tables/sbixStrike.py | 253 +- Lib/fontTools/ttLib/tables/ttProgram.py | 1818 +++- Lib/fontTools/ttLib/ttCollection.py | 177 +- Lib/fontTools/ttLib/ttFont.py | 2015 ++-- Lib/fontTools/ttLib/woff2.py | 2661 ++--- Lib/fontTools/ttx.py | 550 +- Lib/fontTools/ufoLib/__init__.py | 4167 ++++---- Lib/fontTools/ufoLib/converters.py | 14 +- Lib/fontTools/ufoLib/errors.py | 2 - Lib/fontTools/ufoLib/filenames.py | 360 +- Lib/fontTools/ufoLib/glifLib.py | 3151 +++--- Lib/fontTools/ufoLib/kerning.py | 172 +- Lib/fontTools/ufoLib/validators.py | 1838 ++-- Lib/fontTools/unicode.py | 71 +- Lib/fontTools/unicodedata/Blocks.py | 756 +- Lib/fontTools/unicodedata/ScriptExtensions.py | 521 +- Lib/fontTools/unicodedata/Scripts.py | 3644 +++---- Lib/fontTools/unicodedata/__init__.py | 166 +- Lib/fontTools/varLib/__init__.py | 1881 ++-- Lib/fontTools/varLib/__main__.py | 4 +- Lib/fontTools/varLib/builder.py | 201 +- Lib/fontTools/varLib/cff.py | 1126 +- Lib/fontTools/varLib/featureVars.py | 130 +- Lib/fontTools/varLib/instancer/featureVars.py | 14 +- Lib/fontTools/varLib/interpolatable.py | 71 +- Lib/fontTools/varLib/interpolate_layout.py | 157 +- Lib/fontTools/varLib/iup.py | 761 +- Lib/fontTools/varLib/merger.py | 2536 ++--- Lib/fontTools/varLib/models.py | 82 +- Lib/fontTools/varLib/mutator.py | 822 +- Lib/fontTools/varLib/mvar.py | 76 +- Lib/fontTools/varLib/plot.py | 344 +- Lib/fontTools/varLib/varStore.py | 967 +- Lib/fontTools/voltLib/ast.py | 189 +- Lib/fontTools/voltLib/error.py | 2 - Lib/fontTools/voltLib/lexer.py | 22 +- Lib/fontTools/voltLib/parser.py | 147 +- MetaTools/buildTableList.py | 41 +- MetaTools/buildUCD.py | 70 +- MetaTools/roundTrip.py | 135 +- Snippets/checksum.py | 98 +- Snippets/cmap-format.py | 24 +- Snippets/dump_woff_metadata.py | 5 +- Snippets/fix-dflt-langsys.py | 26 +- Snippets/interpolate.py | 38 +- Snippets/layout-features.py | 78 +- Snippets/merge_woff_metadata.py | 8 +- Snippets/otf2ttf.py | 45 +- Snippets/rename-fonts.py | 8 +- Snippets/statShape.py | 32 +- Snippets/subset-fpgm.py | 51 +- Snippets/svg2glif.py | 94 +- Tests/afmLib/afmLib_test.py | 86 +- Tests/agl_test.py | 4 +- Tests/cffLib/cffLib_test.py | 35 +- Tests/cffLib/specializer_test.py | 783 +- Tests/cu2qu/cli_test.py | 26 +- Tests/cu2qu/cu2qu_test.py | 81 +- Tests/cu2qu/ufo_test.py | 103 +- Tests/designspaceLib/designspace_test.py | 340 +- Tests/designspaceLib/split_test.py | 10 +- Tests/encodings/codecs_test.py | 38 +- Tests/feaLib/ast_test.py | 1 + Tests/feaLib/builder_test.py | 5 +- Tests/feaLib/error_test.py | 1 + Tests/feaLib/lexer_test.py | 171 +- Tests/feaLib/parser_test.py | 4 +- Tests/fontBuilder/fontBuilder_test.py | 121 +- Tests/merge/merge_test.py | 355 +- Tests/misc/arrayTools_test.py | 38 +- Tests/misc/bezierTools_test.py | 171 +- Tests/misc/classifyTools_test.py | 37 +- Tests/misc/eexec_test.py | 4 +- Tests/misc/encodingTools_test.py | 42 +- Tests/misc/filenames_test.py | 223 +- Tests/misc/fixedTools_test.py | 60 +- Tests/misc/loggingTools_test.py | 73 +- Tests/misc/macRes_test.py | 132 +- Tests/misc/plistlib_test.py | 51 +- Tests/misc/psCharStrings_test.py | 121 +- Tests/misc/py23_test.py | 627 +- Tests/misc/testTools_test.py | 109 +- Tests/misc/textTools_test.py | 10 +- Tests/misc/timeTools_test.py | 12 +- Tests/misc/transform_test.py | 17 +- Tests/misc/treeTools_test.py | 2 +- Tests/misc/visitor_test.py | 1 + Tests/misc/xmlReader_test.py | 293 +- Tests/misc/xmlWriter_test.py | 220 +- Tests/mtiLib/mti_test.py | 530 +- Tests/otlLib/maxContextCalc_test.py | 27 +- Tests/otlLib/mock_builder_test.py | 8 +- Tests/pens/__init__.py | 9 +- Tests/pens/areaPen_test.py | 195 +- Tests/pens/basePen_test.py | 78 +- Tests/pens/boundsPen_test.py | 3 +- Tests/pens/cocoaPen_test.py | 13 +- Tests/pens/cu2quPen_test.py | 218 +- Tests/pens/perimeterPen_test.py | 195 +- Tests/pens/pointInsidePen_test.py | 190 +- Tests/pens/pointPen_test.py | 253 +- Tests/pens/quartzPen_test.py | 15 +- Tests/pens/reverseContourPen_test.py | 462 +- Tests/pens/t2CharStringPen_test.py | 225 +- Tests/pens/utils.py | 48 +- Tests/subset/subset_test.py | 509 +- Tests/svgLib/path/parser_test.py | 170 +- Tests/svgLib/path/path_test.py | 24 +- Tests/svgLib/path/shapes_test.py | 76 +- Tests/t1Lib/t1Lib_test.py | 311 +- Tests/ttLib/scaleUpem_test.py | 9 +- Tests/ttLib/tables/C_F_F__2_test.py | 22 +- Tests/ttLib/tables/C_F_F_test.py | 18 +- Tests/ttLib/tables/C_O_L_R_test.py | 64 +- Tests/ttLib/tables/C_P_A_L_test.py | 282 +- Tests/ttLib/tables/M_V_A_R_test.py | 149 +- Tests/ttLib/tables/O_S_2f_2_test.py | 99 +- Tests/ttLib/tables/S_T_A_T_test.py | 236 +- Tests/ttLib/tables/T_S_I__0_test.py | 44 +- Tests/ttLib/tables/T_S_I__1_test.py | 111 +- Tests/ttLib/tables/TupleVariation_test.py | 1843 ++-- Tests/ttLib/tables/_a_n_k_r_test.py | 142 +- Tests/ttLib/tables/_a_v_a_r_test.py | 46 +- Tests/ttLib/tables/_b_s_l_n_test.py | 135 +- Tests/ttLib/tables/_c_i_d_g_test.py | 70 +- Tests/ttLib/tables/_c_m_a_p_test.py | 290 +- Tests/ttLib/tables/_c_v_a_r_test.py | 61 +- Tests/ttLib/tables/_f_v_a_r_test.py | 153 +- Tests/ttLib/tables/_g_c_i_d_test.py | 68 +- Tests/ttLib/tables/_g_l_y_f_test.py | 247 +- Tests/ttLib/tables/_g_v_a_r_test.py | 282 +- Tests/ttLib/tables/_h_h_e_a_test.py | 140 +- Tests/ttLib/tables/_h_m_t_x_test.py | 109 +- Tests/ttLib/tables/_k_e_r_n_test.py | 264 +- Tests/ttLib/tables/_l_c_a_r_test.py | 88 +- Tests/ttLib/tables/_l_t_a_g_test.py | 100 +- Tests/ttLib/tables/_m_e_t_a_test.py | 64 +- Tests/ttLib/tables/_m_o_r_t_test.py | 99 +- Tests/ttLib/tables/_m_o_r_x_test.py | 871 +- Tests/ttLib/tables/_n_a_m_e_test.py | 1004 +- Tests/ttLib/tables/_o_p_b_d_test.py | 184 +- Tests/ttLib/tables/_p_r_o_p_test.py | 54 +- Tests/ttLib/tables/_t_r_a_k_test.py | 566 +- Tests/ttLib/tables/_v_h_e_a_test.py | 182 +- Tests/ttLib/tables/_v_m_t_x_test.py | 2 +- Tests/ttLib/tables/otBase_test.py | 4 +- Tests/ttLib/tables/otConverters_test.py | 382 +- Tests/ttLib/tables/otTables_test.py | 454 +- Tests/ttLib/tables/tables_test.py | 449 +- Tests/ttLib/tables/ttProgram_test.py | 117 +- Tests/ttLib/ttFont_test.py | 13 +- Tests/ttLib/ttVisitor_test.py | 1 - Tests/ttLib/woff2_test.py | 2365 +++-- Tests/ttx/ttx_test.py | 14 +- Tests/ufoLib/GLIF1_test.py | 902 +- Tests/ufoLib/GLIF2_test.py | 1364 +-- Tests/ufoLib/UFO1_test.py | 232 +- Tests/ufoLib/UFO2_test.py | 2946 +++--- Tests/ufoLib/UFO3_test.py | 9405 +++++++++-------- Tests/ufoLib/UFOConversion_test.py | 595 +- Tests/ufoLib/UFOZ_test.py | 8 +- Tests/ufoLib/filenames_test.py | 27 +- Tests/ufoLib/glifLib_test.py | 455 +- Tests/ufoLib/testSupport.py | 1226 ++- Tests/unicodedata_test.py | 314 +- Tests/varLib/builder_test.py | 191 +- Tests/varLib/featureVars_test.py | 71 +- Tests/varLib/instancer/instancer_test.py | 8 +- Tests/varLib/instancer/solver_test.py | 225 +- Tests/varLib/interpolatable_test.py | 26 +- Tests/varLib/interpolate_layout_test.py | 565 +- Tests/varLib/iup_test.py | 113 +- Tests/varLib/mutator_test.py | 88 +- Tests/varLib/varLib_test.py | 187 +- Tests/varLib/varStore_test.py | 2 +- Tests/voltLib/lexer_test.py | 15 +- Tests/voltLib/parser_test.py | 1219 ++- setup.py | 732 +- 359 files changed, 80867 insertions(+), 67305 deletions(-) diff --git a/Doc/source/conf.py b/Doc/source/conf.py index b95119dbb..982af8032 100644 --- a/Doc/source/conf.py +++ b/Doc/source/conf.py @@ -30,14 +30,17 @@ 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", "sphinx.ext.napoleon", "sphinx.ext.coverage", "sphinx.ext.autosectionlabel"] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.napoleon", + "sphinx.ext.coverage", + "sphinx.ext.autosectionlabel", +] autodoc_mock_imports = ["gtk", "reportlab"] -autodoc_default_options = { - 'members': True, - 'inherited-members': True -} +autodoc_default_options = {"members": True, "inherited-members": True} # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -52,9 +55,11 @@ source_suffix = ".rst" master_doc = "index" # General information about the project. -project = u"fontTools" -copyright = u"2020, Just van Rossum, Behdad Esfahbod, and the fontTools Authors. CC BY-SA 4.0" -author = u"Just van Rossum, Behdad Esfahbod, and the fontTools Authors" +project = "fontTools" +copyright = ( + "2020, Just van Rossum, Behdad Esfahbod, and the fontTools Authors. CC BY-SA 4.0" +) +author = "Just van Rossum, Behdad Esfahbod, and the fontTools Authors" # HTML page title html_title = "fontTools Documentation" @@ -64,9 +69,9 @@ html_title = "fontTools Documentation" # built documents. # # The short X.Y version. -version = u"4.0" +version = "4.0" # The full version, including alpha/beta/rc tags. -release = u"4.0" +release = "4.0" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -142,8 +147,8 @@ latex_documents = [ ( master_doc, "fontTools.tex", - u"fontTools Documentation", - u"Just van Rossum, Behdad Esfahbod et al.", + "fontTools Documentation", + "Just van Rossum, Behdad Esfahbod et al.", "manual", ) ] @@ -153,7 +158,7 @@ latex_documents = [ # 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)] +man_pages = [(master_doc, "fonttools", "fontTools Documentation", [author], 1)] # -- Options for Texinfo output ------------------------------------------- @@ -165,7 +170,7 @@ texinfo_documents = [ ( master_doc, "fontTools", - u"fontTools Documentation", + "fontTools Documentation", author, "fontTools", "A library for manipulating fonts, written in Python.", diff --git a/Lib/fontTools/__main__.py b/Lib/fontTools/__main__.py index 9b978aaa7..7c74ad3c8 100644 --- a/Lib/fontTools/__main__.py +++ b/Lib/fontTools/__main__.py @@ -2,33 +2,34 @@ import sys def main(args=None): - if args is None: - args = sys.argv[1:] + if args is None: + args = sys.argv[1:] - # TODO Handle library-wide options. Eg.: - # --unicodedata - # --verbose / other logging stuff + # 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? + # 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? - if len(sys.argv) < 2: - sys.argv.append("help") - if sys.argv[1] == "-h" or sys.argv[1] == "--help": - sys.argv[1] = "help" - mod = 'fontTools.'+sys.argv[1] - sys.argv[1] = sys.argv[0] + ' ' + sys.argv[1] - del sys.argv[0] + if len(sys.argv) < 2: + sys.argv.append("help") + if sys.argv[1] == "-h" or sys.argv[1] == "--help": + sys.argv[1] = "help" + 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__') + import runpy + + runpy.run_module(mod, run_name="__main__") -if __name__ == '__main__': - sys.exit(main()) +if __name__ == "__main__": + sys.exit(main()) diff --git a/Lib/fontTools/afmLib.py b/Lib/fontTools/afmLib.py index 49d995122..394b901ff 100644 --- a/Lib/fontTools/afmLib.py +++ b/Lib/fontTools/afmLib.py @@ -53,378 +53,386 @@ identifierRE = re.compile(r"^([A-Za-z]+).*") # regular expression to parse char lines charRE = re.compile( - r"(-?\d+)" # charnum - r"\s*;\s*WX\s+" # ; WX - r"(-?\d+)" # width - r"\s*;\s*N\s+" # ; N - r"([.A-Za-z0-9_]+)" # charname - r"\s*;\s*B\s+" # ; B - r"(-?\d+)" # left - r"\s+" - r"(-?\d+)" # bottom - r"\s+" - r"(-?\d+)" # right - r"\s+" - r"(-?\d+)" # top - r"\s*;\s*" # ; - ) + r"(-?\d+)" # charnum + r"\s*;\s*WX\s+" # ; WX + r"(-?\d+)" # width + r"\s*;\s*N\s+" # ; N + r"([.A-Za-z0-9_]+)" # charname + r"\s*;\s*B\s+" # ; B + r"(-?\d+)" # left + r"\s+" + r"(-?\d+)" # bottom + r"\s+" + r"(-?\d+)" # right + r"\s+" + r"(-?\d+)" # top + r"\s*;\s*" # ; +) # regular expression to parse kerning lines kernRE = re.compile( - r"([.A-Za-z0-9_]+)" # leftchar - r"\s+" - r"([.A-Za-z0-9_]+)" # rightchar - r"\s+" - r"(-?\d+)" # value - r"\s*" - ) + r"([.A-Za-z0-9_]+)" # leftchar + r"\s+" + r"([.A-Za-z0-9_]+)" # rightchar + r"\s+" + r"(-?\d+)" # value + r"\s*" +) # regular expressions to parse composite info lines of the form: # Aacute 2 ; PCC A 0 0 ; PCC acute 182 211 ; compositeRE = re.compile( - r"([.A-Za-z0-9_]+)" # char name - r"\s+" - r"(\d+)" # number of parts - r"\s*;\s*" - ) + r"([.A-Za-z0-9_]+)" r"\s+" r"(\d+)" r"\s*;\s*" # char name # number of parts +) componentRE = re.compile( - r"PCC\s+" # PPC - r"([.A-Za-z0-9_]+)" # base char name - r"\s+" - r"(-?\d+)" # x offset - r"\s+" - r"(-?\d+)" # y offset - r"\s*;\s*" - ) + r"PCC\s+" # PPC + r"([.A-Za-z0-9_]+)" # base char name + r"\s+" + r"(-?\d+)" # x offset + r"\s+" + r"(-?\d+)" # y offset + r"\s*;\s*" +) preferredAttributeOrder = [ - "FontName", - "FullName", - "FamilyName", - "Weight", - "ItalicAngle", - "IsFixedPitch", - "FontBBox", - "UnderlinePosition", - "UnderlineThickness", - "Version", - "Notice", - "EncodingScheme", - "CapHeight", - "XHeight", - "Ascender", - "Descender", + "FontName", + "FullName", + "FamilyName", + "Weight", + "ItalicAngle", + "IsFixedPitch", + "FontBBox", + "UnderlinePosition", + "UnderlineThickness", + "Version", + "Notice", + "EncodingScheme", + "CapHeight", + "XHeight", + "Ascender", + "Descender", ] class error(Exception): - pass + pass class AFM(object): - _attrs = None + _attrs = None - _keywords = ['StartFontMetrics', - 'EndFontMetrics', - 'StartCharMetrics', - 'EndCharMetrics', - 'StartKernData', - 'StartKernPairs', - 'EndKernPairs', - 'EndKernData', - 'StartComposites', - 'EndComposites', - ] + _keywords = [ + "StartFontMetrics", + "EndFontMetrics", + "StartCharMetrics", + "EndCharMetrics", + "StartKernData", + "StartKernPairs", + "EndKernPairs", + "EndKernData", + "StartComposites", + "EndComposites", + ] - def __init__(self, path=None): - """AFM file reader. + def __init__(self, path=None): + """AFM file reader. - Instantiating an object with a path name will cause the file to be opened, - read, and parsed. Alternatively the path can be left unspecified, and a - file can be parsed later with the :meth:`read` method.""" - self._attrs = {} - self._chars = {} - self._kerning = {} - self._index = {} - self._comments = [] - self._composites = {} - if path is not None: - self.read(path) + Instantiating an object with a path name will cause the file to be opened, + read, and parsed. Alternatively the path can be left unspecified, and a + file can be parsed later with the :meth:`read` method.""" + self._attrs = {} + self._chars = {} + self._kerning = {} + self._index = {} + self._comments = [] + self._composites = {} + if path is not None: + self.read(path) - def read(self, path): - """Opens, reads and parses a file.""" - lines = readlines(path) - for line in lines: - if not line.strip(): - continue - m = identifierRE.match(line) - if m is None: - raise error("syntax error in AFM file: " + repr(line)) + def read(self, path): + """Opens, reads and parses a file.""" + lines = readlines(path) + for line in lines: + if not line.strip(): + continue + 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() - if word in self._keywords: - continue - if word == "C": - self.parsechar(rest) - elif word == "KPX": - self.parsekernpair(rest) - elif word == "CC": - self.parsecomposite(rest) - else: - self.parseattr(word, rest) + pos = m.regs[1][1] + word = line[:pos] + rest = line[pos:].strip() + if word in self._keywords: + continue + if word == "C": + self.parsechar(rest) + elif word == "KPX": + self.parsekernpair(rest) + elif word == "CC": + self.parsecomposite(rest) + else: + self.parseattr(word, rest) - def parsechar(self, rest): - m = charRE.match(rest) - if m is None: - raise error("syntax error in AFM file: " + repr(rest)) - things = [] - for fr, to in m.regs[1:]: - things.append(rest[fr:to]) - charname = things[2] - 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 parsechar(self, rest): + m = charRE.match(rest) + if m is None: + raise error("syntax error in AFM file: " + repr(rest)) + things = [] + for fr, to in m.regs[1:]: + things.append(rest[fr:to]) + charname = things[2] + 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: - raise error("syntax error in AFM file: " + repr(rest)) - things = [] - for fr, to in m.regs[1:]: - things.append(rest[fr:to]) - leftchar, rightchar, value = things - value = int(value) - self._kerning[(leftchar, rightchar)] = value + def parsekernpair(self, rest): + m = kernRE.match(rest) + if m is None: + raise error("syntax error in AFM file: " + repr(rest)) + things = [] + for fr, to in m.regs[1:]: + things.append(rest[fr:to]) + 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()] - self._attrs[word] = l, b, r, t - elif word == "Comment": - self._comments.append(rest) - else: - try: - value = int(rest) - except (ValueError, OverflowError): - self._attrs[word] = rest - else: - self._attrs[word] = value + def parseattr(self, word, rest): + if word == "FontBBox": + l, b, r, t = [int(thing) for thing in rest.split()] + self._attrs[word] = l, b, r, t + elif word == "Comment": + self._comments.append(rest) + else: + try: + value = int(rest) + except (ValueError, OverflowError): + self._attrs[word] = rest + else: + self._attrs[word] = value - def parsecomposite(self, rest): - m = compositeRE.match(rest) - if m is None: - raise error("syntax error in AFM file: " + repr(rest)) - charname = m.group(1) - ncomponents = int(m.group(2)) - rest = rest[m.regs[0][1]:] - components = [] - while True: - m = componentRE.match(rest) - if m is None: - raise error("syntax error in AFM file: " + repr(rest)) - basechar = m.group(1) - xoffset = int(m.group(2)) - yoffset = int(m.group(3)) - components.append((basechar, xoffset, yoffset)) - rest = rest[m.regs[0][1]:] - if not rest: - break - assert len(components) == ncomponents - self._composites[charname] = components + def parsecomposite(self, rest): + m = compositeRE.match(rest) + if m is None: + raise error("syntax error in AFM file: " + repr(rest)) + charname = m.group(1) + ncomponents = int(m.group(2)) + rest = rest[m.regs[0][1] :] + components = [] + while True: + m = componentRE.match(rest) + if m is None: + raise error("syntax error in AFM file: " + repr(rest)) + basechar = m.group(1) + xoffset = int(m.group(2)) + yoffset = int(m.group(3)) + components.append((basechar, xoffset, yoffset)) + rest = rest[m.regs[0][1] :] + if not rest: + break + assert len(components) == ncomponents + self._composites[charname] = components - def write(self, path, sep='\r'): - """Writes out an AFM font to the given path.""" - import time - lines = [ "StartFontMetrics 2.0", - "Comment Generated by afmLib; at %s" % ( - time.strftime("%m/%d/%Y %H:%M:%S", - time.localtime(time.time())))] + def write(self, path, sep="\r"): + """Writes out an AFM font to the given path.""" + import time - # write comments, assuming (possibly wrongly!) they should - # all appear at the top - for comment in self._comments: - lines.append("Comment " + comment) + lines = [ + "StartFontMetrics 2.0", + "Comment Generated by afmLib; at %s" + % (time.strftime("%m/%d/%Y %H:%M:%S", time.localtime(time.time()))), + ] - # write attributes, first the ones we know about, in - # a preferred order - attrs = self._attrs - for attr in preferredAttributeOrder: - if attr in attrs: - value = attrs[attr] - if attr == "FontBBox": - value = "%s %s %s %s" % value - lines.append(attr + " " + str(value)) - # then write the attributes we don't know about, - # in alphabetical order - items = sorted(attrs.items()) - for attr, value in items: - if attr in preferredAttributeOrder: - continue - lines.append(attr + " " + str(value)) + # write comments, assuming (possibly wrongly!) they should + # all appear at the top + for comment in self._comments: + lines.append("Comment " + comment) - # write char metrics - lines.append("StartCharMetrics " + repr(len(self._chars))) - items = [(charnum, (charname, width, box)) for charname, (charnum, width, box) in self._chars.items()] + # write attributes, first the ones we know about, in + # a preferred order + attrs = self._attrs + for attr in preferredAttributeOrder: + if attr in attrs: + value = attrs[attr] + if attr == "FontBBox": + value = "%s %s %s %s" % value + lines.append(attr + " " + str(value)) + # then write the attributes we don't know about, + # in alphabetical order + items = sorted(attrs.items()) + for attr, value in items: + if attr in preferredAttributeOrder: + continue + lines.append(attr + " " + str(value)) - def myKey(a): - """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) + # write char metrics + lines.append("StartCharMetrics " + repr(len(self._chars))) + items = [ + (charnum, (charname, width, box)) + for charname, (charnum, width, box) in self._chars.items() + ] - 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") + def myKey(a): + """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 - # write kerning info - lines.append("StartKernData") - lines.append("StartKernPairs " + repr(len(self._kerning))) - items = sorted(self._kerning.items()) - for (leftchar, rightchar), value in items: - lines.append("KPX %s %s %d" % (leftchar, rightchar, value)) - lines.append("EndKernPairs") - lines.append("EndKernData") + items.sort(key=myKey) - if self._composites: - composites = sorted(self._composites.items()) - lines.append("StartComposites %s" % len(self._composites)) - for charname, components in composites: - line = "CC %s %s ;" % (charname, len(components)) - for basechar, xoffset, yoffset in components: - line = line + " PCC %s %s %s ;" % (basechar, xoffset, yoffset) - lines.append(line) - lines.append("EndComposites") + 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") - lines.append("EndFontMetrics") + # write kerning info + lines.append("StartKernData") + lines.append("StartKernPairs " + repr(len(self._kerning))) + items = sorted(self._kerning.items()) + for (leftchar, rightchar), value in items: + lines.append("KPX %s %s %d" % (leftchar, rightchar, value)) + lines.append("EndKernPairs") + lines.append("EndKernData") - writelines(path, lines, sep) + if self._composites: + composites = sorted(self._composites.items()) + lines.append("StartComposites %s" % len(self._composites)) + for charname, components in composites: + line = "CC %s %s ;" % (charname, len(components)) + for basechar, xoffset, yoffset in components: + line = line + " PCC %s %s %s ;" % (basechar, xoffset, yoffset) + lines.append(line) + lines.append("EndComposites") - def has_kernpair(self, pair): - """Returns `True` if the given glyph pair (specified as a tuple) exists - in the kerning dictionary.""" - return pair in self._kerning + lines.append("EndFontMetrics") - def kernpairs(self): - """Returns a list of all kern pairs in the kerning dictionary.""" - return list(self._kerning.keys()) + writelines(path, lines, sep) - def has_char(self, char): - """Returns `True` if the given glyph exists in the font.""" - return char in self._chars + def has_kernpair(self, pair): + """Returns `True` if the given glyph pair (specified as a tuple) exists + in the kerning dictionary.""" + return pair in self._kerning - def chars(self): - """Returns a list of all glyph names in the font.""" - return list(self._chars.keys()) + def kernpairs(self): + """Returns a list of all kern pairs in the kerning dictionary.""" + return list(self._kerning.keys()) - def comments(self): - """Returns all comments from the file.""" - return self._comments + def has_char(self, char): + """Returns `True` if the given glyph exists in the font.""" + return char in self._chars - def addComment(self, comment): - """Adds a new comment to the file.""" - self._comments.append(comment) + def chars(self): + """Returns a list of all glyph names in the font.""" + return list(self._chars.keys()) - def addComposite(self, glyphName, components): - """Specifies that the glyph `glyphName` is made up of the given components. - The components list should be of the following form:: + def comments(self): + """Returns all comments from the file.""" + return self._comments - [ - (glyphname, xOffset, yOffset), - ... - ] - - """ - self._composites[glyphName] = components + def addComment(self, comment): + """Adds a new comment to the file.""" + self._comments.append(comment) - def __getattr__(self, attr): - if attr in self._attrs: - return self._attrs[attr] - else: - raise AttributeError(attr) + def addComposite(self, glyphName, components): + """Specifies that the glyph `glyphName` is made up of the given components. + The components list should be of the following form:: - 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 + [ + (glyphname, xOffset, yOffset), + ... + ] - def __delattr__(self, attr): - # all attrs *not* starting with "_" are consider to be AFM keywords - if attr[:1] == "_": - try: - del self.__dict__[attr] - except KeyError: - raise AttributeError(attr) - else: - try: - del self._attrs[attr] - except KeyError: - raise AttributeError(attr) + """ + self._composites[glyphName] = components - def __getitem__(self, key): - if isinstance(key, tuple): - # key is a tuple, return the kernpair - return self._kerning[key] - else: - # return the metrics instead - return self._chars[key] + def __getattr__(self, attr): + if attr in self._attrs: + return self._attrs[attr] + else: + raise AttributeError(attr) - def __setitem__(self, key, value): - if isinstance(key, tuple): - # key is a tuple, set kernpair - self._kerning[key] = value - else: - # set char metrics - self._chars[key] = value + 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 __delitem__(self, key): - if isinstance(key, tuple): - # key is a tuple, del kernpair - del self._kerning[key] - else: - # del char metrics - del self._chars[key] + def __delattr__(self, attr): + # all attrs *not* starting with "_" are consider to be AFM keywords + if attr[:1] == "_": + try: + del self.__dict__[attr] + except KeyError: + raise AttributeError(attr) + else: + try: + del self._attrs[attr] + except KeyError: + raise AttributeError(attr) - def __repr__(self): - if hasattr(self, "FullName"): - return '' % self.FullName - else: - return '' % id(self) + def __getitem__(self, key): + if isinstance(key, tuple): + # key is a tuple, return the kernpair + return self._kerning[key] + else: + # return the metrics instead + return self._chars[key] + + def __setitem__(self, key, value): + if isinstance(key, tuple): + # key is a tuple, set kernpair + self._kerning[key] = value + else: + # set char metrics + self._chars[key] = value + + def __delitem__(self, key): + if isinstance(key, tuple): + # key is a tuple, del kernpair + del self._kerning[key] + else: + # del char metrics + del self._chars[key] + + def __repr__(self): + if hasattr(self, "FullName"): + return "" % self.FullName + else: + return "" % id(self) def readlines(path): - with open(path, "r", encoding="ascii") as f: - data = f.read() - return data.splitlines() + with open(path, "r", encoding="ascii") as f: + data = f.read() + return data.splitlines() -def writelines(path, lines, sep='\r'): - with open(path, "w", encoding="ascii", newline=sep) as f: - f.write("\n".join(lines) + "\n") + +def writelines(path, lines, sep="\r"): + with open(path, "w", encoding="ascii", newline=sep) as f: + f.write("\n".join(lines) + "\n") if __name__ == "__main__": - import EasyDialogs - path = EasyDialogs.AskFileForOpen() - if path: - afm = AFM(path) - char = 'A' - if afm.has_char(char): - print(afm[char]) # print charnum, width and boundingbox - pair = ('A', 'V') - if afm.has_kernpair(pair): - print(afm[pair]) # print kerning value for pair - print(afm.Version) # various other afm entries have become attributes - print(afm.Weight) - # afm.comments() returns a list of all Comment lines found in the AFM - print(afm.comments()) - #print afm.chars() - #print afm.kernpairs() - print(afm) - afm.write(path + ".muck") + import EasyDialogs + + path = EasyDialogs.AskFileForOpen() + if path: + afm = AFM(path) + char = "A" + if afm.has_char(char): + print(afm[char]) # print charnum, width and boundingbox + pair = ("A", "V") + if afm.has_kernpair(pair): + print(afm[pair]) # print kerning value for pair + print(afm.Version) # various other afm entries have become attributes + print(afm.Weight) + # afm.comments() returns a list of all Comment lines found in the AFM + print(afm.comments()) + # print afm.chars() + # print afm.kernpairs() + print(afm) + afm.write(path + ".muck") diff --git a/Lib/fontTools/agl.py b/Lib/fontTools/agl.py index cc286e42c..d6994628d 100644 --- a/Lib/fontTools/agl.py +++ b/Lib/fontTools/agl.py @@ -5059,174 +5059,175 @@ _aglfnText = """\ class AGLError(Exception): - pass + pass + LEGACY_AGL2UV = {} AGL2UV = {} UV2AGL = {} + def _builddicts(): - import re + import re - lines = _aglText.splitlines() + lines = _aglText.splitlines() - parseAGL_RE = re.compile("([A-Za-z0-9]+);((?:[0-9A-F]{4})(?: (?:[0-9A-F]{4}))*)$") + parseAGL_RE = re.compile("([A-Za-z0-9]+);((?:[0-9A-F]{4})(?: (?:[0-9A-F]{4}))*)$") - for line in lines: - if not line or line[:1] == '#': - continue - m = parseAGL_RE.match(line) - if not m: - raise AGLError("syntax error in glyphlist.txt: %s" % repr(line[:20])) - unicodes = m.group(2) - assert len(unicodes) % 5 == 4 - unicodes = [int(unicode, 16) for unicode in unicodes.split()] - glyphName = tostr(m.group(1)) - LEGACY_AGL2UV[glyphName] = unicodes + for line in lines: + if not line or line[:1] == "#": + continue + m = parseAGL_RE.match(line) + if not m: + raise AGLError("syntax error in glyphlist.txt: %s" % repr(line[:20])) + unicodes = m.group(2) + assert len(unicodes) % 5 == 4 + unicodes = [int(unicode, 16) for unicode in unicodes.split()] + glyphName = tostr(m.group(1)) + LEGACY_AGL2UV[glyphName] = unicodes - lines = _aglfnText.splitlines() + lines = _aglfnText.splitlines() - parseAGLFN_RE = re.compile("([0-9A-F]{4});([A-Za-z0-9]+);.*?$") + parseAGLFN_RE = re.compile("([0-9A-F]{4});([A-Za-z0-9]+);.*?$") + + for line in lines: + if not line or line[:1] == "#": + continue + m = parseAGLFN_RE.match(line) + if not m: + raise AGLError("syntax error in aglfn.txt: %s" % repr(line[:20])) + unicode = m.group(1) + assert len(unicode) == 4 + unicode = int(unicode, 16) + glyphName = tostr(m.group(2)) + AGL2UV[glyphName] = unicode + UV2AGL[unicode] = glyphName - for line in lines: - if not line or line[:1] == '#': - continue - m = parseAGLFN_RE.match(line) - if not m: - raise AGLError("syntax error in aglfn.txt: %s" % repr(line[:20])) - unicode = m.group(1) - assert len(unicode) == 4 - unicode = int(unicode, 16) - glyphName = tostr(m.group(2)) - AGL2UV[glyphName] = unicode - UV2AGL[unicode] = glyphName _builddicts() def toUnicode(glyph, isZapfDingbats=False): - """Convert glyph names to Unicode, such as ``'longs_t.oldstyle'`` --> ``u'ſt'`` + """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] + 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("_") + # 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) + # 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 + # 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. - uchars = LEGACY_AGL2UV.get(component) - if uchars: - return "".join(map(chr, uchars)) + # Otherwise, if the component is in AGL, then map it + # to the corresponding character in that list. + uchars = LEGACY_AGL2UV.get(component) + if uchars: + return "".join(map(chr, uchars)) - # 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 "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, 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 '' + # 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 + """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([chr(c) for c in chars]) + """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([chr(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 chr(value) - return None + """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 chr(value) + return None diff --git a/Lib/fontTools/cffLib/__init__.py b/Lib/fontTools/cffLib/__init__.py index bdca7d94e..be851e348 100644 --- a/Lib/fontTools/cffLib/__init__.py +++ b/Lib/fontTools/cffLib/__init__.py @@ -14,7 +14,14 @@ the demands of variable fonts. This module parses both original CFF and CFF2. from fontTools.misc import sstruct from fontTools.misc import psCharStrings from fontTools.misc.arrayTools import unionRect, intRect -from fontTools.misc.textTools import bytechr, byteord, bytesjoin, tobytes, tostr, safeEval +from fontTools.misc.textTools import ( + bytechr, + byteord, + bytesjoin, + tobytes, + tostr, + safeEval, +) from fontTools.ttLib import TTFont from fontTools.ttLib.tables.otBase import OTTableWriter from fontTools.ttLib.tables.otBase import OTTableReader @@ -39,2130 +46,2172 @@ maxStackLimit = 513 class StopHintCountEvent(Exception): - pass + pass class _DesubroutinizingT2Decompiler(psCharStrings.SimpleT2Decompiler): - stop_hintcount_ops = ("op_hintmask", "op_cntrmask", "op_rmoveto", "op_hmoveto", - "op_vmoveto") + stop_hintcount_ops = ( + "op_hintmask", + "op_cntrmask", + "op_rmoveto", + "op_hmoveto", + "op_vmoveto", + ) - def __init__(self, localSubrs, globalSubrs, private=None): - psCharStrings.SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, - private) + def __init__(self, localSubrs, globalSubrs, private=None): + psCharStrings.SimpleT2Decompiler.__init__( + self, localSubrs, globalSubrs, private + ) - def execute(self, charString): - self.need_hintcount = True # until proven otherwise - for op_name in self.stop_hintcount_ops: - setattr(self, op_name, self.stop_hint_count) + def execute(self, charString): + self.need_hintcount = True # until proven otherwise + for op_name in self.stop_hintcount_ops: + setattr(self, op_name, self.stop_hint_count) - if hasattr(charString, '_desubroutinized'): - # If a charstring has already been desubroutinized, we will still - # need to execute it if we need to count hints in order to - # compute the byte length for mask arguments, and haven't finished - # counting hints pairs. - if self.need_hintcount and self.callingStack: - try: - psCharStrings.SimpleT2Decompiler.execute(self, charString) - except StopHintCountEvent: - del self.callingStack[-1] - return + if hasattr(charString, "_desubroutinized"): + # If a charstring has already been desubroutinized, we will still + # need to execute it if we need to count hints in order to + # compute the byte length for mask arguments, and haven't finished + # counting hints pairs. + if self.need_hintcount and self.callingStack: + try: + psCharStrings.SimpleT2Decompiler.execute(self, charString) + except StopHintCountEvent: + del self.callingStack[-1] + return - 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 not self.private.in_cff2: - 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._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 not self.private.in_cff2: + 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 + charString._desubroutinized = desubroutinized + del charString._patches - 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_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_callgsubr(self, index): + subr = self.globalSubrs[self.operandStack[-1] + self.globalBias] + psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) + self.processSubr(index, subr) - def stop_hint_count(self, *args): - self.need_hintcount = False - for op_name in self.stop_hintcount_ops: - setattr(self, op_name, None) - cs = self.callingStack[-1] - if hasattr(cs, '_desubroutinized'): - raise StopHintCountEvent() + def stop_hint_count(self, *args): + self.need_hintcount = False + for op_name in self.stop_hintcount_ops: + setattr(self, op_name, None) + cs = self.callingStack[-1] + if hasattr(cs, "_desubroutinized"): + raise StopHintCountEvent() - def op_hintmask(self, index): - psCharStrings.SimpleT2Decompiler.op_hintmask(self, index) - if self.need_hintcount: - self.stop_hint_count() + def op_hintmask(self, index): + psCharStrings.SimpleT2Decompiler.op_hintmask(self, index) + if self.need_hintcount: + self.stop_hint_count() - def processSubr(self, index, subr): - cs = self.callingStack[-1] - if not hasattr(cs, '_desubroutinized'): - cs._patches.append((index, subr._desubroutinized)) + def processSubr(self, index, subr): + cs = self.callingStack[-1] + if not hasattr(cs, "_desubroutinized"): + cs._patches.append((index, subr._desubroutinized)) class CFFFontSet(object): - """A CFF font "file" can contain more than one font, although this is - extremely rare (and not allowed within OpenType fonts). + """A CFF font "file" can contain more than one font, although this is + extremely rare (and not allowed within OpenType fonts). - This class is the entry point for parsing a CFF table. To actually - manipulate the data inside the CFF font, you will want to access the - ``CFFFontSet``'s :class:`TopDict` object. To do this, a ``CFFFontSet`` - object can either be treated as a dictionary (with appropriate - ``keys()`` and ``values()`` methods) mapping font names to :class:`TopDict` - objects, or as a list. + This class is the entry point for parsing a CFF table. To actually + manipulate the data inside the CFF font, you will want to access the + ``CFFFontSet``'s :class:`TopDict` object. To do this, a ``CFFFontSet`` + object can either be treated as a dictionary (with appropriate + ``keys()`` and ``values()`` methods) mapping font names to :class:`TopDict` + objects, or as a list. - .. code:: python + .. code:: python - from fontTools import ttLib - tt = ttLib.TTFont("Tests/cffLib/data/LinLibertine_RBI.otf") - tt["CFF "].cff - # - tt["CFF "].cff[0] # Here's your actual font data - # - - """ + from fontTools import ttLib + tt = ttLib.TTFont("Tests/cffLib/data/LinLibertine_RBI.otf") + tt["CFF "].cff + # + tt["CFF "].cff[0] # Here's your actual font data + # - def decompile(self, file, otFont, isCFF2=None): - """Parse a binary CFF file into an internal representation. ``file`` - should be a file handle object. ``otFont`` is the top-level - :py:class:`fontTools.ttLib.ttFont.TTFont` object containing this CFF file. + """ - If ``isCFF2`` is passed and set to ``True`` or ``False``, then the - library makes an assertion that the CFF header is of the appropriate - version. - """ + def decompile(self, file, otFont, isCFF2=None): + """Parse a binary CFF file into an internal representation. ``file`` + should be a file handle object. ``otFont`` is the top-level + :py:class:`fontTools.ttLib.ttFont.TTFont` object containing this CFF file. - 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 + If ``isCFF2`` is passed and set to ``True`` or ``False``, then the + library makes an assertion that the CFF header is of the appropriate + version. + """ - def __len__(self): - return len(self.fontNames) + 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 keys(self): - return list(self.fontNames) + def __len__(self): + return len(self.fontNames) - def values(self): - return self.topDictIndex + def keys(self): + return list(self.fontNames) - 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, str): - name = nameOrIndex - try: - index = self.fontNames.index(name) - except ValueError: - raise KeyError(nameOrIndex) - else: - raise TypeError(nameOrIndex) - return self.topDictIndex[index] + def values(self): + return self.topDictIndex - def compile(self, file, otFont, isCFF2=None): - """Write the object back into binary representation onto the given file. - ``file`` should be a file handle object. ``otFont`` is the top-level - :py:class:`fontTools.ttLib.ttFont.TTFont` object containing this CFF file. + 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, str): + name = nameOrIndex + try: + index = self.fontNames.index(name) + except ValueError: + raise KeyError(nameOrIndex) + else: + raise TypeError(nameOrIndex) + return self.topDictIndex[index] - If ``isCFF2`` is passed and set to ``True`` or ``False``, then the - library makes an assertion that the CFF header is of the appropriate - version. - """ - 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 + def compile(self, file, otFont, isCFF2=None): + """Write the object back into binary representation onto the given file. + ``file`` should be a file handle object. ``otFont`` is the top-level + :py:class:`fontTools.ttLib.ttFont.TTFont` object containing this CFF file. - if otFont.recalcBBoxes and not isCFF2: - for topDict in self.topDictIndex: - topDict.recalcFontBBox() + If ``isCFF2`` is passed and set to ``True`` or ``False``, then the + library makes an assertion that the CFF header is of the appropriate + version. + """ + 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 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) - if not isCFF2: - writer.add(strings.getCompiler()) - writer.add(self.GlobalSubrs.getCompiler(strings, self, isCFF2=isCFF2)) + if otFont.recalcBBoxes and not isCFF2: + for topDict in self.topDictIndex: + topDict.recalcFontBBox() - for topDict in self.topDictIndex: - if not hasattr(topDict, "charset") or topDict.charset is None: - charset = otFont.getGlyphOrder() - topDict.charset = charset - children = topCompiler.getChildren(strings) - for child in children: - writer.add(child) + 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) + if not isCFF2: + writer.add(strings.getCompiler()) + writer.add(self.GlobalSubrs.getCompiler(strings, self, isCFF2=isCFF2)) - writer.toFile(file) + for topDict in self.topDictIndex: + if not hasattr(topDict, "charset") or topDict.charset is None: + charset = otFont.getGlyphOrder() + topDict.charset = charset + children = topCompiler.getChildren(strings) + for child in children: + writer.add(child) - def toXML(self, xmlWriter): - """Write the object into XML representation onto the given - :class:`fontTools.misc.xmlWriter.XMLWriter`. + writer.toFile(file) - .. code:: python + def toXML(self, xmlWriter): + """Write the object into XML representation onto the given + :class:`fontTools.misc.xmlWriter.XMLWriter`. - writer = xmlWriter.XMLWriter(sys.stdout) - tt["CFF "].cff.toXML(writer) + .. code:: python - """ + writer = xmlWriter.XMLWriter(sys.stdout) + tt["CFF "].cff.toXML(writer) - 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) - xmlWriter.endtag("CFFFont") - xmlWriter.newline() - xmlWriter.newline() - xmlWriter.begintag("GlobalSubrs") - xmlWriter.newline() - self.GlobalSubrs.toXML(xmlWriter) - xmlWriter.endtag("GlobalSubrs") - xmlWriter.newline() + """ - def fromXML(self, name, attrs, content, otFont=None): - """Reads data from the XML element into the ``CFFFontSet`` object.""" - self.otFont = otFont + 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) + xmlWriter.endtag("CFFFont") + xmlWriter.newline() + xmlWriter.newline() + xmlWriter.begintag("GlobalSubrs") + xmlWriter.newline() + self.GlobalSubrs.toXML(xmlWriter) + xmlWriter.endtag("GlobalSubrs") + xmlWriter.newline() - # 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 + def fromXML(self, name, attrs, content, otFont=None): + """Reads data from the XML element into the ``CFFFontSet`` object.""" + self.otFont = otFont - if name == "CFFFont": - 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) - self.topDictIndex.append(topDict) - for element in content: - if isinstance(element, str): - continue - name, attrs, content = element - topDict.fromXML(name, attrs, content) + # 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 - if hasattr(topDict, "VarStore") and topDict.FDArray[0].vstore is None: - fdArray = topDict.FDArray - for fontDict in fdArray: - if hasattr(fontDict, "Private"): - fontDict.Private.vstore = topDict.VarStore + if name == "CFFFont": + 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) + self.topDictIndex.append(topDict) + for element in content: + if isinstance(element, str): + continue + 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, str): - continue - name, attrs, content = element - 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']) + if hasattr(topDict, "VarStore") and topDict.FDArray[0].vstore is None: + fdArray = topDict.FDArray + for fontDict in fdArray: + if hasattr(fontDict, "Private"): + fontDict.Private.vstore = topDict.VarStore - def convertCFFToCFF2(self, otFont): - """Converts this object from CFF format to CFF2 format. This conversion - is done 'in-place'. The conversion cannot be reversed. + elif name == "GlobalSubrs": + subrCharStringClass = psCharStrings.T2CharString + if not hasattr(self, "GlobalSubrs"): + self.GlobalSubrs = GlobalSubrsIndex() + for element in content: + if isinstance(element, str): + continue + name, attrs, content = element + 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"]) - This assumes a decompiled CFF table. (i.e. that the object has been - filled via :meth:`decompile`.)""" - self.major = 2 - cff2GetGlyphOrder = self.otFont.getGlyphOrder - topDictData = TopDictIndex(None, cff2GetGlyphOrder) - 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) + def convertCFFToCFF2(self, otFont): + """Converts this object from CFF format to CFF2 format. This conversion + is done 'in-place'. The conversion cannot be reversed. - 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) + This assumes a decompiled CFF table. (i.e. that the object has been + filled via :meth:`decompile`.)""" + self.major = 2 + cff2GetGlyphOrder = self.otFont.getGlyphOrder + topDictData = TopDictIndex(None, cff2GetGlyphOrder) + 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) - 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) + 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) - def desubroutinize(self): - for fontName in self.fontNames: - font = self[fontName] - cs = font.CharStrings - for g in font.charset: - c, _ = cs.getItemAndSelector(g) - c.decompile() - subrs = getattr(c.private, "Subrs", []) - decompiler = _DesubroutinizingT2Decompiler(subrs, c.globalSubrs, c.private) - decompiler.execute(c) - c.program = c._desubroutinized - del c._desubroutinized - # Delete all the local subrs - if hasattr(font, 'FDArray'): - for fd in font.FDArray: - pd = fd.Private - if hasattr(pd, 'Subrs'): - del pd.Subrs - if 'Subrs' in pd.rawDict: - del pd.rawDict['Subrs'] - else: - pd = font.Private - if hasattr(pd, 'Subrs'): - del pd.Subrs - if 'Subrs' in pd.rawDict: - del pd.rawDict['Subrs'] - # as well as the global subrs - self.GlobalSubrs.clear() + 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) + + def desubroutinize(self): + for fontName in self.fontNames: + font = self[fontName] + cs = font.CharStrings + for g in font.charset: + c, _ = cs.getItemAndSelector(g) + c.decompile() + subrs = getattr(c.private, "Subrs", []) + decompiler = _DesubroutinizingT2Decompiler( + subrs, c.globalSubrs, c.private + ) + decompiler.execute(c) + c.program = c._desubroutinized + del c._desubroutinized + # Delete all the local subrs + if hasattr(font, "FDArray"): + for fd in font.FDArray: + pd = fd.Private + if hasattr(pd, "Subrs"): + del pd.Subrs + if "Subrs" in pd.rawDict: + del pd.rawDict["Subrs"] + else: + pd = font.Private + if hasattr(pd, "Subrs"): + del pd.Subrs + if "Subrs" in pd.rawDict: + del pd.rawDict["Subrs"] + # as well as the global subrs + self.GlobalSubrs.clear() class CFFWriter(object): - """Helper class for serializing CFF data to binary. Used by - :meth:`CFFFontSet.compile`.""" - def __init__(self, isCFF2): - self.data = [] - self.isCFF2 = isCFF2 + """Helper class for serializing CFF data to binary. Used by + :meth:`CFFFontSet.compile`.""" - def add(self, table): - self.data.append(table) + def __init__(self, isCFF2): + self.data = [] + self.isCFF2 = isCFF2 - def toFile(self, file): - lastPosList = None - count = 1 - while True: - 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"): - item.setPos(pos, endPos) - pos = endPos - posList.append(pos) - if posList == lastPosList: - break - lastPosList = posList - 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"): - item.toFile(file) - else: - file.write(item) - posList.append(file.tell() - begin) - assert posList == lastPosList + def add(self, table): + self.data.append(table) + + def toFile(self, file): + lastPosList = None + count = 1 + while True: + 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"): + item.setPos(pos, endPos) + pos = endPos + posList.append(pos) + if posList == lastPosList: + break + lastPosList = posList + 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"): + item.toFile(file) + else: + file.write(item) + posList.append(file.tell() - begin) + assert posList == lastPosList def calcOffSize(largestOffset): - if largestOffset < 0x100: - offSize = 1 - elif largestOffset < 0x10000: - offSize = 2 - elif largestOffset < 0x1000000: - offSize = 3 - else: - offSize = 4 - return offSize + if largestOffset < 0x100: + offSize = 1 + elif largestOffset < 0x10000: + offSize = 2 + elif largestOffset < 0x1000000: + offSize = 3 + else: + offSize = 4 + return offSize class IndexCompiler(object): - """Base class for writing CFF `INDEX data `_ - to binary.""" + """Base class for writing CFF `INDEX data `_ + to binary.""" - 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 __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 getItems(self, items, strings): + return items - def getOffsets(self): - # 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 getOffsets(self): + # 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): - if self.isCFF2: - countSize = 4 - else: - countSize = 2 + def getDataLength(self): + 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 + if self.items: + lastOffset = self.getOffsets()[-1] + offSize = calcOffSize(lastOffset) + dataLength = ( + countSize + + 1 # count + + (len(self.items) + 1) * offSize # offSize + + lastOffset # the offsets + - 1 # size of object data + ) + else: + # count. For empty INDEX tables, this is the only entry. + dataLength = countSize - return dataLength + return dataLength - def toFile(self, file): - offsets = self.getOffsets() - 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) + def toFile(self, file): + offsets = self.getOffsets() + 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 + def getItems(self, items, strings): + return items.strings class TopDictIndexCompiler(IndexCompiler): - """Helper class for writing the TopDict to binary.""" + """Helper class for writing the TopDict to binary.""" - def getItems(self, items, strings): - out = [] - for item in items: - out.append(item.getCompiler(strings, self)) - return out + 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 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 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 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) + def toFile(self, file): + if self.isCFF2: + self.items[0].toFile(file) + else: + super(TopDictIndexCompiler, self).toFile(file) class FDArrayIndexCompiler(IndexCompiler): - """Helper class for writing the - `Font DICT INDEX `_ - to binary.""" + """Helper class for writing the + `Font DICT INDEX `_ + to binary.""" - def getItems(self, items, strings): - out = [] - for item in items: - out.append(item.getCompiler(strings, self)) - return out + 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: - children.extend(fontDict.getChildren(strings)) - return children + def getChildren(self, strings): + children = [] + for fontDict in self.items: + children.extend(fontDict.getChildren(strings)) + return children - def toFile(self, file): - offsets = self.getOffsets() - if self.isCFF2: - writeCard32(file, len(self.items)) - else: - 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(item) + def toFile(self, file): + offsets = self.getOffsets() + if self.isCFF2: + writeCard32(file, len(self.items)) + else: + 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(item) - def setPos(self, pos, endPos): - self.parent.rawDict["FDArray"] = pos + def setPos(self, pos, endPos): + self.parent.rawDict["FDArray"] = pos class GlobalSubrsCompiler(IndexCompiler): - """Helper class for writing the `global subroutine INDEX `_ - to binary.""" + """Helper class for writing the `global subroutine INDEX `_ + to binary.""" - def getItems(self, items, strings): - out = [] - for cs in items: - cs.compile(self.isCFF2) - out.append(cs.bytecode) - return out + def getItems(self, items, strings): + out = [] + for cs in items: + cs.compile(self.isCFF2) + out.append(cs.bytecode) + return out class SubrsCompiler(GlobalSubrsCompiler): - """Helper class for writing the `local subroutine INDEX `_ - to binary.""" - - def setPos(self, pos, endPos): - offset = pos - self.parent.pos - self.parent.rawDict["Subrs"] = offset + """Helper class for writing the `local subroutine INDEX `_ + to binary.""" + + def setPos(self, pos, endPos): + offset = pos - self.parent.pos + self.parent.rawDict["Subrs"] = offset class CharStringsCompiler(GlobalSubrsCompiler): - """Helper class for writing the `CharStrings INDEX `_ - to binary.""" - def getItems(self, items, strings): - out = [] - for cs in items: - cs.compile(self.isCFF2) - out.append(cs.bytecode) - return out + """Helper class for writing the `CharStrings INDEX `_ + to binary.""" - def setPos(self, pos, endPos): - self.parent.rawDict["CharStrings"] = pos + 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 (an array of - variable-sized objects). `Index` items can be addressed and set using - Python list indexing.""" + """This class represents what the CFF spec calls an INDEX (an array of + variable-sized objects). `Index` items can be addressed and set using + Python list indexing.""" - compilerClass = IndexCompiler + compilerClass = IndexCompiler - def __init__(self, file=None, isCFF2=None): - assert (isCFF2 is None) == (file is None) - self.items = [] - name = self.__class__.__name__ - if file is None: - return - self._isCFF2 = isCFF2 - log.log(DEBUG, "loading %s at %s", name, file.tell()) - self.file = file - if isCFF2: - count = readCard32(file) - else: - count = readCard16(file) - if count == 0: - return - self.items = [None] * count - offSize = readCard8(file) - 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): - 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 - log.log(DEBUG, " end of %s at %s", name, file.tell()) + def __init__(self, file=None, isCFF2=None): + assert (isCFF2 is None) == (file is None) + self.items = [] + name = self.__class__.__name__ + if file is None: + return + self._isCFF2 = isCFF2 + log.log(DEBUG, "loading %s at %s", name, file.tell()) + self.file = file + if isCFF2: + count = readCard32(file) + else: + count = readCard16(file) + if count == 0: + return + self.items = [None] * count + offSize = readCard8(file) + 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): + 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 + log.log(DEBUG, " end of %s at %s", name, file.tell()) - def __len__(self): - return len(self.items) + 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] - file = self.file - file.seek(offset) - data = file.read(size) - assert len(data) == size - item = self.produceItem(index, data, file, offset) - self.items[index] = item - return item + 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] + file = self.file + file.seek(offset) + data = file.read(size) + assert len(data) == size + item = self.produceItem(index, data, file, offset) + self.items[index] = item + return item - def __setitem__(self, index, item): - self.items[index] = item + def __setitem__(self, index, item): + self.items[index] = item - def produceItem(self, index, data, file, offset): - return data + def produceItem(self, index, data, file, offset): + return data - def append(self, item): - """Add an item to an INDEX.""" - self.items.append(item) + def append(self, item): + """Add an item to an INDEX.""" + self.items.append(item) - def getCompiler(self, strings, parent, isCFF2=None): - return self.compilerClass(self, strings, parent, isCFF2=isCFF2) + def getCompiler(self, strings, parent, isCFF2=None): + return self.compilerClass(self, strings, parent, isCFF2=isCFF2) - def clear(self): - """Empty the INDEX.""" - del self.items[:] + def clear(self): + """Empty the INDEX.""" + del self.items[:] class GlobalSubrsIndex(Index): - """This index contains all the global subroutines in the font. A global - subroutine is a set of ``CharString`` data which is accessible to any - glyph in the font, and are used to store repeated instructions - for - example, components may be encoded as global subroutines, but so could - hinting instructions. + """This index contains all the global subroutines in the font. A global + subroutine is a set of ``CharString`` data which is accessible to any + glyph in the font, and are used to store repeated instructions - for + example, components may be encoded as global subroutines, but so could + hinting instructions. - Remember that when interpreting a ``callgsubr`` instruction (or indeed - a ``callsubr`` instruction) that you will need to add the "subroutine - number bias" to number given: + Remember that when interpreting a ``callgsubr`` instruction (or indeed + a ``callsubr`` instruction) that you will need to add the "subroutine + number bias" to number given: - .. code:: python + .. code:: python - tt = ttLib.TTFont("Almendra-Bold.otf") - u = tt["CFF "].cff[0].CharStrings["udieresis"] - u.decompile() + tt = ttLib.TTFont("Almendra-Bold.otf") + u = tt["CFF "].cff[0].CharStrings["udieresis"] + u.decompile() - u.toXML(XMLWriter(sys.stdout)) - # - # -64 callgsubr <-- Subroutine which implements the dieresis mark - # + u.toXML(XMLWriter(sys.stdout)) + # + # -64 callgsubr <-- Subroutine which implements the dieresis mark + # - tt["CFF "].cff[0].GlobalSubrs[-64] # <-- WRONG - # + tt["CFF "].cff[0].GlobalSubrs[-64] # <-- WRONG + # - tt["CFF "].cff[0].GlobalSubrs[-64 + 107] # <-- RIGHT - # + tt["CFF "].cff[0].GlobalSubrs[-64 + 107] # <-- RIGHT + # - ("The bias applied depends on the number of subrs (gsubrs). If the number of - subrs (gsubrs) is less than 1240, the bias is 107. Otherwise if it is less - than 33900, it is 1131; otherwise it is 32768.", - `Subroutine Operators `) - """ + ("The bias applied depends on the number of subrs (gsubrs). If the number of + subrs (gsubrs) is less than 1240, the bias is 107. Otherwise if it is less + than 33900, it is 1131; otherwise it is 32768.", + `Subroutine Operators `) + """ - compilerClass = GlobalSubrsCompiler - subrClass = psCharStrings.T2CharString - charStringClass = psCharStrings.T2CharString + compilerClass = GlobalSubrsCompiler + 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 __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): - if self.private is not None: - private = self.private - elif hasattr(self, 'fdArray') and self.fdArray is not None: - 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 self.subrClass(data, private=private, globalSubrs=self.globalSubrs) + 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: + 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 self.subrClass(data, private=private, globalSubrs=self.globalSubrs) - def toXML(self, xmlWriter): - """Write the subroutines index into XML representation onto the given - :class:`fontTools.misc.xmlWriter.XMLWriter`. + def toXML(self, xmlWriter): + """Write the subroutines index into XML representation onto the given + :class:`fontTools.misc.xmlWriter.XMLWriter`. - .. code:: python + .. code:: python - writer = xmlWriter.XMLWriter(sys.stdout) - tt["CFF "].cff[0].GlobalSubrs.toXML(writer) + writer = xmlWriter.XMLWriter(sys.stdout) + tt["CFF "].cff[0].GlobalSubrs.toXML(writer) - """ - 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] - if subr.needsDecompilation(): - xmlWriter.begintag("CharString", index=i, raw=1) - else: - xmlWriter.begintag("CharString", index=i) - xmlWriter.newline() - subr.toXML(xmlWriter) - xmlWriter.endtag("CharString") - xmlWriter.newline() + """ + 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] + if subr.needsDecompilation(): + xmlWriter.begintag("CharString", index=i, raw=1) + else: + xmlWriter.begintag("CharString", index=i) + xmlWriter.newline() + subr.toXML(xmlWriter) + xmlWriter.endtag("CharString") + xmlWriter.newline() - def fromXML(self, name, attrs, content): - if name != "CharString": - return - subr = self.subrClass() - subr.fromXML(name, attrs, content) - self.append(subr) + def fromXML(self, name, attrs, content): + if name != "CharString": + return + subr = self.subrClass() + subr.fromXML(name, attrs, content) + self.append(subr) - def getItemAndSelector(self, index): - sel = None - if hasattr(self, 'fdSelect'): - sel = self.fdSelect[index] - return self[index], sel + def getItemAndSelector(self, index): + sel = None + if hasattr(self, "fdSelect"): + sel = self.fdSelect[index] + return self[index], sel class SubrsIndex(GlobalSubrsIndex): - """This index contains a glyph's local subroutines. A local subroutine is a - private set of ``CharString`` data which is accessible only to the glyph to - which the index is attached.""" + """This index contains a glyph's local subroutines. A local subroutine is a + private set of ``CharString`` data which is accessible only to the glyph to + which the index is attached.""" - compilerClass = SubrsCompiler + compilerClass = SubrsCompiler class TopDictIndex(Index): - """This index represents the array of ``TopDict`` structures in the font - (again, usually only one entry is present). Hence the following calls are - equivalent: + """This index represents the array of ``TopDict`` structures in the font + (again, usually only one entry is present). Hence the following calls are + equivalent: - .. code:: python + .. code:: python - tt["CFF "].cff[0] - # - tt["CFF "].cff.topDictIndex[0] - # + tt["CFF "].cff[0] + # + tt["CFF "].cff.topDictIndex[0] + # - """ + """ - compilerClass = TopDictIndexCompiler + compilerClass = TopDictIndexCompiler - 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 __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 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): - for i in range(len(self)): - xmlWriter.begintag("FontDict", index=i) - xmlWriter.newline() - self[i].toXML(xmlWriter) - xmlWriter.endtag("FontDict") - xmlWriter.newline() + 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() class FDArrayIndex(Index): - compilerClass = FDArrayIndexCompiler + 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 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 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 - fontDict = FontDict() - for element in content: - if isinstance(element, str): - continue - name, attrs, content = element - fontDict.fromXML(name, attrs, content) - self.append(fontDict) + def fromXML(self, name, attrs, content): + if name != "FontDict": + return + fontDict = FontDict() + for element in content: + if isinstance(element, str): + continue + name, attrs, content = element + fontDict.fromXML(name, attrs, content) + self.append(fontDict) 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 __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: + # 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 decompile(self): - if self.file: - # 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 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 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 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 __len__(self): - return len(self.data) - - def getNumRegions(self, vsIndex): - if vsIndex is None: - vsIndex = 0 - varData = self.otVarStore.VarData[vsIndex] - numRegions = varData.VarRegionCount - return numRegions + def getNumRegions(self, vsIndex): + if vsIndex is None: + vsIndex = 0 + 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) + if self.format == 0: + from array import array - def __init__(self, file=None, numGlyphs=None, format=None): - if file: - # read data in from file - self.format = readCard8(file) - if self.format == 0: - from array import array - self.gidArray = array("B", file.read(numGlyphs)).tolist() - elif self.format == 3: - gidArray = [None] * numGlyphs - nRanges = readCard16(file) - fd = None - prev = None - for i in range(nRanges): - first = readCard16(file) - if prev is not None: - for glyphID in range(prev, first): - gidArray[glyphID] = fd - prev = first - fd = readCard8(file) - if prev is not None: - first = readCard16(file) - for glyphID in range(prev, first): - gidArray[glyphID] = fd - self.gidArray = gidArray - elif self.format == 4: - gidArray = [None] * numGlyphs - nRanges = readCard32(file) - fd = None - prev = None - for i in range(nRanges): - first = readCard32(file) - if prev is not None: - for glyphID in range(prev, first): - gidArray[glyphID] = fd - prev = first - fd = readCard16(file) - if prev is not None: - first = readCard32(file) - for glyphID in range(prev, first): - gidArray[glyphID] = fd - self.gidArray = gidArray - else: - assert False, "unsupported FDSelect format: %s" % format - else: - # 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 = [] + self.gidArray = array("B", file.read(numGlyphs)).tolist() + elif self.format == 3: + gidArray = [None] * numGlyphs + nRanges = readCard16(file) + fd = None + prev = None + for i in range(nRanges): + first = readCard16(file) + if prev is not None: + for glyphID in range(prev, first): + gidArray[glyphID] = fd + prev = first + fd = readCard8(file) + if prev is not None: + first = readCard16(file) + for glyphID in range(prev, first): + gidArray[glyphID] = fd + self.gidArray = gidArray + elif self.format == 4: + gidArray = [None] * numGlyphs + nRanges = readCard32(file) + fd = None + prev = None + for i in range(nRanges): + first = readCard32(file) + if prev is not None: + for glyphID in range(prev, first): + gidArray[glyphID] = fd + prev = first + fd = readCard16(file) + if prev is not None: + first = readCard32(file) + for glyphID in range(prev, first): + gidArray[glyphID] = fd + self.gidArray = gidArray + else: + assert False, "unsupported FDSelect format: %s" % format + else: + # 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 __len__(self): + return len(self.gidArray) - def __getitem__(self, index): - return self.gidArray[index] + def __getitem__(self, index): + return self.gidArray[index] - def __setitem__(self, index, fdSelectValue): - self.gidArray[index] = fdSelectValue + def __setitem__(self, index, fdSelectValue): + self.gidArray[index] = fdSelectValue - def append(self, fdSelectValue): - self.gidArray.append(fdSelectValue) + def append(self, fdSelectValue): + self.gidArray.append(fdSelectValue) class CharStrings(object): - """The ``CharStrings`` in the font represent the instructions for drawing - each glyph. This object presents a dictionary interface to the font's - CharStrings, indexed by glyph name: + """The ``CharStrings`` in the font represent the instructions for drawing + each glyph. This object presents a dictionary interface to the font's + CharStrings, indexed by glyph name: - .. code:: python - - tt["CFF "].cff[0].CharStrings["a"] - # + .. code:: python - See :class:`fontTools.misc.psCharStrings.T1CharString` and - :class:`fontTools.misc.psCharStrings.T2CharString` for how to decompile, - compile and interpret the glyph drawing instructions in the returned objects. + tt["CFF "].cff[0].CharStrings["a"] + # - """ + See :class:`fontTools.misc.psCharStrings.T1CharString` and + :class:`fontTools.misc.psCharStrings.T2CharString` for how to decompile, + compile and interpret the glyph drawing instructions in the returned objects. - def __init__(self, file, charset, globalSubrs, private, fdSelect, fdArray, - isCFF2=None, varStore=None): - self.globalSubrs = globalSubrs - self.varStore = varStore - if file is not None: - 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.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 __init__( + self, + file, + charset, + globalSubrs, + private, + fdSelect, + fdArray, + isCFF2=None, + varStore=None, + ): + self.globalSubrs = globalSubrs + self.varStore = varStore + if file is not None: + 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.private = private + if fdSelect is not None: + self.fdSelect = fdSelect + if fdArray is not None: + self.fdArray = fdArray - def values(self): - if self.charStringsAreIndexed: - return self.charStringsIndex - else: - return list(self.charStrings.values()) + def keys(self): + return list(self.charStrings.keys()) - def has_key(self, name): - return name in self.charStrings + def values(self): + if self.charStringsAreIndexed: + return self.charStringsIndex + else: + return list(self.charStrings.values()) - __contains__ = has_key + def has_key(self, name): + return name in self.charStrings - def __len__(self): - return len(self.charStrings) + __contains__ = has_key - def __getitem__(self, name): - charString = self.charStrings[name] - if self.charStringsAreIndexed: - charString = self.charStringsIndex[charString] - return charString + def __len__(self): + return len(self.charStrings) - def __setitem__(self, name, charString): - if self.charStringsAreIndexed: - index = self.charStrings[name] - self.charStringsIndex[index] = charString - else: - self.charStrings[name] = charString + def __getitem__(self, name): + charString = self.charStrings[name] + if self.charStringsAreIndexed: + charString = self.charStringsIndex[charString] + return charString - def getItemAndSelector(self, name): - if self.charStringsAreIndexed: - index = self.charStrings[name] - return self.charStringsIndex.getItemAndSelector(index) - else: - if hasattr(self, 'fdArray'): - if hasattr(self, 'fdSelect'): - sel = self.charStrings[name].fdSelectIndex - else: - sel = 0 - else: - sel = None - return self.charStrings[name], sel + def __setitem__(self, name, charString): + if self.charStringsAreIndexed: + index = self.charStrings[name] + self.charStringsIndex[index] = charString + else: + self.charStrings[name] = charString - def toXML(self, xmlWriter): - names = sorted(self.keys()) - for name in names: - charStr, fdSelectIndex = self.getItemAndSelector(name) - if charStr.needsDecompilation(): - raw = [("raw", 1)] - else: - raw = [] - if fdSelectIndex is None: - xmlWriter.begintag("CharString", [('name', name)] + raw) - else: - xmlWriter.begintag( - "CharString", - [('name', name), ('fdSelectIndex', fdSelectIndex)] + raw) - xmlWriter.newline() - charStr.toXML(xmlWriter) - xmlWriter.endtag("CharString") - xmlWriter.newline() + def getItemAndSelector(self, name): + if self.charStringsAreIndexed: + index = self.charStrings[name] + return self.charStringsIndex.getItemAndSelector(index) + else: + if hasattr(self, "fdArray"): + if hasattr(self, "fdSelect"): + sel = self.charStrings[name].fdSelectIndex + else: + sel = 0 + else: + sel = None + return self.charStrings[name], sel - def fromXML(self, name, attrs, content): - for element in content: - if isinstance(element, str): - continue - name, attrs, content = element - if name != "CharString": - continue - fdID = -1 - if hasattr(self, "fdArray"): - try: - fdID = safeEval(attrs["fdSelectIndex"]) - except KeyError: - fdID = 0 - private = self.fdArray[fdID].Private - else: - private = self.private + def toXML(self, xmlWriter): + names = sorted(self.keys()) + for name in names: + charStr, fdSelectIndex = self.getItemAndSelector(name) + if charStr.needsDecompilation(): + raw = [("raw", 1)] + else: + raw = [] + if fdSelectIndex is None: + xmlWriter.begintag("CharString", [("name", name)] + raw) + else: + xmlWriter.begintag( + "CharString", + [("name", name), ("fdSelectIndex", fdSelectIndex)] + raw, + ) + xmlWriter.newline() + charStr.toXML(xmlWriter) + xmlWriter.endtag("CharString") + xmlWriter.newline() - glyphName = attrs["name"] - charStringClass = psCharStrings.T2CharString - charString = charStringClass( - private=private, - globalSubrs=self.globalSubrs) - charString.fromXML(name, attrs, content) - if fdID >= 0: - charString.fdSelectIndex = fdID - self[glyphName] = charString + def fromXML(self, name, attrs, content): + for element in content: + if isinstance(element, str): + continue + name, attrs, content = element + if name != "CharString": + continue + fdID = -1 + if hasattr(self, "fdArray"): + try: + fdID = safeEval(attrs["fdSelectIndex"]) + except KeyError: + fdID = 0 + private = self.fdArray[fdID].Private + else: + private = self.private + + glyphName = attrs["name"] + charStringClass = psCharStrings.T2CharString + charString = charStringClass(private=private, globalSubrs=self.globalSubrs) + charString.fromXML(name, attrs, content) + if fdID >= 0: + charString.fdSelectIndex = fdID + self[glyphName] = charString def readCard8(file): - return byteord(file.read(1)) + return byteord(file.read(1)) def readCard16(file): - value, = struct.unpack(">H", file.read(2)) - return value + (value,) = struct.unpack(">H", file.read(2)) + return value def readCard32(file): - value, = struct.unpack(">L", file.read(4)) - return value + (value,) = struct.unpack(">L", file.read(4)) + return value def writeCard8(file, value): - file.write(bytechr(value)) + file.write(bytechr(value)) def writeCard16(file, value): - file.write(struct.pack(">H", value)) + file.write(struct.pack(">H", value)) def writeCard32(file, value): - file.write(struct.pack(">L", value)) + file.write(struct.pack(">L", value)) def packCard8(value): - return bytechr(value) + return bytechr(value) def packCard16(value): - return struct.pack(">H", value) + return struct.pack(">H", value) def packCard32(value): - return struct.pack(">L", value) + return struct.pack(">L", value) def buildOperatorDict(table): - d = {} - for op, name, arg, default, conv in table: - d[op] = (name, arg) - return d + 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: - if isinstance(op, tuple): - op = bytechr(op[0]) + bytechr(op[1]) - else: - op = bytechr(op) - d[name] = (op, arg) - return d + d = {} + for op, name, arg, default, conv in table: + if isinstance(op, tuple): + op = bytechr(op[0]) + bytechr(op[1]) + else: + op = bytechr(op) + d[name] = (op, arg) + return d def buildOrder(table): - l = [] - for op, name, arg, default, conv in table: - l.append(name) - return l + 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: - if default is not None: - d[name] = default - return d + d = {} + for op, name, arg, default, conv in table: + if default is not None: + d[name] = default + return d def buildConverters(table): - d = {} - for op, name, arg, default, conv in table: - d[name] = conv - return d + d = {} + for op, name, arg, default, conv in table: + d[name] = conv + return d 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): - 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 _read(self, parent, value): - return value + def write(self, parent, value): + return value - def write(self, parent, value): - return value + def xmlWrite(self, xmlWriter, name, value): + xmlWriter.simpletag(name, value=value) + xmlWriter.newline() - def xmlWrite(self, xmlWriter, name, value): - xmlWriter.simpletag(name, value=value) - xmlWriter.newline() - - def xmlRead(self, name, attrs, content, parent): - return attrs["value"] + def xmlRead(self, name, attrs, content, parent): + return attrs["value"] class ASCIIConverter(SimpleConverter): + def _read(self, parent, value): + return tostr(value, encoding="ascii") - def _read(self, parent, value): - return tostr(value, encoding='ascii') + def write(self, parent, value): + return tobytes(value, encoding="ascii") - def write(self, parent, value): - return tobytes(value, encoding='ascii') + def xmlWrite(self, xmlWriter, name, value): + xmlWriter.simpletag(name, value=tostr(value, encoding="ascii")) + xmlWriter.newline() - def xmlWrite(self, xmlWriter, name, value): - xmlWriter.simpletag(name, value=tostr(value, encoding="ascii")) - xmlWriter.newline() - - def xmlRead(self, name, attrs, content, parent): - return tobytes(attrs["value"], encoding=("ascii")) + def xmlRead(self, name, attrs, content, parent): + return tobytes(attrs["value"], encoding=("ascii")) class Latin1Converter(SimpleConverter): + def _read(self, parent, value): + return tostr(value, encoding="latin1") - def _read(self, parent, value): - return tostr(value, encoding='latin1') + def write(self, parent, value): + return tobytes(value, encoding="latin1") - def write(self, parent, value): - return tobytes(value, encoding='latin1') + def xmlWrite(self, xmlWriter, name, value): + value = tostr(value, encoding="latin1") + if name in ["Notice", "Copyright"]: + value = re.sub(r"[\r\n]\s+", " ", value) + xmlWriter.simpletag(name, value=value) + xmlWriter.newline() - def xmlWrite(self, xmlWriter, name, value): - value = tostr(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")) + def xmlRead(self, name, attrs, content, parent): + return tobytes(attrs["value"], encoding=("latin1")) def parseNum(s): - try: - value = int(s) - except: - value = float(s) - return value + try: + value = int(s) + except: + value = float(s) + return value def parseBlendList(s): - valueList = [] - for element in s: - if isinstance(element, str): - 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 + valueList = [] + for element in s: + if isinstance(element, str): + 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 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): - valueString = attrs.get("value", None) - if valueString is None: - value = parseBlendList(content) - else: - value = parseNum(attrs["value"]) - return value + def xmlRead(self, name, attrs, content, parent): + 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): - 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 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): - 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 + def xmlRead(self, name, attrs, content, parent): + 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): + xmlWriter.begintag(name) + xmlWriter.newline() + value.toXML(xmlWriter) + xmlWriter.endtag(name) + xmlWriter.newline() - def xmlWrite(self, xmlWriter, name, value): - xmlWriter.begintag(name) - xmlWriter.newline() - value.toXML(xmlWriter) - xmlWriter.endtag(name) - xmlWriter.newline() - - def xmlRead(self, name, attrs, content, parent): - ob = self.getClass()() - for element in content: - if isinstance(element, str): - continue - name, attrs, content = element - ob.fromXML(name, attrs, content) - return ob + def xmlRead(self, name, attrs, content, parent): + ob = self.getClass()() + for element in content: + if isinstance(element, str): + continue + name, attrs, content = element + ob.fromXML(name, attrs, content) + return ob class PrivateDictConverter(TableConverter): + def getClass(self): + return PrivateDict - def getClass(self): - return PrivateDict + def _read(self, parent, value): + size, offset = value + file = parent.file + 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 _read(self, parent, value): - size, offset = value - file = parent.file - 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 + def write(self, parent, value): + return (0, 0) # dummy value class SubrsConverter(TableConverter): + def getClass(self): + return SubrsIndex - def getClass(self): - return SubrsIndex + def _read(self, parent, value): + file = parent.file + isCFF2 = parent._isCFF2 + file.seek(parent.offset + value) # Offset(self) + return SubrsIndex(file, isCFF2=isCFF2) - def _read(self, parent, value): - file = parent.file - isCFF2 = parent._isCFF2 - file.seek(parent.offset + value) # Offset(self) - return SubrsIndex(file, isCFF2=isCFF2) - - def write(self, parent, value): - return 0 # dummy value + def write(self, parent, value): + return 0 # dummy value class CharStringsConverter(TableConverter): + def _read(self, parent, value): + file = parent.file + isCFF2 = parent._isCFF2 + charset = parent.charset + varStore = getattr(parent, "VarStore", None) + globalSubrs = parent.GlobalSubrs + 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) + charStrings = CharStrings( + file, + charset, + globalSubrs, + private, + fdSelect, + fdArray, + isCFF2=isCFF2, + varStore=varStore, + ) + return charStrings - def _read(self, parent, value): - file = parent.file - isCFF2 = parent._isCFF2 - charset = parent.charset - varStore = getattr(parent, "VarStore", None) - globalSubrs = parent.GlobalSubrs - 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) - charStrings = CharStrings( - file, charset, globalSubrs, private, fdSelect, fdArray, isCFF2=isCFF2, varStore=varStore) - return charStrings + def write(self, parent, value): + return 0 # dummy value - def write(self, parent, value): - return 0 # dummy value - - def xmlRead(self, name, attrs, content, parent): - 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. - private, fdSelect, fdArray = parent.Private, None, None - charStrings = CharStrings( - None, None, parent.GlobalSubrs, private, fdSelect, fdArray, varStore=getattr(parent, "VarStore", None)) - charStrings.fromXML(name, attrs, content) - return charStrings + def xmlRead(self, name, attrs, content, parent): + 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. + private, fdSelect, fdArray = parent.Private, None, None + charStrings = CharStrings( + None, + None, + parent.GlobalSubrs, + private, + fdSelect, + fdArray, + varStore=getattr(parent, "VarStore", None), + ) + charStrings.fromXML(name, attrs, content) + return charStrings class CharsetConverter(SimpleConverter): - def _read(self, parent, value): - isCID = hasattr(parent, "ROS") - if value > 2: - numGlyphs = parent.numGlyphs - file = parent.file - file.seek(value) - log.log(DEBUG, "loading charset at %s", value) - format = readCard8(file) - if format == 0: - charset = parseCharset0(numGlyphs, file, parent.strings, isCID) - elif format == 1 or format == 2: - charset = parseCharset(numGlyphs, file, parent.strings, isCID, format) - else: - raise NotImplementedError - assert len(charset) == numGlyphs - log.log(DEBUG, " charset end at %s", file.tell()) - # make sure glyph names are unique - allNames = {} - newCharset = [] - for glyphName in charset: - 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 + "#" + str(n) - allNames[glyphName] = 1 - newCharset.append(glyphName) - charset = newCharset - 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 - elif value == 1: - charset = cffIExpertStrings - elif value == 2: - charset = cffExpertSubsetStrings - if charset and (len(charset) != parent.numGlyphs): - charset = charset[:parent.numGlyphs] - return charset + def _read(self, parent, value): + isCID = hasattr(parent, "ROS") + if value > 2: + numGlyphs = parent.numGlyphs + file = parent.file + file.seek(value) + log.log(DEBUG, "loading charset at %s", value) + format = readCard8(file) + if format == 0: + charset = parseCharset0(numGlyphs, file, parent.strings, isCID) + elif format == 1 or format == 2: + charset = parseCharset(numGlyphs, file, parent.strings, isCID, format) + else: + raise NotImplementedError + assert len(charset) == numGlyphs + log.log(DEBUG, " charset end at %s", file.tell()) + # make sure glyph names are unique + allNames = {} + newCharset = [] + for glyphName in charset: + 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 + "#" + str(n) + allNames[glyphName] = 1 + newCharset.append(glyphName) + charset = newCharset + 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 + elif value == 1: + 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 write(self, parent, value): + return 0 # dummy value - 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.comment("charset is dumped separately as the 'GlyphOrder' element") - xmlWriter.newline() + 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.comment("charset is dumped separately as the 'GlyphOrder' element") + xmlWriter.newline() - def xmlRead(self, name, attrs, content, parent): - pass + def xmlRead(self, name, attrs, content, parent): + pass class CharsetCompiler(object): + def __init__(self, strings, charset, parent): + assert charset[0] == ".notdef" + isCID = hasattr(parent.dictObj, "ROS") + data0 = packCharset0(charset, isCID, strings) + data = packCharset(charset, isCID, strings) + if len(data) < len(data0): + self.data = data + else: + self.data = data0 + self.parent = parent - def __init__(self, strings, charset, parent): - assert charset[0] == '.notdef' - isCID = hasattr(parent.dictObj, "ROS") - data0 = packCharset0(charset, isCID, strings) - data = packCharset(charset, isCID, strings) - if len(data) < len(data0): - self.data = data - else: - self.data = data0 - self.parent = parent + def setPos(self, pos, endPos): + self.parent.rawDict["charset"] = pos - def setPos(self, pos, endPos): - self.parent.rawDict["charset"] = pos + def getDataLength(self): + return len(self.data) - def getDataLength(self): - return len(self.data) - - def toFile(self, file): - file.write(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 + # 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:]) + return int(name[3:]) def getSIDfromName(name, strings): - return strings.getSID(name) + return strings.getSID(name) def packCharset0(charset, isCID, strings): - fmt = 0 - data = [packCard8(fmt)] - if isCID: - getNameID = getCIDfromName - else: - getNameID = getSIDfromName + fmt = 0 + data = [packCard8(fmt)] + if isCID: + getNameID = getCIDfromName + else: + getNameID = getSIDfromName - for name in charset[1:]: - data.append(packCard16(getNameID(name, strings))) - return bytesjoin(data) + for name in charset[1:]: + data.append(packCard16(getNameID(name, strings))) + return bytesjoin(data) def packCharset(charset, isCID, strings): - fmt = 1 - ranges = [] - first = None - end = 0 - if isCID: - getNameID = getCIDfromName - else: - getNameID = getSIDfromName + fmt = 1 + ranges = [] + first = None + end = 0 + if isCID: + getNameID = getCIDfromName + else: + getNameID = getSIDfromName - for name in charset[1:]: - SID = getNameID(name, strings) - if first is None: - first = SID - elif end + 1 != SID: - nLeft = end - first - if nLeft > 255: - fmt = 2 - ranges.append((first, nLeft)) - first = SID - end = SID - if end: - nLeft = end - first - if nLeft > 255: - fmt = 2 - ranges.append((first, nLeft)) + for name in charset[1:]: + SID = getNameID(name, strings) + if first is None: + first = SID + elif end + 1 != SID: + nLeft = end - first + if nLeft > 255: + fmt = 2 + ranges.append((first, nLeft)) + first = SID + end = SID + if end: + nLeft = end - first + if nLeft > 255: + fmt = 2 + ranges.append((first, nLeft)) - data = [packCard8(fmt)] - if fmt == 1: - nLeftFunc = packCard8 - else: - nLeftFunc = packCard16 - for first, nLeft in ranges: - data.append(packCard16(first) + nLeftFunc(nLeft)) - return bytesjoin(data) + data = [packCard8(fmt)] + if fmt == 1: + nLeftFunc = packCard8 + else: + nLeftFunc = packCard16 + for first, nLeft in ranges: + data.append(packCard16(first) + nLeftFunc(nLeft)) + return bytesjoin(data) def parseCharset0(numGlyphs, file, strings, isCID): - charset = [".notdef"] - if isCID: - for i in range(numGlyphs - 1): - CID = readCard16(file) - charset.append("cid" + str(CID).zfill(5)) - else: - for i in range(numGlyphs - 1): - SID = readCard16(file) - charset.append(strings[SID]) - return charset + charset = [".notdef"] + if isCID: + for i in range(numGlyphs - 1): + CID = readCard16(file) + charset.append("cid" + str(CID).zfill(5)) + else: + for i in range(numGlyphs - 1): + SID = readCard16(file) + charset.append(strings[SID]) + return charset def parseCharset(numGlyphs, file, strings, isCID, fmt): - charset = ['.notdef'] - count = 1 - if fmt == 1: - nLeftFunc = readCard8 - else: - nLeftFunc = readCard16 - while count < numGlyphs: - first = readCard16(file) - nLeft = nLeftFunc(file) - if isCID: - for CID in range(first, first + nLeft + 1): - charset.append("cid" + str(CID).zfill(5)) - else: - for SID in range(first, first + nLeft + 1): - charset.append(strings[SID]) - count = count + nLeft + 1 - return charset + charset = [".notdef"] + count = 1 + if fmt == 1: + nLeftFunc = readCard8 + else: + nLeftFunc = readCard16 + while count < numGlyphs: + first = readCard16(file) + nLeft = nLeftFunc(file) + if isCID: + for CID in range(first, first + nLeft + 1): + charset.append("cid" + str(CID).zfill(5)) + else: + for SID in range(first, first + nLeft + 1): + charset.append(strings[SID]) + count = count + nLeft + 1 + return charset class EncodingCompiler(object): + def __init__(self, strings, encoding, parent): + assert not isinstance(encoding, str) + data0 = packEncoding0(parent.dictObj.charset, encoding, parent.strings) + data1 = packEncoding1(parent.dictObj.charset, encoding, parent.strings) + if len(data0) < len(data1): + self.data = data0 + else: + self.data = data1 + self.parent = parent - def __init__(self, strings, encoding, parent): - assert not isinstance(encoding, str) - data0 = packEncoding0(parent.dictObj.charset, encoding, parent.strings) - data1 = packEncoding1(parent.dictObj.charset, encoding, parent.strings) - if len(data0) < len(data1): - self.data = data0 - else: - self.data = data1 - self.parent = parent + def setPos(self, pos, endPos): + self.parent.rawDict["Encoding"] = pos - def setPos(self, pos, endPos): - self.parent.rawDict["Encoding"] = pos + def getDataLength(self): + return len(self.data) - def getDataLength(self): - return len(self.data) - - def toFile(self, file): - file.write(self.data) + def toFile(self, file): + file.write(self.data) class EncodingConverter(SimpleConverter): + def _read(self, parent, value): + if value == 0: + return "StandardEncoding" + elif value == 1: + return "ExpertEncoding" + else: + assert value > 1 + file = parent.file + file.seek(value) + log.log(DEBUG, "loading Encoding at %s", value) + fmt = readCard8(file) + haveSupplement = fmt & 0x80 + if haveSupplement: + raise NotImplementedError("Encoding supplements are not yet supported") + fmt = fmt & 0x7F + if fmt == 0: + encoding = parseEncoding0( + parent.charset, file, haveSupplement, parent.strings + ) + elif fmt == 1: + encoding = parseEncoding1( + parent.charset, file, haveSupplement, parent.strings + ) + return encoding - def _read(self, parent, value): - if value == 0: - return "StandardEncoding" - elif value == 1: - return "ExpertEncoding" - else: - assert value > 1 - file = parent.file - file.seek(value) - log.log(DEBUG, "loading Encoding at %s", value) - fmt = readCard8(file) - haveSupplement = fmt & 0x80 - if haveSupplement: - raise NotImplementedError("Encoding supplements are not yet supported") - fmt = fmt & 0x7f - if fmt == 0: - encoding = parseEncoding0(parent.charset, file, haveSupplement, - parent.strings) - elif fmt == 1: - encoding = parseEncoding1(parent.charset, file, haveSupplement, - parent.strings) - return encoding + def write(self, parent, value): + if value == "StandardEncoding": + return 0 + elif value == "ExpertEncoding": + return 1 + return 0 # dummy value - def write(self, parent, value): - if value == "StandardEncoding": - return 0 - elif value == "ExpertEncoding": - return 1 - return 0 # dummy value + def xmlWrite(self, xmlWriter, name, value): + if value in ("StandardEncoding", "ExpertEncoding"): + xmlWriter.simpletag(name, name=value) + xmlWriter.newline() + return + xmlWriter.begintag(name) + xmlWriter.newline() + for code in range(len(value)): + glyphName = value[code] + if glyphName != ".notdef": + xmlWriter.simpletag("map", code=hex(code), name=glyphName) + xmlWriter.newline() + xmlWriter.endtag(name) + xmlWriter.newline() - def xmlWrite(self, xmlWriter, name, value): - if value in ("StandardEncoding", "ExpertEncoding"): - xmlWriter.simpletag(name, name=value) - xmlWriter.newline() - return - xmlWriter.begintag(name) - xmlWriter.newline() - for code in range(len(value)): - glyphName = value[code] - if glyphName != ".notdef": - xmlWriter.simpletag("map", code=hex(code), name=glyphName) - xmlWriter.newline() - xmlWriter.endtag(name) - xmlWriter.newline() - - def xmlRead(self, name, attrs, content, parent): - if "name" in attrs: - return attrs["name"] - encoding = [".notdef"] * 256 - for element in content: - if isinstance(element, str): - continue - name, attrs, content = element - code = safeEval(attrs["code"]) - glyphName = attrs["name"] - encoding[code] = glyphName - return encoding + def xmlRead(self, name, attrs, content, parent): + if "name" in attrs: + return attrs["name"] + encoding = [".notdef"] * 256 + for element in content: + if isinstance(element, str): + continue + name, attrs, content = element + code = safeEval(attrs["code"]) + glyphName = attrs["name"] + encoding[code] = glyphName + return encoding def parseEncoding0(charset, file, haveSupplement, strings): - nCodes = readCard8(file) - encoding = [".notdef"] * 256 - for glyphID in range(1, nCodes + 1): - code = readCard8(file) - if code != 0: - encoding[code] = charset[glyphID] - return encoding + nCodes = readCard8(file) + encoding = [".notdef"] * 256 + for glyphID in range(1, nCodes + 1): + code = readCard8(file) + if code != 0: + encoding[code] = charset[glyphID] + return encoding def parseEncoding1(charset, file, haveSupplement, strings): - nRanges = readCard8(file) - encoding = [".notdef"] * 256 - glyphID = 1 - for i in range(nRanges): - code = readCard8(file) - nLeft = readCard8(file) - for glyphID in range(glyphID, glyphID + nLeft + 1): - encoding[code] = charset[glyphID] - code = code + 1 - glyphID = glyphID + 1 - return encoding + nRanges = readCard8(file) + encoding = [".notdef"] * 256 + glyphID = 1 + for i in range(nRanges): + code = readCard8(file) + nLeft = readCard8(file) + for glyphID in range(glyphID, glyphID + nLeft + 1): + encoding[code] = charset[glyphID] + code = code + 1 + glyphID = glyphID + 1 + return encoding def packEncoding0(charset, encoding, strings): - fmt = 0 - m = {} - for code in range(len(encoding)): - name = encoding[code] - if name != ".notdef": - m[name] = code - codes = [] - for name in charset[1:]: - code = m.get(name) - codes.append(code) + fmt = 0 + m = {} + for code in range(len(encoding)): + name = encoding[code] + if name != ".notdef": + m[name] = code + codes = [] + for name in charset[1:]: + code = m.get(name) + codes.append(code) - while codes and codes[-1] is None: - codes.pop() + while codes and codes[-1] is None: + codes.pop() - data = [packCard8(fmt), packCard8(len(codes))] - for code in codes: - if code is None: - code = 0 - data.append(packCard8(code)) - return bytesjoin(data) + data = [packCard8(fmt), packCard8(len(codes))] + for code in codes: + if code is None: + code = 0 + data.append(packCard8(code)) + return bytesjoin(data) def packEncoding1(charset, encoding, strings): - fmt = 1 - m = {} - for code in range(len(encoding)): - name = encoding[code] - if name != ".notdef": - m[name] = code - ranges = [] - first = None - end = 0 - for name in charset[1:]: - code = m.get(name, -1) - if first is None: - first = code - elif end + 1 != code: - nLeft = end - first - ranges.append((first, nLeft)) - first = code - end = code - nLeft = end - first - ranges.append((first, nLeft)) + fmt = 1 + m = {} + for code in range(len(encoding)): + name = encoding[code] + if name != ".notdef": + m[name] = code + ranges = [] + first = None + end = 0 + for name in charset[1:]: + code = m.get(name, -1) + if first is None: + first = code + elif end + 1 != code: + nLeft = end - first + ranges.append((first, nLeft)) + first = code + end = code + nLeft = end - first + ranges.append((first, nLeft)) - # remove unencoded glyphs at the end. - while ranges and ranges[-1][0] == -1: - ranges.pop() + # remove unencoded glyphs at the end. + while ranges and ranges[-1][0] == -1: + ranges.pop() - data = [packCard8(fmt), packCard8(len(ranges))] - for first, nLeft in ranges: - if first == -1: # unencoded - first = 0 - data.append(packCard8(first) + packCard8(nLeft)) - return bytesjoin(data) + data = [packCard8(fmt), packCard8(len(ranges))] + for first, nLeft in ranges: + if first == -1: # unencoded + first = 0 + data.append(packCard8(first) + packCard8(nLeft)) + return bytesjoin(data) class FDArrayConverter(TableConverter): + 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, isCFF2=isCFF2) + fdArray.vstore = vstore + fdArray.strings = parent.strings + fdArray.GlobalSubrs = parent.GlobalSubrs + return fdArray - 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, isCFF2=isCFF2) - fdArray.vstore = vstore - fdArray.strings = parent.strings - fdArray.GlobalSubrs = parent.GlobalSubrs - return fdArray + def write(self, parent, value): + return 0 # dummy value - def write(self, parent, value): - return 0 # dummy value - - def xmlRead(self, name, attrs, content, parent): - fdArray = FDArrayIndex() - for element in content: - if isinstance(element, str): - continue - name, attrs, content = element - fdArray.fromXML(name, attrs, content) - return fdArray + def xmlRead(self, name, attrs, content, parent): + fdArray = FDArrayIndex() + for element in content: + if isinstance(element, str): + continue + name, attrs, content = element + fdArray.fromXML(name, attrs, content) + return fdArray class FDSelectConverter(SimpleConverter): + def _read(self, parent, value): + file = parent.file + file.seek(value) + fdSelect = FDSelect(file, parent.numGlyphs) + return fdSelect - def _read(self, parent, value): - file = parent.file - file.seek(value) - fdSelect = FDSelect(file, parent.numGlyphs) - return fdSelect + def write(self, parent, value): + return 0 # dummy value - 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): + xmlWriter.simpletag(name, [("format", value.format)]) + xmlWriter.newline() - # 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): - xmlWriter.simpletag(name, [('format', value.format)]) - xmlWriter.newline() - - def xmlRead(self, name, attrs, content, parent): - fmt = safeEval(attrs["format"]) - file = None - numGlyphs = None - fdSelect = FDSelect(file, numGlyphs, fmt) - return fdSelect + def xmlRead(self, name, attrs, content, parent): + fmt = safeEval(attrs["format"]) + file = None + 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 _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 write(self, parent, value): - return 0 # dummy value + def xmlWrite(self, xmlWriter, name, value): + value.writeXML(xmlWriter, name) - 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 xmlRead(self, name, attrs, content, parent): + varStore = VarStoreData() + varStore.xmlRead(name, attrs, content, parent) + return varStore def packFDSelect0(fdSelectArray): - fmt = 0 - data = [packCard8(fmt)] - for index in fdSelectArray: - data.append(packCard8(index)) - return bytesjoin(data) + fmt = 0 + data = [packCard8(fmt)] + for index in fdSelectArray: + data.append(packCard8(index)) + return bytesjoin(data) def packFDSelect3(fdSelectArray): - fmt = 3 - fdRanges = [] - lenArray = len(fdSelectArray) - lastFDIndex = -1 - for i in range(lenArray): - fdIndex = fdSelectArray[i] - if lastFDIndex != fdIndex: - fdRanges.append([i, fdIndex]) - lastFDIndex = fdIndex - sentinelGID = i + 1 + fmt = 3 + fdRanges = [] + lenArray = len(fdSelectArray) + lastFDIndex = -1 + for i in range(lenArray): + fdIndex = fdSelectArray[i] + if lastFDIndex != fdIndex: + fdRanges.append([i, fdIndex]) + lastFDIndex = fdIndex + sentinelGID = i + 1 - data = [packCard8(fmt)] - data.append(packCard16(len(fdRanges))) - for fdRange in fdRanges: - data.append(packCard16(fdRange[0])) - data.append(packCard8(fdRange[1])) - data.append(packCard16(sentinelGID)) - return bytesjoin(data) + data = [packCard8(fmt)] + data.append(packCard16(len(fdRanges))) + for fdRange in fdRanges: + data.append(packCard16(fdRange[0])) + data.append(packCard8(fdRange[1])) + data.append(packCard16(sentinelGID)) + return bytesjoin(data) def packFDSelect4(fdSelectArray): - fmt = 4 - fdRanges = [] - lenArray = len(fdSelectArray) - lastFDIndex = -1 - for i in range(lenArray): - fdIndex = fdSelectArray[i] - if lastFDIndex != fdIndex: - fdRanges.append([i, fdIndex]) - lastFDIndex = fdIndex - sentinelGID = i + 1 + fmt = 4 + fdRanges = [] + lenArray = len(fdSelectArray) + lastFDIndex = -1 + for i in range(lenArray): + fdIndex = fdSelectArray[i] + if lastFDIndex != fdIndex: + fdRanges.append([i, fdIndex]) + lastFDIndex = fdIndex + sentinelGID = i + 1 - data = [packCard8(fmt)] - data.append(packCard32(len(fdRanges))) - for fdRange in fdRanges: - data.append(packCard32(fdRange[0])) - data.append(packCard16(fdRange[1])) - data.append(packCard32(sentinelGID)) - return bytesjoin(data) + data = [packCard8(fmt)] + data.append(packCard32(len(fdRanges))) + for fdRange in fdRanges: + data.append(packCard32(fdRange[0])) + data.append(packCard16(fdRange[1])) + data.append(packCard32(sentinelGID)) + return bytesjoin(data) class FDSelectCompiler(object): + def __init__(self, fdSelect, parent): + fmt = fdSelect.format + fdSelectArray = fdSelect.gidArray + if fmt == 0: + self.data = packFDSelect0(fdSelectArray) + elif fmt == 3: + self.data = packFDSelect3(fdSelectArray) + elif fmt == 4: + self.data = packFDSelect4(fdSelectArray) + else: + # choose smaller of the two formats + data0 = packFDSelect0(fdSelectArray) + data3 = packFDSelect3(fdSelectArray) + if len(data0) < len(data3): + self.data = data0 + fdSelect.format = 0 + else: + self.data = data3 + fdSelect.format = 3 - def __init__(self, fdSelect, parent): - fmt = fdSelect.format - fdSelectArray = fdSelect.gidArray - if fmt == 0: - self.data = packFDSelect0(fdSelectArray) - elif fmt == 3: - self.data = packFDSelect3(fdSelectArray) - elif fmt == 4: - self.data = packFDSelect4(fdSelectArray) - else: - # choose smaller of the two formats - data0 = packFDSelect0(fdSelectArray) - data3 = packFDSelect3(fdSelectArray) - if len(data0) < len(data3): - self.data = data0 - fdSelect.format = 0 - else: - self.data = data3 - fdSelect.format = 3 + self.parent = parent - self.parent = parent + def setPos(self, pos, endPos): + self.parent.rawDict["FDSelect"] = pos - def setPos(self, pos, endPos): - self.parent.rawDict["FDSelect"] = pos + def getDataLength(self): + return len(self.data) - def getDataLength(self): - return len(self.data) - - def toFile(self, file): - file.write(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 __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 setPos(self, pos, endPos): - self.parent.rawDict["VarStore"] = pos + def getDataLength(self): + return len(self.data) - def getDataLength(self): - return len(self.data) - - def toFile(self, file): - file.write(self.data) + def toFile(self, file): + file.write(self.data) class ROSConverter(SimpleConverter): + def xmlWrite(self, xmlWriter, name, value): + registry, order, supplement = value + xmlWriter.simpletag( + name, + [ + ("Registry", tostr(registry)), + ("Order", tostr(order)), + ("Supplement", supplement), + ], + ) + xmlWriter.newline() - def xmlWrite(self, xmlWriter, name, value): - registry, order, supplement = value - 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"])) - def xmlRead(self, name, attrs, content, parent): - return (attrs['Registry'], attrs['Order'], safeEval(attrs['Supplement'])) topDictOperators = [ -# 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()), + # 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()), + # 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, @@ -2172,68 +2221,80 @@ kBlendDictOpName = "blend" blendOp = 23 privateDictOperators = [ -# 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()), + # 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), - ((12, 17), 'LanguageGroup', 'number', 0, None), - ((12, 18), 'ExpansionFactor', 'number', 0.06, 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, 17), "LanguageGroup", "number", 0, None), + ((12, 18), "ExpansionFactor", "number", 0.06, None), + (19, "Subrs", "number", None, SubrsConverter()), ] def addConverters(table): - for i in range(len(table)): - op, name, arg, default, conv = table[i] - if conv is not None: - continue - if arg in ("delta", "array"): - conv = ArrayConverter() - elif arg == "number": - conv = NumberConverter() - elif arg == "SID": - conv = ASCIIConverter() - elif arg == 'blendList': - conv = None - else: - assert False - table[i] = op, name, arg, default, conv + for i in range(len(table)): + op, name, arg, default, conv = table[i] + if conv is not None: + continue + if arg in ("delta", "array"): + conv = ArrayConverter() + elif arg == "number": + conv = NumberConverter() + elif arg == "SID": + conv = ASCIIConverter() + elif arg == "blendList": + conv = None + else: + assert False + table[i] = op, name, arg, default, conv addConverters(privateDictOperators) @@ -2241,683 +2302,1027 @@ addConverters(topDictOperators) class TopDictDecompiler(psCharStrings.DictDecompiler): - operators = buildOperatorDict(topDictOperators) + operators = buildOperatorDict(topDictOperators) class PrivateDictDecompiler(psCharStrings.DictDecompiler): - operators = buildOperatorDict(privateDictOperators) + operators = buildOperatorDict(privateDictOperators) class DictCompiler(object): - maxBlendStack = 0 + 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 - rawDict = {} - for name in dictObj.order: - value = getattr(dictObj, name, None) - if value is None: - continue - conv = dictObj.converters[name] - value = conv.write(dictObj, value) - if value == dictObj.defaults.get(name): - continue - rawDict[name] = value - self.rawDict = rawDict + 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 + rawDict = {} + for name in dictObj.order: + value = getattr(dictObj, name, None) + if value is None: + continue + conv = dictObj.converters[name] + value = conv.write(dictObj, value) + if value == dictObj.defaults.get(name): + continue + rawDict[name] = value + self.rawDict = rawDict - def setPos(self, pos, endPos): - pass + def setPos(self, pos, endPos): + pass - def getDataLength(self): - return len(self.compile("getDataLength")) + def getDataLength(self): + return len(self.compile("getDataLength")) - def compile(self, reason): - log.log(DEBUG, "-- compiling %s for %s", self.__class__.__name__, reason) - rawDict = self.rawDict - data = [] - for name in self.dictObj.order: - value = rawDict.get(name) - if value is None: - continue - op, argType = self.opcodes[name] - if isinstance(argType, tuple): - l = len(argType) - assert len(value) == l, "value doesn't match arg type" - for i in range(l): - arg = argType[i] - v = value[i] - arghandler = getattr(self, "arg_" + arg) - data.append(arghandler(v)) - else: - arghandler = getattr(self, "arg_" + argType) - data.append(arghandler(value)) - data.append(op) - data = bytesjoin(data) - return data + def compile(self, reason): + log.log(DEBUG, "-- compiling %s for %s", self.__class__.__name__, reason) + rawDict = self.rawDict + data = [] + for name in self.dictObj.order: + value = rawDict.get(name) + if value is None: + continue + op, argType = self.opcodes[name] + if isinstance(argType, tuple): + l = len(argType) + assert len(value) == l, "value doesn't match arg type" + for i in range(l): + arg = argType[i] + v = value[i] + arghandler = getattr(self, "arg_" + arg) + data.append(arghandler(v)) + else: + arghandler = getattr(self, "arg_" + argType) + data.append(arghandler(value)) + data.append(op) + data = bytesjoin(data) + return data - def toFile(self, file): - data = self.compile("toFile") - file.write(data) + def toFile(self, file): + data = self.compile("toFile") + file.write(data) - def arg_number(self, 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_number(self, 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_SID(self, s): + return psCharStrings.encodeIntCFF(self.strings.getSID(s)) - def arg_array(self, value): - data = [] - for num in value: - data.append(self.arg_number(num)) - return bytesjoin(data) + def arg_array(self, value): + data = [] + for num in value: + data.append(self.arg_number(num)) + return bytesjoin(data) - def arg_delta(self, value): - 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(self, value): + 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. - 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:: - The value is a list is arranged as follows:: + [ + [V0, d0..dn] + [V1, d0..dn] + ... + [Vm, d0..dn] + ] - [ - [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. - ``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:: - We want to return a list:: + [ + [v0, v1..vm] + [d0..dn] + ... + [d0..dn] + numBlends + blendOp + ] - [ - [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. - 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 - 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 + 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) - else: - return psCharStrings.encodeIntCFF(num) + if isinstance(num, float): + return psCharStrings.encodeFloat(num) + else: + return psCharStrings.encodeIntCFF(num) class TopDictCompiler(DictCompiler): - opcodes = buildOpcodeDict(topDictOperators) + opcodes = buildOpcodeDict(topDictOperators) - def getChildren(self, strings): - isCFF2 = self.isCFF2 - children = [] - 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, str): - 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 the FDSelect indices are all - # in the charstring data, or the FDSelect array is already fully defined. - fdSelect = self.dictObj.FDSelect - # 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) - fdSelectComp = FDSelectCompiler(fdSelect, self) - children.append(fdSelectComp) - if hasattr(self.dictObj, "CharStrings"): - items = [] - charStrings = self.dictObj.CharStrings - for name in self.dictObj.charset: - items.append(charStrings[name]) - 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. - fdArrayIndexComp = self.dictObj.FDArray.getCompiler(strings, self) - children.append(fdArrayIndexComp) - children.extend(fdArrayIndexComp.getChildren(strings)) - if hasattr(self.dictObj, "Private"): - privComp = self.dictObj.Private.getCompiler(strings, self) - children.append(privComp) - children.extend(privComp.getChildren(strings)) - return children + def getChildren(self, strings): + isCFF2 = self.isCFF2 + children = [] + 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, str): + 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 the FDSelect indices are all + # in the charstring data, or the FDSelect array is already fully defined. + fdSelect = self.dictObj.FDSelect + # 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) + fdSelectComp = FDSelectCompiler(fdSelect, self) + children.append(fdSelectComp) + if hasattr(self.dictObj, "CharStrings"): + items = [] + charStrings = self.dictObj.CharStrings + for name in self.dictObj.charset: + items.append(charStrings[name]) + 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. + fdArrayIndexComp = self.dictObj.FDArray.getCompiler(strings, self) + children.append(fdArrayIndexComp) + children.extend(fdArrayIndexComp.getChildren(strings)) + if hasattr(self.dictObj, "Private"): + privComp = self.dictObj.Private.getCompiler(strings, self) + children.append(privComp) + children.extend(privComp.getChildren(strings)) + return children class FontDictCompiler(DictCompiler): - opcodes = buildOpcodeDict(topDictOperators) + 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 __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"): - privComp = self.dictObj.Private.getCompiler(strings, self) - children.append(privComp) - children.extend(privComp.getChildren(strings)) - return children + def getChildren(self, strings): + children = [] + if hasattr(self.dictObj, "Private"): + privComp = self.dictObj.Private.getCompiler(strings, self) + children.append(privComp) + children.extend(privComp.getChildren(strings)) + return children class PrivateDictCompiler(DictCompiler): - maxBlendStack = maxStackLimit - opcodes = buildOpcodeDict(privateDictOperators) + maxBlendStack = maxStackLimit + opcodes = buildOpcodeDict(privateDictOperators) - def setPos(self, pos, endPos): - size = endPos - pos - self.parent.rawDict["Private"] = size, pos - self.pos = pos + 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"): - children.append(self.dictObj.Subrs.getCompiler(strings, self)) - return children + def getChildren(self, strings): + children = [] + if hasattr(self.dictObj, "Subrs"): + children.append(self.dictObj.Subrs.getCompiler(strings, self)) + return children class BaseDict(object): + def __init__(self, strings=None, file=None, offset=None, isCFF2=None): + assert (isCFF2 is None) == (file is None) + self.rawDict = {} + 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 __init__(self, strings=None, file=None, offset=None, isCFF2=None): - assert (isCFF2 is None) == (file is None) - self.rawDict = {} - 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): + 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 decompile(self, data): - 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 postDecompile(self): - pass + def getCompiler(self, strings, parent, isCFF2=None): + return self.compilerClass(self, strings, parent, isCFF2=isCFF2) - def getCompiler(self, strings, parent, isCFF2=None): - return self.compilerClass(self, strings, parent, isCFF2=isCFF2) + def __getattr__(self, name): + if name[:2] == name[-2:] == "__": + # to make deepcopy() and pickle.load() work, we need to signal with + # AttributeError that dunder methods like '__deepcopy__' or '__getstate__' + # aren't implemented. For more details, see: + # https://github.com/fonttools/fonttools/pull/1488 + raise AttributeError(name) + value = self.rawDict.get(name, None) + if value is None: + value = self.defaults.get(name) + if value is None: + raise AttributeError(name) + conv = self.converters[name] + value = conv.read(self, value) + setattr(self, name, value) + return value - def __getattr__(self, name): - if name[:2] == name[-2:] == "__": - # to make deepcopy() and pickle.load() work, we need to signal with - # AttributeError that dunder methods like '__deepcopy__' or '__getstate__' - # aren't implemented. For more details, see: - # https://github.com/fonttools/fonttools/pull/1488 - raise AttributeError(name) - value = self.rawDict.get(name, None) - if value is None: - value = self.defaults.get(name) - if value is None: - raise AttributeError(name) - conv = self.converters[name] - value = conv.read(self, value) - setattr(self, name, value) - return value + def toXML(self, xmlWriter): + for name in self.order: + if name in self.skipNames: + continue + value = getattr(self, name, 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) + ignoredNames = set(self.rawDict) - set(self.order) + if ignoredNames: + xmlWriter.comment( + "some keys were ignored: %s" % " ".join(sorted(ignoredNames)) + ) + xmlWriter.newline() - def toXML(self, xmlWriter): - for name in self.order: - if name in self.skipNames: - continue - value = getattr(self, name, 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) - 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) - setattr(self, name, value) + def fromXML(self, name, attrs, content): + conv = self.converters[name] + value = conv.xmlRead(name, attrs, content, self) + setattr(self, name, value) class TopDict(BaseDict): - """The ``TopDict`` represents the top-level dictionary holding font - information. CFF2 tables contain a restricted set of top-level entries - as described `here `_, - but CFF tables may contain a wider range of information. This information - can be accessed through attributes or through the dictionary returned - through the ``rawDict`` property: + """The ``TopDict`` represents the top-level dictionary holding font + information. CFF2 tables contain a restricted set of top-level entries + as described `here `_, + but CFF tables may contain a wider range of information. This information + can be accessed through attributes or through the dictionary returned + through the ``rawDict`` property: - .. code:: python + .. code:: python - font = tt["CFF "].cff[0] - font.FamilyName - # 'Linux Libertine O' - font.rawDict["FamilyName"] - # 'Linux Libertine O' + font = tt["CFF "].cff[0] + font.FamilyName + # 'Linux Libertine O' + font.rawDict["FamilyName"] + # 'Linux Libertine O' - More information is available in the CFF file's private dictionary, accessed - via the ``Private`` property: + More information is available in the CFF file's private dictionary, accessed + via the ``Private`` property: - .. code:: python + .. code:: python - tt["CFF "].cff[0].Private.BlueValues - # [-15, 0, 515, 515, 666, 666] - - """ + tt["CFF "].cff[0].Private.BlueValues + # [-15, 0, 515, 515, 666, 666] - defaults = buildDefaults(topDictOperators) - converters = buildConverters(topDictOperators) - compilerClass = TopDictCompiler - order = buildOrder(topDictOperators) - decompilerClass = TopDictDecompiler + """ - 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) + defaults = buildDefaults(topDictOperators) + converters = buildConverters(topDictOperators) + compilerClass = TopDictCompiler + order = buildOrder(topDictOperators) + decompilerClass = TopDictDecompiler - def getGlyphOrder(self): - """Returns a list of glyph names in the CFF font.""" - return self.charset + 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 postDecompile(self): - offset = self.rawDict.get("CharStrings") - if offset is None: - return - # get the number of glyphs beforehand. - self.file.seek(offset) - if self._isCFF2: - self.numGlyphs = readCard32(self.file) - else: - self.numGlyphs = readCard16(self.file) + def getGlyphOrder(self): + """Returns a list of glyph names in the CFF font.""" + return self.charset - def toXML(self, xmlWriter): - if hasattr(self, "CharStrings"): - 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) + def postDecompile(self): + offset = self.rawDict.get("CharStrings") + if offset is None: + return + # get the number of glyphs beforehand. + self.file.seek(offset) + if self._isCFF2: + self.numGlyphs = readCard32(self.file) + else: + self.numGlyphs = readCard16(self.file) - def decompileAllCharStrings(self): - # Make sure that all the Private Dicts have been instantiated. - for i, charString in enumerate(self.CharStrings.values()): - try: - charString.decompile() - except: - log.error("Error in charstring %s", i) - raise + def toXML(self, xmlWriter): + if hasattr(self, "CharStrings"): + 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) - 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 + def decompileAllCharStrings(self): + # Make sure that all the Private Dicts have been instantiated. + for i, charString in enumerate(self.CharStrings.values()): + try: + charString.decompile() + except: + log.error("Error in charstring %s", i) + raise - if fontBBox is None: - self.FontBBox = self.defaults['FontBBox'][:] - else: - self.FontBBox = list(intRect(fontBBox)) + 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): - # - # 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) - compilerClass = FontDictCompiler - orderCFF = ['FontName', 'FontMatrix', 'Weight', 'Private'] - orderCFF2 = ['Private'] - decompilerClass = TopDictDecompiler + # + # 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) + compilerClass = FontDictCompiler + 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 __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 + 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): - defaults = buildDefaults(privateDictOperators) - converters = buildConverters(privateDictOperators) - order = buildOrder(privateDictOperators) - decompilerClass = PrivateDictDecompiler - compilerClass = PrivateDictCompiler + defaults = buildDefaults(privateDictOperators) + converters = buildConverters(privateDictOperators) + order = buildOrder(privateDictOperators) + 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) - # Provide dummy values. This avoids needing to provide - # an isCFF2 state in a lot of places. - self.nominalWidthX = self.defaultWidthX = None - else: - self.defaults = buildDefaults(privateDictOperators) - self.order = buildOrder(privateDictOperators) + 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) + # Provide dummy values. This avoids needing to provide + # an isCFF2 state in a lot of places. + self.nominalWidthX = self.defaultWidthX = None + else: + self.defaults = buildDefaults(privateDictOperators) + self.order = buildOrder(privateDictOperators) - @property - def in_cff2(self): - return self._isCFF2 + @property + def in_cff2(self): + return self._isCFF2 - 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 + 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.""" + """SID -> string mapping.""" - def __init__(self, file=None): - if file is None: - strings = [] - else: - strings = [ - tostr(s, encoding="latin1") - for s in Index(file, isCFF2=False) - ] - self.strings = strings + def __init__(self, file=None): + if file is None: + strings = [] + else: + strings = [tostr(s, encoding="latin1") for s in Index(file, isCFF2=False)] + self.strings = strings - def getCompiler(self): - return IndexedStringsCompiler(self, None, self, isCFF2=False) + def getCompiler(self): + return IndexedStringsCompiler(self, None, self, isCFF2=False) - def __len__(self): - return len(self.strings) + def __len__(self): + return len(self.strings) - def __getitem__(self, SID): - if SID < cffStandardStringCount: - return cffStandardStrings[SID] - else: - return self.strings[SID - cffStandardStringCount] + 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: - SID = self.stringMapping[s] - else: - SID = len(self.strings) + cffStandardStringCount - self.strings.append(s) - self.stringMapping[s] = SID - return SID + 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: + SID = self.stringMapping[s] + else: + SID = len(self.strings) + cffStandardStringCount + self.strings.append(s) + self.stringMapping[s] = SID + return SID - def getStrings(self): - return self.strings + def getStrings(self): + return self.strings - def buildStringMapping(self): - self.stringMapping = {} - for index in range(len(self.strings)): - self.stringMapping[self.strings[index]] = index + cffStandardStringCount + def buildStringMapping(self): + self.stringMapping = {} + for index in range(len(self.strings)): + self.stringMapping[self.strings[index]] = index + cffStandardStringCount # 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', - 'Semibold' +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", ] cffStandardStringCount = 391 @@ -2925,98 +3330,504 @@ assert len(cffStandardStrings) == cffStandardStringCount # build reverse mapping cffStandardStringMapping = {} for _i in range(cffStandardStringCount): - cffStandardStringMapping[cffStandardStrings[_i]] = _i + cffStandardStringMapping[cffStandardStrings[_i]] = _i -cffISOAdobeStrings = [".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"] +cffISOAdobeStrings = [ + ".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", +] cffISOAdobeStringCount = 229 assert len(cffISOAdobeStrings) == cffISOAdobeStringCount -cffIExpertStrings = [".notdef", "space", "exclamsmall", "Hungarumlautsmall", -"dollaroldstyle", "dollarsuperior", "ampersandsmall", "Acutesmall", -"parenleftsuperior", "parenrightsuperior", "twodotenleader", "onedotenleader", -"comma", "hyphen", "period", "fraction", "zerooldstyle", "oneoldstyle", -"twooldstyle", "threeoldstyle", "fouroldstyle", "fiveoldstyle", "sixoldstyle", -"sevenoldstyle", "eightoldstyle", "nineoldstyle", "colon", "semicolon", -"commasuperior", "threequartersemdash", "periodsuperior", "questionsmall", -"asuperior", "bsuperior", "centsuperior", "dsuperior", "esuperior", "isuperior", -"lsuperior", "msuperior", "nsuperior", "osuperior", "rsuperior", "ssuperior", -"tsuperior", "ff", "fi", "fl", "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", -"onequarter", "onehalf", "threequarters", "questiondownsmall", "oneeighth", -"threeeighths", "fiveeighths", "seveneighths", "onethird", "twothirds", -"zerosuperior", "onesuperior", "twosuperior", "threesuperior", "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"] +cffIExpertStrings = [ + ".notdef", + "space", + "exclamsmall", + "Hungarumlautsmall", + "dollaroldstyle", + "dollarsuperior", + "ampersandsmall", + "Acutesmall", + "parenleftsuperior", + "parenrightsuperior", + "twodotenleader", + "onedotenleader", + "comma", + "hyphen", + "period", + "fraction", + "zerooldstyle", + "oneoldstyle", + "twooldstyle", + "threeoldstyle", + "fouroldstyle", + "fiveoldstyle", + "sixoldstyle", + "sevenoldstyle", + "eightoldstyle", + "nineoldstyle", + "colon", + "semicolon", + "commasuperior", + "threequartersemdash", + "periodsuperior", + "questionsmall", + "asuperior", + "bsuperior", + "centsuperior", + "dsuperior", + "esuperior", + "isuperior", + "lsuperior", + "msuperior", + "nsuperior", + "osuperior", + "rsuperior", + "ssuperior", + "tsuperior", + "ff", + "fi", + "fl", + "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", + "onequarter", + "onehalf", + "threequarters", + "questiondownsmall", + "oneeighth", + "threeeighths", + "fiveeighths", + "seveneighths", + "onethird", + "twothirds", + "zerosuperior", + "onesuperior", + "twosuperior", + "threesuperior", + "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", +] cffExpertStringCount = 166 assert len(cffIExpertStrings) == cffExpertStringCount -cffExpertSubsetStrings = [".notdef", "space", "dollaroldstyle", -"dollarsuperior", "parenleftsuperior", "parenrightsuperior", "twodotenleader", -"onedotenleader", "comma", "hyphen", "period", "fraction", "zerooldstyle", -"oneoldstyle", "twooldstyle", "threeoldstyle", "fouroldstyle", "fiveoldstyle", -"sixoldstyle", "sevenoldstyle", "eightoldstyle", "nineoldstyle", "colon", -"semicolon", "commasuperior", "threequartersemdash", "periodsuperior", -"asuperior", "bsuperior", "centsuperior", "dsuperior", "esuperior", "isuperior", -"lsuperior", "msuperior", "nsuperior", "osuperior", "rsuperior", "ssuperior", -"tsuperior", "ff", "fi", "fl", "ffi", "ffl", "parenleftinferior", -"parenrightinferior", "hyphensuperior", "colonmonetary", "onefitted", "rupiah", -"centoldstyle", "figuredash", "hypheninferior", "onequarter", "onehalf", -"threequarters", "oneeighth", "threeeighths", "fiveeighths", "seveneighths", -"onethird", "twothirds", "zerosuperior", "onesuperior", "twosuperior", -"threesuperior", "foursuperior", "fivesuperior", "sixsuperior", "sevensuperior", -"eightsuperior", "ninesuperior", "zeroinferior", "oneinferior", "twoinferior", -"threeinferior", "fourinferior", "fiveinferior", "sixinferior", "seveninferior", -"eightinferior", "nineinferior", "centinferior", "dollarinferior", -"periodinferior", "commainferior"] +cffExpertSubsetStrings = [ + ".notdef", + "space", + "dollaroldstyle", + "dollarsuperior", + "parenleftsuperior", + "parenrightsuperior", + "twodotenleader", + "onedotenleader", + "comma", + "hyphen", + "period", + "fraction", + "zerooldstyle", + "oneoldstyle", + "twooldstyle", + "threeoldstyle", + "fouroldstyle", + "fiveoldstyle", + "sixoldstyle", + "sevenoldstyle", + "eightoldstyle", + "nineoldstyle", + "colon", + "semicolon", + "commasuperior", + "threequartersemdash", + "periodsuperior", + "asuperior", + "bsuperior", + "centsuperior", + "dsuperior", + "esuperior", + "isuperior", + "lsuperior", + "msuperior", + "nsuperior", + "osuperior", + "rsuperior", + "ssuperior", + "tsuperior", + "ff", + "fi", + "fl", + "ffi", + "ffl", + "parenleftinferior", + "parenrightinferior", + "hyphensuperior", + "colonmonetary", + "onefitted", + "rupiah", + "centoldstyle", + "figuredash", + "hypheninferior", + "onequarter", + "onehalf", + "threequarters", + "oneeighth", + "threeeighths", + "fiveeighths", + "seveneighths", + "onethird", + "twothirds", + "zerosuperior", + "onesuperior", + "twosuperior", + "threesuperior", + "foursuperior", + "fivesuperior", + "sixsuperior", + "sevensuperior", + "eightsuperior", + "ninesuperior", + "zeroinferior", + "oneinferior", + "twoinferior", + "threeinferior", + "fourinferior", + "fiveinferior", + "sixinferior", + "seveninferior", + "eightinferior", + "nineinferior", + "centinferior", + "dollarinferior", + "periodinferior", + "commainferior", +] cffExpertSubsetStringCount = 87 assert len(cffExpertSubsetStrings) == cffExpertSubsetStringCount diff --git a/Lib/fontTools/cffLib/specializer.py b/Lib/fontTools/cffLib/specializer.py index 677f03b77..3d28c82dc 100644 --- a/Lib/fontTools/cffLib/specializer.py +++ b/Lib/fontTools/cffLib/specializer.py @@ -17,751 +17,834 @@ from fontTools.cffLib import maxStackLimit def stringToProgram(string): - if isinstance(string, str): - 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 + if isinstance(string, str): + 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) + return " ".join(str(x) for x in program) def programToCommands(program, getNumRegions=None): - """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 (¯\_(ツ)_/¯). - 'getNumRegions' may be None, or a callable object. It must return the - number of regions. 'getNumRegions' takes a single argument, vsindex. If - the vsindex argument is None, getNumRegions returns the default number - of regions for the charstring, else it returns the numRegions for - the vsindex. - The Charstring may or may not start with a width value. If the first - non-blend operator has an odd number of arguments, then the first argument is - a width, and is popped off. This is complicated with blend operators, as - there may be more than one before the first hint or moveto operator, and each - one reduces several arguments to just one list argument. We have to sum the - number of arguments that are not part of the blend arguments, and all the - 'numBlends' values. We could instead have said that by definition, if there - is a blend operator, there is no width value, since CFF2 Charstrings don't - have width values. I discussed this with Behdad, and we are allowing for an - initial width value in this case because developers may assemble a CFF2 - charstring from CFF Charstrings, which could have width values. - """ + """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 (¯\_(ツ)_/¯). + 'getNumRegions' may be None, or a callable object. It must return the + number of regions. 'getNumRegions' takes a single argument, vsindex. If + the vsindex argument is None, getNumRegions returns the default number + of regions for the charstring, else it returns the numRegions for + the vsindex. + The Charstring may or may not start with a width value. If the first + non-blend operator has an odd number of arguments, then the first argument is + a width, and is popped off. This is complicated with blend operators, as + there may be more than one before the first hint or moveto operator, and each + one reduces several arguments to just one list argument. We have to sum the + number of arguments that are not part of the blend arguments, and all the + 'numBlends' values. We could instead have said that by definition, if there + is a blend operator, there is no width value, since CFF2 Charstrings don't + have width values. I discussed this with Behdad, and we are allowing for an + initial width value in this case because developers may assemble a CFF2 + charstring from CFF Charstrings, which could have width values. + """ - seenWidthOp = False - vsIndex = None - lenBlendStack = 0 - lastBlendIndex = 0 - commands = [] - stack = [] - it = iter(program) + seenWidthOp = False + vsIndex = None + lenBlendStack = 0 + lastBlendIndex = 0 + commands = [] + stack = [] + it = iter(program) - for token in it: - if not isinstance(token, str): - stack.append(token) - continue + for token in it: + if not isinstance(token, str): + stack.append(token) + continue - if token == 'blend': - assert getNumRegions is not None - numSourceFonts = 1 + getNumRegions(vsIndex) - # replace the blend op args on the stack with a single list - # containing all the blend op args. - numBlends = stack[-1] - numBlendArgs = numBlends * numSourceFonts + 1 - # replace first blend op by a list of the blend ops. - stack[-numBlendArgs:] = [stack[-numBlendArgs:]] - lenBlendStack += numBlends + len(stack) - 1 - lastBlendIndex = len(stack) - # if a blend op exists, this is or will be a CFF2 charstring. - continue + if token == "blend": + assert getNumRegions is not None + numSourceFonts = 1 + getNumRegions(vsIndex) + # replace the blend op args on the stack with a single list + # containing all the blend op args. + numBlends = stack[-1] + numBlendArgs = numBlends * numSourceFonts + 1 + # replace first blend op by a list of the blend ops. + stack[-numBlendArgs:] = [stack[-numBlendArgs:]] + lenBlendStack += numBlends + len(stack) - 1 + lastBlendIndex = len(stack) + # if a blend op exists, this is or will be a CFF2 charstring. + continue - elif token == 'vsindex': - vsIndex = stack[-1] - assert type(vsIndex) is int + elif token == "vsindex": + vsIndex = stack[-1] + assert type(vsIndex) is int - elif (not seenWidthOp) and token in {'hstem', 'hstemhm', 'vstem', 'vstemhm', - 'cntrmask', 'hintmask', - 'hmoveto', 'vmoveto', 'rmoveto', - 'endchar'}: - seenWidthOp = True - parity = token in {'hmoveto', 'vmoveto'} - if lenBlendStack: - # lenBlendStack has the number of args represented by the last blend - # arg and all the preceding args. We need to now add the number of - # args following the last blend arg. - numArgs = lenBlendStack + len(stack[lastBlendIndex:]) - else: - numArgs = len(stack) - if numArgs and (numArgs % 2) ^ parity: - width = stack.pop(0) - commands.append(('', [width])) + elif (not seenWidthOp) and token in { + "hstem", + "hstemhm", + "vstem", + "vstemhm", + "cntrmask", + "hintmask", + "hmoveto", + "vmoveto", + "rmoveto", + "endchar", + }: + seenWidthOp = True + parity = token in {"hmoveto", "vmoveto"} + if lenBlendStack: + # lenBlendStack has the number of args represented by the last blend + # arg and all the preceding args. We need to now add the number of + # args following the last blend arg. + numArgs = lenBlendStack + len(stack[lastBlendIndex:]) + else: + numArgs = len(stack) + if numArgs and (numArgs % 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 + 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 _flattenBlendArgs(args): - token_list = [] - for arg in args: - if isinstance(arg, list): - token_list.extend(arg) - token_list.append('blend') - else: - token_list.append(arg) - return token_list + token_list = [] + for arg in args: + if isinstance(arg, list): + token_list.extend(arg) + token_list.append("blend") + else: + token_list.append(arg) + return token_list + 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: - if any(isinstance(arg, list) for arg in args): - args = _flattenBlendArgs(args) - program.extend(args) - if op: - program.append(op) - return program + """Takes a commands list as returned by programToCommands() and converts + it back to a T2CharString program list.""" + program = [] + for op, args in commands: + if any(isinstance(arg, list) for arg in args): + args = _flattenBlendArgs(args) + 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] + """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 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 hmoveto(args): + if len(args) != 1: + raise ValueError(args) + yield ("rmoveto", [args[0], 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 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) - @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 _convertBlendOpToArgs(blendList): - # args is list of blend op args. Since we are supporting - # recursive blend op calls, some of these args may also - # be a list of blend op args, and need to be converted before - # we convert the current list. - if any([isinstance(arg, list) for arg in blendList]): - args = [i for e in blendList for i in - (_convertBlendOpToArgs(e) if isinstance(e,list) else [e]) ] - else: - args = blendList + # args is list of blend op args. Since we are supporting + # recursive blend op calls, some of these args may also + # be a list of blend op args, and need to be converted before + # we convert the current list. + if any([isinstance(arg, list) for arg in blendList]): + args = [ + i + for e in blendList + for i in (_convertBlendOpToArgs(e) if isinstance(e, list) else [e]) + ] + else: + args = blendList - # We now know that blendList contains a blend op argument list, even if - # some of the args are lists that each contain a blend op argument list. - # Convert from: - # [default font arg sequence x0,...,xn] + [delta tuple for x0] + ... + [delta tuple for xn] - # to: - # [ [x0] + [delta tuple for x0], - # ..., - # [xn] + [delta tuple for xn] ] - numBlends = args[-1] - # Can't use args.pop() when the args are being used in a nested list - # comprehension. See calling context - args = args[:-1] + # We now know that blendList contains a blend op argument list, even if + # some of the args are lists that each contain a blend op argument list. + # Convert from: + # [default font arg sequence x0,...,xn] + [delta tuple for x0] + ... + [delta tuple for xn] + # to: + # [ [x0] + [delta tuple for x0], + # ..., + # [xn] + [delta tuple for xn] ] + numBlends = args[-1] + # Can't use args.pop() when the args are being used in a nested list + # comprehension. See calling context + args = args[:-1] - numRegions = len(args)//numBlends - 1 - if not (numBlends*(numRegions + 1) == len(args)): - raise ValueError(blendList) + numRegions = len(args) // numBlends - 1 + if not (numBlends * (numRegions + 1) == len(args)): + raise ValueError(blendList) + + defaultArgs = [[arg] for arg in args[:numBlends]] + deltaArgs = args[numBlends:] + numDeltaValues = len(deltaArgs) + deltaList = [ + deltaArgs[i : i + numRegions] for i in range(0, numDeltaValues, numRegions) + ] + blend_args = [a + b + [1] for a, b in zip(defaultArgs, deltaList)] + return blend_args - defaultArgs = [[arg] for arg in args[:numBlends]] - deltaArgs = args[numBlends:] - numDeltaValues = len(deltaArgs) - deltaList = [ deltaArgs[i:i + numRegions] for i in range(0, numDeltaValues, numRegions) ] - blend_args = [ a + b + [1] for a, b in zip(defaultArgs,deltaList)] - return blend_args def generalizeCommands(commands, ignoreErrors=False): - result = [] - mapping = _GeneralizerDecombinerCommandsMap - for op, args in commands: - # First, generalize any blend args in the arg list. - if any([isinstance(arg, list) for arg in args]): - try: - args = [n for arg in args for n in (_convertBlendOpToArgs(arg) if isinstance(arg, list) else [arg])] - 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 + result = [] + mapping = _GeneralizerDecombinerCommandsMap + for op, args in commands: + # First, generalize any blend args in the arg list. + if any([isinstance(arg, list) for arg in args]): + try: + args = [ + n + for arg in args + for n in ( + _convertBlendOpToArgs(arg) if isinstance(arg, list) else [arg] + ) + ] + 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 + + 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 - 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, getNumRegions=None, **kwargs): - return commandsToProgram(generalizeCommands(programToCommands(program, getNumRegions), **kwargs)) + return commandsToProgram( + generalizeCommands(programToCommands(program, getNumRegions), **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. + """ + 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 - >>> _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 + 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 + if a == "h": + return "v" + if a == "v": + return "h" + assert a in "0r" + return a + def _convertToBlendCmds(args): - # return a list of blend commands, and - # the remaining non-blended args, if any. - num_args = len(args) - stack_use = 0 - new_args = [] - i = 0 - while i < num_args: - arg = args[i] - if not isinstance(arg, list): - new_args.append(arg) - i += 1 - stack_use += 1 - else: - prev_stack_use = stack_use - # The arg is a tuple of blend values. - # These are each (master 0,delta 1..delta n, 1) - # Combine as many successive tuples as we can, - # up to the max stack limit. - num_sources = len(arg) - 1 - blendlist = [arg] - i += 1 - stack_use += 1 + num_sources # 1 for the num_blends arg - while (i < num_args) and isinstance(args[i], list): - blendlist.append(args[i]) - i += 1 - stack_use += num_sources - if stack_use + num_sources > maxStackLimit: - # if we are here, max stack is the CFF2 max stack. - # I use the CFF2 max stack limit here rather than - # the 'maxstack' chosen by the client, as the default - # maxstack may have been used unintentionally. For all - # the other operators, this just produces a little less - # optimization, but here it puts a hard (and low) limit - # on the number of source fonts that can be used. - break - # blendList now contains as many single blend tuples as can be - # combined without exceeding the CFF2 stack limit. - num_blends = len(blendlist) - # append the 'num_blends' default font values - blend_args = [] - for arg in blendlist: - blend_args.append(arg[0]) - for arg in blendlist: - assert arg[-1] == 1 - blend_args.extend(arg[1:-1]) - blend_args.append(num_blends) - new_args.append(blend_args) - stack_use = prev_stack_use + num_blends + # return a list of blend commands, and + # the remaining non-blended args, if any. + num_args = len(args) + stack_use = 0 + new_args = [] + i = 0 + while i < num_args: + arg = args[i] + if not isinstance(arg, list): + new_args.append(arg) + i += 1 + stack_use += 1 + else: + prev_stack_use = stack_use + # The arg is a tuple of blend values. + # These are each (master 0,delta 1..delta n, 1) + # Combine as many successive tuples as we can, + # up to the max stack limit. + num_sources = len(arg) - 1 + blendlist = [arg] + i += 1 + stack_use += 1 + num_sources # 1 for the num_blends arg + while (i < num_args) and isinstance(args[i], list): + blendlist.append(args[i]) + i += 1 + stack_use += num_sources + if stack_use + num_sources > maxStackLimit: + # if we are here, max stack is the CFF2 max stack. + # I use the CFF2 max stack limit here rather than + # the 'maxstack' chosen by the client, as the default + # maxstack may have been used unintentionally. For all + # the other operators, this just produces a little less + # optimization, but here it puts a hard (and low) limit + # on the number of source fonts that can be used. + break + # blendList now contains as many single blend tuples as can be + # combined without exceeding the CFF2 stack limit. + num_blends = len(blendlist) + # append the 'num_blends' default font values + blend_args = [] + for arg in blendlist: + blend_args.append(arg[0]) + for arg in blendlist: + assert arg[-1] == 1 + blend_args.extend(arg[1:-1]) + blend_args.append(num_blends) + new_args.append(blend_args) + stack_use = prev_stack_use + num_blends + + return new_args - return new_args def _addArgs(a, b): - if isinstance(b, list): - if isinstance(a, list): - if len(a) != len(b) or a[-1] != b[-1]: - raise ValueError() - return [_addArgs(va, vb) for va,vb in zip(a[:-1], b[:-1])] + [a[-1]] - else: - a, b = b, a - if isinstance(a, list): - assert a[-1] == 1 - return [_addArgs(a[0], b)] + a[1:] - return a + b + if isinstance(b, list): + if isinstance(a, list): + if len(a) != len(b) or a[-1] != b[-1]: + raise ValueError() + return [_addArgs(va, vb) for va, vb in zip(a[:-1], b[:-1])] + [a[-1]] + else: + a, b = b, a + if isinstance(a, list): + assert a[-1] == 1 + return [_addArgs(a[0], b)] + a[1:] + return a + b -def specializeCommands(commands, - ignoreErrors=False, - generalizeFirst=True, - preserveTopology=False, - maxstack=48): +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. - # - # 7. For any args which are blend lists, convert them to a blend command. + # 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. + # + # 7. For any args which are blend lists, convert them to a blend command. + # 0. Generalize commands. + if generalizeFirst: + commands = generalizeCommands(commands, ignoreErrors=ignoreErrors) + else: + commands = list(commands) # Make copy since we modify in-place later. - # 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] - # 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] - # 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 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 - 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" - # 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... - # Some other redundancies change topology (point numbers). - if not preserveTopology: - for i in range(len(commands)-1, -1, -1): - op, args = commands[i] + # A 0lineto can be deleted. + if op == "0lineto": + del commands[i] + continue - # 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... + # Merge adjacent hlineto's and vlineto's. + # In CFF2 charstrings from variable fonts, each + # arg item may be a list of blendable values, one from + # each source font. + 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 + try: + new_args = [_addArgs(args[0], other_args[0])] + except ValueError: + continue + commands[i - 1] = (op, new_args) + del commands[i] + continue - # A 0lineto can be deleted. - if op == '0lineto': - 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] - # Merge adjacent hlineto's and vlineto's. - # In CFF2 charstrings from variable fonts, each - # arg item may be a list of blendable values, one from - # each source font. - 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 - try: - new_args = [_addArgs(args[0], other_args[0])] - except ValueError: - continue - commands[i-1] = (op, new_args) - del commands[i] - continue + 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 - # 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[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 - 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 + # 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 - 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 + # 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" - # 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 + elif (op1, op2) in {("rlineto", "rlinecurve"), ("rrcurveto", "rcurveline")}: + new_op = op2 - # 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} == {"vlineto", "hlineto"}: + new_op = op1 - elif (op1, op2) in {('rlineto', 'rlinecurve'), ('rrcurveto', 'rcurveline')}: - new_op = op2 + elif "curveto" == op1[2:] == op2[2:]: + d0, d1 = op1[:2] + d2, d3 = op2[:2] - elif {op1, op2} == {'vlineto', 'hlineto'}: - new_op = op1 + if d1 == "r" or d2 == "r" or d0 == d3 == "r": + continue - elif 'curveto' == op1[2:] == op2[2:]: - d0, d1 = op1[:2] - d2, d3 = op2[:2] + 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 d1 == 'r' or d2 == 'r' or d0 == d3 == 'r': - continue + # Make sure the stack depth does not exceed (maxstack - 1), so + # that subroutinizer can insert subroutine calls at any point. + if new_op and len(args1) + len(args2) < maxstack: + commands[i - 1] = (new_op, args1 + args2) + del commands[i] - 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' + # 6. Resolve any remaining made-up operators into real operators. + for i in range(len(commands)): + op, args = commands[i] - # Make sure the stack depth does not exceed (maxstack - 1), so - # that subroutinizer can insert subroutine calls at any point. - if new_op and len(args1) + len(args2) < maxstack: - commands[i-1] = (new_op, args1+args2) - del commands[i] + if op in {"0moveto", "0lineto"}: + commands[i] = "h" + op[1:], args + continue - # 6. Resolve any remaining made-up operators into real operators. - for i in range(len(commands)): - op,args = commands[i] + 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 op in {'0moveto', '0lineto'}: - commands[i] = 'h'+op[1:], args - continue + 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:] - 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) + commands[i] = op0 + op1 + "curveto", args + continue - 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:] + # 7. For any series of args which are blend lists, convert the series to a single blend arg. + for i in range(len(commands)): + op, args = commands[i] + if any(isinstance(arg, list) for arg in args): + commands[i] = op, _convertToBlendCmds(args) - commands[i] = op0+op1+'curveto', args - continue + return commands - # 7. For any series of args which are blend lists, convert the series to a single blend arg. - for i in range(len(commands)): - op, args = commands[i] - if any(isinstance(arg, list) for arg in args): - commands[i] = op, _convertToBlendCmds(args) - - return commands def specializeProgram(program, getNumRegions=None, **kwargs): - return commandsToProgram(specializeCommands(programToCommands(program, getNumRegions), **kwargs)) + return commandsToProgram( + specializeCommands(programToCommands(program, getNumRegions), **kwargs) + ) -if __name__ == '__main__': - import sys - if len(sys.argv) == 1: - import doctest - sys.exit(doctest.testmod().failed) +if __name__ == "__main__": + import sys - import argparse + if len(sys.argv) == 1: + import doctest - parser = argparse.ArgumentParser( - "fonttools cffLib.specialer", description="CFF CharString generalizer/specializer") - parser.add_argument( - "program", metavar="command", nargs="*", help="Commands.") - parser.add_argument( - "--num-regions", metavar="NumRegions", nargs="*", default=None, - help="Number of variable-font regions for blend opertaions.") + sys.exit(doctest.testmod().failed) - options = parser.parse_args(sys.argv[1:]) + import argparse - getNumRegions = None if options.num_regions is None else lambda vsIndex: int(options.num_regions[0 if vsIndex is None else vsIndex]) + parser = argparse.ArgumentParser( + "fonttools cffLib.specialer", + description="CFF CharString generalizer/specializer", + ) + parser.add_argument("program", metavar="command", nargs="*", help="Commands.") + parser.add_argument( + "--num-regions", + metavar="NumRegions", + nargs="*", + default=None, + help="Number of variable-font regions for blend opertaions.", + ) - program = stringToProgram(options.program) - print("Program:"); print(programToString(program)) - commands = programToCommands(program, getNumRegions) - print("Commands:"); print(commands) - program2 = commandsToProgram(commands) - print("Program from commands:"); print(programToString(program2)) - assert program == program2 - print("Generalized program:"); print(programToString(generalizeProgram(program, getNumRegions))) - print("Specialized program:"); print(programToString(specializeProgram(program, getNumRegions))) + options = parser.parse_args(sys.argv[1:]) + + getNumRegions = ( + None + if options.num_regions is None + else lambda vsIndex: int(options.num_regions[0 if vsIndex is None else vsIndex]) + ) + + program = stringToProgram(options.program) + print("Program:") + print(programToString(program)) + commands = programToCommands(program, getNumRegions) + print("Commands:") + print(commands) + program2 = commandsToProgram(commands) + print("Program from commands:") + print(programToString(program2)) + assert program == program2 + print("Generalized program:") + print(programToString(generalizeProgram(program, getNumRegions))) + print("Specialized program:") + print(programToString(specializeProgram(program, getNumRegions))) diff --git a/Lib/fontTools/cffLib/width.py b/Lib/fontTools/cffLib/width.py index 303c94620..c0a746b69 100644 --- a/Lib/fontTools/cffLib/width.py +++ b/Lib/fontTools/cffLib/width.py @@ -14,170 +14,196 @@ from functools import reduce class missingdict(dict): - def __init__(self, missing_func): - self.missing_func = missing_func - def __missing__(self, v): - return self.missing_func(v) + 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) + keys = sorted(f.keys()) + minx, maxx = keys[0], keys[-1] - 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) + total = reduce(op, f.values(), start) - out = missingdict(missing) + 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) - v = start - for x in domain: - v = op(v, f[x]) - out[x] = v + out = missingdict(missing) + + v = start + for x in domain: + v = op(v, f[x]) + out[x] = v + + return out - return out def byteCost(widths, default, nominal): - if not hasattr(widths, 'items'): - d = defaultdict(int) - for w in widths: - d[w] += 1 - widths = d + 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 + 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.""" + """Bruteforce version. Veeeeeeeeeeeeeeeeery slow. Only works for smallests of fonts.""" - d = defaultdict(int) - for w in widths: - d[w] += 1 + d = defaultdict(int) + for w in widths: + d[w] += 1 - # Maximum number of bytes using default can possibly save - maxDefaultAdvantage = 5 * max(d.values()) + # 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)) + minw, maxw = min(widths), max(widths) + domain = list(range(minw, maxw + 1)) - bestCostWithoutDefault = min(byteCost(widths, None, nominal) for nominal in domain) + 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 + 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 + 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. + """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.""" + 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)) + if not hasattr(widths, "items"): + d = defaultdict(int) + for w in widths: + d[w] += 1 + widths = d - # 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) + keys = sorted(widths.keys()) + minw, maxw = keys[0], keys[-1] + domain = list(range(minw, maxw + 1)) - # 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]) + # 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-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])) + # 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]) - # Combined cost per nominal choice. - bestCost = missingdict(lambda x: nomnCost[x] - dfltCost[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])) - # Best nominal. - nominal = min(domain, key=lambda x: bestCost[x]) + # Combined cost per nominal choice. + bestCost = missingdict(lambda x: nomnCost[x] - dfltCost[x]) - # Work back the best default. - bestC = bestCost[nominal] - dfltC = nomnCost[nominal] - bestCost[nominal] - ends = [] - if dfltC == dfltCostU[nominal]: - starts = [nominal, nominal-108, nominal-1132] - for start in starts: - while cumMaxU[start] and cumMaxU[start] == cumMaxU[start-1]: - start -= 1 - ends.append(start) - else: - starts = [nominal, nominal+108, nominal+1132] - 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)) + # 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 - 1132] + for start in starts: + while cumMaxU[start] and cumMaxU[start] == cumMaxU[start - 1]: + start -= 1 + ends.append(start) + else: + starts = [nominal, nominal + 108, nominal + 1132] + 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 - return default, nominal def main(args=None): - """Calculate optimum defaultWidthX/nominalWidthX values""" + """Calculate optimum defaultWidthX/nominalWidthX values""" - import argparse - parser = argparse.ArgumentParser( - "fonttools cffLib.width", - description=main.__doc__, - ) - parser.add_argument('inputs', metavar='FILE', type=str, nargs='+', - help="Input TTF files") - parser.add_argument('-b', '--brute-force', dest="brute", action="store_true", - help="Use brute-force approach (VERY slow)") + import argparse - args = parser.parse_args(args) + parser = argparse.ArgumentParser( + "fonttools cffLib.width", + description=main.__doc__, + ) + parser.add_argument( + "inputs", metavar="FILE", type=str, nargs="+", help="Input TTF files" + ) + parser.add_argument( + "-b", + "--brute-force", + dest="brute", + action="store_true", + help="Use brute-force approach (VERY slow)", + ) - for fontfile in args.inputs: - font = TTFont(fontfile) - hmtx = font['hmtx'] - widths = [m[0] for m in hmtx.metrics.values()] - if args.brute: - default, nominal = optimizeWidthsBruteforce(widths) - else: - default, nominal = optimizeWidths(widths) - print("glyphs=%d default=%d nominal=%d byteCost=%d" % (len(widths), default, nominal, byteCost(widths, default, nominal))) + args = parser.parse_args(args) -if __name__ == '__main__': - import sys - if len(sys.argv) == 1: - import doctest - sys.exit(doctest.testmod().failed) - main() + for fontfile in args.inputs: + font = TTFont(fontfile) + hmtx = font["hmtx"] + widths = [m[0] for m in hmtx.metrics.values()] + if args.brute: + default, nominal = optimizeWidthsBruteforce(widths) + else: + default, nominal = optimizeWidths(widths) + print( + "glyphs=%d default=%d nominal=%d byteCost=%d" + % (len(widths), default, nominal, byteCost(widths, default, nominal)) + ) + + +if __name__ == "__main__": + import sys + + if len(sys.argv) == 1: + import doctest + + sys.exit(doctest.testmod().failed) + main() diff --git a/Lib/fontTools/colorLib/errors.py b/Lib/fontTools/colorLib/errors.py index a0bdda174..18cbebbaf 100644 --- a/Lib/fontTools/colorLib/errors.py +++ b/Lib/fontTools/colorLib/errors.py @@ -1,3 +1,2 @@ - class ColorLibError(Exception): pass diff --git a/Lib/fontTools/colorLib/table_builder.py b/Lib/fontTools/colorLib/table_builder.py index 763115b96..f1e182c43 100644 --- a/Lib/fontTools/colorLib/table_builder.py +++ b/Lib/fontTools/colorLib/table_builder.py @@ -67,9 +67,7 @@ def _split_format(cls, source): assert isinstance( fmt, collections.abc.Hashable ), f"{cls} Format is not hashable: {fmt!r}" - assert ( - fmt in cls.convertersByName - ), f"{cls} invalid Format: {fmt!r}" + assert fmt in cls.convertersByName, f"{cls} invalid Format: {fmt!r}" return fmt, remainder diff --git a/Lib/fontTools/cu2qu/benchmark.py b/Lib/fontTools/cu2qu/benchmark.py index 2eac756a8..63e7433e9 100644 --- a/Lib/fontTools/cu2qu/benchmark.py +++ b/Lib/fontTools/cu2qu/benchmark.py @@ -6,44 +6,52 @@ import timeit MAX_ERR = 5 + def generate_curve(): return [ tuple(float(random.randint(0, 2048)) for coord in range(2)) - for point in range(4)] + for point in range(4) + ] + def setup_curve_to_quadratic(): return generate_curve(), MAX_ERR + def setup_curves_to_quadratic(): num_curves = 3 - return ( - [generate_curve() for curve in range(num_curves)], - [MAX_ERR] * num_curves) + return ([generate_curve() for curve in range(num_curves)], [MAX_ERR] * num_curves) + def run_benchmark( - benchmark_module, module, function, setup_suffix='', repeat=5, number=1000): - setup_func = 'setup_' + function + benchmark_module, module, function, setup_suffix="", repeat=5, number=1000 +): + setup_func = "setup_" + function if setup_suffix: - print('%s with %s:' % (function, setup_suffix), end='') - setup_func += '_' + setup_suffix + print("%s with %s:" % (function, setup_suffix), end="") + setup_func += "_" + setup_suffix else: - print('%s:' % function, end='') + print("%s:" % function, end="") def wrapper(function, setup_func): function = globals()[function] setup_func = globals()[setup_func] + def wrapped(): return function(*setup_func()) + return wrapped + results = timeit.repeat(wrapper(function, setup_func), repeat=repeat, number=number) - print('\t%5.1fus' % (min(results) * 1000000. / number)) + print("\t%5.1fus" % (min(results) * 1000000.0 / number)) + def main(): """Benchmark the cu2qu algorithm performance.""" - run_benchmark('cu2qu.benchmark', 'cu2qu', 'curve_to_quadratic') - run_benchmark('cu2qu.benchmark', 'cu2qu', 'curves_to_quadratic') + run_benchmark("cu2qu.benchmark", "cu2qu", "curve_to_quadratic") + run_benchmark("cu2qu.benchmark", "cu2qu", "curves_to_quadratic") -if __name__ == '__main__': +if __name__ == "__main__": random.seed(1) main() diff --git a/Lib/fontTools/cu2qu/cli.py b/Lib/fontTools/cu2qu/cli.py index 34520fc04..a57dd0da9 100644 --- a/Lib/fontTools/cu2qu/cli.py +++ b/Lib/fontTools/cu2qu/cli.py @@ -37,7 +37,7 @@ def open_ufo(path): def _font_to_quadratic(input_path, output_path=None, **kwargs): ufo = open_ufo(input_path) - logger.info('Converting curves for %s', input_path) + logger.info("Converting curves for %s", input_path) if font_to_quadratic(ufo, **kwargs): logger.info("Saving %s", output_path) if output_path: @@ -67,13 +67,13 @@ def _copytree(input_path, output_path): def main(args=None): """Convert a UFO font from cubic to quadratic curves""" parser = argparse.ArgumentParser(prog="cu2qu") - parser.add_argument( - "--version", action="version", version=fontTools.__version__) + parser.add_argument("--version", action="version", version=fontTools.__version__) parser.add_argument( "infiles", nargs="+", metavar="INPUT", - help="one or more input UFO source file(s).") + help="one or more input UFO source file(s).", + ) parser.add_argument("-v", "--verbose", action="count", default=0) parser.add_argument( "-e", @@ -81,19 +81,21 @@ def main(args=None): type=float, metavar="ERROR", default=None, - help="maxiumum approximation error measured in EM (default: 0.001)") + help="maxiumum approximation error measured in EM (default: 0.001)", + ) parser.add_argument( "--keep-direction", dest="reverse_direction", action="store_false", - help="do not reverse the contour direction") + help="do not reverse the contour direction", + ) mode_parser = parser.add_mutually_exclusive_group() mode_parser.add_argument( "-i", "--interpolatable", action="store_true", - help="whether curve conversion should keep interpolation compatibility" + help="whether curve conversion should keep interpolation compatibility", ) mode_parser.add_argument( "-j", @@ -103,7 +105,8 @@ def main(args=None): default=1, const=_cpu_count(), metavar="N", - help="Convert using N multiple processes (default: %(default)s)") + help="Convert using N multiple processes (default: %(default)s)", + ) output_parser = parser.add_mutually_exclusive_group() output_parser.add_argument( @@ -111,14 +114,18 @@ def main(args=None): "--output-file", default=None, metavar="OUTPUT", - help=("output filename for the converted UFO. By default fonts are " - "modified in place. This only works with a single input.")) + help=( + "output filename for the converted UFO. By default fonts are " + "modified in place. This only works with a single input." + ), + ) output_parser.add_argument( "-d", "--output-dir", default=None, metavar="DIRECTORY", - help="output directory where to save converted UFOs") + help="output directory where to save converted UFOs", + ) options = parser.parse_args(args) @@ -143,8 +150,7 @@ def main(args=None): elif not os.path.isdir(output_dir): parser.error("'%s' is not a directory" % output_dir) output_paths = [ - os.path.join(output_dir, os.path.basename(p)) - for p in options.infiles + os.path.join(output_dir, os.path.basename(p)) for p in options.infiles ] elif options.output_file: output_paths = [options.output_file] @@ -152,12 +158,14 @@ def main(args=None): # save in-place output_paths = [None] * len(options.infiles) - kwargs = dict(dump_stats=options.verbose > 0, - max_err_em=options.conversion_error, - reverse_direction=options.reverse_direction) + kwargs = dict( + dump_stats=options.verbose > 0, + max_err_em=options.conversion_error, + reverse_direction=options.reverse_direction, + ) if options.interpolatable: - logger.info('Converting curves compatibly') + logger.info("Converting curves compatibly") ufos = [open_ufo(infile) for infile in options.infiles] if fonts_to_quadratic(ufos, **kwargs): for ufo, output_path in zip(ufos, output_paths): @@ -171,11 +179,10 @@ def main(args=None): if output_path: _copytree(input_path, output_path) else: - jobs = min(len(options.infiles), - options.jobs) if options.jobs > 1 else 1 + jobs = min(len(options.infiles), options.jobs) if options.jobs > 1 else 1 if jobs > 1: func = partial(_font_to_quadratic, **kwargs) - logger.info('Running %d parallel processes', jobs) + logger.info("Running %d parallel processes", jobs) with closing(mp.Pool(jobs)) as pool: pool.starmap(func, zip(options.infiles, output_paths)) else: diff --git a/Lib/fontTools/cu2qu/cu2qu.py b/Lib/fontTools/cu2qu/cu2qu.py index 3301681cc..9dd75656f 100644 --- a/Lib/fontTools/cu2qu/cu2qu.py +++ b/Lib/fontTools/cu2qu/cu2qu.py @@ -1,5 +1,5 @@ -#cython: language_level=3 -#distutils: define_macros=CYTHON_TRACE_NOGIL=1 +# cython: language_level=3 +# distutils: define_macros=CYTHON_TRACE_NOGIL=1 # Copyright 2015 Google Inc. All Rights Reserved. # @@ -26,7 +26,7 @@ import math from .errors import Error as Cu2QuError, ApproxNotFoundError -__all__ = ['curve_to_quadratic', 'curves_to_quadratic'] +__all__ = ["curve_to_quadratic", "curves_to_quadratic"] MAX_N = 100 @@ -61,7 +61,9 @@ def dot(v1, v2): @cython.cfunc @cython.inline @cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex) -@cython.locals(_1=cython.complex, _2=cython.complex, _3=cython.complex, _4=cython.complex) +@cython.locals( + _1=cython.complex, _2=cython.complex, _3=cython.complex, _4=cython.complex +) def calc_cubic_points(a, b, c, d): _1 = d _2 = (c / 3.0) + d @@ -72,7 +74,9 @@ def calc_cubic_points(a, b, c, d): @cython.cfunc @cython.inline -@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex) +@cython.locals( + p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex +) @cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex) def calc_cubic_parameters(p0, p1, p2, p3): c = (p1 - p0) * 3.0 @@ -83,7 +87,9 @@ def calc_cubic_parameters(p0, p1, p2, p3): @cython.cfunc -@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex) +@cython.locals( + p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex +) def split_cubic_into_n_iter(p0, p1, p2, p3, n): """Split a cubic Bezier into n equal parts. @@ -112,13 +118,23 @@ def split_cubic_into_n_iter(p0, p1, p2, p3, n): a, b = split_cubic_into_two(p0, p1, p2, p3) return iter(split_cubic_into_three(*a) + split_cubic_into_three(*b)) - return _split_cubic_into_n_gen(p0,p1,p2,p3,n) + return _split_cubic_into_n_gen(p0, p1, p2, p3, n) -@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex, n=cython.int) +@cython.locals( + p0=cython.complex, + p1=cython.complex, + p2=cython.complex, + p3=cython.complex, + n=cython.int, +) @cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex) -@cython.locals(dt=cython.double, delta_2=cython.double, delta_3=cython.double, i=cython.int) -@cython.locals(a1=cython.complex, b1=cython.complex, c1=cython.complex, d1=cython.complex) +@cython.locals( + dt=cython.double, delta_2=cython.double, delta_3=cython.double, i=cython.int +) +@cython.locals( + a1=cython.complex, b1=cython.complex, c1=cython.complex, d1=cython.complex +) def _split_cubic_into_n_gen(p0, p1, p2, p3, n): a, b, c, d = calc_cubic_parameters(p0, p1, p2, p3) dt = 1 / n @@ -129,13 +145,15 @@ def _split_cubic_into_n_gen(p0, p1, p2, p3, n): t1_2 = t1 * t1 # calc new a, b, c and d a1 = a * delta_3 - b1 = (3*a*t1 + b) * delta_2 - c1 = (2*b*t1 + c + 3*a*t1_2) * dt - d1 = a*t1*t1_2 + b*t1_2 + c*t1 + d + b1 = (3 * a * t1 + b) * delta_2 + c1 = (2 * b * t1 + c + 3 * a * t1_2) * dt + d1 = a * t1 * t1_2 + b * t1_2 + c * t1 + d yield calc_cubic_points(a1, b1, c1, d1) -@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex) +@cython.locals( + p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex +) @cython.locals(mid=cython.complex, deriv3=cython.complex) def split_cubic_into_two(p0, p1, p2, p3): """Split a cubic Bezier into two equal parts. @@ -152,15 +170,28 @@ def split_cubic_into_two(p0, p1, p2, p3): tuple: Two cubic Beziers (each expressed as a tuple of four complex values). """ - 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)) + mid = (p0 + 3 * (p1 + p2) + p3) * 0.125 + deriv3 = (p3 + p2 - p1 - p0) * 0.125 + return ( + (p0, (p0 + p1) * 0.5, mid - deriv3, mid), + (mid, mid + deriv3, (p2 + p3) * 0.5, p3), + ) -@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex, _27=cython.double) -@cython.locals(mid1=cython.complex, deriv1=cython.complex, mid2=cython.complex, deriv2=cython.complex) -def split_cubic_into_three(p0, p1, p2, p3, _27=1/27): +@cython.locals( + p0=cython.complex, + p1=cython.complex, + p2=cython.complex, + p3=cython.complex, + _27=cython.double, +) +@cython.locals( + mid1=cython.complex, + deriv1=cython.complex, + mid2=cython.complex, + deriv2=cython.complex, +) +def split_cubic_into_three(p0, p1, p2, p3, _27=1 / 27): """Split a cubic Bezier into three equal parts. Splits the curve into three equal parts at t = 1/3 and t = 2/3 @@ -177,17 +208,25 @@ def split_cubic_into_three(p0, p1, p2, p3, _27=1/27): """ # we define 1/27 as a keyword argument so that it will be evaluated only # once but still in the scope of this function - mid1 = (8*p0 + 12*p1 + 6*p2 + p3) * _27 - deriv1 = (p3 + 3*p2 - 4*p0) * _27 - mid2 = (p0 + 6*p1 + 12*p2 + 8*p3) * _27 - deriv2 = (4*p3 - 3*p1 - p0) * _27 - return ((p0, (2*p0 + p1) / 3.0, mid1 - deriv1, mid1), - (mid1, mid1 + deriv1, mid2 - deriv2, mid2), - (mid2, mid2 + deriv2, (p2 + 2*p3) / 3.0, p3)) + mid1 = (8 * p0 + 12 * p1 + 6 * p2 + p3) * _27 + deriv1 = (p3 + 3 * p2 - 4 * p0) * _27 + mid2 = (p0 + 6 * p1 + 12 * p2 + 8 * p3) * _27 + deriv2 = (4 * p3 - 3 * p1 - p0) * _27 + return ( + (p0, (2 * p0 + p1) / 3.0, mid1 - deriv1, mid1), + (mid1, mid1 + deriv1, mid2 - deriv2, mid2), + (mid2, mid2 + deriv2, (p2 + 2 * p3) / 3.0, p3), + ) @cython.returns(cython.complex) -@cython.locals(t=cython.double, p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex) +@cython.locals( + t=cython.double, + p0=cython.complex, + p1=cython.complex, + p2=cython.complex, + p3=cython.complex, +) @cython.locals(_p1=cython.complex, _p2=cython.complex) def cubic_approx_control(t, p0, p1, p2, p3): """Approximate a cubic Bezier using a quadratic one. @@ -235,7 +274,13 @@ def calc_intersect(a, b, c, d): @cython.cfunc @cython.returns(cython.int) -@cython.locals(tolerance=cython.double, p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex) +@cython.locals( + tolerance=cython.double, + p0=cython.complex, + p1=cython.complex, + p2=cython.complex, + p3=cython.complex, +) @cython.locals(mid=cython.complex, deriv3=cython.complex) def cubic_farthest_fit_inside(p0, p1, p2, p3, tolerance): """Check if a cubic Bezier lies within a given distance of the origin. @@ -260,18 +305,25 @@ def cubic_farthest_fit_inside(p0, p1, p2, p3, tolerance): return True # Split. - mid = (p0 + 3 * (p1 + p2) + p3) * .125 + mid = (p0 + 3 * (p1 + p2) + p3) * 0.125 if abs(mid) > tolerance: return False - deriv3 = (p3 + p2 - p1 - p0) * .125 - return (cubic_farthest_fit_inside(p0, (p0+p1)*.5, mid-deriv3, mid, tolerance) and - cubic_farthest_fit_inside(mid, mid+deriv3, (p2+p3)*.5, p3, tolerance)) + deriv3 = (p3 + p2 - p1 - p0) * 0.125 + return cubic_farthest_fit_inside( + p0, (p0 + p1) * 0.5, mid - deriv3, mid, tolerance + ) and cubic_farthest_fit_inside(mid, mid + deriv3, (p2 + p3) * 0.5, p3, tolerance) @cython.cfunc @cython.locals(tolerance=cython.double, _2_3=cython.double) -@cython.locals(q1=cython.complex, c0=cython.complex, c1=cython.complex, c2=cython.complex, c3=cython.complex) -def cubic_approx_quadratic(cubic, tolerance, _2_3=2/3): +@cython.locals( + q1=cython.complex, + c0=cython.complex, + c1=cython.complex, + c2=cython.complex, + c3=cython.complex, +) +def cubic_approx_quadratic(cubic, tolerance, _2_3=2 / 3): """Approximate a cubic Bezier with a single quadratic within a given tolerance. Args: @@ -294,10 +346,7 @@ def cubic_approx_quadratic(cubic, tolerance, _2_3=2/3): c3 = cubic[3] c1 = c0 + (q1 - c0) * _2_3 c2 = c3 + (q1 - c3) * _2_3 - if not cubic_farthest_fit_inside(0, - c1 - cubic[1], - c2 - cubic[2], - 0, tolerance): + if not cubic_farthest_fit_inside(0, c1 - cubic[1], c2 - cubic[2], 0, tolerance): return None return c0, q1, c3 @@ -305,9 +354,17 @@ def cubic_approx_quadratic(cubic, tolerance, _2_3=2/3): @cython.cfunc @cython.locals(n=cython.int, tolerance=cython.double, _2_3=cython.double) @cython.locals(i=cython.int) -@cython.locals(c0=cython.complex, c1=cython.complex, c2=cython.complex, c3=cython.complex) -@cython.locals(q0=cython.complex, q1=cython.complex, next_q1=cython.complex, q2=cython.complex, d1=cython.complex) -def cubic_approx_spline(cubic, n, tolerance, _2_3=2/3): +@cython.locals( + c0=cython.complex, c1=cython.complex, c2=cython.complex, c3=cython.complex +) +@cython.locals( + q0=cython.complex, + q1=cython.complex, + next_q1=cython.complex, + q2=cython.complex, + d1=cython.complex, +) +def cubic_approx_spline(cubic, n, tolerance, _2_3=2 / 3): """Approximate a cubic Bezier curve with a spline of n quadratics. Args: @@ -335,7 +392,7 @@ def cubic_approx_spline(cubic, n, tolerance, _2_3=2/3): q2 = cubic[0] d1 = 0j spline = [cubic[0], next_q1] - for i in range(1, n+1): + for i in range(1, n + 1): # Current cubic to convert c0, c1, c2, c3 = next_cubic @@ -345,9 +402,9 @@ def cubic_approx_spline(cubic, n, tolerance, _2_3=2/3): q1 = next_q1 if i < n: next_cubic = next(cubics) - next_q1 = cubic_approx_control(i / (n-1), *next_cubic) + next_q1 = cubic_approx_control(i / (n - 1), *next_cubic) spline.append(next_q1) - q2 = (q1 + next_q1) * .5 + q2 = (q1 + next_q1) * 0.5 else: q2 = c3 @@ -355,12 +412,9 @@ def cubic_approx_spline(cubic, n, tolerance, _2_3=2/3): d0 = d1 d1 = q2 - c3 - if (abs(d1) > tolerance or - not cubic_farthest_fit_inside(d0, - q0 + (q1 - q0) * _2_3 - c1, - q2 + (q1 - q2) * _2_3 - c2, - d1, - tolerance)): + if abs(d1) > tolerance or not cubic_farthest_fit_inside( + d0, q0 + (q1 - q0) * _2_3 - c1, q2 + (q1 - q2) * _2_3 - c2, d1, tolerance + ): return None spline.append(cubic[3]) @@ -394,7 +448,6 @@ def curve_to_quadratic(curve, max_err): raise ApproxNotFoundError(curve) - @cython.locals(l=cython.int, last_i=cython.int, i=cython.int) def curves_to_quadratic(curves, max_errors): """Return quadratic Bezier splines approximating the input cubic Beziers. @@ -448,5 +501,3 @@ def curves_to_quadratic(curves, max_errors): return [[(s.real, s.imag) for s in spline] for spline in splines] raise ApproxNotFoundError(curves) - - diff --git a/Lib/fontTools/cu2qu/errors.py b/Lib/fontTools/cu2qu/errors.py index 74c4c2271..fa3dc4293 100644 --- a/Lib/fontTools/cu2qu/errors.py +++ b/Lib/fontTools/cu2qu/errors.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. + class Error(Exception): """Base Cu2Qu exception class for all other errors.""" diff --git a/Lib/fontTools/cu2qu/ufo.py b/Lib/fontTools/cu2qu/ufo.py index 447de7bbf..660d4410a 100644 --- a/Lib/fontTools/cu2qu/ufo.py +++ b/Lib/fontTools/cu2qu/ufo.py @@ -30,12 +30,15 @@ from fontTools.pens.reverseContourPen import ReverseContourPen from . import curves_to_quadratic from .errors import ( - UnequalZipLengthsError, IncompatibleSegmentNumberError, - IncompatibleSegmentTypesError, IncompatibleGlyphsError, - IncompatibleFontsError) + UnequalZipLengthsError, + IncompatibleSegmentNumberError, + IncompatibleSegmentTypesError, + IncompatibleGlyphsError, + IncompatibleFontsError, +) -__all__ = ['fonts_to_quadratic', 'font_to_quadratic'] +__all__ = ["fonts_to_quadratic", "font_to_quadratic"] # The default approximation error below is a relative value (1/1000 of the EM square). # Later on, we convert it to absolute font units by multiplying it by a font's UPEM @@ -47,6 +50,8 @@ logger = logging.getLogger(__name__) _zip = zip + + def zip(*args): """Ensure each argument to zip has the same length. Also make sure a list is returned for python 2/3 compatibility. @@ -69,27 +74,27 @@ class GetSegmentsPen(AbstractPen): self.segments = [] def _add_segment(self, tag, *args): - if tag in ['move', 'line', 'qcurve', 'curve']: + if tag in ["move", "line", "qcurve", "curve"]: self._last_pt = args[-1] self.segments.append((tag, args)) def moveTo(self, pt): - self._add_segment('move', pt) + self._add_segment("move", pt) def lineTo(self, pt): - self._add_segment('line', pt) + self._add_segment("line", pt) def qCurveTo(self, *points): - self._add_segment('qcurve', self._last_pt, *points) + self._add_segment("qcurve", self._last_pt, *points) def curveTo(self, *points): - self._add_segment('curve', self._last_pt, *points) + self._add_segment("curve", self._last_pt, *points) def closePath(self): - self._add_segment('close') + self._add_segment("close") def endPath(self): - self._add_segment('end') + self._add_segment("end") def addComponent(self, glyphName, transformation): pass @@ -122,17 +127,17 @@ def _set_segments(glyph, segments, reverse_direction): if reverse_direction: pen = ReverseContourPen(pen) for tag, args in segments: - if tag == 'move': + if tag == "move": pen.moveTo(*args) - elif tag == 'line': + elif tag == "line": pen.lineTo(*args) - elif tag == 'curve': + elif tag == "curve": pen.curveTo(*args[1:]) - elif tag == 'qcurve': + elif tag == "qcurve": pen.qCurveTo(*args[1:]) - elif tag == 'close': + elif tag == "close": pen.closePath() - elif tag == 'end': + elif tag == "end": pen.endPath() else: raise AssertionError('Unhandled segment type "%s"' % tag) @@ -141,16 +146,16 @@ def _set_segments(glyph, segments, reverse_direction): def _segments_to_quadratic(segments, max_err, stats): """Return quadratic approximations of cubic segments.""" - assert all(s[0] == 'curve' for s in segments), 'Non-cubic given to convert' + assert all(s[0] == "curve" for s in segments), "Non-cubic given to convert" new_points = curves_to_quadratic([s[1] for s in segments], max_err) n = len(new_points[0]) - assert all(len(s) == n for s in new_points[1:]), 'Converted incompatibly' + assert all(len(s) == n for s in new_points[1:]), "Converted incompatibly" spline_length = str(n - 2) stats[spline_length] = stats.get(spline_length, 0) + 1 - return [('qcurve', p) for p in new_points] + return [("qcurve", p) for p in new_points] def _glyphs_to_quadratic(glyphs, max_err, reverse_direction, stats): @@ -176,7 +181,7 @@ def _glyphs_to_quadratic(glyphs, max_err, reverse_direction, stats): tag = segments[0][0] if not all(s[0] == tag for s in segments[1:]): incompatible[i] = [s[0] for s in segments] - elif tag == 'curve': + elif tag == "curve": segments = _segments_to_quadratic(segments, max_err, stats) glyphs_modified = True new_segments_by_location.append(segments) @@ -191,8 +196,7 @@ def _glyphs_to_quadratic(glyphs, max_err, reverse_direction, stats): return glyphs_modified -def glyphs_to_quadratic( - glyphs, max_err=None, reverse_direction=False, stats=None): +def glyphs_to_quadratic(glyphs, max_err=None, reverse_direction=False, stats=None): """Convert the curves of a set of compatible of glyphs to quadratic. All curves will be converted to quadratic at once, ensuring interpolation @@ -220,8 +224,14 @@ def glyphs_to_quadratic( def fonts_to_quadratic( - fonts, max_err_em=None, max_err=None, reverse_direction=False, - stats=None, dump_stats=False, remember_curve_type=True): + fonts, + max_err_em=None, + max_err=None, + reverse_direction=False, + stats=None, + dump_stats=False, + remember_curve_type=True, +): """Convert the curves of a collection of fonts to quadratic. All curves will be converted to quadratic at once, ensuring interpolation @@ -258,7 +268,7 @@ def fonts_to_quadratic( stats = {} if max_err_em and max_err: - raise TypeError('Only one of max_err and max_err_em can be specified.') + raise TypeError("Only one of max_err and max_err_em can be specified.") if not (max_err_em or max_err): max_err_em = DEFAULT_MAX_ERR @@ -270,8 +280,7 @@ def fonts_to_quadratic( if isinstance(max_err_em, (list, tuple)): assert len(fonts) == len(max_err_em) - max_errors = [f.info.unitsPerEm * e - for f, e in zip(fonts, max_err_em)] + max_errors = [f.info.unitsPerEm * e for f, e in zip(fonts, max_err_em)] elif max_err_em: max_errors = [f.info.unitsPerEm * max_err_em for f in fonts] @@ -286,7 +295,8 @@ def fonts_to_quadratic( cur_max_errors.append(error) try: modified |= _glyphs_to_quadratic( - glyphs, cur_max_errors, reverse_direction, stats) + glyphs, cur_max_errors, reverse_direction, stats + ) except IncompatibleGlyphsError as exc: logger.error(exc) glyph_errors[name] = exc @@ -296,8 +306,10 @@ def fonts_to_quadratic( if modified and dump_stats: spline_lengths = sorted(stats.keys()) - logger.info('New spline lengths: %s' % (', '.join( - '%s: %d' % (l, stats[l]) for l in spline_lengths))) + logger.info( + "New spline lengths: %s" + % (", ".join("%s: %d" % (l, stats[l]) for l in spline_lengths)) + ) if remember_curve_type: for font in fonts: diff --git a/Lib/fontTools/designspaceLib/__init__.py b/Lib/fontTools/designspaceLib/__init__.py index 230ea19f4..f8a0146d0 100644 --- a/Lib/fontTools/designspaceLib/__init__.py +++ b/Lib/fontTools/designspaceLib/__init__.py @@ -22,20 +22,20 @@ from fontTools.misc.textTools import tobytes, tostr """ __all__ = [ - 'AxisDescriptor', - 'AxisLabelDescriptor', - 'BaseDocReader', - 'BaseDocWriter', - 'DesignSpaceDocument', - 'DesignSpaceDocumentError', - 'DiscreteAxisDescriptor', - 'InstanceDescriptor', - 'LocationLabelDescriptor', - 'RangeAxisSubsetDescriptor', - 'RuleDescriptor', - 'SourceDescriptor', - 'ValueAxisSubsetDescriptor', - 'VariableFontDescriptor', + "AxisDescriptor", + "AxisLabelDescriptor", + "BaseDocReader", + "BaseDocWriter", + "DesignSpaceDocument", + "DesignSpaceDocumentError", + "DiscreteAxisDescriptor", + "InstanceDescriptor", + "LocationLabelDescriptor", + "RangeAxisSubsetDescriptor", + "RuleDescriptor", + "SourceDescriptor", + "ValueAxisSubsetDescriptor", + "VariableFontDescriptor", ] # ElementTree allows to find namespace-prefixed elements, but not attributes @@ -47,17 +47,18 @@ XML_LANG = XML_NS + "lang" 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('/'): + if path.startswith("/"): # The above transformation loses absolute paths - new_path = '/' + new_path - elif path.startswith(r'\\'): + new_path = "/" + new_path + elif path.startswith(r"\\"): # The above transformation loses leading slashes of UNC path mounts - new_path = '//' + new_path + new_path = "//" + new_path return new_path def posixpath_property(private_name): """Generate a propery that holds a path always using forward slashes.""" + def getter(self): # Normal getter return getattr(self, private_name) @@ -77,12 +78,10 @@ class DesignSpaceDocumentError(Exception): self.obj = obj def __str__(self): - return str(self.msg) + ( - ": %r" % self.obj if self.obj is not None else "") + return str(self.msg) + (": %r" % self.obj if self.obj is not None else "") class AsDictMixin(object): - def asdict(self): d = {} for attr, value in self.__dict__.items(): @@ -91,15 +90,13 @@ class AsDictMixin(object): if hasattr(value, "asdict"): value = value.asdict() elif isinstance(value, list): - value = [ - v.asdict() if hasattr(v, "asdict") else v for v in value - ] + value = [v.asdict() if hasattr(v, "asdict") else v for v in value] d[attr] = value return d class SimpleDescriptor(AsDictMixin): - """ Containers for a bunch of attributes""" + """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 @@ -107,13 +104,19 @@ class SimpleDescriptor(AsDictMixin): # test if this object contains the same data as the other for attr in self._attrs: try: - assert(getattr(self, attr) == getattr(other, attr)) + assert getattr(self, attr) == getattr(other, attr) except AssertionError: - print("failed attribute", attr, getattr(self, attr), "!=", getattr(other, attr)) + print( + "failed attribute", + attr, + getattr(self, attr), + "!=", + getattr(other, attr), + ) def __repr__(self): attrs = [f"{a}={repr(getattr(self, a))}," for a in self._attrs] - attrs = indent('\n'.join(attrs), ' ') + attrs = indent("\n".join(attrs), " ") return f"{self.__class__.__name__}(\n{attrs}\n)" @@ -136,13 +139,24 @@ class SourceDescriptor(SimpleDescriptor): doc.addSource(s1) """ + flavor = "source" - _attrs = ['filename', 'path', 'name', 'layerName', - 'location', 'copyLib', - 'copyGroups', 'copyFeatures', - 'muteKerning', 'muteInfo', - 'mutedGlyphNames', - 'familyName', 'styleName', 'localisedFamilyName'] + _attrs = [ + "filename", + "path", + "name", + "layerName", + "location", + "copyLib", + "copyGroups", + "copyFeatures", + "muteKerning", + "muteInfo", + "mutedGlyphNames", + "familyName", + "styleName", + "localisedFamilyName", + ] filename = posixpath_property("_filename") path = posixpath_property("_path") @@ -194,7 +208,9 @@ class SourceDescriptor(SimpleDescriptor): MutatorMath + Varlib. """ - self.designLocation = designLocation if designLocation is not None else location or {} + self.designLocation = ( + designLocation if designLocation is not None else location or {} + ) """dict. Axis values for this source, in design space coordinates. MutatorMath + Varlib. @@ -312,8 +328,9 @@ class SourceDescriptor(SimpleDescriptor): """ return self.localisedFamilyName.get(languageCode) - - def getFullDesignLocation(self, doc: 'DesignSpaceDocument') -> AnisotropicLocationDict: + def getFullDesignLocation( + self, doc: "DesignSpaceDocument" + ) -> AnisotropicLocationDict: """Get the complete design location of this source, from its :attr:`designLocation` and the document's axis defaults. @@ -355,7 +372,8 @@ class RuleDescriptor(SimpleDescriptor): """ - _attrs = ['name', 'conditionSets', 'subs'] # what do we need here + + _attrs = ["name", "conditionSets", "subs"] # what do we need here def __init__(self, *, name=None, conditionSets=None, subs=None): self.name = name @@ -391,14 +409,14 @@ def evaluateConditions(conditions, location): - 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']: + 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: + elif cd.get("maximum") is None: + if cd["minimum"] > value: return False - elif not cd['minimum'] <= value <= cd['maximum']: + elif not cd["minimum"] <= value <= cd["maximum"]: return False return True @@ -451,27 +469,30 @@ class InstanceDescriptor(SimpleDescriptor): i2.lib['com.coolDesignspaceApp.specimenText'] = 'Hamburgerwhatever' doc.addInstance(i2) """ + flavor = "instance" _defaultLanguageCode = "en" - _attrs = ['filename', - 'path', - 'name', - 'locationLabel', - 'designLocation', - 'userLocation', - 'familyName', - 'styleName', - 'postScriptFontName', - 'styleMapFamilyName', - 'styleMapStyleName', - 'localisedFamilyName', - 'localisedStyleName', - 'localisedStyleMapFamilyName', - 'localisedStyleMapStyleName', - 'glyphs', - 'kerning', - 'info', - 'lib'] + _attrs = [ + "filename", + "path", + "name", + "locationLabel", + "designLocation", + "userLocation", + "familyName", + "styleName", + "postScriptFontName", + "styleMapFamilyName", + "styleMapStyleName", + "localisedFamilyName", + "localisedStyleName", + "localisedStyleMapFamilyName", + "localisedStyleMapStyleName", + "glyphs", + "kerning", + "info", + "lib", + ] filename = posixpath_property("_filename") path = posixpath_property("_path") @@ -535,7 +556,9 @@ class InstanceDescriptor(SimpleDescriptor): .. versionadded:: 5.0 """ - self.designLocation: AnisotropicLocationDict = designLocation if designLocation is not None else (location or {}) + self.designLocation: AnisotropicLocationDict = ( + designLocation if designLocation is not None else (location or {}) + ) """dict. Axis values for this instance, in design space coordinates. MutatorMath + Varlib. @@ -708,7 +731,9 @@ class InstanceDescriptor(SimpleDescriptor): if axisName in self.userLocation: del self.userLocation[axisName] - def getLocationLabelDescriptor(self, doc: 'DesignSpaceDocument') -> Optional[LocationLabelDescriptor]: + def getLocationLabelDescriptor( + self, doc: "DesignSpaceDocument" + ) -> Optional[LocationLabelDescriptor]: """Get the :class:`LocationLabelDescriptor` instance that matches this instances's :attr:`locationLabel`. @@ -721,12 +746,14 @@ class InstanceDescriptor(SimpleDescriptor): label = doc.getLocationLabel(self.locationLabel) if label is None: raise DesignSpaceDocumentError( - 'InstanceDescriptor.getLocationLabelDescriptor(): ' - f'unknown location label `{self.locationLabel}` in instance `{self.name}`.' + "InstanceDescriptor.getLocationLabelDescriptor(): " + f"unknown location label `{self.locationLabel}` in instance `{self.name}`." ) return label - def getFullDesignLocation(self, doc: 'DesignSpaceDocument') -> AnisotropicLocationDict: + def getFullDesignLocation( + self, doc: "DesignSpaceDocument" + ) -> AnisotropicLocationDict: """Get the complete design location of this instance, by combining data from the various location fields, default axis values and mappings, and top-level location labels. @@ -757,7 +784,7 @@ class InstanceDescriptor(SimpleDescriptor): result[axis.name] = axis.map_forward(axis.default) return result - def getFullUserLocation(self, doc: 'DesignSpaceDocument') -> SimpleLocationDict: + def getFullUserLocation(self, doc: "DesignSpaceDocument") -> SimpleLocationDict: """Get the complete user location for this instance. .. seealso:: :meth:`getFullDesignLocation` @@ -770,11 +797,11 @@ class InstanceDescriptor(SimpleDescriptor): 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')), + "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()] @@ -848,7 +875,7 @@ class AbstractAxisDescriptor(SimpleDescriptor): class AxisDescriptor(AbstractAxisDescriptor): - """ Simple container for the axis data. + """Simple container for the axis data. Add more localisations? @@ -869,7 +896,17 @@ class AxisDescriptor(AbstractAxisDescriptor): ] doc.addAxis(a1) """ - _attrs = ['tag', 'name', 'maximum', 'minimum', 'default', 'map', 'axisOrdering', 'axisLabels'] + + _attrs = [ + "tag", + "name", + "maximum", + "minimum", + "default", + "map", + "axisOrdering", + "axisLabels", + ] def __init__( self, @@ -976,7 +1013,7 @@ class DiscreteAxisDescriptor(AbstractAxisDescriptor): """ flavor = "axis" - _attrs = ('tag', 'name', 'values', 'default', 'map', 'axisOrdering', 'axisLabels') + _attrs = ("tag", "name", "values", "default", "map", "axisOrdering", "axisLabels") def __init__( self, @@ -1053,7 +1090,16 @@ class AxisLabelDescriptor(SimpleDescriptor): """ flavor = "label" - _attrs = ('userMinimum', 'userValue', 'userMaximum', 'name', 'elidable', 'olderSibling', 'linkedUserValue', 'labelNames') + _attrs = ( + "userMinimum", + "userValue", + "userMaximum", + "name", + "elidable", + "olderSibling", + "linkedUserValue", + "labelNames", + ) def __init__( self, @@ -1127,7 +1173,7 @@ class LocationLabelDescriptor(SimpleDescriptor): """ flavor = "label" - _attrs = ('name', 'elidable', 'olderSibling', 'userLocation', 'labelNames') + _attrs = ("name", "elidable", "olderSibling", "userLocation", "labelNames") def __init__( self, @@ -1168,7 +1214,7 @@ class LocationLabelDescriptor(SimpleDescriptor): """Return the English name from :attr:`labelNames` or the :attr:`name`.""" return self.labelNames.get("en") or self.name - def getFullUserLocation(self, doc: 'DesignSpaceDocument') -> SimpleLocationDict: + def getFullUserLocation(self, doc: "DesignSpaceDocument") -> SimpleLocationDict: """Get the complete user location of this label, by combining data from the explicit user location and default axis values. @@ -1195,7 +1241,7 @@ class VariableFontDescriptor(SimpleDescriptor): """ flavor = "variable-font" - _attrs = ('filename', 'axisSubsets', 'lib') + _attrs = ("filename", "axisSubsets", "lib") filename = posixpath_property("_filename") @@ -1213,7 +1259,9 @@ class VariableFontDescriptor(SimpleDescriptor): If not specified, the :attr:`name` will be used as a basename for the file. """ - self.axisSubsets: List[Union[RangeAxisSubsetDescriptor, ValueAxisSubsetDescriptor]] = axisSubsets or [] + self.axisSubsets: List[ + Union[RangeAxisSubsetDescriptor, ValueAxisSubsetDescriptor] + ] = (axisSubsets or []) """Axis subsets to include in this variable font. If an axis is not mentioned, assume that we only want the default @@ -1228,10 +1276,13 @@ class RangeAxisSubsetDescriptor(SimpleDescriptor): .. versionadded:: 5.0 """ - flavor = "axis-subset" - _attrs = ('name', 'userMinimum', 'userDefault', 'userMaximum') - def __init__(self, *, name, userMinimum=-math.inf, userDefault=None, userMaximum=math.inf): + flavor = "axis-subset" + _attrs = ("name", "userMinimum", "userDefault", "userMaximum") + + def __init__( + self, *, name, userMinimum=-math.inf, userDefault=None, userMaximum=math.inf + ): self.name: str = name """Name of the :class:`AxisDescriptor` to subset.""" self.userMinimum: float = userMinimum @@ -1256,8 +1307,9 @@ class ValueAxisSubsetDescriptor(SimpleDescriptor): .. versionadded:: 5.0 """ + flavor = "axis-subset" - _attrs = ('name', 'userValue') + _attrs = ("name", "userValue") def __init__(self, *, name, userValue): self.name: str = name @@ -1304,12 +1356,17 @@ class BaseDocWriter(object): self.root = ET.Element("designspace") def write(self, pretty=True, encoding="UTF-8", xml_declaration=True): - self.root.attrib['format'] = ".".join(str(i) for i in self.effectiveFormatTuple) + self.root.attrib["format"] = ".".join(str(i) for i in self.effectiveFormatTuple) - if self.documentObject.axes or self.documentObject.elidedFallbackName is not None: + if ( + self.documentObject.axes + or self.documentObject.elidedFallbackName is not None + ): axesElement = ET.Element("axes") if self.documentObject.elidedFallbackName is not None: - axesElement.attrib['elidedfallbackname'] = self.documentObject.elidedFallbackName + axesElement.attrib[ + "elidedfallbackname" + ] = self.documentObject.elidedFallbackName self.root.append(axesElement) for axisObject in self.documentObject.axes: self._addAxis(axisObject) @@ -1352,7 +1409,7 @@ class BaseDocWriter(object): tree.write( self.path, encoding=encoding, - method='xml', + method="xml", xml_declaration=xml_declaration, pretty_print=pretty, ) @@ -1364,20 +1421,16 @@ class BaseDocWriter(object): minVersion = self.documentObject.formatTuple if ( any( - hasattr(axis, 'values') or - axis.axisOrdering is not None or - axis.axisLabels + hasattr(axis, "values") + or axis.axisOrdering is not None + or axis.axisLabels for axis in self.documentObject.axes - ) or - self.documentObject.locationLabels or - any( - source.localisedFamilyName - for source in self.documentObject.sources - ) or - self.documentObject.variableFonts or - any( - instance.locationLabel or - instance.userLocation + ) + or self.documentObject.locationLabels + or any(source.localisedFamilyName for source in self.documentObject.sources) + or self.documentObject.variableFonts + or any( + instance.locationLabel or instance.userLocation for instance in self.documentObject.instances ) ): @@ -1386,118 +1439,130 @@ class BaseDocWriter(object): return minVersion def _makeLocationElement(self, locationObject, name=None): - """ Convert Location dict to a locationElement.""" + """Convert Location dict to a locationElement.""" locElement = ET.Element("location") if name is not None: - locElement.attrib['name'] = name + 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 + 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]) + dimElement.attrib["xvalue"] = self.intOrFloat(dimensionValue[0]) + dimElement.attrib["yvalue"] = self.intOrFloat(dimensionValue[1]) else: - dimElement.attrib['xvalue'] = self.intOrFloat(dimensionValue) + 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).rstrip('0').rstrip('.') + return ("%f" % num).rstrip("0").rstrip(".") def _addRule(self, ruleObject): # if none of the conditions have minimum or maximum values, do not add the rule. - ruleElement = ET.Element('rule') + ruleElement = ET.Element("rule") if ruleObject.name is not None: - ruleElement.attrib['name'] = ruleObject.name + ruleElement.attrib["name"] = ruleObject.name for conditions in ruleObject.conditionSets: - conditionsetElement = ET.Element('conditionset') + conditionsetElement = ET.Element("conditionset") for cond in conditions: - if cond.get('minimum') is None and cond.get('maximum') is None: + 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')) + 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] + 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) + self.root.findall(".rules")[0].append(ruleElement) def _addAxis(self, axisObject): - axisElement = ET.Element('axis') - axisElement.attrib['tag'] = axisObject.tag - axisElement.attrib['name'] = axisObject.name + axisElement = ET.Element("axis") + axisElement.attrib["tag"] = axisObject.tag + axisElement.attrib["name"] = axisObject.name self._addLabelNames(axisElement, axisObject.labelNames) 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) + mapElement = ET.Element("map") + mapElement.attrib["input"] = self.intOrFloat(inputValue) + mapElement.attrib["output"] = self.intOrFloat(outputValue) axisElement.append(mapElement) if axisObject.axisOrdering or axisObject.axisLabels: - labelsElement = ET.Element('labels') + labelsElement = ET.Element("labels") if axisObject.axisOrdering is not None: - labelsElement.attrib['ordering'] = str(axisObject.axisOrdering) + labelsElement.attrib["ordering"] = str(axisObject.axisOrdering) for label in axisObject.axisLabels: self._addAxisLabel(labelsElement, label) axisElement.append(labelsElement) if hasattr(axisObject, "minimum"): - axisElement.attrib['minimum'] = self.intOrFloat(axisObject.minimum) - axisElement.attrib['maximum'] = self.intOrFloat(axisObject.maximum) + axisElement.attrib["minimum"] = self.intOrFloat(axisObject.minimum) + axisElement.attrib["maximum"] = self.intOrFloat(axisObject.maximum) elif hasattr(axisObject, "values"): - axisElement.attrib['values'] = " ".join(self.intOrFloat(v) for v in axisObject.values) - axisElement.attrib['default'] = self.intOrFloat(axisObject.default) + axisElement.attrib["values"] = " ".join( + self.intOrFloat(v) for v in axisObject.values + ) + axisElement.attrib["default"] = self.intOrFloat(axisObject.default) if axisObject.hidden: - axisElement.attrib['hidden'] = "1" - self.root.findall('.axes')[0].append(axisElement) + axisElement.attrib["hidden"] = "1" + self.root.findall(".axes")[0].append(axisElement) - def _addAxisLabel(self, axisElement: ET.Element, label: AxisLabelDescriptor) -> None: - labelElement = ET.Element('label') - labelElement.attrib['uservalue'] = self.intOrFloat(label.userValue) + def _addAxisLabel( + self, axisElement: ET.Element, label: AxisLabelDescriptor + ) -> None: + labelElement = ET.Element("label") + labelElement.attrib["uservalue"] = self.intOrFloat(label.userValue) if label.userMinimum is not None: - labelElement.attrib['userminimum'] = self.intOrFloat(label.userMinimum) + labelElement.attrib["userminimum"] = self.intOrFloat(label.userMinimum) if label.userMaximum is not None: - labelElement.attrib['usermaximum'] = self.intOrFloat(label.userMaximum) - labelElement.attrib['name'] = label.name + labelElement.attrib["usermaximum"] = self.intOrFloat(label.userMaximum) + labelElement.attrib["name"] = label.name if label.elidable: - labelElement.attrib['elidable'] = "true" + labelElement.attrib["elidable"] = "true" if label.olderSibling: - labelElement.attrib['oldersibling'] = "true" + labelElement.attrib["oldersibling"] = "true" if label.linkedUserValue is not None: - labelElement.attrib['linkeduservalue'] = self.intOrFloat(label.linkedUserValue) + labelElement.attrib["linkeduservalue"] = self.intOrFloat( + label.linkedUserValue + ) self._addLabelNames(labelElement, label.labelNames) axisElement.append(labelElement) def _addLabelNames(self, parentElement, labelNames): for languageCode, labelName in sorted(labelNames.items()): - languageElement = ET.Element('labelname') + languageElement = ET.Element("labelname") languageElement.attrib[XML_LANG] = languageCode languageElement.text = labelName parentElement.append(languageElement) - def _addLocationLabel(self, parentElement: ET.Element, label: LocationLabelDescriptor) -> None: - labelElement = ET.Element('label') - labelElement.attrib['name'] = label.name + def _addLocationLabel( + self, parentElement: ET.Element, label: LocationLabelDescriptor + ) -> None: + labelElement = ET.Element("label") + labelElement.attrib["name"] = label.name if label.elidable: - labelElement.attrib['elidable'] = "true" + labelElement.attrib["elidable"] = "true" if label.olderSibling: - labelElement.attrib['oldersibling'] = "true" + labelElement.attrib["oldersibling"] = "true" self._addLabelNames(labelElement, label.labelNames) self._addLocationElement(labelElement, userLocation=label.userLocation) parentElement.append(labelElement) @@ -1507,39 +1572,39 @@ class BaseDocWriter(object): parentElement, *, designLocation: AnisotropicLocationDict = None, - userLocation: SimpleLocationDict = None + userLocation: SimpleLocationDict = None, ): locElement = ET.Element("location") for axis in self.documentObject.axes: if designLocation is not None and axis.name in designLocation: - dimElement = ET.Element('dimension') - dimElement.attrib['name'] = axis.name + dimElement = ET.Element("dimension") + dimElement.attrib["name"] = axis.name value = designLocation[axis.name] if isinstance(value, tuple): - dimElement.attrib['xvalue'] = self.intOrFloat(value[0]) - dimElement.attrib['yvalue'] = self.intOrFloat(value[1]) + dimElement.attrib["xvalue"] = self.intOrFloat(value[0]) + dimElement.attrib["yvalue"] = self.intOrFloat(value[1]) else: - dimElement.attrib['xvalue'] = self.intOrFloat(value) + dimElement.attrib["xvalue"] = self.intOrFloat(value) locElement.append(dimElement) elif userLocation is not None and axis.name in userLocation: - dimElement = ET.Element('dimension') - dimElement.attrib['name'] = axis.name + dimElement = ET.Element("dimension") + dimElement.attrib["name"] = axis.name value = userLocation[axis.name] - dimElement.attrib['uservalue'] = self.intOrFloat(value) + dimElement.attrib["uservalue"] = self.intOrFloat(value) locElement.append(dimElement) if len(locElement) > 0: parentElement.append(locElement) def _addInstance(self, instanceObject): - instanceElement = ET.Element('instance') + instanceElement = ET.Element("instance") if instanceObject.name is not None: - instanceElement.attrib['name'] = instanceObject.name + instanceElement.attrib["name"] = instanceObject.name if instanceObject.locationLabel is not None: - instanceElement.attrib['location'] = instanceObject.locationLabel + instanceElement.attrib["location"] = instanceObject.locationLabel if instanceObject.familyName is not None: - instanceElement.attrib['familyname'] = instanceObject.familyName + instanceElement.attrib["familyname"] = instanceObject.familyName if instanceObject.styleName is not None: - instanceElement.attrib['stylename'] = instanceObject.styleName + instanceElement.attrib["stylename"] = instanceObject.styleName # add localisations if instanceObject.localisedStyleName: languageCodes = list(instanceObject.localisedStyleName.keys()) @@ -1547,7 +1612,7 @@ class BaseDocWriter(object): for code in languageCodes: if code == "en": continue # already stored in the element attribute - localisedStyleNameElement = ET.Element('stylename') + localisedStyleNameElement = ET.Element("stylename") localisedStyleNameElement.attrib[XML_LANG] = code localisedStyleNameElement.text = instanceObject.getStyleName(code) instanceElement.append(localisedStyleNameElement) @@ -1557,7 +1622,7 @@ class BaseDocWriter(object): for code in languageCodes: if code == "en": continue # already stored in the element attribute - localisedFamilyNameElement = ET.Element('familyname') + localisedFamilyNameElement = ET.Element("familyname") localisedFamilyNameElement.attrib[XML_LANG] = code localisedFamilyNameElement.text = instanceObject.getFamilyName(code) instanceElement.append(localisedFamilyNameElement) @@ -1567,9 +1632,11 @@ class BaseDocWriter(object): for code in languageCodes: if code == "en": continue - localisedStyleMapStyleNameElement = ET.Element('stylemapstylename') + localisedStyleMapStyleNameElement = ET.Element("stylemapstylename") localisedStyleMapStyleNameElement.attrib[XML_LANG] = code - localisedStyleMapStyleNameElement.text = instanceObject.getStyleMapStyleName(code) + localisedStyleMapStyleNameElement.text = ( + instanceObject.getStyleMapStyleName(code) + ) instanceElement.append(localisedStyleMapStyleNameElement) if instanceObject.localisedStyleMapFamilyName: languageCodes = list(instanceObject.localisedStyleMapFamilyName.keys()) @@ -1577,9 +1644,11 @@ class BaseDocWriter(object): for code in languageCodes: if code == "en": continue - localisedStyleMapFamilyNameElement = ET.Element('stylemapfamilyname') + localisedStyleMapFamilyNameElement = ET.Element("stylemapfamilyname") localisedStyleMapFamilyNameElement.attrib[XML_LANG] = code - localisedStyleMapFamilyNameElement.text = instanceObject.getStyleMapFamilyName(code) + localisedStyleMapFamilyNameElement.text = ( + instanceObject.getStyleMapFamilyName(code) + ) instanceElement.append(localisedStyleMapFamilyNameElement) if self.effectiveFormatTuple >= (5, 0): @@ -1587,127 +1656,151 @@ class BaseDocWriter(object): self._addLocationElement( instanceElement, designLocation=instanceObject.designLocation, - userLocation=instanceObject.userLocation + userLocation=instanceObject.userLocation, ) else: # Pre-version 5.0 code was validating and filling in the location # dict while writing it out, as preserved below. if instanceObject.location is not None: - locationElement, instanceObject.location = self._makeLocationElement(instanceObject.location) + locationElement, instanceObject.location = self._makeLocationElement( + instanceObject.location + ) instanceElement.append(locationElement) if instanceObject.filename is not None: - instanceElement.attrib['filename'] = instanceObject.filename + instanceElement.attrib["filename"] = instanceObject.filename if instanceObject.postScriptFontName is not None: - instanceElement.attrib['postscriptfontname'] = instanceObject.postScriptFontName + instanceElement.attrib[ + "postscriptfontname" + ] = instanceObject.postScriptFontName if instanceObject.styleMapFamilyName is not None: - instanceElement.attrib['stylemapfamilyname'] = instanceObject.styleMapFamilyName + instanceElement.attrib[ + "stylemapfamilyname" + ] = instanceObject.styleMapFamilyName if instanceObject.styleMapStyleName is not None: - instanceElement.attrib['stylemapstylename'] = instanceObject.styleMapStyleName + instanceElement.attrib[ + "stylemapstylename" + ] = instanceObject.styleMapStyleName if self.effectiveFormatTuple < (5, 0): # Deprecated members as of version 5.0 if instanceObject.glyphs: - if instanceElement.findall('.glyphs') == []: - glyphsElement = ET.Element('glyphs') + if instanceElement.findall(".glyphs") == []: + glyphsElement = ET.Element("glyphs") instanceElement.append(glyphsElement) - glyphsElement = instanceElement.findall('.glyphs')[0] + glyphsElement = instanceElement.findall(".glyphs")[0] for glyphName, data in sorted(instanceObject.glyphs.items()): - glyphElement = self._writeGlyphElement(instanceElement, instanceObject, glyphName, data) + glyphElement = self._writeGlyphElement( + instanceElement, instanceObject, glyphName, data + ) glyphsElement.append(glyphElement) if instanceObject.kerning: - kerningElement = ET.Element('kerning') + kerningElement = ET.Element("kerning") instanceElement.append(kerningElement) if instanceObject.info: - infoElement = ET.Element('info') + infoElement = ET.Element("info") instanceElement.append(infoElement) self._addLib(instanceElement, instanceObject.lib, 4) - self.root.findall('.instances')[0].append(instanceElement) + 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 + 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 + sourceElement.attrib["name"] = sourceObject.name if sourceObject.familyName is not None: - sourceElement.attrib['familyname'] = sourceObject.familyName + sourceElement.attrib["familyname"] = sourceObject.familyName if sourceObject.styleName is not None: - sourceElement.attrib['stylename'] = sourceObject.styleName + sourceElement.attrib["stylename"] = sourceObject.styleName if sourceObject.layerName is not None: - sourceElement.attrib['layer'] = sourceObject.layerName + sourceElement.attrib["layer"] = sourceObject.layerName if sourceObject.localisedFamilyName: languageCodes = list(sourceObject.localisedFamilyName.keys()) languageCodes.sort() for code in languageCodes: if code == "en": continue # already stored in the element attribute - localisedFamilyNameElement = ET.Element('familyname') + localisedFamilyNameElement = ET.Element("familyname") localisedFamilyNameElement.attrib[XML_LANG] = code localisedFamilyNameElement.text = sourceObject.getFamilyName(code) sourceElement.append(localisedFamilyNameElement) if sourceObject.copyLib: - libElement = ET.Element('lib') - libElement.attrib['copy'] = "1" + libElement = ET.Element("lib") + libElement.attrib["copy"] = "1" sourceElement.append(libElement) if sourceObject.copyGroups: - groupsElement = ET.Element('groups') - groupsElement.attrib['copy'] = "1" + groupsElement = ET.Element("groups") + groupsElement.attrib["copy"] = "1" sourceElement.append(groupsElement) if sourceObject.copyFeatures: - featuresElement = ET.Element('features') - featuresElement.attrib['copy'] = "1" + featuresElement = ET.Element("features") + featuresElement.attrib["copy"] = "1" sourceElement.append(featuresElement) if sourceObject.copyInfo or sourceObject.muteInfo: - infoElement = ET.Element('info') + infoElement = ET.Element("info") if sourceObject.copyInfo: - infoElement.attrib['copy'] = "1" + infoElement.attrib["copy"] = "1" if sourceObject.muteInfo: - infoElement.attrib['mute'] = "1" + infoElement.attrib["mute"] = "1" sourceElement.append(infoElement) if sourceObject.muteKerning: kerningElement = ET.Element("kerning") - kerningElement.attrib["mute"] = '1' + 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' + glyphElement.attrib["mute"] = "1" sourceElement.append(glyphElement) if self.effectiveFormatTuple >= (5, 0): - self._addLocationElement(sourceElement, designLocation=sourceObject.location) + self._addLocationElement( + sourceElement, designLocation=sourceObject.location + ) else: # Pre-version 5.0 code was validating and filling in the location # dict while writing it out, as preserved below. - locationElement, sourceObject.location = self._makeLocationElement(sourceObject.location) + locationElement, sourceObject.location = self._makeLocationElement( + sourceObject.location + ) sourceElement.append(locationElement) - self.root.findall('.sources')[0].append(sourceElement) + self.root.findall(".sources")[0].append(sourceElement) - def _addVariableFont(self, parentElement: ET.Element, vf: VariableFontDescriptor) -> None: - vfElement = ET.Element('variable-font') - vfElement.attrib['name'] = vf.name + def _addVariableFont( + self, parentElement: ET.Element, vf: VariableFontDescriptor + ) -> None: + vfElement = ET.Element("variable-font") + vfElement.attrib["name"] = vf.name if vf.filename is not None: - vfElement.attrib['filename'] = vf.filename + vfElement.attrib["filename"] = vf.filename if vf.axisSubsets: - subsetsElement = ET.Element('axis-subsets') + subsetsElement = ET.Element("axis-subsets") for subset in vf.axisSubsets: - subsetElement = ET.Element('axis-subset') - subsetElement.attrib['name'] = subset.name + subsetElement = ET.Element("axis-subset") + subsetElement.attrib["name"] = subset.name # Mypy doesn't support narrowing union types via hasattr() # https://mypy.readthedocs.io/en/stable/type_narrowing.html # TODO(Python 3.10): use TypeGuard if hasattr(subset, "userMinimum"): subset = cast(RangeAxisSubsetDescriptor, subset) if subset.userMinimum != -math.inf: - subsetElement.attrib['userminimum'] = self.intOrFloat(subset.userMinimum) + subsetElement.attrib["userminimum"] = self.intOrFloat( + subset.userMinimum + ) if subset.userMaximum != math.inf: - subsetElement.attrib['usermaximum'] = self.intOrFloat(subset.userMaximum) + subsetElement.attrib["usermaximum"] = self.intOrFloat( + subset.userMaximum + ) if subset.userDefault is not None: - subsetElement.attrib['userdefault'] = self.intOrFloat(subset.userDefault) + subsetElement.attrib["userdefault"] = self.intOrFloat( + subset.userDefault + ) elif hasattr(subset, "userValue"): subset = cast(ValueAxisSubsetDescriptor, subset) - subsetElement.attrib['uservalue'] = self.intOrFloat(subset.userValue) + subsetElement.attrib["uservalue"] = self.intOrFloat( + subset.userValue + ) subsetsElement.append(subsetElement) vfElement.append(subsetsElement) self._addLib(vfElement, vf.lib, 4) @@ -1716,35 +1809,41 @@ class BaseDocWriter(object): def _addLib(self, parentElement: ET.Element, data: Any, indent_level: int) -> None: if not data: return - libElement = ET.Element('lib') + libElement = ET.Element("lib") libElement.append(plistlib.totree(data, indent_level=indent_level)) parentElement.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 = 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.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: + if data.get("masters") is not None: mastersElement = ET.Element("masters") - for m in data.get('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')) + 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) @@ -1801,7 +1900,8 @@ class BaseDocReader(LogMixin): if processingValue not in {"first", "last"}: raise DesignSpaceDocumentError( " processing attribute value is not valid: %r, " - "expected 'first' or 'last'" % processingValue) + "expected 'first' or 'last'" % processingValue + ) self.documentObject.rulesProcessingLast = processingValue == "last" for ruleElement in self.root.findall(".rules/rule"): ruleObject = self.ruleDescriptorClass() @@ -1818,71 +1918,79 @@ class BaseDocReader(LogMixin): "Wrapped them in a new conditionset." ) # read the conditionsets - for conditionSetElement in ruleElement.findall('.conditionset'): + 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'] + 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'): + for conditionElement in parentElement.findall(".condition"): cd = {} cdMin = conditionElement.attrib.get("minimum") if cdMin is not None: - cd['minimum'] = float(cdMin) + cd["minimum"] = float(cdMin) else: # will allow these to be None, assume axis.minimum - cd['minimum'] = None + cd["minimum"] = None cdMax = conditionElement.attrib.get("maximum") if cdMax is not None: - cd['maximum'] = float(cdMax) + cd["maximum"] = float(cdMax) else: # will allow these to be None, assume axis.maximum - cd['maximum'] = None - cd['name'] = conditionElement.attrib.get("name") + cd["maximum"] = None + cd["name"] = conditionElement.attrib.get("name") # # test for things - if cd.get('minimum') is None and cd.get('maximum') is None: + 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 "")) + "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. axesElement = self.root.find(".axes") - if axesElement is not None and 'elidedfallbackname' in axesElement.attrib: - self.documentObject.elidedFallbackName = axesElement.attrib['elidedfallbackname'] + if axesElement is not None and "elidedfallbackname" in axesElement.attrib: + self.documentObject.elidedFallbackName = axesElement.attrib[ + "elidedfallbackname" + ] axisElements = self.root.findall(".axes/axis") if not axisElements: return for axisElement in axisElements: - if self.documentObject.formatTuple >= (5, 0) and "values" in axisElement.attrib: + if ( + self.documentObject.formatTuple >= (5, 0) + and "values" in axisElement.attrib + ): axisObject = self.discreteAxisDescriptorClass() - axisObject.values = [float(s) for s in axisElement.attrib["values"].split(" ")] + axisObject.values = [ + float(s) for s in axisElement.attrib["values"].split(" ") + ] else: axisObject = self.axisDescriptorClass() axisObject.minimum = float(axisElement.attrib.get("minimum")) axisObject.maximum = float(axisElement.attrib.get("maximum")) axisObject.default = float(axisElement.attrib.get("default")) axisObject.name = axisElement.attrib.get("name") - if axisElement.attrib.get('hidden', False): + if axisElement.attrib.get("hidden", False): axisObject.hidden = True axisObject.tag = axisElement.attrib.get("tag") - for mapElement in axisElement.findall('map'): - a = float(mapElement.attrib['input']) - b = float(mapElement.attrib['output']) + 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'): + 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(): @@ -1898,17 +2006,29 @@ class BaseDocReader(LogMixin): self.axisDefaults[axisObject.name] = axisObject.default def readAxisLabel(self, element: ET.Element): - xml_attrs = {'userminimum', 'uservalue', 'usermaximum', 'name', 'elidable', 'oldersibling', 'linkeduservalue'} + xml_attrs = { + "userminimum", + "uservalue", + "usermaximum", + "name", + "elidable", + "oldersibling", + "linkeduservalue", + } unknown_attrs = set(element.attrib) - xml_attrs if unknown_attrs: - raise DesignSpaceDocumentError(f"label element contains unknown attributes: {', '.join(unknown_attrs)}") + raise DesignSpaceDocumentError( + f"label element contains unknown attributes: {', '.join(unknown_attrs)}" + ) name = element.get("name") if name is None: raise DesignSpaceDocumentError("label element must have a name attribute.") valueStr = element.get("uservalue") if valueStr is None: - raise DesignSpaceDocumentError("label element must have a uservalue attribute.") + raise DesignSpaceDocumentError( + "label element must have a uservalue attribute." + ) value = float(valueStr) minimumStr = element.get("userminimum") minimum = float(minimumStr) if minimumStr is not None else None @@ -1941,18 +2061,24 @@ class BaseDocReader(LogMixin): if self.documentObject.formatTuple < (5, 0): return - xml_attrs = {'name', 'elidable', 'oldersibling'} + xml_attrs = {"name", "elidable", "oldersibling"} for labelElement in self.root.findall(".labels/label"): unknown_attrs = set(labelElement.attrib) - xml_attrs if unknown_attrs: - raise DesignSpaceDocumentError(f"Label element contains unknown attributes: {', '.join(unknown_attrs)}") + raise DesignSpaceDocumentError( + f"Label element contains unknown attributes: {', '.join(unknown_attrs)}" + ) name = labelElement.get("name") if name is None: - raise DesignSpaceDocumentError("label element must have a name attribute.") + raise DesignSpaceDocumentError( + "label element must have a name attribute." + ) designLocation, userLocation = self.locationFromElement(labelElement) if designLocation: - raise DesignSpaceDocumentError(f'