Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support building a .designspace system from scratch #771

Merged
merged 19 commits into from
Sep 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions src/fontra/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,33 @@


def getFileSystemBackend(path):
return _getFileSystemBackend(path, False)


def newFileSystemBackend(path):
return _getFileSystemBackend(path, True)


def _getFileSystemBackend(path, create):
logVerb = "creating" if create else "loading"

path = pathlib.Path(path)
if not path.exists():

if not create and not path.exists():
raise FileNotFoundError(path)
logger.info(f"loading project {path.name}...")

logger.info(f"{logVerb} project {path.name}...")
fileType = path.suffix.lstrip(".").lower()
backendEntryPoints = entry_points(group="fontra.filesystem.backends")
entryPoint = backendEntryPoints[fileType]
backendClass = entryPoint.load()

backend = backendClass.fromPath(path)
logger.info(f"done loading {path.name}")
if create:
if not hasattr(backendClass, "createFromPath"):
raise ValueError(f"Creating a new .{fileType} is not supported")
backend = backendClass.createFromPath(path)
else:
backend = backendClass.fromPath(path)

logger.info(f"done {logVerb} {path.name}")
return backend
195 changes: 128 additions & 67 deletions src/fontra/backends/designspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,25 +40,62 @@
LAYER_NAME_MAPPING_LIB_KEY = "xyz.fontra.layer-names"


infoAttrsToCopy = [
"unitsPerEm",
"ascender",
"descender",
"xHeight",
"capHeight",
"familyName",
"copyright",
"year",
]
defaultUFOInfoAttrs = {
"unitsPerEm": 1000,
"ascender": 750,
"descender": -250,
"xHeight": 500,
"capHeight": 750,
"familyName": None,
"copyright": None,
"year": None,
}


class DesignspaceBackend:
@classmethod
def fromPath(cls, path):
return cls(DesignSpaceDocument.fromfile(path))

@classmethod
def createFromPath(cls, path):
path = pathlib.Path(path)
ufoDir = path.parent

# Create default UFO
makeUniqueFileName = uniqueNameMaker(p.stem for p in ufoDir.glob("*.ufo"))
familyName = path.stem
styleName = "Regular"
ufoFileName = makeUniqueFileName(f"{familyName}_{styleName}")
ufoFileName = ufoFileName + ".ufo"
ufoPath = os.fspath(ufoDir / ufoFileName)
assert not os.path.exists(ufoPath)
writer = UFOReaderWriter(ufoPath) # this creates the UFO
info = UFOFontInfo()
for infoAttr, value in defaultUFOInfoAttrs.items():
if value is not None:
setattr(info, infoAttr, value)
writer.writeInfo(info)
_ = writer.getGlyphSet() # this creates the default layer
writer.writeLayerContents()
assert os.path.isdir(ufoPath)

dsDoc = DesignSpaceDocument()
dsDoc.addSourceDescriptor(styleName=styleName, path=ufoPath, location={})

dsDoc.write(path)
return cls(dsDoc)

def __init__(self, dsDoc):
self.dsDoc = dsDoc
self.ufoManager = UFOManager()
self.updateAxisInfo()
self.loadUFOLayers()
self.buildGlyphFileNameMapping()
self.glyphMap = getGlyphMapFromGlyphSet(self.defaultDSSource.layer.glyphSet)
self.savedGlyphModificationTimes = {}

def updateAxisInfo(self):
self.dsDoc.findDefault()
axes = []
axisPolePositions = {}
Expand Down Expand Up @@ -86,10 +123,6 @@ def __init__(self, dsDoc):
axisName: polePosition[1]
for axisName, polePosition in axisPolePositions.items()
}
self.loadUFOLayers()
self.buildGlyphFileNameMapping()
self.glyphMap = getGlyphMapFromGlyphSet(self.defaultDSSource.layer.glyphSet)
self.savedGlyphModificationTimes = {}

def close(self):
pass
Expand All @@ -113,7 +146,7 @@ def defaultFontInfo(self):
return fontInfo

def loadUFOLayers(self):
self.ufoManager = manager = UFOManager()
manager = self.ufoManager
self.dsSources = ItemList()
self.ufoLayers = ItemList()

Expand Down Expand Up @@ -249,30 +282,29 @@ async def putGlyph(self, glyphName, glyph, unicodes):
defaultLayerGlyph = readGlyphOrCreate(
self.defaultUFOLayer.glyphSet, glyphName, unicodes
)

revLayerNameMapping = reverseSparseDict(
defaultLayerGlyph.lib.get(LAYER_NAME_MAPPING_LIB_KEY, {})
)

