diff --git a/README.md b/README.md index 36e7b92..bcf19dd 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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. diff --git a/src/adjustFont.ts b/src/adjustFont.ts index 04a4776..ab93b01 100644 --- a/src/adjustFont.ts +++ b/src/adjustFont.ts @@ -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'; @@ -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); @@ -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; @@ -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`)); } \ No newline at end of file diff --git a/src/genFont.ts b/src/genFont.ts index e3010ab..c663b09 100644 --- a/src/genFont.ts +++ b/src/genFont.ts @@ -20,6 +20,7 @@ import path from 'path'; import chalk from 'chalk'; import { fileURLToPath } from 'url' import generateBMFont from 'msdf-bmfont-xml'; +import opentype from 'opentype.js'; let fontSrcDir: string = ''; let fontDstDir: string = ''; @@ -53,6 +54,24 @@ export interface SdfFontInfo { fontPath: string; jsonPath: string; pngPath: string; + pngPaths: string[]; + dstDir: string; + fontStyle?: string; + fontFamily?: string; + stylePageIndex?: number; +} + +export interface FontFamilyInfo { + fontFamily: string; + fieldType: 'ssdf' | 'msdf'; + styles: Array<{ + fontStyle: string; + fontPath: string; + pageIndex: number; + pageCount?: number; + }>; + jsonPath: string; + pngPaths: string[]; dstDir: string; } @@ -64,6 +83,7 @@ type FontOptions = { pot: boolean; fontSize: number; distanceRange: number; + textureSize?: [number, number]; charset?: string; } @@ -72,6 +92,312 @@ interface CharsetConfig{ presets: string[] } +/** + * Get the font style and family from a font file + * @param fontPath - Path to the font file + * @returns Object with fontStyle and fontFamily or undefined values if not readable + */ +function getFontInfo(fontPath: string): { fontStyle?: string; fontFamily?: string } { + try { + // Only try to read TTF and OTF files with opentype.js + const ext = path.extname(fontPath).toLowerCase(); + if (ext !== '.ttf' && ext !== '.otf') { + return {}; + } + + const font = opentype.loadSync(fontPath); + + // Get font family name (name ID 1) + const fontFamily = font.names.fontFamily?.en; + + // Get font style from subfamily name (name ID 2) + const subfamily = font.names.fontSubfamily?.en; + let fontStyle = subfamily; + + if (!fontStyle || fontStyle === 'Regular') { + // Fallback: try to extract from full font name + const fullName = font.names.fullName?.en; + if (fullName && fontFamily) { + // Remove family name to get style part + const stylePart = fullName.replace(fontFamily, '').trim(); + if (stylePart) { + fontStyle = stylePart; + } + } + } + + // If still no specific style found, default to Regular + if (!fontStyle || fontStyle === fontFamily) { + fontStyle = 'Regular'; + } + + return { + fontFamily, + fontStyle + }; + } catch (e) { + console.warn(`Could not read font info from ${fontPath}:`, e); + return {}; + } +} + +/** + * Groups font files by family and generates them in a single atlas with styles on different pages + * @param fontFiles - Array of font file names + * @param fieldType - The type of the font field (msdf or ssdf) + * @returns Promise - Array of font family information + */ +export async function genFontsByFamily(fontFiles: string[], fieldType: 'ssdf' | 'msdf'): Promise { + console.log(chalk.blue(`Generating ${fieldType} fonts grouped by family...`)); + + // Group fonts by family + const fontFamilies = new Map>(); + + for (const fontFile of fontFiles) { + const fontPath = path.join(fontSrcDir, fontFile); + if (!fs.existsSync(fontPath)) { + console.log(`Font ${fontFile} does not exist, skipping...`); + continue; + } + + const fontInfo = getFontInfo(fontPath); + const family = fontInfo.fontFamily || path.parse(fontFile).name.split('-')[0] || 'Unknown'; + const style = fontInfo.fontStyle || 'Regular'; + + if (!fontFamilies.has(family)) { + fontFamilies.set(family, []); + } + + fontFamilies.get(family)!.push({ + path: fontPath, + style, + fileName: fontFile + }); + } + + const results: FontFamilyInfo[] = []; + + // Generate each family + for (const [familyName, fonts] of fontFamilies) { + console.log(chalk.green(`Processing family: ${familyName} with ${fonts.length} styles`)); + + // Sort fonts to ensure Regular is first (page 0), then other styles + const sortedFonts = fonts.sort((a, b) => { + if (a.style.toLowerCase() === 'regular') return -1; + if (b.style.toLowerCase() === 'regular') return 1; + + // Then sort alphabetically for consistency + return a.style.localeCompare(b.style); + }); + + const familyResult = await generateFontFamily(familyName, sortedFonts, fieldType); + if (familyResult) { + results.push(familyResult); + } + } + + return results; +} + +/** + * Generate a single font family with multiple styles in one atlas + */ +async function generateFontFamily( + familyName: string, + fonts: Array<{ path: string; style: string; fileName: string }>, + fieldType: 'ssdf' | 'msdf' +): Promise { + + if (fonts.length === 0) { + console.log(`No fonts provided for family ${familyName}`); + return null; + } + + let bmfont_field_type: string = fieldType; + if (bmfont_field_type === 'ssdf') { + bmfont_field_type = 'sdf'; + } + + // Use only family-level overrides to ensure consistent metrics across all styles + const overrides = fs.existsSync(overridesPath) ? JSON.parse(fs.readFileSync(overridesPath, 'utf8')) : {}; + const familyOverrides = overrides[familyName]; + const font_size = familyOverrides?.[fieldType]?.fontSize || 42; + const distance_range = familyOverrides?.[fieldType]?.distanceRange || 4; + const textureWidth = familyOverrides?.[fieldType]?.textureWidth || 2048; + const textureHeight = familyOverrides?.[fieldType]?.textureHeight || 2048; + + // Create combined charset for all presets + let combinedCharset = ''; + if (fs.existsSync(charsetPath)) { + const config: CharsetConfig = JSON.parse(fs.readFileSync(charsetPath, 'utf8')); + combinedCharset = config.charset; + const presetsToApply = config.presets ? config.presets : []; + for (let i = 0; i < presetsToApply.length; i++) { + const key = presetsToApply[i]; + if (key && key in presets) { + combinedCharset += presets[key]; + } else { + console.warn(`preset, '${key}' is not available in msdf-generator presets`); + } + } + } + + // Generate individual style atlases first + const styleInfos: Array<{ fontStyle: string; fontPath: string; pageIndex: number; pageCount?: number }> = []; + const individualPngPaths: string[] = []; + let combinedJsonData: any = null; + + for (let i = 0; i < fonts.length; i++) { + const font = fonts[i]!; + console.log(chalk.cyan(`Generating style: ${font.style} (will be page ${i})`)); + + const options: FontOptions = { + fieldType: bmfont_field_type, + outputType: 'json', + roundDecimal: 6, + smartSize: true, + pot: true, + fontSize: font_size, + distanceRange: distance_range, + textureSize: [textureWidth, textureHeight], + charset: combinedCharset || undefined, + }; + + const textureData = await generateFont(font.path, fontDstDir, `${familyName}-${font.style}`, fieldType, options); + + styleInfos.push({ + fontStyle: font.style, + fontPath: font.path, + pageIndex: i + }); + + individualPngPaths.push(...textureData.pngPaths); + + // Collect JSON data from the first style as base + if (i === 0) { + const jsonPath = path.join(fontDstDir, `${familyName}-${font.style}.${fieldType}.json`); + if (fs.existsSync(jsonPath)) { + combinedJsonData = JSON.parse(fs.readFileSync(jsonPath, 'utf8')); + } + } + } + + // Create a family atlas by combining individual textures into pages + const familyPngPaths: string[] = []; + let globalPageIndex = 0; + + // Update styleInfos to track page information correctly + for (let i = 0; i < fonts.length; i++) { + const font = fonts[i]!; + const individualBaseName = `${familyName}-${font.style}.${fieldType}`; + + // Check for paginated individual files first (e.g., Ubuntu-Regular.msdf_0.png, Ubuntu-Regular.msdf_1.png) + const files = fs.readdirSync(fontDstDir); + const paginatedFiles = files + .filter(file => file.startsWith(`${individualBaseName}_`) && file.endsWith('.png')) + .sort(); + + const startPageIndex = globalPageIndex; + const stylePagesCount = paginatedFiles.length > 0 ? paginatedFiles.length : 1; + + if (paginatedFiles.length > 0) { + console.log(chalk.cyan(`Style ${font.style} has ${paginatedFiles.length} pages`)); + + for (let pageIndex = 0; pageIndex < paginatedFiles.length; pageIndex++) { + const paginatedFile = paginatedFiles[pageIndex]!; + const paginatedPath = path.join(fontDstDir, paginatedFile); + const familyPagePath = path.join(fontDstDir, `${familyName}.${fieldType}_${globalPageIndex}.png`); + + fs.copyFileSync(paginatedPath, familyPagePath); + familyPngPaths.push(familyPagePath); + console.log(chalk.green(`Created family page ${globalPageIndex}: ${familyPagePath} (${font.style} page ${pageIndex})`)); + globalPageIndex++; + } + } else { + // Fallback to single file + const individualPath = path.join(fontDstDir, `${individualBaseName}.png`); + if (fs.existsSync(individualPath)) { + const familyPagePath = path.join(fontDstDir, `${familyName}.${fieldType}_${globalPageIndex}.png`); + fs.copyFileSync(individualPath, familyPagePath); + familyPngPaths.push(familyPagePath); + console.log(chalk.green(`Created family page ${globalPageIndex}: ${familyPagePath} (${font.style})`)); + globalPageIndex++; + } + } + + // Update the style info with correct page information + styleInfos[i] = { + fontStyle: font.style, + fontPath: font.path, + pageIndex: startPageIndex, + pageCount: stylePagesCount + }; + + // Clean up individual files (we only need the family files) + // Remove the main individual PNG file if it exists + const individualPath = path.join(fontDstDir, `${individualBaseName}.png`); + if (fs.existsSync(individualPath)) { + fs.unlinkSync(individualPath); + } + + // Remove any paginated individual PNG files + for (const file of files) { + if (file.startsWith(`${individualBaseName}_`) && file.endsWith('.png')) { + const paginatedPath = path.join(fontDstDir, file); + fs.unlinkSync(paginatedPath); + console.log(chalk.gray(`Cleaned up paginated file: ${file}`)); + } + } + + // Remove individual JSON file + const individualJsonPath = path.join(fontDstDir, `${individualBaseName}.json`); + if (fs.existsSync(individualJsonPath)) { + fs.unlinkSync(individualJsonPath); + } + console.log(chalk.gray(`Cleaned up individual files for ${font.style}`)); + } + + // Create a combined JSON file for the family + if (combinedJsonData) { + const familyJsonPath = path.join(fontDstDir, `${familyName}.${fieldType}.json`); + + // Modify the JSON to include style page information + combinedJsonData.info = combinedJsonData.info || {}; + combinedJsonData.info.face = familyName; + + // Add pages array to match the actual number of family pages created + combinedJsonData.pages = []; + for (let i = 0; i < familyPngPaths.length; i++) { + const fileName = path.basename(familyPngPaths[i] || ''); + combinedJsonData.pages.push(fileName); + } + + // Add custom style mapping with page ranges + combinedJsonData.styles = styleInfos.map(style => ({ + style: style.fontStyle, + pageIndex: style.pageIndex, + pageCount: style.pageCount || 1, + pageRange: style.pageCount && style.pageCount > 1 + ? `${style.pageIndex}-${style.pageIndex + style.pageCount - 1}` + : `${style.pageIndex}` + })); + + fs.writeFileSync(familyJsonPath, JSON.stringify(combinedJsonData, null, 2)); + console.log(chalk.green(`Created family descriptor: ${familyJsonPath}`)); + + return { + fontFamily: familyName, + fieldType, + styles: styleInfos, + jsonPath: familyJsonPath, + pngPaths: familyPngPaths, + dstDir: fontDstDir + }; + } + + return null; +} + /** * Generates a font file in the specified field type. * @param fontFileName - The name of the font. @@ -100,6 +426,8 @@ export async function genFont(fontFileName: string, fieldType: 'ssdf' | 'msdf'): const font_size = overrides[fontNameNoExt]?.[fieldType]?.fontSize || 42; const distance_range = overrides[fontNameNoExt]?.[fieldType]?.distanceRange || 4; + const textureWidth = overrides[fontNameNoExt]?.[fieldType]?.textureWidth || 2048; + const textureHeight = overrides[fontNameNoExt]?.[fieldType]?.textureHeight || 2048; let options: FontOptions = { fieldType: bmfont_field_type, @@ -109,6 +437,7 @@ export async function genFont(fontFileName: string, fieldType: 'ssdf' | 'msdf'): pot: true, fontSize: font_size, distanceRange: distance_range, + textureSize: [textureWidth, textureHeight], } if (fs.existsSync(charsetPath)) { @@ -126,21 +455,23 @@ export async function genFont(fontFileName: string, fieldType: 'ssdf' | 'msdf'): options['charset'] = charset } - await generateFont(fontPath, fontDstDir, fontNameNoExt, fieldType, options) + const textureData = await generateFont(fontPath, fontDstDir, fontNameNoExt, fieldType, options) const info: SdfFontInfo = { fontName: fontNameNoExt, fieldType, jsonPath: path.join(fontDstDir, `${fontNameNoExt}.${fieldType}.json`), - pngPath: path.join(fontDstDir, `${fontNameNoExt}.${fieldType}.png`), + pngPath: textureData.pngPaths[0] || path.join(fontDstDir, `${fontNameNoExt}.${fieldType}.png`), + pngPaths: textureData.pngPaths, fontPath, dstDir: fontDstDir, + stylePageIndex: 0, }; return info; } -const generateFont = (fontSrcPath: string, fontDestPath: string, fontName: string, fieldType: string, options: FontOptions): Promise => { +const generateFont = (fontSrcPath: string, fontDestPath: string, fontName: string, fieldType: string, options: FontOptions): Promise<{pngPaths: string[]}> => { return new Promise((resolve, reject) => { if (!fs.existsSync(fontDestPath)) { fs.mkdirSync(fontDestPath, { recursive: true }) @@ -153,9 +484,16 @@ const generateFont = (fontSrcPath: string, fontDestPath: string, fontName: strin console.error(err) reject(err) } else { - textures.forEach((texture: any) => { + const pngPaths: string[] = []; + textures.forEach((texture: any, index: number) => { try { - fs.writeFileSync(path.resolve(fontDestPath, `${fontName}.${fieldType}.png`), texture.texture) + // Handle multiple textures for pagination + const textureFileName = textures.length > 1 + ? `${fontName}.${fieldType}_${index}.png` + : `${fontName}.${fieldType}.png`; + const texturePath = path.resolve(fontDestPath, textureFileName); + fs.writeFileSync(texturePath, texture.texture); + pngPaths.push(texturePath); } catch (e) { console.error(e) reject(e) @@ -163,7 +501,7 @@ const generateFont = (fontSrcPath: string, fontDestPath: string, fontName: strin }) try { fs.writeFileSync(path.resolve(fontDestPath, `${fontName}.${fieldType}.json`), font.data) - resolve() + resolve({pngPaths}) } catch (e) { console.error(err) reject(e) diff --git a/src/index.ts b/src/index.ts index 461b192..2f2e9b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,7 @@ */ import { adjustFont } from './adjustFont.js'; -import { genFont, setGeneratePaths } from './genFont.js'; +import { genFont, genFontsByFamily, setGeneratePaths } from './genFont.js'; import fs from 'fs-extra'; import chalk from 'chalk'; @@ -37,23 +37,75 @@ fs.ensureDirSync(fontDstDir); export async function generateFonts() { try { const files = fs.readdirSync(fontSrcDir); - let fontsFound = 0; - for (const file of files) { - for (const ext of font_exts) { - if (file.endsWith(ext)) { - fontsFound++; - const msdfFont = await genFont(file, 'msdf'); - if (msdfFont) await adjustFont(msdfFont); - - const ssdfFont = await genFont(file, 'ssdf'); - if (ssdfFont) await adjustFont(ssdfFont); - } - } - } - if (fontsFound === 0) { + const fontFiles = files.filter(file => + font_exts.some(ext => file.endsWith(ext)) + ); + + if (fontFiles.length === 0) { console.log(chalk.red.bold('No font files found in `font-src` directory. Exiting...')); process.exit(1); } + + // Parse command line arguments + const args = process.argv.slice(2); + let useFamily = false; // Default to no family grouping + + // Check for --individual or -i flag to use individual mode + if (args.includes('--individual') || args.includes('-i')) { + useFamily = false; + } + + // Check for --family or -f flag to explicitly use family mode + if (args.includes('--family') || args.includes('-f')) { + useFamily = true; + } + + // Check for --help or -h flag + if (args.includes('--help') || args.includes('-h')) { + console.log(chalk.cyan('\nUsage: npm run generate [options]')); + console.log(chalk.cyan('\nOptions:')); + console.log(chalk.cyan(' --family, -f Group fonts by family (Only ttf/otf files)')); + console.log(chalk.cyan(' --individual, -i Generate individual fonts (default)')); + console.log(chalk.cyan(' --help, -h Show this help message')); + console.log(chalk.cyan('\nFamily mode groups font styles (Regular, Bold, Italic) into')); + console.log(chalk.cyan('separate pages within the same atlas for optimal WebGL performance.')); + console.log(chalk.cyan('\nIndividual mode generates separate atlases for each font file.')); + console.log(chalk.cyan("Woff and Woff2 formats are only supported in individual mode.")); + process.exit(0); + } + + console.log(chalk.yellow(`Generation mode: ${useFamily ? 'Family grouping' : 'Individual fonts'}`)); + console.log(chalk.gray(`(Use --individual or --family to change mode, --help for options)`)); + + if (useFamily) { + console.log(chalk.green('\nGenerating fonts by family (styles as separate pages)...')); + + // Generate MSDF fonts grouped by family + const msdfFamilies = await genFontsByFamily(fontFiles, 'msdf'); + console.log(chalk.green(`Generated ${msdfFamilies.length} MSDF families`)); + for (const family of msdfFamilies) { + console.log(chalk.green(`Generated MSDF family: ${family.fontFamily} with ${family.styles.length} styles`)); + if (family) await adjustFont(family); + } + + // Generate SSDF fonts grouped by family + const ssdfFamilies = await genFontsByFamily(fontFiles, 'ssdf'); + for (const family of ssdfFamilies) { + console.log(chalk.green(`Generated SSDF family: ${family.fontFamily} with ${family.styles.length} styles`)); + if (family) await adjustFont(family); + } + } else { + console.log(chalk.green('\nGenerating individual fonts (original behavior)...')); + + // Original individual font generation + for (const file of fontFiles) { + const msdfFont = await genFont(file, 'msdf'); + if (msdfFont) await adjustFont(msdfFont); + + const ssdfFont = await genFont(file, 'ssdf'); + if (ssdfFont) await adjustFont(ssdfFont); + } + } } catch (error) { console.error(chalk.red('Error generating fonts:'), error); process.exit(1);