fontforge-variable-font 0.3.0__tar.gz → 0.4.1__tar.gz

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.
Files changed (26) hide show
  1. {fontforge_variable_font-0.3.0/fontforge_variable_font.egg-info → fontforge_variable_font-0.4.1}/PKG-INFO +2 -1
  2. fontforge_variable_font-0.4.1/fontforgeVF/__init__.py +50 -0
  3. {fontforge_variable_font-0.3.0 → fontforge_variable_font-0.4.1}/fontforgeVF/__main__.py +12 -21
  4. {fontforge_variable_font-0.3.0 → fontforge_variable_font-0.4.1}/fontforgeVF/delete.py +6 -9
  5. {fontforge_variable_font-0.3.0 → fontforge_variable_font-0.4.1}/fontforgeVF/design_axes.py +21 -15
  6. {fontforge_variable_font-0.3.0 → fontforge_variable_font-0.4.1}/fontforgeVF/export.py +49 -40
  7. {fontforge_variable_font-0.3.0 → fontforge_variable_font-0.4.1}/fontforgeVF/instance.py +15 -7
  8. {fontforge_variable_font-0.3.0 → fontforge_variable_font-0.4.1}/fontforgeVF/language.py +1 -8
  9. {fontforge_variable_font-0.3.0 → fontforge_variable_font-0.4.1}/fontforgeVF/load.py +23 -39
  10. {fontforge_variable_font-0.3.0 → fontforge_variable_font-0.4.1}/fontforgeVF/utils.py +62 -16
  11. {fontforge_variable_font-0.3.0 → fontforge_variable_font-0.4.1/fontforge_variable_font.egg-info}/PKG-INFO +2 -1
  12. fontforge_variable_font-0.4.1/fontforge_variable_font.egg-info/requires.txt +3 -0
  13. {fontforge_variable_font-0.3.0 → fontforge_variable_font-0.4.1}/setup.cfg +2 -1
  14. {fontforge_variable_font-0.3.0 → fontforge_variable_font-0.4.1}/test/test_export.py +2 -1
  15. fontforge_variable_font-0.3.0/fontforgeVF/__init__.py +0 -6
  16. fontforge_variable_font-0.3.0/fontforge_variable_font.egg-info/requires.txt +0 -2
  17. {fontforge_variable_font-0.3.0 → fontforge_variable_font-0.4.1}/LICENSE +0 -0
  18. {fontforge_variable_font-0.3.0 → fontforge_variable_font-0.4.1}/README.md +0 -0
  19. {fontforge_variable_font-0.3.0 → fontforge_variable_font-0.4.1}/fontforge_variable_font.egg-info/SOURCES.txt +0 -0
  20. {fontforge_variable_font-0.3.0 → fontforge_variable_font-0.4.1}/fontforge_variable_font.egg-info/dependency_links.txt +0 -0
  21. {fontforge_variable_font-0.3.0 → fontforge_variable_font-0.4.1}/fontforge_variable_font.egg-info/entry_points.txt +0 -0
  22. {fontforge_variable_font-0.3.0 → fontforge_variable_font-0.4.1}/fontforge_variable_font.egg-info/top_level.txt +0 -0
  23. {fontforge_variable_font-0.3.0 → fontforge_variable_font-0.4.1}/pyproject.toml +0 -0
  24. {fontforge_variable_font-0.3.0 → fontforge_variable_font-0.4.1}/test/test_design_axes.py +1 -1
  25. {fontforge_variable_font-0.3.0 → fontforge_variable_font-0.4.1}/test/test_language.py +0 -0
  26. {fontforge_variable_font-0.3.0 → fontforge_variable_font-0.4.1}/test/test_utils.py +1 -1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fontforge_variable_font
3
- Version: 0.3.0
3
+ Version: 0.4.1
4
4
  Summary: A FontForge_plugin to create a variable font
5
5
  Home-page: https://github.com/MihailJP/fontforge-variable-font
6
6
  Author: MihailJP
@@ -17,6 +17,7 @@ Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Requires-Dist: fonttools
19
19
  Requires-Dist: fontmake
20
+ Requires-Dist: fontforge_plugin_helper
20
21
  Dynamic: license-file
21
22
 
22
23
  Fontforge Variable Font Plugin
@@ -0,0 +1,50 @@
1
+ """FontForge plugin to create a variable font"""
2
+
3
+ from .delete import deleteVFInfo
4
+ from .design_axes import getAxisValue
5
+ from .export import exportVariableFont
6
+ from .language import languageCodeIterator, languageCodeLookup, languageCodeReverseLookup, getLanguageList
7
+ from .load import openVariableFont
8
+ from .utils import (
9
+ intOrFloat,
10
+ initPersistentDict,
11
+ vfInfoExists,
12
+ getVFValue,
13
+ setVFValue,
14
+ deleteEmptyDicts,
15
+ deleteVFValue,
16
+ setOrDeleteVFValue,
17
+ checkExtensionTtfOrWoff2,
18
+ )
19
+
20
+
21
+ __all__ = [
22
+ # delete
23
+ "deleteVFInfo",
24
+
25
+ # design_axes
26
+ "getAxisValue",
27
+
28
+ # export
29
+ "exportVariableFont",
30
+
31
+ # language
32
+ 'languageCodeIterator',
33
+ 'languageCodeLookup',
34
+ 'languageCodeReverseLookup',
35
+ 'getLanguageList',
36
+
37
+ # load
38
+ "openVariableFont",
39
+
40
+ # utils
41
+ "intOrFloat",
42
+ "initPersistentDict",
43
+ "vfInfoExists",
44
+ "getVFValue",
45
+ "setVFValue",
46
+ "deleteEmptyDicts",
47
+ "deleteVFValue",
48
+ "setOrDeleteVFValue",
49
+ "checkExtensionTtfOrWoff2",
50
+ ]
@@ -1,3 +1,5 @@
1
+ import fontforge
2
+
1
3
  from fontforgeVF import (
2
4
  delete,
3
5
  design_axes,
@@ -5,25 +7,7 @@ from fontforgeVF import (
5
7
  instance,
6
8
  load
7
9
  )
8
- import fontforge
9
- from typing import Literal, Callable
10
-
11
-
12
- def _addHook(
13
- name: Literal['newFontHook', 'loadFontHook'],
14
- hook: Callable[[fontforge.font], None]
15
- ):
16
- assert isinstance(fontforge.hooks, dict)
17
- if name in fontforge.hooks:
18
- currentHook = fontforge.hooks[name]
19
-
20
- def chainHook(font: fontforge.font):
21
- currentHook(font)
22
- hook(font)
23
-
24
- fontforge.hooks[name] = chainHook
25
- else:
26
- fontforge.hooks[name] = hook
10
+ from fontforge_plugin_helper import addSystemHook
27
11
 
28
12
 
29
13
  def fontforge_plugin_init(**kw):
@@ -50,6 +34,13 @@ def fontforge_plugin_init(**kw):
50
34
  submenu="_Variable Font",
51
35
  name="_Generate a variable font..."
52
36
  )