sourceNameMapping = {}
layerNameMapping = {}
localAxes = packLocalAxes(glyph.axes)
localAxisNames = {axis.name for axis in glyph.axes}
localSources = []

# Prepare UFO source layers and local sources
sourceNameMapping = {}
layerNameMapping = {}
localSources = []
for source in glyph.sources:
(
normalizedSourceName,
normalizedLayerName,
localSourceDict,
) = self._prepareUFOLayer(source, localAxisNames, revLayerNameMapping)
if normalizedSourceName != source.name:
sourceNameMapping[normalizedSourceName] = source.name
if normalizedLayerName != source.layerName:
layerNameMapping[normalizedLayerName] = source.layerName
if localSourceDict is not None:
localSources.append(localSourceDict)

sourceInfo = self._prepareUFOSourceLayer(
source, localAxisNames, revLayerNameMapping
)
if sourceInfo.sourceName != source.name:
sourceNameMapping[sourceInfo.sourceName] = source.name
if sourceInfo.layerName != source.layerName:
layerNameMapping[sourceInfo.layerName] = source.layerName
if sourceInfo.localSourceDict is not None:
localSources.append(sourceInfo.localSourceDict)

# Prepare local design space
localDS = {}
if localAxes:
localDS["axes"] = localAxes
Expand All @@ -281,20 +313,28 @@ async def putGlyph(self, glyphName, glyph, unicodes):

revLayerNameMapping = reverseSparseDict(layerNameMapping)

haveVariableComponents = any(
compo.location
or compo.transformation.tCenterX
or compo.transformation.tCenterY
for layer in glyph.layers.values()
for compo in layer.glyph.components
)

modTimes = set()
# Gather all UFO layers
usedLayers = set()
layers = []
for layerName, layer in glyph.layers.items():
layerName = revLayerNameMapping.get(layerName, layerName)
glyphSet = self.ufoLayers.findItem(fontraLayerName=layerName).glyphSet
ufoLayer = self.ufoLayers.findItem(fontraLayerName=layerName)

if ufoLayer is None:
# This layer is not used by any source and we haven't seen it
# before. Let's create a new layer in the default UFO.
ufoLayer = self._newUFOLayer(self.defaultUFOLayer.path, layerName)
if ufoLayer.fontraLayerName != layerName:
layerNameMapping[ufoLayer.fontraLayerName] = layerName
layerName = ufoLayer.fontraLayerName
layers.append((layer, ufoLayer))
usedLayers.add(layerName)

# Write all UFO layers
hasVariableComponents = glyphHasVariableComponents(glyph)
modTimes = set()
for layer, ufoLayer in layers:
glyphSet = ufoLayer.glyphSet
writeGlyphSetContents = glyphName not in glyphSet

if glyphSet == self.defaultUFOLayer.glyphSet:
Expand All @@ -306,7 +346,7 @@ async def putGlyph(self, glyphName, glyph, unicodes):
layerGlyph = readGlyphOrCreate(glyphSet, glyphName, unicodes)

drawPointsFunc = populateUFOLayerGlyph(
layerGlyph, layer.glyph, haveVariableComponents
layerGlyph, layer.glyph, hasVariableComponents
)
glyphSet.writeGlyph(glyphName, layerGlyph, drawPointsFunc=drawPointsFunc)
if writeGlyphSetContents:
Expand All @@ -315,6 +355,7 @@ async def putGlyph(self, glyphName, glyph, unicodes):

modTimes.add(glyphSet.getGLIFModificationTime(glyphName))

