fonttools 4.56.0__cp39-cp39-win32.whl → 4.58.0__cp39-cp39-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.
Potentially problematic release.
This version of fonttools might be problematic. Click here for more details.
- fontTools/__init__.py +1 -1
- fontTools/cffLib/__init__.py +61 -26
- fontTools/config/__init__.py +15 -0
- fontTools/cu2qu/cu2qu.cp39-win32.pyd +0 -0
- fontTools/designspaceLib/statNames.py +14 -7
- fontTools/feaLib/ast.py +92 -13
- fontTools/feaLib/builder.py +52 -13
- fontTools/feaLib/lexer.cp39-win32.pyd +0 -0
- fontTools/feaLib/parser.py +59 -39
- fontTools/fontBuilder.py +6 -0
- fontTools/misc/bezierTools.cp39-win32.pyd +0 -0
- fontTools/misc/etree.py +4 -27
- fontTools/misc/testTools.py +2 -1
- fontTools/mtiLib/__init__.py +0 -2
- fontTools/otlLib/builder.py +195 -145
- fontTools/otlLib/optimize/gpos.py +49 -63
- fontTools/pens/momentsPen.cp39-win32.pyd +0 -0
- fontTools/pens/pointPen.py +21 -12
- fontTools/qu2cu/qu2cu.cp39-win32.pyd +0 -0
- fontTools/subset/__init__.py +11 -0
- fontTools/ttLib/__init__.py +4 -0
- fontTools/ttLib/__main__.py +47 -8
- fontTools/ttLib/tables/D__e_b_g.py +20 -2
- fontTools/ttLib/tables/G_V_A_R_.py +5 -0
- fontTools/ttLib/tables/T_S_I__0.py +14 -3
- fontTools/ttLib/tables/T_S_I__5.py +16 -5
- fontTools/ttLib/tables/__init__.py +1 -0
- fontTools/ttLib/tables/_c_m_a_p.py +19 -6
- fontTools/ttLib/tables/_c_v_t.py +2 -0
- fontTools/ttLib/tables/_f_p_g_m.py +3 -1
- fontTools/ttLib/tables/_g_l_y_f.py +11 -10
- fontTools/ttLib/tables/_g_v_a_r.py +62 -17
- fontTools/ttLib/tables/_p_o_s_t.py +5 -2
- fontTools/ttLib/tables/otBase.py +1 -0
- fontTools/ttLib/tables/otConverters.py +5 -2
- fontTools/ttLib/tables/otTables.py +5 -1
- fontTools/ttLib/ttFont.py +3 -5
- fontTools/ttLib/ttGlyphSet.py +0 -10
- fontTools/ttx.py +13 -1
- fontTools/ufoLib/__init__.py +2 -2
- fontTools/ufoLib/converters.py +89 -25
- fontTools/ufoLib/errors.py +8 -0
- fontTools/ufoLib/etree.py +1 -1
- fontTools/ufoLib/filenames.py +155 -100
- fontTools/ufoLib/glifLib.py +9 -2
- fontTools/ufoLib/kerning.py +66 -36
- fontTools/ufoLib/utils.py +5 -2
- fontTools/unicodedata/Mirrored.py +446 -0
- fontTools/unicodedata/__init__.py +6 -2
- fontTools/varLib/__init__.py +94 -89
- fontTools/varLib/hvar.py +113 -0
- fontTools/varLib/iup.cp39-win32.pyd +0 -0
- fontTools/varLib/varStore.py +1 -1
- fontTools/voltLib/__main__.py +206 -0
- fontTools/voltLib/ast.py +4 -0
- fontTools/voltLib/parser.py +16 -8
- fontTools/voltLib/voltToFea.py +347 -166
- {fonttools-4.56.0.dist-info → fonttools-4.58.0.dist-info}/METADATA +60 -12
- {fonttools-4.56.0.dist-info → fonttools-4.58.0.dist-info}/RECORD +65 -66
- {fonttools-4.56.0.dist-info → fonttools-4.58.0.dist-info}/WHEEL +1 -1
- fonttools-4.58.0.dist-info/licenses/LICENSE.external +359 -0
- fontTools/cu2qu/cu2qu.c +0 -14829
- fontTools/feaLib/lexer.c +0 -17986
- fontTools/misc/bezierTools.c +0 -41831
- fontTools/pens/momentsPen.c +0 -13448
- fontTools/qu2cu/qu2cu.c +0 -16269
- fontTools/varLib/iup.c +0 -19154
- {fonttools-4.56.0.data → fonttools-4.58.0.data}/data/share/man/man1/ttx.1 +0 -0
- {fonttools-4.56.0.dist-info → fonttools-4.58.0.dist-info}/entry_points.txt +0 -0
- {fonttools-4.56.0.dist-info → fonttools-4.58.0.dist-info/licenses}/LICENSE +0 -0
- {fonttools-4.56.0.dist-info → fonttools-4.58.0.dist-info}/top_level.txt +0 -0
fontTools/varLib/__init__.py
CHANGED
|
@@ -12,13 +12,13 @@ a Glyphs source, eg., using noto-source as an example:
|
|
|
12
12
|
|
|
13
13
|
.. code-block:: sh
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
$ fontmake -o ttf-interpolatable -g NotoSansArabic-MM.glyphs
|
|
16
16
|
|
|
17
17
|
Then you can make a variable-font this way:
|
|
18
18
|
|
|
19
19
|
.. code-block:: sh
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
$ fonttools varLib master_ufo/NotoSansArabic.designspace
|
|
22
22
|
|
|
23
23
|
API *will* change in near future.
|
|
24
24
|
"""
|
|
@@ -479,7 +479,15 @@ def _merge_TTHinting(font, masterModel, master_ttfs):
|
|
|
479
479
|
|
|
480
480
|
_MetricsFields = namedtuple(
|
|
481
481
|
"_MetricsFields",
|
|
482
|
-
[
|
|
482
|
+
[
|
|
483
|
+
"tableTag",
|
|
484
|
+
"metricsTag",
|
|
485
|
+
"sb1",
|
|
486
|
+
"sb2",
|
|
487
|
+
"advMapping",
|
|
488
|
+
"vOrigMapping",
|
|
489
|
+
"phantomIndex",
|
|
490
|
+
],
|
|
483
491
|
)
|
|
484
492
|
|
|
485
493
|
HVAR_FIELDS = _MetricsFields(
|
|
@@ -489,6 +497,7 @@ HVAR_FIELDS = _MetricsFields(
|
|
|
489
497
|
sb2="RsbMap",
|
|
490
498
|
advMapping="AdvWidthMap",
|
|
491
499
|
vOrigMapping=None,
|
|
500
|
+
phantomIndex=0,
|
|
492
501
|
)
|
|
493
502
|
|
|
494
503
|
VVAR_FIELDS = _MetricsFields(
|
|
@@ -498,109 +507,43 @@ VVAR_FIELDS = _MetricsFields(
|
|
|
498
507
|
sb2="BsbMap",
|
|
499
508
|
advMapping="AdvHeightMap",
|
|
500
509
|
vOrigMapping="VOrgMap",
|
|
510
|
+
phantomIndex=1,
|
|
501
511
|
)
|
|
502
512
|
|
|
503
513
|
|
|
504
514
|
def _add_HVAR(font, masterModel, master_ttfs, axisTags):
|
|
505
|
-
|
|
515
|
+
getAdvanceMetrics = partial(
|
|
516
|
+
_get_advance_metrics, font, masterModel, master_ttfs, axisTags, HVAR_FIELDS
|
|
517
|
+
)
|
|
518
|
+
_add_VHVAR(font, axisTags, HVAR_FIELDS, getAdvanceMetrics)
|
|
506
519
|
|
|
507
520
|
|
|
508
521
|
def _add_VVAR(font, masterModel, master_ttfs, axisTags):
|
|
509
|
-
|
|
522
|
+
getAdvanceMetrics = partial(
|
|
523
|
+
_get_advance_metrics, font, masterModel, master_ttfs, axisTags, VVAR_FIELDS
|
|
524
|
+
)
|
|
525
|
+
_add_VHVAR(font, axisTags, VVAR_FIELDS, getAdvanceMetrics)
|
|
510
526
|
|
|
511
527
|
|
|
512
|
-
def _add_VHVAR(font,
|
|
528
|
+
def _add_VHVAR(font, axisTags, tableFields, getAdvanceMetrics):
|
|
513
529
|
tableTag = tableFields.tableTag
|
|
514
530
|
assert tableTag not in font
|
|
531
|
+
glyphOrder = font.getGlyphOrder()
|
|
515
532
|
log.info("Generating " + tableTag)
|
|
516
533
|
VHVAR = newTable(tableTag)
|
|
517
534
|
tableClass = getattr(ot, tableTag)
|
|
518
535
|
vhvar = VHVAR.table = tableClass()
|
|
519
536
|
vhvar.Version = 0x00010000
|
|
520
537
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
# Build list of source font advance widths for each glyph
|
|
524
|
-
metricsTag = tableFields.metricsTag
|
|
525
|
-
advMetricses = [m[metricsTag].metrics for m in master_ttfs]
|
|
526
|
-
|
|
527
|
-
# Build list of source font vertical origin coords for each glyph
|
|
528
|
-
if tableTag == "VVAR" and "VORG" in master_ttfs[0]:
|
|
529
|
-
vOrigMetricses = [m["VORG"].VOriginRecords for m in master_ttfs]
|
|
530
|
-
defaultYOrigs = [m["VORG"].defaultVertOriginY for m in master_ttfs]
|
|
531
|
-
vOrigMetricses = list(zip(vOrigMetricses, defaultYOrigs))
|
|
532
|
-
else:
|
|
533
|
-
vOrigMetricses = None
|
|
534
|
-
|
|
535
|
-
metricsStore, advanceMapping, vOrigMapping = _get_advance_metrics(
|
|
536
|
-
font,
|
|
537
|
-
masterModel,
|
|
538
|
-
master_ttfs,
|
|
539
|
-
axisTags,
|
|
540
|
-
glyphOrder,
|
|
541
|
-
advMetricses,
|
|
542
|
-
vOrigMetricses,
|
|
543
|
-
)
|
|
538
|
+
vhAdvanceDeltasAndSupports, vOrigDeltasAndSupports = getAdvanceMetrics()
|
|
544
539
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
setattr(vhvar, tableFields.advMapping, None)
|
|
540
|
+
if vOrigDeltasAndSupports:
|
|
541
|
+
singleModel = False
|
|
548
542
|
else:
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
setattr(vhvar, tableFields.vOrigMapping, vOrigMapping)
|
|
552
|
-
setattr(vhvar, tableFields.sb1, None)
|
|
553
|
-
setattr(vhvar, tableFields.sb2, None)
|
|
554
|
-
|
|
555
|
-
font[tableTag] = VHVAR
|
|
556
|
-
return
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
def _get_advance_metrics(
|
|
560
|
-
font,
|
|
561
|
-
masterModel,
|
|
562
|
-
master_ttfs,
|
|
563
|
-
axisTags,
|
|
564
|
-
glyphOrder,
|
|
565
|
-
advMetricses,
|
|
566
|
-
vOrigMetricses=None,
|
|
567
|
-
):
|
|
568
|
-
vhAdvanceDeltasAndSupports = {}
|
|
569
|
-
vOrigDeltasAndSupports = {}
|
|
570
|
-
# HACK: we treat width 65535 as a sentinel value to signal that a glyph
|
|
571
|
-
# from a non-default master should not participate in computing {H,V}VAR,
|
|
572
|
-
# as if it were missing. Allows to variate other glyph-related data independently
|
|
573
|
-
# from glyph metrics
|
|
574
|
-
sparse_advance = 0xFFFF
|
|
575
|
-
for glyph in glyphOrder:
|
|
576
|
-
vhAdvances = [
|
|
577
|
-
(
|
|
578
|
-
metrics[glyph][0]
|
|
579
|
-
if glyph in metrics and metrics[glyph][0] != sparse_advance
|
|
580
|
-
else None
|
|
581
|
-
)
|
|
582
|
-
for metrics in advMetricses
|
|
583
|
-
]
|
|
584
|
-
vhAdvanceDeltasAndSupports[glyph] = masterModel.getDeltasAndSupports(
|
|
585
|
-
vhAdvances, round=round
|
|
543
|
+
singleModel = models.allEqual(
|
|
544
|
+
id(v[1]) for v in vhAdvanceDeltasAndSupports.values()
|
|
586
545
|
)
|
|
587
546
|
|
|
588
|
-
singleModel = models.allEqual(id(v[1]) for v in vhAdvanceDeltasAndSupports.values())
|
|
589
|
-
|
|
590
|
-
if vOrigMetricses:
|
|
591
|
-
singleModel = False
|
|
592
|
-
for glyph in glyphOrder:
|
|
593
|
-
# We need to supply a vOrigs tuple with non-None default values
|
|
594
|
-
# for each glyph. vOrigMetricses contains values only for those
|
|
595
|
-
# glyphs which have a non-default vOrig.
|
|
596
|
-
vOrigs = [
|
|
597
|
-
metrics[glyph] if glyph in metrics else defaultVOrig
|
|
598
|
-
for metrics, defaultVOrig in vOrigMetricses
|
|
599
|
-
]
|
|
600
|
-
vOrigDeltasAndSupports[glyph] = masterModel.getDeltasAndSupports(
|
|
601
|
-
vOrigs, round=round
|
|
602
|
-
)
|
|
603
|
-
|
|
604
547
|
directStore = None
|
|
605
548
|
if singleModel:
|
|
606
549
|
# Build direct mapping
|
|
@@ -612,6 +555,8 @@ def _get_advance_metrics(
|
|
|
612
555
|
varData.addItem(vhAdvanceDeltasAndSupports[glyphName][0], round=noRound)
|
|
613
556
|
varData.optimize()
|
|
614
557
|
directStore = builder.buildVarStore(varTupleList, [varData])
|
|
558
|
+
# remove unused regions from VarRegionList
|
|
559
|
+
directStore.prune_regions()
|
|
615
560
|
|
|
616
561
|
# Build optimized indirect mapping
|
|
617
562
|
storeBuilder = varStore.OnlineVarStoreBuilder(axisTags)
|
|
@@ -621,7 +566,7 @@ def _get_advance_metrics(
|
|
|
621
566
|
storeBuilder.setSupports(supports)
|
|
622
567
|
advMapping[glyphName] = storeBuilder.storeDeltas(deltas, round=noRound)
|
|
623
568
|
|
|
624
|
-
if
|
|
569
|
+
if vOrigDeltasAndSupports:
|
|
625
570
|
vOrigMap = {}
|
|
626
571
|
for glyphName in glyphOrder:
|
|
627
572
|
deltas, supports = vOrigDeltasAndSupports[glyphName]
|
|
@@ -633,7 +578,7 @@ def _get_advance_metrics(
|
|
|
633
578
|
advMapping = [mapping2[advMapping[g]] for g in glyphOrder]
|
|
634
579
|
advanceMapping = builder.buildVarIdxMap(advMapping, glyphOrder)
|
|
635
580
|
|
|
636
|
-
if
|
|
581
|
+
if vOrigDeltasAndSupports:
|
|
637
582
|
vOrigMap = [mapping2[vOrigMap[g]] for g in glyphOrder]
|
|
638
583
|
|
|
639
584
|
useDirect = False
|
|
@@ -657,10 +602,70 @@ def _get_advance_metrics(
|
|
|
657
602
|
advanceMapping = None
|
|
658
603
|
else:
|
|
659
604
|
metricsStore = indirectStore
|
|
660
|
-
if
|
|
605
|
+
if vOrigDeltasAndSupports:
|
|
661
606
|
vOrigMapping = builder.buildVarIdxMap(vOrigMap, glyphOrder)
|
|
662
607
|
|
|
663
|
-
|
|
608
|
+
vhvar.VarStore = metricsStore
|
|
609
|
+
setattr(vhvar, tableFields.advMapping, advanceMapping)
|
|
610
|
+
if vOrigMapping is not None:
|
|
611
|
+
setattr(vhvar, tableFields.vOrigMapping, vOrigMapping)
|
|
612
|
+
setattr(vhvar, tableFields.sb1, None)
|
|
613
|
+
setattr(vhvar, tableFields.sb2, None)
|
|
614
|
+
|
|
615
|
+
font[tableTag] = VHVAR
|
|
616
|
+
return
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _get_advance_metrics(font, masterModel, master_ttfs, axisTags, tableFields):
|
|
620
|
+
tableTag = tableFields.tableTag
|
|
621
|
+
glyphOrder = font.getGlyphOrder()
|
|
622
|
+
|
|
623
|
+
# Build list of source font advance widths for each glyph
|
|
624
|
+
metricsTag = tableFields.metricsTag
|
|
625
|
+
advMetricses = [m[metricsTag].metrics for m in master_ttfs]
|
|
626
|
+
|
|
627
|
+
# Build list of source font vertical origin coords for each glyph
|
|
628
|
+
if tableTag == "VVAR" and "VORG" in master_ttfs[0]:
|
|
629
|
+
vOrigMetricses = [m["VORG"].VOriginRecords for m in master_ttfs]
|
|
630
|
+
defaultYOrigs = [m["VORG"].defaultVertOriginY for m in master_ttfs]
|
|
631
|
+
vOrigMetricses = list(zip(vOrigMetricses, defaultYOrigs))
|
|
632
|
+
else:
|
|
633
|
+
vOrigMetricses = None
|
|
634
|
+
|
|
635
|
+
vhAdvanceDeltasAndSupports = {}
|
|
636
|
+
vOrigDeltasAndSupports = {}
|
|
637
|
+
# HACK: we treat width 65535 as a sentinel value to signal that a glyph
|
|
638
|
+
# from a non-default master should not participate in computing {H,V}VAR,
|
|
639
|
+
# as if it were missing. Allows to variate other glyph-related data independently
|
|
640
|
+
# from glyph metrics
|
|
641
|
+
sparse_advance = 0xFFFF
|
|
642
|
+
for glyph in glyphOrder:
|
|
643
|
+
vhAdvances = [
|
|
644
|
+
(
|
|
645
|
+
metrics[glyph][0]
|
|
646
|
+
if glyph in metrics and metrics[glyph][0] != sparse_advance
|
|
647
|
+
else None
|
|
648
|
+
)
|
|
649
|
+
for metrics in advMetricses
|
|
650
|
+
]
|
|
651
|
+
vhAdvanceDeltasAndSupports[glyph] = masterModel.getDeltasAndSupports(
|
|
652
|
+
vhAdvances, round=round
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
if vOrigMetricses:
|
|
656
|
+
for glyph in glyphOrder:
|
|
657
|
+
# We need to supply a vOrigs tuple with non-None default values
|
|
658
|
+
# for each glyph. vOrigMetricses contains values only for those
|
|
659
|
+
# glyphs which have a non-default vOrig.
|
|
660
|
+
vOrigs = [
|
|
661
|
+
metrics[glyph] if glyph in metrics else defaultVOrig
|
|
662
|
+
for metrics, defaultVOrig in vOrigMetricses
|
|
663
|
+
]
|
|
664
|
+
vOrigDeltasAndSupports[glyph] = masterModel.getDeltasAndSupports(
|
|
665
|
+
vOrigs, round=round
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
return vhAdvanceDeltasAndSupports, vOrigDeltasAndSupports
|
|
664
669
|
|
|
665
670
|
|
|
666
671
|
def _add_MVAR(font, masterModel, master_ttfs, axisTags):
|
fontTools/varLib/hvar.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from fontTools.misc.roundTools import noRound
|
|
2
|
+
from fontTools.ttLib import TTFont, newTable
|
|
3
|
+
from fontTools.ttLib.tables import otTables as ot
|
|
4
|
+
from fontTools.ttLib.tables.otBase import OTTableWriter
|
|
5
|
+
from fontTools.varLib import HVAR_FIELDS, VVAR_FIELDS, _add_VHVAR
|
|
6
|
+
from fontTools.varLib import builder, models, varStore
|
|
7
|
+
from fontTools.misc.fixedTools import fixedToFloat as fi2fl
|
|
8
|
+
from fontTools.misc.cliTools import makeOutputFileName
|
|
9
|
+
from functools import partial
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
log = logging.getLogger("fontTools.varLib.avar")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_advance_metrics(font, axisTags, tableFields):
|
|
16
|
+
# There's two ways we can go from here:
|
|
17
|
+
# 1. For each glyph, at each master peak, compute the value of the
|
|
18
|
+
# advance width at that peak. Then pass these all to a VariationModel
|
|
19
|
+
# builder to compute back the deltas.
|
|
20
|
+
# 2. For each master peak, pull out the deltas of the advance width directly,
|
|
21
|
+
# and feed these to the VarStoreBuilder, forgoing the remoding step.
|
|
22
|
+
# We'll go with the second option, as it's simpler, faster, and more direct.
|
|
23
|
+
gvar = font["gvar"]
|
|
24
|
+
vhAdvanceDeltasAndSupports = {}
|
|
25
|
+
glyphOrder = font.getGlyphOrder()
|
|
26
|
+
phantomIndex = tableFields.phantomIndex
|
|
27
|
+
for glyphName in glyphOrder:
|
|
28
|
+
supports = []
|
|
29
|
+
deltas = []
|
|
30
|
+
variations = gvar.variations.get(glyphName, [])
|
|
31
|
+
|
|
32
|
+
for tv in variations:
|
|
33
|
+
supports.append(tv.axes)
|
|
34
|
+
phantoms = tv.coordinates[-4:]
|
|
35
|
+
phantoms = phantoms[phantomIndex * 2 : phantomIndex * 2 + 2]
|
|
36
|
+
assert len(phantoms) == 2
|
|
37
|
+
phantoms[0] = phantoms[0][phantomIndex] if phantoms[0] is not None else 0
|
|
38
|
+
phantoms[1] = phantoms[1][phantomIndex] if phantoms[1] is not None else 0
|
|
39
|
+
deltas.append(phantoms[1] - phantoms[0])
|
|
40
|
+
|
|
41
|
+
vhAdvanceDeltasAndSupports[glyphName] = (deltas, supports)
|
|
42
|
+
|
|
43
|
+
vOrigDeltasAndSupports = None # TODO
|
|
44
|
+
|
|
45
|
+
return vhAdvanceDeltasAndSupports, vOrigDeltasAndSupports
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def add_HVAR(font):
|
|
49
|
+
if "HVAR" in font:
|
|
50
|
+
del font["HVAR"]
|
|
51
|
+
axisTags = [axis.axisTag for axis in font["fvar"].axes]
|
|
52
|
+
getAdvanceMetrics = partial(_get_advance_metrics, font, axisTags, HVAR_FIELDS)
|
|
53
|
+
_add_VHVAR(font, axisTags, HVAR_FIELDS, getAdvanceMetrics)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def add_VVAR(font):
|
|
57
|
+
if "VVAR" in font:
|
|
58
|
+
del font["VVAR"]
|
|
59
|
+
getAdvanceMetrics = partial(_get_advance_metrics, font, axisTags, HVAR_FIELDS)
|
|
60
|
+
axisTags = [axis.axisTag for axis in font["fvar"].axes]
|
|
61
|
+
_add_VHVAR(font, axisTags, VVAR_FIELDS, getAdvanceMetrics)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main(args=None):
|
|
65
|
+
"""Add `HVAR` table to variable font."""
|
|
66
|
+
|
|
67
|
+
if args is None:
|
|
68
|
+
import sys
|
|
69
|
+
|
|
70
|
+
args = sys.argv[1:]
|
|
71
|
+
|
|
72
|
+
from fontTools import configLogger
|
|
73
|
+
from fontTools.designspaceLib import DesignSpaceDocument
|
|
74
|
+
import argparse
|
|
75
|
+
|
|
76
|
+
parser = argparse.ArgumentParser(
|
|
77
|
+
"fonttools varLib.hvar",
|
|
78
|
+
description="Add `HVAR` table from to variable font.",
|
|
79
|
+
)
|
|
80
|
+
parser.add_argument("font", metavar="varfont.ttf", help="Variable-font file.")
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"-o",
|
|
83
|
+
"--output-file",
|
|
84
|
+
type=str,
|
|
85
|
+
help="Output font file name.",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
options = parser.parse_args(args)
|
|
89
|
+
|
|
90
|
+
configLogger(level="WARNING")
|
|
91
|
+
|
|
92
|
+
font = TTFont(options.font)
|
|
93
|
+
if not "fvar" in font:
|
|
94
|
+
log.error("Not a variable font.")
|
|
95
|
+
return 1
|
|
96
|
+
|
|
97
|
+
add_HVAR(font)
|
|
98
|
+
if "vmtx" in font:
|
|
99
|
+
add_VVAR(font)
|
|
100
|
+
|
|
101
|
+
if options.output_file is None:
|
|
102
|
+
outfile = makeOutputFileName(options.font, overWrite=True, suffix=".hvar")
|
|
103
|
+
else:
|
|
104
|
+
outfile = options.output_file
|
|
105
|
+
if outfile:
|
|
106
|
+
log.info("Saving %s", outfile)
|
|
107
|
+
font.save(outfile)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
import sys
|
|
112
|
+
|
|
113
|
+
sys.exit(main())
|
|
Binary file
|
fontTools/varLib/varStore.py
CHANGED
|
@@ -41,7 +41,7 @@ class OnlineVarStoreBuilder(object):
|
|
|
41
41
|
def setSupports(self, supports):
|
|
42
42
|
self._model = None
|
|
43
43
|
self._supports = list(supports)
|
|
44
|
-
if not self._supports[0]:
|
|
44
|
+
if self._supports and not self._supports[0]:
|
|
45
45
|
del self._supports[0] # Drop base master support
|
|
46
46
|
self._cache = None
|
|
47
47
|
self._data = None
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
from io import StringIO
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from fontTools import configLogger
|
|
8
|
+
from fontTools.feaLib.builder import addOpenTypeFeaturesFromString
|
|
9
|
+
from fontTools.feaLib.error import FeatureLibError
|
|
10
|
+
from fontTools.feaLib.lexer import Lexer
|
|
11
|
+
from fontTools.misc.cliTools import makeOutputFileName
|
|
12
|
+
from fontTools.ttLib import TTFont, TTLibError
|
|
13
|
+
from fontTools.voltLib.parser import Parser
|
|
14
|
+
from fontTools.voltLib.voltToFea import TABLES, VoltToFea
|
|
15
|
+
|
|
16
|
+
log = logging.getLogger("fontTools.feaLib")
|
|
17
|
+
|
|
18
|
+
SUPPORTED_TABLES = TABLES + ["cmap"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def invalid_fea_glyph_name(name):
|
|
22
|
+
"""Check if the glyph name is valid according to FEA syntax."""
|
|
23
|
+
if name[0] not in Lexer.CHAR_NAME_START_:
|
|
24
|
+
return True
|
|
25
|
+
if any(c not in Lexer.CHAR_NAME_CONTINUATION_ for c in name[1:]):
|
|
26
|
+
return True
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def sanitize_glyph_name(name):
|
|
31
|
+
"""Sanitize the glyph name to ensure it is valid according to FEA syntax."""
|
|
32
|
+
sanitized = ""
|
|
33
|
+
for i, c in enumerate(name):
|
|
34
|
+
if i == 0 and c not in Lexer.CHAR_NAME_START_:
|
|
35
|
+
sanitized += "a" + c
|
|
36
|
+
elif c not in Lexer.CHAR_NAME_CONTINUATION_:
|
|
37
|
+
sanitized += "_"
|
|
38
|
+
else:
|
|
39
|
+
sanitized += c
|
|
40
|
+
|
|
41
|
+
return sanitized
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main(args=None):
|
|
45
|
+
"""Build tables from a MS VOLT project into an OTF font"""
|
|
46
|
+
parser = argparse.ArgumentParser(
|
|
47
|
+
description="Use fontTools to compile MS VOLT projects."
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"input",
|
|
51
|
+
metavar="INPUT",
|
|
52
|
+
help="Path to the input font/VTP file to process",
|
|
53
|
+
type=Path,
|
|
54
|
+
)
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"-f",
|
|
57
|
+
"--font",
|
|
58
|
+
metavar="INPUT_FONT",
|
|
59
|
+
help="Path to the input font (if INPUT is a VTP file)",
|
|
60
|
+
type=Path,
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"-o",
|
|
64
|
+
"--output",
|
|
65
|
+
dest="output",
|
|
66
|
+
metavar="OUTPUT",
|
|
67
|
+
help="Path to the output font.",
|
|
68
|
+
type=Path,
|
|
69
|
+
)
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
"-t",
|
|
72
|
+
"--tables",
|
|
73
|
+
metavar="TABLE_TAG",
|
|
74
|
+
choices=SUPPORTED_TABLES,
|
|
75
|
+
nargs="+",
|
|
76
|
+
help="Specify the table(s) to be built.",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"-F",
|
|
80
|
+
"--debug-feature-file",
|
|
81
|
+
help="Write the generated feature file to disk.",
|
|
82
|
+
action="store_true",
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"--ship",
|
|
86
|
+
help="Remove source VOLT tables from output font.",
|
|
87
|
+
action="store_true",
|
|
88
|
+
)
|
|
89
|
+
parser.add_argument(
|
|
90
|
+
"-v",
|
|
91
|
+
"--verbose",
|
|
92
|
+
help="Increase the logger verbosity. Multiple -v options are allowed.",
|
|
93
|
+
action="count",
|
|
94
|
+
default=0,
|
|
95
|
+
)
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
"-T",
|
|
98
|
+
"--traceback",
|
|
99
|
+
help="show traceback for exceptions.",
|
|
100
|
+
action="store_true",
|
|
101
|
+
)
|
|
102
|
+
options = parser.parse_args(args)
|
|
103
|
+
|
|
104
|
+
levels = ["WARNING", "INFO", "DEBUG"]
|
|
105
|
+
configLogger(level=levels[min(len(levels) - 1, options.verbose)])
|
|
106
|
+
|
|
107
|
+
output_font = options.output or Path(
|
|
108
|
+
makeOutputFileName(options.font or options.input)
|
|
109
|
+
)
|
|
110
|
+
log.info(f"Compiling MS VOLT to '{output_font}'")
|
|
111
|
+
|
|
112
|
+
file_or_path = options.input
|
|
113
|
+
font = None
|
|
114
|
+
|
|
115
|
+
# If the input is a font file, extract the VOLT data from the "TSIV" table
|
|
116
|
+
try:
|
|
117
|
+
font = TTFont(file_or_path)
|
|
118
|
+
if "TSIV" in font:
|
|
119
|
+
file_or_path = StringIO(font["TSIV"].data.decode("utf-8"))
|
|
120
|
+
else:
|
|
121
|
+
log.error('"TSIV" table is missing')
|
|
122
|
+
return 1
|
|
123
|
+
except TTLibError:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
# If input is not a font file, the font must be provided
|
|
127
|
+
if font is None:
|
|
128
|
+
if not options.font:
|
|
129
|
+
log.error("Please provide an input font")
|
|
130
|
+
return 1
|
|
131
|
+
font = TTFont(options.font)
|
|
132
|
+
|
|
133
|
+
# FEA syntax does not allow some glyph names that VOLT accepts, so if we
|
|
134
|
+
# found such glyph name we will temporarily rename such glyphs.
|
|
135
|
+
glyphOrder = font.getGlyphOrder()
|
|
136
|
+
tempGlyphOrder = None
|
|
137
|
+
if any(invalid_fea_glyph_name(n) for n in glyphOrder):
|
|
138
|
+
tempGlyphOrder = []
|
|
139
|
+
for n in glyphOrder:
|
|
140
|
+
if invalid_fea_glyph_name(n):
|
|
141
|
+
n = sanitize_glyph_name(n)
|
|
142
|
+
existing = set(tempGlyphOrder) | set(glyphOrder)
|
|
143
|
+
while n in existing:
|
|
144
|
+
n = "a" + n
|
|
145
|
+
tempGlyphOrder.append(n)
|
|
146
|
+
font.setGlyphOrder(tempGlyphOrder)
|
|
147
|
+
|
|
148
|
+
doc = Parser(file_or_path).parse()
|
|
149
|
+
|
|
150
|
+
log.info("Converting VTP data to FEA")
|
|
151
|
+
converter = VoltToFea(doc, font)
|
|
152
|
+
try:
|
|
153
|
+
fea = converter.convert(options.tables, ignore_unsupported_settings=True)
|
|
154
|
+
except NotImplementedError as e:
|
|
155
|
+
if options.traceback:
|
|
156
|
+
raise
|
|
157
|
+
location = getattr(e.args[0], "location", None)
|
|
158
|
+
message = f'"{e}" is not supported'
|
|
159
|
+
if location:
|
|
160
|
+
path, line, column = location
|
|
161
|
+
log.error(f"{path}:{line}:{column}: {message}")
|
|
162
|
+
else:
|
|
163
|
+
log.error(message)
|
|
164
|
+
return 1
|
|
165
|
+
|
|
166
|
+
fea_filename = options.input
|
|
167
|
+
if options.debug_feature_file:
|
|
168
|
+
fea_filename = output_font.with_suffix(".fea")
|
|
169
|
+
log.info(f"Writing FEA to '{fea_filename}'")
|
|
170
|
+
with open(fea_filename, "w") as fp:
|
|
171
|
+
fp.write(fea)
|
|
172
|
+
|
|
173
|
+
log.info("Compiling FEA to OpenType tables")
|
|
174
|
+
try:
|
|
175
|
+
addOpenTypeFeaturesFromString(
|
|
176
|
+
font,
|
|
177
|
+
fea,
|
|
178
|
+
filename=fea_filename,
|
|
179
|
+
tables=options.tables,
|
|
180
|
+
)
|
|
181
|
+
except FeatureLibError as e:
|
|
182
|
+
if options.traceback:
|
|
183
|
+
raise
|
|
184
|
+
log.error(e)
|
|
185
|
+
return 1
|
|
186
|
+
|
|
187
|
+
if options.ship:
|
|
188
|
+
for tag in ["TSIV", "TSIS", "TSIP", "TSID"]:
|
|
189
|
+
if tag in font:
|
|
190
|
+
del font[tag]
|
|
191
|
+
|
|
192
|
+
# Restore original glyph names.
|
|
193
|
+
if tempGlyphOrder:
|
|
194
|
+
import io
|
|
195
|
+
|
|
196
|
+
f = io.BytesIO()
|
|
197
|
+
font.save(f)
|
|
198
|
+
font = TTFont(f)
|
|
199
|
+
font.setGlyphOrder(glyphOrder)
|
|
200
|
+
font["post"].extraNames = []
|
|
201
|
+
|
|
202
|
+
font.save(output_font)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
if __name__ == "__main__":
|
|
206
|
+
sys.exit(main())
|
fontTools/voltLib/ast.py
CHANGED
|
@@ -317,6 +317,10 @@ class SubstitutionLigatureDefinition(SubstitutionDefinition):
|
|
|
317
317
|
pass
|
|
318
318
|
|
|
319
319
|
|
|
320
|
+
class SubstitutionAlternateDefinition(SubstitutionDefinition):
|
|
321
|
+
pass
|
|
322
|
+
|
|
323
|
+
|
|
320
324
|
class SubstitutionReverseChainingSingleDefinition(SubstitutionDefinition):
|
|
321
325
|
pass
|
|
322
326
|
|
fontTools/voltLib/parser.py
CHANGED
|
@@ -313,19 +313,27 @@ class Parser(object):
|
|
|
313
313
|
self.expect_keyword_("END_SUBSTITUTION")
|
|
314
314
|
max_src = max([len(cov) for cov in src])
|
|
315
315
|
max_dest = max([len(cov) for cov in dest])
|
|
316
|
+
|
|
316
317
|
# many to many or mixed is invalid
|
|
317
|
-
if
|
|
318
|
-
reversal and (max_src > 1 or max_dest > 1)
|
|
319
|
-
):
|
|
318
|
+
if max_src > 1 and max_dest > 1:
|
|
320
319
|
raise VoltLibError("Invalid substitution type", location)
|
|
320
|
+
|
|
321
321
|
mapping = dict(zip(tuple(src), tuple(dest)))
|
|
322
322
|
if max_src == 1 and max_dest == 1:
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
323
|
+
# Alternate substitutions are represented by adding multiple
|
|
324
|
+
# substitutions for the same glyph, so we detect that here
|
|
325
|
+
glyphs = [x.glyphSet() for cov in src for x in cov] # flatten src
|
|
326
|
+
if len(set(glyphs)) != len(glyphs): # src has duplicates
|
|
327
|
+
sub = ast.SubstitutionAlternateDefinition(mapping, location=location)
|
|
327
328
|
else:
|
|
328
|
-
|
|
329
|
+
if reversal:
|
|
330
|
+
# Reversal is valid only for single glyph substitutions
|
|
331
|
+
# and VOLT ignores it otherwise.
|
|
332
|
+
sub = ast.SubstitutionReverseChainingSingleDefinition(
|
|
333
|
+
mapping, location=location
|
|
334
|
+
)
|
|
335
|
+
else:
|
|
336
|
+
sub = ast.SubstitutionSingleDefinition(mapping, location=location)
|
|
329
337
|
elif max_src == 1 and max_dest > 1:
|
|
330
338
|
sub = ast.SubstitutionMultipleDefinition(mapping, location=location)
|
|
331
339
|
elif max_src > 1 and max_dest == 1:
|