-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1967 from googlefonts/add-glyphset-dialog
[font overview] Vastly improve preset glyphset selection
- Loading branch information
Showing
10 changed files
with
1,209 additions
and
256 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
#!/usr/bin/env python | ||
|
||
|
||
import argparse | ||
import json | ||
from urllib.request import Request, urlopen | ||
|
||
AUTH_TOKEN = None | ||
|
||
|
||
def fetchJSON(url): | ||
request = Request(url) | ||
if AUTH_TOKEN: | ||
request.add_header("Authorization", f"token {AUTH_TOKEN}") | ||
response = urlopen(request) | ||
data = response.read() | ||
return json.loads(data) | ||
|
||
|
||
# Unauthenticated: max. 60 request per hour, authenticated 5000 per hour | ||
# https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#primary-rate-limit-for-unauthenticated-users | ||
def getGitHubDirectoryInfo(org, repo, path): | ||
dirURL = f"https://api.github.com/repos/{org}/{repo}/contents/{path}" | ||
return fetchJSON(dirURL) | ||
|
||
|
||
def jsDelivrURL(org, repo, path): | ||
return f"https://cdn.jsdelivr.net/gh/{org}/{repo}/{path}" | ||
|
||
|
||
def getGoogleFontsGlyphSets(): | ||
sourceURL = "https://github.com/googlefonts/glyphsets" | ||
|
||
dirContents = getGitHubDirectoryInfo( | ||
"googlefonts", "glyphsets", "data/results/txt/nice-names/" | ||
) | ||
|
||
glyphSets = [] | ||
|
||
for dirInfo in dirContents: | ||
name = dirInfo["name"] | ||
assert name.endswith(".txt") | ||
name = " ".join(name[:-4].split("_")) | ||
glyphSets.append( | ||
{ | ||
"name": name, | ||
"url": jsDelivrURL("googlefonts", "glyphsets", dirInfo["path"]), | ||
} | ||
) | ||
|
||
return { | ||
"name": "Google Fonts", | ||
"sourceURL": sourceURL, | ||
"dataOptions": {"dataFormat": "glyph-names", "commentChars": "#"}, | ||
"glyphSets": glyphSets, | ||
} | ||
|
||
|
||
def getBlackFoundryGlyphSets(): | ||
sourceURL = "https://github.com/BlackFoundryCom/BF_font_standard" | ||
|
||
glyphSets = [] | ||
|
||
for topInfo in getGitHubDirectoryInfo("BlackFoundryCom", "BF_font_standard", ""): | ||
if topInfo["type"] != "dir": | ||
continue | ||
|
||
for dirInfo in getGitHubDirectoryInfo( | ||
"BlackFoundryCom", "BF_font_standard", topInfo["name"] | ||
): | ||
name = dirInfo["name"] | ||
if not name.endswith(".csv"): | ||
continue | ||
|
||
name = " ".join(name[:-4].split("_")) | ||
name = name.capitalize() | ||
name = "BF " + name | ||
glyphSets.append( | ||
{ | ||
"name": name, | ||
"url": jsDelivrURL( | ||
"BlackFoundryCom", "BF_font_standard", dirInfo["path"] | ||
), | ||
} | ||
) | ||
|
||
return { | ||
"name": "Black Foundry", | ||
"sourceURL": sourceURL, | ||
"dataOptions": { | ||
"dataFormat": "tsv/csv", | ||
"hasHeader": True, | ||
"codePointColumn": "unicode hex", | ||
"glyphNameColumn": "name", | ||
}, | ||
"glyphSets": glyphSets, | ||
} | ||
|
||
|
||
def getAdobeLatinCyrGreekGlyphSets(): | ||
sourceURL = "https://github.com/orgs/adobe-type-tools/repositories?q=charsets" | ||
|
||
glyphSets = [] | ||
|
||
repos = ["adobe-latin-charsets", "adobe-cyrillic-charsets", "adobe-greek-charsets"] | ||
|
||
for repo in repos: | ||
for topInfo in getGitHubDirectoryInfo("adobe-type-tools", repo, ""): | ||
name = topInfo["name"] | ||
if not name.endswith(".txt"): | ||
continue | ||
if "-combined" in name: | ||
# Incompatible format | ||
continue | ||
|
||
name = " ".join(p.capitalize() for p in name[:-4].split("-")) | ||
|
||
glyphSets.append( | ||
{ | ||
"name": name, | ||
"url": jsDelivrURL("adobe-type-tools", repo, topInfo["path"]), | ||
} | ||
) | ||
|
||
return { | ||
"name": "Adobe Latin, Cyrillic, Greek", | ||
"sourceURL": sourceURL, | ||
"dataOptions": { | ||
"dataFormat": "tsv/csv", | ||
"hasHeader": True, | ||
"codePointColumn": "Unicode", | ||
"glyphNameColumn": "Glyph name", | ||
}, | ||
"glyphSets": glyphSets, | ||
} | ||
|
||
|
||
def collectCollections(): | ||
collections = [] | ||
collections.append(getGoogleFontsGlyphSets()) | ||
collections.append(getBlackFoundryGlyphSets()) | ||
collections.append(getAdobeLatinCyrGreekGlyphSets()) | ||
return collections | ||
|
||
|
||
if __name__ == "__main__": | ||
parser = argparse.ArgumentParser() | ||
parser.add_argument("--token") | ||
args = parser.parse_args() | ||
AUTH_TOKEN = args.token | ||
|
||
collections = collectCollections() | ||
print(json.dumps(collections, indent=2)) |
This file was deleted.
Oops, something went wrong.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
import { getCodePointFromGlyphName, getSuggestedGlyphName } from "./glyph-data.js"; | ||
|
||
export const glyphSetDataFormats = [ | ||
{ | ||
value: "glyph-names", | ||
label: "Glyph names (whitespace-separated)", | ||
}, | ||
{ | ||
value: "tsv/csv", | ||
label: "TSV/CSV (tab-, comma-, or semicolon-separated)", | ||
}, | ||
]; | ||
|
||
export function parseGlyphSet(sourceData, dataFormat, dataOptions) { | ||
const sourceLines = sourceData.split(/\r?\n/); | ||
|
||
switch (dataFormat) { | ||
case "glyph-names": | ||
return parseGlyphSetGlyphNames(sourceLines, dataOptions); | ||
case "tsv/csv": | ||
return parseGlyphSetGlyphTable(sourceLines, dataOptions); | ||
default: | ||
throw new Error(`unknow data format: ${dataFormat}`); | ||
} | ||
} | ||
|
||
function parseGlyphSetGlyphNames(sourceLines, dataOptions) { | ||
const glyphSet = []; | ||
|
||
for (let line of sourceLines) { | ||
line = stripLineComments(line, dataOptions?.commentChars); | ||
line = line.trim(); | ||
if (!line) { | ||
continue; | ||
} | ||
|
||
for (const glyphName of line.split(/\s+/)) { | ||
const codePoint = getCodePointFromGlyphName(glyphName); | ||
glyphSet.push({ glyphName, codePoints: codePoint ? [codePoint] : [] }); | ||
} | ||
} | ||
|
||
return glyphSet; | ||
} | ||
|
||
function parseGlyphSetGlyphTable(sourceLines, dataOptions) { | ||
const rowSeparator = guessRowSeparator(sourceLines.slice(0, 200)); | ||
|
||
let tableHeader; | ||
let glyphNameColumnIndex = parseInt(dataOptions.glyphNameColumn); | ||
let codePointColumnIndex = parseInt(dataOptions.codePointColumn); | ||
if (!dataOptions.hasHeader) { | ||
// If we don't have a header, we should at least have an index for the | ||
// code point column OR the glyph name column | ||
if (isNaN(glyphNameColumnIndex) && isNaN(codePointColumnIndex)) { | ||
throw new Error( | ||
`invalid glyph name column and/or code point column: | ||
“${dataOptions.glyphNameColumn}” / “${dataOptions.codePointColumn}”. | ||
Without a table header, these values must be zero-based indices.` | ||
); | ||
} | ||
} | ||
|
||
const glyphSet = []; | ||
for (let line of sourceLines) { | ||
line = stripLineComments(line, dataOptions?.commentChars); | ||
|
||
const row = line.split(rowSeparator).map((item) => item.trim()); | ||
if (!tableHeader && dataOptions.hasHeader) { | ||
tableHeader = row; | ||
|
||
if (isNaN(glyphNameColumnIndex) && dataOptions.glyphNameColumn) { | ||
glyphNameColumnIndex = tableHeader.indexOf(dataOptions.glyphNameColumn); | ||
if (glyphNameColumnIndex < 0) { | ||
throw new Error(`invalid glyphNameColumn: ${dataOptions.glyphNameColumn}`); | ||
} | ||
} | ||
|
||
if (isNaN(codePointColumnIndex) && dataOptions.codePointColumn) { | ||
codePointColumnIndex = tableHeader.indexOf(dataOptions.codePointColumn); | ||
if (codePointColumnIndex < 0) { | ||
throw new Error(`invalid codePointColumn: ${dataOptions.codePointColumn}`); | ||
} | ||
} | ||
continue; | ||
} | ||
|
||
let glyphName = row[glyphNameColumnIndex]; | ||
let codePoint; | ||
|
||
let codePointCell = row[codePointColumnIndex]; | ||
if (codePointCell) { | ||
if (dataOptions.codePointIsDecimal) { | ||
codePoint = parseInt(codePointCell); | ||
} else { | ||
// Hex | ||
if (codePointCell.startsWith("U+") || codePointCell.startsWith("0x")) { | ||
codePointCell = codePointCell.slice(2); | ||
} | ||
codePoint = parseInt(codePointCell, 16); | ||
} | ||
} | ||
|
||
if (!glyphName && !codePoint) { | ||
continue; | ||
} | ||
if (!glyphName) { | ||
glyphName = getSuggestedGlyphName(codePoint); | ||
} else if (!codePoint) { | ||
codePoint = getCodePointFromGlyphName(glyphName); | ||
} | ||
|
||
glyphSet.push({ glyphName, codePoints: codePoint ? [codePoint] : [] }); | ||
} | ||
|
||
return glyphSet; | ||
} | ||
|
||
function stripLineComments(line, commentChars) { | ||
for (const commentChar of commentChars || "") { | ||
const commentIndex = line.indexOf(commentChar); | ||
if (commentIndex >= 0) { | ||
line = line.slice(0, commentIndex); | ||
} | ||
} | ||
return line; | ||
} | ||
|
||
function guessRowSeparator(lines) { | ||
let tabCount = 0; | ||
let commaCount = 0; | ||
let semiColonCount = 0; | ||
for (const line of lines) { | ||
for (const char of line) { | ||
switch (char) { | ||
case "\t": | ||
tabCount++; | ||
break; | ||
case ",": | ||
commaCount++; | ||
break; | ||
case ";": | ||
semiColonCount++; | ||
break; | ||
} | ||
} | ||
} | ||
if (tabCount > commaCount && tabCount > semiColonCount) { | ||
return "\t"; | ||
} else if (commaCount > semiColonCount) { | ||
return ","; | ||
} | ||
return ";"; | ||
} |
Oops, something went wrong.