Skip to content

Commit

Permalink
Merge pull request #1967 from googlefonts/add-glyphset-dialog
Browse files Browse the repository at this point in the history
[font overview] Vastly improve preset glyphset selection
  • Loading branch information
justvanrossum authored Jan 21, 2025
2 parents 677622f + 5e9327f commit b01efbf
Show file tree
Hide file tree
Showing 10 changed files with 1,209 additions and 256 deletions.
153 changes: 153 additions & 0 deletions scripts/rebuild_glyphset_presets.py
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))
33 changes: 0 additions & 33 deletions src/fontra/client/core/parse-glyph-set.js

This file was deleted.

155 changes: 155 additions & 0 deletions src/fontra/client/core/parse-glyphset.js
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 ";";
}
Loading

0 comments on commit b01efbf

Please sign in to comment.