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.

Files changed (71) hide show
  1. fontTools/__init__.py +1 -1
  2. fontTools/cffLib/__init__.py +61 -26
  3. fontTools/config/__init__.py +15 -0
  4. fontTools/cu2qu/cu2qu.cp39-win32.pyd +0 -0
  5. fontTools/designspaceLib/statNames.py +14 -7
  6. fontTools/feaLib/ast.py +92 -13
  7. fontTools/feaLib/builder.py +52 -13
  8. fontTools/feaLib/lexer.cp39-win32.pyd +0 -0
  9. fontTools/feaLib/parser.py +59 -39
  10. fontTools/fontBuilder.py +6 -0
  11. fontTools/misc/bezierTools.cp39-win32.pyd +0 -0
  12. fontTools/misc/etree.py +4 -27
  13. fontTools/misc/testTools.py +2 -1
  14. fontTools/mtiLib/__init__.py +0 -2
  15. fontTools/otlLib/builder.py +195 -145
  16. fontTools/otlLib/optimize/gpos.py +49 -63
  17. fontTools/pens/momentsPen.cp39-win32.pyd +0 -0
  18. fontTools/pens/pointPen.py +21 -12
  19. fontTools/qu2cu/qu2cu.cp39-win32.pyd +0 -0
  20. fontTools/subset/__init__.py +11 -0
  21. fontTools/ttLib/__init__.py +4 -0
  22. fontTools/ttLib/__main__.py +47 -8
  23. fontTools/ttLib/tables/D__e_b_g.py +20 -2
  24. fontTools/ttLib/tables/G_V_A_R_.py +5 -0
  25. fontTools/ttLib/tables/T_S_I__0.py +14 -3
  26. fontTools/ttLib/tables/T_S_I__5.py +16 -5
  27. fontTools/ttLib/tables/__init__.py +1 -0
  28. fontTools/ttLib/tables/_c_m_a_p.py +19 -6
  29. fontTools/ttLib/tables/_c_v_t.py +2 -0
  30. fontTools/ttLib/tables/_f_p_g_m.py +3 -1
  31. fontTools/ttLib/tables/_g_l_y_f.py +11 -10
  32. fontTools/ttLib/tables/_g_v_a_r.py +62 -17
  33. fontTools/ttLib/tables/_p_o_s_t.py +5 -2
  34. fontTools/ttLib/tables/otBase.py +1 -0
  35. fontTools/ttLib/tables/otConverters.py +5 -2
  36. fontTools/ttLib/tables/otTables.py +5 -1
  37. fontTools/ttLib/ttFont.py +3 -5
  38. fontTools/ttLib/ttGlyphSet.py +0 -10
  39. fontTools/ttx.py +13 -1
  40. fontTools/ufoLib/__init__.py +2 -2
  41. fontTools/ufoLib/converters.py +89 -25
  42. fontTools/ufoLib/errors.py +8 -0
  43. fontTools/ufoLib/etree.py +1 -1
  44. fontTools/ufoLib/filenames.py +155 -100
  45. fontTools/ufoLib/glifLib.py +9 -2
  46. fontTools/ufoLib/kerning.py +66 -36
  47. fontTools/ufoLib/utils.py +5 -2
  48. fontTools/unicodedata/Mirrored.py +446 -0
  49. fontTools/unicodedata/__init__.py +6 -2
  50. fontTools/varLib/__init__.py +94 -89
  51. fontTools/varLib/hvar.py +113 -0
  52. fontTools/varLib/iup.cp39-win32.pyd +0 -0
  53. fontTools/varLib/varStore.py +1 -1
  54. fontTools/voltLib/__main__.py +206 -0
  55. fontTools/voltLib/ast.py +4 -0
  56. fontTools/voltLib/parser.py +16 -8
  57. fontTools/voltLib/voltToFea.py +347 -166
  58. {fonttools-4.56.0.dist-info → fonttools-4.58.0.dist-info}/METADATA +60 -12
  59. {fonttools-4.56.0.dist-info → fonttools-4.58.0.dist-info}/RECORD +65 -66
  60. {fonttools-4.56.0.dist-info → fonttools-4.58.0.dist-info}/WHEEL +1 -1
  61. fonttools-4.58.0.dist-info/licenses/LICENSE.external +359 -0
  62. fontTools/cu2qu/cu2qu.c +0 -14829
  63. fontTools/feaLib/lexer.c +0 -17986
  64. fontTools/misc/bezierTools.c +0 -41831
  65. fontTools/pens/momentsPen.c +0 -13448
  66. fontTools/qu2cu/qu2cu.c +0 -16269
  67. fontTools/varLib/iup.c +0 -19154
  68. {fonttools-4.56.0.data → fonttools-4.58.0.data}/data/share/man/man1/ttx.1 +0 -0
  69. {fonttools-4.56.0.dist-info → fonttools-4.58.0.dist-info}/entry_points.txt +0 -0
  70. {fonttools-4.56.0.dist-info → fonttools-4.58.0.dist-info/licenses}/LICENSE +0 -0
  71. {fonttools-4.56.0.dist-info → fonttools-4.58.0.dist-info}/top_level.txt +0 -0
@@ -12,13 +12,13 @@ a Glyphs source, eg., using noto-source as an example:
12
12
 
13
13
  .. code-block:: sh
14
14
 
15
- $ fontmake -o ttf-interpolatable -g NotoSansArabic-MM.glyphs
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
- $ fonttools varLib master_ufo/NotoSansArabic.designspace
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
- ["tableTag", "metricsTag", "sb1", "sb2", "advMapping", "vOrigMapping"],
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
- _add_VHVAR(font, masterModel, master_ttfs, axisTags, HVAR_FIELDS)
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
- _add_VHVAR(font, masterModel, master_ttfs, axisTags, VVAR_FIELDS)
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, masterModel, master_ttfs, axisTags, tableFields):
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
- glyphOrder = font.getGlyphOrder()
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
- vhvar.VarStore = metricsStore
546
- if advanceMapping is None:
547
- setattr(vhvar, tableFields.advMapping, None)
540
+ if vOrigDeltasAndSupports:
541
+ singleModel = False
548
542
  else:
549
- setattr(vhvar, tableFields.advMapping, advanceMapping)
550
- if vOrigMapping is not None:
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 vOrigMetricses:
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 vOrigMetricses:
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 vOrigMetricses:
605
+ if vOrigDeltasAndSupports:
661
606
  vOrigMapping = builder.buildVarIdxMap(vOrigMap, glyphOrder)
662
607
 
663
- return metricsStore, advanceMapping, vOrigMapping
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):
@@ -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
@@ -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
 
@@ -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 (max_src > 1 and max_dest > 1) or (
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
- if reversal:
324
- sub = ast.SubstitutionReverseChainingSingleDefinition(
325
- mapping, location=location
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
- sub = ast.SubstitutionSingleDefinition(mapping, location=location)
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: