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
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,26 @@ cp -R font-src-sample font-src

1. Copy Fonts: Place all the font files you want to convert to SDF fonts into the `font-src` directory.

2. Generate SDF Textures: Run the following command to generate SDF textures from the font files:
2. Generate SDF Textures: Run the following command to generate SDF textures from the font files. You can control how fonts are processed using command line options:

```sh
pnpm generate [options]
```
pnpm generate

**Options:**
- `--family`, `-f` Group fonts by family (Only ttf/otf files)
- `--individual`, `-i` Generate individual fonts (default)
- `--help`, `-h` Show this help message

**Family mode** groups font styles (Regular, Bold, Italic) into separate pages within the same atlas for optimal WebGL performance.

**Individual mode** generates separate atlases for each font file. WOFF and WOFF2 formats are only supported in individual mode.

**Examples:**
```sh
pnpm generate --family
pnpm generate --individual
pnpm generate --help
```

3. Access Generated Files: The generated SDF font files will be available in the `font-dst` directory.
Expand Down Expand Up @@ -121,6 +137,10 @@ By default this tool will generate SDF fonts with these properties:
- Distance Range: 4
- The distance range defines number of pixels of used in rendering the actual signed-distance field of the atlas texture.
- Generally this value shouldn't have to be adjusted, but feel free to tweak along with the font size in order to get the highest quality text rendering with the smallest atlas texture size. It **must** be a multiple of 2.
- textureWidth: 2048
The width of the output PNG texture atlas that contains the rendered font glyphs.
- textureHeight: 2048
The height of the output PNG texture atlas that contains the rendered font glyphs.

For each font file in the `font-src` directory you can define overrides for these values in the `font-src/overrides.json` file.

Expand Down
126 changes: 117 additions & 9 deletions src/adjustFont.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import fs from 'fs-extra';
import path from 'path';
import chalk from 'chalk';
import opentype from 'opentype.js';
import type { SdfFontInfo } from "./genFont.js";
import type { SdfFontInfo, FontFamilyInfo } from "./genFont.js";

const metricsSubDir = 'metrics';

Expand All @@ -36,13 +36,23 @@ const metricsSubDir = 'metrics';
*
* @param fontInfo
*/
export async function adjustFont(fontInfo: SdfFontInfo) {
export async function adjustFont(fontInfo: SdfFontInfo | FontFamilyInfo) {
console.log(chalk.magenta(`Adjusting ${chalk.bold(path.basename(fontInfo.jsonPath))}...`));
const [
jsonFileContents,
font,
] = await Promise.all([
fs.readFile(fontInfo.jsonPath, 'utf8'),

// Handle FontFamilyInfo differently from SdfFontInfo
if ('styles' in fontInfo) {
await adjustFontFamily(fontInfo);
} else {
await adjustSingleFont(fontInfo);
}
}

/**
* Adjust a single font (SdfFontInfo)
*/
async function adjustSingleFont(fontInfo: SdfFontInfo) {
const [jsonFileContents, font] = await Promise.all([
fs.readFile(fontInfo.jsonPath, "utf8"),
opentype.load(fontInfo.fontPath),
]);
const json = JSON.parse(jsonFileContents);
Expand All @@ -52,7 +62,7 @@ export async function adjustFont(fontInfo: SdfFontInfo) {
*
* (This is really just distanceField / 2 but guarantees a truncated integer result)
*/
const pad = (distanceField >> 1);
const pad = distanceField >> 1;

// Remove 1x pad from the baseline
json.common.base = json.common.base - pad;
Expand Down Expand Up @@ -82,6 +92,104 @@ export async function adjustFont(fontInfo: SdfFontInfo) {
await fs.ensureDir(metricsDir);
await fs.writeFile(metricsFilePath, JSON.stringify(fontMetrics, null, 2));
})(),
fs.writeFile(fontInfo.jsonPath, JSON.stringify(json, null, 2))
fs.writeFile(fontInfo.jsonPath, JSON.stringify(json, null, 2)),
]);
}

/**
* Adjust a font family (FontFamilyInfo)
*/
async function adjustFontFamily(fontInfo: FontFamilyInfo) {
const jsonFileContents = await fs.readFile(fontInfo.jsonPath, "utf8");
const json = JSON.parse(jsonFileContents);

// Use the first style to get reference font data (should be Regular if sorted properly)
const firstStyle = fontInfo.styles[0];
if (!firstStyle) {
console.warn(`No styles found in font family ${fontInfo.fontFamily}`);
return;
}

// Load the reference font to get metrics and adjustment data
const font = await opentype.load(firstStyle.fontPath);

let distanceField = 4; // Default fallback

// Try to determine distanceField from the family JSON structure
if (json.distanceField && json.distanceField.distanceRange) {
distanceField = json.distanceField.distanceRange;
}

const pad = distanceField >> 1;

// Adjust the common baseline if present
if (json.common && json.common.base) {
json.common.base = json.common.base - pad;
}

// Adjust character offsets if present
if (json.chars) {
for (const char of json.chars) {
char.yoffset = char.yoffset - pad - pad;
}
}

const fontMetrics = {
ascender: font.tables.os2!.sTypoAscender as number,
descender: font.tables.os2!.sTypoDescender as number,
lineGap: font.tables.os2!.sTypoLineGap as number,
unitsPerEm: font.unitsPerEm,
};

// Add the font metrics to the family JSON
json.lightningMetrics = fontMetrics;

// Write metrics files for the family
const metricsDir = path.join(fontInfo.dstDir, metricsSubDir);
await fs.ensureDir(metricsDir);

const writePromises = [];

// Check if all styles have the same metrics
const allStyleMetrics = [];
for (const style of fontInfo.styles) {
const styleFont = await opentype.load(style.fontPath);
const styleMetrics = {
ascender: styleFont.tables.os2!.sTypoAscender as number,
descender: styleFont.tables.os2!.sTypoDescender as number,
lineGap: styleFont.tables.os2!.sTypoLineGap as number,
unitsPerEm: styleFont.unitsPerEm,
};
allStyleMetrics.push({ style: style.fontStyle, metrics: styleMetrics });
}

// Compare all metrics to see if they're identical
const firstMetrics = allStyleMetrics[0]?.metrics;
const allMetricsIdentical = allStyleMetrics.every(item =>
item.metrics.ascender === firstMetrics?.ascender &&
item.metrics.descender === firstMetrics?.descender &&
item.metrics.lineGap === firstMetrics?.lineGap &&
item.metrics.unitsPerEm === firstMetrics?.unitsPerEm
);

if (allMetricsIdentical && firstMetrics) {
// All styles have identical metrics - write a single family metrics file
const familyMetricsPath = path.join(metricsDir, `${fontInfo.fontFamily}.metrics.json`);
writePromises.push(fs.writeFile(familyMetricsPath, JSON.stringify(firstMetrics, null, 2)));
console.log(chalk.cyan(`All styles have identical metrics - created shared family metrics file`));
} else {
// Metrics differ between styles - write individual files
for (const item of allStyleMetrics) {
const metricsFilePath = path.join(metricsDir, `${fontInfo.fontFamily}-${item.style}.metrics.json`);
writePromises.push(fs.writeFile(metricsFilePath, JSON.stringify(item.metrics, null, 2)));
}
console.log(chalk.yellow(`Styles have different metrics - created individual metrics files`));
}

// Write the updated family JSON
writePromises.push(fs.writeFile(fontInfo.jsonPath, JSON.stringify(json, null, 2)));

await Promise.all(writePromises);

console.log(chalk.green(`Adjusted family ${fontInfo.fontFamily} with ${fontInfo.styles.length} styles`));
}
Loading