37
+
38
+ fontforge.registerMenuItem(
39
+ divider=True,
40
+ context="Font",
41
+ submenu="_Variable Font",
42
+ )
43
+
53
44
  fontforge.registerMenuItem(
54
45
  callback=design_axes.designAxesMenu,
55
46
  enable=None,
@@ -73,5 +64,5 @@ def fontforge_plugin_init(**kw):
73
64
  )
74
65
 
75
66
  if fontforge.hasUserInterface:
76
- _addHook('loadFontHook', load.loadHook)
77
- _addHook('newFontHook', load.newFontHook)
67
+ addSystemHook('loadFontHook', load.loadHook)
68
+ addSystemHook('newFontHook', load.newFontHook)
@@ -1,10 +1,6 @@
1
- from fontforgeVF import utils
2
1
  import fontforge
3
2
 
4
-
5
- __all__ = [
6
- "deleteVFInfo",
7
- ]
3
+ from . import utils
8
4
 
9
5
 
10
6
  def deleteVFInfo(font: fontforge.font) -> bool:
@@ -17,6 +13,7 @@ def deleteVFInfo(font: fontforge.font) -> bool:
17
13
  :return: ``True`` if the key was deleted, ``False`` otherwise.
18
14
  """
19
15
  if utils.vfInfoExists(font):
16
+ assert isinstance(font.persistent, dict)
20
17
  del font.persistent['VF']
21
18
  if len(font.persistent) == 0:
22
19
  font.persistent = None
@@ -25,13 +22,13 @@ def deleteVFInfo(font: fontforge.font) -> bool:
25
22
  return False
26
23
 
27
24
 
28
- def deleteVFInfoMenu(u, glyph):
25
+ def deleteVFInfoMenu(u, font: fontforge.font):
29
26
  """Menu emtry to delete VF info from UI
30
27
 
31
28
  This menu is enabled if active font has VF info.
