pax_global_header 0000666 0000000 0000000 00000000064 14724542715 0014525 g ustar 00root root 0000000 0000000 52 comment=6f2a15fe3316685a54ec8809b95ccc823b6e88da
ufoProcessor-1.13.3/ 0000775 0000000 0000000 00000000000 14724542715 0014303 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/.github/ 0000775 0000000 0000000 00000000000 14724542715 0015643 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/.github/workflows/ 0000775 0000000 0000000 00000000000 14724542715 0017700 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/.github/workflows/release.yml 0000664 0000000 0000000 00000002645 14724542715 0022052 0 ustar 00root root 0000000 0000000 # This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Upload Python Package
on:
push:
tags:
- '\d+\.\d+\.[0-9a-z]+'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
permissions:
contents: read
jobs:
release-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Build release distributions
run: |
# NOTE: put your own distribution build steps here.
python -m pip install build
python -m build
- name: Upload distributions
uses: actions/upload-artifact@v4
with:
name: release-dists
path: dist/
pypi-publish:
name: upload release to PyPI
runs-on: ubuntu-latest
environment: release
needs:
- release-build
permissions:
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write
contents: write
steps:
- name: Retrieve release distributions
uses: actions/download-artifact@v4
with:
name: release-dists
path: dist/
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
ufoProcessor-1.13.3/.gitignore 0000664 0000000 0000000 00000001740 14724542715 0016275 0 ustar 00root root 0000000 0000000 # Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*.idea
*.log
# Distribution / Packaging
*.egg-info
*.eggs
build
dist
# Unit test / coverage files
.coverage
.coverage.*
.pytest_cache
.tox/
htmlcov/
# auto-generated version file
Lib/ufoProcessor/_version.py
Lib/ufoProcessor/automatic_testfonts
Lib/ufoProcessor/automatic_testfonts_mutator
Lib/ufoProcessor/automatic_testfonts_varlib
Tests/automatic_testfonts_mutator
Tests/automatic_testfonts_mutator_defcon
Tests/automatic_testfonts_mutator_fontparts
Tests/automatic_testfonts_varlib
Tests/automatic_testfonts_varlib_defcon
Tests/automatic_testfonts_varlib_fontparts
Tests/automatic_testfonts*
test*.designspace
test*.sp3
Tests/Instances
Tests/_*
Tests/20190830 benders/Skateboard Previews
/Tests/202206 discrete spaces/instances
/Tests/202206 discrete spaces/instances_mutMath
/Tests/202206 discrete spaces/instances_varlib
/_issues
/_old_stuff
/Tests/ds5/instances
/Tests/ds5/ds5_log.txt
Tests/ds5/makeOneInstanceOutput*
ufoProcessor-1.13.3/Designspaces with python.md 0000664 0000000 0000000 00000010352 14724542715 0021474 0 ustar 00root root 0000000 0000000 # Designspaces and python
Designspaces can do different things in different processes. Maybe you want to generate a variable font. Maybe you want to generate UFOs. Maybe you want to resample an existing designspace into something else.
While [fonttools.designspacelib](https://fonttools.readthedocs.io/en/latest/designspaceLib/index.html) contains the basic objects to construct, read and write designspaces, the [ufoProcessor package](https://github.com/LettError/ufoProcessor) can also generate instances.
## Basics
First I have to make a `DesignSpaceDocument` object. This is an empty container, it has no masters, no axes, no path.
from fontTools.designspaceLib import *
ds = DesignSpaceDocument()
Now I will add an axis to the document by making an `AxisDescriptor` object and adding some values to its attributes.
ad = AxisDescriptor()
ad.name = "weight" # readable name
ad.tag = "wght" # 4 letter tag
ad.minimum = 200
ad.maximum = 1000
ad.default = 400
Finally we add the axisDescriptor to the document:
ds.addAxis(ad)
print(ds)
path = "my.designspace"
ds.write(path)
This writes a very small designspace file:
Let's add some sources to the designspace: this needs the absolute path to the file (usually a ufo). When the document is saved the paths will be written as relative to the designspace document. A `SourceDescriptor` object has a lot of attributes, but `path` and `location` are the most important ones.
s1 = SourceDescriptor()
s1.path = "geometryMaster1.ufo"
s1.location = dict(weight=200)
ds.addSource(s1)
s2 = SourceDescriptor()
s2.path = "geometryMaster2.ufo"
s2.location = dict(weight=1000)
ds.addSource(s2)
Let's add some instances. Instances are specific locations in the designspace with names and sometimes paths associated with them. In a variable font you might want these to show up as styles in a menu. But you could also generate UFOs from them.
for w in [ad.minimum, .5*(ad.minimum + ad.default), ad.default, .5*(ad.maximum + ad.default), ad.maximum]:
# you will probably know more compact
# and easier ways to write this, go ahead!
i = InstanceDescriptor()
i.fileName = "InstanceFamily"
i.styleName = "Weight_%d" % w
i.location = dict(weight = w)
i.filename = "instance_%s.ufo" % i.styleName
ds.addInstance(i)
The XML now has all it needs: an axis, some sources and ome instances.
Whoop well done.
ufoProcessor-1.13.3/LICENSE 0000664 0000000 0000000 00000002115 14724542715 0015307 0 ustar 00root root 0000000 0000000 Copyright (c) 2017-2018 LettError and Erik van Blokland
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
ufoProcessor-1.13.3/Lib/ 0000775 0000000 0000000 00000000000 14724542715 0015011 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Lib/ufoProcessor/ 0000775 0000000 0000000 00000000000 14724542715 0017502 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Lib/ufoProcessor/__init__.py 0000664 0000000 0000000 00000121021 14724542715 0021610 0 ustar 00root root 0000000 0000000 # coding: utf-8
from __future__ import print_function, division, absolute_import
import os
import logging, traceback
import collections
# from pprint import pprint
from fontTools.designspaceLib import DesignSpaceDocument, SourceDescriptor, InstanceDescriptor, AxisDescriptor, RuleDescriptor, processRules
from fontTools.misc import plistlib
from fontTools.ufoLib import fontInfoAttributesVersion1, fontInfoAttributesVersion2, fontInfoAttributesVersion3
from fontTools.varLib.models import VariationModel, normalizeLocation
import defcon
import fontParts.fontshell.font
import defcon.objects.font
from defcon.objects.font import Font
from defcon.pens.transformPointPen import TransformPointPen
from defcon.objects.component import _defaultTransformation
from fontMath.mathGlyph import MathGlyph
from fontMath.mathInfo import MathInfo
from fontMath.mathKerning import MathKerning
# if you only intend to use varLib.model then importing mutatorMath is not necessary.
from mutatorMath.objects.mutator import buildMutator
from mutatorMath.objects.location import Location
from ufoProcessor.varModels import VariationModelMutator
from ufoProcessor.emptyPen import checkGlyphIsEmpty, DecomposePointPen
try:
from ._version import version as __version__
except ImportError:
__version__ = "0.0.0+unknown"
class UFOProcessorError(Exception):
def __init__(self, msg, obj=None):
self.msg = msg
self.obj = obj
def __str__(self):
return repr(self.msg) + repr(self.obj)
def getDefaultLayerName(f):
# get the name of the default layer from a defcon font and from a fontparts font
if issubclass(type(f), defcon.objects.font.Font):
return f.layers.defaultLayer.name
elif issubclass(type(f), fontParts.fontshell.font.RFont):
return f.defaultLayer.name
return None
def getLayer(f, layerName):
# get the layer from a defcon font and from a fontparts font
if issubclass(type(f), defcon.objects.font.Font):
if layerName in f.layers:
return f.layers[layerName]
elif issubclass(type(f), fontParts.fontshell.font.RFont):
if layerName in f.layerOrder:
return f.getLayer(layerName)
return None
"""
Processing of rules when generating UFOs.
Swap the contents of two glyphs.
- contours
- components
- width
- group membership
- kerning
+ Remap components so that glyphs that reference either of the swapped glyphs maintain appearance
+ Keep the unicode value of the original glyph.
Notes
Parking the glyphs under a swapname is a bit lazy, but at least it guarantees the glyphs have the right parent.
"""
"""
build() is a convenience function for reading and executing a designspace file.
documentPath: path to the designspace file.
outputUFOFormatVersion: integer, 2, 3. Format for generated UFOs. Note: can be different from source UFO format.
useVarlib: True if you want the geometry to be generated with varLib.model instead of mutatorMath.
"""
def build(
documentPath,
outputUFOFormatVersion=3,
roundGeometry=True,
verbose=True, # not supported
logPath=None, # not supported
progressFunc=None, # not supported
processRules=True,
logger=None,
useVarlib=False,
):
"""
Simple builder for UFO designspaces.
"""
import os, glob
if os.path.isdir(documentPath):
# process all *.designspace documents in this folder
todo = glob.glob(os.path.join(documentPath, "*.designspace"))
else:
# process the
todo = [documentPath]
results = []
for path in todo:
document = DesignSpaceProcessor(ufoVersion=outputUFOFormatVersion)
document.useVarlib = useVarlib
document.roundGeometry = roundGeometry
document.read(path)
try:
r = document.generateUFO(processRules=processRules)
results.append(r)
except:
if logger:
logger.exception("ufoProcessor error")
reader = None
return results
def getUFOVersion(ufoPath):
# Peek into a ufo to read its format version.
#
#
#
#
# creator
# org.robofab.ufoLib
# formatVersion
# 2
#
#
metaInfoPath = os.path.join(ufoPath, "metainfo.plist")
with open(metaInfoPath, 'rb') as f:
p = plistlib.load(f)
return p.get('formatVersion')
def swapGlyphNames(font, oldName, newName, swapNameExtension = "_______________swap"):
# In font swap the glyphs oldName and newName.
# Also swap the names in components in order to preserve appearance.
# Also swap the names in font groups.
if not oldName in font or not newName in font:
return None
swapName = oldName + swapNameExtension
# park the old glyph
if not swapName in font:
font.newGlyph(swapName)
# get anchors
oldAnchors = font[oldName].anchors
newAnchors = font[newName].anchors
# swap the outlines
font[swapName].clear()
p = font[swapName].getPointPen()
font[oldName].drawPoints(p)
font[swapName].width = font[oldName].width
# lib?
font[oldName].clear()
p = font[oldName].getPointPen()
font[newName].drawPoints(p)
font[oldName].width = font[newName].width
for a in newAnchors:
na = defcon.Anchor()
na.name = a.name
na.x = a.x
na.y = a.y
# FontParts and Defcon add anchors in different ways
# this works around that.
try:
font[oldName].naked().appendAnchor(na)
except AttributeError:
font[oldName].appendAnchor(na)
font[newName].clear()
p = font[newName].getPointPen()
font[swapName].drawPoints(p)
font[newName].width = font[swapName].width
for a in oldAnchors:
na = defcon.Anchor()
na.name = a.name
na.x = a.x
na.y = a.y
try:
font[newName].naked().appendAnchor(na)
except AttributeError:
font[newName].appendAnchor(na)
# remap the components
for g in font:
for c in g.components:
if c.baseGlyph == oldName:
c.baseGlyph = swapName
continue
for g in font:
for c in g.components:
if c.baseGlyph == newName:
c.baseGlyph = oldName
continue
for g in font:
for c in g.components:
if c.baseGlyph == swapName:
c.baseGlyph = newName
# change the names in groups
# the shapes will swap, that will invalidate the kerning
# so the names need to swap in the kerning as well.
newKerning = {}
for first, second in font.kerning.keys():
value = font.kerning[(first,second)]
if first == oldName:
first = newName
elif first == newName:
first = oldName
if second == oldName:
second = newName
elif second == newName:
second = oldName
newKerning[(first, second)] = value
font.kerning.clear()
font.kerning.update(newKerning)
for groupName, members in font.groups.items():
newMembers = []
for name in members:
if name == oldName:
newMembers.append(newName)
elif name == newName:
newMembers.append(oldName)
else:
newMembers.append(name)
font.groups[groupName] = newMembers
remove = []
for g in font:
if g.name.find(swapNameExtension)!=-1:
remove.append(g.name)
for r in remove:
del font[r]
class DesignSpaceProcessor(DesignSpaceDocument):
"""
A subclassed DesignSpaceDocument that can
- process the document and generate finished UFOs with MutatorMath or varLib.model.
- read and write documents
- Replacement for the mutatorMath.ufo generator.
"""
fontClass = defcon.Font
layerClass = defcon.Layer
glyphClass = defcon.Glyph
libClass = defcon.Lib
glyphContourClass = defcon.Contour
glyphPointClass = defcon.Point
glyphComponentClass = defcon.Component
glyphAnchorClass = defcon.Anchor
kerningClass = defcon.Kerning
groupsClass = defcon.Groups
infoClass = defcon.Info
featuresClass = defcon.Features
mathInfoClass = MathInfo
mathGlyphClass = MathGlyph
mathKerningClass = MathKerning
def __init__(self, readerClass=None, writerClass=None, fontClass=None, ufoVersion=3, useVarlib=False):
super(DesignSpaceProcessor, self).__init__(readerClass=readerClass, writerClass=writerClass)
self.ufoVersion = ufoVersion # target UFO version
self.useVarlib = useVarlib
self.roundGeometry = False
self._glyphMutators = {}
self._infoMutator = None
self._kerningMutator = None
self._kerningMutatorPairs = None
self.fonts = {}
self._fontsLoaded = False
self.mutedAxisNames = None # list of axisname that need to be muted
self.glyphNames = [] # list of all glyphnames
self.processRules = True
self.problems = [] # receptacle for problem notifications. Not big enough to break, but also not small enough to ignore.
self.toolLog = []
def generateUFO(self, processRules=True, glyphNames=None, pairs=None, bend=False):
# makes the instances
# option to execute the rules
# make sure we're not trying to overwrite a newer UFO format
self.loadFonts()
self.findDefault()
if self.default is None:
# we need one to genenerate
raise UFOProcessorError("Can't generate UFO from this designspace: no default font.", self)
v = 0
for instanceDescriptor in self.instances:
if instanceDescriptor.path is None:
continue
font = self.makeInstance(instanceDescriptor,
processRules,
glyphNames=glyphNames,
pairs=pairs,
bend=bend)
folder = os.path.dirname(os.path.abspath(instanceDescriptor.path))
path = instanceDescriptor.path
if not os.path.exists(folder):
os.makedirs(folder)
if os.path.exists(path):
existingUFOFormatVersion = getUFOVersion(path)
if existingUFOFormatVersion > self.ufoVersion:
self.problems.append("Can’t overwrite existing UFO%d with UFO%d." % (existingUFOFormatVersion, self.ufoVersion))
continue
font.save(path, self.ufoVersion)
self.problems.append("Generated %s as UFO%d"%(os.path.basename(path), self.ufoVersion))
return True
def getSerializedAxes(self):
return [a.serialize() for a in self.axes]
def getMutatorAxes(self):
# map the axis values?
d = collections.OrderedDict()
for a in self.axes:
d[a.name] = a.serialize()
return d
def _getAxisOrder(self):
return [a.name for a in self.axes]
axisOrder = property(_getAxisOrder, doc="get the axis order from the axis descriptors")
serializedAxes = property(getSerializedAxes, doc="a list of dicts with the axis values")
def getVariationModel(self, items, axes, bias=None):
# Return either a mutatorMath or a varlib.model object for calculating.
try:
if self.useVarlib:
# use the varlib variation model
try:
return dict(), VariationModelMutator(items, self.axes)
except (KeyError, AssertionError):
error = traceback.format_exc()
self.toolLog.append("UFOProcessor.getVariationModel error: %s" % error)
self.toolLog.append(items)
return {}, None
else:
# use mutatormath model
axesForMutator = self.getMutatorAxes()
return buildMutator(items, axes=axesForMutator, bias=bias)
except:
error = traceback.format_exc()
self.toolLog.append("UFOProcessor.getVariationModel error: %s" % error)
return {}, None
def getInfoMutator(self):
""" Returns a info mutator """
if self._infoMutator:
return self._infoMutator
infoItems = []
for sourceDescriptor in self.sources:
if sourceDescriptor.layerName is not None:
continue
loc = Location(sourceDescriptor.location)
sourceFont = self.fonts[sourceDescriptor.name]
if sourceFont is None:
continue
if hasattr(sourceFont.info, "toMathInfo"):
infoItems.append((loc, sourceFont.info.toMathInfo()))
else:
infoItems.append((loc, self.mathInfoClass(sourceFont.info)))
infoBias = self.newDefaultLocation(bend=True)
bias, self._infoMutator = self.getVariationModel(infoItems, axes=self.serializedAxes, bias=infoBias)
return self._infoMutator
def getKerningMutator(self, pairs=None):
""" Return a kerning mutator, collect the sources, build mathGlyphs.
If no pairs are given: calculate the whole table.
If pairs are given then query the sources for a value and make a mutator only with those values.
"""
if self._kerningMutator and pairs == self._kerningMutatorPairs:
return self._kerningMutator
kerningItems = []
foregroundLayers = [None, 'foreground', 'public.default']
if pairs is None:
for sourceDescriptor in self.sources:
if sourceDescriptor.layerName not in foregroundLayers:
continue
if not sourceDescriptor.muteKerning:
loc = Location(sourceDescriptor.location)
sourceFont = self.fonts[sourceDescriptor.name]
if sourceFont is None: continue
# this makes assumptions about the groups of all sources being the same.
kerningItems.append((loc, self.mathKerningClass(sourceFont.kerning, sourceFont.groups)))
else:
self._kerningMutatorPairs = pairs
for sourceDescriptor in self.sources:
# XXX check sourceDescriptor layerName, only foreground should contribute
if sourceDescriptor.layerName is not None:
continue
if not os.path.exists(sourceDescriptor.path):
continue
if not sourceDescriptor.muteKerning:
sourceFont = self.fonts[sourceDescriptor.name]
if sourceFont is None:
continue
loc = Location(sourceDescriptor.location)
# XXX can we get the kern value from the fontparts kerning object?
kerningItem = self.mathKerningClass(sourceFont.kerning, sourceFont.groups)
if kerningItem is not None:
sparseKerning = {}
for pair in pairs:
v = kerningItem.get(pair)
if v is not None:
sparseKerning[pair] = v
kerningItems.append((loc, self.mathKerningClass(sparseKerning)))
kerningBias = self.newDefaultLocation(bend=True)
bias, self._kerningMutator = self.getVariationModel(kerningItems, axes=self.serializedAxes, bias=kerningBias)
return self._kerningMutator
def filterThisLocation(self, location, mutedAxes):
# return location with axes is mutedAxes removed
# this means checking if the location is a non-default value
if not mutedAxes:
return False, location
defaults = {}
ignoreMaster = False
for aD in self.axes:
defaults[aD.name] = aD.default
new = {}
new.update(location)
for mutedAxisName in mutedAxes:
if mutedAxisName not in location:
continue
if mutedAxisName not in defaults:
continue
if location[mutedAxisName] != defaults.get(mutedAxisName):
ignoreMaster = True
del new[mutedAxisName]
return ignoreMaster, new
def getGlyphMutator(self, glyphName,
decomposeComponents=False,
fromCache=None):
# make a mutator / varlib object for glyphName.
cacheKey = (glyphName, decomposeComponents)
if cacheKey in self._glyphMutators and fromCache:
return self._glyphMutators[cacheKey]
items = self.collectMastersForGlyph(glyphName, decomposeComponents=decomposeComponents)
new = []
for a, b, c in items:
if hasattr(b, "toMathGlyph"):
# note: calling toMathGlyph ignores the mathGlyphClass preference
# maybe the self.mathGlyphClass is not necessary?
new.append((a,b.toMathGlyph()))
else:
new.append((a,self.mathGlyphClass(b)))
thing = None
try:
bias, thing = self.getVariationModel(new, axes=self.serializedAxes, bias=self.newDefaultLocation(bend=True)) #xx
except TypeError:
self.toolLog.append("getGlyphMutator %s items: %s new: %s" % (glyphName, items, new))
self.problems.append("\tCan't make processor for glyph %s" % (glyphName))
if thing is not None:
self._glyphMutators[cacheKey] = thing
return thing
def collectMastersForGlyph(self, glyphName, decomposeComponents=False):
""" Return a glyph mutator.defaultLoc
decomposeComponents = True causes the source glyphs to be decomposed first
before building the mutator. That gives you instances that do not depend
on a complete font. If you're calculating previews for instance.
XXX check glyphs in layers
"""
items = []
empties = []
foundEmpty = False
for sourceDescriptor in self.sources:
if not os.path.exists(sourceDescriptor.path):
#kthxbai
p = "\tMissing UFO at %s" % sourceDescriptor.path
if p not in self.problems:
self.problems.append(p)
continue
if glyphName in sourceDescriptor.mutedGlyphNames:
continue
thisIsDefault = self.default == sourceDescriptor
ignoreMaster, filteredLocation = self.filterThisLocation(sourceDescriptor.location, self.mutedAxisNames)
if ignoreMaster:
continue
f = self.fonts.get(sourceDescriptor.name)
if f is None: continue
loc = Location(sourceDescriptor.location)
sourceLayer = f
if not glyphName in f:
# log this>
continue
layerName = getDefaultLayerName(f)
sourceGlyphObject = None
# handle source layers
if sourceDescriptor.layerName is not None:
# start looking for a layer
# Do not bother for mutatorMath designspaces
layerName = sourceDescriptor.layerName
sourceLayer = getLayer(f, sourceDescriptor.layerName)
if sourceLayer is None:
continue
if glyphName not in sourceLayer:
# start looking for a glyph
# this might be a support in a sparse layer
# so we're skipping!
continue
# still have to check if the sourcelayer glyph is empty
if not glyphName in sourceLayer:
continue
else:
sourceGlyphObject = sourceLayer[glyphName]
if checkGlyphIsEmpty(sourceGlyphObject, allowWhiteSpace=True):
foundEmpty = True
#sourceGlyphObject = None
#continue
if decomposeComponents:
# what about decomposing glyphs in a partial font?
temp = self.glyphClass()
p = temp.getPointPen()
dpp = DecomposePointPen(sourceLayer, p)
sourceGlyphObject.drawPoints(dpp)
temp.width = sourceGlyphObject.width
temp.name = sourceGlyphObject.name
processThis = temp
else:
processThis = sourceGlyphObject
sourceInfo = dict(source=f.path, glyphName=glyphName,
layerName=layerName,
location=filteredLocation, # sourceDescriptor.location,
sourceName=sourceDescriptor.name,
)
if hasattr(processThis, "toMathGlyph"):
processThis = processThis.toMathGlyph()
else:
processThis = self.mathGlyphClass(processThis)
items.append((loc, processThis, sourceInfo))
empties.append((thisIsDefault, foundEmpty))
# check the empties:
# if the default glyph is empty, then all must be empty
# if the default glyph is not empty then none can be empty
checkedItems = []
emptiesAllowed = False
# first check if the default is empty.
# remember that the sources can be in any order
for i, p in enumerate(empties):
isDefault, isEmpty = p
if isDefault and isEmpty:
emptiesAllowed = True
# now we know what to look for
if not emptiesAllowed:
for i, p in enumerate(empties):
isDefault, isEmpty = p
if not isEmpty:
checkedItems.append(items[i])
else:
for i, p in enumerate(empties):
isDefault, isEmpty = p
if isEmpty:
checkedItems.append(items[i])
return checkedItems
def getNeutralFont(self):
# Return a font object for the neutral font
# self.fonts[self.default.name] ?
neutralLoc = self.newDefaultLocation(bend=True)
for sd in self.sources:
if sd.location == neutralLoc:
if sd.name in self.fonts:
#candidate = self.fonts[sd.name]
#if sd.layerName:
# if sd.layerName in candidate.layers:
return self.fonts[sd.name]
return None
def findDefault(self):
"""Set and return SourceDescriptor at the default location or None.
The default location is the set of all `default` values in user space of all axes.
"""
self.default = None
# Convert the default location from user space to design space before comparing
# it against the SourceDescriptor locations (always in design space).
default_location_design = self.newDefaultLocation(bend=True)
for sourceDescriptor in self.sources:
if sourceDescriptor.location == default_location_design:
self.default = sourceDescriptor
return sourceDescriptor
return None
def newDefaultLocation(self, bend=False):
# overwrite from fontTools.newDefaultLocation
# we do not want this default location to be mapped.
loc = collections.OrderedDict()
for axisDescriptor in self.axes:
if bend:
loc[axisDescriptor.name] = axisDescriptor.map_forward(
axisDescriptor.default
)
else:
loc[axisDescriptor.name] = axisDescriptor.default
return loc
def loadFonts(self, reload=False):
# Load the fonts and find the default candidate based on the info flag
if self._fontsLoaded and not reload:
return
names = set()
for i, sourceDescriptor in enumerate(self.sources):
if sourceDescriptor.name is None:
# make sure it has a unique name
sourceDescriptor.name = "master.%d" % i
if sourceDescriptor.name not in self.fonts:
if os.path.exists(sourceDescriptor.path):
self.fonts[sourceDescriptor.name] = self._instantiateFont(sourceDescriptor.path)
self.problems.append("loaded master from %s, layer %s, format %d" % (sourceDescriptor.path, sourceDescriptor.layerName, getUFOVersion(sourceDescriptor.path)))
names |= set(self.fonts[sourceDescriptor.name].keys())
else:
self.fonts[sourceDescriptor.name] = None
self.problems.append("source ufo not found at %s" % (sourceDescriptor.path))
self.glyphNames = list(names)
self._fontsLoaded = True
def getFonts(self):
# returnn a list of (font object, location) tuples
fonts = []
for sourceDescriptor in self.sources:
f = self.fonts.get(sourceDescriptor.name)
if f is not None:
fonts.append((f, sourceDescriptor.location))
return fonts
def makeInstance(self, instanceDescriptor,
doRules=False,
glyphNames=None,
pairs=None,
bend=False):
""" Generate a font object for this instance """
font = self._instantiateFont(None)
# make fonty things here
loc = Location(instanceDescriptor.location)
anisotropic = False
locHorizontal = locVertical = loc
if self.isAnisotropic(loc):
anisotropic = True
locHorizontal, locVertical = self.splitAnisotropic(loc)
# groups
renameMap = getattr(self.fonts[self.default.name], "kerningGroupConversionRenameMaps", None)
font.kerningGroupConversionRenameMaps = renameMap if renameMap is not None else {'side1': {}, 'side2': {}}
# make the kerning
# this kerning is always horizontal. We can take the horizontal location
# filter the available pairs?
if instanceDescriptor.kerning:
if pairs:
try:
kerningMutator = self.getKerningMutator(pairs=pairs)
kerningObject = kerningMutator.makeInstance(locHorizontal, bend=bend)
kerningObject.extractKerning(font)
except:
self.problems.append("Could not make kerning for %s. %s" % (loc, traceback.format_exc()))
else:
kerningMutator = self.getKerningMutator()
if kerningMutator is not None:
kerningObject = kerningMutator.makeInstance(locHorizontal, bend=bend)
kerningObject.extractKerning(font)
# make the info
try:
infoMutator = self.getInfoMutator()
if infoMutator is not None:
if not anisotropic:
infoInstanceObject = infoMutator.makeInstance(loc, bend=bend)
else:
horizontalInfoInstanceObject = infoMutator.makeInstance(locHorizontal, bend=bend)
verticalInfoInstanceObject = infoMutator.makeInstance(locVertical, bend=bend)
# merge them again
infoInstanceObject = (1,0)*horizontalInfoInstanceObject + (0,1)*verticalInfoInstanceObject
if self.roundGeometry:
try:
infoInstanceObject = infoInstanceObject.round()
except AttributeError:
pass
infoInstanceObject.extractInfo(font.info)
font.info.familyName = instanceDescriptor.familyName
font.info.styleName = instanceDescriptor.styleName
font.info.postscriptFontName = instanceDescriptor.postScriptFontName # yikes, note the differences in capitalisation..
font.info.styleMapFamilyName = instanceDescriptor.styleMapFamilyName
font.info.styleMapStyleName = instanceDescriptor.styleMapStyleName
# NEED SOME HELP WITH THIS
# localised names need to go to the right openTypeNameRecords
# records = []
# nameID = 1
# platformID =
# for languageCode, name in instanceDescriptor.localisedStyleMapFamilyName.items():
# # Name ID 1 (font family name) is found at the generic styleMapFamily attribute.
# records.append((nameID, ))
except:
self.problems.append("Could not make fontinfo for %s. %s" % (loc, traceback.format_exc()))
for sourceDescriptor in self.sources:
if sourceDescriptor.copyInfo:
# this is the source
if self.fonts[sourceDescriptor.name] is not None:
self._copyFontInfo(self.fonts[sourceDescriptor.name].info, font.info)
if sourceDescriptor.copyLib:
# excplicitly copy the font.lib items
if self.fonts[sourceDescriptor.name] is not None:
for key, value in self.fonts[sourceDescriptor.name].lib.items():
font.lib[key] = value
if sourceDescriptor.copyGroups:
if self.fonts[sourceDescriptor.name] is not None:
sides = font.kerningGroupConversionRenameMaps.get('side1', {})
sides.update(font.kerningGroupConversionRenameMaps.get('side2', {}))
for key, value in self.fonts[sourceDescriptor.name].groups.items():
if key not in sides:
font.groups[key] = value
if sourceDescriptor.copyFeatures:
if self.fonts[sourceDescriptor.name] is not None:
featuresText = self.fonts[sourceDescriptor.name].features.text
font.features.text = featuresText
# glyphs
if glyphNames:
selectedGlyphNames = glyphNames
else:
selectedGlyphNames = self.glyphNames
# add the glyphnames to the font.lib['public.glyphOrder']
if not 'public.glyphOrder' in font.lib.keys():
font.lib['public.glyphOrder'] = selectedGlyphNames
for glyphName in selectedGlyphNames:
try:
glyphMutator = self.getGlyphMutator(glyphName)
if glyphMutator is None:
self.problems.append("Could not make mutator for glyph %s" % (glyphName))
continue
except:
self.problems.append("Could not make mutator for glyph %s %s" % (glyphName, traceback.format_exc()))
continue
if glyphName in instanceDescriptor.glyphs.keys():
# XXX this should be able to go now that we have full rule support.
# reminder: this is what the glyphData can look like
# {'instanceLocation': {'custom': 0.0, 'weight': 824.0},
# 'masters': [{'font': 'master.Adobe VF Prototype.Master_0.0',
# 'glyphName': 'dollar.nostroke',
# 'location': {'custom': 0.0, 'weight': 0.0}},
# {'font': 'master.Adobe VF Prototype.Master_1.1',
# 'glyphName': 'dollar.nostroke',
# 'location': {'custom': 0.0, 'weight': 368.0}},
# {'font': 'master.Adobe VF Prototype.Master_2.2',
# 'glyphName': 'dollar.nostroke',
# 'location': {'custom': 0.0, 'weight': 1000.0}},
# {'font': 'master.Adobe VF Prototype.Master_3.3',
# 'glyphName': 'dollar.nostroke',
# 'location': {'custom': 100.0, 'weight': 1000.0}},
# {'font': 'master.Adobe VF Prototype.Master_0.4',
# 'glyphName': 'dollar.nostroke',
# 'location': {'custom': 100.0, 'weight': 0.0}},
# {'font': 'master.Adobe VF Prototype.Master_4.5',
# 'glyphName': 'dollar.nostroke',
# 'location': {'custom': 100.0, 'weight': 368.0}}],
# 'unicodes': [36]}
glyphData = instanceDescriptor.glyphs[glyphName]
else:
glyphData = {}
font.newGlyph(glyphName)
font[glyphName].clear()
if glyphData.get('mute', False):
# mute this glyph, skip
continue
glyphInstanceLocation = glyphData.get("instanceLocation", instanceDescriptor.location)
glyphInstanceLocation = Location(glyphInstanceLocation)
uniValues = []
neutral = glyphMutator.get(())
if neutral is not None:
uniValues = neutral[0].unicodes
else:
neutralFont = self.getNeutralFont()
if glyphName in neutralFont:
uniValues = neutralFont[glyphName].unicodes
glyphInstanceUnicodes = glyphData.get("unicodes", uniValues)
note = glyphData.get("note")
if note:
font[glyphName] = note
# XXXX phase out support for instance-specific masters
# this should be handled by the rules system.
masters = glyphData.get("masters", None)
if masters is not None:
items = []
for glyphMaster in masters:
sourceGlyphFont = glyphMaster.get("font")
sourceGlyphName = glyphMaster.get("glyphName", glyphName)
m = self.fonts.get(sourceGlyphFont)
if not sourceGlyphName in m:
continue
if hasattr(m[sourceGlyphName], "toMathGlyph"):
sourceGlyph = m[sourceGlyphName].toMathGlyph()
else:
sourceGlyph = MathGlyph(m[sourceGlyphName])
sourceGlyphLocation = glyphMaster.get("location")
items.append((Location(sourceGlyphLocation), sourceGlyph))
bias, glyphMutator = self.getVariationModel(items, axes=self.serializedAxes, bias=self.newDefaultLocation(bend=True))
try:
if not self.isAnisotropic(glyphInstanceLocation):
glyphInstanceObject = glyphMutator.makeInstance(glyphInstanceLocation, bend=bend)
else:
# split anisotropic location into horizontal and vertical components
horizontal, vertical = self.splitAnisotropic(glyphInstanceLocation)
horizontalGlyphInstanceObject = glyphMutator.makeInstance(horizontal, bend=bend)
verticalGlyphInstanceObject = glyphMutator.makeInstance(vertical, bend=bend)
# merge them again
glyphInstanceObject = (1,0)*horizontalGlyphInstanceObject + (0,1)*verticalGlyphInstanceObject
except IndexError:
# alignment problem with the data?
self.problems.append("Quite possibly some sort of data alignment error in %s" % glyphName)
continue
font.newGlyph(glyphName)
font[glyphName].clear()
if self.roundGeometry:
try:
glyphInstanceObject = glyphInstanceObject.round()
except AttributeError:
pass
try:
# File "/Users/erik/code/ufoProcessor/Lib/ufoProcessor/__init__.py", line 649, in makeInstance
# glyphInstanceObject.extractGlyph(font[glyphName], onlyGeometry=True)
# File "/Applications/RoboFont.app/Contents/Resources/lib/python3.6/fontMath/mathGlyph.py", line 315, in extractGlyph
# glyph.anchors = [dict(anchor) for anchor in self.anchors]
# File "/Applications/RoboFont.app/Contents/Resources/lib/python3.6/fontParts/base/base.py", line 103, in __set__
# raise FontPartsError("no setter for %r" % self.name)
# fontParts.base.errors.FontPartsError: no setter for 'anchors'
if hasattr(font[glyphName], "fromMathGlyph"):
font[glyphName].fromMathGlyph(glyphInstanceObject)
else:
glyphInstanceObject.extractGlyph(font[glyphName], onlyGeometry=True)
except TypeError:
# this causes ruled glyphs to end up in the wrong glyphname
# but defcon2 objects don't support it
pPen = font[glyphName].getPointPen()
font[glyphName].clear()
glyphInstanceObject.drawPoints(pPen)
font[glyphName].width = glyphInstanceObject.width
font[glyphName].unicodes = glyphInstanceUnicodes
if doRules:
resultNames = processRules(self.rules, loc, self.glyphNames)
for oldName, newName in zip(self.glyphNames, resultNames):
if oldName != newName:
swapGlyphNames(font, oldName, newName)
# copy the glyph lib?
#for sourceDescriptor in self.sources:
# if sourceDescriptor.copyLib:
# pass
# pass
# store designspace location in the font.lib
font.lib['designspace.location'] = list(instanceDescriptor.location.items())
return font
def isAnisotropic(self, location):
for v in location.values():
if type(v)==tuple:
return True
return False
def splitAnisotropic(self, location):
x = Location()
y = Location()
for dim, val in location.items():
if type(val)==tuple:
x[dim] = val[0]
y[dim] = val[1]
else:
x[dim] = y[dim] = val
return x, y
def _instantiateFont(self, path):
""" Return a instance of a font object with all the given subclasses"""
try:
return self.fontClass(path,
layerClass=self.layerClass,
libClass=self.libClass,
kerningClass=self.kerningClass,
groupsClass=self.groupsClass,
infoClass=self.infoClass,
featuresClass=self.featuresClass,
glyphClass=self.glyphClass,
glyphContourClass=self.glyphContourClass,
glyphPointClass=self.glyphPointClass,
glyphComponentClass=self.glyphComponentClass,
glyphAnchorClass=self.glyphAnchorClass)
except TypeError:
# if our fontClass doesnt support all the additional classes
return self.fontClass(path)
def _copyFontInfo(self, sourceInfo, targetInfo):
""" Copy the non-calculating fields from the source info."""
infoAttributes = [
"versionMajor",
"versionMinor",
"copyright",
"trademark",
"note",
"openTypeGaspRangeRecords",
"openTypeHeadCreated",
"openTypeHeadFlags",
"openTypeNameDesigner",
"openTypeNameDesignerURL",
"openTypeNameManufacturer",
"openTypeNameManufacturerURL",
"openTypeNameLicense",
"openTypeNameLicenseURL",
"openTypeNameVersion",
"openTypeNameUniqueID",
"openTypeNameDescription",
"#openTypeNamePreferredFamilyName",
"#openTypeNamePreferredSubfamilyName",
"#openTypeNameCompatibleFullName",
"openTypeNameSampleText",
"openTypeNameWWSFamilyName",
"openTypeNameWWSSubfamilyName",
"openTypeNameRecords",
"openTypeOS2Selection",
"openTypeOS2VendorID",
"openTypeOS2Panose",
"openTypeOS2FamilyClass",
"openTypeOS2UnicodeRanges",
"openTypeOS2CodePageRanges",
"openTypeOS2Type",
"postscriptIsFixedPitch",
"postscriptForceBold",
"postscriptDefaultCharacter",
"postscriptWindowsCharacterSet"
]
for infoAttribute in infoAttributes:
copy = False
if self.ufoVersion == 1 and infoAttribute in fontInfoAttributesVersion1:
copy = True
elif self.ufoVersion == 2 and infoAttribute in fontInfoAttributesVersion2:
copy = True
elif self.ufoVersion == 3 and infoAttribute in fontInfoAttributesVersion3:
copy = True
if copy:
value = getattr(sourceInfo, infoAttribute)
setattr(targetInfo, infoAttribute, value)
ufoProcessor-1.13.3/Lib/ufoProcessor/emptyPen.py 0000775 0000000 0000000 00000007552 14724542715 0021671 0 ustar 00root root 0000000 0000000 # coding: utf-8
from fontTools.pens.pointPen import AbstractPointPen
from defcon.pens.transformPointPen import TransformPointPen
from defcon.objects.component import _defaultTransformation
"""
Decompose
"""
class DecomposePointPen(object):
def __init__(self, glyphSet, outPointPen):
self._glyphSet = glyphSet
self._outPointPen = outPointPen
self.beginPath = outPointPen.beginPath
self.endPath = outPointPen.endPath
self.addPoint = outPointPen.addPoint
def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs):
if baseGlyphName in self._glyphSet:
baseGlyph = self._glyphSet[baseGlyphName]
if transformation == _defaultTransformation:
baseGlyph.drawPoints(self)
else:
transformPointPen = TransformPointPen(self, transformation)
baseGlyph.drawPoints(transformPointPen)
"""
Simple pen object to determine if a glyph contains any geometry.
"""
class EmptyPen(AbstractPointPen):
def __init__(self):
self.points = 0
self.contours = 0
self.components = 0
def beginPath(self, identifier=None, **kwargs):
pass
def endPath(self):
self.contours += 1
def addPoint(self, pt, segmentType=None, smooth=False, name=None, identifier=None, **kwargs):
self.points+=1
def addComponent(self, baseGlyphName=None, transformation=None, identifier=None, **kwargs):
self.components+=1
def getCount(self):
return self.points, self.contours, self.components
def isEmpty(self):
return self.points==0 and self.contours==0 and self.components==0
def checkGlyphIsEmpty(glyph, allowWhiteSpace=True):
"""
This will establish if the glyph is completely empty by drawing the glyph with an EmptyPen.
Additionally, the unicode of the glyph is checked against a list of known unicode whitespace
characters. This makes it possible to filter out glyphs that have a valid reason to be empty
and those that can be ignored.
"""
whiteSpace = [ 0x9, # horizontal tab
0xa, # line feed
0xb, # vertical tab
0xc, # form feed
0xd, # carriage return
0x20, # space
0x85, # next line
0xa0, # nobreak space
0x1680, # ogham space mark
0x180e, # mongolian vowel separator
0x2000, # en quad
0x2001, # em quad
0x2003, # en space
0x2004, # three per em space
0x2005, # four per em space
0x2006, # six per em space
0x2007, # figure space
0x2008, # punctuation space
0x2009, # thin space
0x200a, # hair space
0x2028, # line separator
0x2029, # paragraph separator
0x202f, # narrow no break space
0x205f, # medium mathematical space
0x3000, # ideographic space
]
emptyPen = EmptyPen()
glyph.drawPoints(emptyPen)
if emptyPen.isEmpty():
# we're empty?
if glyph.unicode in whiteSpace and allowWhiteSpace:
# are we allowed to be?
return False
if "space" in glyph.name:
# this is a bold assumption,
# and certainly not inclusive
return False
return True
return False
if __name__ == "__main__":
p = EmptyPen()
assert p.isEmpty() == True
p.addPoint((0,0))
assert p.isEmpty() == False
p = EmptyPen()
assert p.isEmpty() == True
p.addComponent((0,0))
assert p.isEmpty() == False
ufoProcessor-1.13.3/Lib/ufoProcessor/logger.py 0000664 0000000 0000000 00000003516 14724542715 0021340 0 ustar 00root root 0000000 0000000 import sys
import time
import os
import logging
class Logger:
def __init__(self, path, rootDirectory, nest=0):
self.path = path
self.rootDirectory = rootDirectory
self.nest = nest
if not nest:
if path is not None:
if os.path.exists(path):
os.remove(path)
if not os.path.exists(path):
f = open(path, "w")
f.close()
def child(self, text=None):
logger = Logger(
self.path,
self.rootDirectory,
nest=self.nest + 1
)
if text:
logger.info(text)
return logger
def relativePath(self, path):
return os.path.relpath(path, self.rootDirectory)
def _makeText(self, text):
if self.nest:
text = f"{('| ' * self.nest).strip()} {text}"
return text
def _toConsole(self, text):
print(text)
def _toFile(self, text):
if self.path is None:
return
text += "\n"
f = open(self.path, "a")
f.write(text)
f.close()
def time(self, prefix=None):
now = time.strftime("%Y-%m-%d %H:%M")
if prefix:
now = prefix + " " + now
self.info(now)
def info(self, text):
text = self._makeText(text)
self._toConsole(text)
self._toFile(text)
def infoItem(self, text):
text = f"\t- {text}"
self.info(text)
def infoPath(self, path):
text = self.relativePath(path)
self.infoItem(text)
def detail(self, text):
text = self._makeText(text)
self._toFile(text)
def detailItem(self, text):
text = f"- {text}"
self.detail(text)
def detailPath(self, path):
text = self.relativePath(path)
self.detailItem(text)
ufoProcessor-1.13.3/Lib/ufoProcessor/ufoOperator.py 0000664 0000000 0000000 00000240011 14724542715 0022357 0 ustar 00root root 0000000 0000000 import os
import functools
import itertools
import inspect
import random
import defcon
from warnings import warn
import collections
import traceback
from fontTools.designspaceLib import DesignSpaceDocument, processRules, InstanceDescriptor
from fontTools.designspaceLib.split import splitInterpolable, splitVariableFonts
from fontTools.ufoLib import fontInfoAttributesVersion1, fontInfoAttributesVersion2, fontInfoAttributesVersion3
from fontTools.misc import plistlib
from fontMath.mathGlyph import MathGlyph
from fontMath.mathInfo import MathInfo
from fontMath.mathKerning import MathKerning
from mutatorMath.objects.mutator import buildMutator
from mutatorMath.objects.location import Location
import fontParts.fontshell.font
from ufoProcessor.varModels import VariationModelMutator
from ufoProcessor.emptyPen import checkGlyphIsEmpty, DecomposePointPen
from ufoProcessor.logger import Logger
_memoizeCache = dict()
_memoizeStats = dict()
def ip(a, b, f):
return a+f*(b-a)
def immutify(obj):
# make an immutable version of this object.
# assert immutify(10) == (10,)
# assert immutify([10, 20, "a"]) == (10, 20, 'a')
# assert immutify(dict(aSet={1,2,3}, foo="bar", world=["a", "b"])) == ('foo', ('bar',), 'world', ('a', 'b'))
hashValues = []
if isinstance(obj, dict):
hashValues.append(
MemoizeDict(
[(key, immutify(value)) for key, value in obj.items()]
)
)
elif isinstance(obj, set):
for value in sorted(obj):
hashValues.append(immutify(value))
elif isinstance(obj, (list, tuple)):
for value in obj:
hashValues.append(immutify(value))
else:
hashValues.append(obj)
if len(hashValues) == 1:
return hashValues[0]
return tuple(hashValues)
class MemoizeDict(dict):
"""
An immutable dictionary.
>>> d = MemoizeDict(name="a", test="b")
>>> d["name"]
'a'
>>> d["name"] = "c"
Traceback (most recent call last):
...
RuntimeError: Cannot modify ImmutableDict
"""
def __readonly__(self, *args, **kwargs):
raise RuntimeError("Cannot modify MemoizeDict")
__setitem__ = __readonly__
__delitem__ = __readonly__
pop = __readonly__
popitem = __readonly__
clear = __readonly__
update = __readonly__
setdefault = __readonly__
del __readonly__
_hash = None
def __hash__(self):
if self._hash is None:
self._hash = hash(frozenset(self.items()))
return self._hash
def memoize(function):
signature = inspect.signature(function)
argsKeys = [parameter.name for parameter in signature.parameters.values()]
@functools.wraps(function)
def wrapper(*args, **kwargs):
immutablekwargs = immutify(dict(
**{key: value for key, value in zip(argsKeys, args)},
**kwargs
))
key = (function.__name__, immutablekwargs)
if key in _memoizeCache:
# keep track of how often we get to serve something from the cache
# note: if the object itself is part of the key
# keeping these stats will keep the object around
_memoizeStats[key] += 1
return _memoizeCache[key]
else:
result = function(*args, **kwargs)
_memoizeCache[key] = result
_memoizeStats[key] = 1
return result
return wrapper
def inspectMemoizeCache():
frequency = []
objects = {}
items = []
for (funcName, data), value in _memoizeCache.items():
if funcName == "getGlyphMutator":
functionName = f"{id(data['self']):X} {funcName}: {data['glyphName']}"
else:
functionName = f"{id(data['self']):X} {funcName}"
if functionName not in objects:
objects[functionName] = 0
objects[functionName] += 1
items = [(k, v) for k, v in objects.items()]
for key in _memoizeStats.keys():
if funcName == "getGlyphMutator":
functionName = f"{id(data['self']):X} {funcName}: {data['glyphName']}"
else:
functionName = f"{id(data['self']):X} {funcName}"
called = _memoizeStats[key]
frequency.append((functionName, called))
frequency.sort()
return items, frequency
def getDefaultLayerName(f):
# get the name of the default layer from a defcon font (outside RF) and from a fontparts font (outside and inside RF)
if isinstance(f, defcon.objects.font.Font):
return f.layers.defaultLayer.name
elif isinstance(f, fontParts.fontshell.font.RFont):
return f.defaultLayer.name
return None
def getLayer(f, layerName):
# get the layer from a defcon font and from a fontparts font
if isinstance(f, defcon.objects.font.Font):
if layerName in f.layers:
return f.layers[layerName]
elif isinstance(f, fontParts.fontshell.font.RFont):
if layerName in f.layerOrder:
return f.getLayer(layerName)
return None
class UFOOperator(object):
# wrapped, not inherited, as Just says.
fontClass = defcon.Font
layerClass = defcon.Layer
glyphClass = defcon.Glyph
libClass = defcon.Lib
glyphContourClass = defcon.Contour
glyphPointClass = defcon.Point
glyphComponentClass = defcon.Component
glyphAnchorClass = defcon.Anchor
kerningClass = defcon.Kerning
groupsClass = defcon.Groups
infoClass = defcon.Info
featuresClass = defcon.Features
mathInfoClass = MathInfo
mathGlyphClass = MathGlyph
mathKerningClass = MathKerning
# RF italic slant offset lib key
italicSlantOffsetLibKey = "com.typemytype.robofont.italicSlantOffset"
def __init__(self, pathOrObject=None, ufoVersion=3, useVarlib=True, extrapolate=False, strict=False, debug=False):
self.ufoVersion = ufoVersion
self.useVarlib = useVarlib
self._fontsLoaded = False
self.fonts = {}
self.tempLib = {}
self.libKeysForProcessing = [self.italicSlantOffsetLibKey]
self.roundGeometry = False
self.mutedAxisNames = None # list of axisname that need to be muted
self.strict = strict
self.debug = debug
self.extrapolate = extrapolate # if true allow extrapolation
self.logger = None
self.doc = None
if isinstance(pathOrObject, DesignSpaceDocument):
self.doc = pathOrObject
elif isinstance(pathOrObject, str):
self.doc = DesignSpaceDocument()
self.doc.read(pathOrObject)
else:
self.doc = DesignSpaceDocument()
if self.debug:
self.startLog()
def startLog(self):
# so we can call it later
self.debug = True
docBaseName = os.path.splitext(self.doc.path)[0]
logPath = f"{docBaseName}_log.txt"
self.logger = Logger(path=logPath, rootDirectory=None)
self.logger.time()
self.logger.info(f"## {self.doc.path}")
self.logger.info(f"\tUFO version: {self.ufoVersion}")
self.logger.info(f"\tround Geometry: {self.roundGeometry}")
if self.useVarlib:
self.logger.info(f"\tinterpolating with varlib")
else:
self.logger.info(f"\tinterpolating with mutatorMath")
def _instantiateFont(self, path):
""" Return a instance of a font object with all the given subclasses"""
try:
return self.fontClass(
path,
layerClass=self.layerClass,
libClass=self.libClass,
kerningClass=self.kerningClass,
groupsClass=self.groupsClass,
infoClass=self.infoClass,
featuresClass=self.featuresClass,
glyphClass=self.glyphClass,
glyphContourClass=self.glyphContourClass,
glyphPointClass=self.glyphPointClass,
glyphComponentClass=self.glyphComponentClass,
glyphAnchorClass=self.glyphAnchorClass
)
except TypeError:
# if our fontClass doesnt support all the additional classes
return self.fontClass(path)
# UFOProcessor compatibility
# not sure whether to expose all the DesignSpaceDocument internals here
# One can just use ufoOperator.doc to get it going?
# Let's see how difficilt it is
def read(self, path):
"""Wrap a DesignSpaceDocument"""
self.doc = DesignSpaceDocument()
self.doc.read(path)
self.changed()
def write(self, path):
"""Write the wrapped DesignSpaceDocument"""
self.doc.write(path)
def addAxis(self, axisDescriptor):
self.doc.addAxis(axisDescriptor)
def addAxisDescriptor(self, **kwargs):
return self.doc.addAxisDescriptor(**kwargs)
def addLocationLabel(self, locationLabelDescriptor):
self.doc.addLocationLabel(locationLabelDescriptor)
def addLocationLabelDescriptor(self, **kwargs):
return self.doc.addLocationLabelDescriptor(**kwargs)
def addRule(self, ruleDescriptor):
self.doc.addRule(ruleDescriptor)
def addRuleDescriptor(self, **kwargs):
return self.doc.addRuleDescriptor(**kwargs)
def addSource(self, sourceDescriptor):
if sourceDescriptor.font is not None:
self.fonts[sourceDescriptor.name] = sourceDescriptor.font
self.doc.addSource(sourceDescriptor)
def addSourceDescriptor(self, **kwargs):
if "font" in kwargs:
self.fonts[kwargs["name"]] = kwargs["font"]
return self.doc.addSourceDescriptor(**kwargs)
def addInstance(self, instanceDescriptor):
self.doc.addInstance(instanceDescriptor)
def addInstanceDescriptor(self, **kwargs):
return self.doc.addInstanceDescriptor(**kwargs)
def addVariableFont(self, variableFontDescriptor):
self.doc.addVariableFont(variableFontDescriptor)
def addVariableFontDescriptor(self, **kwargs):
return self.doc.addVariableFontDescriptor(**kwargs)
def getVariableFonts(self):
return self.doc.getVariableFonts()
def getInterpolableUFOOperators(self, useVariableFonts=True):
if useVariableFonts:
splitFunction = splitVariableFonts
else:
splitFunction = splitInterpolable
for discreteLocationOrName, interpolableDesignspace in splitFunction(self.doc):
if isinstance(discreteLocationOrName, dict):
basename = ""
if self.doc.filename is not None:
basename = os.path.splitext(self.doc.filename)[0]
elif self.doc.path is not None:
basename = os.path.splitext(os.path.basename(self.doc.path))[0]
discreteLocationOrName = basename + "-".join([f"{key}_{value:g}" for key, value in discreteLocationOrName.items()])
yield discreteLocationOrName, self.__class__(
interpolableDesignspace,
ufoVersion=self.ufoVersion,
useVarlib=self.useVarlib,
extrapolate=self.extrapolate,
strict=self.strict,
debug=self.debug
)
@property
def path(self):
return self.doc.path
@path.setter
def path(self, value):
self.doc.path = value
@property
def lib(self):
return self.doc.lib
@property
def axes(self):
return self.doc.axes
@property
def sources(self):
return self.doc.sources
@property
def instances(self):
return self.doc.instances
@property
def formatVersion(self):
return self.doc.formatVersion
@property
def rules(self):
return self.doc.rules
@property
def rulesProcessingLast(self):
return self.doc.rulesProcessingLast
@property
def map_backward(self):
return self.doc.map_backward
@property
def labelForUserLocation(self):
return self.doc.labelForUserLocation
@property
def locationLabels(self):
return self.doc.locationLabels
@locationLabels.setter
def locationLabels(self, locationLabels):
self.doc.locationLabels = locationLabels
@property
def variableFonts(self):
return self.doc.variableFonts
@property
def writerClass(self):
return self.doc.writerClass
def nameLocation(self, loc):
# return a nicely formatted string for this location
return ",".join([f"{k}:{v}" for k, v in loc.items()])
@formatVersion.setter
def formatVersion(self, value):
self.doc.formatVersion = value
def getAxis(self, axisName):
return self.doc.getAxis(axisName)
# loading and updating fonts
def loadFonts(self, reload=False):
# Load the fonts and find the default candidate based on the info flag
if self.logger is None and self.debug:
# in some cases the UFOProcessor is initialised without debug
# and then it is switched on afterwards. So have to check if
# we have a logger before proceding.
self.startLog()
self.glyphNames = list({glyphname for font in self.fonts.values() for glyphname in font.keys()})
if self._fontsLoaded and not reload:
if self.debug:
self.logger.info("\t\t-- loadFonts called, but fonts are loaded already and no reload requested")
return
actions = []
if self.debug:
self.logger.info("## loadFonts")
for i, sourceDescriptor in enumerate(self.doc.sources):
if sourceDescriptor.name is None:
# make sure it has a unique name
sourceDescriptor.name = "source.%d" % i
if sourceDescriptor.name not in self.fonts:
if os.path.exists(sourceDescriptor.path):
font = self.fonts[sourceDescriptor.name] = self._instantiateFont(sourceDescriptor.path)
thisLayerName = getDefaultLayerName(font)
if self.debug:
actions.append(f"loaded: {os.path.basename(sourceDescriptor.path)}, layer: {thisLayerName}, format: {font.ufoFormatVersionTuple}, id: {id(font):X}")
else:
self.fonts[sourceDescriptor.name] = None
if self.debug:
actions.append("source ufo not found at %s" % (sourceDescriptor.path))
if self.debug:
for item in actions:
self.logger.infoItem(item)
self._fontsLoaded = True
# XX maybe also make a character map here?
def _logLoadedFonts(self):
# dump info about the loaded fonts to the log
self.logger.info("\t# font status:")
for name, font in self.fonts.items():
self.logger.info(f"\t\tloaded: , id: {id(font):X}, {os.path.basename(font.path)}, format: {font.ufoFormatVersionTuple}")
def updateFonts(self, fontObjects):
# this is to update the loaded fonts.
# it should be the way for an editor to provide a list of fonts that are open
# self.fonts[sourceDescriptor.name] = None
hasUpdated = False
for newFont in fontObjects:
# XX can we update font objects which arent stored on disk?
if newFont.path is not None:
for fontName, haveFont in self.fonts.items():
# XX what happens here when the font did not load?
# haveFont will be None. Scenario: font initially missing, then added.
if haveFont is None:
if self.debug:
self.logger.time()
self.logger.info(f"## updating unloaded source {fontName} with {newFont}")
self.fonts[fontName] = newFont
hasUpdated = True
elif haveFont.path == newFont.path:
if self.debug:
self.logger.time()
self.logger.info(f"## updating source {self.fonts[fontName]} with {newFont}")
self.fonts[fontName] = newFont
hasUpdated = True
if hasUpdated:
self.changed()
def getFonts(self):
# return a list of (font object, location) tuples
fonts = []
for sourceDescriptor in self.sources:
f = self.fonts.get(sourceDescriptor.name)
if f is not None:
fonts.append((f, sourceDescriptor.location))
return fonts
def usesFont(self, fontObj=None):
# return True if font is used in this designspace.
if fontObj is None:
return False
for name, otherFontObj in self.fonts.items():
if otherFontObj is None: continue
if otherFontObj.path == fontObj.path:
# we don't need to know anything else
return True
return False
def getCharacterMapping(self, discreteLocation=None):
# return a unicode -> glyphname map for the default of the system or discreteLocation
characterMap = {}
defaultSourceDescriptor = self.findDefault(discreteLocation=discreteLocation)
if not defaultSourceDescriptor:
return {}
defaultFont = self.fonts.get(defaultSourceDescriptor.name)
if defaultFont is None:
return {}
for glyph in defaultFont:
if glyph.unicodes:
for u in glyph.unicodes:
characterMap[u] = glyph.name
return characterMap
# caching
def __del__(self):
self.changed()
def changed(self):
# clears everything relating to this designspacedocument
# the cache could contain more designspacedocument objects.
if _memoizeCache == None:
# it can happen that changed is called after we're already clearing out.
# Otherwise it _memoizeCache will be a dict.
# If it is no longer a dict, it will not have anything left in store.
return
for key in list(_memoizeCache.keys()):
funcName, data = key
if data["self"] == self:
del _memoizeCache[key]
if key in _memoizeStats:
del _memoizeStats[key]
_cachedCallbacksWithGlyphNames = ("getGlyphMutator", "collectSourcesForGlyph", "makeOneGlyph")
def glyphChanged(self, glyphName, includeDependencies=False):
"""Clears this one specific glyph from the memoize cache
includeDependencies = True: check where glyphName is used as a component
and remove those as well.
Note: this must be check in each discreteLocation separately
because they can have different constructions."""
changedNames = set()
changedNames.add(glyphName)
if includeDependencies:
dependencies = self.getGlyphDependencies(glyphName)
if dependencies:
changedNames.update(dependencies)
remove = []
for key in list(_memoizeCache.keys()):
funcName, data = key
if data["self"] == self and funcName in self._cachedCallbacksWithGlyphNames and data["glyphName"] in changedNames:
remove.append(key)
remove = set(remove)
for key in remove:
del _memoizeCache[key]
if key in _memoizeStats:
del _memoizeStats[key]
def getGlyphDependencies(self, glyphName):
dependencies = set()
discreteLocation = self.getDiscreteLocations()
if not discreteLocation:
discreteLocation = [None]
for discreteLocation in discreteLocation:
# this is expensive, should it be cached?
reverseComponentMap = self.getReverseComponentMapping(discreteLocation)
if glyphName not in reverseComponentMap:
return None
for compName in reverseComponentMap[glyphName]:
dependencies.add(compName)
return dependencies
def glyphsInCache(self):
"""report which glyphs are in the cache at the moment"""
names = set()
for funcName, data in list(_memoizeCache.keys()):
if funcName in self._cachedCallbacksWithGlyphNames and data["self"] == self:
names.add(data["glyphName"])
names = list(names)
names.sort()
return names
# manipulate locations and axes
def findAllDefaults(self):
# collect all default sourcedescriptors for all discrete locations
defaults = []
discreteLocation = self.getDiscreteLocations()
if not discreteLocation:
discreteLocation = [None]
for discreteLocation in discreteLocation:
defaultSourceDescriptor = self.findDefault(discreteLocation=discreteLocation)
defaults.append(defaultSourceDescriptor)
return defaults
def findDefault(self, discreteLocation=None):
defaultDesignLocation = self.newDefaultLocation(bend=True, discreteLocation=discreteLocation)
sources = self.findSourceDescriptorsForDiscreteLocation(discreteLocation)
for s in sources:
if s.location == defaultDesignLocation:
return s
return None
def findDefaultFont(self, discreteLocation=None):
# A system without discrete axes should be able to
# find a default here.
defaultSourceDescriptor = self.findDefault(discreteLocation=discreteLocation)
if defaultSourceDescriptor is None:
return None
# find the font now
return self.fonts.get(defaultSourceDescriptor.name, None)
getNeutralFont = findDefaultFont
def splitLocation(self, location):
# split a location in a continouous and a discrete part
# Note: discrete can be None
discreteAxes = [a.name for a in self.getOrderedDiscreteAxes()]
continuous = {}
discrete = {}
for name, value in location.items():
if name in discreteAxes:
discrete[name] = value
else:
continuous[name] = value
if not discrete:
return continuous, None
return continuous, discrete
def _serializeAnyAxis(self, axis):
if hasattr(axis, "serialize"):
return axis.serialize()
else:
if hasattr(axis, "values"):
# discrete axis does not have serialize method, meh
return dict(
tag=axis.tag,
name=axis.name,
labelNames=axis.labelNames,
minimum=min(axis.values), # XX is this allowed
maximum=max(axis.values), # XX is this allowed
values=axis.values,
default=axis.default,
hidden=axis.hidden,
map=axis.map,
axisOrdering=axis.axisOrdering,
axisLabels=axis.axisLabels,
)
def getSerializedAxes(self, discreteLocation=None):
serialized = []
for axis in self.getOrderedContinuousAxes():
serialized.append(self._serializeAnyAxis(axis))
return serialized
def getContinuousAxesForMutator(self):
# map the axis values?
d = collections.OrderedDict()
for axis in self.getOrderedContinuousAxes():
d[axis.name] = self._serializeAnyAxis(axis)
return d
def _getAxisOrder(self):
# XX this might be different from the axis order labels
return [axisDescriptor.name for axisDescriptor in self.doc.axes]
axisOrder = property(_getAxisOrder, doc="get the axis order from the axis descriptors")
def getFullDesignLocation(self, location):
return self.doc.getFullDesignLocation(location, self.doc)
def getDiscreteLocations(self):
# return a list of all permutated discrete locations
# do we have a list of ordered axes?
values = []
names = []
discreteCoordinates = []
for axis in self.getOrderedDiscreteAxes():
values.append(axis.values)
names.append(axis.name)
if values:
for r in itertools.product(*values):
# make a small dict for the discrete location values
discreteCoordinates.append({a: b for a, b in zip(names, r)})
return discreteCoordinates
def getOrderedDiscreteAxes(self):
# return the list of discrete axis objects, in the right order
axes = []
for axisName in self.doc.getAxisOrder():
axisObj = self.doc.getAxis(axisName)
if hasattr(axisObj, "values"):
axes.append(axisObj)
return axes
def getOrderedContinuousAxes(self):
# return the list of continuous axis objects, in the right order
axes = []
for axisName in self.doc.getAxisOrder():
axisObj = self.doc.getAxis(axisName)
if not hasattr(axisObj, "values"):
axes.append(axisObj)
return axes
def checkDiscreteAxisValues(self, location):
# check if the discrete values in this location are allowed
for discreteAxis in self.getOrderedDiscreteAxes():
testValue = location.get(discreteAxis.name)
if testValue not in discreteAxis.values:
return False
return True
def collectBaseGlyphs(self, glyphName, location):
# make a list of all baseglyphs needed to build this glyph, at this location
# Note: different discrete values mean that the glyph component set up can be different too
continuousLocation, discreteLocation = self.splitLocation(location)
names = set()
def _getComponentNames(glyph):
# so we can do recursion
names = set()
for comp in glyph.components:
names.add(comp.baseGlyph)
for n in _getComponentNames(glyph.font[comp.baseGlyph]):
names.add(n)
return list(names)
for sourceDescriptor in self.findSourceDescriptorsForDiscreteLocation(discreteLocation):
sourceFont = self.fonts[sourceDescriptor.name]
if glyphName not in sourceFont:
continue
[names.add(n) for n in _getComponentNames(sourceFont[glyphName])]
return list(names)
def findSourceDescriptorsForDiscreteLocation(self, discreteLocDict=None):
# return a list of all sourcedescriptors that share the values in the discrete loc tuple
# so this includes all sourcedescriptors that point to layers
# discreteLocDict {'countedItems': 1.0, 'outlined': 0.0}, {'countedItems': 1.0, 'outlined': 1.0}
sources = []
for s in self.doc.sources:
ok = True
if discreteLocDict is None:
sources.append(s)
continue
for name, value in discreteLocDict.items():
if name in s.location:
if s.location[name] != value:
ok = False
else:
ok = False
continue
if ok:
sources.append(s)
return sources
def getVariationModel(self, items, axes, bias=None):
# Return either a mutatorMath or a varlib.model object for calculating.
if self.useVarlib:
# use the varlib variation model
try:
return dict(), VariationModelMutator(items, axes=self.doc.axes, extrapolate=True)
except TypeError:
if self.debug:
note = "Error while making VariationModelMutator for {loc}:\n{traceback.format_exc()}"
self.logger.info(note)
return {}, None
except (KeyError, AssertionError):
if self.debug:
note = "UFOProcessor.getVariationModel error: {traceback.format_exc()}"
self.logger.info(note)
return {}, None
else:
# use mutatormath model
axesForMutator = self.getContinuousAxesForMutator()
# mutator will be confused by discrete axis values.
# the bias needs to be for the continuous axes only
biasForMutator, _ = self.splitLocation(bias)
return buildMutator(items, axes=axesForMutator, bias=biasForMutator)
return {}, None
def newDefaultLocation(self, bend=False, discreteLocation=None):
# overwrite from fontTools.newDefaultLocation
# we do not want this default location always to be mapped.
loc = collections.OrderedDict()
for axisDescriptor in self.doc.axes:
axisName = axisDescriptor.name
axisValue = axisDescriptor.default
if discreteLocation is not None:
# if we want to find the default for a specific discreteLoation
# we can not use the discrete axis' default value
# -> we have to use the value in the given discreteLocation
if axisDescriptor.name in discreteLocation:
axisValue = discreteLocation[axisDescriptor.name]
else:
axisValue = axisDescriptor.default
if bend:
loc[axisName] = axisDescriptor.map_forward(
axisValue
)
else:
loc[axisName] = axisValue
return loc
def isAnisotropic(self, location):
# check if the location has anisotropic values
for v in location.values():
if isinstance(v, (list, tuple)):
return True
return False
def splitAnisotropic(self, location):
# split the anisotropic location into a horizontal and vertical component
x = Location()
y = Location()
for dim, val in location.items():
if isinstance(val, (tuple, list)):
x[dim] = val[0]
y[dim] = val[1]
else:
x[dim] = y[dim] = val
return x, y
# find out stuff about this designspace
def collectForegroundLayerNames(self):
"""Return list of names of the default layers of all the fonts in this system.
Include None and foreground. XX Why
"""
names = set([None, 'foreground'])
for key, font in self.fonts.items():
names.add(getDefaultLayerName(font))
return list(names)
def getReverseComponentMapping(self, discreteLocation=None):
"""Return a dict with reverse component mappings.
Check if we're using fontParts or defcon
Check which part of the designspace we're in.
"""
if discreteLocation is not None:
sources = self.findSourceDescriptorsForDiscreteLocation(discreteLocation)
else:
sources = self.doc.sources
for sourceDescriptor in sources:
isDefault = self.isLocalDefault(sourceDescriptor.location)
if isDefault:
font = self.fonts.get(sourceDescriptor.name)
if font is None:
return {}
if isinstance(font, defcon.objects.font.Font):
# defcon
reverseComponentMapping = {}
for base, comps in font.componentReferences.items():
for c in comps:
if base not in reverseComponentMapping:
reverseComponentMapping[base] = set()
reverseComponentMapping[base].add(c)
else:
if hasattr(font, "getReverseComponentMapping"):
reverseComponentMapping = font.getReverseComponentMapping()
return reverseComponentMapping
return {}
def generateUFOs(self, useVarlib=None):
# generate an UFO for each of the instance locations
previousModel = self.useVarlib
generatedFontPaths = []
if useVarlib is not None:
self.useVarlib = useVarlib
glyphCount = 0
self.loadFonts()
if self.debug:
self.logger.info("## generateUFO")
for instanceDescriptor in self.doc.instances:
if self.debug:
self.logger.infoItem(f"Generating UFO at designspaceLocation {instanceDescriptor.getFullDesignLocation(self.doc)}")
if instanceDescriptor.path is None:
continue
pairs = None
bend = False
font = self.makeInstance(
instanceDescriptor,
# processRules,
glyphNames=self.glyphNames,
decomposeComponents=False,
pairs=pairs,
bend=bend,
)
if self.debug:
self.logger.info(f"\t\t{os.path.basename(instanceDescriptor.path)}")
instanceFolder = os.path.dirname(instanceDescriptor.path)
if instanceFolder and not os.path.exists(instanceFolder):
os.makedirs(instanceFolder)
font.save(instanceDescriptor.path)
generatedFontPaths.append(instanceDescriptor.path)
glyphCount += len(font)
if self.debug:
self.logger.info(f"\t\tGenerated {glyphCount} glyphs altogether.")
self.useVarlib = previousModel
return generatedFontPaths
generateUFO = generateUFOs
@memoize
def getInfoMutator(self, discreteLocation=None):
""" Returns a info mutator for this discrete location """
infoItems = []
foregroundLayers = self.collectForegroundLayerNames()
if discreteLocation is not None and discreteLocation is not {}:
sources = self.findSourceDescriptorsForDiscreteLocation(discreteLocation)
else:
sources = self.doc.sources
for sourceDescriptor in sources:
if sourceDescriptor.layerName not in foregroundLayers:
continue
continuous, discrete = self.splitLocation(sourceDescriptor.location)
loc = Location(continuous)
sourceFont = self.fonts[sourceDescriptor.name]
if sourceFont is None:
continue
if hasattr(sourceFont.info, "toMathInfo"):
infoItems.append((loc, sourceFont.info.toMathInfo()))
else:
infoItems.append((loc, self.mathInfoClass(sourceFont.info)))
infoBias = self.newDefaultLocation(bend=True, discreteLocation=discreteLocation)
bias, self._infoMutator = self.getVariationModel(infoItems, axes=self.getSerializedAxes(), bias=infoBias)
return self._infoMutator
@memoize
def getLibEntryMutator(self, discreteLocation=None):
""" Returns a mutator for selected lib keys store in self.libKeysForProcessing
If there is no entry in the lib, it will ignore the source
If there are no libkeys, it will return None.
"""
libMathItems = []
allValues = {}
foregroundLayers = self.collectForegroundLayerNames()
if discreteLocation is not None and discreteLocation is not {}:
sources = self.findSourceDescriptorsForDiscreteLocation(discreteLocation)
else:
sources = self.doc.sources
for sourceDescriptor in sources:
#if sourceDescriptor.layerName not in foregroundLayers:
# continue
continuous, discrete = self.splitLocation(sourceDescriptor.location)
loc = Location(continuous)
sourceFont = self.fonts[sourceDescriptor.name]
if sourceFont is None:
continue
mathDict = Location() # we're using this for its math dict skills
for libKey in self.libKeysForProcessing:
if libKey in sourceFont.lib:
# only add values we know
mathDict[libKey] = sourceFont.lib[libKey]
libMathItems.append((loc, mathDict))
if not libMathItems:
# no keys, no mutator.
return None
libMathBias = self.newDefaultLocation(bend=True, discreteLocation=discreteLocation)
bias, libMathMutator = self.getVariationModel(libMathItems, axes=self.getSerializedAxes(), bias=libMathBias)
return libMathMutator
@memoize
def getKerningMutator(self, pairs=None, discreteLocation=None):
""" Return a kerning mutator, collect the sources, build mathGlyphs.
If no pairs are given: calculate the whole table.
If pairs are given then query the sources for a value and make a mutator only with those values.
"""
if discreteLocation is not None:
sources = self.findSourceDescriptorsForDiscreteLocation(discreteLocation)
else:
sources = self.sources
kerningItems = []
foregroundLayers = self.collectForegroundLayerNames()
if pairs is None:
for sourceDescriptor in sources:
if sourceDescriptor.layerName not in foregroundLayers:
continue
if not sourceDescriptor.muteKerning:
continuous, discrete = self.splitLocation(sourceDescriptor.location)
loc = Location(continuous)
sourceFont = self.fonts[sourceDescriptor.name]
if sourceFont is None:
continue
# this makes assumptions about the groups of all sources being the same.
kerningItems.append((loc, self.mathKerningClass(sourceFont.kerning, sourceFont.groups)))
else:
self._kerningMutatorPairs = pairs
for sourceDescriptor in sources:
# XXX check sourceDescriptor layerName, only foreground should contribute
if sourceDescriptor.layerName is not None:
continue
if not os.path.exists(sourceDescriptor.path):
continue
if not sourceDescriptor.muteKerning:
sourceFont = self.fonts[sourceDescriptor.name]
if sourceFont is None:
continue
continuous, discrete = self.splitLocation(sourceDescriptor.location)
loc = Location(continuous)
# XXX can we get the kern value from the fontparts kerning object?
kerningItem = self.mathKerningClass(sourceFont.kerning, sourceFont.groups)
if kerningItem is not None:
sparseKerning = {}
for pair in pairs:
v = kerningItem.get(pair)
if v is not None:
sparseKerning[pair] = v
kerningItems.append((loc, self.mathKerningClass(sparseKerning)))
kerningBias = self.newDefaultLocation(bend=True, discreteLocation=discreteLocation)
bias, thing = self.getVariationModel(kerningItems, axes=self.getSerializedAxes(), bias=kerningBias) #xx
bias, self._kerningMutator = self.getVariationModel(kerningItems, axes=self.getSerializedAxes(), bias=kerningBias)
return self._kerningMutator
@memoize
def getGlyphMutator(self, glyphName, decomposeComponents=False, **discreteLocation):
"""make a mutator / varlib object for glyphName, with the sources for the given discrete location"""
items, unicodes = self.collectSourcesForGlyph(glyphName, decomposeComponents=decomposeComponents, **discreteLocation)
new = []
for a, b, c in items:
if hasattr(b, "toMathGlyph"):
# note: calling toMathGlyph ignores the mathGlyphClass preference
# maybe the self.mathGlyphClass is not necessary?
new.append((a, b.toMathGlyph(strict=self.strict)))
else:
new.append((a, self.mathGlyphClass(b, strict=self.strict)))
thing = None
thisBias = self.newDefaultLocation(bend=True, discreteLocation=discreteLocation)
try:
serializedAxes = self.getSerializedAxes()
bias, thing = self.getVariationModel(new, axes=serializedAxes, bias=thisBias) # xx
except Exception:
error = traceback.format_exc()
note = f"Error in getGlyphMutator for {glyphName}:\n{error}"
if self.debug:
self.logger.info(note)
return thing, unicodes
def isLocalDefault(self, location):
# return True if location is a local default
# check for bending
defaults = {}
for aD in self.doc.axes:
defaults[aD.name] = aD.map_forward(aD.default)
for axisName, value in location.items():
if defaults[axisName] != value:
return False
return True
def axesByName(self):
# return a dict[axisName]: axisDescriptor
axes = {}
for aD in self.doc.axes:
axes[aD.name] = aD
return axes
def locationWillClip(self, location):
# return True if this location will be clipped.
clipped = self.clipDesignLocation(location)
return not clipped == location
def getAxisExtremes(self, axisRecord):
# return the axis values in designspace coordinates
if axisRecord.map is not None:
aD_minimum = axisRecord.map_forward(axisRecord.minimum)
aD_maximum = axisRecord.map_forward(axisRecord.maximum)
aD_default = axisRecord.map_forward(axisRecord.default)
return aD_minimum, aD_default, aD_maximum
return axisRecord.minimum, axisRecord.default, axisRecord.maximum
def clipDesignLocation(self, location):
# return a copy of the design location without extrapolation
# assume location is in designspace coordinates.
# use map_forward on axis extremes,
axesByName = self.axesByName()
new = {}
for axisName, value in location.items():
aD = axesByName.get(axisName)
clippedValues = []
if type(value) == tuple:
testValues = list(value)
else:
testValues = [value]
for value in testValues:
if hasattr(aD, "values"):
# a discrete axis
# will there be mapped discrete values?
mx = max(aD.values)
mn = min(aD.values)
if value in aD.values:
clippedValues.append(value)
elif value > mx:
clippedValues.append(mx)
elif value < mn:
clippedValues.append(mn)
else:
# do we want to test if the value is part of the values allowed in this axes?
# or do we just assume it is correct?
# possibility: snap to the nearest value?
clippedValues.append(value)
else:
# a continuous axis
aD_minimum = aD.map_forward(aD.minimum)
aD_maximum = aD.map_forward(aD.maximum)
if value < aD_minimum:
clippedValues.append(aD_minimum)
elif value > aD_maximum:
clippedValues.append(aD_maximum)
else:
clippedValues.append(value)
if len(clippedValues)==1:
new[axisName] = clippedValues[0]
elif len(clippedValues)==2:
new[axisName] = tuple(clippedValues)
return new
def filterThisLocation(self, location, mutedAxes=None):
# return location with axes is mutedAxes removed
# this means checking if the location is a non-default value
if not mutedAxes:
return False, location
defaults = {}
ignoreSource = False
for aD in self.doc.axes:
defaults[aD.name] = aD.default
new = {}
new.update(location)
for mutedAxisName in mutedAxes:
if mutedAxisName not in location:
continue
if mutedAxisName not in defaults:
continue
if location[mutedAxisName] != defaults.get(mutedAxisName):
ignoreSource = True
del new[mutedAxisName]
return ignoreSource, new
@memoize
def collectSourcesForGlyph(self, glyphName, decomposeComponents=False, discreteLocation=None, asMathGlyph=True):
""" Return all source glyph objects.
+ either as mathglyphs (for use in mutators)
+ or source glyphs straight from the fonts
decomposeComponents = True causes the source glyphs to be decomposed first
before building the mutator. That gives you instances that do not depend
on a complete font. If you're calculating previews for instance.
findSourceDescriptorsForDiscreteLocation returns sources from layers as well
"""
items = []
empties = []
foundEmpty = False
# is bend=True necessary here?
defaultLocation = self.newDefaultLocation(bend=True, discreteLocation=discreteLocation)
#
if discreteLocation is not None:
sources = self.findSourceDescriptorsForDiscreteLocation(discreteLocation)
else:
sources = self.doc.sources
unicodes = set() # unicodes for this glyph
for sourceDescriptor in sources:
if not os.path.exists(sourceDescriptor.path):
#kthxbai
note = "\tMissing UFO at %s" % sourceDescriptor.path
if self.debug:
self.logger.info(note)
continue
if glyphName in sourceDescriptor.mutedGlyphNames:
if self.debug:
self.logger.info(f"\t\tglyphName {glyphName} is muted")
continue
thisIsDefault = self.isLocalDefault(sourceDescriptor.location)
ignoreSource, filteredLocation = self.filterThisLocation(sourceDescriptor.location, self.mutedAxisNames)
if ignoreSource:
continue
f = self.fonts.get(sourceDescriptor.name)
if f is None:
continue
loc = Location(sourceDescriptor.location)
sourceLayer = f
if glyphName not in f:
# log this>
continue
layerName = getDefaultLayerName(f)
sourceGlyphObject = None
# handle source layers
if sourceDescriptor.layerName is not None:
# start looking for a layer
# Do not bother for mutatorMath designspaces
layerName = sourceDescriptor.layerName
sourceLayer = getLayer(f, sourceDescriptor.layerName)
if sourceLayer is None:
continue
if glyphName not in sourceLayer:
# start looking for a glyph
# this might be a support in a sparse layer
# so we're skipping!
continue
# still have to check if the sourcelayer glyph is empty
if glyphName not in sourceLayer:
continue
else:
sourceGlyphObject = sourceLayer[glyphName]
if sourceGlyphObject.unicodes is not None:
for u in sourceGlyphObject.unicodes:
unicodes.add(u)
if checkGlyphIsEmpty(sourceGlyphObject, allowWhiteSpace=True):
foundEmpty = True
# sourceGlyphObject = None
# continue
if decomposeComponents:
# what about decomposing glyphs in a partial font?
temp = self.glyphClass()
sourceGlyphObject.drawPoints(
DecomposePointPen(sourceLayer, temp.getPointPen())
)
temp.width = sourceGlyphObject.width
temp.name = sourceGlyphObject.name
temp.anchors = [dict(
x=anchor.x,
y=anchor.y,
name=anchor.name,
identifier=anchor.identifier,
color=anchor.color
) for anchor in sourceGlyphObject.anchors]
temp.guidelines = [dict(
x=guideline.x,
y=guideline.y,
angle=guideline.angle,
name=guideline.name,
identifier=guideline.identifier,
color=guideline.color
) for guideline in sourceGlyphObject.guidelines]
processThis = temp
else:
processThis = sourceGlyphObject
sourceInfo = dict(
source=f.path,
glyphName=glyphName,
layerName=layerName,
location=filteredLocation, # sourceDescriptor.location,
sourceName=sourceDescriptor.name,
)
if asMathGlyph:
if hasattr(processThis, "toMathGlyph"):
processThis = processThis.toMathGlyph(strict=self.strict)
else:
processThis = self.mathGlyphClass(processThis, strict=self.strict)
continuous, discrete = self.splitLocation(loc)
items.append((continuous, processThis, sourceInfo))
empties.append((thisIsDefault, foundEmpty))
# check the empties:
# if the default glyph is empty, then all must be empty
# if the default glyph is not empty then none can be empty
checkedItems = []
emptiesAllowed = False
# first check if the default is empty.
# remember that the sources can be in any order
for i, p in enumerate(empties):
isDefault, isEmpty = p
if isDefault and isEmpty:
emptiesAllowed = True
# now we know what to look for
if not emptiesAllowed:
for i, p in enumerate(empties):
isDefault, isEmpty = p
if not isEmpty:
checkedItems.append(items[i])
else:
for i, p in enumerate(empties):
isDefault, isEmpty = p
if isEmpty:
checkedItems.append(items[i])
return checkedItems, unicodes
def collectMastersForGlyph(self, glyphName, decomposeComponents=False, discreteLocation=None):
# compatibility thing for designspaceProblems.
checkedItems, unicodes = self.collectSourcesForGlyph(glyphName, decomposeComponents=False, discreteLocation=None)
return checkedItems
def getLocationType(self, location):
"""Determine the type of the location:
continuous / discrete
anisotropic / normal.
"""
continuousLocation, discreteLocation = self.splitLocation(location)
if not self.extrapolate:
# Axis values are in userspace, so this needs to happen before bending
continuousLocation = self.clipDesignLocation(continuousLocation)
#font = self._instantiateFont(None)
loc = Location(continuousLocation)
anisotropic = False
locHorizontal = locVertical = loc
if self.isAnisotropic(loc):
anisotropic = True
locHorizontal, locVertical = self.splitAnisotropic(loc)
return anisotropic, continuousLocation, discreteLocation, locHorizontal, locVertical
def collectSkippedGlyphs(self):
# return a list of all the glyphnames listed in public.skipExportGlyphs
names = []
for fontPath, fontObj in self.fonts.items():
for name in fontObj.lib.get('public.skipExportGlyphs', []):
if name not in names:
names.append(name)
if self.debug:
self.logger.info(f"collectSkippedGlyphs: {names}")
return names
def makeInstance(self, instanceDescriptor,
doRules=None,
glyphNames=None,
decomposeComponents=False,
pairs=None,
bend=False):
""" Generate a font object for this instance """
if doRules is not None:
warn('The doRules argument in DesignSpaceProcessor.makeInstance() is deprecated', DeprecationWarning, stacklevel=2)
if isinstance(instanceDescriptor, dict):
instanceDescriptor = self.doc.writerClass.instanceDescriptorClass(**instanceDescriptor)
# hmm getFullDesignLocation does not support anisotropc locations?
fullDesignLocation = instanceDescriptor.getFullDesignLocation(self.doc)
anisotropic, continuousLocation, discreteLocation, locHorizontal, locVertical = self.getLocationType(fullDesignLocation)
self.loadFonts()
if not self.extrapolate:
# Axis values are in userspace, so this needs to happen before bending
continuousLocation = self.clipDesignLocation(continuousLocation)
font = self._instantiateFont(None)
loc = Location(continuousLocation)
anisotropic = False
locHorizontal = locVertical = loc
if self.isAnisotropic(loc):
anisotropic = True
locHorizontal, locVertical = self.splitAnisotropic(loc)
if self.debug:
self.logger.info(f"\t\t\tAnisotropic location for \"{instanceDescriptor.name}\"\n\t\t\t{fullDesignLocation}")
# makeOneKerning
# discreteLocation ?
if instanceDescriptor.kerning:
kerningObject = self.makeOneKerning(fullDesignLocation, pairs=pairs)
if kerningObject is not None:
kerningObject.extractKerning(font)
# makeOneInfo
infoInstanceObject = self.makeOneInfo(fullDesignLocation, roundGeometry=self.roundGeometry, clip=False)
if infoInstanceObject is not None:
infoInstanceObject.extractInfo(font.info)
font.info.familyName = instanceDescriptor.familyName
font.info.styleName = instanceDescriptor.styleName
font.info.postscriptFontName = instanceDescriptor.postScriptFontName # yikes, note the differences in capitalisation..
font.info.styleMapFamilyName = instanceDescriptor.styleMapFamilyName
font.info.styleMapStyleName = instanceDescriptor.styleMapStyleName
# calculate selected lib key values here
libMathMutator = self.getLibEntryMutator(discreteLocation=discreteLocation)
if self.debug:
self.logger.info(f"\t\t\tlibMathMutator \"{libMathMutator}\"\n\t\t\t{discreteLocation}")
if libMathMutator:
# use locHorizontal in case this was anisotropic.
# remember: libMathDict is a Location object,
# each key in the location is the libKey
# each value is the calculated value
libMathDict = libMathMutator.makeInstance(locHorizontal)
if libMathDict:
for libKey, mutatedValue in libMathDict.items():
# only add the value to the lib if it is not 0.
# otherwise it will always add it? Not sure?
font.lib[libKey] = mutatedValue
if self.debug:
self.logger.info(f"\t\t\tlibMathMutator: libKey \"{libKey}: {mutatedValue}")
defaultSourceFont = self.findDefaultFont(discreteLocation=discreteLocation)
# found a default source font
if defaultSourceFont:
# copy info
self._copyFontInfo(defaultSourceFont.info, font.info)
# copy lib
for key, value in defaultSourceFont.lib.items():
# don't overwrite the keys we calculated
if key in self.libKeysForProcessing: continue
font.lib[key] = value
# copy groups
for key, value in defaultSourceFont.groups.items():
font.groups[key] = value
# copy features
font.features.text = defaultSourceFont.features.text
# ok maybe now it is time to calculate some glyphs
# glyphs
if glyphNames:
selectedGlyphNames = glyphNames
else:
# since all glyphs are processed, decomposing components is unecessary
# maybe that's confusing and components should be decomposed anyway
# if decomposeComponents was set to True?
decomposeComponents = False
selectedGlyphNames = self.glyphNames
if 'public.glyphOrder' not in font.lib.keys():
# should be the glyphorder from the default, yes?
font.lib['public.glyphOrder'] = selectedGlyphNames
# remove skippable glyphs
toSkip = self.collectSkippedGlyphs()
selectedGlyphNames = [name for name in selectedGlyphNames if name not in toSkip]
for glyphName in selectedGlyphNames:
glyphMutator, unicodes = self.getGlyphMutator(glyphName, decomposeComponents=decomposeComponents, discreteLocation=discreteLocation)
if glyphMutator is None:
if self.debug:
note = f"makeInstance: Could not make mutator for glyph {glyphName}"
self.logger.info(note)
continue
font.newGlyph(glyphName)
font[glyphName].clear()
font[glyphName].unicodes = unicodes
try:
if not self.isAnisotropic(continuousLocation):
glyphInstanceObject = glyphMutator.makeInstance(continuousLocation, bend=bend)
else:
# split anisotropic location into horizontal and vertical components
horizontalGlyphInstanceObject = glyphMutator.makeInstance(locHorizontal, bend=bend)
verticalGlyphInstanceObject = glyphMutator.makeInstance(locVertical, bend=bend)
# merge them again in a beautiful single line:
glyphInstanceObject = (1, 0) * horizontalGlyphInstanceObject + (0, 1) * verticalGlyphInstanceObject
except IndexError:
# alignment problem with the data?
if self.debug:
note = "makeInstance: Quite possibly some sort of data alignment error in %s" % glyphName
self.logger.info(note)
continue
if self.roundGeometry:
try:
glyphInstanceObject = glyphInstanceObject.round()
except AttributeError:
# what are we catching here?
# math objects without a round method?
if self.debug:
note = f"makeInstance: no round method for {glyphInstanceObject} ?"
self.logger.info(note)
try:
# File "/Users/erik/code/ufoProcessor/Lib/ufoProcessor/__init__.py", line 649, in makeInstance
# glyphInstanceObject.extractGlyph(font[glyphName], onlyGeometry=True)
# File "/Applications/RoboFont.app/Contents/Resources/lib/python3.6/fontMath/mathGlyph.py", line 315, in extractGlyph
# glyph.anchors = [dict(anchor) for anchor in self.anchors]
# File "/Applications/RoboFont.app/Contents/Resources/lib/python3.6/fontParts/base/base.py", line 103, in __set__
# raise FontPartsError("no setter for %r" % self.name)
# fontParts.base.errors.FontPartsError: no setter for 'anchors'
if hasattr(font[glyphName], "fromMathGlyph"):
font[glyphName].fromMathGlyph(glyphInstanceObject)
else:
glyphInstanceObject.extractGlyph(font[glyphName], onlyGeometry=True)
except TypeError:
# this causes ruled glyphs to end up in the wrong glyphname
# but defcon2 objects don't support it
pPen = font[glyphName].getPointPen()
font[glyphName].clear()
glyphInstanceObject.drawPoints(pPen)
font[glyphName].width = glyphInstanceObject.width
# add designspace location to lib
font.lib['ufoProcessor.fullDesignspaceLocation'] = list(instanceDescriptor.getFullDesignLocation(self.doc).items())
if self.useVarlib:
font.lib['ufoProcessor.mathmodel'] = "fonttools.varlib"
else:
font.lib['ufoProcessor.mathmodel'] = "mutatorMath"
if self.debug:
self.logger.info(f"\t\t\t{len(selectedGlyphNames)} glyphs added")
return font
def locationToDescriptiveString(self, loc):
# make a nice descriptive string from the location
# Check if the discrete location is None.
t = []
cl, dl = self.splitLocation(loc)
for continuousAxis in sorted(cl.keys()):
t.append(f'{continuousAxis}_{cl[continuousAxis]}')
if dl is not None:
for discreteAxis in sorted(dl.keys()):
t.append(f'{discreteAxis}_{dl[discreteAxis]}')
return '_'.join(t)
def pathForInstance(self, instanceDescriptor):
# generate the complete path for this instance descriptor.
if self.path is not None and instanceDescriptor.filename is not None:
return os.path.abspath(os.path.join(os.path.dirname(self.path), instanceDescriptor.filename))
return None
def makeOneInstance(self, location,
doRules=None,
glyphNames=None,
decomposeComponents=False,
pairs=None,
bend=False):
# make one instance for this location. This is a shortcut for making an
# instanceDescriptor. So it makes some assumptions about the font names.
# Otherwise all the geometry will be exactly what it needs to be.
self.loadFonts()
continuousLocation, discreteLocation = self.splitLocation(location)
defaultFont = self.findDefaultFont(discreteLocation=discreteLocation)
if defaultFont is not None:
instanceFamilyName = defaultFont.info.familyName
else:
if self.doc.path is not None:
instanceFamilyName = os.path.splitext(self.doc.path)[0]
else:
instanceFamilyName = "UFOOperatorInstance"
tempInstanceDescriptor = InstanceDescriptor()
tempInstanceDescriptor.location = location
tempInstanceDescriptor.familyName = instanceFamilyName
tempInstanceDescriptor.styleName = self.locationToDescriptiveString(location)
return self.makeInstance(tempInstanceDescriptor, doRules=doRules, glyphNames=glyphNames, decomposeComponents=decomposeComponents, pairs=pairs, bend=bend)
def randomLocation(self, extrapolate=0, anisotropic=False, roundValues=True, discreteLocation=None):
"""A good random location, for quick testing and entertainment
extrapolate: is a factor of the (max-min) distance. 0 = nothing, 0.1 = 0.1 * (max - min)
anisotropic= True: *all* continuous axes get separate x, y values
for discrete axes: random choice from the defined values
for continuous axes: interpolated value between axis.minimum and axis.maximum
if discreteLocation is given, make a random location for the continuous part.
assuming we want this location for testing the ufoOperator machine:
we will eventually need a designspace location, not a userspace location.
"""
workLocation = {}
if discreteLocation:
workLocation.update(discreteLocation)
else:
for aD in self.getOrderedDiscreteAxes():
workLocation[aD.name] = random.choice(aD.values)
for aD in self.getOrderedContinuousAxes():
# use the map on the extremes to make sure we randomise between the proper extremes.
aD_minimum = aD.map_forward(aD.minimum)
aD_maximum = aD.map_forward(aD.maximum)
if extrapolate:
delta = (aD.maximum - aD.minimum)
extraMinimum = aD_minimum - extrapolate * delta
extraMaximum = aD_maximum + extrapolate * delta
else:
extraMinimum = aD_minimum
extraMaximum = aD_maximum
if anisotropic:
x = ip(extraMinimum, extraMaximum, random.random())
y = ip(extraMinimum, extraMaximum, random.random())
if roundValues:
x = round(x)
y = round(y)
workLocation[aD.name] = (x, y)
else:
v = ip(extraMinimum, extraMaximum, random.random())
if roundValues:
v = round(v)
workLocation[aD.name] = v
return workLocation
def getLocationsForFont(self, fontObj):
# returns the locations this fontObj is used at, in this designspace
# returns [], [] if the fontObj is not used at all
# returns [loc], [] if the fontObj has no discrete location.
# Note: this returns *a list* as one fontObj can be used at multiple locations in a designspace.
# Note: fontObj must have a path.
discreteLocations = []
continuousLocations = []
for s in self.sources:
if s.path == fontObj.path:
cl, dl = self.splitLocation(s.location)
discreteLocations.append(dl)
continuousLocations.append(cl)
return continuousLocations, discreteLocations
# @memoize
def makeFontProportions(self, location, bend=False, roundGeometry=True):
"""Calculate the basic font proportions for this location, to map out expectations for drawing"""
self.loadFonts()
continuousLocation, discreteLocation = self.splitLocation(location)
infoMutator = self.getInfoMutator(discreteLocation=discreteLocation)
data = dict(unitsPerEm=1000, ascender=750, descender=-250, xHeight=500)
if infoMutator is None:
return data
if not self.isAnisotropic(continuousLocation):
infoInstanceObject = infoMutator.makeInstance(continuousLocation, bend=bend)
else:
locHorizontal, locVertical = self.splitAnisotropic(continuousLocation)
horizontalInfoInstanceObject = infoMutator.makeInstance(locHorizontal, bend=bend)
verticalInfoInstanceObject = infoMutator.makeInstance(locVertical, bend=bend)
# merge them again
infoInstanceObject = (1, 0) * horizontalInfoInstanceObject + (0, 1) * verticalInfoInstanceObject
if roundGeometry:
infoInstanceObject = infoInstanceObject.round()
data = dict(unitsPerEm=infoInstanceObject.unitsPerEm, ascender=infoInstanceObject.ascender, descender=infoInstanceObject.descender, xHeight=infoInstanceObject.xHeight)
return data
@memoize
def makeOneGlyph(self, glyphName, location, decomposeComponents=True, useVarlib=False, roundGeometry=False, clip=False):
"""
glyphName:
location: location including discrete axes, in **designspace** coordinates.
decomposeComponents: decompose all components so we get a proper representation of the shape
useVarlib: use varlib as mathmodel. Otherwise it is mutatorMath
roundGeometry: round all geometry to integers
clip: restrict axis values to the defined minimum and maximum
+ Supports extrapolation for varlib and mutatormath: though the results can be different
+ Supports anisotropic locations for varlib and mutatormath. Obviously this will not be present in any Variable font exports.
Returns: a mathglyph, results are cached
"""
self.loadFonts()
continuousLocation, discreteLocation = self.splitLocation(location)
bend=False #
if not self.extrapolate:
# Axis values are in userspace, so this needs to happen *after* clipping.
continuousLocation = self.clipDesignLocation(continuousLocation)
# check if the discreteLocation, if there is one, is within limits
if discreteLocation is not None:
if not self.checkDiscreteAxisValues(discreteLocation):
if self.debug:
self.logger.info(f"\t\tmakeOneGlyph reports: {location} has illegal value for discrete location")
return None
previousModel = self.useVarlib
self.useVarlib = useVarlib
glyphInstanceObject = None
glyphMutator, unicodes = self.getGlyphMutator(glyphName, decomposeComponents=decomposeComponents, discreteLocation=discreteLocation)
if not glyphMutator: return None
try:
if not self.isAnisotropic(location):
glyphInstanceObject = glyphMutator.makeInstance(continuousLocation, bend=bend)
else:
if self.debug:
self.logger.info(f"\t\tmakeOneGlyph anisotropic location: {location}")
loc = Location(continuousLocation)
locHorizontal, locVertical = self.splitAnisotropic(loc)
# split anisotropic location into horizontal and vertical components
horizontalGlyphInstanceObject = glyphMutator.makeInstance(locHorizontal, bend=bend)
verticalGlyphInstanceObject = glyphMutator.makeInstance(locVertical, bend=bend)
# merge them again
glyphInstanceObject = (1, 0) * horizontalGlyphInstanceObject + (0, 1) * verticalGlyphInstanceObject
if self.debug:
self.logger.info(f"makeOneGlyph anisotropic glyphInstanceObject {glyphInstanceObject}")
except IndexError:
# alignment problem with the data?
if self.debug:
note = "makeOneGlyph: Quite possibly some sort of data alignment error in %s" % glyphName
self.logger.info(note)
return None
if glyphInstanceObject:
glyphInstanceObject.unicodes = unicodes
if roundGeometry:
glyphInstanceObject.round()
self.useVarlib = previousModel
return glyphInstanceObject
def makeOneInfo(self, location, roundGeometry=False, clip=False):
""" Make the fontMath.mathInfo object for this location.
You need to extract this to an instance font.
location: location including discrete axes, in **designspace** coordinates.
"""
if self.debug:
self.logger.info(f"\t\t\tmakeOneInfo for {location}")
self.loadFonts()
bend = False
anisotropic, continuousLocation, discreteLocation, locHorizontal, locVertical = self.getLocationType(location)
# so we can take the math object that comes out of the calculation
infoMutator = self.getInfoMutator(discreteLocation=discreteLocation)
infoInstanceObject = None
if infoMutator is not None:
if not anisotropic:
infoInstanceObject = infoMutator.makeInstance(continuousLocation, bend=bend)
else:
horizontalInfoInstanceObject = infoMutator.makeInstance(locHorizontal, bend=bend)
verticalInfoInstanceObject = infoMutator.makeInstance(locVertical, bend=bend)
# merge them again
infoInstanceObject = (1,0) * horizontalInfoInstanceObject + (0,1) * verticalInfoInstanceObject
if self.roundGeometry:
infoInstanceObject = infoInstanceObject.round()
if self.debug:
if infoInstanceObject is not None:
self.logger.info(f"\t\t\t\tmakeOneInfo outcome: {infoInstanceObject}")
else:
self.logger.info(f"\t\t\t\tmakeOneInfo outcome: None")
return infoInstanceObject
def makeOneKerning(self, location, pairs=None):
"""
Make the fontMath.mathKerning for this location.
location: location including discrete axes, in **designspace** coordinates.
pairs: a list of pairs, if you want to get a subset
"""
if self.debug:
self.logger.info(f"\t\t\tmakeOneKerning for {location}")
self.loadFonts()
bend = False
kerningObject = None
anisotropic, continuousLocation, discreteLocation, locHorizontal, locVertical = self.getLocationType(location)
if pairs:
try:
kerningMutator = self.getKerningMutator(pairs=pairs, discreteLocation=discreteLocation)
kerningObject = kerningMutator.makeInstance(locHorizontal, bend=bend)
except Exception:
note = f"makeOneKerning: Could not make kerning for {location}\n{traceback.format_exc()}"
if self.debug:
self.logger.info(note)
else:
kerningMutator = self.getKerningMutator(discreteLocation=discreteLocation)
if kerningMutator is not None:
kerningObject = kerningMutator.makeInstance(locHorizontal, bend=bend)
# extract the object later
if self.debug:
self.logger.info(f"\t\t\t\t{len(kerningObject.keys())} kerning pairs added")
if self.roundGeometry:
kerningObject.round()
if self.debug:
if kerningObject is not None:
self.logger.info(f"\t\t\t\tmakeOneKerning outcome: {kerningObject.items()}")
else:
self.logger.info(f"\t\t\t\tmakeOneKerning outcome: None")
return kerningObject
def _copyFontInfo(self, sourceInfo, targetInfo):
""" Copy the non-calculating fields from the source info."""
infoAttributes = [
"versionMajor",
"versionMinor",
"copyright",
"trademark",
"note",
"openTypeGaspRangeRecords",
"openTypeHeadCreated",
"openTypeHeadFlags",
"openTypeNameDesigner",
"openTypeNameDesignerURL",
"openTypeNameManufacturer",
"openTypeNameManufacturerURL",
"openTypeNameLicense",
"openTypeNameLicenseURL",
"openTypeNameVersion",
"openTypeNameUniqueID",
"openTypeNameDescription",
"#openTypeNamePreferredFamilyName",
"#openTypeNamePreferredSubfamilyName",
"#openTypeNameCompatibleFullName",
"openTypeNameSampleText",
"openTypeNameWWSFamilyName",
"openTypeNameWWSSubfamilyName",
"openTypeNameRecords",
"openTypeOS2Selection",
"openTypeOS2VendorID",
"openTypeOS2Panose",
"openTypeOS2FamilyClass",
"openTypeOS2UnicodeRanges",
"openTypeOS2CodePageRanges",
"openTypeOS2Type",
"postscriptIsFixedPitch",
"postscriptForceBold",
"postscriptDefaultCharacter",
"postscriptWindowsCharacterSet"
]
for infoAttribute in infoAttributes:
copy = False
if self.ufoVersion == 1 and infoAttribute in fontInfoAttributesVersion1:
copy = True
elif self.ufoVersion == 2 and infoAttribute in fontInfoAttributesVersion2:
copy = True
elif self.ufoVersion == 3 and infoAttribute in fontInfoAttributesVersion3:
copy = True
if copy:
value = getattr(sourceInfo, infoAttribute)
setattr(targetInfo, infoAttribute, value)
if __name__ == "__main__":
import time, random
from fontParts.world import RFont
ds5Path = "../../Tests/ds5/ds5.designspace"
dumpCacheLog = True
makeUFOs = True
debug = True
startTime = time.time()
if ds5Path is None:
doc = UFOOperator()
else:
doc = UFOOperator(ds5Path, useVarlib=True, debug=False)
print("Initialised without debug, no logger:", doc.logger)
doc.debug=True
print("Set debug to True, after initialisation:", doc.debug)
doc.loadFonts()
print("loadFonts checks and creates a logger if needed:", doc.logger)
# test the getLibEntryMutator
testLibMathKey = 'com.letterror.ufoOperator.libMathTestValue'
doc.libKeysForProcessing.append(testLibMathKey)
print('processing these keys', doc.libKeysForProcessing)
if makeUFOs:
doc.generateUFOs()
randomLocation = doc.randomLocation()
randomGlyphName = random.choice(doc.glyphNames)
res = doc.makeOneGlyph(randomGlyphName, location=randomLocation)
endTime = time.time()
duration = endTime - startTime
print(f"duration: {duration}" )
# make some font proportions
print(doc.makeFontProportions(randomLocation))
# some random locations
for i in range(10):
print(doc.randomLocation(extrapolate=0.1))
# this is what reverse component mapping looks like:
print("getReverseComponentMapping:")
print(doc.getReverseComponentMapping())
# these are all the discrete locations in this designspace
print("getDiscreteLocations()", doc.getDiscreteLocations())
for discreteLocation in doc.getDiscreteLocations():
s = doc.findDefault(discreteLocation)
print(f"default for discreteLocation {discreteLocation} {s}")
# include glyphs in which the glyph is used a component
print(doc.glyphChanged(randomGlyphName, includeDependencies=True))
# get a list of font objects
doc.loadFonts()
print(doc.glyphsInCache())
print(doc.clipDesignLocation(dict(width=(-1000, 2000))))
print("locationWillClip()", doc.locationWillClip(dict(width=(-1000, 2000))))
defaultLocation = doc.newDefaultLocation()
print("locationWillClip(default)", doc.locationWillClip(defaultLocation))
print('newDefaultLocation()', doc.newDefaultLocation(discreteLocation={'countedItems': 3.0, 'outlined': 1.0}))
print('newDefaultLocation()', doc.newDefaultLocation())
print("findDefaultFont()", doc.findDefaultFont().path)
print("findDefaultFont()", doc.findDefaultFont(discreteLocation={'countedItems': 3.0, 'outlined': 1.0}).path)
print("getNeutralFont()", doc.getNeutralFont().path)
print("getNeutralFont()", doc.getNeutralFont(discreteLocation={'countedItems': 3.0, 'outlined': 1.0}).path)
# generate instances with a limited set of decomposed glyphs
# (useful for quick previews)
glyph_names = ["glyphTwo"]
instanceCounter = 1
for instanceDescriptor in doc.instances:
instance = doc.makeInstance(instanceDescriptor, glyphNames=glyph_names, decomposeComponents=True)
print("-"*100+"\n"+f"Generated instance {instanceCounter} at {instanceDescriptor.getFullDesignLocation(doc)} with decomposed partial glyph set: {','.join(instance.keys())}")
for name in glyph_names:
glyph = instance[name]
print(f"- {glyph.name} countours:{len(glyph)}, components: {len(glyph.components)}")
print()
instanceCounter+=1
# component related dependencies
glyphName = "glyphOne"
dependencies = doc.getGlyphDependencies(glyphName)
print(f"{glyphName} dependencies: {dependencies}")
# make kerning for one location, for a subset of glyphs
randomLocation = doc.randomLocation()
kerns = doc.makeOneKerning(randomLocation, pairs=[('glyphOne', 'glyphTwo')])
print('kerns', kerns.items(), "at randomLocation", randomLocation)
for i in range(30):
print('random location to string', doc.locationToDescriptiveString(doc.randomLocation()))
instanceFontObj = doc.makeOneInstance(randomLocation)
instanceFontName = doc.locationToDescriptiveString(randomLocation)
print("instanceFontObj", instanceFontObj)
testInstanceSavePath = f"../../Tests/ds5/makeOneInstanceOutput_{instanceFontObj.info.familyName}-{instanceFontName}.ufo"
instanceFontObj.save(testInstanceSavePath)
# make font info for one location
randomLocation = doc.randomLocation()
info = doc.makeOneInfo(randomLocation)
outFont = RFont()
print(type(outFont))
outFont.info.fromMathInfo(info)
print('info', outFont.info, "at randomLocation", randomLocation)
for f, loc in doc.getFonts():
continuousLocs, discreteLocs = doc.getLocationsForFont(f)
testLoc = continuousLocs[0]
testLoc.update(discreteLocs[0])
print(f, testLoc == loc)
print(doc.getOrderedDiscreteAxes())
for loc, fontObj in doc.fonts.items():
print("uses", fontObj.path, doc.usesFont(fontObj))
newFontObj = RFont()
print(doc.usesFont(newFontObj))
print(doc.findAllDefaults())
# the ds5 test fonts have a value for the italic slant offset.
for discreteLocation in doc.getDiscreteLocations():
m = doc.getLibEntryMutator(discreteLocation=discreteLocation)
if m:
randomLocation = doc.randomLocation()
print('italicslantoffset at', randomLocation, m.makeInstance(randomLocation))
else:
print("getLibEntryMutator() returned None.")
for instanceDescriptor in doc.instances:
print('path for instancedescriptor', doc.pathForInstance(instanceDescriptor))
doc.collectSkippedGlyphs() ufoProcessor-1.13.3/Lib/ufoProcessor/varModels.py 0000664 0000000 0000000 00000015620 14724542715 0022014 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
from __future__ import print_function, division, absolute_import
from fontTools.varLib.models import VariationModel, normalizeLocation
# alternative axisMapper that uses map_forward and map_backward from fonttools
class AxisMapper(object):
def __init__(self, axes):
# axes: list of axis axisdescriptors
self.axisOrder = [a.name for a in axes]
self.axisDescriptors = {}
for a in axes:
self.axisDescriptors[a.name] = a
def getMappedAxisValues(self):
values = {}
for axisName in self.axisOrder:
a = self.axisDescriptors[axisName]
values[axisName] = a.map_forward(a.minimum), a.map_forward(a.default), a.map_forward(a.maximum)
return values
def __call__(self, location):
return self.map_forward(location)
def _normalize(self, location):
new = {}
for axisName in location.keys():
new[axisName] = normalizeLocation(dict(w=location[axisName]), dict(w=self.axes[axisName]))
return new
def map_backward(self, location):
new = {}
for axisName in location.keys():
if not axisName in self.axisOrder:
continue
if axisName not in location:
continue
new[axisName] = self.axisDescriptors[axisName].map_backward(location[axisName])
return new
def map_forward(self, location):
new = {}
for axisName in location.keys():
if not axisName in self.axisOrder:
continue
if axisName not in location:
continue
new[axisName] = self.axisDescriptors[axisName].map_forward(location[axisName])
return new
class VariationModelMutator(object):
""" a thing that looks like a mutator on the outside,
but uses the fonttools varlib logic to calculate.
"""
def __init__(self, items, axes, model=None, extrapolate=True):
# items: list of locationdict, value tuples
# axes: list of axis dictionaries, not axisdescriptor objects.
# model: a model, if we want to share one
self.extrapolate = extrapolate
self.axisOrder = [a.name for a in axes]
self.axisMapper = AxisMapper(axes)
self.axes = {}
for a in axes:
axisMinimum, axisMaximum = self.getAxisMinMax(a)
mappedMinimum, mappedDefault, mappedMaximum = a.map_forward(axisMinimum), a.map_forward(a.default), a.map_forward(axisMaximum)
self.axes[a.name] = (mappedMinimum, mappedDefault, mappedMaximum)
if model is None:
dd = [self._normalize(a) for a,b in items]
ee = self.axisOrder
self.model = VariationModel(dd, axisOrder=ee, extrapolate=self.extrapolate)
else:
self.model = model
self.masters = [b for a, b in items]
self.locations = [a for a, b in items]
def getAxisMinMax(self, axis):
# return tha axis.minimum and axis.maximum for continuous axes
# return the min(axis.values), max(axis.values) for discrete axes
if hasattr(axis, "values"):
return min(axis.values), max(axis.values)
return axis.minimum, axis.maximum
def get(self, key):
if key in self.model.locations:
i = self.model.locations.index(key)
return self.masters[i]
return None
def getFactors(self, location):
nl = self._normalize(location)
return self.model.getScalars(nl)
def getMasters(self):
return self.masters
def getSupports(self):
return self.model.supports
def getReach(self):
items = []
for supportIndex, s in enumerate(self.getSupports()):
sortedOrder = self.model.reverseMapping[supportIndex]
items.append((self.masters[sortedOrder], s))
return items
def makeInstance(self, location, bend=False):
# check for anisotropic locations here
if bend:
location = self.axisMapper(location)
nl = self._normalize(location)
return self.model.interpolateFromMasters(nl, self.masters)
def _normalize(self, location):
return normalizeLocation(location, self.axes)
if __name__ == "__main__":
from fontTools.designspaceLib import AxisDescriptor
a = AxisDescriptor()
a.name = "A"
a.tag = "A___"
a.minimum = 40
a.default = 45
a.maximum = 50
a.map = [(40, -100), (45,0), (50, 100)]
b = AxisDescriptor()
b.name = "B"
b.tag = "B___"
b.minimum = 0
b.default = 50
b.maximum = 100
axes = [a,b]
items = [
({}, 0),
#({'A': 50, 'B': 50}, 10),
({'A': 40}, 10),
({'B': 50}, -10),
#({'B': -100}, -10), # this will fail, no extrapolating
({'A': 40, 'B': 50}, 22),
#({'A': 55, 'B': 75}, 1),
#({'A': 65, 'B': 99}, 1),
]
am = AxisMapper(axes)
#assert am(dict(A=0)) == {'A': 45}
print(1, am(dict(A=40, B=None)))
#assert am(dict(A=0, B=100)) == {'A': 45}
# mm = VariationModelMutator(items, axes)
# assert mm.makeInstance(dict(A=0, B=0)) == 0
# assert mm.makeInstance(dict(A=100, B=0)) == 10
# assert mm.makeInstance(dict(A=0, B=100)) == 10
# assert mm.makeInstance(dict(A=100, B=100)) == 0
# assert mm.makeInstance(dict(A=50, B=0),bend=False) == 5
# assert mm.makeInstance(dict(A=50, B=0),bend=True) == 2.5
# mm.getReach()
a = AxisDescriptor()
a.name = "Weight"
a.tag = "wght"
a.minimum = 300
a.default = 300
a.maximum = 600
a.map = ((300,0), (600,1000))
b = AxisDescriptor()
b.name = "Width"
b.tag = "wdth"
b.minimum = 200
b.default = 800
b.maximum = 800
b.map = ((200,5), (800,10))
axes = [a,b]
aam = AxisMapper(axes)
print(2, aam({}))
print(2, aam(dict(Weight=300, Width=200)))
print(2, aam(dict(Weight=0, Width=0)))
print(2, 'getMappedAxisValues', aam.getMappedAxisValues())
print(2, aam.map_forward({'Weight': 0}))
# fine. sources are in user values. Progress.
# are they?
items = [
({}, 13),
({'Weight': 0, 'Width': 5}, 20),
({'Weight': 1000, 'Width': 10}, 60),
]
mm = VariationModelMutator(items, axes)
# ok so normalise uses designspace coordinates
print(3, "_normalize", mm._normalize(dict(Weight=0, Width=1000)))
# oh wow, master locations need to be in user coordinates!?
print('mm.makeInstance(dict())', mm.makeInstance(dict()))
assert mm.makeInstance(dict()) == 13
assert mm.makeInstance(dict(Weight=0, Width=10)) == 13
l = dict(Weight=400, Width=20)
lmapped = aam(l)
print('0 loc', l)
print('0 loc mapped', lmapped)
print('1 with map', mm.makeInstance(l, bend=True))
print('1 without map', mm.makeInstance(l, bend=False))
print('2 with map', mm.makeInstance(lmapped, bend=True))
print('2 without map', mm.makeInstance(lmapped, bend=False))
ufoProcessor-1.13.3/README.md 0000664 0000000 0000000 00000010104 14724542715 0015556 0 ustar 00root root 0000000 0000000 [](https://pypi.org/project/ufoprocessor)
# ufoProcessor
Python package based on the **designSpaceDocument** from [fontTools.designspaceLib](https://github.com/fonttools/fonttools/tree/master/Lib/fontTools/designspaceLib)) specifically to _process_ and _generate_ instances for UFO files, glyphs and other data.
## UFOOperator takes over from UfoProcessor
Some deep changes were necessary to support designspace format 5 files. Rather than try to work it into unwilling old code, UFOOperator is rewritten from the bottom up. The new object *wraps* the FontTools DesignSpaceDocument object, rather than *subclassing* it. The goals for UFOOperator remain largely the same. Though the emphasis is now on providing objects that provide live interpolation, rather than generate stand alone UFOs.
* Support designspace format 5, with layers. *Not all elements of designspace 5 are supported.*
* Collect source materials
* Provide mutators for specific glyphs, font info, kerning so that other tools can generate partial instances. Either from `MutatorMath` or `fonttools varlib.model`.
* Support anisotropic locations in both math models, even if Variable Fonts won't. These are super useful during design, so I need them to work.
* Support extrapolation in both math models, even if Variable Fonts won't. Note that MutatorMath and VarLib approach extrapolation differently. Useful for design-explorations.
* Support discrete axes. This is a bit complex, but it is nice when it works.
* Apply avar-like designspace bending
* Generate actual UFO instances in formats 3.
* Round geometry as requested
* Try to stay up to date with fontTools
* Baseclass for tools that need access to designspace data.
* Some caching of MutatorMath and Varlib flavored mutators.
## No more rules
UFOProcessor could execute *some* of the feature-variation rules when it generated UFOs. But these variations has become much more complex than can be faked with simple glyph-swapping. So I did not port that to UFOOperator. UFOs generated with UFOOperator will have all the glyphs in the same places as their sources.
## Discrete axes
A *discrete axis* is a way to fit different interpolating systems into a single designspace. For instance a *roman* could be on ```italic=0``` and the *italic* on ```italic=1```. The roman and italic are in the same file, but to UFOOperator they are separate systems that interpolate along the normal axes. If is are more than one discrete axis in a designspace, each combination of the axis values, each *discrete location* can be an interpolating system. So some of the methods of UFOOperator require a `discrete location` to know which interpolating system is needed.
## Examples UFOProcessor (old)
Generate all the instances (using the varlib model, no rounding):
```python
import ufoProcessor
myPath = "myDesignspace.designspace"
ufoProcessor.build(myPath)
```
Generate all the instances (using the varlib model, but round all the geometry to integers):
```python
import ufoProcessor
myPath = "myDesignspace.designspace"
ufoProcessor.build(myPath, roundGeometry=True)
```
Generate all the instances (using the mutatormath model, no rounding):
```python
import ufoProcessor
myPath = "myDesignspace.designspace"
ufoProcessor.build(myPath, useVarlib=False)
```
Generate an instance for one glyph, `"A"` at `width=100, weight=200`. (assuming the designspace has those axes and the masters have that glyph)
```python
import ufoProcessor
myPath = "myDesignspace.designspace"
doc = ufoProcessor.DesignSpaceProcessor()
doc.read(myPath)
doc.loadFonts()
glyphMutator = doc.getGlyphMutator("A")
instance = glyphMutator.makeInstance(Location(width=100, weight=200)
```
Depending on the setting for `usevarlib`, the `glyphMutator` object returned by `getGlyphMutator` in the example above can either be a `MutatorMath.Mutator`, or a `VariationModelMutator` object. That uses the `fontTools.varLib.models.VariationModel` but it is wrapped and can be called as a Mutator object to generate instances. This way `DesignSpaceProcessor` does not need to know much about which math model it is using.
ufoProcessor-1.13.3/Tests/ 0000775 0000000 0000000 00000000000 14724542715 0015405 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/ 0000775 0000000 0000000 00000000000 14724542715 0016077 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/doc_version_test.py 0000664 0000000 0000000 00000001050 14724542715 0022016 0 ustar 00root root 0000000 0000000 # test designspacedocument versioning
from fontTools.designspaceLib import DesignSpaceDocument, SourceDescriptor, InstanceDescriptor, AxisDescriptor, RuleDescriptor, processRules, DiscreteAxisDescriptor
doc = DesignSpaceDocument()
doc.formatVersion = "4.0"
print(doc.formatTuple)
a1 = AxisDescriptor()
a1.minimum = 400
a1.maximum = 1000
a1.default = 400
#a1.map = ((400,400), (700,900), (1000,1000))
a1.name = "width"
a1.tag = "wdth"
#a1.axisOrdering = 1
doc.addAxis(a1)
path = "ds4_version_test.designspace"
print(doc.formatTuple)
doc.write(path)
ufoProcessor-1.13.3/Tests/ds4/ds4.designspace 0000664 0000000 0000000 00000006013 14724542715 0021000 0 ustar 00root root 0000000 0000000
ufoProcessor-1.13.3/Tests/ds4/ds4_makeTestDoc.py 0000664 0000000 0000000 00000004440 14724542715 0021430 0 ustar 00root root 0000000 0000000 # make a test designspace format 4 with 1 continuous axis
# shouls save as format 4?
# axis width is a normal interpolation with a change in width
from fontTools.designspaceLib import DesignSpaceDocument, SourceDescriptor, InstanceDescriptor, AxisDescriptor, RuleDescriptor, processRules, DiscreteAxisDescriptor
from fontTools.designspaceLib.split import splitInterpolable
import os
import fontTools
print(fontTools.version)
import ufoProcessor
print(ufoProcessor.__file__)
import ufoProcessor.ufoOperator
#doc = DesignSpaceDocument()
doc = ufoProcessor.ufoOperator.UFOOperator()
doc.formatVersion = "4.0"
print(doc.formatVersion)
#https://fonttools.readthedocs.io/en/latest/designspaceLib/python.html#axisdescriptor
a1 = AxisDescriptor()
a1.minimum = 400
a1.maximum = 1000
a1.default = 400
a1.map = ((400,400), (700,900), (1000,1000))
a1.name = "width"
a1.tag = "wdth"
#a1.axisOrdering = 1 # if we add this the doc version will go to 5.0
doc.addAxis(a1)
default = {a1.name: a1.default}
# add sources
for c in [a1.minimum, a1.maximum]:
s1 = SourceDescriptor()
s1.path = os.path.join("sources", f"geometrySource_c_{c}.ufo")
s1.name = f"geometrySource{c}"
sourceLocation = dict(width=c)
s1.location = sourceLocation
s1.kerning = True
s1.familyName = "SourceFamilyName"
if default == sourceLocation:
s1.copyGroups = True
s1.copyFeatures = True
s1.copyInfo = True
if c == 400:
tc = "Narrow"
elif c == 1000:
tc = "Wide"
else:
tc = f"weight_{c}"
s1.styleName = tc
doc.addSource(s1)
# add instances
extrapolateAmount = 100
interestingWeightValues = [a1.minimum-extrapolateAmount, (400, 1200), 300, 400, 550, 700, a1.maximum, a1.maximum+extrapolateAmount]
for c in interestingWeightValues:
s1 = InstanceDescriptor()
s1.path = os.path.join("instances", f"geometryInstance_c_{c}.ufo")
s1.location = dict(width=c)
s1.familyName = "InstanceFamilyName"
if c == 400:
tc = "Narrow"
elif c == 1000:
tc = "Wide"
else:
tc = f"weight_{c}"
s1.name = f"geometryInstance"
s1.styleName = tc
s1.kerning = True
s1.info = True
doc.addInstance(s1)
path = "ds4.designspace"
print(doc.formatVersion)
doc.write(path)
for s in doc.sources:
print(s.location)
# ok. now about generating the instances.
#udoc = ufoProcessor.ufoOperator.UFOOperator(path)
#udoc.read(path)
#udoc.loadFonts()
#udoc.generateUFOs()
ufoProcessor-1.13.3/Tests/ds4/ds4_test_designspaceProblems.py 0000664 0000000 0000000 00000000533 14724542715 0024254 0 ustar 00root root 0000000 0000000 import designspaceProblems
print(designspaceProblems.__file__)
path= "ds5.designspace"
checker = designspaceProblems.DesignSpaceChecker(path)
checker.checkEverything()
print(checker.checkDesignSpaceGeometry())
checker.checkSources()
checker.checkInstances()
print("hasStructuralProblems", checker.hasStructuralProblems())
print(checker.problems) ufoProcessor-1.13.3/Tests/ds4/ds4_version_test.designspace 0000664 0000000 0000000 00000000263 14724542715 0023605 0 ustar 00root root 0000000 0000000
ufoProcessor-1.13.3/Tests/ds4/instances/ 0000775 0000000 0000000 00000000000 14724542715 0020066 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_(400, 1200).ufo/ 0000775 0000000 0000000 00000000000 14724542715 0025623 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_(400, 1200).ufo/features.fea 0000664 0000000 0000000 00000000067 14724542715 0030121 0 ustar 00root root 0000000 0000000 # features from ufo: geometryMaster_c_400_d1_1_d2_0.ufo ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_(400, 1200).ufo/fontinfo.plist 0000664 0000000 0000000 00000003075 14724542715 0030527 0 ustar 00root root 0000000 0000000
ascender400capHeight400copyright# font.info from ufo: geometryMaster_c_400_d1_1_d2_0.ufodescender-200familyNameInstanceFamilyNameguidelinesopenTypeHheaAscender1036openTypeHheaDescender-335openTypeOS2TypoAscender730openTypeOS2TypoDescender-270openTypeOS2WinAscent1036openTypeOS2WinDescent335postscriptBlueFuzz0postscriptBlueScale0.22postscriptBlueValues100110postscriptFamilyBluespostscriptFamilyOtherBluespostscriptOtherBluespostscriptStemSnapHpostscriptStemSnapVstyleNameweight_(400, 1200)unitsPerEm1000.0xHeight200
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_(400, 1200).ufo/glyphs/ 0000775 0000000 0000000 00000000000 14724542715 0027131 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_(400, 1200).ufo/glyphs/contents.plist 0000664 0000000 0000000 00000000470 14724542715 0032044 0 ustar 00root root 0000000 0000000
glyphOneglyphO_ne.glifglyphTwoglyphT_wo.glif
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_(400, 1200).ufo/glyphs/glyphO_ne.glif 0000664 0000000 0000000 00000001111 14724542715 0031712 0 ustar 00root root 0000000 0000000
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_(400, 1200).ufo/glyphs/glyphT_wo.glif 0000664 0000000 0000000 00000000300 14724542715 0031741 0 ustar 00root root 0000000 0000000
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_(400, 1200).ufo/groups.plist 0000664 0000000 0000000 00000000673 14724542715 0030225 0 ustar 00root root 0000000 0000000
public.kern1.groupAglyphOneglyphTwopublic.kern2.groupBglyphThreeglyphFour
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_(400, 1200).ufo/kerning.plist 0000664 0000000 0000000 00000000774 14724542715 0030345 0 ustar 00root root 0000000 0000000
glyphOneglyphOne400glyphTwo100glyphTwoglyphOne-100glyphTwo-400
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_(400, 1200).ufo/layercontents.plist 0000664 0000000 0000000 00000000437 14724542715 0031576 0 ustar 00root root 0000000 0000000
public.defaultglyphs
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_(400, 1200).ufo/lib.plist 0000664 0000000 0000000 00000000472 14724542715 0027451 0 ustar 00root root 0000000 0000000
public.glyphOrderglyphOneglyphTwo
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_(400, 1200).ufo/metainfo.plist 0000664 0000000 0000000 00000000476 14724542715 0030511 0 ustar 00root root 0000000 0000000
creatorcom.github.fonttools.ufoLibformatVersion3
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_100.ufo/ 0000775 0000000 0000000 00000000000 14724542715 0025060 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_100.ufo/features.fea 0000664 0000000 0000000 00000000067 14724542715 0027356 0 ustar 00root root 0000000 0000000 # features from ufo: geometryMaster_c_400_d1_1_d2_0.ufo ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_100.ufo/fontinfo.plist 0000664 0000000 0000000 00000003065 14724542715 0027763 0 ustar 00root root 0000000 0000000
ascender400capHeight400copyright# font.info from ufo: geometryMaster_c_400_d1_1_d2_0.ufodescender-200familyNameInstanceFamilyNameguidelinesopenTypeHheaAscender1036openTypeHheaDescender-335openTypeOS2TypoAscender730openTypeOS2TypoDescender-270openTypeOS2WinAscent1036openTypeOS2WinDescent335postscriptBlueFuzz0postscriptBlueScale0.22postscriptBlueValues100110postscriptFamilyBluespostscriptFamilyOtherBluespostscriptOtherBluespostscriptStemSnapHpostscriptStemSnapVstyleNameweight_100unitsPerEm1000.0xHeight200
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_100.ufo/glyphs/ 0000775 0000000 0000000 00000000000 14724542715 0026366 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_100.ufo/glyphs/contents.plist 0000664 0000000 0000000 00000000470 14724542715 0031301 0 ustar 00root root 0000000 0000000
glyphOneglyphO_ne.glifglyphTwoglyphT_wo.glif
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_100.ufo/glyphs/glyphO_ne.glif 0000664 0000000 0000000 00000001111 14724542715 0031147 0 ustar 00root root 0000000 0000000
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_100.ufo/glyphs/glyphT_wo.glif 0000664 0000000 0000000 00000000300 14724542715 0031176 0 ustar 00root root 0000000 0000000
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_100.ufo/groups.plist 0000664 0000000 0000000 00000000673 14724542715 0027462 0 ustar 00root root 0000000 0000000
public.kern1.groupAglyphOneglyphTwopublic.kern2.groupBglyphThreeglyphFour
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_100.ufo/kerning.plist 0000664 0000000 0000000 00000000774 14724542715 0027602 0 ustar 00root root 0000000 0000000
glyphOneglyphOne400glyphTwo100glyphTwoglyphOne-100glyphTwo-400
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_100.ufo/layercontents.plist 0000664 0000000 0000000 00000000437 14724542715 0031033 0 ustar 00root root 0000000 0000000
public.defaultglyphs
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_100.ufo/lib.plist 0000664 0000000 0000000 00000000472 14724542715 0026706 0 ustar 00root root 0000000 0000000
public.glyphOrderglyphOneglyphTwo
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_100.ufo/metainfo.plist 0000664 0000000 0000000 00000000476 14724542715 0027746 0 ustar 00root root 0000000 0000000
creatorcom.github.fonttools.ufoLibformatVersion3
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1000.ufo/ 0000775 0000000 0000000 00000000000 14724542715 0025140 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1000.ufo/features.fea 0000664 0000000 0000000 00000000067 14724542715 0027436 0 ustar 00root root 0000000 0000000 # features from ufo: geometryMaster_c_400_d1_1_d2_0.ufo ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1000.ufo/fontinfo.plist 0000664 0000000 0000000 00000003057 14724542715 0030044 0 ustar 00root root 0000000 0000000
ascender400capHeight400copyright# font.info from ufo: geometryMaster_c_400_d1_1_d2_0.ufodescender-200familyNameInstanceFamilyNameguidelinesopenTypeHheaAscender1036openTypeHheaDescender-335openTypeOS2TypoAscender730openTypeOS2TypoDescender-270openTypeOS2WinAscent1036openTypeOS2WinDescent335postscriptBlueFuzz0postscriptBlueScale0.22postscriptBlueValues100110postscriptFamilyBluespostscriptFamilyOtherBluespostscriptOtherBluespostscriptStemSnapHpostscriptStemSnapVstyleNameWideunitsPerEm1000.0xHeight200
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1000.ufo/glyphs/ 0000775 0000000 0000000 00000000000 14724542715 0026446 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1000.ufo/glyphs/contents.plist 0000664 0000000 0000000 00000000470 14724542715 0031361 0 ustar 00root root 0000000 0000000
glyphOneglyphO_ne.glifglyphTwoglyphT_wo.glif
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1000.ufo/glyphs/glyphO_ne.glif 0000664 0000000 0000000 00000001113 14724542715 0031231 0 ustar 00root root 0000000 0000000
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1000.ufo/glyphs/glyphT_wo.glif 0000664 0000000 0000000 00000000300 14724542715 0031256 0 ustar 00root root 0000000 0000000
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1000.ufo/groups.plist 0000664 0000000 0000000 00000000673 14724542715 0027542 0 ustar 00root root 0000000 0000000
public.kern1.groupAglyphOneglyphTwopublic.kern2.groupBglyphThreeglyphFour
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1000.ufo/kerning.plist 0000664 0000000 0000000 00000000774 14724542715 0027662 0 ustar 00root root 0000000 0000000
glyphOneglyphOne400glyphTwo800glyphTwoglyphOne-800glyphTwo-400
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1000.ufo/layercontents.plist 0000664 0000000 0000000 00000000437 14724542715 0031113 0 ustar 00root root 0000000 0000000
public.defaultglyphs
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1000.ufo/lib.plist 0000664 0000000 0000000 00000000472 14724542715 0026766 0 ustar 00root root 0000000 0000000
public.glyphOrderglyphOneglyphTwo
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1000.ufo/metainfo.plist 0000664 0000000 0000000 00000000476 14724542715 0030026 0 ustar 00root root 0000000 0000000
creatorcom.github.fonttools.ufoLibformatVersion3
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1100.ufo/ 0000775 0000000 0000000 00000000000 14724542715 0025141 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1100.ufo/features.fea 0000664 0000000 0000000 00000000067 14724542715 0027437 0 ustar 00root root 0000000 0000000 # features from ufo: geometryMaster_c_400_d1_1_d2_0.ufo ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1100.ufo/fontinfo.plist 0000664 0000000 0000000 00000003066 14724542715 0030045 0 ustar 00root root 0000000 0000000
ascender400capHeight400copyright# font.info from ufo: geometryMaster_c_400_d1_1_d2_0.ufodescender-200familyNameInstanceFamilyNameguidelinesopenTypeHheaAscender1036openTypeHheaDescender-335openTypeOS2TypoAscender730openTypeOS2TypoDescender-270openTypeOS2WinAscent1036openTypeOS2WinDescent335postscriptBlueFuzz0postscriptBlueScale0.22postscriptBlueValues100110postscriptFamilyBluespostscriptFamilyOtherBluespostscriptOtherBluespostscriptStemSnapHpostscriptStemSnapVstyleNameweight_1100unitsPerEm1000.0xHeight200
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1100.ufo/glyphs/ 0000775 0000000 0000000 00000000000 14724542715 0026447 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1100.ufo/glyphs/contents.plist 0000664 0000000 0000000 00000000470 14724542715 0031362 0 ustar 00root root 0000000 0000000
glyphOneglyphO_ne.glifglyphTwoglyphT_wo.glif
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1100.ufo/glyphs/glyphO_ne.glif 0000664 0000000 0000000 00000001113 14724542715 0031232 0 ustar 00root root 0000000 0000000
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1100.ufo/glyphs/glyphT_wo.glif 0000664 0000000 0000000 00000000300 14724542715 0031257 0 ustar 00root root 0000000 0000000
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1100.ufo/groups.plist 0000664 0000000 0000000 00000000673 14724542715 0027543 0 ustar 00root root 0000000 0000000
public.kern1.groupAglyphOneglyphTwopublic.kern2.groupBglyphThreeglyphFour
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1100.ufo/kerning.plist 0000664 0000000 0000000 00000000774 14724542715 0027663 0 ustar 00root root 0000000 0000000
glyphOneglyphOne400glyphTwo800glyphTwoglyphOne-800glyphTwo-400
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1100.ufo/layercontents.plist 0000664 0000000 0000000 00000000437 14724542715 0031114 0 ustar 00root root 0000000 0000000
public.defaultglyphs
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1100.ufo/lib.plist 0000664 0000000 0000000 00000000472 14724542715 0026767 0 ustar 00root root 0000000 0000000
public.glyphOrderglyphOneglyphTwo
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_1100.ufo/metainfo.plist 0000664 0000000 0000000 00000000476 14724542715 0030027 0 ustar 00root root 0000000 0000000
creatorcom.github.fonttools.ufoLibformatVersion3
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_300.ufo/ 0000775 0000000 0000000 00000000000 14724542715 0025062 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_300.ufo/features.fea 0000664 0000000 0000000 00000000067 14724542715 0027360 0 ustar 00root root 0000000 0000000 # features from ufo: geometryMaster_c_400_d1_1_d2_0.ufo ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_300.ufo/fontinfo.plist 0000664 0000000 0000000 00000003065 14724542715 0027765 0 ustar 00root root 0000000 0000000
ascender400capHeight400copyright# font.info from ufo: geometryMaster_c_400_d1_1_d2_0.ufodescender-200familyNameInstanceFamilyNameguidelinesopenTypeHheaAscender1036openTypeHheaDescender-335openTypeOS2TypoAscender730openTypeOS2TypoDescender-270openTypeOS2WinAscent1036openTypeOS2WinDescent335postscriptBlueFuzz0postscriptBlueScale0.22postscriptBlueValues100110postscriptFamilyBluespostscriptFamilyOtherBluespostscriptOtherBluespostscriptStemSnapHpostscriptStemSnapVstyleNameweight_300unitsPerEm1000.0xHeight200
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_300.ufo/glyphs/ 0000775 0000000 0000000 00000000000 14724542715 0026370 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_300.ufo/glyphs/contents.plist 0000664 0000000 0000000 00000000470 14724542715 0031303 0 ustar 00root root 0000000 0000000
glyphOneglyphO_ne.glifglyphTwoglyphT_wo.glif
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_300.ufo/glyphs/glyphO_ne.glif 0000664 0000000 0000000 00000001111 14724542715 0031151 0 ustar 00root root 0000000 0000000
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_300.ufo/glyphs/glyphT_wo.glif 0000664 0000000 0000000 00000000300 14724542715 0031200 0 ustar 00root root 0000000 0000000
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_300.ufo/groups.plist 0000664 0000000 0000000 00000000673 14724542715 0027464 0 ustar 00root root 0000000 0000000
public.kern1.groupAglyphOneglyphTwopublic.kern2.groupBglyphThreeglyphFour
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_300.ufo/kerning.plist 0000664 0000000 0000000 00000000774 14724542715 0027604 0 ustar 00root root 0000000 0000000
glyphOneglyphOne400glyphTwo100glyphTwoglyphOne-100glyphTwo-400
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_300.ufo/layercontents.plist 0000664 0000000 0000000 00000000437 14724542715 0031035 0 ustar 00root root 0000000 0000000
public.defaultglyphs
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_300.ufo/lib.plist 0000664 0000000 0000000 00000000472 14724542715 0026710 0 ustar 00root root 0000000 0000000
public.glyphOrderglyphOneglyphTwo
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_300.ufo/metainfo.plist 0000664 0000000 0000000 00000000476 14724542715 0027750 0 ustar 00root root 0000000 0000000
creatorcom.github.fonttools.ufoLibformatVersion3
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_400.ufo/ 0000775 0000000 0000000 00000000000 14724542715 0025063 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_400.ufo/features.fea 0000664 0000000 0000000 00000000067 14724542715 0027361 0 ustar 00root root 0000000 0000000 # features from ufo: geometryMaster_c_400_d1_1_d2_0.ufo ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_400.ufo/fontinfo.plist 0000664 0000000 0000000 00000003061 14724542715 0027762 0 ustar 00root root 0000000 0000000
ascender400capHeight400copyright# font.info from ufo: geometryMaster_c_400_d1_1_d2_0.ufodescender-200familyNameInstanceFamilyNameguidelinesopenTypeHheaAscender1036openTypeHheaDescender-335openTypeOS2TypoAscender730openTypeOS2TypoDescender-270openTypeOS2WinAscent1036openTypeOS2WinDescent335postscriptBlueFuzz0postscriptBlueScale0.22postscriptBlueValues100110postscriptFamilyBluespostscriptFamilyOtherBluespostscriptOtherBluespostscriptStemSnapHpostscriptStemSnapVstyleNameNarrowunitsPerEm1000.0xHeight200
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_400.ufo/glyphs/ 0000775 0000000 0000000 00000000000 14724542715 0026371 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_400.ufo/glyphs/contents.plist 0000664 0000000 0000000 00000000470 14724542715 0031304 0 ustar 00root root 0000000 0000000
glyphOneglyphO_ne.glifglyphTwoglyphT_wo.glif
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_400.ufo/glyphs/glyphO_ne.glif 0000664 0000000 0000000 00000001111 14724542715 0031152 0 ustar 00root root 0000000 0000000
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_400.ufo/glyphs/glyphT_wo.glif 0000664 0000000 0000000 00000000300 14724542715 0031201 0 ustar 00root root 0000000 0000000
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_400.ufo/groups.plist 0000664 0000000 0000000 00000000673 14724542715 0027465 0 ustar 00root root 0000000 0000000
public.kern1.groupAglyphOneglyphTwopublic.kern2.groupBglyphThreeglyphFour
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_400.ufo/kerning.plist 0000664 0000000 0000000 00000000774 14724542715 0027605 0 ustar 00root root 0000000 0000000
glyphOneglyphOne400glyphTwo100glyphTwoglyphOne-100glyphTwo-400
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_400.ufo/layercontents.plist 0000664 0000000 0000000 00000000437 14724542715 0031036 0 ustar 00root root 0000000 0000000
public.defaultglyphs
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_400.ufo/lib.plist 0000664 0000000 0000000 00000000472 14724542715 0026711 0 ustar 00root root 0000000 0000000
public.glyphOrderglyphOneglyphTwo
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_400.ufo/metainfo.plist 0000664 0000000 0000000 00000000476 14724542715 0027751 0 ustar 00root root 0000000 0000000
creatorcom.github.fonttools.ufoLibformatVersion3
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_550.ufo/ 0000775 0000000 0000000 00000000000 14724542715 0025071 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_550.ufo/features.fea 0000664 0000000 0000000 00000000067 14724542715 0027367 0 ustar 00root root 0000000 0000000 # features from ufo: geometryMaster_c_400_d1_1_d2_0.ufo ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_550.ufo/fontinfo.plist 0000664 0000000 0000000 00000003065 14724542715 0027774 0 ustar 00root root 0000000 0000000
ascender400capHeight400copyright# font.info from ufo: geometryMaster_c_400_d1_1_d2_0.ufodescender-200familyNameInstanceFamilyNameguidelinesopenTypeHheaAscender1036openTypeHheaDescender-335openTypeOS2TypoAscender730openTypeOS2TypoDescender-270openTypeOS2WinAscent1036openTypeOS2WinDescent335postscriptBlueFuzz0postscriptBlueScale0.22postscriptBlueValues100110postscriptFamilyBluespostscriptFamilyOtherBluespostscriptOtherBluespostscriptStemSnapHpostscriptStemSnapVstyleNameweight_550unitsPerEm1000.0xHeight200
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_550.ufo/glyphs/ 0000775 0000000 0000000 00000000000 14724542715 0026377 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_550.ufo/glyphs/contents.plist 0000664 0000000 0000000 00000000470 14724542715 0031312 0 ustar 00root root 0000000 0000000
glyphOneglyphO_ne.glifglyphTwoglyphT_wo.glif
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_550.ufo/glyphs/glyphO_ne.glif 0000664 0000000 0000000 00000001113 14724542715 0031162 0 ustar 00root root 0000000 0000000
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_550.ufo/glyphs/glyphT_wo.glif 0000664 0000000 0000000 00000000300 14724542715 0031207 0 ustar 00root root 0000000 0000000
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_550.ufo/groups.plist 0000664 0000000 0000000 00000000673 14724542715 0027473 0 ustar 00root root 0000000 0000000
public.kern1.groupAglyphOneglyphTwopublic.kern2.groupBglyphThreeglyphFour
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_550.ufo/kerning.plist 0000664 0000000 0000000 00000000774 14724542715 0027613 0 ustar 00root root 0000000 0000000
glyphOneglyphOne400glyphTwo275glyphTwoglyphOne-275glyphTwo-400
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_550.ufo/layercontents.plist 0000664 0000000 0000000 00000000437 14724542715 0031044 0 ustar 00root root 0000000 0000000
public.defaultglyphs
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_550.ufo/lib.plist 0000664 0000000 0000000 00000000472 14724542715 0026717 0 ustar 00root root 0000000 0000000
public.glyphOrderglyphOneglyphTwo
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_550.ufo/metainfo.plist 0000664 0000000 0000000 00000000476 14724542715 0027757 0 ustar 00root root 0000000 0000000
creatorcom.github.fonttools.ufoLibformatVersion3
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_700.ufo/ 0000775 0000000 0000000 00000000000 14724542715 0025066 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_700.ufo/features.fea 0000664 0000000 0000000 00000000067 14724542715 0027364 0 ustar 00root root 0000000 0000000 # features from ufo: geometryMaster_c_400_d1_1_d2_0.ufo ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_700.ufo/fontinfo.plist 0000664 0000000 0000000 00000003065 14724542715 0027771 0 ustar 00root root 0000000 0000000
ascender400capHeight400copyright# font.info from ufo: geometryMaster_c_400_d1_1_d2_0.ufodescender-200familyNameInstanceFamilyNameguidelinesopenTypeHheaAscender1036openTypeHheaDescender-335openTypeOS2TypoAscender730openTypeOS2TypoDescender-270openTypeOS2WinAscent1036openTypeOS2WinDescent335postscriptBlueFuzz0postscriptBlueScale0.22postscriptBlueValues100110postscriptFamilyBluespostscriptFamilyOtherBluespostscriptOtherBluespostscriptStemSnapHpostscriptStemSnapVstyleNameweight_700unitsPerEm1000.0xHeight200
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_700.ufo/glyphs/ 0000775 0000000 0000000 00000000000 14724542715 0026374 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_700.ufo/glyphs/contents.plist 0000664 0000000 0000000 00000000470 14724542715 0031307 0 ustar 00root root 0000000 0000000
glyphOneglyphO_ne.glifglyphTwoglyphT_wo.glif
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_700.ufo/glyphs/glyphO_ne.glif 0000664 0000000 0000000 00000001113 14724542715 0031157 0 ustar 00root root 0000000 0000000
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_700.ufo/glyphs/glyphT_wo.glif 0000664 0000000 0000000 00000000300 14724542715 0031204 0 ustar 00root root 0000000 0000000
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_700.ufo/groups.plist 0000664 0000000 0000000 00000000673 14724542715 0027470 0 ustar 00root root 0000000 0000000
public.kern1.groupAglyphOneglyphTwopublic.kern2.groupBglyphThreeglyphFour
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_700.ufo/kerning.plist 0000664 0000000 0000000 00000000774 14724542715 0027610 0 ustar 00root root 0000000 0000000
glyphOneglyphOne400glyphTwo450glyphTwoglyphOne-450glyphTwo-400
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_700.ufo/layercontents.plist 0000664 0000000 0000000 00000000437 14724542715 0031041 0 ustar 00root root 0000000 0000000
public.defaultglyphs
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_700.ufo/lib.plist 0000664 0000000 0000000 00000000472 14724542715 0026714 0 ustar 00root root 0000000 0000000
public.glyphOrderglyphOneglyphTwo
ufoProcessor-1.13.3/Tests/ds4/instances/geometryInstance_c_700.ufo/metainfo.plist 0000664 0000000 0000000 00000000476 14724542715 0027754 0 ustar 00root root 0000000 0000000
creatorcom.github.fonttools.ufoLibformatVersion3
ufoProcessor-1.13.3/Tests/ds4/readme.md 0000664 0000000 0000000 00000001334 14724542715 0017657 0 ustar 00root root 0000000 0000000 # Test for designspace 5 format with discrete and interpolating axes
I need to keep notes somewhere.
The script `ds5_makeTestDoc.py` makes a new DS5 designspace file with 3 axes.
* `wdth` with minimum at `400`, default `400`, maximum at `1000`. An interpolating axis.
* `DSC1`, a discrete axis. Values at `1,2,3`. Default at `1`. Named `countedItems` Variations along this axis consist of 1, 2, and 3 boxes.
* `DSC2`, a discrete axis. Values at `0, 1`. Default at `0`. Named `outlined`. Variations along this axis consist of solid shapes and outlined shapes.
The `masters` folder has sources for all intersections of these axes. The default of the whole system is at `wdth: 400, countedItems: 1, outlined: 0`

ufoProcessor-1.13.3/Tests/ds4/sources/ 0000775 0000000 0000000 00000000000 14724542715 0017562 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_1000.ufo/ 0000775 0000000 0000000 00000000000 14724542715 0024330 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_1000.ufo/features.fea 0000664 0000000 0000000 00000000070 14724542715 0026620 0 ustar 00root root 0000000 0000000 # features from ufo: geometryMaster_c_1000_d1_1_d2_0.ufo ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_1000.ufo/fontinfo.plist 0000664 0000000 0000000 00000003073 14724542715 0027232 0 ustar 00root root 0000000 0000000
ascender400capHeight400copyright# font.info from ufo: geometryMaster_c_1000_d1_1_d2_0.ufodescender-200familyNameOne_wide_openguidelinesopenTypeHheaAscender1036openTypeHheaDescender-335openTypeOS2TypoAscender730openTypeOS2TypoDescender-270openTypeOS2WinAscent1036openTypeOS2WinDescent335postscriptBlueFuzz0postscriptBlueScale0.22postscriptBlueValues100110postscriptFamilyBluespostscriptFamilyOtherBluespostscriptOtherBluespostscriptStemSnapHpostscriptStemSnapVstyleNamec_1000_d1_1_d2_0unitsPerEm1000xHeight200
ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_1000.ufo/glyphs/ 0000775 0000000 0000000 00000000000 14724542715 0025636 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_1000.ufo/glyphs/contents.plist 0000664 0000000 0000000 00000000470 14724542715 0030551 0 ustar 00root root 0000000 0000000
glyphOneglyphO_ne.glifglyphTwoglyphT_wo.glif
ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_1000.ufo/glyphs/glyphO_ne.glif 0000664 0000000 0000000 00000001051 14724542715 0030422 0 ustar 00root root 0000000 0000000
ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_1000.ufo/glyphs/glyphT_wo.glif 0000664 0000000 0000000 00000000276 14724542715 0030462 0 ustar 00root root 0000000 0000000
ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_1000.ufo/glyphs/layerinfo.plist 0000664 0000000 0000000 00000000367 14724542715 0030711 0 ustar 00root root 0000000 0000000
color1,0.75,0,0.7
ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_1000.ufo/groups.plist 0000664 0000000 0000000 00000000673 14724542715 0026732 0 ustar 00root root 0000000 0000000
public.kern1.groupAglyphOneglyphTwopublic.kern2.groupBglyphThreeglyphFour
ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_1000.ufo/kerning.plist 0000664 0000000 0000000 00000000774 14724542715 0027052 0 ustar 00root root 0000000 0000000
glyphOneglyphOne400glyphTwo800glyphTwoglyphOne-800glyphTwo-400
ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_1000.ufo/layercontents.plist 0000664 0000000 0000000 00000000437 14724542715 0030303 0 ustar 00root root 0000000 0000000
public.defaultglyphs
ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_1000.ufo/lib.plist 0000664 0000000 0000000 00000002205 14724542715 0026152 0 ustar 00root root 0000000 0000000
com.typemytype.robofont.compileSettings.autohintcom.typemytype.robofont.compileSettings.checkOutlinescom.typemytype.robofont.compileSettings.createDummyDSIGcom.typemytype.robofont.compileSettings.decomposecom.typemytype.robofont.compileSettings.generateFormat0com.typemytype.robofont.compileSettings.releaseModecom.typemytype.robofont.generateFeaturesWithFontToolscom.typemytype.robofont.italicSlantOffset0com.typemytype.robofont.shouldAddPointsInSplineConversion1public.glyphOrderglyphOneglyphTwoufoProcessor.test.lib.entryLib entry for master 1
ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_1000.ufo/metainfo.plist 0000664 0000000 0000000 00000000476 14724542715 0027216 0 ustar 00root root 0000000 0000000
creatorcom.github.fonttools.ufoLibformatVersion3
ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_400.ufo/ 0000775 0000000 0000000 00000000000 14724542715 0024253 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_400.ufo/features.fea 0000664 0000000 0000000 00000000067 14724542715 0026551 0 ustar 00root root 0000000 0000000 # features from ufo: geometryMaster_c_400_d1_1_d2_0.ufo ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_400.ufo/fontinfo.plist 0000664 0000000 0000000 00000003073 14724542715 0027155 0 ustar 00root root 0000000 0000000
ascender400capHeight400copyright# font.info from ufo: geometryMaster_c_400_d1_1_d2_0.ufodescender-200familyNameOne_narrow_openguidelinesopenTypeHheaAscender1036openTypeHheaDescender-335openTypeOS2TypoAscender730openTypeOS2TypoDescender-270openTypeOS2WinAscent1036openTypeOS2WinDescent335postscriptBlueFuzz0postscriptBlueScale0.22postscriptBlueValues100110postscriptFamilyBluespostscriptFamilyOtherBluespostscriptOtherBluespostscriptStemSnapHpostscriptStemSnapVstyleNamec_400_d1_1_d2_0unitsPerEm1000xHeight200
ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_400.ufo/glyphs/ 0000775 0000000 0000000 00000000000 14724542715 0025561 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_400.ufo/glyphs/contents.plist 0000664 0000000 0000000 00000000470 14724542715 0030474 0 ustar 00root root 0000000 0000000
glyphOneglyphO_ne.glifglyphTwoglyphT_wo.glif
ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_400.ufo/glyphs/glyphO_ne.glif 0000664 0000000 0000000 00000001047 14724542715 0030352 0 ustar 00root root 0000000 0000000
ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_400.ufo/glyphs/glyphT_wo.glif 0000664 0000000 0000000 00000000276 14724542715 0030405 0 ustar 00root root 0000000 0000000
ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_400.ufo/glyphs/layerinfo.plist 0000664 0000000 0000000 00000000367 14724542715 0030634 0 ustar 00root root 0000000 0000000
color1,0.75,0,0.7
ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_400.ufo/groups.plist 0000664 0000000 0000000 00000000673 14724542715 0026655 0 ustar 00root root 0000000 0000000
public.kern1.groupAglyphOneglyphTwopublic.kern2.groupBglyphThreeglyphFour
ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_400.ufo/kerning.plist 0000664 0000000 0000000 00000000774 14724542715 0026775 0 ustar 00root root 0000000 0000000
glyphOneglyphOne400glyphTwo100glyphTwoglyphOne-100glyphTwo-400
ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_400.ufo/layercontents.plist 0000664 0000000 0000000 00000000437 14724542715 0030226 0 ustar 00root root 0000000 0000000
public.defaultglyphs
ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_400.ufo/lib.plist 0000664 0000000 0000000 00000002205 14724542715 0026075 0 ustar 00root root 0000000 0000000
com.typemytype.robofont.compileSettings.autohintcom.typemytype.robofont.compileSettings.checkOutlinescom.typemytype.robofont.compileSettings.createDummyDSIGcom.typemytype.robofont.compileSettings.decomposecom.typemytype.robofont.compileSettings.generateFormat0com.typemytype.robofont.compileSettings.releaseModecom.typemytype.robofont.generateFeaturesWithFontToolscom.typemytype.robofont.italicSlantOffset0com.typemytype.robofont.shouldAddPointsInSplineConversion1public.glyphOrderglyphOneglyphTwoufoProcessor.test.lib.entryLib entry for master 1
ufoProcessor-1.13.3/Tests/ds4/sources/geometrySource_c_400.ufo/metainfo.plist 0000664 0000000 0000000 00000000476 14724542715 0027141 0 ustar 00root root 0000000 0000000
creatorcom.github.fonttools.ufoLibformatVersion3
ufoProcessor-1.13.3/Tests/ds5/ 0000775 0000000 0000000 00000000000 14724542715 0016100 5 ustar 00root root 0000000 0000000 ufoProcessor-1.13.3/Tests/ds5/ds5.designspace 0000664 0000000 0000000 00000051041 14724542715 0021003 0 ustar 00root root 0000000 0000000
ufoProcessor-1.13.3/Tests/ds5/ds5_addKerningToTheseMasters.py 0000664 0000000 0000000 00000000510 14724542715 0024122 0 ustar 00root root 0000000 0000000
p1 = ('glyphOne', 'glyphTwo')
p2 = ('glyphTwo', 'glyphOne')
g = "glyphOne"
for f in AllFonts():
print(f.path, f.kerning.items())
f.kerning[p1] = f[g].width
f.kerning[p2] = -f[g].width
f.kerning[('glyphTwo', 'glyphTwo')] = -400
f.kerning[('glyphOne', 'glyphOne')] = 400
f.save()
f.close()
ufoProcessor-1.13.3/Tests/ds5/ds5_extrapolateTest.py 0000664 0000000 0000000 00000001543 14724542715 0022420 0 ustar 00root root 0000000 0000000 # test the extrapolation in VariationModel
from fontTools.varLib.models import VariationModel
locations = [
dict(wgth=0),
dict(wght=1000),
]
values = [10, 20]
m = VariationModel(locations, extrapolate=True)
# interpolating
assert m.interpolateFromMasters(dict(wght=0), values) == 10
assert m.interpolateFromMasters(dict(wght=500), values) == 15
assert m.interpolateFromMasters(dict(wght=1000), values) == 20
# extrapolate over max
assert m.interpolateFromMasters(dict(wght=1500), values) == 25
assert m.interpolateFromMasters(dict(wght=2000), values) == 30
# extrapolation over min gets stuck
print(m.interpolateFromMasters(dict(wght=-500), values), m.interpolateFromMasters(dict(wght=-1000), values))
# would expect:
assert m.interpolateFromMasters(dict(wght=-500), values) == -5
assert m.interpolateFromMasters(dict(wght=-1000), values) == -10
ufoProcessor-1.13.3/Tests/ds5/ds5_makeTestDoc.py 0000664 0000000 0000000 00000007432 14724542715 0021436 0 ustar 00root root 0000000 0000000 # make a test designspace format 5 with 1 continuous and 2 discrete axes.
# axis width is a normal interpolation with a change in width
# axis DSC1 is a discrete axis showing 1, 2, 3 items in the glyph
# axis DSC2 is a discrete axis showing a solid or outlined shape
from fontTools.designspaceLib import DesignSpaceDocument, SourceDescriptor, InstanceDescriptor, AxisDescriptor, RuleDescriptor, processRules, DiscreteAxisDescriptor
from fontTools.designspaceLib.split import splitInterpolable
import os
import fontTools
print(fontTools.version)
import ufoProcessor
print(ufoProcessor.__file__)
doc = DesignSpaceDocument()
#https://fonttools.readthedocs.io/en/latest/designspaceLib/python.html#axisdescriptor
a1 = AxisDescriptor()
a1.minimum = 400
a1.maximum = 1000
a1.default = 400
a1.map = ((400,400), (700,900), (1000,1000))
a1.name = "width"
a1.tag = "wdth"
a1.axisOrdering = 1
doc.addAxis(a1)
a2 = DiscreteAxisDescriptor()
a2.values = [1, 2, 3]
a2.default = 1
a2.name = "countedItems"
a2.tag = "DSC1"
a2.axisOrdering = 2
doc.addAxis(a2)
a3 = DiscreteAxisDescriptor()
a3.values = [0, 1]
a3.default = 0
a3.name = "outlined"
a3.tag = "DSC2"
a3.axisOrdering = 3
doc.addAxis(a3)
default = {a1.name: a1.default, a2.name: a2.default, a3.name: a3.default}
# add sources
# public.skipExportGlyphs
for c in [a1.minimum, a1.maximum]:
for d1 in a2.values:
for d2 in a3.values:
s1 = SourceDescriptor()
s1.path = os.path.join("sources", f"geometrySource_c_{c}_d1_{d1}_d2_{d2}.ufo")
s1.name = f"geometrySource{c} {d1} {d2}"
sourceLocation = dict(width=c, countedItems=d1, outlined=d2)
s1.location = sourceLocation
s1.kerning = True
s1.familyName = "SourceFamilyName"
if default == sourceLocation:
s1.copyGroups = True
s1.copyFeatures = True
s1.copyInfo = True
td1 = ["One", "Two", "Three"][(d1-1)]
if c == 400:
tc = "Narrow"
elif c == 1000:
tc = "Wide"
if d2 == 0:
td2 = "solid"
else:
td2 = "open"
s1.styleName = f"{td1}{tc}{td2}"
doc.addSource(s1)
def ip(a,b,f):
return a + f*(b-a)
# add instances
steps = 8
extrapolateAmount = 100
interestingWeightValues = [(400, 700), 300, 400, 550, 700, 1000, 1100]
mathModelPrefKey = "com.letterror.mathModelPref"
mathModelVarlibPref = "previewVarLib"
mathModelMutatorMathPref = "previewMutatorMath"
# com.letterror.mathModelPref
# previewVarLib
for c in interestingWeightValues:
for d1 in a2.values:
for d2 in a3.values:
s1 = InstanceDescriptor()
s1.path = os.path.join("instances", f"geometryInstance_c_{c}_d1_{d1}_d2_{d2}.ufo")
s1.location = dict(width=c, countedItems=d1, outlined=d2)
s1.familyName = "InstanceFamilyName"
td1 = ["One", "Two", "Three"][(d1-1)]
if c == 400:
tc = "Narrow"
elif c == 1000:
tc = "Wide"
if d2 == 0:
td2 = "Solid"
else:
td2 = "Open"
s1.name = f"geometryInstance {td1} {tc} {td2}"
s1.styleName = f"{td1}{tc}{td2}"
s1.kerning = True
s1.info = True
doc.addInstance(s1)
# add variable font descriptors
splits = splitInterpolable(doc)
for discreteLocation, subSpace in splitInterpolable(doc):
print(discreteLocation, subSpace)
#print(doc.getVariableFonts())
#for item in doc.getVariableFonts():
# doc.addVariableFont(item)
doc.variableFonts.clear()
print(doc.variableFonts)
variableFonts = doc.getVariableFonts()
print("variableFonts", variableFonts)
doc.addVariableFont(variableFonts[0])
for i, item in enumerate(variableFonts):
print(i, item)
path = "ds5.designspace"
print(doc.lib)
doc.write(path)
print(dir(doc))
for a in doc.axes:
if hasattr(a, "values"):
print(a.name, "d", a.values)
else:
print(a.name, "r", a.minimum, a.maximum)
for s in doc.sources:
print(s.location)
# ok. now about generating the instances.
udoc = ufoProcessor.DesignSpaceProcessor()
udoc.read(path)
ufoProcessor-1.13.3/Tests/ds5/ds5_no_discrete_axes.designspace 0000664 0000000 0000000 00000004141 14724542715 0024400 0 ustar 00root root 0000000 0000000
com.letterror.mathModelPrefpreviewMutatorMathcom.letterror.skateboard.interactionSourceshorizontalwidthignoreverticalcom.letterror.skateboard.previewLocationwidth790.9379266617385com.letterror.skateboard.previewTextSKATEBOARD
ufoProcessor-1.13.3/Tests/ds5/ds5_test_designspaceProblems.py 0000664 0000000 0000000 00000000533 14724542715 0024256 0 ustar 00root root 0000000 0000000 import designspaceProblems
print(designspaceProblems.__file__)
path= "ds5.designspace"
checker = designspaceProblems.DesignSpaceChecker(path)
checker.checkEverything()
print(checker.checkDesignSpaceGeometry())
checker.checkSources()
checker.checkInstances()
print("hasStructuralProblems", checker.hasStructuralProblems())
print(checker.problems) ufoProcessor-1.13.3/Tests/ds5/readme.md 0000664 0000000 0000000 00000001334 14724542715 0017660 0 ustar 00root root 0000000 0000000 # Test for designspace 5 format with discrete and interpolating axes
I need to keep notes somewhere.
The script `ds5_makeTestDoc.py` makes a new DS5 designspace file with 3 axes.
* `wdth` with minimum at `400`, default `400`, maximum at `1000`. An interpolating axis.
* `DSC1`, a discrete axis. Values at `1,2,3`. Default at `1`. Named `countedItems` Variations along this axis consist of 1, 2, and 3 boxes.
* `DSC2`, a discrete axis. Values at `0, 1`. Default at `0`. Named `outlined`. Variations along this axis consist of solid shapes and outlined shapes.
The `masters` folder has sources for all intersections of these axes. The default of the whole system is at `wdth: 400, countedItems: 1, outlined: 0`

ufoProcessor-1.13.3/Tests/ds5/sources.jpg 0000664 0000000 0000000 00000227776 14724542715 0020312 0 ustar 00root root 0000000 0000000 JFIF H H jExif MM * i &