fonttools 4.60.2__cp311-cp311-win32.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fontTools/__init__.py +8 -0
- fontTools/__main__.py +35 -0
- fontTools/afmLib.py +439 -0
- fontTools/agl.py +5233 -0
- fontTools/annotations.py +30 -0
- fontTools/cffLib/CFF2ToCFF.py +258 -0
- fontTools/cffLib/CFFToCFF2.py +305 -0
- fontTools/cffLib/__init__.py +3694 -0
- fontTools/cffLib/specializer.py +927 -0
- fontTools/cffLib/transforms.py +495 -0
- fontTools/cffLib/width.py +210 -0
- fontTools/colorLib/__init__.py +0 -0
- fontTools/colorLib/builder.py +664 -0
- fontTools/colorLib/errors.py +2 -0
- fontTools/colorLib/geometry.py +143 -0
- fontTools/colorLib/table_builder.py +223 -0
- fontTools/colorLib/unbuilder.py +81 -0
- fontTools/config/__init__.py +90 -0
- fontTools/cu2qu/__init__.py +15 -0
- fontTools/cu2qu/__main__.py +6 -0
- fontTools/cu2qu/benchmark.py +54 -0
- fontTools/cu2qu/cli.py +198 -0
- fontTools/cu2qu/cu2qu.c +15817 -0
- fontTools/cu2qu/cu2qu.cp311-win32.pyd +0 -0
- fontTools/cu2qu/cu2qu.py +563 -0
- fontTools/cu2qu/errors.py +77 -0
- fontTools/cu2qu/ufo.py +363 -0
- fontTools/designspaceLib/__init__.py +3343 -0
- fontTools/designspaceLib/__main__.py +6 -0
- fontTools/designspaceLib/split.py +475 -0
- fontTools/designspaceLib/statNames.py +260 -0
- fontTools/designspaceLib/types.py +147 -0
- fontTools/encodings/MacRoman.py +258 -0
- fontTools/encodings/StandardEncoding.py +258 -0
- fontTools/encodings/__init__.py +1 -0
- fontTools/encodings/codecs.py +135 -0
- fontTools/feaLib/__init__.py +4 -0
- fontTools/feaLib/__main__.py +78 -0
- fontTools/feaLib/ast.py +2143 -0
- fontTools/feaLib/builder.py +1814 -0
- fontTools/feaLib/error.py +22 -0
- fontTools/feaLib/lexer.c +17029 -0
- fontTools/feaLib/lexer.cp311-win32.pyd +0 -0
- fontTools/feaLib/lexer.py +287 -0
- fontTools/feaLib/location.py +12 -0
- fontTools/feaLib/lookupDebugInfo.py +12 -0
- fontTools/feaLib/parser.py +2394 -0
- fontTools/feaLib/variableScalar.py +118 -0
- fontTools/fontBuilder.py +1014 -0
- fontTools/help.py +36 -0
- fontTools/merge/__init__.py +248 -0
- fontTools/merge/__main__.py +6 -0
- fontTools/merge/base.py +81 -0
- fontTools/merge/cmap.py +173 -0
- fontTools/merge/layout.py +526 -0
- fontTools/merge/options.py +85 -0
- fontTools/merge/tables.py +352 -0
- fontTools/merge/unicode.py +78 -0
- fontTools/merge/util.py +143 -0
- fontTools/misc/__init__.py +1 -0
- fontTools/misc/arrayTools.py +424 -0
- fontTools/misc/bezierTools.c +39731 -0
- fontTools/misc/bezierTools.cp311-win32.pyd +0 -0
- fontTools/misc/bezierTools.py +1500 -0
- fontTools/misc/classifyTools.py +170 -0
- fontTools/misc/cliTools.py +53 -0
- fontTools/misc/configTools.py +349 -0
- fontTools/misc/cython.py +27 -0
- fontTools/misc/dictTools.py +83 -0
- fontTools/misc/eexec.py +119 -0
- fontTools/misc/encodingTools.py +72 -0
- fontTools/misc/enumTools.py +23 -0
- fontTools/misc/etree.py +456 -0
- fontTools/misc/filenames.py +245 -0
- fontTools/misc/filesystem/__init__.py +68 -0
- fontTools/misc/filesystem/_base.py +134 -0
- fontTools/misc/filesystem/_copy.py +45 -0
- fontTools/misc/filesystem/_errors.py +54 -0
- fontTools/misc/filesystem/_info.py +75 -0
- fontTools/misc/filesystem/_osfs.py +164 -0
- fontTools/misc/filesystem/_path.py +67 -0
- fontTools/misc/filesystem/_subfs.py +92 -0
- fontTools/misc/filesystem/_tempfs.py +34 -0
- fontTools/misc/filesystem/_tools.py +34 -0
- fontTools/misc/filesystem/_walk.py +55 -0
- fontTools/misc/filesystem/_zipfs.py +204 -0
- fontTools/misc/fixedTools.py +253 -0
- fontTools/misc/intTools.py +25 -0
- fontTools/misc/iterTools.py +12 -0
- fontTools/misc/lazyTools.py +42 -0
- fontTools/misc/loggingTools.py +543 -0
- fontTools/misc/macCreatorType.py +56 -0
- fontTools/misc/macRes.py +261 -0
- fontTools/misc/plistlib/__init__.py +681 -0
- fontTools/misc/plistlib/py.typed +0 -0
- fontTools/misc/psCharStrings.py +1511 -0
- fontTools/misc/psLib.py +398 -0
- fontTools/misc/psOperators.py +572 -0
- fontTools/misc/py23.py +96 -0
- fontTools/misc/roundTools.py +110 -0
- fontTools/misc/sstruct.py +227 -0
- fontTools/misc/symfont.py +242 -0
- fontTools/misc/testTools.py +233 -0
- fontTools/misc/textTools.py +156 -0
- fontTools/misc/timeTools.py +88 -0
- fontTools/misc/transform.py +516 -0
- fontTools/misc/treeTools.py +45 -0
- fontTools/misc/vector.py +147 -0
- fontTools/misc/visitor.py +158 -0
- fontTools/misc/xmlReader.py +188 -0
- fontTools/misc/xmlWriter.py +231 -0
- fontTools/mtiLib/__init__.py +1400 -0
- fontTools/mtiLib/__main__.py +5 -0
- fontTools/otlLib/__init__.py +1 -0
- fontTools/otlLib/builder.py +3465 -0
- fontTools/otlLib/error.py +11 -0
- fontTools/otlLib/maxContextCalc.py +96 -0
- fontTools/otlLib/optimize/__init__.py +53 -0
- fontTools/otlLib/optimize/__main__.py +6 -0
- fontTools/otlLib/optimize/gpos.py +439 -0
- fontTools/pens/__init__.py +1 -0
- fontTools/pens/areaPen.py +52 -0
- fontTools/pens/basePen.py +475 -0
- fontTools/pens/boundsPen.py +98 -0
- fontTools/pens/cairoPen.py +26 -0
- fontTools/pens/cocoaPen.py +26 -0
- fontTools/pens/cu2quPen.py +325 -0
- fontTools/pens/explicitClosingLinePen.py +101 -0
- fontTools/pens/filterPen.py +433 -0
- fontTools/pens/freetypePen.py +462 -0
- fontTools/pens/hashPointPen.py +89 -0
- fontTools/pens/momentsPen.c +13378 -0
- fontTools/pens/momentsPen.cp311-win32.pyd +0 -0
- fontTools/pens/momentsPen.py +879 -0
- fontTools/pens/perimeterPen.py +69 -0
- fontTools/pens/pointInsidePen.py +192 -0
- fontTools/pens/pointPen.py +643 -0
- fontTools/pens/qtPen.py +29 -0
- fontTools/pens/qu2cuPen.py +105 -0
- fontTools/pens/quartzPen.py +43 -0
- fontTools/pens/recordingPen.py +335 -0
- fontTools/pens/reportLabPen.py +79 -0
- fontTools/pens/reverseContourPen.py +96 -0
- fontTools/pens/roundingPen.py +130 -0
- fontTools/pens/statisticsPen.py +312 -0
- fontTools/pens/svgPathPen.py +310 -0
- fontTools/pens/t2CharStringPen.py +88 -0
- fontTools/pens/teePen.py +55 -0
- fontTools/pens/transformPen.py +115 -0
- fontTools/pens/ttGlyphPen.py +335 -0
- fontTools/pens/wxPen.py +29 -0
- fontTools/qu2cu/__init__.py +15 -0
- fontTools/qu2cu/__main__.py +7 -0
- fontTools/qu2cu/benchmark.py +56 -0
- fontTools/qu2cu/cli.py +125 -0
- fontTools/qu2cu/qu2cu.c +16682 -0
- fontTools/qu2cu/qu2cu.cp311-win32.pyd +0 -0
- fontTools/qu2cu/qu2cu.py +405 -0
- fontTools/subset/__init__.py +4096 -0
- fontTools/subset/__main__.py +6 -0
- fontTools/subset/cff.py +184 -0
- fontTools/subset/svg.py +253 -0
- fontTools/subset/util.py +25 -0
- fontTools/svgLib/__init__.py +3 -0
- fontTools/svgLib/path/__init__.py +65 -0
- fontTools/svgLib/path/arc.py +154 -0
- fontTools/svgLib/path/parser.py +322 -0
- fontTools/svgLib/path/shapes.py +183 -0
- fontTools/t1Lib/__init__.py +648 -0
- fontTools/tfmLib.py +460 -0
- fontTools/ttLib/__init__.py +30 -0
- fontTools/ttLib/__main__.py +148 -0
- fontTools/ttLib/macUtils.py +54 -0
- fontTools/ttLib/removeOverlaps.py +395 -0
- fontTools/ttLib/reorderGlyphs.py +285 -0
- fontTools/ttLib/scaleUpem.py +436 -0
- fontTools/ttLib/sfnt.py +661 -0
- fontTools/ttLib/standardGlyphOrder.py +271 -0
- fontTools/ttLib/tables/B_A_S_E_.py +14 -0
- fontTools/ttLib/tables/BitmapGlyphMetrics.py +64 -0
- fontTools/ttLib/tables/C_B_D_T_.py +113 -0
- fontTools/ttLib/tables/C_B_L_C_.py +19 -0
- fontTools/ttLib/tables/C_F_F_.py +61 -0
- fontTools/ttLib/tables/C_F_F__2.py +26 -0
- fontTools/ttLib/tables/C_O_L_R_.py +165 -0
- fontTools/ttLib/tables/C_P_A_L_.py +305 -0
- fontTools/ttLib/tables/D_S_I_G_.py +158 -0
- fontTools/ttLib/tables/D__e_b_g.py +35 -0
- fontTools/ttLib/tables/DefaultTable.py +49 -0
- fontTools/ttLib/tables/E_B_D_T_.py +835 -0
- fontTools/ttLib/tables/E_B_L_C_.py +718 -0
- fontTools/ttLib/tables/F_F_T_M_.py +52 -0
- fontTools/ttLib/tables/F__e_a_t.py +149 -0
- fontTools/ttLib/tables/G_D_E_F_.py +13 -0
- fontTools/ttLib/tables/G_M_A_P_.py +148 -0
- fontTools/ttLib/tables/G_P_K_G_.py +133 -0
- fontTools/ttLib/tables/G_P_O_S_.py +14 -0
- fontTools/ttLib/tables/G_S_U_B_.py +13 -0
- fontTools/ttLib/tables/G_V_A_R_.py +5 -0
- fontTools/ttLib/tables/G__l_a_t.py +235 -0
- fontTools/ttLib/tables/G__l_o_c.py +85 -0
- fontTools/ttLib/tables/H_V_A_R_.py +13 -0
- fontTools/ttLib/tables/J_S_T_F_.py +13 -0
- fontTools/ttLib/tables/L_T_S_H_.py +58 -0
- fontTools/ttLib/tables/M_A_T_H_.py +13 -0
- fontTools/ttLib/tables/M_E_T_A_.py +352 -0
- fontTools/ttLib/tables/M_V_A_R_.py +13 -0
- fontTools/ttLib/tables/O_S_2f_2.py +752 -0
- fontTools/ttLib/tables/S_I_N_G_.py +99 -0
- fontTools/ttLib/tables/S_T_A_T_.py +15 -0
- fontTools/ttLib/tables/S_V_G_.py +223 -0
- fontTools/ttLib/tables/S__i_l_f.py +1040 -0
- fontTools/ttLib/tables/S__i_l_l.py +92 -0
- fontTools/ttLib/tables/T_S_I_B_.py +13 -0
- fontTools/ttLib/tables/T_S_I_C_.py +14 -0
- fontTools/ttLib/tables/T_S_I_D_.py +13 -0
- fontTools/ttLib/tables/T_S_I_J_.py +13 -0
- fontTools/ttLib/tables/T_S_I_P_.py +13 -0
- fontTools/ttLib/tables/T_S_I_S_.py +13 -0
- fontTools/ttLib/tables/T_S_I_V_.py +26 -0
- fontTools/ttLib/tables/T_S_I__0.py +70 -0
- fontTools/ttLib/tables/T_S_I__1.py +163 -0
- fontTools/ttLib/tables/T_S_I__2.py +17 -0
- fontTools/ttLib/tables/T_S_I__3.py +22 -0
- fontTools/ttLib/tables/T_S_I__5.py +60 -0
- fontTools/ttLib/tables/T_T_F_A_.py +14 -0
- fontTools/ttLib/tables/TupleVariation.py +884 -0
- fontTools/ttLib/tables/V_A_R_C_.py +12 -0
- fontTools/ttLib/tables/V_D_M_X_.py +249 -0
- fontTools/ttLib/tables/V_O_R_G_.py +165 -0
- fontTools/ttLib/tables/V_V_A_R_.py +13 -0
- fontTools/ttLib/tables/__init__.py +98 -0
- fontTools/ttLib/tables/_a_n_k_r.py +15 -0
- fontTools/ttLib/tables/_a_v_a_r.py +193 -0
- fontTools/ttLib/tables/_b_s_l_n.py +15 -0
- fontTools/ttLib/tables/_c_i_d_g.py +24 -0
- fontTools/ttLib/tables/_c_m_a_p.py +1591 -0
- fontTools/ttLib/tables/_c_v_a_r.py +94 -0
- fontTools/ttLib/tables/_c_v_t.py +56 -0
- fontTools/ttLib/tables/_f_e_a_t.py +15 -0
- fontTools/ttLib/tables/_f_p_g_m.py +62 -0
- fontTools/ttLib/tables/_f_v_a_r.py +261 -0
- fontTools/ttLib/tables/_g_a_s_p.py +63 -0
- fontTools/ttLib/tables/_g_c_i_d.py +13 -0
- fontTools/ttLib/tables/_g_l_y_f.py +2311 -0
- fontTools/ttLib/tables/_g_v_a_r.py +340 -0
- fontTools/ttLib/tables/_h_d_m_x.py +127 -0
- fontTools/ttLib/tables/_h_e_a_d.py +130 -0
- fontTools/ttLib/tables/_h_h_e_a.py +147 -0
- fontTools/ttLib/tables/_h_m_t_x.py +164 -0
- fontTools/ttLib/tables/_k_e_r_n.py +289 -0
- fontTools/ttLib/tables/_l_c_a_r.py +13 -0
- fontTools/ttLib/tables/_l_o_c_a.py +70 -0
- fontTools/ttLib/tables/_l_t_a_g.py +72 -0
- fontTools/ttLib/tables/_m_a_x_p.py +147 -0
- fontTools/ttLib/tables/_m_e_t_a.py +112 -0
- fontTools/ttLib/tables/_m_o_r_t.py +14 -0
- fontTools/ttLib/tables/_m_o_r_x.py +15 -0
- fontTools/ttLib/tables/_n_a_m_e.py +1242 -0
- fontTools/ttLib/tables/_o_p_b_d.py +14 -0
- fontTools/ttLib/tables/_p_o_s_t.py +319 -0
- fontTools/ttLib/tables/_p_r_e_p.py +16 -0
- fontTools/ttLib/tables/_p_r_o_p.py +12 -0
- fontTools/ttLib/tables/_s_b_i_x.py +129 -0
- fontTools/ttLib/tables/_t_r_a_k.py +332 -0
- fontTools/ttLib/tables/_v_h_e_a.py +139 -0
- fontTools/ttLib/tables/_v_m_t_x.py +19 -0
- fontTools/ttLib/tables/asciiTable.py +20 -0
- fontTools/ttLib/tables/grUtils.py +92 -0
- fontTools/ttLib/tables/otBase.py +1458 -0
- fontTools/ttLib/tables/otConverters.py +2068 -0
- fontTools/ttLib/tables/otData.py +6400 -0
- fontTools/ttLib/tables/otTables.py +2703 -0
- fontTools/ttLib/tables/otTraverse.py +163 -0
- fontTools/ttLib/tables/sbixGlyph.py +149 -0
- fontTools/ttLib/tables/sbixStrike.py +177 -0
- fontTools/ttLib/tables/table_API_readme.txt +91 -0
- fontTools/ttLib/tables/ttProgram.py +594 -0
- fontTools/ttLib/ttCollection.py +125 -0
- fontTools/ttLib/ttFont.py +1148 -0
- fontTools/ttLib/ttGlyphSet.py +490 -0
- fontTools/ttLib/ttVisitor.py +32 -0
- fontTools/ttLib/woff2.py +1680 -0
- fontTools/ttx.py +479 -0
- fontTools/ufoLib/__init__.py +2575 -0
- fontTools/ufoLib/converters.py +407 -0
- fontTools/ufoLib/errors.py +30 -0
- fontTools/ufoLib/etree.py +6 -0
- fontTools/ufoLib/filenames.py +356 -0
- fontTools/ufoLib/glifLib.py +2120 -0
- fontTools/ufoLib/kerning.py +141 -0
- fontTools/ufoLib/plistlib.py +47 -0
- fontTools/ufoLib/pointPen.py +6 -0
- fontTools/ufoLib/utils.py +107 -0
- fontTools/ufoLib/validators.py +1208 -0
- fontTools/unicode.py +50 -0
- fontTools/unicodedata/Blocks.py +817 -0
- fontTools/unicodedata/Mirrored.py +446 -0
- fontTools/unicodedata/OTTags.py +50 -0
- fontTools/unicodedata/ScriptExtensions.py +832 -0
- fontTools/unicodedata/Scripts.py +3639 -0
- fontTools/unicodedata/__init__.py +306 -0
- fontTools/varLib/__init__.py +1600 -0
- fontTools/varLib/__main__.py +6 -0
- fontTools/varLib/avar/__init__.py +0 -0
- fontTools/varLib/avar/__main__.py +72 -0
- fontTools/varLib/avar/build.py +79 -0
- fontTools/varLib/avar/map.py +108 -0
- fontTools/varLib/avar/plan.py +1004 -0
- fontTools/varLib/avar/unbuild.py +271 -0
- fontTools/varLib/avarPlanner.py +8 -0
- fontTools/varLib/builder.py +215 -0
- fontTools/varLib/cff.py +631 -0
- fontTools/varLib/errors.py +219 -0
- fontTools/varLib/featureVars.py +703 -0
- fontTools/varLib/hvar.py +113 -0
- fontTools/varLib/instancer/__init__.py +2052 -0
- fontTools/varLib/instancer/__main__.py +5 -0
- fontTools/varLib/instancer/featureVars.py +190 -0
- fontTools/varLib/instancer/names.py +388 -0
- fontTools/varLib/instancer/solver.py +309 -0
- fontTools/varLib/interpolatable.py +1209 -0
- fontTools/varLib/interpolatableHelpers.py +399 -0
- fontTools/varLib/interpolatablePlot.py +1269 -0
- fontTools/varLib/interpolatableTestContourOrder.py +82 -0
- fontTools/varLib/interpolatableTestStartingPoint.py +107 -0
- fontTools/varLib/interpolate_layout.py +124 -0
- fontTools/varLib/iup.c +19815 -0
- fontTools/varLib/iup.cp311-win32.pyd +0 -0
- fontTools/varLib/iup.py +490 -0
- fontTools/varLib/merger.py +1717 -0
- fontTools/varLib/models.py +642 -0
- fontTools/varLib/multiVarStore.py +253 -0
- fontTools/varLib/mutator.py +529 -0
- fontTools/varLib/mvar.py +40 -0
- fontTools/varLib/plot.py +238 -0
- fontTools/varLib/stat.py +149 -0
- fontTools/varLib/varStore.py +739 -0
- fontTools/voltLib/__init__.py +5 -0
- fontTools/voltLib/__main__.py +206 -0
- fontTools/voltLib/ast.py +452 -0
- fontTools/voltLib/error.py +12 -0
- fontTools/voltLib/lexer.py +99 -0
- fontTools/voltLib/parser.py +664 -0
- fontTools/voltLib/voltToFea.py +911 -0
- fonttools-4.60.2.data/data/share/man/man1/ttx.1 +225 -0
- fonttools-4.60.2.dist-info/METADATA +2250 -0
- fonttools-4.60.2.dist-info/RECORD +353 -0
- fonttools-4.60.2.dist-info/WHEEL +5 -0
- fonttools-4.60.2.dist-info/entry_points.txt +5 -0
- fonttools-4.60.2.dist-info/licenses/LICENSE +21 -0
- fonttools-4.60.2.dist-info/licenses/LICENSE.external +388 -0
- fonttools-4.60.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,2703 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
"""fontTools.ttLib.tables.otTables -- A collection of classes representing the various
|
|
3
|
+
OpenType subtables.
|
|
4
|
+
|
|
5
|
+
Most are constructed upon import from data in otData.py, all are populated with
|
|
6
|
+
converter objects from otConverters.py.
|
|
7
|
+
"""
|
|
8
|
+
import copy
|
|
9
|
+
from enum import IntEnum
|
|
10
|
+
from functools import reduce
|
|
11
|
+
from math import radians
|
|
12
|
+
import itertools
|
|
13
|
+
from collections import defaultdict, namedtuple
|
|
14
|
+
from fontTools.ttLib import OPTIMIZE_FONT_SPEED
|
|
15
|
+
from fontTools.ttLib.tables.TupleVariation import TupleVariation
|
|
16
|
+
from fontTools.ttLib.tables.otTraverse import dfs_base_table
|
|
17
|
+
from fontTools.misc.arrayTools import quantizeRect
|
|
18
|
+
from fontTools.misc.roundTools import otRound
|
|
19
|
+
from fontTools.misc.transform import Transform, Identity, DecomposedTransform
|
|
20
|
+
from fontTools.misc.textTools import bytesjoin, pad, safeEval
|
|
21
|
+
from fontTools.misc.vector import Vector
|
|
22
|
+
from fontTools.pens.boundsPen import ControlBoundsPen
|
|
23
|
+
from fontTools.pens.transformPen import TransformPen
|
|
24
|
+
from .otBase import (
|
|
25
|
+
BaseTable,
|
|
26
|
+
FormatSwitchingBaseTable,
|
|
27
|
+
ValueRecord,
|
|
28
|
+
CountReference,
|
|
29
|
+
getFormatSwitchingBaseTableClass,
|
|
30
|
+
)
|
|
31
|
+
from fontTools.misc.fixedTools import (
|
|
32
|
+
fixedToFloat as fi2fl,
|
|
33
|
+
floatToFixed as fl2fi,
|
|
34
|
+
floatToFixedToStr as fl2str,
|
|
35
|
+
strToFixedToFloat as str2fl,
|
|
36
|
+
)
|
|
37
|
+
from fontTools.feaLib.lookupDebugInfo import LookupDebugInfo, LOOKUP_DEBUG_INFO_KEY
|
|
38
|
+
import logging
|
|
39
|
+
import struct
|
|
40
|
+
import array
|
|
41
|
+
import sys
|
|
42
|
+
from enum import IntFlag
|
|
43
|
+
from typing import TYPE_CHECKING, Iterator, List, Optional, Set
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
from fontTools.ttLib.ttGlyphSet import _TTGlyphSet
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
log = logging.getLogger(__name__)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class VarComponentFlags(IntFlag):
|
|
53
|
+
RESET_UNSPECIFIED_AXES = 1 << 0
|
|
54
|
+
|
|
55
|
+
HAVE_AXES = 1 << 1
|
|
56
|
+
|
|
57
|
+
AXIS_VALUES_HAVE_VARIATION = 1 << 2
|
|
58
|
+
TRANSFORM_HAS_VARIATION = 1 << 3
|
|
59
|
+
|
|
60
|
+
HAVE_TRANSLATE_X = 1 << 4
|
|
61
|
+
HAVE_TRANSLATE_Y = 1 << 5
|
|
62
|
+
HAVE_ROTATION = 1 << 6
|
|
63
|
+
|
|
64
|
+
HAVE_CONDITION = 1 << 7
|
|
65
|
+
|
|
66
|
+
HAVE_SCALE_X = 1 << 8
|
|
67
|
+
HAVE_SCALE_Y = 1 << 9
|
|
68
|
+
HAVE_TCENTER_X = 1 << 10
|
|
69
|
+
HAVE_TCENTER_Y = 1 << 11
|
|
70
|
+
|
|
71
|
+
GID_IS_24BIT = 1 << 12
|
|
72
|
+
|
|
73
|
+
HAVE_SKEW_X = 1 << 13
|
|
74
|
+
HAVE_SKEW_Y = 1 << 14
|
|
75
|
+
|
|
76
|
+
RESERVED_MASK = (1 << 32) - (1 << 15)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
VarTransformMappingValues = namedtuple(
|
|
80
|
+
"VarTransformMappingValues",
|
|
81
|
+
["flag", "fractionalBits", "scale", "defaultValue"],
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
VAR_TRANSFORM_MAPPING = {
|
|
85
|
+
"translateX": VarTransformMappingValues(
|
|
86
|
+
VarComponentFlags.HAVE_TRANSLATE_X, 0, 1, 0
|
|
87
|
+
),
|
|
88
|
+
"translateY": VarTransformMappingValues(
|
|
89
|
+
VarComponentFlags.HAVE_TRANSLATE_Y, 0, 1, 0
|
|
90
|
+
),
|
|
91
|
+
"rotation": VarTransformMappingValues(VarComponentFlags.HAVE_ROTATION, 12, 180, 0),
|
|
92
|
+
"scaleX": VarTransformMappingValues(VarComponentFlags.HAVE_SCALE_X, 10, 1, 1),
|
|
93
|
+
"scaleY": VarTransformMappingValues(VarComponentFlags.HAVE_SCALE_Y, 10, 1, 1),
|
|
94
|
+
"skewX": VarTransformMappingValues(VarComponentFlags.HAVE_SKEW_X, 12, -180, 0),
|
|
95
|
+
"skewY": VarTransformMappingValues(VarComponentFlags.HAVE_SKEW_Y, 12, 180, 0),
|
|
96
|
+
"tCenterX": VarTransformMappingValues(VarComponentFlags.HAVE_TCENTER_X, 0, 1, 0),
|
|
97
|
+
"tCenterY": VarTransformMappingValues(VarComponentFlags.HAVE_TCENTER_Y, 0, 1, 0),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Probably should be somewhere in fontTools.misc
|
|
101
|
+
_packer = {
|
|
102
|
+
1: lambda v: struct.pack(">B", v),
|
|
103
|
+
2: lambda v: struct.pack(">H", v),
|
|
104
|
+
3: lambda v: struct.pack(">L", v)[1:],
|
|
105
|
+
4: lambda v: struct.pack(">L", v),
|
|
106
|
+
}
|
|
107
|
+
_unpacker = {
|
|
108
|
+
1: lambda v: struct.unpack(">B", v)[0],
|
|
109
|
+
2: lambda v: struct.unpack(">H", v)[0],
|
|
110
|
+
3: lambda v: struct.unpack(">L", b"\0" + v)[0],
|
|
111
|
+
4: lambda v: struct.unpack(">L", v)[0],
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _read_uint32var(data, i):
|
|
116
|
+
"""Read a variable-length number from data starting at index i.
|
|
117
|
+
|
|
118
|
+
Return the number and the next index.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
b0 = data[i]
|
|
122
|
+
if b0 < 0x80:
|
|
123
|
+
return b0, i + 1
|
|
124
|
+
elif b0 < 0xC0:
|
|
125
|
+
return (b0 - 0x80) << 8 | data[i + 1], i + 2
|
|
126
|
+
elif b0 < 0xE0:
|
|
127
|
+
return (b0 - 0xC0) << 16 | data[i + 1] << 8 | data[i + 2], i + 3
|
|
128
|
+
elif b0 < 0xF0:
|
|
129
|
+
return (b0 - 0xE0) << 24 | data[i + 1] << 16 | data[i + 2] << 8 | data[
|
|
130
|
+
i + 3
|
|
131
|
+
], i + 4
|
|
132
|
+
else:
|
|
133
|
+
return (b0 - 0xF0) << 32 | data[i + 1] << 24 | data[i + 2] << 16 | data[
|
|
134
|
+
i + 3
|
|
135
|
+
] << 8 | data[i + 4], i + 5
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _write_uint32var(v):
|
|
139
|
+
"""Write a variable-length number.
|
|
140
|
+
|
|
141
|
+
Return the data.
|
|
142
|
+
"""
|
|
143
|
+
if v < 0x80:
|
|
144
|
+
return struct.pack(">B", v)
|
|
145
|
+
elif v < 0x4000:
|
|
146
|
+
return struct.pack(">H", (v | 0x8000))
|
|
147
|
+
elif v < 0x200000:
|
|
148
|
+
return struct.pack(">L", (v | 0xC00000))[1:]
|
|
149
|
+
elif v < 0x10000000:
|
|
150
|
+
return struct.pack(">L", (v | 0xE0000000))
|
|
151
|
+
else:
|
|
152
|
+
return struct.pack(">B", 0xF0) + struct.pack(">L", v)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class VarComponent:
|
|
156
|
+
def __init__(self):
|
|
157
|
+
self.populateDefaults()
|
|
158
|
+
|
|
159
|
+
def populateDefaults(self, propagator=None):
|
|
160
|
+
self.flags = 0
|
|
161
|
+
self.glyphName = None
|
|
162
|
+
self.conditionIndex = None
|
|
163
|
+
self.axisIndicesIndex = None
|
|
164
|
+
self.axisValues = ()
|
|
165
|
+
self.axisValuesVarIndex = NO_VARIATION_INDEX
|
|
166
|
+
self.transformVarIndex = NO_VARIATION_INDEX
|
|
167
|
+
self.transform = DecomposedTransform()
|
|
168
|
+
|
|
169
|
+
def decompile(self, data, font, localState):
|
|
170
|
+
i = 0
|
|
171
|
+
self.flags, i = _read_uint32var(data, i)
|
|
172
|
+
flags = self.flags
|
|
173
|
+
|
|
174
|
+
gidSize = 3 if flags & VarComponentFlags.GID_IS_24BIT else 2
|
|
175
|
+
glyphID = _unpacker[gidSize](data[i : i + gidSize])
|
|
176
|
+
i += gidSize
|
|
177
|
+
self.glyphName = font.glyphOrder[glyphID]
|
|
178
|
+
|
|
179
|
+
if flags & VarComponentFlags.HAVE_CONDITION:
|
|
180
|
+
self.conditionIndex, i = _read_uint32var(data, i)
|
|
181
|
+
|
|
182
|
+
if flags & VarComponentFlags.HAVE_AXES:
|
|
183
|
+
self.axisIndicesIndex, i = _read_uint32var(data, i)
|
|
184
|
+
else:
|
|
185
|
+
self.axisIndicesIndex = None
|
|
186
|
+
|
|
187
|
+
if self.axisIndicesIndex is None:
|
|
188
|
+
numAxes = 0
|
|
189
|
+
else:
|
|
190
|
+
axisIndices = localState["AxisIndicesList"].Item[self.axisIndicesIndex]
|
|
191
|
+
numAxes = len(axisIndices)
|
|
192
|
+
|
|
193
|
+
if flags & VarComponentFlags.HAVE_AXES:
|
|
194
|
+
axisValues, i = TupleVariation.decompileDeltas_(numAxes, data, i)
|
|
195
|
+
self.axisValues = tuple(fi2fl(v, 14) for v in axisValues)
|
|
196
|
+
else:
|
|
197
|
+
self.axisValues = ()
|
|
198
|
+
assert len(self.axisValues) == numAxes
|
|
199
|
+
|
|
200
|
+
if flags & VarComponentFlags.AXIS_VALUES_HAVE_VARIATION:
|
|
201
|
+
self.axisValuesVarIndex, i = _read_uint32var(data, i)
|
|
202
|
+
else:
|
|
203
|
+
self.axisValuesVarIndex = NO_VARIATION_INDEX
|
|
204
|
+
if flags & VarComponentFlags.TRANSFORM_HAS_VARIATION:
|
|
205
|
+
self.transformVarIndex, i = _read_uint32var(data, i)
|
|
206
|
+
else:
|
|
207
|
+
self.transformVarIndex = NO_VARIATION_INDEX
|
|
208
|
+
|
|
209
|
+
self.transform = DecomposedTransform()
|
|
210
|
+
|
|
211
|
+
def read_transform_component(values):
|
|
212
|
+
nonlocal i
|
|
213
|
+
if flags & values.flag:
|
|
214
|
+
v = (
|
|
215
|
+
fi2fl(
|
|
216
|
+
struct.unpack(">h", data[i : i + 2])[0], values.fractionalBits
|
|
217
|
+
)
|
|
218
|
+
* values.scale
|
|
219
|
+
)
|
|
220
|
+
i += 2
|
|
221
|
+
return v
|
|
222
|
+
else:
|
|
223
|
+
return values.defaultValue
|
|
224
|
+
|
|
225
|
+
for attr_name, mapping_values in VAR_TRANSFORM_MAPPING.items():
|
|
226
|
+
value = read_transform_component(mapping_values)
|
|
227
|
+
setattr(self.transform, attr_name, value)
|
|
228
|
+
|
|
229
|
+
if not (flags & VarComponentFlags.HAVE_SCALE_Y):
|
|
230
|
+
self.transform.scaleY = self.transform.scaleX
|
|
231
|
+
|
|
232
|
+
n = flags & VarComponentFlags.RESERVED_MASK
|
|
233
|
+
while n:
|
|
234
|
+
_, i = _read_uint32var(data, i)
|
|
235
|
+
n &= n - 1
|
|
236
|
+
|
|
237
|
+
return data[i:]
|
|
238
|
+
|
|
239
|
+
def compile(self, font):
|
|
240
|
+
optimizeSpeed = font.cfg[OPTIMIZE_FONT_SPEED]
|
|
241
|
+
|
|
242
|
+
data = []
|
|
243
|
+
|
|
244
|
+
flags = self.flags
|
|
245
|
+
|
|
246
|
+
glyphID = font.getGlyphID(self.glyphName)
|
|
247
|
+
if glyphID > 65535:
|
|
248
|
+
flags |= VarComponentFlags.GID_IS_24BIT
|
|
249
|
+
data.append(_packer[3](glyphID))
|
|
250
|
+
else:
|
|
251
|
+
flags &= ~VarComponentFlags.GID_IS_24BIT
|
|
252
|
+
data.append(_packer[2](glyphID))
|
|
253
|
+
|
|
254
|
+
if self.conditionIndex is not None:
|
|
255
|
+
flags |= VarComponentFlags.HAVE_CONDITION
|
|
256
|
+
data.append(_write_uint32var(self.conditionIndex))
|
|
257
|
+
|
|
258
|
+
numAxes = len(self.axisValues)
|
|
259
|
+
|
|
260
|
+
if numAxes:
|
|
261
|
+
flags |= VarComponentFlags.HAVE_AXES
|
|
262
|
+
data.append(_write_uint32var(self.axisIndicesIndex))
|
|
263
|
+
data.append(
|
|
264
|
+
TupleVariation.compileDeltaValues_(
|
|
265
|
+
[fl2fi(v, 14) for v in self.axisValues],
|
|
266
|
+
optimizeSize=not optimizeSpeed,
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
else:
|
|
270
|
+
flags &= ~VarComponentFlags.HAVE_AXES
|
|
271
|
+
|
|
272
|
+
if self.axisValuesVarIndex != NO_VARIATION_INDEX:
|
|
273
|
+
flags |= VarComponentFlags.AXIS_VALUES_HAVE_VARIATION
|
|
274
|
+
data.append(_write_uint32var(self.axisValuesVarIndex))
|
|
275
|
+
else:
|
|
276
|
+
flags &= ~VarComponentFlags.AXIS_VALUES_HAVE_VARIATION
|
|
277
|
+
if self.transformVarIndex != NO_VARIATION_INDEX:
|
|
278
|
+
flags |= VarComponentFlags.TRANSFORM_HAS_VARIATION
|
|
279
|
+
data.append(_write_uint32var(self.transformVarIndex))
|
|
280
|
+
else:
|
|
281
|
+
flags &= ~VarComponentFlags.TRANSFORM_HAS_VARIATION
|
|
282
|
+
|
|
283
|
+
def write_transform_component(value, values):
|
|
284
|
+
if flags & values.flag:
|
|
285
|
+
return struct.pack(
|
|
286
|
+
">h", fl2fi(value / values.scale, values.fractionalBits)
|
|
287
|
+
)
|
|
288
|
+
else:
|
|
289
|
+
return b""
|
|
290
|
+
|
|
291
|
+
for attr_name, mapping_values in VAR_TRANSFORM_MAPPING.items():
|
|
292
|
+
value = getattr(self.transform, attr_name)
|
|
293
|
+
data.append(write_transform_component(value, mapping_values))
|
|
294
|
+
|
|
295
|
+
return _write_uint32var(flags) + bytesjoin(data)
|
|
296
|
+
|
|
297
|
+
def toXML(self, writer, ttFont, attrs):
|
|
298
|
+
writer.begintag("VarComponent", attrs)
|
|
299
|
+
writer.newline()
|
|
300
|
+
|
|
301
|
+
def write(name, value, attrs=()):
|
|
302
|
+
if value is not None:
|
|
303
|
+
writer.simpletag(name, (("value", value),) + attrs)
|
|
304
|
+
writer.newline()
|
|
305
|
+
|
|
306
|
+
write("glyphName", self.glyphName)
|
|
307
|
+
|
|
308
|
+
if self.conditionIndex is not None:
|
|
309
|
+
write("conditionIndex", self.conditionIndex)
|
|
310
|
+
if self.axisIndicesIndex is not None:
|
|
311
|
+
write("axisIndicesIndex", self.axisIndicesIndex)
|
|
312
|
+
if (
|
|
313
|
+
self.axisIndicesIndex is not None
|
|
314
|
+
or self.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES
|
|
315
|
+
):
|
|
316
|
+
if self.flags & VarComponentFlags.RESET_UNSPECIFIED_AXES:
|
|
317
|
+
attrs = (("resetUnspecifiedAxes", 1),)
|
|
318
|
+
else:
|
|
319
|
+
attrs = ()
|
|
320
|
+
write("axisValues", [float(fl2str(v, 14)) for v in self.axisValues], attrs)
|
|
321
|
+
|
|
322
|
+
if self.axisValuesVarIndex != NO_VARIATION_INDEX:
|
|
323
|
+
write("axisValuesVarIndex", self.axisValuesVarIndex)
|
|
324
|
+
if self.transformVarIndex != NO_VARIATION_INDEX:
|
|
325
|
+
write("transformVarIndex", self.transformVarIndex)
|
|
326
|
+
|
|
327
|
+
# Only write transform components that are specified in the
|
|
328
|
+
# flags, even if they are the default value.
|
|
329
|
+
for attr_name, mapping in VAR_TRANSFORM_MAPPING.items():
|
|
330
|
+
if not (self.flags & mapping.flag):
|
|
331
|
+
continue
|
|
332
|
+
v = getattr(self.transform, attr_name)
|
|
333
|
+
write(attr_name, fl2str(v, mapping.fractionalBits))
|
|
334
|
+
|
|
335
|
+
writer.endtag("VarComponent")
|
|
336
|
+
writer.newline()
|
|
337
|
+
|
|
338
|
+
def fromXML(self, name, attrs, content, ttFont):
|
|
339
|
+
content = [c for c in content if isinstance(c, tuple)]
|
|
340
|
+
|
|
341
|
+
self.populateDefaults()
|
|
342
|
+
|
|
343
|
+
for name, attrs, content in content:
|
|
344
|
+
assert not content
|
|
345
|
+
v = attrs["value"]
|
|
346
|
+
|
|
347
|
+
if name == "glyphName":
|
|
348
|
+
self.glyphName = v
|
|
349
|
+
elif name == "conditionIndex":
|
|
350
|
+
self.conditionIndex = safeEval(v)
|
|
351
|
+
elif name == "axisIndicesIndex":
|
|
352
|
+
self.axisIndicesIndex = safeEval(v)
|
|
353
|
+
elif name == "axisValues":
|
|
354
|
+
self.axisValues = tuple(str2fl(v, 14) for v in safeEval(v))
|
|
355
|
+
if safeEval(attrs.get("resetUnspecifiedAxes", "0")):
|
|
356
|
+
self.flags |= VarComponentFlags.RESET_UNSPECIFIED_AXES
|
|
357
|
+
elif name == "axisValuesVarIndex":
|
|
358
|
+
self.axisValuesVarIndex = safeEval(v)
|
|
359
|
+
elif name == "transformVarIndex":
|
|
360
|
+
self.transformVarIndex = safeEval(v)
|
|
361
|
+
elif name in VAR_TRANSFORM_MAPPING:
|
|
362
|
+
setattr(
|
|
363
|
+
self.transform,
|
|
364
|
+
name,
|
|
365
|
+
safeEval(v),
|
|
366
|
+
)
|
|
367
|
+
self.flags |= VAR_TRANSFORM_MAPPING[name].flag
|
|
368
|
+
else:
|
|
369
|
+
assert False, name
|
|
370
|
+
|
|
371
|
+
def applyTransformDeltas(self, deltas):
|
|
372
|
+
i = 0
|
|
373
|
+
|
|
374
|
+
def read_transform_component_delta(values):
|
|
375
|
+
nonlocal i
|
|
376
|
+
if self.flags & values.flag:
|
|
377
|
+
v = fi2fl(deltas[i], values.fractionalBits) * values.scale
|
|
378
|
+
i += 1
|
|
379
|
+
return v
|
|
380
|
+
else:
|
|
381
|
+
return 0
|
|
382
|
+
|
|
383
|
+
for attr_name, mapping_values in VAR_TRANSFORM_MAPPING.items():
|
|
384
|
+
value = read_transform_component_delta(mapping_values)
|
|
385
|
+
setattr(
|
|
386
|
+
self.transform, attr_name, getattr(self.transform, attr_name) + value
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
if not (self.flags & VarComponentFlags.HAVE_SCALE_Y):
|
|
390
|
+
self.transform.scaleY = self.transform.scaleX
|
|
391
|
+
|
|
392
|
+
assert i == len(deltas), (i, len(deltas))
|
|
393
|
+
|
|
394
|
+
def __eq__(self, other):
|
|
395
|
+
if type(self) != type(other):
|
|
396
|
+
return NotImplemented
|
|
397
|
+
return self.__dict__ == other.__dict__
|
|
398
|
+
|
|
399
|
+
def __ne__(self, other):
|
|
400
|
+
result = self.__eq__(other)
|
|
401
|
+
return result if result is NotImplemented else not result
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class VarCompositeGlyph:
|
|
405
|
+
def __init__(self, components=None):
|
|
406
|
+
self.components = components if components is not None else []
|
|
407
|
+
|
|
408
|
+
def decompile(self, data, font, localState):
|
|
409
|
+
self.components = []
|
|
410
|
+
while data:
|
|
411
|
+
component = VarComponent()
|
|
412
|
+
data = component.decompile(data, font, localState)
|
|
413
|
+
self.components.append(component)
|
|
414
|
+
|
|
415
|
+
def compile(self, font):
|
|
416
|
+
data = []
|
|
417
|
+
for component in self.components:
|
|
418
|
+
data.append(component.compile(font))
|
|
419
|
+
return bytesjoin(data)
|
|
420
|
+
|
|
421
|
+
def toXML(self, xmlWriter, font, attrs, name):
|
|
422
|
+
xmlWriter.begintag("VarCompositeGlyph", attrs)
|
|
423
|
+
xmlWriter.newline()
|
|
424
|
+
for i, component in enumerate(self.components):
|
|
425
|
+
component.toXML(xmlWriter, font, [("index", i)])
|
|
426
|
+
xmlWriter.endtag("VarCompositeGlyph")
|
|
427
|
+
xmlWriter.newline()
|
|
428
|
+
|
|
429
|
+
def fromXML(self, name, attrs, content, font):
|
|
430
|
+
content = [c for c in content if isinstance(c, tuple)]
|
|
431
|
+
for name, attrs, content in content:
|
|
432
|
+
assert name == "VarComponent"
|
|
433
|
+
component = VarComponent()
|
|
434
|
+
component.fromXML(name, attrs, content, font)
|
|
435
|
+
self.components.append(component)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class AATStateTable(object):
|
|
439
|
+
def __init__(self):
|
|
440
|
+
self.GlyphClasses = {} # GlyphID --> GlyphClass
|
|
441
|
+
self.States = [] # List of AATState, indexed by state number
|
|
442
|
+
self.PerGlyphLookups = [] # [{GlyphID:GlyphID}, ...]
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class AATState(object):
|
|
446
|
+
def __init__(self):
|
|
447
|
+
self.Transitions = {} # GlyphClass --> AATAction
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class AATAction(object):
|
|
451
|
+
_FLAGS = None
|
|
452
|
+
|
|
453
|
+
@staticmethod
|
|
454
|
+
def compileActions(font, states):
|
|
455
|
+
return (None, None)
|
|
456
|
+
|
|
457
|
+
def _writeFlagsToXML(self, xmlWriter):
|
|
458
|
+
flags = [f for f in self._FLAGS if self.__dict__[f]]
|
|
459
|
+
if flags:
|
|
460
|
+
xmlWriter.simpletag("Flags", value=",".join(flags))
|
|
461
|
+
xmlWriter.newline()
|
|
462
|
+
if self.ReservedFlags != 0:
|
|
463
|
+
xmlWriter.simpletag("ReservedFlags", value="0x%04X" % self.ReservedFlags)
|
|
464
|
+
xmlWriter.newline()
|
|
465
|
+
|
|
466
|
+
def _setFlag(self, flag):
|
|
467
|
+
assert flag in self._FLAGS, "unsupported flag %s" % flag
|
|
468
|
+
self.__dict__[flag] = True
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
class RearrangementMorphAction(AATAction):
|
|
472
|
+
staticSize = 4
|
|
473
|
+
actionHeaderSize = 0
|
|
474
|
+
_FLAGS = ["MarkFirst", "DontAdvance", "MarkLast"]
|
|
475
|
+
|
|
476
|
+
_VERBS = {
|
|
477
|
+
0: "no change",
|
|
478
|
+
1: "Ax ⇒ xA",
|
|
479
|
+
2: "xD ⇒ Dx",
|
|
480
|
+
3: "AxD ⇒ DxA",
|
|
481
|
+
4: "ABx ⇒ xAB",
|
|
482
|
+
5: "ABx ⇒ xBA",
|
|
483
|
+
6: "xCD ⇒ CDx",
|
|
484
|
+
7: "xCD ⇒ DCx",
|
|
485
|
+
8: "AxCD ⇒ CDxA",
|
|
486
|
+
9: "AxCD ⇒ DCxA",
|
|
487
|
+
10: "ABxD ⇒ DxAB",
|
|
488
|
+
11: "ABxD ⇒ DxBA",
|
|
489
|
+
12: "ABxCD ⇒ CDxAB",
|
|
490
|
+
13: "ABxCD ⇒ CDxBA",
|
|
491
|
+
14: "ABxCD ⇒ DCxAB",
|
|
492
|
+
15: "ABxCD ⇒ DCxBA",
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
def __init__(self):
|
|
496
|
+
self.NewState = 0
|
|
497
|
+
self.Verb = 0
|
|
498
|
+
self.MarkFirst = False
|
|
499
|
+
self.DontAdvance = False
|
|
500
|
+
self.MarkLast = False
|
|
501
|
+
self.ReservedFlags = 0
|
|
502
|
+
|
|
503
|
+
def compile(self, writer, font, actionIndex):
|
|
504
|
+
assert actionIndex is None
|
|
505
|
+
writer.writeUShort(self.NewState)
|
|
506
|
+
assert self.Verb >= 0 and self.Verb <= 15, self.Verb
|
|
507
|
+
flags = self.Verb | self.ReservedFlags
|
|
508
|
+
if self.MarkFirst:
|
|
509
|
+
flags |= 0x8000
|
|
510
|
+
if self.DontAdvance:
|
|
511
|
+
flags |= 0x4000
|
|
512
|
+
if self.MarkLast:
|
|
513
|
+
flags |= 0x2000
|
|
514
|
+
writer.writeUShort(flags)
|
|
515
|
+
|
|
516
|
+
def decompile(self, reader, font, actionReader):
|
|
517
|
+
assert actionReader is None
|
|
518
|
+
self.NewState = reader.readUShort()
|
|
519
|
+
flags = reader.readUShort()
|
|
520
|
+
self.Verb = flags & 0xF
|
|
521
|
+
self.MarkFirst = bool(flags & 0x8000)
|
|
522
|
+
self.DontAdvance = bool(flags & 0x4000)
|
|
523
|
+
self.MarkLast = bool(flags & 0x2000)
|
|
524
|
+
self.ReservedFlags = flags & 0x1FF0
|
|
525
|
+
|
|
526
|
+
def toXML(self, xmlWriter, font, attrs, name):
|
|
527
|
+
xmlWriter.begintag(name, **attrs)
|
|
528
|
+
xmlWriter.newline()
|
|
529
|
+
xmlWriter.simpletag("NewState", value=self.NewState)
|
|
530
|
+
xmlWriter.newline()
|
|
531
|
+
self._writeFlagsToXML(xmlWriter)
|
|
532
|
+
xmlWriter.simpletag("Verb", value=self.Verb)
|
|
533
|
+
verbComment = self._VERBS.get(self.Verb)
|
|
534
|
+
if verbComment is not None:
|
|
535
|
+
xmlWriter.comment(verbComment)
|
|
536
|
+
xmlWriter.newline()
|
|
537
|
+
xmlWriter.endtag(name)
|
|
538
|
+
xmlWriter.newline()
|
|
539
|
+
|
|
540
|
+
def fromXML(self, name, attrs, content, font):
|
|
541
|
+
self.NewState = self.Verb = self.ReservedFlags = 0
|
|
542
|
+
self.MarkFirst = self.DontAdvance = self.MarkLast = False
|
|
543
|
+
content = [t for t in content if isinstance(t, tuple)]
|
|
544
|
+
for eltName, eltAttrs, eltContent in content:
|
|
545
|
+
if eltName == "NewState":
|
|
546
|
+
self.NewState = safeEval(eltAttrs["value"])
|
|
547
|
+
elif eltName == "Verb":
|
|
548
|
+
self.Verb = safeEval(eltAttrs["value"])
|
|
549
|
+
elif eltName == "ReservedFlags":
|
|
550
|
+
self.ReservedFlags = safeEval(eltAttrs["value"])
|
|
551
|
+
elif eltName == "Flags":
|
|
552
|
+
for flag in eltAttrs["value"].split(","):
|
|
553
|
+
self._setFlag(flag.strip())
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
class ContextualMorphAction(AATAction):
|
|
557
|
+
staticSize = 8
|
|
558
|
+
actionHeaderSize = 0
|
|
559
|
+
_FLAGS = ["SetMark", "DontAdvance"]
|
|
560
|
+
|
|
561
|
+
def __init__(self):
|
|
562
|
+
self.NewState = 0
|
|
563
|
+
self.SetMark, self.DontAdvance = False, False
|
|
564
|
+
self.ReservedFlags = 0
|
|
565
|
+
self.MarkIndex, self.CurrentIndex = 0xFFFF, 0xFFFF
|
|
566
|
+
|
|
567
|
+
def compile(self, writer, font, actionIndex):
|
|
568
|
+
assert actionIndex is None
|
|
569
|
+
writer.writeUShort(self.NewState)
|
|
570
|
+
flags = self.ReservedFlags
|
|
571
|
+
if self.SetMark:
|
|
572
|
+
flags |= 0x8000
|
|
573
|
+
if self.DontAdvance:
|
|
574
|
+
flags |= 0x4000
|
|
575
|
+
writer.writeUShort(flags)
|
|
576
|
+
writer.writeUShort(self.MarkIndex)
|
|
577
|
+
writer.writeUShort(self.CurrentIndex)
|
|
578
|
+
|
|
579
|
+
def decompile(self, reader, font, actionReader):
|
|
580
|
+
assert actionReader is None
|
|
581
|
+
self.NewState = reader.readUShort()
|
|
582
|
+
flags = reader.readUShort()
|
|
583
|
+
self.SetMark = bool(flags & 0x8000)
|
|
584
|
+
self.DontAdvance = bool(flags & 0x4000)
|
|
585
|
+
self.ReservedFlags = flags & 0x3FFF
|
|
586
|
+
self.MarkIndex = reader.readUShort()
|
|
587
|
+
self.CurrentIndex = reader.readUShort()
|
|
588
|
+
|
|
589
|
+
def toXML(self, xmlWriter, font, attrs, name):
|
|
590
|
+
xmlWriter.begintag(name, **attrs)
|
|
591
|
+
xmlWriter.newline()
|
|
592
|
+
xmlWriter.simpletag("NewState", value=self.NewState)
|
|
593
|
+
xmlWriter.newline()
|
|
594
|
+
self._writeFlagsToXML(xmlWriter)
|
|
595
|
+
xmlWriter.simpletag("MarkIndex", value=self.MarkIndex)
|
|
596
|
+
xmlWriter.newline()
|
|
597
|
+
xmlWriter.simpletag("CurrentIndex", value=self.CurrentIndex)
|
|
598
|
+
xmlWriter.newline()
|
|
599
|
+
xmlWriter.endtag(name)
|
|
600
|
+
xmlWriter.newline()
|
|
601
|
+
|
|
602
|
+
def fromXML(self, name, attrs, content, font):
|
|
603
|
+
self.NewState = self.ReservedFlags = 0
|
|
604
|
+
self.SetMark = self.DontAdvance = False
|
|
605
|
+
self.MarkIndex, self.CurrentIndex = 0xFFFF, 0xFFFF
|
|
606
|
+
content = [t for t in content if isinstance(t, tuple)]
|
|
607
|
+
for eltName, eltAttrs, eltContent in content:
|
|
608
|
+
if eltName == "NewState":
|
|
609
|
+
self.NewState = safeEval(eltAttrs["value"])
|
|
610
|
+
elif eltName == "Flags":
|
|
611
|
+
for flag in eltAttrs["value"].split(","):
|
|
612
|
+
self._setFlag(flag.strip())
|
|
613
|
+
elif eltName == "ReservedFlags":
|
|
614
|
+
self.ReservedFlags = safeEval(eltAttrs["value"])
|
|
615
|
+
elif eltName == "MarkIndex":
|
|
616
|
+
self.MarkIndex = safeEval(eltAttrs["value"])
|
|
617
|
+
elif eltName == "CurrentIndex":
|
|
618
|
+
self.CurrentIndex = safeEval(eltAttrs["value"])
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
class LigAction(object):
|
|
622
|
+
def __init__(self):
|
|
623
|
+
self.Store = False
|
|
624
|
+
# GlyphIndexDelta is a (possibly negative) delta that gets
|
|
625
|
+
# added to the glyph ID at the top of the AAT runtime
|
|
626
|
+
# execution stack. It is *not* a byte offset into the
|
|
627
|
+
# morx table. The result of the addition, which is performed
|
|
628
|
+
# at run time by the shaping engine, is an index into
|
|
629
|
+
# the ligature components table. See 'morx' specification.
|
|
630
|
+
# In the AAT specification, this field is called Offset;
|
|
631
|
+
# but its meaning is quite different from other offsets
|
|
632
|
+
# in either AAT or OpenType, so we use a different name.
|
|
633
|
+
self.GlyphIndexDelta = 0
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
class LigatureMorphAction(AATAction):
|
|
637
|
+
staticSize = 6
|
|
638
|
+
|
|
639
|
+
# 4 bytes for each of {action,ligComponents,ligatures}Offset
|
|
640
|
+
actionHeaderSize = 12
|
|
641
|
+
|
|
642
|
+
_FLAGS = ["SetComponent", "DontAdvance"]
|
|
643
|
+
|
|
644
|
+
def __init__(self):
|
|
645
|
+
self.NewState = 0
|
|
646
|
+
self.SetComponent, self.DontAdvance = False, False
|
|
647
|
+
self.ReservedFlags = 0
|
|
648
|
+
self.Actions = []
|
|
649
|
+
|
|
650
|
+
def compile(self, writer, font, actionIndex):
|
|
651
|
+
assert actionIndex is not None
|
|
652
|
+
writer.writeUShort(self.NewState)
|
|
653
|
+
flags = self.ReservedFlags
|
|
654
|
+
if self.SetComponent:
|
|
655
|
+
flags |= 0x8000
|
|
656
|
+
if self.DontAdvance:
|
|
657
|
+
flags |= 0x4000
|
|
658
|
+
if len(self.Actions) > 0:
|
|
659
|
+
flags |= 0x2000
|
|
660
|
+
writer.writeUShort(flags)
|
|
661
|
+
if len(self.Actions) > 0:
|
|
662
|
+
actions = self.compileLigActions()
|
|
663
|
+
writer.writeUShort(actionIndex[actions])
|
|
664
|
+
else:
|
|
665
|
+
writer.writeUShort(0)
|
|
666
|
+
|
|
667
|
+
def decompile(self, reader, font, actionReader):
|
|
668
|
+
assert actionReader is not None
|
|
669
|
+
self.NewState = reader.readUShort()
|
|
670
|
+
flags = reader.readUShort()
|
|
671
|
+
self.SetComponent = bool(flags & 0x8000)
|
|
672
|
+
self.DontAdvance = bool(flags & 0x4000)
|
|
673
|
+
performAction = bool(flags & 0x2000)
|
|
674
|
+
# As of 2017-09-12, the 'morx' specification says that
|
|
675
|
+
# the reserved bitmask in ligature subtables is 0x3FFF.
|
|
676
|
+
# However, the specification also defines a flag 0x2000,
|
|
677
|
+
# so the reserved value should actually be 0x1FFF.
|
|
678
|
+
# TODO: Report this specification bug to Apple.
|
|
679
|
+
self.ReservedFlags = flags & 0x1FFF
|
|
680
|
+
actionIndex = reader.readUShort()
|
|
681
|
+
if performAction:
|
|
682
|
+
self.Actions = self._decompileLigActions(actionReader, actionIndex)
|
|
683
|
+
else:
|
|
684
|
+
self.Actions = []
|
|
685
|
+
|
|
686
|
+
@staticmethod
|
|
687
|
+
def compileActions(font, states):
|
|
688
|
+
result, actions, actionIndex = b"", set(), {}
|
|
689
|
+
for state in states:
|
|
690
|
+
for _glyphClass, trans in state.Transitions.items():
|
|
691
|
+
actions.add(trans.compileLigActions())
|
|
692
|
+
# Sort the compiled actions in decreasing order of
|
|
693
|
+
# length, so that the longer sequence come before the
|
|
694
|
+
# shorter ones. For each compiled action ABCD, its
|
|
695
|
+
# suffixes BCD, CD, and D do not be encoded separately
|
|
696
|
+
# (in case they occur); instead, we can just store an
|
|
697
|
+
# index that points into the middle of the longer
|
|
698
|
+
# sequence. Every compiled AAT ligature sequence is
|
|
699
|
+
# terminated with an end-of-sequence flag, which can
|
|
700
|
+
# only be set on the last element of the sequence.
|
|
701
|
+
# Therefore, it is sufficient to consider just the
|
|
702
|
+
# suffixes.
|
|
703
|
+
for a in sorted(actions, key=lambda x: (-len(x), x)):
|
|
704
|
+
if a not in actionIndex:
|
|
705
|
+
for i in range(0, len(a), 4):
|
|
706
|
+
suffix = a[i:]
|
|
707
|
+
suffixIndex = (len(result) + i) // 4
|
|
708
|
+
actionIndex.setdefault(suffix, suffixIndex)
|
|
709
|
+
result += a
|
|
710
|
+
result = pad(result, 4)
|
|
711
|
+
return (result, actionIndex)
|
|
712
|
+
|
|
713
|
+
def compileLigActions(self):
|
|
714
|
+
result = []
|
|
715
|
+
for i, action in enumerate(self.Actions):
|
|
716
|
+
last = i == len(self.Actions) - 1
|
|
717
|
+
value = action.GlyphIndexDelta & 0x3FFFFFFF
|
|
718
|
+
value |= 0x80000000 if last else 0
|
|
719
|
+
value |= 0x40000000 if action.Store else 0
|
|
720
|
+
result.append(struct.pack(">L", value))
|
|
721
|
+
return bytesjoin(result)
|
|
722
|
+
|
|
723
|
+
def _decompileLigActions(self, actionReader, actionIndex):
|
|
724
|
+
actions = []
|
|
725
|
+
last = False
|
|
726
|
+
reader = actionReader.getSubReader(actionReader.pos + actionIndex * 4)
|
|
727
|
+
while not last:
|
|
728
|
+
value = reader.readULong()
|
|
729
|
+
last = bool(value & 0x80000000)
|
|
730
|
+
action = LigAction()
|
|
731
|
+
actions.append(action)
|
|
732
|
+
action.Store = bool(value & 0x40000000)
|
|
733
|
+
delta = value & 0x3FFFFFFF
|
|
734
|
+
if delta >= 0x20000000: # sign-extend 30-bit value
|
|
735
|
+
delta = -0x40000000 + delta
|
|
736
|
+
action.GlyphIndexDelta = delta
|
|
737
|
+
return actions
|
|
738
|
+
|
|
739
|
+
def fromXML(self, name, attrs, content, font):
|
|
740
|
+
self.NewState = self.ReservedFlags = 0
|
|
741
|
+
self.SetComponent = self.DontAdvance = False
|
|
742
|
+
self.ReservedFlags = 0
|
|
743
|
+
self.Actions = []
|
|
744
|
+
content = [t for t in content if isinstance(t, tuple)]
|
|
745
|
+
for eltName, eltAttrs, eltContent in content:
|
|
746
|
+
if eltName == "NewState":
|
|
747
|
+
self.NewState = safeEval(eltAttrs["value"])
|
|
748
|
+
elif eltName == "Flags":
|
|
749
|
+
for flag in eltAttrs["value"].split(","):
|
|
750
|
+
self._setFlag(flag.strip())
|
|
751
|
+
elif eltName == "ReservedFlags":
|
|
752
|
+
self.ReservedFlags = safeEval(eltAttrs["value"])
|
|
753
|
+
elif eltName == "Action":
|
|
754
|
+
action = LigAction()
|
|
755
|
+
flags = eltAttrs.get("Flags", "").split(",")
|
|
756
|
+
flags = [f.strip() for f in flags]
|
|
757
|
+
action.Store = "Store" in flags
|
|
758
|
+
action.GlyphIndexDelta = safeEval(eltAttrs["GlyphIndexDelta"])
|
|
759
|
+
self.Actions.append(action)
|
|
760
|
+
|
|
761
|
+
def toXML(self, xmlWriter, font, attrs, name):
|
|
762
|
+
xmlWriter.begintag(name, **attrs)
|
|
763
|
+
xmlWriter.newline()
|
|
764
|
+
xmlWriter.simpletag("NewState", value=self.NewState)
|
|
765
|
+
xmlWriter.newline()
|
|
766
|
+
self._writeFlagsToXML(xmlWriter)
|
|
767
|
+
for action in self.Actions:
|
|
768
|
+
attribs = [("GlyphIndexDelta", action.GlyphIndexDelta)]
|
|
769
|
+
if action.Store:
|
|
770
|
+
attribs.append(("Flags", "Store"))
|
|
771
|
+
xmlWriter.simpletag("Action", attribs)
|
|
772
|
+
xmlWriter.newline()
|
|
773
|
+
xmlWriter.endtag(name)
|
|
774
|
+
xmlWriter.newline()
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
class InsertionMorphAction(AATAction):
|
|
778
|
+
staticSize = 8
|
|
779
|
+
actionHeaderSize = 4 # 4 bytes for actionOffset
|
|
780
|
+
_FLAGS = [
|
|
781
|
+
"SetMark",
|
|
782
|
+
"DontAdvance",
|
|
783
|
+
"CurrentIsKashidaLike",
|
|
784
|
+
"MarkedIsKashidaLike",
|
|
785
|
+
"CurrentInsertBefore",
|
|
786
|
+
"MarkedInsertBefore",
|
|
787
|
+
]
|
|
788
|
+
|
|
789
|
+
def __init__(self):
|
|
790
|
+
self.NewState = 0
|
|
791
|
+
for flag in self._FLAGS:
|
|
792
|
+
setattr(self, flag, False)
|
|
793
|
+
self.ReservedFlags = 0
|
|
794
|
+
self.CurrentInsertionAction, self.MarkedInsertionAction = [], []
|
|
795
|
+
|
|
796
|
+
def compile(self, writer, font, actionIndex):
|
|
797
|
+
assert actionIndex is not None
|
|
798
|
+
writer.writeUShort(self.NewState)
|
|
799
|
+
flags = self.ReservedFlags
|
|
800
|
+
if self.SetMark:
|
|
801
|
+
flags |= 0x8000
|
|
802
|
+
if self.DontAdvance:
|
|
803
|
+
flags |= 0x4000
|
|
804
|
+
if self.CurrentIsKashidaLike:
|
|
805
|
+
flags |= 0x2000
|
|
806
|
+
if self.MarkedIsKashidaLike:
|
|
807
|
+
flags |= 0x1000
|
|
808
|
+
if self.CurrentInsertBefore:
|
|
809
|
+
flags |= 0x0800
|
|
810
|
+
if self.MarkedInsertBefore:
|
|
811
|
+
flags |= 0x0400
|
|
812
|
+
flags |= len(self.CurrentInsertionAction) << 5
|
|
813
|
+
flags |= len(self.MarkedInsertionAction)
|
|
814
|
+
writer.writeUShort(flags)
|
|
815
|
+
if len(self.CurrentInsertionAction) > 0:
|
|
816
|
+
currentIndex = actionIndex[tuple(self.CurrentInsertionAction)]
|
|
817
|
+
else:
|
|
818
|
+
currentIndex = 0xFFFF
|
|
819
|
+
writer.writeUShort(currentIndex)
|
|
820
|
+
if len(self.MarkedInsertionAction) > 0:
|
|
821
|
+
markedIndex = actionIndex[tuple(self.MarkedInsertionAction)]
|
|
822
|
+
else:
|
|
823
|
+
markedIndex = 0xFFFF
|
|
824
|
+
writer.writeUShort(markedIndex)
|
|
825
|
+
|
|
826
|
+
def decompile(self, reader, font, actionReader):
|
|
827
|
+
assert actionReader is not None
|
|
828
|
+
self.NewState = reader.readUShort()
|
|
829
|
+
flags = reader.readUShort()
|
|
830
|
+
self.SetMark = bool(flags & 0x8000)
|
|
831
|
+
self.DontAdvance = bool(flags & 0x4000)
|
|
832
|
+
self.CurrentIsKashidaLike = bool(flags & 0x2000)
|
|
833
|
+
self.MarkedIsKashidaLike = bool(flags & 0x1000)
|
|
834
|
+
self.CurrentInsertBefore = bool(flags & 0x0800)
|
|
835
|
+
self.MarkedInsertBefore = bool(flags & 0x0400)
|
|
836
|
+
self.CurrentInsertionAction = self._decompileInsertionAction(
|
|
837
|
+
actionReader, font, index=reader.readUShort(), count=((flags & 0x03E0) >> 5)
|
|
838
|
+
)
|
|
839
|
+
self.MarkedInsertionAction = self._decompileInsertionAction(
|
|
840
|
+
actionReader, font, index=reader.readUShort(), count=(flags & 0x001F)
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
def _decompileInsertionAction(self, actionReader, font, index, count):
|
|
844
|
+
if index == 0xFFFF or count == 0:
|
|
845
|
+
return []
|
|
846
|
+
reader = actionReader.getSubReader(actionReader.pos + index * 2)
|
|
847
|
+
return font.getGlyphNameMany(reader.readUShortArray(count))
|
|
848
|
+
|
|
849
|
+
def toXML(self, xmlWriter, font, attrs, name):
|
|
850
|
+
xmlWriter.begintag(name, **attrs)
|
|
851
|
+
xmlWriter.newline()
|
|
852
|
+
xmlWriter.simpletag("NewState", value=self.NewState)
|
|
853
|
+
xmlWriter.newline()
|
|
854
|
+
self._writeFlagsToXML(xmlWriter)
|
|
855
|
+
for g in self.CurrentInsertionAction:
|
|
856
|
+
xmlWriter.simpletag("CurrentInsertionAction", glyph=g)
|
|
857
|
+
xmlWriter.newline()
|
|
858
|
+
for g in self.MarkedInsertionAction:
|
|
859
|
+
xmlWriter.simpletag("MarkedInsertionAction", glyph=g)
|
|
860
|
+
xmlWriter.newline()
|
|
861
|
+
xmlWriter.endtag(name)
|
|
862
|
+
xmlWriter.newline()
|
|
863
|
+
|
|
864
|
+
def fromXML(self, name, attrs, content, font):
|
|
865
|
+
self.__init__()
|
|
866
|
+
content = [t for t in content if isinstance(t, tuple)]
|
|
867
|
+
for eltName, eltAttrs, eltContent in content:
|
|
868
|
+
if eltName == "NewState":
|
|
869
|
+
self.NewState = safeEval(eltAttrs["value"])
|
|
870
|
+
elif eltName == "Flags":
|
|
871
|
+
for flag in eltAttrs["value"].split(","):
|
|
872
|
+
self._setFlag(flag.strip())
|
|
873
|
+
elif eltName == "CurrentInsertionAction":
|
|
874
|
+
self.CurrentInsertionAction.append(eltAttrs["glyph"])
|
|
875
|
+
elif eltName == "MarkedInsertionAction":
|
|
876
|
+
self.MarkedInsertionAction.append(eltAttrs["glyph"])
|
|
877
|
+
else:
|
|
878
|
+
assert False, eltName
|
|
879
|
+
|
|
880
|
+
@staticmethod
|
|
881
|
+
def compileActions(font, states):
|
|
882
|
+
actions, actionIndex, result = set(), {}, b""
|
|
883
|
+
for state in states:
|
|
884
|
+
for _glyphClass, trans in state.Transitions.items():
|
|
885
|
+
if trans.CurrentInsertionAction is not None:
|
|
886
|
+
actions.add(tuple(trans.CurrentInsertionAction))
|
|
887
|
+
if trans.MarkedInsertionAction is not None:
|
|
888
|
+
actions.add(tuple(trans.MarkedInsertionAction))
|
|
889
|
+
# Sort the compiled actions in decreasing order of
|
|
890
|
+
# length, so that the longer sequence come before the
|
|
891
|
+
# shorter ones.
|
|
892
|
+
for action in sorted(actions, key=lambda x: (-len(x), x)):
|
|
893
|
+
# We insert all sub-sequences of the action glyph sequence
|
|
894
|
+
# into actionIndex. For example, if one action triggers on
|
|
895
|
+
# glyph sequence [A, B, C, D, E] and another action triggers
|
|
896
|
+
# on [C, D], we return result=[A, B, C, D, E] (as list of
|
|
897
|
+
# encoded glyph IDs), and actionIndex={('A','B','C','D','E'): 0,
|
|
898
|
+
# ('C','D'): 2}.
|
|
899
|
+
if action in actionIndex:
|
|
900
|
+
continue
|
|
901
|
+
for start in range(0, len(action)):
|
|
902
|
+
startIndex = (len(result) // 2) + start
|
|
903
|
+
for limit in range(start, len(action)):
|
|
904
|
+
glyphs = action[start : limit + 1]
|
|
905
|
+
actionIndex.setdefault(glyphs, startIndex)
|
|
906
|
+
for glyph in action:
|
|
907
|
+
glyphID = font.getGlyphID(glyph)
|
|
908
|
+
result += struct.pack(">H", glyphID)
|
|
909
|
+
return result, actionIndex
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
class FeatureParams(BaseTable):
|
|
913
|
+
def compile(self, writer, font):
|
|
914
|
+
assert (
|
|
915
|
+
featureParamTypes.get(writer["FeatureTag"]) == self.__class__
|
|
916
|
+
), "Wrong FeatureParams type for feature '%s': %s" % (
|
|
917
|
+
writer["FeatureTag"],
|
|
918
|
+
self.__class__.__name__,
|
|
919
|
+
)
|
|
920
|
+
BaseTable.compile(self, writer, font)
|
|
921
|
+
|
|
922
|
+
def toXML(self, xmlWriter, font, attrs=None, name=None):
|
|
923
|
+
BaseTable.toXML(self, xmlWriter, font, attrs, name=self.__class__.__name__)
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
class FeatureParamsSize(FeatureParams):
|
|
927
|
+
pass
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
class FeatureParamsStylisticSet(FeatureParams):
|
|
931
|
+
pass
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
class FeatureParamsCharacterVariants(FeatureParams):
|
|
935
|
+
pass
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
class Coverage(FormatSwitchingBaseTable):
|
|
939
|
+
# manual implementation to get rid of glyphID dependencies
|
|
940
|
+
|
|
941
|
+
def populateDefaults(self, propagator=None):
|
|
942
|
+
if not hasattr(self, "glyphs"):
|
|
943
|
+
self.glyphs = []
|
|
944
|
+
|
|
945
|
+
def postRead(self, rawTable, font):
|
|
946
|
+
if self.Format == 1:
|
|
947
|
+
self.glyphs = rawTable["GlyphArray"]
|
|
948
|
+
elif self.Format == 2:
|
|
949
|
+
glyphs = self.glyphs = []
|
|
950
|
+
ranges = rawTable["RangeRecord"]
|
|
951
|
+
# Some SIL fonts have coverage entries that don't have sorted
|
|
952
|
+
# StartCoverageIndex. If it is so, fixup and warn. We undo
|
|
953
|
+
# this when writing font out.
|
|
954
|
+
sorted_ranges = sorted(ranges, key=lambda a: a.StartCoverageIndex)
|
|
955
|
+
if ranges != sorted_ranges:
|
|
956
|
+
log.warning("GSUB/GPOS Coverage is not sorted by glyph ids.")
|
|
957
|
+
ranges = sorted_ranges
|
|
958
|
+
del sorted_ranges
|
|
959
|
+
for r in ranges:
|
|
960
|
+
start = r.Start
|
|
961
|
+
end = r.End
|
|
962
|
+
startID = font.getGlyphID(start)
|
|
963
|
+
endID = font.getGlyphID(end) + 1
|
|
964
|
+
glyphs.extend(font.getGlyphNameMany(range(startID, endID)))
|
|
965
|
+
else:
|
|
966
|
+
self.glyphs = []
|
|
967
|
+
log.warning("Unknown Coverage format: %s", self.Format)
|
|
968
|
+
del self.Format # Don't need this anymore
|
|
969
|
+
|
|
970
|
+
def preWrite(self, font):
|
|
971
|
+
glyphs = getattr(self, "glyphs", None)
|
|
972
|
+
if glyphs is None:
|
|
973
|
+
glyphs = self.glyphs = []
|
|
974
|
+
format = 1
|
|
975
|
+
rawTable = {"GlyphArray": glyphs}
|
|
976
|
+
if glyphs:
|
|
977
|
+
# find out whether Format 2 is more compact or not
|
|
978
|
+
glyphIDs = font.getGlyphIDMany(glyphs)
|
|
979
|
+
brokenOrder = sorted(glyphIDs) != glyphIDs
|
|
980
|
+
|
|
981
|
+
last = glyphIDs[0]
|
|
982
|
+
ranges = [[last]]
|
|
983
|
+
for glyphID in glyphIDs[1:]:
|
|
984
|
+
if glyphID != last + 1:
|
|
985
|
+
ranges[-1].append(last)
|
|
986
|
+
ranges.append([glyphID])
|
|
987
|
+
last = glyphID
|
|
988
|
+
ranges[-1].append(last)
|
|
989
|
+
|
|
990
|
+
if brokenOrder or len(ranges) * 3 < len(glyphs): # 3 words vs. 1 word
|
|
991
|
+
# Format 2 is more compact
|
|
992
|
+
index = 0
|
|
993
|
+
for i, (start, end) in enumerate(ranges):
|
|
994
|
+
r = RangeRecord()
|
|
995
|
+
r.StartID = start
|
|
996
|
+
r.Start = font.getGlyphName(start)
|
|
997
|
+
r.End = font.getGlyphName(end)
|
|
998
|
+
r.StartCoverageIndex = index
|
|
999
|
+
ranges[i] = r
|
|
1000
|
+
index = index + end - start + 1
|
|
1001
|
+
if brokenOrder:
|
|
1002
|
+
log.warning("GSUB/GPOS Coverage is not sorted by glyph ids.")
|
|
1003
|
+
ranges.sort(key=lambda a: a.StartID)
|
|
1004
|
+
for r in ranges:
|
|
1005
|
+
del r.StartID
|
|
1006
|
+
format = 2
|
|
1007
|
+
rawTable = {"RangeRecord": ranges}
|
|
1008
|
+
# else:
|
|
1009
|
+
# fallthrough; Format 1 is more compact
|
|
1010
|
+
self.Format = format
|
|
1011
|
+
return rawTable
|
|
1012
|
+
|
|
1013
|
+
def toXML2(self, xmlWriter, font):
|
|
1014
|
+
for glyphName in getattr(self, "glyphs", []):
|
|
1015
|
+
xmlWriter.simpletag("Glyph", value=glyphName)
|
|
1016
|
+
xmlWriter.newline()
|
|
1017
|
+
|
|
1018
|
+
def fromXML(self, name, attrs, content, font):
|
|
1019
|
+
glyphs = getattr(self, "glyphs", None)
|
|
1020
|
+
if glyphs is None:
|
|
1021
|
+
glyphs = []
|
|
1022
|
+
self.glyphs = glyphs
|
|
1023
|
+
glyphs.append(attrs["value"])
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
# The special 0xFFFFFFFF delta-set index is used to indicate that there
|
|
1027
|
+
# is no variation data in the ItemVariationStore for a given variable field
|
|
1028
|
+
NO_VARIATION_INDEX = 0xFFFFFFFF
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")):
|
|
1032
|
+
def populateDefaults(self, propagator=None):
|
|
1033
|
+
if not hasattr(self, "mapping"):
|
|
1034
|
+
self.mapping = []
|
|
1035
|
+
|
|
1036
|
+
def postRead(self, rawTable, font):
|
|
1037
|
+
assert (rawTable["EntryFormat"] & 0xFFC0) == 0
|
|
1038
|
+
self.mapping = rawTable["mapping"]
|
|
1039
|
+
|
|
1040
|
+
@staticmethod
|
|
1041
|
+
def getEntryFormat(mapping):
|
|
1042
|
+
ored = 0
|
|
1043
|
+
for idx in mapping:
|
|
1044
|
+
ored |= idx
|
|
1045
|
+
|
|
1046
|
+
inner = ored & 0xFFFF
|
|
1047
|
+
innerBits = 0
|
|
1048
|
+
while inner:
|
|
1049
|
+
innerBits += 1
|
|
1050
|
+
inner >>= 1
|
|
1051
|
+
innerBits = max(innerBits, 1)
|
|
1052
|
+
assert innerBits <= 16
|
|
1053
|
+
|
|
1054
|
+
ored = (ored >> (16 - innerBits)) | (ored & ((1 << innerBits) - 1))
|
|
1055
|
+
if ored <= 0x000000FF:
|
|
1056
|
+
entrySize = 1
|
|
1057
|
+
elif ored <= 0x0000FFFF:
|
|
1058
|
+
entrySize = 2
|
|
1059
|
+
elif ored <= 0x00FFFFFF:
|
|
1060
|
+
entrySize = 3
|
|
1061
|
+
else:
|
|
1062
|
+
entrySize = 4
|
|
1063
|
+
|
|
1064
|
+
return ((entrySize - 1) << 4) | (innerBits - 1)
|
|
1065
|
+
|
|
1066
|
+
def preWrite(self, font):
|
|
1067
|
+
mapping = getattr(self, "mapping", None)
|
|
1068
|
+
if mapping is None:
|
|
1069
|
+
mapping = self.mapping = []
|
|
1070
|
+
self.Format = 1 if len(mapping) > 0xFFFF else 0
|
|
1071
|
+
rawTable = self.__dict__.copy()
|
|
1072
|
+
rawTable["MappingCount"] = len(mapping)
|
|
1073
|
+
rawTable["EntryFormat"] = self.getEntryFormat(mapping)
|
|
1074
|
+
return rawTable
|
|
1075
|
+
|
|
1076
|
+
def toXML2(self, xmlWriter, font):
|
|
1077
|
+
# Make xml dump less verbose, by omitting no-op entries like:
|
|
1078
|
+
# <Map index="..." outer="65535" inner="65535"/>
|
|
1079
|
+
xmlWriter.comment("Omitted values default to 0xFFFF/0xFFFF (no variations)")
|
|
1080
|
+
xmlWriter.newline()
|
|
1081
|
+
for i, value in enumerate(getattr(self, "mapping", [])):
|
|
1082
|
+
attrs = [("index", i)]
|
|
1083
|
+
if value != NO_VARIATION_INDEX:
|
|
1084
|
+
attrs.extend(
|
|
1085
|
+
[
|
|
1086
|
+
("outer", value >> 16),
|
|
1087
|
+
("inner", value & 0xFFFF),
|
|
1088
|
+
]
|
|
1089
|
+
)
|
|
1090
|
+
xmlWriter.simpletag("Map", attrs)
|
|
1091
|
+
xmlWriter.newline()
|
|
1092
|
+
|
|
1093
|
+
def fromXML(self, name, attrs, content, font):
|
|
1094
|
+
mapping = getattr(self, "mapping", None)
|
|
1095
|
+
if mapping is None:
|
|
1096
|
+
self.mapping = mapping = []
|
|
1097
|
+
index = safeEval(attrs["index"])
|
|
1098
|
+
outer = safeEval(attrs.get("outer", "0xFFFF"))
|
|
1099
|
+
inner = safeEval(attrs.get("inner", "0xFFFF"))
|
|
1100
|
+
assert inner <= 0xFFFF
|
|
1101
|
+
mapping.insert(index, (outer << 16) | inner)
|
|
1102
|
+
|
|
1103
|
+
def __getitem__(self, i):
|
|
1104
|
+
return self.mapping[i] if i < len(self.mapping) else NO_VARIATION_INDEX
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
class VarIdxMap(BaseTable):
|
|
1108
|
+
def populateDefaults(self, propagator=None):
|
|
1109
|
+
if not hasattr(self, "mapping"):
|
|
1110
|
+
self.mapping = {}
|
|
1111
|
+
|
|
1112
|
+
def postRead(self, rawTable, font):
|
|
1113
|
+
assert (rawTable["EntryFormat"] & 0xFFC0) == 0
|
|
1114
|
+
glyphOrder = font.getGlyphOrder()
|
|
1115
|
+
mapList = rawTable["mapping"]
|
|
1116
|
+
mapList.extend([mapList[-1]] * (len(glyphOrder) - len(mapList)))
|
|
1117
|
+
self.mapping = dict(zip(glyphOrder, mapList))
|
|
1118
|
+
|
|
1119
|
+
def preWrite(self, font):
|
|
1120
|
+
mapping = getattr(self, "mapping", None)
|
|
1121
|
+
if mapping is None:
|
|
1122
|
+
mapping = self.mapping = {}
|
|
1123
|
+
|
|
1124
|
+
glyphOrder = font.getGlyphOrder()
|
|
1125
|
+
mapping = [mapping[g] for g in glyphOrder]
|
|
1126
|
+
while len(mapping) > 1 and mapping[-2] == mapping[-1]:
|
|
1127
|
+
del mapping[-1]
|
|
1128
|
+
|
|
1129
|
+
rawTable = {"mapping": mapping}
|
|
1130
|
+
rawTable["MappingCount"] = len(mapping)
|
|
1131
|
+
rawTable["EntryFormat"] = DeltaSetIndexMap.getEntryFormat(mapping)
|
|
1132
|
+
return rawTable
|
|
1133
|
+
|
|
1134
|
+
def toXML2(self, xmlWriter, font):
|
|
1135
|
+
for glyph, value in sorted(getattr(self, "mapping", {}).items()):
|
|
1136
|
+
attrs = (
|
|
1137
|
+
("glyph", glyph),
|
|
1138
|
+
("outer", value >> 16),
|
|
1139
|
+
("inner", value & 0xFFFF),
|
|
1140
|
+
)
|
|
1141
|
+
xmlWriter.simpletag("Map", attrs)
|
|
1142
|
+
xmlWriter.newline()
|
|
1143
|
+
|
|
1144
|
+
def fromXML(self, name, attrs, content, font):
|
|
1145
|
+
mapping = getattr(self, "mapping", None)
|
|
1146
|
+
if mapping is None:
|
|
1147
|
+
mapping = {}
|
|
1148
|
+
self.mapping = mapping
|
|
1149
|
+
try:
|
|
1150
|
+
glyph = attrs["glyph"]
|
|
1151
|
+
except: # https://github.com/fonttools/fonttools/commit/21cbab8ce9ded3356fef3745122da64dcaf314e9#commitcomment-27649836
|
|
1152
|
+
glyph = font.getGlyphOrder()[attrs["index"]]
|
|
1153
|
+
outer = safeEval(attrs["outer"])
|
|
1154
|
+
inner = safeEval(attrs["inner"])
|
|
1155
|
+
assert inner <= 0xFFFF
|
|
1156
|
+
mapping[glyph] = (outer << 16) | inner
|
|
1157
|
+
|
|
1158
|
+
def __getitem__(self, glyphName):
|
|
1159
|
+
return self.mapping.get(glyphName, NO_VARIATION_INDEX)
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
class VarRegionList(BaseTable):
|
|
1163
|
+
def preWrite(self, font):
|
|
1164
|
+
# The OT spec says VarStore.VarRegionList.RegionAxisCount should always
|
|
1165
|
+
# be equal to the fvar.axisCount, and OTS < v8.0.0 enforces this rule
|
|
1166
|
+
# even when the VarRegionList is empty. We can't treat RegionAxisCount
|
|
1167
|
+
# like a normal propagated count (== len(Region[i].VarRegionAxis)),
|
|
1168
|
+
# otherwise it would default to 0 if VarRegionList is empty.
|
|
1169
|
+
# Thus, we force it to always be equal to fvar.axisCount.
|
|
1170
|
+
# https://github.com/khaledhosny/ots/pull/192
|
|
1171
|
+
fvarTable = font.get("fvar")
|
|
1172
|
+
if fvarTable:
|
|
1173
|
+
self.RegionAxisCount = len(fvarTable.axes)
|
|
1174
|
+
return {
|
|
1175
|
+
**self.__dict__,
|
|
1176
|
+
"RegionAxisCount": CountReference(self.__dict__, "RegionAxisCount"),
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
class SingleSubst(FormatSwitchingBaseTable):
|
|
1181
|
+
def populateDefaults(self, propagator=None):
|
|
1182
|
+
if not hasattr(self, "mapping"):
|
|
1183
|
+
self.mapping = {}
|
|
1184
|
+
|
|
1185
|
+
def postRead(self, rawTable, font):
|
|
1186
|
+
mapping = {}
|
|
1187
|
+
input = _getGlyphsFromCoverageTable(rawTable["Coverage"])
|
|
1188
|
+
if self.Format == 1:
|
|
1189
|
+
delta = rawTable["DeltaGlyphID"]
|
|
1190
|
+
inputGIDS = font.getGlyphIDMany(input)
|
|
1191
|
+
outGIDS = [(glyphID + delta) % 65536 for glyphID in inputGIDS]
|
|
1192
|
+
outNames = font.getGlyphNameMany(outGIDS)
|
|
1193
|
+
for inp, out in zip(input, outNames):
|
|
1194
|
+
mapping[inp] = out
|
|
1195
|
+
elif self.Format == 2:
|
|
1196
|
+
assert (
|
|
1197
|
+
len(input) == rawTable["GlyphCount"]
|
|
1198
|
+
), "invalid SingleSubstFormat2 table"
|
|
1199
|
+
subst = rawTable["Substitute"]
|
|
1200
|
+
for inp, sub in zip(input, subst):
|
|
1201
|
+
mapping[inp] = sub
|
|
1202
|
+
else:
|
|
1203
|
+
assert 0, "unknown format: %s" % self.Format
|
|
1204
|
+
self.mapping = mapping
|
|
1205
|
+
del self.Format # Don't need this anymore
|
|
1206
|
+
|
|
1207
|
+
def preWrite(self, font):
|
|
1208
|
+
mapping = getattr(self, "mapping", None)
|
|
1209
|
+
if mapping is None:
|
|
1210
|
+
mapping = self.mapping = {}
|
|
1211
|
+
items = list(mapping.items())
|
|
1212
|
+
getGlyphID = font.getGlyphID
|
|
1213
|
+
gidItems = [(getGlyphID(a), getGlyphID(b)) for a, b in items]
|
|
1214
|
+
sortableItems = sorted(zip(gidItems, items))
|
|
1215
|
+
|
|
1216
|
+
# figure out format
|
|
1217
|
+
format = 2
|
|
1218
|
+
delta = None
|
|
1219
|
+
for inID, outID in gidItems:
|
|
1220
|
+
if delta is None:
|
|
1221
|
+
delta = (outID - inID) % 65536
|
|
1222
|
+
|
|
1223
|
+
if (inID + delta) % 65536 != outID:
|
|
1224
|
+
break
|
|
1225
|
+
else:
|
|
1226
|
+
if delta is None:
|
|
1227
|
+
# the mapping is empty, better use format 2
|
|
1228
|
+
format = 2
|
|
1229
|
+
else:
|
|
1230
|
+
format = 1
|
|
1231
|
+
|
|
1232
|
+
rawTable = {}
|
|
1233
|
+
self.Format = format
|
|
1234
|
+
cov = Coverage()
|
|
1235
|
+
input = [item[1][0] for item in sortableItems]
|
|
1236
|
+
subst = [item[1][1] for item in sortableItems]
|
|
1237
|
+
cov.glyphs = input
|
|
1238
|
+
rawTable["Coverage"] = cov
|
|
1239
|
+
if format == 1:
|
|
1240
|
+
assert delta is not None
|
|
1241
|
+
rawTable["DeltaGlyphID"] = delta
|
|
1242
|
+
else:
|
|
1243
|
+
rawTable["Substitute"] = subst
|
|
1244
|
+
return rawTable
|
|
1245
|
+
|
|
1246
|
+
def toXML2(self, xmlWriter, font):
|
|
1247
|
+
items = sorted(self.mapping.items())
|
|
1248
|
+
for inGlyph, outGlyph in items:
|
|
1249
|
+
xmlWriter.simpletag("Substitution", [("in", inGlyph), ("out", outGlyph)])
|
|
1250
|
+
xmlWriter.newline()
|
|
1251
|
+
|
|
1252
|
+
def fromXML(self, name, attrs, content, font):
|
|
1253
|
+
mapping = getattr(self, "mapping", None)
|
|
1254
|
+
if mapping is None:
|
|
1255
|
+
mapping = {}
|
|
1256
|
+
self.mapping = mapping
|
|
1257
|
+
mapping[attrs["in"]] = attrs["out"]
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
class MultipleSubst(FormatSwitchingBaseTable):
|
|
1261
|
+
def populateDefaults(self, propagator=None):
|
|
1262
|
+
if not hasattr(self, "mapping"):
|
|
1263
|
+
self.mapping = {}
|
|
1264
|
+
|
|
1265
|
+
def postRead(self, rawTable, font):
|
|
1266
|
+
mapping = {}
|
|
1267
|
+
if self.Format == 1:
|
|
1268
|
+
glyphs = _getGlyphsFromCoverageTable(rawTable["Coverage"])
|
|
1269
|
+
subst = [s.Substitute for s in rawTable["Sequence"]]
|
|
1270
|
+
mapping = dict(zip(glyphs, subst))
|
|
1271
|
+
else:
|
|
1272
|
+
assert 0, "unknown format: %s" % self.Format
|
|
1273
|
+
self.mapping = mapping
|
|
1274
|
+
del self.Format # Don't need this anymore
|
|
1275
|
+
|
|
1276
|
+
def preWrite(self, font):
|
|
1277
|
+
mapping = getattr(self, "mapping", None)
|
|
1278
|
+
if mapping is None:
|
|
1279
|
+
mapping = self.mapping = {}
|
|
1280
|
+
cov = Coverage()
|
|
1281
|
+
cov.glyphs = sorted(list(mapping.keys()), key=font.getGlyphID)
|
|
1282
|
+
self.Format = 1
|
|
1283
|
+
rawTable = {
|
|
1284
|
+
"Coverage": cov,
|
|
1285
|
+
"Sequence": [self.makeSequence_(mapping[glyph]) for glyph in cov.glyphs],
|
|
1286
|
+
}
|
|
1287
|
+
return rawTable
|
|
1288
|
+
|
|
1289
|
+
def toXML2(self, xmlWriter, font):
|
|
1290
|
+
items = sorted(self.mapping.items())
|
|
1291
|
+
for inGlyph, outGlyphs in items:
|
|
1292
|
+
out = ",".join(outGlyphs)
|
|
1293
|
+
xmlWriter.simpletag("Substitution", [("in", inGlyph), ("out", out)])
|
|
1294
|
+
xmlWriter.newline()
|
|
1295
|
+
|
|
1296
|
+
def fromXML(self, name, attrs, content, font):
|
|
1297
|
+
mapping = getattr(self, "mapping", None)
|
|
1298
|
+
if mapping is None:
|
|
1299
|
+
mapping = {}
|
|
1300
|
+
self.mapping = mapping
|
|
1301
|
+
|
|
1302
|
+
# TTX v3.0 and earlier.
|
|
1303
|
+
if name == "Coverage":
|
|
1304
|
+
self.old_coverage_ = []
|
|
1305
|
+
for element in content:
|
|
1306
|
+
if not isinstance(element, tuple):
|
|
1307
|
+
continue
|
|
1308
|
+
element_name, element_attrs, _ = element
|
|
1309
|
+
if element_name == "Glyph":
|
|
1310
|
+
self.old_coverage_.append(element_attrs["value"])
|
|
1311
|
+
return
|
|
1312
|
+
if name == "Sequence":
|
|
1313
|
+
index = int(attrs.get("index", len(mapping)))
|
|
1314
|
+
glyph = self.old_coverage_[index]
|
|
1315
|
+
glyph_mapping = mapping[glyph] = []
|
|
1316
|
+
for element in content:
|
|
1317
|
+
if not isinstance(element, tuple):
|
|
1318
|
+
continue
|
|
1319
|
+
element_name, element_attrs, _ = element
|
|
1320
|
+
if element_name == "Substitute":
|
|
1321
|
+
glyph_mapping.append(element_attrs["value"])
|
|
1322
|
+
return
|
|
1323
|
+
|
|
1324
|
+
# TTX v3.1 and later.
|
|
1325
|
+
outGlyphs = attrs["out"].split(",") if attrs["out"] else []
|
|
1326
|
+
mapping[attrs["in"]] = [g.strip() for g in outGlyphs]
|
|
1327
|
+
|
|
1328
|
+
@staticmethod
|
|
1329
|
+
def makeSequence_(g):
|
|
1330
|
+
seq = Sequence()
|
|
1331
|
+
seq.Substitute = g
|
|
1332
|
+
return seq
|
|
1333
|
+
|
|
1334
|
+
|
|
1335
|
+
class ClassDef(FormatSwitchingBaseTable):
|
|
1336
|
+
def populateDefaults(self, propagator=None):
|
|
1337
|
+
if not hasattr(self, "classDefs"):
|
|
1338
|
+
self.classDefs = {}
|
|
1339
|
+
|
|
1340
|
+
def postRead(self, rawTable, font):
|
|
1341
|
+
classDefs = {}
|
|
1342
|
+
|
|
1343
|
+
if self.Format == 1:
|
|
1344
|
+
start = rawTable["StartGlyph"]
|
|
1345
|
+
classList = rawTable["ClassValueArray"]
|
|
1346
|
+
startID = font.getGlyphID(start)
|
|
1347
|
+
endID = startID + len(classList)
|
|
1348
|
+
glyphNames = font.getGlyphNameMany(range(startID, endID))
|
|
1349
|
+
for glyphName, cls in zip(glyphNames, classList):
|
|
1350
|
+
if cls:
|
|
1351
|
+
classDefs[glyphName] = cls
|
|
1352
|
+
|
|
1353
|
+
elif self.Format == 2:
|
|
1354
|
+
records = rawTable["ClassRangeRecord"]
|
|
1355
|
+
for rec in records:
|
|
1356
|
+
cls = rec.Class
|
|
1357
|
+
if not cls:
|
|
1358
|
+
continue
|
|
1359
|
+
start = rec.Start
|
|
1360
|
+
end = rec.End
|
|
1361
|
+
startID = font.getGlyphID(start)
|
|
1362
|
+
endID = font.getGlyphID(end) + 1
|
|
1363
|
+
glyphNames = font.getGlyphNameMany(range(startID, endID))
|
|
1364
|
+
for glyphName in glyphNames:
|
|
1365
|
+
classDefs[glyphName] = cls
|
|
1366
|
+
else:
|
|
1367
|
+
log.warning("Unknown ClassDef format: %s", self.Format)
|
|
1368
|
+
self.classDefs = classDefs
|
|
1369
|
+
del self.Format # Don't need this anymore
|
|
1370
|
+
|
|
1371
|
+
def _getClassRanges(self, font):
|
|
1372
|
+
classDefs = getattr(self, "classDefs", None)
|
|
1373
|
+
if classDefs is None:
|
|
1374
|
+
self.classDefs = {}
|
|
1375
|
+
return
|
|
1376
|
+
getGlyphID = font.getGlyphID
|
|
1377
|
+
items = []
|
|
1378
|
+
for glyphName, cls in classDefs.items():
|
|
1379
|
+
if not cls:
|
|
1380
|
+
continue
|
|
1381
|
+
items.append((getGlyphID(glyphName), glyphName, cls))
|
|
1382
|
+
if items:
|
|
1383
|
+
items.sort()
|
|
1384
|
+
last, lastName, lastCls = items[0]
|
|
1385
|
+
ranges = [[lastCls, last, lastName]]
|
|
1386
|
+
for glyphID, glyphName, cls in items[1:]:
|
|
1387
|
+
if glyphID != last + 1 or cls != lastCls:
|
|
1388
|
+
ranges[-1].extend([last, lastName])
|
|
1389
|
+
ranges.append([cls, glyphID, glyphName])
|
|
1390
|
+
last = glyphID
|
|
1391
|
+
lastName = glyphName
|
|
1392
|
+
lastCls = cls
|
|
1393
|
+
ranges[-1].extend([last, lastName])
|
|
1394
|
+
return ranges
|
|
1395
|
+
|
|
1396
|
+
def preWrite(self, font):
|
|
1397
|
+
format = 2
|
|
1398
|
+
rawTable = {"ClassRangeRecord": []}
|
|
1399
|
+
ranges = self._getClassRanges(font)
|
|
1400
|
+
if ranges:
|
|
1401
|
+
startGlyph = ranges[0][1]
|
|
1402
|
+
endGlyph = ranges[-1][3]
|
|
1403
|
+
glyphCount = endGlyph - startGlyph + 1
|
|
1404
|
+
if len(ranges) * 3 < glyphCount + 1:
|
|
1405
|
+
# Format 2 is more compact
|
|
1406
|
+
for i, (cls, start, startName, end, endName) in enumerate(ranges):
|
|
1407
|
+
rec = ClassRangeRecord()
|
|
1408
|
+
rec.Start = startName
|
|
1409
|
+
rec.End = endName
|
|
1410
|
+
rec.Class = cls
|
|
1411
|
+
ranges[i] = rec
|
|
1412
|
+
format = 2
|
|
1413
|
+
rawTable = {"ClassRangeRecord": ranges}
|
|
1414
|
+
else:
|
|
1415
|
+
# Format 1 is more compact
|
|
1416
|
+
startGlyphName = ranges[0][2]
|
|
1417
|
+
classes = [0] * glyphCount
|
|
1418
|
+
for cls, start, startName, end, endName in ranges:
|
|
1419
|
+
for g in range(start - startGlyph, end - startGlyph + 1):
|
|
1420
|
+
classes[g] = cls
|
|
1421
|
+
format = 1
|
|
1422
|
+
rawTable = {"StartGlyph": startGlyphName, "ClassValueArray": classes}
|
|
1423
|
+
self.Format = format
|
|
1424
|
+
return rawTable
|
|
1425
|
+
|
|
1426
|
+
def toXML2(self, xmlWriter, font):
|
|
1427
|
+
items = sorted(self.classDefs.items())
|
|
1428
|
+
for glyphName, cls in items:
|
|
1429
|
+
xmlWriter.simpletag("ClassDef", [("glyph", glyphName), ("class", cls)])
|
|
1430
|
+
xmlWriter.newline()
|
|
1431
|
+
|
|
1432
|
+
def fromXML(self, name, attrs, content, font):
|
|
1433
|
+
classDefs = getattr(self, "classDefs", None)
|
|
1434
|
+
if classDefs is None:
|
|
1435
|
+
classDefs = {}
|
|
1436
|
+
self.classDefs = classDefs
|
|
1437
|
+
classDefs[attrs["glyph"]] = int(attrs["class"])
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
class AlternateSubst(FormatSwitchingBaseTable):
|
|
1441
|
+
def populateDefaults(self, propagator=None):
|
|
1442
|
+
if not hasattr(self, "alternates"):
|
|
1443
|
+
self.alternates = {}
|
|
1444
|
+
|
|
1445
|
+
def postRead(self, rawTable, font):
|
|
1446
|
+
alternates = {}
|
|
1447
|
+
if self.Format == 1:
|
|
1448
|
+
input = _getGlyphsFromCoverageTable(rawTable["Coverage"])
|
|
1449
|
+
alts = rawTable["AlternateSet"]
|
|
1450
|
+
assert len(input) == len(alts)
|
|
1451
|
+
for inp, alt in zip(input, alts):
|
|
1452
|
+
alternates[inp] = alt.Alternate
|
|
1453
|
+
else:
|
|
1454
|
+
assert 0, "unknown format: %s" % self.Format
|
|
1455
|
+
self.alternates = alternates
|
|
1456
|
+
del self.Format # Don't need this anymore
|
|
1457
|
+
|
|
1458
|
+
def preWrite(self, font):
|
|
1459
|
+
self.Format = 1
|
|
1460
|
+
alternates = getattr(self, "alternates", None)
|
|
1461
|
+
if alternates is None:
|
|
1462
|
+
alternates = self.alternates = {}
|
|
1463
|
+
items = list(alternates.items())
|
|
1464
|
+
for i, (glyphName, set) in enumerate(items):
|
|
1465
|
+
items[i] = font.getGlyphID(glyphName), glyphName, set
|
|
1466
|
+
items.sort()
|
|
1467
|
+
cov = Coverage()
|
|
1468
|
+
cov.glyphs = [item[1] for item in items]
|
|
1469
|
+
alternates = []
|
|
1470
|
+
setList = [item[-1] for item in items]
|
|
1471
|
+
for set in setList:
|
|
1472
|
+
alts = AlternateSet()
|
|
1473
|
+
alts.Alternate = set
|
|
1474
|
+
alternates.append(alts)
|
|
1475
|
+
# a special case to deal with the fact that several hundred Adobe Japan1-5
|
|
1476
|
+
# CJK fonts will overflow an offset if the coverage table isn't pushed to the end.
|
|
1477
|
+
# Also useful in that when splitting a sub-table because of an offset overflow
|
|
1478
|
+
# I don't need to calculate the change in the subtable offset due to the change in the coverage table size.
|
|
1479
|
+
# Allows packing more rules in subtable.
|
|
1480
|
+
self.sortCoverageLast = 1
|
|
1481
|
+
return {"Coverage": cov, "AlternateSet": alternates}
|
|
1482
|
+
|
|
1483
|
+
def toXML2(self, xmlWriter, font):
|
|
1484
|
+
items = sorted(self.alternates.items())
|
|
1485
|
+
for glyphName, alternates in items:
|
|
1486
|
+
xmlWriter.begintag("AlternateSet", glyph=glyphName)
|
|
1487
|
+
xmlWriter.newline()
|
|
1488
|
+
for alt in alternates:
|
|
1489
|
+
xmlWriter.simpletag("Alternate", glyph=alt)
|
|
1490
|
+
xmlWriter.newline()
|
|
1491
|
+
xmlWriter.endtag("AlternateSet")
|
|
1492
|
+
xmlWriter.newline()
|
|
1493
|
+
|
|
1494
|
+
def fromXML(self, name, attrs, content, font):
|
|
1495
|
+
alternates = getattr(self, "alternates", None)
|
|
1496
|
+
if alternates is None:
|
|
1497
|
+
alternates = {}
|
|
1498
|
+
self.alternates = alternates
|
|
1499
|
+
glyphName = attrs["glyph"]
|
|
1500
|
+
set = []
|
|
1501
|
+
alternates[glyphName] = set
|
|
1502
|
+
for element in content:
|
|
1503
|
+
if not isinstance(element, tuple):
|
|
1504
|
+
continue
|
|
1505
|
+
name, attrs, content = element
|
|
1506
|
+
set.append(attrs["glyph"])
|
|
1507
|
+
|
|
1508
|
+
|
|
1509
|
+
class LigatureSubst(FormatSwitchingBaseTable):
|
|
1510
|
+
def populateDefaults(self, propagator=None):
|
|
1511
|
+
if not hasattr(self, "ligatures"):
|
|
1512
|
+
self.ligatures = {}
|
|
1513
|
+
|
|
1514
|
+
def postRead(self, rawTable, font):
|
|
1515
|
+
ligatures = {}
|
|
1516
|
+
if self.Format == 1:
|
|
1517
|
+
input = _getGlyphsFromCoverageTable(rawTable["Coverage"])
|
|
1518
|
+
ligSets = rawTable["LigatureSet"]
|
|
1519
|
+
assert len(input) == len(ligSets)
|
|
1520
|
+
for i, inp in enumerate(input):
|
|
1521
|
+
ligatures[inp] = ligSets[i].Ligature
|
|
1522
|
+
else:
|
|
1523
|
+
assert 0, "unknown format: %s" % self.Format
|
|
1524
|
+
self.ligatures = ligatures
|
|
1525
|
+
del self.Format # Don't need this anymore
|
|
1526
|
+
|
|
1527
|
+
@staticmethod
|
|
1528
|
+
def _getLigatureSortKey(components):
|
|
1529
|
+
# Computes a key for ordering ligatures in a GSUB Type-4 lookup.
|
|
1530
|
+
|
|
1531
|
+
# When building the OpenType lookup, we need to make sure that
|
|
1532
|
+
# the longest sequence of components is listed first, so we
|
|
1533
|
+
# use the negative length as the key for sorting.
|
|
1534
|
+
# Note, we no longer need to worry about deterministic order because the
|
|
1535
|
+
# ligature mapping `dict` remembers the insertion order, and this in
|
|
1536
|
+
# turn depends on the order in which the ligatures are written in the FEA.
|
|
1537
|
+
# Since python sort algorithm is stable, the ligatures of equal length
|
|
1538
|
+
# will keep the relative order in which they appear in the feature file.
|
|
1539
|
+
# For example, given the following ligatures (all starting with 'f' and
|
|
1540
|
+
# thus belonging to the same LigatureSet):
|
|
1541
|
+
#
|
|
1542
|
+
# feature liga {
|
|
1543
|
+
# sub f i by f_i;
|
|
1544
|
+
# sub f f f by f_f_f;
|
|
1545
|
+
# sub f f by f_f;
|
|
1546
|
+
# sub f f i by f_f_i;
|
|
1547
|
+
# } liga;
|
|
1548
|
+
#
|
|
1549
|
+
# this should sort to: f_f_f, f_f_i, f_i, f_f
|
|
1550
|
+
# This is also what fea-rs does, see:
|
|
1551
|
+
# https://github.com/adobe-type-tools/afdko/issues/1727
|
|
1552
|
+
# https://github.com/fonttools/fonttools/issues/3428
|
|
1553
|
+
# https://github.com/googlefonts/fontc/pull/680
|
|
1554
|
+
return -len(components)
|
|
1555
|
+
|
|
1556
|
+
def preWrite(self, font):
|
|
1557
|
+
self.Format = 1
|
|
1558
|
+
ligatures = getattr(self, "ligatures", None)
|
|
1559
|
+
if ligatures is None:
|
|
1560
|
+
ligatures = self.ligatures = {}
|
|
1561
|
+
|
|
1562
|
+
if ligatures and isinstance(next(iter(ligatures)), tuple):
|
|
1563
|
+
# New high-level API in v3.1 and later. Note that we just support compiling this
|
|
1564
|
+
# for now. We don't load to this API, and don't do XML with it.
|
|
1565
|
+
|
|
1566
|
+
# ligatures is map from components-sequence to lig-glyph
|
|
1567
|
+
newLigatures = dict()
|
|
1568
|
+
for comps in sorted(ligatures.keys(), key=self._getLigatureSortKey):
|
|
1569
|
+
ligature = Ligature()
|
|
1570
|
+
ligature.Component = comps[1:]
|
|
1571
|
+
ligature.CompCount = len(comps)
|
|
1572
|
+
ligature.LigGlyph = ligatures[comps]
|
|
1573
|
+
newLigatures.setdefault(comps[0], []).append(ligature)
|
|
1574
|
+
ligatures = newLigatures
|
|
1575
|
+
|
|
1576
|
+
items = list(ligatures.items())
|
|
1577
|
+
for i, (glyphName, set) in enumerate(items):
|
|
1578
|
+
items[i] = font.getGlyphID(glyphName), glyphName, set
|
|
1579
|
+
items.sort()
|
|
1580
|
+
cov = Coverage()
|
|
1581
|
+
cov.glyphs = [item[1] for item in items]
|
|
1582
|
+
|
|
1583
|
+
ligSets = []
|
|
1584
|
+
setList = [item[-1] for item in items]
|
|
1585
|
+
for set in setList:
|
|
1586
|
+
ligSet = LigatureSet()
|
|
1587
|
+
ligs = ligSet.Ligature = []
|
|
1588
|
+
for lig in set:
|
|
1589
|
+
ligs.append(lig)
|
|
1590
|
+
ligSets.append(ligSet)
|
|
1591
|
+
# Useful in that when splitting a sub-table because of an offset overflow
|
|
1592
|
+
# I don't need to calculate the change in subtabl offset due to the coverage table size.
|
|
1593
|
+
# Allows packing more rules in subtable.
|
|
1594
|
+
self.sortCoverageLast = 1
|
|
1595
|
+
return {"Coverage": cov, "LigatureSet": ligSets}
|
|
1596
|
+
|
|
1597
|
+
def toXML2(self, xmlWriter, font):
|
|
1598
|
+
items = sorted(self.ligatures.items())
|
|
1599
|
+
for glyphName, ligSets in items:
|
|
1600
|
+
xmlWriter.begintag("LigatureSet", glyph=glyphName)
|
|
1601
|
+
xmlWriter.newline()
|
|
1602
|
+
for lig in ligSets:
|
|
1603
|
+
xmlWriter.simpletag(
|
|
1604
|
+
"Ligature", glyph=lig.LigGlyph, components=",".join(lig.Component)
|
|
1605
|
+
)
|
|
1606
|
+
xmlWriter.newline()
|
|
1607
|
+
xmlWriter.endtag("LigatureSet")
|
|
1608
|
+
xmlWriter.newline()
|
|
1609
|
+
|
|
1610
|
+
def fromXML(self, name, attrs, content, font):
|
|
1611
|
+
ligatures = getattr(self, "ligatures", None)
|
|
1612
|
+
if ligatures is None:
|
|
1613
|
+
ligatures = {}
|
|
1614
|
+
self.ligatures = ligatures
|
|
1615
|
+
glyphName = attrs["glyph"]
|
|
1616
|
+
ligs = []
|
|
1617
|
+
ligatures[glyphName] = ligs
|
|
1618
|
+
for element in content:
|
|
1619
|
+
if not isinstance(element, tuple):
|
|
1620
|
+
continue
|
|
1621
|
+
name, attrs, content = element
|
|
1622
|
+
lig = Ligature()
|
|
1623
|
+
lig.LigGlyph = attrs["glyph"]
|
|
1624
|
+
components = attrs["components"]
|
|
1625
|
+
lig.Component = components.split(",") if components else []
|
|
1626
|
+
lig.CompCount = len(lig.Component)
|
|
1627
|
+
ligs.append(lig)
|
|
1628
|
+
|
|
1629
|
+
|
|
1630
|
+
class COLR(BaseTable):
|
|
1631
|
+
def decompile(self, reader, font):
|
|
1632
|
+
# COLRv0 is exceptional in that LayerRecordCount appears *after* the
|
|
1633
|
+
# LayerRecordArray it counts, but the parser logic expects Count fields
|
|
1634
|
+
# to always precede the arrays. Here we work around this by parsing the
|
|
1635
|
+
# LayerRecordCount before the rest of the table, and storing it in
|
|
1636
|
+
# the reader's local state.
|
|
1637
|
+
subReader = reader.getSubReader(offset=0)
|
|
1638
|
+
for conv in self.getConverters():
|
|
1639
|
+
if conv.name != "LayerRecordCount":
|
|
1640
|
+
subReader.advance(conv.staticSize)
|
|
1641
|
+
continue
|
|
1642
|
+
reader[conv.name] = conv.read(subReader, font, tableDict={})
|
|
1643
|
+
break
|
|
1644
|
+
else:
|
|
1645
|
+
raise AssertionError("LayerRecordCount converter not found")
|
|
1646
|
+
return BaseTable.decompile(self, reader, font)
|
|
1647
|
+
|
|
1648
|
+
def preWrite(self, font):
|
|
1649
|
+
# The writer similarly assumes Count values precede the things counted,
|
|
1650
|
+
# thus here we pre-initialize a CountReference; the actual count value
|
|
1651
|
+
# will be set to the lenght of the array by the time this is assembled.
|
|
1652
|
+
self.LayerRecordCount = None
|
|
1653
|
+
return {
|
|
1654
|
+
**self.__dict__,
|
|
1655
|
+
"LayerRecordCount": CountReference(self.__dict__, "LayerRecordCount"),
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
def computeClipBoxes(self, glyphSet: "_TTGlyphSet", quantization: int = 1):
|
|
1659
|
+
if self.Version == 0:
|
|
1660
|
+
return
|
|
1661
|
+
|
|
1662
|
+
clips = {}
|
|
1663
|
+
for rec in self.BaseGlyphList.BaseGlyphPaintRecord:
|
|
1664
|
+
try:
|
|
1665
|
+
clipBox = rec.Paint.computeClipBox(self, glyphSet, quantization)
|
|
1666
|
+
except Exception as e:
|
|
1667
|
+
from fontTools.ttLib import TTLibError
|
|
1668
|
+
|
|
1669
|
+
raise TTLibError(
|
|
1670
|
+
f"Failed to compute COLR ClipBox for {rec.BaseGlyph!r}"
|
|
1671
|
+
) from e
|
|
1672
|
+
|
|
1673
|
+
if clipBox is not None:
|
|
1674
|
+
clips[rec.BaseGlyph] = clipBox
|
|
1675
|
+
|
|
1676
|
+
hasClipList = hasattr(self, "ClipList") and self.ClipList is not None
|
|
1677
|
+
if not clips:
|
|
1678
|
+
if hasClipList:
|
|
1679
|
+
self.ClipList = None
|
|
1680
|
+
else:
|
|
1681
|
+
if not hasClipList:
|
|
1682
|
+
self.ClipList = ClipList()
|
|
1683
|
+
self.ClipList.Format = 1
|
|
1684
|
+
self.ClipList.clips = clips
|
|
1685
|
+
|
|
1686
|
+
|
|
1687
|
+
class LookupList(BaseTable):
|
|
1688
|
+
@property
|
|
1689
|
+
def table(self):
|
|
1690
|
+
for l in self.Lookup:
|
|
1691
|
+
for st in l.SubTable:
|
|
1692
|
+
if type(st).__name__.endswith("Subst"):
|
|
1693
|
+
return "GSUB"
|
|
1694
|
+
if type(st).__name__.endswith("Pos"):
|
|
1695
|
+
return "GPOS"
|
|
1696
|
+
raise ValueError
|
|
1697
|
+
|
|
1698
|
+
def toXML2(self, xmlWriter, font):
|
|
1699
|
+
if (
|
|
1700
|
+
not font
|
|
1701
|
+
or "Debg" not in font
|
|
1702
|
+
or LOOKUP_DEBUG_INFO_KEY not in font["Debg"].data
|
|
1703
|
+
):
|
|
1704
|
+
return super().toXML2(xmlWriter, font)
|
|
1705
|
+
debugData = font["Debg"].data[LOOKUP_DEBUG_INFO_KEY][self.table]
|
|
1706
|
+
for conv in self.getConverters():
|
|
1707
|
+
if conv.repeat:
|
|
1708
|
+
value = getattr(self, conv.name, [])
|
|
1709
|
+
for lookupIndex, item in enumerate(value):
|
|
1710
|
+
if str(lookupIndex) in debugData:
|
|
1711
|
+
info = LookupDebugInfo(*debugData[str(lookupIndex)])
|
|
1712
|
+
tag = info.location
|
|
1713
|
+
if info.name:
|
|
1714
|
+
tag = f"{info.name}: {tag}"
|
|
1715
|
+
if info.feature:
|
|
1716
|
+
script, language, feature = info.feature
|
|
1717
|
+
tag = f"{tag} in {feature} ({script}/{language})"
|
|
1718
|
+
xmlWriter.comment(tag)
|
|
1719
|
+
xmlWriter.newline()
|
|
1720
|
+
|
|
1721
|
+
conv.xmlWrite(
|
|
1722
|
+
xmlWriter, font, item, conv.name, [("index", lookupIndex)]
|
|
1723
|
+
)
|
|
1724
|
+
else:
|
|
1725
|
+
if conv.aux and not eval(conv.aux, None, vars(self)):
|
|
1726
|
+
continue
|
|
1727
|
+
value = getattr(
|
|
1728
|
+
self, conv.name, None
|
|
1729
|
+
) # TODO Handle defaults instead of defaulting to None!
|
|
1730
|
+
conv.xmlWrite(xmlWriter, font, value, conv.name, [])
|
|
1731
|
+
|
|
1732
|
+
|
|
1733
|
+
class BaseGlyphRecordArray(BaseTable):
|
|
1734
|
+
def preWrite(self, font):
|
|
1735
|
+
self.BaseGlyphRecord = sorted(
|
|
1736
|
+
self.BaseGlyphRecord, key=lambda rec: font.getGlyphID(rec.BaseGlyph)
|
|
1737
|
+
)
|
|
1738
|
+
return self.__dict__.copy()
|
|
1739
|
+
|
|
1740
|
+
|
|
1741
|
+
class BaseGlyphList(BaseTable):
|
|
1742
|
+
def preWrite(self, font):
|
|
1743
|
+
self.BaseGlyphPaintRecord = sorted(
|
|
1744
|
+
self.BaseGlyphPaintRecord, key=lambda rec: font.getGlyphID(rec.BaseGlyph)
|
|
1745
|
+
)
|
|
1746
|
+
return self.__dict__.copy()
|
|
1747
|
+
|
|
1748
|
+
|
|
1749
|
+
class ClipBoxFormat(IntEnum):
|
|
1750
|
+
Static = 1
|
|
1751
|
+
Variable = 2
|
|
1752
|
+
|
|
1753
|
+
def is_variable(self):
|
|
1754
|
+
return self is self.Variable
|
|
1755
|
+
|
|
1756
|
+
def as_variable(self):
|
|
1757
|
+
return self.Variable
|
|
1758
|
+
|
|
1759
|
+
|
|
1760
|
+
class ClipBox(getFormatSwitchingBaseTableClass("uint8")):
|
|
1761
|
+
formatEnum = ClipBoxFormat
|
|
1762
|
+
|
|
1763
|
+
def as_tuple(self):
|
|
1764
|
+
return tuple(getattr(self, conv.name) for conv in self.getConverters())
|
|
1765
|
+
|
|
1766
|
+
def __repr__(self):
|
|
1767
|
+
return f"{self.__class__.__name__}{self.as_tuple()}"
|
|
1768
|
+
|
|
1769
|
+
|
|
1770
|
+
class ClipList(getFormatSwitchingBaseTableClass("uint8")):
|
|
1771
|
+
def populateDefaults(self, propagator=None):
|
|
1772
|
+
if not hasattr(self, "clips"):
|
|
1773
|
+
self.clips = {}
|
|
1774
|
+
|
|
1775
|
+
def postRead(self, rawTable, font):
|
|
1776
|
+
clips = {}
|
|
1777
|
+
glyphOrder = font.getGlyphOrder()
|
|
1778
|
+
for i, rec in enumerate(rawTable["ClipRecord"]):
|
|
1779
|
+
if rec.StartGlyphID > rec.EndGlyphID:
|
|
1780
|
+
log.warning(
|
|
1781
|
+
"invalid ClipRecord[%i].StartGlyphID (%i) > "
|
|
1782
|
+
"EndGlyphID (%i); skipped",
|
|
1783
|
+
i,
|
|
1784
|
+
rec.StartGlyphID,
|
|
1785
|
+
rec.EndGlyphID,
|
|
1786
|
+
)
|
|
1787
|
+
continue
|
|
1788
|
+
redefinedGlyphs = []
|
|
1789
|
+
missingGlyphs = []
|
|
1790
|
+
for glyphID in range(rec.StartGlyphID, rec.EndGlyphID + 1):
|
|
1791
|
+
try:
|
|
1792
|
+
glyph = glyphOrder[glyphID]
|
|
1793
|
+
except IndexError:
|
|
1794
|
+
missingGlyphs.append(glyphID)
|
|
1795
|
+
continue
|
|
1796
|
+
if glyph not in clips:
|
|
1797
|
+
clips[glyph] = copy.copy(rec.ClipBox)
|
|
1798
|
+
else:
|
|
1799
|
+
redefinedGlyphs.append(glyphID)
|
|
1800
|
+
if redefinedGlyphs:
|
|
1801
|
+
log.warning(
|
|
1802
|
+
"ClipRecord[%i] overlaps previous records; "
|
|
1803
|
+
"ignoring redefined clip boxes for the "
|
|
1804
|
+
"following glyph ID range: [%i-%i]",
|
|
1805
|
+
i,
|
|
1806
|
+
min(redefinedGlyphs),
|
|
1807
|
+
max(redefinedGlyphs),
|
|
1808
|
+
)
|
|
1809
|
+
if missingGlyphs:
|
|
1810
|
+
log.warning(
|
|
1811
|
+
"ClipRecord[%i] range references missing " "glyph IDs: [%i-%i]",
|
|
1812
|
+
i,
|
|
1813
|
+
min(missingGlyphs),
|
|
1814
|
+
max(missingGlyphs),
|
|
1815
|
+
)
|
|
1816
|
+
self.clips = clips
|
|
1817
|
+
|
|
1818
|
+
def groups(self):
|
|
1819
|
+
glyphsByClip = defaultdict(list)
|
|
1820
|
+
uniqueClips = {}
|
|
1821
|
+
for glyphName, clipBox in self.clips.items():
|
|
1822
|
+
key = clipBox.as_tuple()
|
|
1823
|
+
glyphsByClip[key].append(glyphName)
|
|
1824
|
+
if key not in uniqueClips:
|
|
1825
|
+
uniqueClips[key] = clipBox
|
|
1826
|
+
return {
|
|
1827
|
+
frozenset(glyphs): uniqueClips[key] for key, glyphs in glyphsByClip.items()
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
def preWrite(self, font):
|
|
1831
|
+
if not hasattr(self, "clips"):
|
|
1832
|
+
self.clips = {}
|
|
1833
|
+
clipBoxRanges = {}
|
|
1834
|
+
glyphMap = font.getReverseGlyphMap()
|
|
1835
|
+
for glyphs, clipBox in self.groups().items():
|
|
1836
|
+
glyphIDs = sorted(
|
|
1837
|
+
glyphMap[glyphName] for glyphName in glyphs if glyphName in glyphMap
|
|
1838
|
+
)
|
|
1839
|
+
if not glyphIDs:
|
|
1840
|
+
continue
|
|
1841
|
+
last = glyphIDs[0]
|
|
1842
|
+
ranges = [[last]]
|
|
1843
|
+
for glyphID in glyphIDs[1:]:
|
|
1844
|
+
if glyphID != last + 1:
|
|
1845
|
+
ranges[-1].append(last)
|
|
1846
|
+
ranges.append([glyphID])
|
|
1847
|
+
last = glyphID
|
|
1848
|
+
ranges[-1].append(last)
|
|
1849
|
+
for start, end in ranges:
|
|
1850
|
+
assert (start, end) not in clipBoxRanges
|
|
1851
|
+
clipBoxRanges[(start, end)] = clipBox
|
|
1852
|
+
|
|
1853
|
+
clipRecords = []
|
|
1854
|
+
for (start, end), clipBox in sorted(clipBoxRanges.items()):
|
|
1855
|
+
record = ClipRecord()
|
|
1856
|
+
record.StartGlyphID = start
|
|
1857
|
+
record.EndGlyphID = end
|
|
1858
|
+
record.ClipBox = clipBox
|
|
1859
|
+
clipRecords.append(record)
|
|
1860
|
+
rawTable = {
|
|
1861
|
+
"ClipCount": len(clipRecords),
|
|
1862
|
+
"ClipRecord": clipRecords,
|
|
1863
|
+
}
|
|
1864
|
+
return rawTable
|
|
1865
|
+
|
|
1866
|
+
def toXML(self, xmlWriter, font, attrs=None, name=None):
|
|
1867
|
+
tableName = name if name else self.__class__.__name__
|
|
1868
|
+
if attrs is None:
|
|
1869
|
+
attrs = []
|
|
1870
|
+
if hasattr(self, "Format"):
|
|
1871
|
+
attrs.append(("Format", self.Format))
|
|
1872
|
+
xmlWriter.begintag(tableName, attrs)
|
|
1873
|
+
xmlWriter.newline()
|
|
1874
|
+
# sort clips alphabetically to ensure deterministic XML dump
|
|
1875
|
+
for glyphs, clipBox in sorted(
|
|
1876
|
+
self.groups().items(), key=lambda item: min(item[0])
|
|
1877
|
+
):
|
|
1878
|
+
xmlWriter.begintag("Clip")
|
|
1879
|
+
xmlWriter.newline()
|
|
1880
|
+
for glyphName in sorted(glyphs):
|
|
1881
|
+
xmlWriter.simpletag("Glyph", value=glyphName)
|
|
1882
|
+
xmlWriter.newline()
|
|
1883
|
+
xmlWriter.begintag("ClipBox", [("Format", clipBox.Format)])
|
|
1884
|
+
xmlWriter.newline()
|
|
1885
|
+
clipBox.toXML2(xmlWriter, font)
|
|
1886
|
+
xmlWriter.endtag("ClipBox")
|
|
1887
|
+
xmlWriter.newline()
|
|
1888
|
+
xmlWriter.endtag("Clip")
|
|
1889
|
+
xmlWriter.newline()
|
|
1890
|
+
xmlWriter.endtag(tableName)
|
|
1891
|
+
xmlWriter.newline()
|
|
1892
|
+
|
|
1893
|
+
def fromXML(self, name, attrs, content, font):
|
|
1894
|
+
clips = getattr(self, "clips", None)
|
|
1895
|
+
if clips is None:
|
|
1896
|
+
self.clips = clips = {}
|
|
1897
|
+
assert name == "Clip"
|
|
1898
|
+
glyphs = []
|
|
1899
|
+
clipBox = None
|
|
1900
|
+
for elem in content:
|
|
1901
|
+
if not isinstance(elem, tuple):
|
|
1902
|
+
continue
|
|
1903
|
+
name, attrs, content = elem
|
|
1904
|
+
if name == "Glyph":
|
|
1905
|
+
glyphs.append(attrs["value"])
|
|
1906
|
+
elif name == "ClipBox":
|
|
1907
|
+
clipBox = ClipBox()
|
|
1908
|
+
clipBox.Format = safeEval(attrs["Format"])
|
|
1909
|
+
for elem in content:
|
|
1910
|
+
if not isinstance(elem, tuple):
|
|
1911
|
+
continue
|
|
1912
|
+
name, attrs, content = elem
|
|
1913
|
+
clipBox.fromXML(name, attrs, content, font)
|
|
1914
|
+
if clipBox:
|
|
1915
|
+
for glyphName in glyphs:
|
|
1916
|
+
clips[glyphName] = clipBox
|
|
1917
|
+
|
|
1918
|
+
|
|
1919
|
+
class ExtendMode(IntEnum):
|
|
1920
|
+
PAD = 0
|
|
1921
|
+
REPEAT = 1
|
|
1922
|
+
REFLECT = 2
|
|
1923
|
+
|
|
1924
|
+
|
|
1925
|
+
# Porter-Duff modes for COLRv1 PaintComposite:
|
|
1926
|
+
# https://github.com/googlefonts/colr-gradients-spec/tree/off_sub_1#compositemode-enumeration
|
|
1927
|
+
class CompositeMode(IntEnum):
|
|
1928
|
+
CLEAR = 0
|
|
1929
|
+
SRC = 1
|
|
1930
|
+
DEST = 2
|
|
1931
|
+
SRC_OVER = 3
|
|
1932
|
+
DEST_OVER = 4
|
|
1933
|
+
SRC_IN = 5
|
|
1934
|
+
DEST_IN = 6
|
|
1935
|
+
SRC_OUT = 7
|
|
1936
|
+
DEST_OUT = 8
|
|
1937
|
+
SRC_ATOP = 9
|
|
1938
|
+
DEST_ATOP = 10
|
|
1939
|
+
XOR = 11
|
|
1940
|
+
PLUS = 12
|
|
1941
|
+
SCREEN = 13
|
|
1942
|
+
OVERLAY = 14
|
|
1943
|
+
DARKEN = 15
|
|
1944
|
+
LIGHTEN = 16
|
|
1945
|
+
COLOR_DODGE = 17
|
|
1946
|
+
COLOR_BURN = 18
|
|
1947
|
+
HARD_LIGHT = 19
|
|
1948
|
+
SOFT_LIGHT = 20
|
|
1949
|
+
DIFFERENCE = 21
|
|
1950
|
+
EXCLUSION = 22
|
|
1951
|
+
MULTIPLY = 23
|
|
1952
|
+
HSL_HUE = 24
|
|
1953
|
+
HSL_SATURATION = 25
|
|
1954
|
+
HSL_COLOR = 26
|
|
1955
|
+
HSL_LUMINOSITY = 27
|
|
1956
|
+
|
|
1957
|
+
|
|
1958
|
+
class PaintFormat(IntEnum):
|
|
1959
|
+
PaintColrLayers = 1
|
|
1960
|
+
PaintSolid = 2
|
|
1961
|
+
PaintVarSolid = 3
|
|
1962
|
+
PaintLinearGradient = 4
|
|
1963
|
+
PaintVarLinearGradient = 5
|
|
1964
|
+
PaintRadialGradient = 6
|
|
1965
|
+
PaintVarRadialGradient = 7
|
|
1966
|
+
PaintSweepGradient = 8
|
|
1967
|
+
PaintVarSweepGradient = 9
|
|
1968
|
+
PaintGlyph = 10
|
|
1969
|
+
PaintColrGlyph = 11
|
|
1970
|
+
PaintTransform = 12
|
|
1971
|
+
PaintVarTransform = 13
|
|
1972
|
+
PaintTranslate = 14
|
|
1973
|
+
PaintVarTranslate = 15
|
|
1974
|
+
PaintScale = 16
|
|
1975
|
+
PaintVarScale = 17
|
|
1976
|
+
PaintScaleAroundCenter = 18
|
|
1977
|
+
PaintVarScaleAroundCenter = 19
|
|
1978
|
+
PaintScaleUniform = 20
|
|
1979
|
+
PaintVarScaleUniform = 21
|
|
1980
|
+
PaintScaleUniformAroundCenter = 22
|
|
1981
|
+
PaintVarScaleUniformAroundCenter = 23
|
|
1982
|
+
PaintRotate = 24
|
|
1983
|
+
PaintVarRotate = 25
|
|
1984
|
+
PaintRotateAroundCenter = 26
|
|
1985
|
+
PaintVarRotateAroundCenter = 27
|
|
1986
|
+
PaintSkew = 28
|
|
1987
|
+
PaintVarSkew = 29
|
|
1988
|
+
PaintSkewAroundCenter = 30
|
|
1989
|
+
PaintVarSkewAroundCenter = 31
|
|
1990
|
+
PaintComposite = 32
|
|
1991
|
+
|
|
1992
|
+
def is_variable(self):
|
|
1993
|
+
return self.name.startswith("PaintVar")
|
|
1994
|
+
|
|
1995
|
+
def as_variable(self):
|
|
1996
|
+
if self.is_variable():
|
|
1997
|
+
return self
|
|
1998
|
+
try:
|
|
1999
|
+
return PaintFormat.__members__[f"PaintVar{self.name[5:]}"]
|
|
2000
|
+
except KeyError:
|
|
2001
|
+
return None
|
|
2002
|
+
|
|
2003
|
+
|
|
2004
|
+
class Paint(getFormatSwitchingBaseTableClass("uint8")):
|
|
2005
|
+
formatEnum = PaintFormat
|
|
2006
|
+
|
|
2007
|
+
def getFormatName(self):
|
|
2008
|
+
try:
|
|
2009
|
+
return self.formatEnum(self.Format).name
|
|
2010
|
+
except ValueError:
|
|
2011
|
+
raise NotImplementedError(f"Unknown Paint format: {self.Format}")
|
|
2012
|
+
|
|
2013
|
+
def toXML(self, xmlWriter, font, attrs=None, name=None):
|
|
2014
|
+
tableName = name if name else self.__class__.__name__
|
|
2015
|
+
if attrs is None:
|
|
2016
|
+
attrs = []
|
|
2017
|
+
attrs.append(("Format", self.Format))
|
|
2018
|
+
xmlWriter.begintag(tableName, attrs)
|
|
2019
|
+
xmlWriter.comment(self.getFormatName())
|
|
2020
|
+
xmlWriter.newline()
|
|
2021
|
+
self.toXML2(xmlWriter, font)
|
|
2022
|
+
xmlWriter.endtag(tableName)
|
|
2023
|
+
xmlWriter.newline()
|
|
2024
|
+
|
|
2025
|
+
def iterPaintSubTables(self, colr: COLR) -> Iterator[BaseTable.SubTableEntry]:
|
|
2026
|
+
if self.Format == PaintFormat.PaintColrLayers:
|
|
2027
|
+
# https://github.com/fonttools/fonttools/issues/2438: don't die when no LayerList exists
|
|
2028
|
+
layers = []
|
|
2029
|
+
if colr.LayerList is not None:
|
|
2030
|
+
layers = colr.LayerList.Paint
|
|
2031
|
+
yield from (
|
|
2032
|
+
BaseTable.SubTableEntry(name="Layers", value=v, index=i)
|
|
2033
|
+
for i, v in enumerate(
|
|
2034
|
+
layers[self.FirstLayerIndex : self.FirstLayerIndex + self.NumLayers]
|
|
2035
|
+
)
|
|
2036
|
+
)
|
|
2037
|
+
return
|
|
2038
|
+
|
|
2039
|
+
if self.Format == PaintFormat.PaintColrGlyph:
|
|
2040
|
+
for record in colr.BaseGlyphList.BaseGlyphPaintRecord:
|
|
2041
|
+
if record.BaseGlyph == self.Glyph:
|
|
2042
|
+
yield BaseTable.SubTableEntry(name="BaseGlyph", value=record.Paint)
|
|
2043
|
+
return
|
|
2044
|
+
else:
|
|
2045
|
+
raise KeyError(f"{self.Glyph!r} not in colr.BaseGlyphList")
|
|
2046
|
+
|
|
2047
|
+
for conv in self.getConverters():
|
|
2048
|
+
if conv.tableClass is not None and issubclass(conv.tableClass, type(self)):
|
|
2049
|
+
value = getattr(self, conv.name)
|
|
2050
|
+
yield BaseTable.SubTableEntry(name=conv.name, value=value)
|
|
2051
|
+
|
|
2052
|
+
def getChildren(self, colr) -> List["Paint"]:
|
|
2053
|
+
# this is kept for backward compatibility (e.g. it's used by the subsetter)
|
|
2054
|
+
return [p.value for p in self.iterPaintSubTables(colr)]
|
|
2055
|
+
|
|
2056
|
+
def traverse(self, colr: COLR, callback):
|
|
2057
|
+
"""Depth-first traversal of graph rooted at self, callback on each node."""
|
|
2058
|
+
if not callable(callback):
|
|
2059
|
+
raise TypeError("callback must be callable")
|
|
2060
|
+
|
|
2061
|
+
for path in dfs_base_table(
|
|
2062
|
+
self, iter_subtables_fn=lambda paint: paint.iterPaintSubTables(colr)
|
|
2063
|
+
):
|
|
2064
|
+
paint = path[-1].value
|
|
2065
|
+
callback(paint)
|
|
2066
|
+
|
|
2067
|
+
def getTransform(self) -> Transform:
|
|
2068
|
+
if self.Format == PaintFormat.PaintTransform:
|
|
2069
|
+
t = self.Transform
|
|
2070
|
+
return Transform(t.xx, t.yx, t.xy, t.yy, t.dx, t.dy)
|
|
2071
|
+
elif self.Format == PaintFormat.PaintTranslate:
|
|
2072
|
+
return Identity.translate(self.dx, self.dy)
|
|
2073
|
+
elif self.Format == PaintFormat.PaintScale:
|
|
2074
|
+
return Identity.scale(self.scaleX, self.scaleY)
|
|
2075
|
+
elif self.Format == PaintFormat.PaintScaleAroundCenter:
|
|
2076
|
+
return (
|
|
2077
|
+
Identity.translate(self.centerX, self.centerY)
|
|
2078
|
+
.scale(self.scaleX, self.scaleY)
|
|
2079
|
+
.translate(-self.centerX, -self.centerY)
|
|
2080
|
+
)
|
|
2081
|
+
elif self.Format == PaintFormat.PaintScaleUniform:
|
|
2082
|
+
return Identity.scale(self.scale)
|
|
2083
|
+
elif self.Format == PaintFormat.PaintScaleUniformAroundCenter:
|
|
2084
|
+
return (
|
|
2085
|
+
Identity.translate(self.centerX, self.centerY)
|
|
2086
|
+
.scale(self.scale)
|
|
2087
|
+
.translate(-self.centerX, -self.centerY)
|
|
2088
|
+
)
|
|
2089
|
+
elif self.Format == PaintFormat.PaintRotate:
|
|
2090
|
+
return Identity.rotate(radians(self.angle))
|
|
2091
|
+
elif self.Format == PaintFormat.PaintRotateAroundCenter:
|
|
2092
|
+
return (
|
|
2093
|
+
Identity.translate(self.centerX, self.centerY)
|
|
2094
|
+
.rotate(radians(self.angle))
|
|
2095
|
+
.translate(-self.centerX, -self.centerY)
|
|
2096
|
+
)
|
|
2097
|
+
elif self.Format == PaintFormat.PaintSkew:
|
|
2098
|
+
return Identity.skew(radians(-self.xSkewAngle), radians(self.ySkewAngle))
|
|
2099
|
+
elif self.Format == PaintFormat.PaintSkewAroundCenter:
|
|
2100
|
+
return (
|
|
2101
|
+
Identity.translate(self.centerX, self.centerY)
|
|
2102
|
+
.skew(radians(-self.xSkewAngle), radians(self.ySkewAngle))
|
|
2103
|
+
.translate(-self.centerX, -self.centerY)
|
|
2104
|
+
)
|
|
2105
|
+
if PaintFormat(self.Format).is_variable():
|
|
2106
|
+
raise NotImplementedError(f"Variable Paints not supported: {self.Format}")
|
|
2107
|
+
|
|
2108
|
+
return Identity
|
|
2109
|
+
|
|
2110
|
+
def computeClipBox(
|
|
2111
|
+
self, colr: COLR, glyphSet: "_TTGlyphSet", quantization: int = 1
|
|
2112
|
+
) -> Optional[ClipBox]:
|
|
2113
|
+
pen = ControlBoundsPen(glyphSet)
|
|
2114
|
+
for path in dfs_base_table(
|
|
2115
|
+
self, iter_subtables_fn=lambda paint: paint.iterPaintSubTables(colr)
|
|
2116
|
+
):
|
|
2117
|
+
paint = path[-1].value
|
|
2118
|
+
if paint.Format == PaintFormat.PaintGlyph:
|
|
2119
|
+
transformation = reduce(
|
|
2120
|
+
Transform.transform,
|
|
2121
|
+
(st.value.getTransform() for st in path),
|
|
2122
|
+
Identity,
|
|
2123
|
+
)
|
|
2124
|
+
glyphSet[paint.Glyph].draw(TransformPen(pen, transformation))
|
|
2125
|
+
|
|
2126
|
+
if pen.bounds is None:
|
|
2127
|
+
return None
|
|
2128
|
+
|
|
2129
|
+
cb = ClipBox()
|
|
2130
|
+
cb.Format = int(ClipBoxFormat.Static)
|
|
2131
|
+
cb.xMin, cb.yMin, cb.xMax, cb.yMax = quantizeRect(pen.bounds, quantization)
|
|
2132
|
+
return cb
|
|
2133
|
+
|
|
2134
|
+
|
|
2135
|
+
# For each subtable format there is a class. However, we don't really distinguish
|
|
2136
|
+
# between "field name" and "format name": often these are the same. Yet there's
|
|
2137
|
+
# a whole bunch of fields with different names. The following dict is a mapping
|
|
2138
|
+
# from "format name" to "field name". _buildClasses() uses this to create a
|
|
2139
|
+
# subclass for each alternate field name.
|
|
2140
|
+
#
|
|
2141
|
+
_equivalents = {
|
|
2142
|
+
"MarkArray": ("Mark1Array",),
|
|
2143
|
+
"LangSys": ("DefaultLangSys",),
|
|
2144
|
+
"Coverage": (
|
|
2145
|
+
"MarkCoverage",
|
|
2146
|
+
"BaseCoverage",
|
|
2147
|
+
"LigatureCoverage",
|
|
2148
|
+
"Mark1Coverage",
|
|
2149
|
+
"Mark2Coverage",
|
|
2150
|
+
"BacktrackCoverage",
|
|
2151
|
+
"InputCoverage",
|
|
2152
|
+
"LookAheadCoverage",
|
|
2153
|
+
"VertGlyphCoverage",
|
|
2154
|
+
"HorizGlyphCoverage",
|
|
2155
|
+
"TopAccentCoverage",
|
|
2156
|
+
"ExtendedShapeCoverage",
|
|
2157
|
+
"MathKernCoverage",
|
|
2158
|
+
),
|
|
2159
|
+
"ClassDef": (
|
|
2160
|
+
"ClassDef1",
|
|
2161
|
+
"ClassDef2",
|
|
2162
|
+
"BacktrackClassDef",
|
|
2163
|
+
"InputClassDef",
|
|
2164
|
+
"LookAheadClassDef",
|
|
2165
|
+
"GlyphClassDef",
|
|
2166
|
+
"MarkAttachClassDef",
|
|
2167
|
+
),
|
|
2168
|
+
"Anchor": (
|
|
2169
|
+
"EntryAnchor",
|
|
2170
|
+
"ExitAnchor",
|
|
2171
|
+
"BaseAnchor",
|
|
2172
|
+
"LigatureAnchor",
|
|
2173
|
+
"Mark2Anchor",
|
|
2174
|
+
"MarkAnchor",
|
|
2175
|
+
),
|
|
2176
|
+
"Device": (
|
|
2177
|
+
"XPlaDevice",
|
|
2178
|
+
"YPlaDevice",
|
|
2179
|
+
"XAdvDevice",
|
|
2180
|
+
"YAdvDevice",
|
|
2181
|
+
"XDeviceTable",
|
|
2182
|
+
"YDeviceTable",
|
|
2183
|
+
"DeviceTable",
|
|
2184
|
+
),
|
|
2185
|
+
"Axis": (
|
|
2186
|
+
"HorizAxis",
|
|
2187
|
+
"VertAxis",
|
|
2188
|
+
),
|
|
2189
|
+
"MinMax": ("DefaultMinMax",),
|
|
2190
|
+
"BaseCoord": (
|
|
2191
|
+
"MinCoord",
|
|
2192
|
+
"MaxCoord",
|
|
2193
|
+
),
|
|
2194
|
+
"JstfLangSys": ("DefJstfLangSys",),
|
|
2195
|
+
"JstfGSUBModList": (
|
|
2196
|
+
"ShrinkageEnableGSUB",
|
|
2197
|
+
"ShrinkageDisableGSUB",
|
|
2198
|
+
"ExtensionEnableGSUB",
|
|
2199
|
+
"ExtensionDisableGSUB",
|
|
2200
|
+
),
|
|
2201
|
+
"JstfGPOSModList": (
|
|
2202
|
+
"ShrinkageEnableGPOS",
|
|
2203
|
+
"ShrinkageDisableGPOS",
|
|
2204
|
+
"ExtensionEnableGPOS",
|
|
2205
|
+
"ExtensionDisableGPOS",
|
|
2206
|
+
),
|
|
2207
|
+
"JstfMax": (
|
|
2208
|
+
"ShrinkageJstfMax",
|
|
2209
|
+
"ExtensionJstfMax",
|
|
2210
|
+
),
|
|
2211
|
+
"MathKern": (
|
|
2212
|
+
"TopRightMathKern",
|
|
2213
|
+
"TopLeftMathKern",
|
|
2214
|
+
"BottomRightMathKern",
|
|
2215
|
+
"BottomLeftMathKern",
|
|
2216
|
+
),
|
|
2217
|
+
"MathGlyphConstruction": ("VertGlyphConstruction", "HorizGlyphConstruction"),
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
#
|
|
2221
|
+
# OverFlow logic, to automatically create ExtensionLookups
|
|
2222
|
+
# XXX This should probably move to otBase.py
|
|
2223
|
+
#
|
|
2224
|
+
|
|
2225
|
+
|
|
2226
|
+
def fixLookupOverFlows(ttf, overflowRecord):
|
|
2227
|
+
"""Either the offset from the LookupList to a lookup overflowed, or
|
|
2228
|
+
an offset from a lookup to a subtable overflowed.
|
|
2229
|
+
|
|
2230
|
+
The table layout is::
|
|
2231
|
+
|
|
2232
|
+
GPSO/GUSB
|
|
2233
|
+
Script List
|
|
2234
|
+
Feature List
|
|
2235
|
+
LookUpList
|
|
2236
|
+
Lookup[0] and contents
|
|
2237
|
+
SubTable offset list
|
|
2238
|
+
SubTable[0] and contents
|
|
2239
|
+
...
|
|
2240
|
+
SubTable[n] and contents
|
|
2241
|
+
...
|
|
2242
|
+
Lookup[n] and contents
|
|
2243
|
+
SubTable offset list
|
|
2244
|
+
SubTable[0] and contents
|
|
2245
|
+
...
|
|
2246
|
+
SubTable[n] and contents
|
|
2247
|
+
|
|
2248
|
+
If the offset to a lookup overflowed (SubTableIndex is None)
|
|
2249
|
+
we must promote the *previous* lookup to an Extension type.
|
|
2250
|
+
|
|
2251
|
+
If the offset from a lookup to subtable overflowed, then we must promote it
|
|
2252
|
+
to an Extension Lookup type.
|
|
2253
|
+
"""
|
|
2254
|
+
ok = 0
|
|
2255
|
+
lookupIndex = overflowRecord.LookupListIndex
|
|
2256
|
+
if overflowRecord.SubTableIndex is None:
|
|
2257
|
+
lookupIndex = lookupIndex - 1
|
|
2258
|
+
if lookupIndex < 0:
|
|
2259
|
+
return ok
|
|
2260
|
+
if overflowRecord.tableType == "GSUB":
|
|
2261
|
+
extType = 7
|
|
2262
|
+
elif overflowRecord.tableType == "GPOS":
|
|
2263
|
+
extType = 9
|
|
2264
|
+
|
|
2265
|
+
lookups = ttf[overflowRecord.tableType].table.LookupList.Lookup
|
|
2266
|
+
lookup = lookups[lookupIndex]
|
|
2267
|
+
# If the previous lookup is an extType, look further back. Very unlikely, but possible.
|
|
2268
|
+
while lookup.SubTable[0].__class__.LookupType == extType:
|
|
2269
|
+
lookupIndex = lookupIndex - 1
|
|
2270
|
+
if lookupIndex < 0:
|
|
2271
|
+
return ok
|
|
2272
|
+
lookup = lookups[lookupIndex]
|
|
2273
|
+
|
|
2274
|
+
for lookupIndex in range(lookupIndex, len(lookups)):
|
|
2275
|
+
lookup = lookups[lookupIndex]
|
|
2276
|
+
if lookup.LookupType != extType:
|
|
2277
|
+
lookup.LookupType = extType
|
|
2278
|
+
for si, subTable in enumerate(lookup.SubTable):
|
|
2279
|
+
extSubTableClass = lookupTypes[overflowRecord.tableType][extType]
|
|
2280
|
+
extSubTable = extSubTableClass()
|
|
2281
|
+
extSubTable.Format = 1
|
|
2282
|
+
extSubTable.ExtSubTable = subTable
|
|
2283
|
+
lookup.SubTable[si] = extSubTable
|
|
2284
|
+
ok = 1
|
|
2285
|
+
return ok
|
|
2286
|
+
|
|
2287
|
+
|
|
2288
|
+
def splitMultipleSubst(oldSubTable, newSubTable, overflowRecord):
|
|
2289
|
+
ok = 1
|
|
2290
|
+
oldMapping = sorted(oldSubTable.mapping.items())
|
|
2291
|
+
oldLen = len(oldMapping)
|
|
2292
|
+
|
|
2293
|
+
if overflowRecord.itemName in ["Coverage", "RangeRecord"]:
|
|
2294
|
+
# Coverage table is written last. Overflow is to or within the
|
|
2295
|
+
# the coverage table. We will just cut the subtable in half.
|
|
2296
|
+
newLen = oldLen // 2
|
|
2297
|
+
|
|
2298
|
+
elif overflowRecord.itemName == "Sequence":
|
|
2299
|
+
# We just need to back up by two items from the overflowed
|
|
2300
|
+
# Sequence index to make sure the offset to the Coverage table
|
|
2301
|
+
# doesn't overflow.
|
|
2302
|
+
newLen = overflowRecord.itemIndex - 1
|
|
2303
|
+
|
|
2304
|
+
newSubTable.mapping = {}
|
|
2305
|
+
for i in range(newLen, oldLen):
|
|
2306
|
+
item = oldMapping[i]
|
|
2307
|
+
key = item[0]
|
|
2308
|
+
newSubTable.mapping[key] = item[1]
|
|
2309
|
+
del oldSubTable.mapping[key]
|
|
2310
|
+
|
|
2311
|
+
return ok
|
|
2312
|
+
|
|
2313
|
+
|
|
2314
|
+
def splitAlternateSubst(oldSubTable, newSubTable, overflowRecord):
|
|
2315
|
+
ok = 1
|
|
2316
|
+
if hasattr(oldSubTable, "sortCoverageLast"):
|
|
2317
|
+
newSubTable.sortCoverageLast = oldSubTable.sortCoverageLast
|
|
2318
|
+
|
|
2319
|
+
oldAlts = sorted(oldSubTable.alternates.items())
|
|
2320
|
+
oldLen = len(oldAlts)
|
|
2321
|
+
|
|
2322
|
+
if overflowRecord.itemName in ["Coverage", "RangeRecord"]:
|
|
2323
|
+
# Coverage table is written last. overflow is to or within the
|
|
2324
|
+
# the coverage table. We will just cut the subtable in half.
|
|
2325
|
+
newLen = oldLen // 2
|
|
2326
|
+
|
|
2327
|
+
elif overflowRecord.itemName == "AlternateSet":
|
|
2328
|
+
# We just need to back up by two items
|
|
2329
|
+
# from the overflowed AlternateSet index to make sure the offset
|
|
2330
|
+
# to the Coverage table doesn't overflow.
|
|
2331
|
+
newLen = overflowRecord.itemIndex - 1
|
|
2332
|
+
|
|
2333
|
+
newSubTable.alternates = {}
|
|
2334
|
+
for i in range(newLen, oldLen):
|
|
2335
|
+
item = oldAlts[i]
|
|
2336
|
+
key = item[0]
|
|
2337
|
+
newSubTable.alternates[key] = item[1]
|
|
2338
|
+
del oldSubTable.alternates[key]
|
|
2339
|
+
|
|
2340
|
+
return ok
|
|
2341
|
+
|
|
2342
|
+
|
|
2343
|
+
def splitLigatureSubst(oldSubTable, newSubTable, overflowRecord):
|
|
2344
|
+
ok = 1
|
|
2345
|
+
oldLigs = sorted(oldSubTable.ligatures.items())
|
|
2346
|
+
oldLen = len(oldLigs)
|
|
2347
|
+
|
|
2348
|
+
if overflowRecord.itemName in ["Coverage", "RangeRecord"]:
|
|
2349
|
+
# Coverage table is written last. overflow is to or within the
|
|
2350
|
+
# the coverage table. We will just cut the subtable in half.
|
|
2351
|
+
newLen = oldLen // 2
|
|
2352
|
+
|
|
2353
|
+
elif overflowRecord.itemName == "LigatureSet":
|
|
2354
|
+
# We just need to back up by two items
|
|
2355
|
+
# from the overflowed AlternateSet index to make sure the offset
|
|
2356
|
+
# to the Coverage table doesn't overflow.
|
|
2357
|
+
newLen = overflowRecord.itemIndex - 1
|
|
2358
|
+
|
|
2359
|
+
newSubTable.ligatures = {}
|
|
2360
|
+
for i in range(newLen, oldLen):
|
|
2361
|
+
item = oldLigs[i]
|
|
2362
|
+
key = item[0]
|
|
2363
|
+
newSubTable.ligatures[key] = item[1]
|
|
2364
|
+
del oldSubTable.ligatures[key]
|
|
2365
|
+
|
|
2366
|
+
return ok
|
|
2367
|
+
|
|
2368
|
+
|
|
2369
|
+
def splitPairPos(oldSubTable, newSubTable, overflowRecord):
|
|
2370
|
+
st = oldSubTable
|
|
2371
|
+
ok = False
|
|
2372
|
+
newSubTable.Format = oldSubTable.Format
|
|
2373
|
+
if oldSubTable.Format == 1 and len(oldSubTable.PairSet) > 1:
|
|
2374
|
+
for name in "ValueFormat1", "ValueFormat2":
|
|
2375
|
+
setattr(newSubTable, name, getattr(oldSubTable, name))
|
|
2376
|
+
|
|
2377
|
+
# Move top half of coverage to new subtable
|
|
2378
|
+
|
|
2379
|
+
newSubTable.Coverage = oldSubTable.Coverage.__class__()
|
|
2380
|
+
|
|
2381
|
+
coverage = oldSubTable.Coverage.glyphs
|
|
2382
|
+
records = oldSubTable.PairSet
|
|
2383
|
+
|
|
2384
|
+
oldCount = len(oldSubTable.PairSet) // 2
|
|
2385
|
+
|
|
2386
|
+
oldSubTable.Coverage.glyphs = coverage[:oldCount]
|
|
2387
|
+
oldSubTable.PairSet = records[:oldCount]
|
|
2388
|
+
|
|
2389
|
+
newSubTable.Coverage.glyphs = coverage[oldCount:]
|
|
2390
|
+
newSubTable.PairSet = records[oldCount:]
|
|
2391
|
+
|
|
2392
|
+
oldSubTable.PairSetCount = len(oldSubTable.PairSet)
|
|
2393
|
+
newSubTable.PairSetCount = len(newSubTable.PairSet)
|
|
2394
|
+
|
|
2395
|
+
ok = True
|
|
2396
|
+
|
|
2397
|
+
elif oldSubTable.Format == 2 and len(oldSubTable.Class1Record) > 1:
|
|
2398
|
+
if not hasattr(oldSubTable, "Class2Count"):
|
|
2399
|
+
oldSubTable.Class2Count = len(oldSubTable.Class1Record[0].Class2Record)
|
|
2400
|
+
for name in "Class2Count", "ClassDef2", "ValueFormat1", "ValueFormat2":
|
|
2401
|
+
setattr(newSubTable, name, getattr(oldSubTable, name))
|
|
2402
|
+
|
|
2403
|
+
# The two subtables will still have the same ClassDef2 and the table
|
|
2404
|
+
# sharing will still cause the sharing to overflow. As such, disable
|
|
2405
|
+
# sharing on the one that is serialized second (that's oldSubTable).
|
|
2406
|
+
oldSubTable.DontShare = True
|
|
2407
|
+
|
|
2408
|
+
# Move top half of class numbers to new subtable
|
|
2409
|
+
|
|
2410
|
+
newSubTable.Coverage = oldSubTable.Coverage.__class__()
|
|
2411
|
+
newSubTable.ClassDef1 = oldSubTable.ClassDef1.__class__()
|
|
2412
|
+
|
|
2413
|
+
coverage = oldSubTable.Coverage.glyphs
|
|
2414
|
+
classDefs = oldSubTable.ClassDef1.classDefs
|
|
2415
|
+
records = oldSubTable.Class1Record
|
|
2416
|
+
|
|
2417
|
+
oldCount = len(oldSubTable.Class1Record) // 2
|
|
2418
|
+
newGlyphs = set(k for k, v in classDefs.items() if v >= oldCount)
|
|
2419
|
+
|
|
2420
|
+
oldSubTable.Coverage.glyphs = [g for g in coverage if g not in newGlyphs]
|
|
2421
|
+
oldSubTable.ClassDef1.classDefs = {
|
|
2422
|
+
k: v for k, v in classDefs.items() if v < oldCount
|
|
2423
|
+
}
|
|
2424
|
+
oldSubTable.Class1Record = records[:oldCount]
|
|
2425
|
+
|
|
2426
|
+
newSubTable.Coverage.glyphs = [g for g in coverage if g in newGlyphs]
|
|
2427
|
+
newSubTable.ClassDef1.classDefs = {
|
|
2428
|
+
k: (v - oldCount) for k, v in classDefs.items() if v > oldCount
|
|
2429
|
+
}
|
|
2430
|
+
newSubTable.Class1Record = records[oldCount:]
|
|
2431
|
+
|
|
2432
|
+
oldSubTable.Class1Count = len(oldSubTable.Class1Record)
|
|
2433
|
+
newSubTable.Class1Count = len(newSubTable.Class1Record)
|
|
2434
|
+
|
|
2435
|
+
ok = True
|
|
2436
|
+
|
|
2437
|
+
return ok
|
|
2438
|
+
|
|
2439
|
+
|
|
2440
|
+
def splitMarkBasePos(oldSubTable, newSubTable, overflowRecord):
|
|
2441
|
+
# split half of the mark classes to the new subtable
|
|
2442
|
+
classCount = oldSubTable.ClassCount
|
|
2443
|
+
if classCount < 2:
|
|
2444
|
+
# oh well, not much left to split...
|
|
2445
|
+
return False
|
|
2446
|
+
|
|
2447
|
+
oldClassCount = classCount // 2
|
|
2448
|
+
newClassCount = classCount - oldClassCount
|
|
2449
|
+
|
|
2450
|
+
oldMarkCoverage, oldMarkRecords = [], []
|
|
2451
|
+
newMarkCoverage, newMarkRecords = [], []
|
|
2452
|
+
for glyphName, markRecord in zip(
|
|
2453
|
+
oldSubTable.MarkCoverage.glyphs, oldSubTable.MarkArray.MarkRecord
|
|
2454
|
+
):
|
|
2455
|
+
if markRecord.Class < oldClassCount:
|
|
2456
|
+
oldMarkCoverage.append(glyphName)
|
|
2457
|
+
oldMarkRecords.append(markRecord)
|
|
2458
|
+
else:
|
|
2459
|
+
markRecord.Class -= oldClassCount
|
|
2460
|
+
newMarkCoverage.append(glyphName)
|
|
2461
|
+
newMarkRecords.append(markRecord)
|
|
2462
|
+
|
|
2463
|
+
oldBaseRecords, newBaseRecords = [], []
|
|
2464
|
+
for rec in oldSubTable.BaseArray.BaseRecord:
|
|
2465
|
+
oldBaseRecord, newBaseRecord = rec.__class__(), rec.__class__()
|
|
2466
|
+
oldBaseRecord.BaseAnchor = rec.BaseAnchor[:oldClassCount]
|
|
2467
|
+
newBaseRecord.BaseAnchor = rec.BaseAnchor[oldClassCount:]
|
|
2468
|
+
oldBaseRecords.append(oldBaseRecord)
|
|
2469
|
+
newBaseRecords.append(newBaseRecord)
|
|
2470
|
+
|
|
2471
|
+
newSubTable.Format = oldSubTable.Format
|
|
2472
|
+
|
|
2473
|
+
oldSubTable.MarkCoverage.glyphs = oldMarkCoverage
|
|
2474
|
+
newSubTable.MarkCoverage = oldSubTable.MarkCoverage.__class__()
|
|
2475
|
+
newSubTable.MarkCoverage.glyphs = newMarkCoverage
|
|
2476
|
+
|
|
2477
|
+
# share the same BaseCoverage in both halves
|
|
2478
|
+
newSubTable.BaseCoverage = oldSubTable.BaseCoverage
|
|
2479
|
+
|
|
2480
|
+
oldSubTable.ClassCount = oldClassCount
|
|
2481
|
+
newSubTable.ClassCount = newClassCount
|
|
2482
|
+
|
|
2483
|
+
oldSubTable.MarkArray.MarkRecord = oldMarkRecords
|
|
2484
|
+
newSubTable.MarkArray = oldSubTable.MarkArray.__class__()
|
|
2485
|
+
newSubTable.MarkArray.MarkRecord = newMarkRecords
|
|
2486
|
+
|
|
2487
|
+
oldSubTable.MarkArray.MarkCount = len(oldMarkRecords)
|
|
2488
|
+
newSubTable.MarkArray.MarkCount = len(newMarkRecords)
|
|
2489
|
+
|
|
2490
|
+
oldSubTable.BaseArray.BaseRecord = oldBaseRecords
|
|
2491
|
+
newSubTable.BaseArray = oldSubTable.BaseArray.__class__()
|
|
2492
|
+
newSubTable.BaseArray.BaseRecord = newBaseRecords
|
|
2493
|
+
|
|
2494
|
+
oldSubTable.BaseArray.BaseCount = len(oldBaseRecords)
|
|
2495
|
+
newSubTable.BaseArray.BaseCount = len(newBaseRecords)
|
|
2496
|
+
|
|
2497
|
+
return True
|
|
2498
|
+
|
|
2499
|
+
|
|
2500
|
+
splitTable = {
|
|
2501
|
+
"GSUB": {
|
|
2502
|
+
# 1: splitSingleSubst,
|
|
2503
|
+
2: splitMultipleSubst,
|
|
2504
|
+
3: splitAlternateSubst,
|
|
2505
|
+
4: splitLigatureSubst,
|
|
2506
|
+
# 5: splitContextSubst,
|
|
2507
|
+
# 6: splitChainContextSubst,
|
|
2508
|
+
# 7: splitExtensionSubst,
|
|
2509
|
+
# 8: splitReverseChainSingleSubst,
|
|
2510
|
+
},
|
|
2511
|
+
"GPOS": {
|
|
2512
|
+
# 1: splitSinglePos,
|
|
2513
|
+
2: splitPairPos,
|
|
2514
|
+
# 3: splitCursivePos,
|
|
2515
|
+
4: splitMarkBasePos,
|
|
2516
|
+
# 5: splitMarkLigPos,
|
|
2517
|
+
# 6: splitMarkMarkPos,
|
|
2518
|
+
# 7: splitContextPos,
|
|
2519
|
+
# 8: splitChainContextPos,
|
|
2520
|
+
# 9: splitExtensionPos,
|
|
2521
|
+
},
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
|
|
2525
|
+
def fixSubTableOverFlows(ttf, overflowRecord):
|
|
2526
|
+
"""
|
|
2527
|
+
An offset has overflowed within a sub-table. We need to divide this subtable into smaller parts.
|
|
2528
|
+
"""
|
|
2529
|
+
table = ttf[overflowRecord.tableType].table
|
|
2530
|
+
lookup = table.LookupList.Lookup[overflowRecord.LookupListIndex]
|
|
2531
|
+
subIndex = overflowRecord.SubTableIndex
|
|
2532
|
+
subtable = lookup.SubTable[subIndex]
|
|
2533
|
+
|
|
2534
|
+
# First, try not sharing anything for this subtable...
|
|
2535
|
+
if not hasattr(subtable, "DontShare"):
|
|
2536
|
+
subtable.DontShare = True
|
|
2537
|
+
return True
|
|
2538
|
+
|
|
2539
|
+
if hasattr(subtable, "ExtSubTable"):
|
|
2540
|
+
# We split the subtable of the Extension table, and add a new Extension table
|
|
2541
|
+
# to contain the new subtable.
|
|
2542
|
+
|
|
2543
|
+
subTableType = subtable.ExtSubTable.__class__.LookupType
|
|
2544
|
+
extSubTable = subtable
|
|
2545
|
+
subtable = extSubTable.ExtSubTable
|
|
2546
|
+
newExtSubTableClass = lookupTypes[overflowRecord.tableType][
|
|
2547
|
+
extSubTable.__class__.LookupType
|
|
2548
|
+
]
|
|
2549
|
+
newExtSubTable = newExtSubTableClass()
|
|
2550
|
+
newExtSubTable.Format = extSubTable.Format
|
|
2551
|
+
toInsert = newExtSubTable
|
|
2552
|
+
|
|
2553
|
+
newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType]
|
|
2554
|
+
newSubTable = newSubTableClass()
|
|
2555
|
+
newExtSubTable.ExtSubTable = newSubTable
|
|
2556
|
+
else:
|
|
2557
|
+
subTableType = subtable.__class__.LookupType
|
|
2558
|
+
newSubTableClass = lookupTypes[overflowRecord.tableType][subTableType]
|
|
2559
|
+
newSubTable = newSubTableClass()
|
|
2560
|
+
toInsert = newSubTable
|
|
2561
|
+
|
|
2562
|
+
if hasattr(lookup, "SubTableCount"): # may not be defined yet.
|
|
2563
|
+
lookup.SubTableCount = lookup.SubTableCount + 1
|
|
2564
|
+
|
|
2565
|
+
try:
|
|
2566
|
+
splitFunc = splitTable[overflowRecord.tableType][subTableType]
|
|
2567
|
+
except KeyError:
|
|
2568
|
+
log.error(
|
|
2569
|
+
"Don't know how to split %s lookup type %s",
|
|
2570
|
+
overflowRecord.tableType,
|
|
2571
|
+
subTableType,
|
|
2572
|
+
)
|
|
2573
|
+
return False
|
|
2574
|
+
|
|
2575
|
+
ok = splitFunc(subtable, newSubTable, overflowRecord)
|
|
2576
|
+
if ok:
|
|
2577
|
+
lookup.SubTable.insert(subIndex + 1, toInsert)
|
|
2578
|
+
return ok
|
|
2579
|
+
|
|
2580
|
+
|
|
2581
|
+
# End of OverFlow logic
|
|
2582
|
+
|
|
2583
|
+
|
|
2584
|
+
def _buildClasses():
|
|
2585
|
+
import re
|
|
2586
|
+
from .otData import otData
|
|
2587
|
+
|
|
2588
|
+
formatPat = re.compile(r"([A-Za-z0-9]+)Format(\d+)$")
|
|
2589
|
+
namespace = globals()
|
|
2590
|
+
|
|
2591
|
+
# populate module with classes
|
|
2592
|
+
for name, table in otData:
|
|
2593
|
+
baseClass = BaseTable
|
|
2594
|
+
m = formatPat.match(name)
|
|
2595
|
+
if m:
|
|
2596
|
+
# XxxFormatN subtable, we only add the "base" table
|
|
2597
|
+
name = m.group(1)
|
|
2598
|
+
# the first row of a format-switching otData table describes the Format;
|
|
2599
|
+
# the first column defines the type of the Format field.
|
|
2600
|
+
# Currently this can be either 'uint16' or 'uint8'.
|
|
2601
|
+
formatType = table[0][0]
|
|
2602
|
+
baseClass = getFormatSwitchingBaseTableClass(formatType)
|
|
2603
|
+
if name not in namespace:
|
|
2604
|
+
# the class doesn't exist yet, so the base implementation is used.
|
|
2605
|
+
cls = type(name, (baseClass,), {})
|
|
2606
|
+
if name in ("GSUB", "GPOS"):
|
|
2607
|
+
cls.DontShare = True
|
|
2608
|
+
namespace[name] = cls
|
|
2609
|
+
|
|
2610
|
+
# link Var{Table} <-> {Table} (e.g. ColorStop <-> VarColorStop, etc.)
|
|
2611
|
+
for name, _ in otData:
|
|
2612
|
+
if name.startswith("Var") and len(name) > 3 and name[3:] in namespace:
|
|
2613
|
+
varType = namespace[name]
|
|
2614
|
+
noVarType = namespace[name[3:]]
|
|
2615
|
+
varType.NoVarType = noVarType
|
|
2616
|
+
noVarType.VarType = varType
|
|
2617
|
+
|
|
2618
|
+
for base, alts in _equivalents.items():
|
|
2619
|
+
base = namespace[base]
|
|
2620
|
+
for alt in alts:
|
|
2621
|
+
namespace[alt] = base
|
|
2622
|
+
|
|
2623
|
+
global lookupTypes
|
|
2624
|
+
lookupTypes = {
|
|
2625
|
+
"GSUB": {
|
|
2626
|
+
1: SingleSubst,
|
|
2627
|
+
2: MultipleSubst,
|
|
2628
|
+
3: AlternateSubst,
|
|
2629
|
+
4: LigatureSubst,
|
|
2630
|
+
5: ContextSubst,
|
|
2631
|
+
6: ChainContextSubst,
|
|
2632
|
+
7: ExtensionSubst,
|
|
2633
|
+
8: ReverseChainSingleSubst,
|
|
2634
|
+
},
|
|
2635
|
+
"GPOS": {
|
|
2636
|
+
1: SinglePos,
|
|
2637
|
+
2: PairPos,
|
|
2638
|
+
3: CursivePos,
|
|
2639
|
+
4: MarkBasePos,
|
|
2640
|
+
5: MarkLigPos,
|
|
2641
|
+
6: MarkMarkPos,
|
|
2642
|
+
7: ContextPos,
|
|
2643
|
+
8: ChainContextPos,
|
|
2644
|
+
9: ExtensionPos,
|
|
2645
|
+
},
|
|
2646
|
+
"mort": {
|
|
2647
|
+
4: NoncontextualMorph,
|
|
2648
|
+
},
|
|
2649
|
+
"morx": {
|
|
2650
|
+
0: RearrangementMorph,
|
|
2651
|
+
1: ContextualMorph,
|
|
2652
|
+
2: LigatureMorph,
|
|
2653
|
+
# 3: Reserved,
|
|
2654
|
+
4: NoncontextualMorph,
|
|
2655
|
+
5: InsertionMorph,
|
|
2656
|
+
},
|
|
2657
|
+
}
|
|
2658
|
+
lookupTypes["JSTF"] = lookupTypes["GPOS"] # JSTF contains GPOS
|
|
2659
|
+
for lookupEnum in lookupTypes.values():
|
|
2660
|
+
for enum, cls in lookupEnum.items():
|
|
2661
|
+
cls.LookupType = enum
|
|
2662
|
+
|
|
2663
|
+
global featureParamTypes
|
|
2664
|
+
featureParamTypes = {
|
|
2665
|
+
"size": FeatureParamsSize,
|
|
2666
|
+
}
|
|
2667
|
+
for i in range(1, 20 + 1):
|
|
2668
|
+
featureParamTypes["ss%02d" % i] = FeatureParamsStylisticSet
|
|
2669
|
+
for i in range(1, 99 + 1):
|
|
2670
|
+
featureParamTypes["cv%02d" % i] = FeatureParamsCharacterVariants
|
|
2671
|
+
|
|
2672
|
+
# add converters to classes
|
|
2673
|
+
from .otConverters import buildConverters
|
|
2674
|
+
|
|
2675
|
+
for name, table in otData:
|
|
2676
|
+
m = formatPat.match(name)
|
|
2677
|
+
if m:
|
|
2678
|
+
# XxxFormatN subtable, add converter to "base" table
|
|
2679
|
+
name, format = m.groups()
|
|
2680
|
+
format = int(format)
|
|
2681
|
+
cls = namespace[name]
|
|
2682
|
+
if not hasattr(cls, "converters"):
|
|
2683
|
+
cls.converters = {}
|
|
2684
|
+
cls.convertersByName = {}
|
|
2685
|
+
converters, convertersByName = buildConverters(table[1:], namespace)
|
|
2686
|
+
cls.converters[format] = converters
|
|
2687
|
+
cls.convertersByName[format] = convertersByName
|
|
2688
|
+
# XXX Add staticSize?
|
|
2689
|
+
else:
|
|
2690
|
+
cls = namespace[name]
|
|
2691
|
+
cls.converters, cls.convertersByName = buildConverters(table, namespace)
|
|
2692
|
+
# XXX Add staticSize?
|
|
2693
|
+
|
|
2694
|
+
|
|
2695
|
+
_buildClasses()
|
|
2696
|
+
|
|
2697
|
+
|
|
2698
|
+
def _getGlyphsFromCoverageTable(coverage):
|
|
2699
|
+
if coverage is None:
|
|
2700
|
+
# empty coverage table
|
|
2701
|
+
return []
|
|
2702
|
+
else:
|
|
2703
|
+
return coverage.glyphs
|