32
29
  """
33
- deleteVFInfo(fontforge.activeFont())
30
+ deleteVFInfo(font)
34
31
 
35
32
 
36
- def deleteVFInfoEnable(u, glyph):
37
- return utils.vfInfoExists(fontforge.activeFont())
33
+ def deleteVFInfoEnable(u, font: fontforge.font):
34
+ return utils.vfInfoExists(font)
@@ -1,10 +1,6 @@
1
- from fontforgeVF import utils
2
1
  import fontforge
3
2
 
4
-
5
- __all__ = [
6
- "getAxisValue",
7
- ]
3
+ from . import utils
8
4
 
9
5
 
10
6
  def _getWidthFromOS2Width(font: fontforge.font) -> int | float:
@@ -28,7 +24,7 @@ def _searchCustomAxis(font: fontforge.font, tag: str) -> str | None:
28
24
  return tag
29
25
  else:
30
26
  for k, v in designAxes.items():
31
- if k.startswith('custom') and utils.getVFValue(font, 'axes.' + k + '.tag', '').rstrip() == tag.rstrip():
27
+ if k.startswith('custom') and str(utils.getVFValue(font, 'axes.' + k + '.tag', '')).rstrip() == tag.rstrip():
32
28
  return k
33
29
  return None
34
30
 
@@ -60,21 +56,21 @@ def _getFlags(font: fontforge.font, labelAddr: str) -> int:
60
56
 
61
57
  def _loadLabels(tag: str, lang=None):
62
58
  font = fontforge.activeFont()
59
+ assert font is not None
63
60
  addr = 'axes.' + tag + '.labels'
64
- if label := utils.getVFValue(font, addr):
61
+ if label := utils.getVFValueAsDict(font, addr):
65
62
  text = ''
66
- assert isinstance(label, dict)
67
63
  for k, v in label.items():
68
64
  labelAddr = addr + '.' + str(k).replace('.', ',') # escape decimal point
69
65
  if lang:
70
66
  text += str(k) + ',' + \
71
- utils.getVFValue(font, labelAddr + '.localNames.' + hex(lang), '') + \
67
+ str(utils.getVFValue(font, labelAddr + '.localNames.' + hex(lang), '')) + \
72
68
  ', '
73
69
  else:
74
70
  text += str(k) + ',' + \
75
71
  str(_getFlags(font, labelAddr)) + \
76
72
  ',' + str(utils.getVFValue(font, labelAddr + '.linkedValue', '')) + \
77
- ',' + utils.getVFValue(font, labelAddr + '.name', '') + \
73
+ ',' + str(utils.getVFValue(font, labelAddr + '.name', '')) + \
78
74
  ', '
79
75
  return text[:-2]
80
76
  elif utils.vfInfoExists(font):
@@ -105,9 +101,10 @@ def _loadLabels(tag: str, lang=None):
105
101
  def _prepareQuestions_languages(questions):
106
102
  from fontforgeVF.language import getLanguageList
107
103
  font = fontforge.activeFont()
104
+ assert font is not None
108
105
  languages = set()
109
106
  for k, v in designAxes.items():
110
- languages |= set(utils.getVFValue(font, 'axes.' + k + '.localNames', {}).keys())
107
+ languages |= set(utils.getVFValueAsDict(font, 'axes.' + k + '.localNames').keys())
111
108
  languages = tuple(languages)
112
109
  localNameCategory = len(questions)
113
110
  localNameRange = range(1, max(((len(languages) + 7) // 4) * 4, 8)+1)
@@ -120,6 +117,7 @@ def _prepareQuestions_languages(questions):
120
117
 
121
118
  def _prepareQuestions_values(questions, k, v):
122
119
  font = fontforge.activeFont()
120
+ assert font is not None
123
121
  tagStat = 1 if utils.getVFValue(font, 'axes.' + k + '.active', False) else 0
124
122
  if tagStat == 1 and not k.startswith('custom') and utils.getVFValue(font, 'axes.' + k + '.useDefault', False):
125
123
  tagStat = 2
@@ -159,19 +157,21 @@ def _prepareQuestions_values(questions, k, v):
159
157
 
160
158
  def _prepareQuestions_map(questions, k, v):
161
159
  font = fontforge.activeFont()
160
+ assert font is not None
162
161
  questions[2]["questions"].append({
163
162
  'type': 'string',
164
163
  'question': v["name"] + ':',
165
164
  'tag': k + 'map',
166
165
  'default': ', '.join(list(map(
167
166
  lambda x: str(x[0]) + ',' + str(x[1]),
168
- utils.getVFValue(font, 'axes.' + k + '.map', [])
167
+ utils.getVFValueAsList(font, 'axes.' + k + '.map')
169
168
  ))),
170
169
  })
171
170
 
172
171
 
173
172
  def _prepareQuestions_order(questions, k, v):
174
173
  font = fontforge.activeFont()
174
+ assert font is not None
175
175
  questions[3]["questions"].append({
176
176
  'type': 'string',
177
177
  'question': v["name"] + ':',
@@ -182,6 +182,7 @@ def _prepareQuestions_order(questions, k, v):
182
182
 
183
183
  def _prepareQuestions_names(questions, k, v):
184
184
  font = fontforge.activeFont()
185
+ assert font is not None
185
186
  questions[4]["questions"].append({
186
187
  'type': 'string',
187
188
  'question': v["name"] + ':',
@@ -202,6 +203,7 @@ def _prepareQuestions_names(questions, k, v):
202
203
 
203
204
  def _prepareQuestions_localNames(questions, k, v, languages, localNameRange, localNameCategory):
204
205
  font = fontforge.activeFont()
206
+ assert font is not None
205
207
  for i in localNameRange:
206
208
  questions[localNameCategory + i - 1]["questions"].append({
207
209
  'type': 'string',
@@ -221,6 +223,7 @@ def _prepareQuestions_localNames(questions, k, v, languages, localNameRange, loc
221
223
 
222
224
  def _prepareQuestions_custom(questions, k, v):
223
225
  font = fontforge.activeFont()
226
+ assert font is not None
224
227
  if k.startswith('custom'):
225
228
  questions[1]["questions"].append({
226
229
  'type': 'string',
@@ -253,6 +256,7 @@ def _prepareQuestions():
253
256
  # Save result
254
257
  def _saveResult_values(result, k, v):
255
258
  font = fontforge.activeFont()
259
+ assert font is not None
256
260
  utils.setVFValue(font, 'axes.' + k + '.active', result[k] != 'unset')
257
261
  if not k.startswith('custom') and result[k] != 'unset':
258
262
  utils.setVFValue(font, 'axes.' + k + '.useDefault', result[k] == 'auto')
@@ -270,6 +274,7 @@ def _saveResult_values(result, k, v):
270
274
 
271
275
  def _saveResult_labels(result, k, v):
272
276
  font = fontforge.activeFont()
277
+ assert font is not None
273
278
  utils.setOrDeleteVFValue(font, 'axes.' + k + '.order', int(result[k + 'order']) if result[k + 'order'] else None)
274
279
  utils.deleteVFValue(font, 'axes.' + k + '.labels')
275
280
  if result[k + 'labels']:
@@ -280,9 +285,9 @@ def _saveResult_labels(result, k, v):
280
285
  val = val.replace('.', ',') # escape decimal point
281
286
  valAddr = 'axes.' + k + '.labels.' + val
282
287
  utils.setOrDeleteVFValue(font, valAddr + '.olderSibling',
283
- None if el == '' else bool(utils.intOrFloat(el) & 1))
288
+ None if el == '' else bool(int(el) & 1))
284
289
  utils.setOrDeleteVFValue(font, valAddr + '.elidable',
285
- None if el == '' else bool(utils.intOrFloat(el) & 2))
290
+ None if el == '' else bool(int(el) & 2))
286
291
  utils.setOrDeleteVFValue(font, valAddr + '.linkedValue',
287
292
  None if lv == '' else utils.intOrFloat(lv))
288
293
  utils.setOrDeleteVFValue(font, valAddr + '.name',
@@ -299,6 +304,7 @@ def _saveResult_labels(result, k, v):
299
304
 
300
305
  def _saveResult_localNames(result, k, v):
301
306
  font = fontforge.activeFont()
307
+ assert font is not None
302
308
  utils.setOrDeleteVFValue(font, 'axes.' + k + '.name', result[k + 'name'])
303
309
  utils.deleteVFValue(font, 'axes.' + k + '.localNames')
304
310
  for i in (x.replace('lang', '') for x in result if x.startswith('lang')):
@@ -421,6 +427,6 @@ def designAxesMenu(u, glyph):
421
427
  * Axis value
422
428
  * Name
423
429
  """
424
- result = fontforge.askMulti("Design axes", _prepareQuestions())
430
+ result = fontforge.askMulti("Design axes", _prepareQuestions()) # type: ignore
425
431
  if result:
426
432
  _saveResult(result)
@@ -1,8 +1,8 @@
1
- from fontforgeVF import utils, language
2
- from fontforgeVF.design_axes import designAxes, getAxisValue
3
- import fontforge
4
- import tempfile
5
1
  from os import PathLike
2
+ import tempfile
3
+
4
+ import fontforge
5
+ from fontTools import ttLib
6
6
  from fontTools.designspaceLib import (
7
7
  DesignSpaceDocument,
8
8
  SourceDescriptor,
@@ -11,12 +11,9 @@ from fontTools.designspaceLib import (
11
11
  AxisLabelDescriptor,
12
12
  InstanceDescriptor,
13
13
  )
14
- from fontTools import ttLib
15
-
16
14
 
17
- __all__ = [
18
- "exportVariableFont",
19
- ]
15
+ from . import utils, language
16
+ from .design_axes import designAxes, getAxisValue
20
17
 
21
18
 
22
19
  def _getSourceFonts(defaultFont: fontforge.font, filterItalicRoman: bool | None = None) -> list[fontforge.font]:
@@ -33,7 +30,7 @@ def _axisMinValue(defaultFont: fontforge.font, tag: str, filterItalicRoman: bool
33
30
  getAxisValue(f, tag) for f
34
31
  in _getSourceFonts(defaultFont, filterItalicRoman)
35
32
  if getAxisValue(f, tag) is not None
36
- )
33
+ ) # type: ignore
37
34
  )
