Last active
June 26, 2023 17:03
-
-
Save arrowtype/db9562858a56ab45010e390c3f788b84 to your computer and use it in GitHub Desktop.
A command-line tool to produce a "trial font" for input OTF or TTF font files, using Python 3 & FontTools
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
From | |
https://github.com/arrowtype/recursive/blob/eb821735e66402b4b485ce97ed32b09a8555341e/src/build-scripts/make-release/compute-remaining-unicodes-in-font.py | |
The FontTools Subsetter expects unicode ranges for *inclusion*, but | |
often you just know which ranges you want to *exclude* from a font subset. | |
So, this script will accept a font, then report what Unicode values | |
it includes *beyond* given Unicode ranges. | |
USAGE: | |
python compute-remaining-unicodes-in-font.py <font> --unicodes <unicode-ranges> | |
Or see usage information at: | |
python compute-remaining-unicodes-in-font.py --help | |
""" | |
from fontTools.ttLib import TTFont | |
def listUnicodeRanges(unicodeRanges): | |
# remove "U+"" from ranges | |
unicodeRanges = unicodeRanges.replace("U+", "").replace(" ", "") | |
# create set | |
unicodesIncluded = set() | |
# split up separate ranges by commas | |
for unicodeChunk in unicodeRanges.split(","): | |
# if it's a range... | |
if "-" in unicodeChunk: | |
# get start and end of range | |
start, end = unicodeChunk.split("-") | |
# go through range and add each value to the set | |
for unicodeInteger in range(int(start, 16), int(end, 16) + 1): | |
unicodesIncluded.add(unicodeInteger) | |
# if it's a single unicode... | |
else: | |
unicodesIncluded.add(int(unicodeChunk, 16)) | |
return unicodesIncluded | |
def main(): | |
# get arguments from argparse | |
args = parser.parse_args() | |
# open font at TTFont object | |
ttfont = TTFont(str(args.fontPath[0])) | |
# get set of unicode ints in font | |
rangeInFont = {x for x in ttfont["cmap"].getBestCmap()} | |
# get unicode values for ranges given in arg | |
unicodesGiven = listUnicodeRanges(args.unicodes) | |
# Find unicodes in the font which aren’t listed, but are in the font | |
unicodesRemaining = {intUnicode for intUnicode in rangeInFont if intUnicode not in unicodesGiven} | |
# convert to hex, then join in comma-separated string | |
unicodesRemaining = [hex(n) for n in unicodesRemaining] | |
unicodesRemaining = [str(hex).replace("0x", "U+").upper() for hex in unicodesRemaining] | |
unicodesRemaining = ",".join(unicodesRemaining) | |
print(unicodesRemaining) | |
if __name__ == '__main__': | |
import argparse | |
parser = argparse.ArgumentParser(description='Report what Unicode values a font includes *beyond* given Unicode ranges.') | |
parser.add_argument('fontPath', | |
help='Path to a font file', | |
nargs="+") | |
parser.add_argument("-u", "--unicodes", | |
default="U+0020-0039, U+003A-005A, U+0061-007A, U+2018-201D, U+005B, U+005D", | |
help='String of unicodes or unicode ranges, comma-separated. Example – a basic Latin set would be "U+0020-0039, U+003A-005A, U+0061-007A, U+2018-201D, U+005B, U+005D"') | |
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# !/bin/bash | |
# A shell script to coordinate the building of a trial font package | |
# | |
# USAGE: | |
# | |
# 1. Give this file permission to execute (read it first): | |
# | |
# chmod +x <path>/make-trial-package.sh | |
# | |
# 2. Run it on the command line, and include path to folder of production fonts | |
# | |
# <path>/source/scripts/make-trial-package.sh <path>/fonts-v01 | |
source venv/bin/activate | |
fontdir=$1 | |
trialdir=fonts__trial/$(basename $fontdir) | |
# get font paths in a list | |
fonts=$(find $fontdir -name "*.ttf") | |
# create trial fonts with python script | |
for font in $fonts; do | |
# compute unicodes to exclude (basically, most symbols and punctuation) | |
unicodesToKeep=$(python3 "compute-remaining-unicodes.py" $font -u "U+0023-0026,U+003C-003E,U+005B-005F,U+007B-007D,U+00A2-00BE,U+00F7,U+2018,U+2019,U+201c,U+201d,U+201a,U+201e,U+2039,U+203A,U+2026,U+2032,U+2033,U+20ac,U+2013,U+2014,U+002a") | |
# for the "replacer" glyph arg, this uses /uni25AE, a filled rectangle character. Won’t work if your font doesn’t have this glyph. | |
python3 source/03-build-scripts/02-make-trial-font.py $font -r "uni25AE" -u $unicodesToKeep | |
done | |
# ------------------------ | |
# move fonts | |
mkdir -p $trialdir/ttf | |
# move all trial fonts to a fonts__trial/ttf folder | |
trialTTFs=$(find $fontdir -name "*.trial.ttf*") | |
for ttf in $trialTTFs; do | |
mv $ttf $trialdir/ttf/$(basename $ttf) | |
done | |
# move variable TTF back up a level | |
trialVarTTF=$(find $trialdir/ttf -name "*variable.trial.ttf") | |
mv $trialVarTTF $trialdir/$(basename $trialVarTTF) | |
# ----------------- | |
# TODO: add trial license |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
WARNING: While this script will produce trial fonts from either OTF or TTF outputs, the OTFs produced do not work in Windows. | |
For this reason, it is strongly advised that you only produce TTF trial fonts with this, unless you are certain that trial users | |
will be only using them on Mac computers (which is hard to be certain of). | |
A script to create a trial font from an OpenType font file (OTF or TTF), keeping characters for specified unicodes, while hiding the rest with a "replacer" glyph. | |
This allows trial-font users to see how a given font looks to type words, but makes it simple to restrict it from full use in documents. | |
Hiding the extended character set under a "replacer" glyph prevents fallback fonts from easily being subbed in for unintended use. | |
This script is licensed under Apache 2.0: you are free to use and modify this script for commercial work. | |
USAGE: | |
1. On the command line, install python requirement FontTools: | |
pip install fonttools | |
2. Run this script on font file(s), either from the command line or from a shell script: | |
python3 <filepath>/02-make-trial-font.py <filepath>/font.ttf | |
Or, to see optional arguments, run: | |
python3 <filepath>/02-make-trial-font.py --help | |
TODO: | |
- Add trial suffix to variable postscript name IDs, if relevant. E.g. Recursive VF has nameIDs 275 RecursiveMonoLnr-Light, etc for each instance | |
- Replacer glyph shouldn't actually need to have a *unicode* value; it just needs to have a glyph. Fix this. | |
- Maybe: "hideRanges" should be an option. By default (as it works right now), the script justs hide anything that is in the font but not in the "unicodes" arg. | |
LICENSE: | |
Copyright 2021 Arrow Type LLC / Stephen Nixon | |
Licensed under the Apache License, Version 2.0 (the "License"); | |
you may not use this file except in compliance with the License. | |
You may obtain a copy of the License at | |
http://www.apache.org/licenses/LICENSE-2.0 | |
Unless required by applicable law or agreed to in writing, software | |
distributed under the License is distributed on an "AS IS" BASIS, | |
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
See the License for the specific language governing permissions and | |
limitations under the License. | |
""" | |
import shutil | |
import os | |
from fontTools.ttLib import TTFont | |
from fontTools import subset | |
# GET / SET NAME HELPER FUNCTIONS | |
def getFontNameID(font, ID, platformID=3, platEncID=1): | |
name = str(font['name'].getName(ID, platformID, platEncID)) | |
return name | |
def setFontNameID(font, ID, newName): | |
print(f"\n\t• name {ID}:") | |
macIDs = {"platformID": 3, "platEncID": 1, "langID": 0x409} | |
winIDs = {"platformID": 1, "platEncID": 0, "langID": 0x0} | |
oldMacName = font['name'].getName(ID, *macIDs.values()) | |
oldWinName = font['name'].getName(ID, *winIDs.values()) | |
if oldMacName != newName: | |
print(f"\n\t\t Mac name was '{oldMacName}'") | |
font['name'].setName(newName, ID, *macIDs.values()) | |
print(f"\n\t\t Mac name now '{newName}'") | |
if oldWinName != newName: | |
print(f"\n\t\t Win name was '{oldWinName}'") | |
font['name'].setName(newName, ID, *winIDs.values()) | |
print(f"\n\t\t Win name now '{newName}'") | |
def listUnicodeRanges(unicodeRanges): | |
# remove "U+"" from ranges | |
unicodeRanges = unicodeRanges.replace("U+", "").replace(" ", "") | |
# create set | |
unicodesIncluded = set() | |
# split up separate ranges by commas | |
for unicodeChunk in unicodeRanges.split(","): | |
# if it's a range... | |
if "-" in unicodeChunk: | |
# get start and end of range | |
start, end = unicodeChunk.split("-") | |
# go through range and add each value to the set | |
for unicodeInteger in range(int(start,16), int(end,16)+1): | |
unicodesIncluded.add(unicodeInteger) | |
# if it's a single unicode... | |
else: | |
unicodesIncluded.add(int(unicodeChunk,16)) | |
return unicodesIncluded | |
def main(): | |
# get arguments from argparse | |
args = parser.parse_args() | |
for fontPath in args.fontPaths: | |
# open font at TTFont object | |
ttfont = TTFont(fontPath) | |
filetype = fontPath.split(".")[-1] | |
# make path of temporary font for subsetting | |
tempFontPath = fontPath.replace(f".{filetype}",f".temporary.{filetype}") | |
if args.extended: | |
shutil.copyfile(fontPath, tempFontPath) | |
tempFont = TTFont(tempFontPath) | |
if not args.extended: | |
# get set of unicode ints in font | |
rangeInFont = {x for x in ttfont["cmap"].getBestCmap()} | |
unicodesToKeep = listUnicodeRanges(args.unicodes) | |
unicodesToHide = {intUnicode for intUnicode in rangeInFont if intUnicode not in unicodesToKeep} | |
# get cmap of font, find unicode for glyph with name of replacerGlyph | |
try: | |
if "U+" in args.replacer: | |
replacerGlyphUnicode = args.replacer.replace("U+","") | |
else: | |
replacerGlyphUnicode = list(ttfont["cmap"].buildReversed()[args.replacer])[0] | |
unicodesToKeep.add(replacerGlyphUnicode) | |
if replacerGlyphUnicode in unicodesToHide: | |
unicodesToHide.remove(replacerGlyphUnicode) # TODO: check if this fails if item not in set | |
except KeyError: | |
print("\nReplacer glyph has no unicode; try checking the font file to copy in an exact name.\n") | |
print("Try checking the font file to copy in an exact glyph name, e.g. 'asterisk' rather than '*'.\n") | |
print("Stopping execution.\n") | |
break | |
unicodesToKeep = [hex(n) for n in unicodesToKeep] | |
unicodesToKeep = ",".join(unicodesToKeep) | |
# Subset input font. Keep specified unicodes only. Keep all font name IDs. Keep glyph names as-is. Keep notdef from font. Output to temporary path. | |
# Note: you may wish to remove '--layout-features=*' from this list to limit opentype features. | |
subset.main([fontPath, f'--unicodes={unicodesToKeep}', "--name-IDs=*", "--layout-features=*", "--glyph-names", '--notdef-outline', f'--output-file={tempFontPath}']) | |
tempFont = TTFont(tempFontPath) | |
# ------------------------------------------------------------------------------------------------- | |
# then, add many additional unicodes to the replacer glyph to cover all diacritics, etc | |
for table in tempFont['cmap'].tables: | |
for c in unicodesToHide: | |
table.cmap[c] = args.replacer | |
# ------------------------------------------------------------------------------------------------- | |
# update font names | |
familyName = getFontNameID(ttfont, 16) | |
nameSuffix = args.suffix | |
# MUST check if familyName is not 'None', or this doesn't work (e.g. can't just check if None) | |
if familyName != 'None': | |
newFamName = familyName + f" {nameSuffix}" | |
setFontNameID(tempFont, 16, newFamName) | |
else: | |
familyName = getFontNameID(ttfont, 1) | |
newFamName = familyName + f" {nameSuffix}" | |
print("familyName is", familyName) | |
# UPDATE NAME ID 6, postscript name | |
# Format: FamilynameTrial-Stylename | |
currentPsName = getFontNameID(ttfont, 6) | |
newPsName = currentPsName.replace('-',f'{nameSuffix}-') | |
setFontNameID(tempFont, 6, newPsName) | |
# UPDATE NAME ID 4, full font name | |
# Format: Familyname Trial Stylename | |
currentFullName = getFontNameID(ttfont, 4) | |
newFullName = currentFullName.replace(familyName,f'{familyName} {nameSuffix}') | |
setFontNameID(tempFont, 4, newFullName) | |
# UPDATE NAME ID 3, unique font ID | |
# Format: 1.001;ARRW;FamilynameTrial-Stylename | |
currentUniqueName = getFontNameID(ttfont, 3) | |
newUniqueName = currentUniqueName.replace('-',f'{nameSuffix}-') | |
setFontNameID(tempFont, 3, newUniqueName) | |
# UPDATE NAME ID 1, unique font ID | |
# Format: Familyname Trial OR Familyname Trial Style (if not Regular, Italic, Bold, or Bold Italic) | |
currentFamName = getFontNameID(ttfont, 1) | |
newFamNameOne = currentFamName.replace(familyName,newFamName) | |
setFontNameID(tempFont, 1, newFamNameOne) | |
# ------------------------------------------------------------------------------------------------- | |
# save font with suffix added to name | |
tempFont.save(tempFontPath.replace(f".temporary.{filetype}",f".{nameSuffix}.{filetype}")) | |
# clean up temp subset font | |
os.remove(tempFontPath) | |
if __name__ == '__main__': | |
import argparse | |
parser = argparse.ArgumentParser(description='Make a "trial font" from OpenType font files, keeping characters for specified unicodes, while hiding the rest.') | |
parser.add_argument('fontPaths', | |
help='Path(s) to font file(s)', | |
nargs="+") | |
parser.add_argument("-x", "--extended", | |
action='store_true', | |
help='Skips character subsetting to make an "extended" trial font, with a full, unmodified character set.') | |
parser.add_argument("-u", "--unicodes", | |
default="U+0020-0039, U+003A-005A, U+0061-007A, U+2018-201D, U+005B, U+005D", | |
help='String of unicodes or unicode ranges to keep, comma-separated. Default is a basic Latin set: "U+0020-0039, U+003A-005A, U+0061-007A, U+2018-201D, U+005B, U+005D"') | |
parser.add_argument('-r','--replacer', | |
default="X", | |
help='Name of glyph that will replace unicodes to hide. If you wish to use unicode, start with "U+" like "U+0058". Glyph be in the font & cannnot be ".notdef". Default: "X". ') | |
parser.add_argument('-s','--suffix', | |
default="Trial", | |
help='Suffix to add to trial font names. Default: "Trial".') | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment