Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ Finally, you can copy in the font feature options you want:
- ss01 # Single-story a
- ss02 # Single-story g
- ss03 # Simplified f
- ss04 # Simplified i ### NOT CURRENTLY WORKING, see issue #4
- ss04 # Simplified i
- ss05 # Simplified l
- ss06 # Simplified r

Expand Down
3 changes: 0 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ fonttools==4.39.3
# to check font versions in the build-all shell script
font-v==1.0.5

# to run the code font instantiation script
opentype-feature-freezer==1.32.2

# to remove components in code ligatures for improved rendering
skia-pathops==0.7.4

Expand Down
185 changes: 185 additions & 0 deletions scripts/fontfreeze_activation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
"""
Utilities for freezing OpenType features using FontFreeze logic.

Portions of this module are adapted from FontFreeze
(https://github.com/MuTsunTsai/fontfreeze) and retain the original MIT License.
"""

from __future__ import annotations

from typing import Iterable, Sequence

from fontTools.ttLib import TTFont
from fontTools.ttLib.tables.otTables import (
FeatureParamsCharacterVariants,
FeatureParamsStylisticSet,
featureParamTypes,
)

hideRemovedFeature = True


def clearFeatureRecord(featureRecord) -> None:
featureRecord.Feature.LookupListIndex.clear()
featureRecord.Feature.LookupCount = 0
if hideRemovedFeature:
featureRecord.FeatureTag = "DELT"


class Activator:
def __init__(self, font: TTFont, args: dict) -> None:
self.font = font
self.features = args.get("features") or []
options: dict = args.get("options") or {}
self.target = options.get("target")
self.singleSub = options.get("singleSub")
self.singleSubs: dict[str, str] = {}

if len(self.features) == 0 or "GSUB" not in self.font:
return

self.cmapTables = self.font["cmap"].tables
self.unicodeGlyphs = {name for table in self.cmapTables for name in table.cmap.values()}

gsub_table = self.font["GSUB"].table
self.featureRecords = gsub_table.FeatureList.FeatureRecord
self.lookup = gsub_table.LookupList.Lookup

scriptRecords = gsub_table.ScriptList.ScriptRecord
for scriptRecord in scriptRecords:
self.activateInScript(scriptRecord.Script)

if self.singleSub:
self.applySingleSubstitutions()

def activateInScript(self, script) -> None:
if script.DefaultLangSys is not None:
self.activateInLangSys(script.DefaultLangSys)
for langSysRecord in script.LangSysRecord:
self.activateInLangSys(langSysRecord.LangSys)

def activateInLangSys(self, langSys) -> None:
targetRecord = None

# try to find existing target feature
for index in langSys.FeatureIndex:
featureRecord = self.featureRecords[index]
if featureRecord.FeatureTag == self.target:
targetRecord = featureRecord

for index in langSys.FeatureIndex:
featureRecord = self.featureRecords[index]
if featureRecord.FeatureTag in self.features:
if self.singleSub:
self.findSingleSubstitution(featureRecord)

if targetRecord is None:
# if there's no existing one, use the first matching feature as target
targetRecord = featureRecord
featureParamTypes[self.target] = (
FeatureParamsStylisticSet
if featureRecord.FeatureTag.startswith("ss")
else FeatureParamsCharacterVariants
)
featureRecord.FeatureTag = self.target
else:
Activator.moveFeatureLookups(featureRecord.Feature, targetRecord.Feature)
clearFeatureRecord(featureRecord)

if targetRecord is not None:
targetRecord.Feature.LookupListIndex.sort()

def findSingleSubstitution(self, featureRecord) -> None:
for lookupIndex in featureRecord.Feature.LookupListIndex:
lookup = self.lookup[lookupIndex]
if lookup.LookupType == 1: # Single substitution
subTables = lookup.SubTable
for sub in subTables:
for key, value in sub.mapping.items():
self.singleSubs[key] = value
elif lookup.LookupType == 2: # Multiple substitution (one-to-many)
for sub in lookup.SubTable:
for key, value_list in sub.mapping.items():
# Only handle single-element lists as single substitutions
if len(value_list) == 1:
self.singleSubs[key] = value_list[0]
elif lookup.LookupType == 7: # Extension lookup
for sub in lookup.SubTable:
ext = sub.ExtSubTable
if ext.LookupType == 1:
for key, value in ext.mapping.items():
self.singleSubs[key] = value
elif ext.LookupType == 2:
for key, value_list in ext.mapping.items():
if len(value_list) == 1:
self.singleSubs[key] = value_list[0]