38
35
 
39
36
 
@@ -43,7 +40,7 @@ def _axisMaxValue(defaultFont: fontforge.font, tag: str, filterItalicRoman: bool
43
40
  getAxisValue(f, tag) for f
44
41
  in _getSourceFonts(defaultFont, filterItalicRoman)
45
42
  if getAxisValue(f, tag) is not None
46
- )
43
+ ) # type: ignore
47
44
  )
48
45
 
49
46
 
@@ -133,7 +130,7 @@ def _outputUfo(font: fontforge.font, outputDir: str | PathLike, outputFile: str
133
130
  import re
134
131
  # import shutil # for debug
135
132
 
136
- assert outputFile.endswith('.ufo')
133
+ assert str(outputFile).endswith('.ufo')
137
134
  ufoPath = str(outputDir) + '/' + str(outputFile)
138
135
  changed = font.changed
139
136
  unlinkRmOvrlpSave = {}
@@ -151,10 +148,10 @@ def _outputUfo(font: fontforge.font, outputDir: str | PathLike, outputFile: str
151
148
  info = _ufoInfo()
152
149
  ufo.readInfo(info)
153
150
  if _isFixedPitch(font):
154
- ufo.postscriptIsFixedPitch = True
155
- ufo.styleMapFamilyName = _getFontFamilyName(font)
156
- ufo.styleMapStyleName = _getFontSubFamilyName(font)
157
- ufo.writeInfo(info)
151
+ ufo.postscriptIsFixedPitch = True # type: ignore
152
+ ufo.styleMapFamilyName = _getFontFamilyName(font) # type: ignore
153
+ ufo.styleMapStyleName = _getFontSubFamilyName(font) # type: ignore
154
+ ufo.writeInfo(info) # type: ignore
158
155
 
159
156
  if _aaltExists(font) or _allGSUBTags(font):
160
157
  feat = ufo.readFeatures()
@@ -172,7 +169,7 @@ def _outputUfo(font: fontforge.font, outputDir: str | PathLike, outputFile: str
172
169
  if aaltInclude or existingAalt:
173
170
  newAalt = "feature aalt {\n" + aaltInclude + existingAalt + "} aalt;\n\n"
174
171
  feat = re.sub(featPosPattern, newAalt, feat, count=1)
175
- ufo.writeFeatures(feat)
172
+ ufo.writeFeatures(feat) # type: ignore
176
173
 
177
174
  font.changed = changed
178
175
 
@@ -185,6 +182,7 @@ def _designSpaceSources(font: fontforge.font, doc: DesignSpaceDocument, filterIt
185
182
  # print(_getSourceFonts(font))
186
183
  for f in _getSourceFonts(font, filterItalicRoman):
187
184
  s = SourceDescriptor()
185
+ assert isinstance(f.temporary, dict)
188
186
  s.path = f.temporary['ufo']
189
187
  if f is font:
190
188
  s.copyLib = True
@@ -197,11 +195,15 @@ def _designSpaceSources(font: fontforge.font, doc: DesignSpaceDocument, filterIt
197
195
  for k, v in designAxes.items():
198
196
  active = 'axes.' + k + '.active'
199
197
  if utils.getVFValue(font, active, False) and utils.getVFValue(f, active, False):
200
- tag = utils.getVFValue(font, 'axes.' + k + '.tag', '????') \
198
+ tag = str(
199
+ utils.getVFValue(font, 'axes.' + k + '.tag', '????')
201
200
  if k.startswith('custom') else k
202
- name = utils.getVFValue(font, 'axes.' + k + '.name', v['name']) \
201
+ )
202
+ name = str(
203
+ utils.getVFValue(font, 'axes.' + k + '.name', v['name'])
203
204
  if k.startswith('custom') else v['name']
204
- s.location[name] = getAxisValue(f, tag)
205
+ )
206
+ s.location[name] = getAxisValue(f, tag) # type: ignore
205
207
  s.familyName = _getFontFamilyName(f)
206
208
  s.styleName = _getFontSubFamilyName(f)
207
209
  doc.addSource(s)
@@ -210,7 +212,7 @@ def _designSpaceSources(font: fontforge.font, doc: DesignSpaceDocument, filterIt
210
212
  def _designSpaceAxes_labels(labels, a: AxisDescriptor | DiscreteAxisDescriptor):
211
213
  L = []
212
214
  for u, d in labels.items():
213
- if not (a.minimum <= u <= a.maximum):
215
+ if not (a.minimum <= u <= a.maximum): # type: ignore
214
216
  fontforge.logWarning('Ignored label {0} = {1} because out of range'.format(
215
217
  a.name, str(u)))
216
218
  elif 'name' not in d:
@@ -232,15 +234,17 @@ def _designSpaceAxes(font: fontforge.font, doc: DesignSpaceDocument, filterItali
232
234
  for k, v in designAxes.items():
233
235
  if utils.getVFValue(font, 'axes.' + k + '.active', False):
234
236
  a = DiscreteAxisDescriptor() if k == 'ital' else AxisDescriptor()
235
- a.tag = utils.getVFValue(font, 'axes.' + k + '.tag', '????') \
237
+ a.tag = str(
238
+ utils.getVFValue(font, 'axes.' + k + '.tag', '????')
236
239
  if k.startswith('custom') else k
240
+ )
237
241
  if k == 'ital' and filterItalicRoman is not None:
238
- a.minimum = 1 if filterItalicRoman else _axisMinValue(font, a.tag)
239
- a.maximum = 0 if not filterItalicRoman else _axisMaxValue(font, a.tag)
242
+ a.minimum = 1 if filterItalicRoman else _axisMinValue(font, a.tag) # type: ignore
243
+ a.maximum = 0 if not filterItalicRoman else _axisMaxValue(font, a.tag) # type: ignore
240
244
  else:
241
- a.minimum = _axisMinValue(font, a.tag)
242
- a.maximum = _axisMaxValue(font, a.tag)
243
- a.default = getAxisValue(font, a.tag)
245
+ a.minimum = _axisMinValue(font, a.tag) # type: ignore
246
+ a.maximum = _axisMaxValue(font, a.tag) # type: ignore
247
+ a.default = getAxisValue(font, a.tag) # type: ignore
244
248
  a.name = utils.getVFValue(font, 'axes.' + k + '.name', v['name'])
245
249
  if val := utils.getVFValue(font, 'axes.' + k + '.map'):
246
250
  a.map = val
@@ -252,7 +256,8 @@ def _designSpaceAxes(font: fontforge.font, doc: DesignSpaceDocument, filterItali
252
256
 
253
257
 
254
258
  def _designSpaceInstances(font: fontforge.font, doc: DesignSpaceDocument, filterItalicRoman: bool | None = None):
255
- for instance in utils.getVFValue(font, 'instances', []):
259
+ instances = utils.getVFValueAsList(font, 'instances')
260
+ for instance in instances:
256
261
  i = InstanceDescriptor()
257
262
  i.styleName = instance['name']
258
263
  i.postScriptFontName = instance['psName']
@@ -325,7 +330,7 @@ def _fixTtf_axes(font: fontforge.font, ttf: ttLib.TTFont):
325
330
  if k.startswith('custom') else k
326
331
  )
327
332
  if utils.getVFValue(font, 'axes.' + k + '.active', False):
328
- localNames = utils.getVFValue(font, 'axes.' + k + '.localNames', {})
333
+ localNames = utils.getVFValueAsDict(font, 'axes.' + k + '.localNames')
329
334
  for lang, name in localNames.items():
330
335
  axis = [a for a in ttf["fvar"].axes if a.axisTag == tag]
331
336
  if axis:
@@ -337,9 +342,9 @@ def _fixTtf_labels(font: fontforge.font, ttf: ttLib.TTFont):
337
342
  axisIndex = axisLabel.AxisIndex
338
343
  tag = ttf['STAT'].table.DesignAxisRecord.Axis[axisIndex].AxisTag
339
344
  value = axisLabel.Value
340
- localNames = utils.getVFValue(
345
+ localNames = utils.getVFValueAsDict(
341
346
  font, 'axes.' + tag + '.labels.' + str(int(value)) + '.localNames',
342
- utils.getVFValue(
347
+ utils.getVFValueAsDict(
343
348
  font, 'axes.' + tag + '.labels.' + str(float(value)).replace('.', ',') + '.localNames',
344
349
  {}
345
350
  )
@@ -351,7 +356,7 @@ def _fixTtf_labels(font: fontforge.font, ttf: ttLib.TTFont):
351
356
  def _fixTtf_instances(font: fontforge.font, ttf: ttLib.TTFont):
352
357
  for i, instance in enumerate(ttf['fvar'].instances):
353
358
  subfamilyNameID = instance.subfamilyNameID
354
- localNames = utils.getVFValue(
359
+ localNames = utils.getVFValueAsDict(
355
360
  font, 'instances[' + str(i) + '].localNames', {}
356
361
  )
357
362
  for lang, name in localNames.items():
@@ -362,8 +367,12 @@ def _fixTtf(font: fontforge.font, filename: str | PathLike):
362
367
  with ttLib.TTFont(str(filename)) as ttf:
363
368
  for i in font.sfnt_names:
364
369
  if i[0] != 'English (US)':
370
+ if isinstance(i[0], str): # likely
371
+ langCode = language.languageCodeReverseLookup(i[0])
372
+ else: # unlikely
373
+ langCode = i[0]
365
374
  if i[1] in _fields:
366
- ttf['name'].setName(i[2], _fields[i[1]], 3, 1, language.languageCodeReverseLookup(i[0]))
375
+ ttf['name'].setName(i[2], _fields[i[1]], 3, 1, langCode)
367
376
  _fixTtf_axes(font, ttf)
368
377
  _fixTtf_labels(font, ttf)
369
378
  _fixTtf_instances(font, ttf)
@@ -543,7 +552,7 @@ def _exportVariableFont(font: fontforge.font, dialogResult: dict[str, str]):
543
552
 
544
553
  def _saveMenuDialog(font: fontforge.font) -> dict | None:
545
554
  if _hasBothRomanAndItalic(font):
546
- questions = [
555
+ questions: list = [
547
556
  {
548
557
  'type': 'savepath', 'question': '_Roman VF:', 'tag': 'file',
549
558
  'default':
@@ -560,7 +569,7 @@ def _saveMenuDialog(font: fontforge.font) -> dict | None:
560
569
  },
561
570
  ]
562
571
  else:
563
- questions = [
572
+ questions: list = [
564
573
  {
565
574
  'type': 'savepath', 'question': '_Save as:', 'tag': 'file',
566
575
  'default':
@@ -583,10 +592,10 @@ def _saveMenuDialog(font: fontforge.font) -> dict | None:
583
592
  return fontforge.askMulti("Save variable font", questions)
584
593
 
585
594
 
586
- def saveMenu(u, glyph):
587
- if result := _saveMenuDialog(fontforge.activeFont()):
588
- _exportVariableFont(fontforge.activeFont(), result)
595
+ def saveMenu(u, font: fontforge.font):
596
+ if result := _saveMenuDialog(font):
597
+ _exportVariableFont(font, result)
589
598
 
590
599
 
591
- def saveEnable(u, glyph):
592
- return utils.vfInfoExists(fontforge.activeFont())
600
+ def saveEnable(u, font: fontforge.font):
601
+ return utils.vfInfoExists(font)
@@ -1,10 +1,11 @@
1
1
  import fontforge
2
- from fontforgeVF import utils
3
- from fontforgeVF.design_axes import designAxes
2
+
3
+ from . import utils
4
+ from .design_axes import designAxes
4
5
 
5
6
 
6
7
  def _instances_getval(font: fontforge.font, cnt: int, key: str, defaultVal):
7
- instances = utils.getVFValue(font, 'instances', [])
8
+ instances = utils.getVFValueAsList(font, 'instances')
8
9
  if (cnt - 1) < len(instances):
9
10
  if isinstance(instances[cnt - 1], dict):
10
11
  if key in instances[cnt - 1]:
@@ -14,6 +15,7 @@ def _instances_getval(font: fontforge.font, cnt: int, key: str, defaultVal):
14
15
 
15
16
  def _prepareQuestions_instances(cnt: int):
16
17
  font = fontforge.activeFont()
18
+ assert font is not None
17
19
  questions = [
18
20
  {
19
21
  'type': 'string',
@@ -57,13 +59,16 @@ def _prepareQuestions_instanceLocalNames(questions: list):
57
59
  from math import ceil
58
60
 
59
61
  font = fontforge.activeFont()
60
- numberOfInstances = max(((len(utils.getVFValue(font, 'instances', [])) + 7) // 4) * 4, 8)
62
+ assert font is not None
63
+ instances = utils.getVFValueAsList(font, 'instances')
64
+ numberOfInstances = max(((len(instances) + 7) // 4) * 4, 8)
61
65
 
62
66
  languages = set()
63
67
  numberOfLanguages = 8
64
- for i in range(len(utils.getVFValue(font, 'instances', []))):
68
+ for i in range(len(instances)):
65
69
  for k, v in designAxes.items():
66
- languages |= set(utils.getVFValue(font, 'instances[' + str(i) + '].localNames', {}).keys())
70
+ langdict = utils.getVFValueAsDict(font, 'instances[' + str(i) + '].localNames')
71
+ languages |= set(langdict.keys())
67
72
  languages = tuple(languages)
68
73
  numberOfLanguages = max(((len(languages) + 7) // 4) * 4, 8)
69
74
 
@@ -100,8 +105,10 @@ def _prepareQuestions_instanceLocalNames(questions: list):
100
105
 
101
106
  def _prepareQuestions():
102
107
  font = fontforge.activeFont()
108
+ assert font is not None
103
109
  questions = []
104
- numberOfInstances = max(((len(utils.getVFValue(font, 'instances', [])) + 7) // 4) * 4, 8)
110
+ instances = utils.getVFValueAsList(font, 'instances')
111
+ numberOfInstances = max(((len(instances) + 7) // 4) * 4, 8)
105
112
 
106
113
  for i in range(numberOfInstances):
107
114
  questions.append({
@@ -115,6 +122,7 @@ def _prepareQuestions():
115
122
 
116
123
  def _saveInstances(result: dict):
117
124
  font = fontforge.activeFont()
125
+ assert font is not None
118
126
  instanceList = []
119
127
  cnt = 1
120
128
  while 'psName' + str(cnt) in result:
@@ -1,10 +1,3 @@
1
- __all__ = [
2
- 'languageCodeIterator',
3
- 'languageCodeLookup',
4
- 'languageCodeReverseLookup',
5
- 'getLanguageList',
6
- ]
7
-
8
1
  languageData = {
9
2
  1: {
10
3
  'name': 'Arabic',
@@ -832,7 +825,7 @@ def getLanguageList(listNumber: int, defaultCode: int | None = None):
832
825
  for langId, langCode, langName in sorted(languageCodeIterator(), key=lambda x: x[2]):
833
826
  languageList.append({'name': langName, 'tag': hex(langId), 'default': langId == defaultCode})
834
827
  questions = {
835
- 'category': 'Localized names ' + (
828
+ 'category': 'Localized names ' + str(
836
829
  languageCodeLookup(defaultCode) if defaultCode else str(listNumber)
837
830
  ),
838
831
  'questions': [
@@ -1,14 +1,11 @@
1
- import fontforge
2
- from fontforgeVF.utils import intOrFloat, checkExtensionTtfOrWoff2
3
- from os import PathLike
4
- from fontTools import ttLib
5
1
  import faulthandler
6
- from typing import Literal, Callable
2
+ from os import PathLike
7
3
 
4
+ import fontforge
5
+ from fontTools import ttLib
8
6
 
9
- __all__ = [
10
- "openVariableFont",
11
- ]
7
+ from .utils import intOrFloat, checkExtensionTtfOrWoff2, ensureTuple
8
+ from fontforge_plugin_helper import addFontGenerateHook
12
9
 
13
10
 
14
11
  def _checkAxisValue(ttf: ttLib.TTFont, axisValues: dict[str, int | float]):
@@ -64,7 +61,7 @@ def _denormalize(minimum, default, maximum, value):
64
61
  def _getVFData_fvar_axes(ttf: ttLib.TTFont, axisValues: dict[str, int | float], vfData: dict):
65
62
  for axis in ttf['fvar'].axes:
66
63
  tag = axis.axisTag
67
- axisData = {'active': True}
64
+ axisData: dict[str, str | int | float] = {'active': True}
68
65
  if tag in axisValues:
69
66
  axisData['useDefault'] = False
70
67
  axisData['value'] = intOrFloat(axisValues[tag])
@@ -211,6 +208,7 @@ def _doOpenVariableFont(
211
208
  partial.save(instancePath)
212
209
  font = fontforge.open(instancePath)
213
210
  initPersistentDict(font)
211
+ assert isinstance(font.persistent, dict)
214
212
  font.persistent['VF'] = vfData
215
213
  return font
216
214
 
@@ -222,12 +220,12 @@ def _openVF(
222
220
  ) -> fontforge.font:
223
221
  with ttLib.TTFont(filename) as ttf:
224
222
  if 'fvar' not in ttf:
225
- return fontforge.open(filename)
223
+ return fontforge.open(str(filename))
226
224
  elif isinstance(axisValuesOrInstance, dict):
227
225
  if [axis for axis in ttf['fvar'].axes if axis.minValue != axis.maxValue]:
228
226
  return _doOpenVariableFont(filename, axisValuesOrInstance, ttf, tmpdir)
229
227
  else:
230
- return fontforge.open(filename)
228
+ return fontforge.open(str(filename))
231
229
  elif isinstance(axisValuesOrInstance, int):
232
230
  return _doOpenVariableFont(
233
231
  filename,
@@ -290,10 +288,11 @@ def openVariableFont(
290
288
  filetype = checkExtensionTtfOrWoff2(filename)
291
289
  with tempfile.TemporaryDirectory() as tmpdir:
292
290
  if filetype == 'ttf':
293
- _openVF(filename, axisValuesOrInstance, tmpdir)
291
+ font = _openVF(filename, axisValuesOrInstance, tmpdir)
294
292
  else: # woff2
295
293
  ttFile = _woff2Decompress(filename, tmpdir)
296
- _openVF(ttFile, axisValuesOrInstance, tmpdir)
294
+ font = _openVF(ttFile, axisValuesOrInstance, tmpdir)
295
+ return font
297
296
 
298
297
 
299
298
  def _setParameterDialog(filename: str | PathLike, ttf: ttLib.TTFont, tmpdir: str | PathLike):
@@ -314,8 +313,8 @@ def _setParameterDialog(filename: str | PathLike, ttf: ttLib.TTFont, tmpdir: str
314
313
 
315
314
  if result := fontforge.askMulti('Please specify an instance to open', questions):
316
315
  for key in result:
317
- result[key] = intOrFloat(result[key])
318
- _doOpenVariableFont(filename, result, ttf, tmpdir)
316
+ result[key] = intOrFloat(result[key]) # type: ignore
317
+ _doOpenVariableFont(filename, result, ttf, tmpdir) # type: ignore
319
318
 
320
319
 
321
320
  def _chooseInstanceDialog(filename: str | PathLike, ttf: ttLib.TTFont, tmpdir: str | PathLike):
@@ -323,10 +322,10 @@ def _chooseInstanceDialog(filename: str | PathLike, ttf: ttLib.TTFont, tmpdir: s
323
322
  result = fontforge.askChoices(
324
323
  'Choose instance(s) to open',
325
324
  'Instances in this font',
326
- [str(ttf['name'].getName(i.subfamilyNameID, 3, 1, 0x409)) for i in ttf['fvar'].instances],
325
+ tuple([str(ttf['name'].getName(i.subfamilyNameID, 3, 1, 0x409)) for i in ttf['fvar'].instances]),
327
326
  multiple=True
328
327
  )
329
- for i in (x[1] for x in zip(result, ttf['fvar'].instances) if x[0]):
328
+ for i in (x[1] for x in zip(ensureTuple(result), ttf['fvar'].instances) if x[0]):
330
329
  _doOpenVariableFont(filename, i.coordinates, ttf, tmpdir)
331
330
 
332
331
 
@@ -370,6 +369,7 @@ def loadMenu(u, glyph):
370
369
  def _generatePreHook(font: fontforge.font, target: str):
371
370
  from fontforgeVF.utils import vfInfoExists
372
371
 
372
+ assert isinstance(font.temporary, dict)
373
373
  changed = font.changed
374
374
  if vfInfoExists(font):
375
375
  if target.endswith('.ttf') or target.endswith('.woff2'):
@@ -378,7 +378,7 @@ def _generatePreHook(font: fontforge.font, target: str):
378
378
  "This font has variable font metadata in its persistent dict.\n"
379
379
  "Did you intend to output a variable font?\n"
380
380
  "(all masters need to be opened beforehand)",
381
- ["_Yes", "_No"],
381
+ ["_Yes", "_No"], # type: ignore
382
382
  )
383
383
  font.temporary['generateVF'] = (ans == 0)
384
384
  font.changed = changed
@@ -388,6 +388,7 @@ def _generatePostHook(font: fontforge.font, target: str):
388
388
  from fontforgeVF.utils import vfInfoExists
389
389
  from fontforgeVF.export import exportVariableFont
390
390
 
391
+ assert isinstance(font.temporary, dict)
391
392
  changed = font.changed
392
393
  if vfInfoExists(font):
393
394
  if 'generateVF' in font.temporary:
@@ -401,29 +402,11 @@ def _generatePostHook(font: fontforge.font, target: str):
401
402
  font.changed = changed
402
403
 
403
404
 
404
- def _addHook(
405
- font: fontforge.font,
406
- name: Literal['generateFontPreHook', 'generateFontPostHook'],
407
- hook: Callable[[fontforge.font, str], None]
408
- ):
409
- assert isinstance(font.temporary, dict)
410
- if name in font.temporary:
411
- currentHook = font.temporary[name]
412
-
413
- def chainHook(font: fontforge.font, target: str):
414
- currentHook(font, target)
415
- hook(font, target)
416
-
417
- font.temporary[name] = chainHook
418
- else:
419
- font.temporary[name] = hook
420
-
421
-
422
405
  def _addGenerateHook(font: fontforge.font):
423
406
  if not isinstance(font.temporary, dict):
424
407
  font.temporary = {}
425
- _addHook(font, 'generateFontPreHook', _generatePreHook)
426
- _addHook(font, 'generateFontPostHook', _generatePostHook)
408
+ addFontGenerateHook(font, 'generateFontPreHook', _generatePreHook)
409
+ addFontGenerateHook(font, 'generateFontPostHook', _generatePostHook)
427
410
 
428
411
 
429
412
  def _loadHook_ttf(font: fontforge.font):
@@ -442,13 +425,14 @@ def _loadHook_ttf(font: fontforge.font):
442
425
  "_Yes",
443
426
  "Open with _parameters",
444
427
  "_No",
445
- ]
428
+ ] # type: ignore
446
429
  )
447
430
  if ans == 0 or ans == 1:
448
431
  _selectInstanceDialog(font.path, ttf, ans)
449
432
  else:
450
433
  _selectInstanceDialog(font.path, ttf, 1)
451
434
  initPersistentDict(font)
435
+ assert isinstance(font.persistent, dict)
452
436
  vfData = _getVFData(ttf, {})
453
437
  font.persistent['VF'] = vfData
454
438
 
@@ -1,22 +1,10 @@
1
- import fontforge
2
- import re
3
1
  from os import PathLike
2
+ import re
4
3
 
5
-
6
- __all__ = [
7
- "intOrFloat",
8
- "initPersistentDict",
9
- "vfInfoExists",
10
- "getVFValue",
11
- "setVFValue",
12
- "deleteEmptyDicts",
13
- "deleteVFValue",
14
- "setOrDeleteVFValue",
15
- "checkExtensionTtfOrWoff2",
16
- ]
4
+ import fontforge
17
5
 
18
6
 
19
- def intOrFloat(val):
7
+ def intOrFloat(val: str | int | float):
20
8
  """Convert to ``int`` or ``float`` if possible
21
9
 
22
10
  If parameter represents an integer, returns it converted to ``int``
@@ -43,6 +31,33 @@ def intOrFloat(val):
43
31
  return f
44
32
 
45
33
 
34
+ def ensureList(obj) -> list:
35
+ if obj is None:
36
+ return []
37
+ elif isinstance(obj, list):
38
+ return obj
39
+ else:
40
+ return list(obj)
41
+
42
+
43
+ def ensureTuple(obj) -> tuple:
44
+ if obj is None:
45
+ return ()
46
+ elif isinstance(obj, tuple):
47
+ return obj
48
+ else:
49
+ return tuple(obj)
50
+
51
+
52
+ def ensureDict(obj) -> dict:
53
+ if obj is None:
54
+ return {}
55
+ elif isinstance(obj, dict):
56
+ return obj
57
+ else:
58
+ return dict(obj)
59
+
60
+
46
61
  def initPersistentDict(font: fontforge.font):
47
62
  """Make sure ``font.persistent`` is a ``dict``
48
63
 
@@ -135,6 +150,7 @@ def getVFValue(font: fontforge.font, key: str, default=None):
135
150
  if not vfInfoExists(font):
136
151
  return default
137
152
  else:
153
+ assert isinstance(font.persistent, dict)
138
154
  info = font.persistent["VF"]
139
155
  for part in _checkVFAddr(key):
140
156
  c, k = part
@@ -149,6 +165,34 @@ def getVFValue(font: fontforge.font, key: str, default=None):
149
165
  return info
150
166
 
151
167
 
168
+ def getVFValueAsList(font: fontforge.font, key: str, default: list = []) -> list:
169
+ """Gets a value from VF info
170
+
171
+ Same as ``getVFValue()`` but ensures returning a list
172
+
173
+ :param font: Fontforge font object
174
+ :param key: Name of key
175
+ :param default: Optional. Returns this value if ``key`` does not \
176
+ exist. Without this parameter defaults to ``[]``.
177
+ :return: the value for ``key``, or ``default`` if no such ``key``.
178
+ """
179
+ return ensureList(getVFValue(font, key, default))
180
+
181
+
182
+ def getVFValueAsDict(font: fontforge.font, key: str, default: dict = {}) -> dict:
183
+ """Gets a value from VF info
184
+
185
+ Same as ``getVFValue()`` but ensures returning a dict
186
+
187
+ :param font: Fontforge font object
188
+ :param key: Name of key
189
+ :param default: Optional. Returns this value if ``key`` does not \
190
+ exist. Without this parameter defaults to ``{}``.
191
+ :return: the value for ``key``, or ``default`` if no such ``key``.
192
+ """
193
+ return ensureDict(getVFValue(font, key, default))
194
+
195
+
152
196
  def _makeSureKeyExists(container, key):
153
197
  if isinstance(container, list):
154
198
  while len(container) <= key:
@@ -179,6 +223,7 @@ def setVFValue(font: fontforge.font, key: str, val):
179
223
  :raises ``RuntimeError``: user refused ``initPersistentDict``.
180
224
  """
181
225
  initPersistentDict(font)
226
+ assert isinstance(font.persistent, dict)
182
227
  parent = font.persistent
183
228
  info = parent["VF"]
184
229
  prevk = "VF"
@@ -200,7 +245,7 @@ def setVFValue(font: fontforge.font, key: str, val):
200
245
  parent[k] = val
201
246
 
202
247
 
203
- def _deleteEmptyLists(d: dict):
248
+ def _deleteEmptyLists(d: dict | list):
204
249
  if isinstance(d, list):
205
250
  for i in range(len(d)):
206
251
  if isinstance(d[i], dict):
@@ -253,6 +298,7 @@ def deleteVFValue(font: fontforge.font, key: str) -> bool:
253
298
  :return: ``True`` if the key was deleted, ``False`` otherwise.
254
299
  """
255
300
  if vfInfoExists(font):
301
+ assert isinstance(font.persistent, dict)
256
302
  parent = font.persistent
257
303
  info = parent["VF"]
258
304
  for part in _checkVFAddr(key):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fontforge_variable_font
3
- Version: 0.3.0
3
+ Version: 0.4.1
4
4
  Summary: A FontForge_plugin to create a variable font
5
5
  Home-page: https://github.com/MihailJP/fontforge-variable-font
6
6
  Author: MihailJP
@@ -17,6 +17,7 @@ Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Requires-Dist: fonttools
19
19
  Requires-Dist: fontmake
20
+ Requires-Dist: fontforge_plugin_helper
20
21
  Dynamic: license-file
21
22
 
22
23
  Fontforge Variable Font Plugin
@@ -0,0 +1,3 @@
1
+ fonttools
2
+ fontmake
3
+ fontforge_plugin_helper
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = fontforge_variable_font
3
- version = 0.3.0
3
+ version = 0.4.1
4
4
  author = MihailJP
5
5
  author_email = mihailjp@gmail.com
6
6
  description = A FontForge_plugin to create a variable font
@@ -24,6 +24,7 @@ python_requires = >=3.10
24
24
  install_requires =
25
25
  fonttools
26
26
  fontmake
27
+ fontforge_plugin_helper
27
28
 
28
29
  [options.entry_points]
29
30
  fontforge_plugin =
@@ -1,5 +1,6 @@
1
- import pytest
2
1
  import fontforge
2
+ import pytest
3
+
3
4
  from fontforgeVF.utils import setVFValue
4
5
 
5
6
 
@@ -1,6 +0,0 @@
1
- from fontforgeVF.delete import *
2
- from fontforgeVF.design_axes import *
3
- from fontforgeVF.export import *
4
- from fontforgeVF.language import *
5
- from fontforgeVF.load import *
6
- from fontforgeVF.utils import *
@@ -1,2 +0,0 @@
1
- fonttools
2
- fontmake
@@ -1,5 +1,5 @@
1
- import pytest
2
1
  import fontforge
2
+ import pytest
3
3
 
4
4
 
5
5
  def axisTestFont(useDefault):
@@ -1,5 +1,5 @@
1
- import pytest
2
1
  import fontforge
2
+ import pytest
3
3
 
4
4
 
5
5
  @pytest.mark.parametrize(('param', 'expectedVal', 'expectedType'), [