# Prune unused UFO layers
relevantLayerNames = set(
layer.fontraLayerName
for layer in self.ufoLayers
Expand All @@ -330,7 +371,7 @@ async def putGlyph(self, glyphName, glyph, unicodes):

self.savedGlyphModificationTimes[glyphName] = modTimes

def _prepareUFOLayer(self, source, localAxisNames, revLayerNameMapping):
def _prepareUFOSourceLayer(self, source, localAxisNames, revLayerNameMapping):
sourceLocation = {**self.defaultLocation, **source.location}
globalLocation = {
name: value
Expand All @@ -353,13 +394,8 @@ def _prepareUFOLayer(self, source, localAxisNames, revLayerNameMapping):

if ufoLayer is None:
ufoPath = dsSource.layer.path
ufoLayerName = self._newUFOLayer(ufoPath, source.layerName)
ufoLayer = UFOLayer(
manager=self.ufoManager,
path=ufoPath,
name=ufoLayerName,
)
self.ufoLayers.append(ufoLayer)
ufoLayer = self._newUFOLayer(ufoPath, source.layerName)
ufoLayerName = ufoLayer.name
else:
ufoLayerName = ufoLayer.name
normalizedSourceName = source.name
Expand All @@ -375,7 +411,11 @@ def _prepareUFOLayer(self, source, localAxisNames, revLayerNameMapping):
normalizedLayerName = dsSource.layer.fontraLayerName
localSourceDict = None

return normalizedSourceName, normalizedLayerName, localSourceDict
return SimpleNamespace(
sourceName=normalizedSourceName,
layerName=normalizedLayerName,
localSourceDict=localSourceDict,
)

def _createDSSource(self, source, globalLocation):
manager = self.ufoManager
Expand All @@ -393,14 +433,21 @@ def _createDSSource(self, source, globalLocation):
assert not os.path.exists(ufoPath)
reader = manager.getReader(ufoPath) # this creates the UFO
info = UFOFontInfo()
for infoAttr in infoAttrsToCopy:
for infoAttr in defaultUFOInfoAttrs:
value = getattr(self.defaultFontInfo, infoAttr, None)
if value is not None:
setattr(info, infoAttr, value)
_ = reader.getGlyphSet() # this creates the default layer
reader.writeLayerContents()
ufoLayerName = reader.getDefaultLayerName()
assert os.path.isdir(ufoPath)

ufoLayer = UFOLayer(
manager=manager,
path=ufoPath,
name=ufoLayerName,
)
self.ufoLayers.append(ufoLayer)
else:
# Create a new layer in the appropriate existing UFO
atPole = {**self.defaultLocation, **atPole}
Expand All @@ -411,7 +458,8 @@ def _createDSSource(self, source, globalLocation):
poleDSSource = self.defaultDSSource
assert poleDSSource is not None
ufoPath = poleDSSource.layer.path
ufoLayerName = self._newUFOLayer(poleDSSource.layer.path, source.layerName)
ufoLayer = self._newUFOLayer(poleDSSource.layer.path, source.layerName)
ufoLayerName = ufoLayer.name

self.dsDoc.addSourceDescriptor(
styleName=source.name,
Expand All @@ -421,37 +469,36 @@ def _createDSSource(self, source, globalLocation):
)
self.dsDoc.write(self.dsDoc.path)

ufoLayer = UFOLayer(
manager=manager,
path=ufoPath,
name=ufoLayerName,
)

dsSource = DSSource(
name=source.name,
layer=ufoLayer,
location=globalLocation,
)
self.dsSources.append(dsSource)
self.ufoLayers.append(ufoLayer)

return dsSource

def _newUFOLayer(self, path, suggestedLayerName):
reader = self.ufoManager.getReader(path)
def _newUFOLayer(self, ufoPath, suggestedLayerName):
reader = self.ufoManager.getReader(ufoPath)
makeUniqueName = uniqueNameMaker(reader.getLayerNames())
ufoLayerName = makeUniqueName(suggestedLayerName)
# Create the new UFO layer now
_ = self.ufoManager.getGlyphSet(path, ufoLayerName)
_ = self.ufoManager.getGlyphSet(ufoPath, ufoLayerName)
reader.writeLayerContents()
return ufoLayerName

ufoLayer = UFOLayer(
manager=self.ufoManager,
path=ufoPath,
name=ufoLayerName,
)
self.ufoLayers.append(ufoLayer)

return ufoLayer

async def getGlobalAxes(self):
return self.axes

async def putGlobalAxes(self, axes):
axes = deepcopy(axes)
self.axes = axes
self.dsDoc.axes = []
for axis in axes:
self.dsDoc.addAxisDescriptor(
Expand All @@ -460,9 +507,11 @@ async def putGlobalAxes(self, axes):
minimum=axis.minValue,
default=axis.defaultValue,
maximum=axis.maxValue,
map=axis.mapping if axis.mapping else None,
map=deepcopy(axis.mapping) if axis.mapping else None,
)
self.dsDoc.write(self.dsDoc.path)
self.updateAxisInfo()
self.loadUFOLayers()

async def getUnitsPerEm(self):
return self.defaultFontInfo.unitsPerEm
Expand Down Expand Up @@ -613,6 +662,10 @@ def fromPath(cls, path):
dsDoc.addSourceDescriptor(path=os.fspath(path), styleName="default")
return DesignspaceBackend(dsDoc)

@classmethod
def createFromPath(cls, path):
raise NotImplementedError()


class UFOGlyph:
unicodes = ()
Expand Down Expand Up @@ -867,3 +920,11 @@ def storeInLib(layerGlyph, key, value):
layerGlyph.lib[key] = value
else:
layerGlyph.lib.pop(key, None)


def glyphHasVariableComponents(glyph):
return any(
compo.location or compo.transformation.tCenterX or compo.transformation.tCenterY
for layer in glyph.layers.values()
for compo in layer.glyph.components
)
Loading