def applySingleSubstitutions(self) -> None:
if not self.singleSubs:
return

cache: dict[str, str] = {}

def resolve(glyph: str) -> str:
if glyph in cache:
return cache[glyph]
seen = set()
current = glyph
while current in self.singleSubs and current not in seen:
seen.add(current)
current = self.singleSubs[current]
cache[glyph] = current
return current

for table in self.cmapTables:
for index, glyph in table.cmap.items():
table.cmap[index] = resolve(glyph)

@staticmethod
def moveFeatureLookups(fromFeature, toFeature) -> None:
toFeature.LookupListIndex.extend(fromFeature.LookupListIndex)
toFeature.LookupCount += fromFeature.LookupCount


def freeze_features(
input_path: str,
features: Sequence[str] | Iterable[str],
output_path: str | None = None,
*,
target_feature: str = "calt",
single_sub: bool = True,
) -> None:
"""
Freeze the requested features by moving their lookups into the target feature.
"""
feature_list = list(features)
if not feature_list:
return

if output_path is None:
output_path = input_path

def _apply(hide_removed: bool) -> TTFont:
global hideRemovedFeature
hideRemovedFeature = hide_removed
font = TTFont(input_path)
Activator(
font,
{
"features": feature_list,
"options": {
"target": target_feature,
"singleSub": single_sub,
},
},
)
return font

font = _apply(True)
try:
font.save(output_path)
except AssertionError as exc:
if "DELT" not in str(exc):
raise
font = _apply(False)
font.save(output_path)
36 changes: 22 additions & 14 deletions scripts/instantiate-code-fonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,14 @@
import shutil
import yaml
import sys
import logging
import ttfautohint
from fontTools.varLib import instancer
from fontTools.varLib.instancer import OverlapMode
from opentype_feature_freezer import cli as pyftfeatfreeze
from fontTools.varLib.instancer.featureVars import instantiateFeatureVariations
from dlig2calt import dlig2calt
from mergePowerlineFont import mergePowerlineFont
from ttfautohint.options import USER_OPTIONS as ttfautohint_options

# prevents over-active warning logs
logging.getLogger("opentype_feature_freezer").setLevel(logging.ERROR)
from fontfreeze_activation import freeze_features

# if you provide a custom config path, this picks it up
try:
Expand Down Expand Up @@ -92,18 +89,22 @@ def splitFont(

print("\n--------------------------------------------------------------------------------------\n" + instance)

axisLocation = {
"wght": fontOptions["Fonts"][instance]["wght"],
"CASL": fontOptions["Fonts"][instance]["CASL"],
"MONO": fontOptions["Fonts"][instance]["MONO"],
"slnt": fontOptions["Fonts"][instance]["slnt"],
"CRSV": fontOptions["Fonts"][instance]["CRSV"],
}

instanceFont = instancer.instantiateVariableFont(
varfont,
{
"wght": fontOptions["Fonts"][instance]["wght"],
"CASL": fontOptions["Fonts"][instance]["CASL"],
"MONO": fontOptions["Fonts"][instance]["MONO"],
"slnt": fontOptions["Fonts"][instance]["slnt"],
"CRSV": fontOptions["Fonts"][instance]["CRSV"],
},
axisLocation,
overlap=OverlapMode.REMOVE
)

instantiateFeatureVariations(instanceFont, axisLocation)

# UPDATE NAME ID 6, postscript name
currentPsName = getFontNameID(instanceFont, 6)
newPsName = (currentPsName\
Expand Down Expand Up @@ -155,8 +156,15 @@ def splitFont(
# -------------------------------------------------------
# Code font special stuff in post processing

# freeze in rvrn & stylistic set features with pyftfeatfreeze
pyftfeatfreeze.main([f"--features=rvrn,{','.join(fontOptions['Features'])}", outputPath, outputPath])
# Freeze rvrn and stylistic set features
# Note: ss04 and similar features use Type 2 (Multiple Substitution) lookups
features_to_freeze = ["rvrn"] + fontOptions["Features"]
freeze_features(
outputPath,
features_to_freeze,
target_feature="calt",
single_sub=True,
)

if fontOptions['Code Ligatures']:
# swap dlig2calt to make code ligatures work in old code editor apps
Expand Down