fontmap / src /typography /new-pipe /2-generate-svgs.mjs
tfrere's picture
tfrere HF Staff
first commit
eebc40f
#!/usr/bin/env node
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import opentype from 'opentype.js';
import cliProgress from 'cli-progress';
import chalk from 'chalk';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Configuration
const FONTS_DIR = path.join(__dirname, 'output', 'fonts');
const SVGS_DIR = path.join(__dirname, 'output', 'svgs');
const FONT_INDEX_PATH = path.join(__dirname, 'input', 'font-index.json');
// Progress bar configuration
const progressBar = new cliProgress.SingleBar({
format: chalk.cyan('{bar}') + ' | {percentage}% | {value}/{total} | {fontName}',
barCompleteChar: '\u2588',
barIncompleteChar: '\u2591',
hideCursor: true
});
/**
* Valide la qualité d'un SVG
*/
function validateSVGQuality(svg, fontFamily) {
const issues = [];
if (!svg || svg.trim().length === 0) {
issues.push('Empty SVG');
return { valid: false, issues };
}
if (!svg.includes('<path')) {
issues.push('No path elements found');
return { valid: false, issues };
}
const pathMatch = svg.match(/<path[^>]*d=["']([^"']+)["']/);
if (!pathMatch || !pathMatch[1] || pathMatch[1].trim().length === 0) {
issues.push('Empty path data');
return { valid: false, issues };
}
const pathData = pathMatch[1];
if (pathData.length < 10) {
issues.push('Path data too simple');
return { valid: false, issues };
}
if (!svg.includes('xmlns="http://www.w3.org/2000/svg"')) {
issues.push('Structure SVG invalide');
return { valid: false, issues };
}
return { valid: true, issues: [] };
}
/**
* Génère un SVG de la lettre A à partir d'une police
*/
async function generateLetterASVG(fontPath, fontFamily) {
try {
const fontBuffer = await fs.readFile(fontPath);
const font = opentype.parse(fontBuffer.buffer);
const glyph = font.charToGlyph('A');
if (!glyph || !glyph.path) {
throw new Error('Glyph A not found or without path');
}
const SVG_SIZE = 80;
const fontSize = 60;
const tempPath = glyph.getPath(0, 0, fontSize);
const bbox = tempPath.getBoundingBox();
if (!bbox || bbox.x1 === undefined || bbox.x2 === undefined ||
bbox.y1 === undefined || bbox.y2 === undefined) {
throw new Error('Bounding box invalide');
}
const glyphWidth = bbox.x2 - bbox.x1;
const glyphHeight = bbox.y2 - bbox.y1;
if (glyphWidth <= 0 || glyphHeight <= 0) {
throw new Error('Dimensions de glyphe invalides');
}
if (glyphWidth < 5 || glyphHeight < 5) {
throw new Error('Glyphe trop petit (possiblement vide)');
}
const centerX = SVG_SIZE / 2;
const centerY = SVG_SIZE / 2;
const offsetX = centerX - (bbox.x1 + glyphWidth / 2);
const offsetY = centerY - (bbox.y1 + glyphHeight / 2);
const adjustedPath = glyph.getPath(offsetX, offsetY, fontSize);
const svgPathData = adjustedPath.toPathData(2);
if (!svgPathData || svgPathData.trim().length === 0) {
throw new Error('Empty path data après génération');
}
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${SVG_SIZE} ${SVG_SIZE}" width="${SVG_SIZE}" height="${SVG_SIZE}">
<path d="${svgPathData}" fill="currentColor"/>
</svg>`;
const validation = validateSVGQuality(svg, fontFamily);
if (!validation.valid) {
throw new Error(`SVG de mauvaise qualité: ${validation.issues.join(', ')}`);
}
return {
svg,
width: SVG_SIZE,
height: SVG_SIZE,
fontMetrics: {
unitsPerEm: font.unitsPerEm,
ascender: font.ascender,
descender: font.descender
}
};
} catch (error) {
console.error(`❌ Error generating SVG for ${fontFamily}:`, error.message);
return null;
}
}
/**
* Génère un SVG de phrase à partir d'une police (vectorisé)
*/
async function generateSentenceSVG(fontPath, fontFamily) {
try {
const fontBuffer = await fs.readFile(fontPath);
const font = opentype.parse(fontBuffer.buffer);
const loremText = 'Lorem Ipsum';
const fontSize = 24;
const padding = 10;
// Vectoriser le texte complet en une seule opération
const textPath = font.getPath(loremText, 0, 0, fontSize);
const bbox = textPath.getBoundingBox();
if (!bbox || bbox.x1 === undefined || bbox.x2 === undefined ||
bbox.y1 === undefined || bbox.y2 === undefined) {
throw new Error('Bounding box invalide pour le texte');
}
const textWidth = bbox.x2 - bbox.x1;
const textHeight = bbox.y2 - bbox.y1;
if (textWidth <= 0 || textHeight <= 0) {
throw new Error('Dimensions de texte invalides');
}
// Calculer les dimensions du SVG avec padding
const margin = padding;
const svgWidth = Math.ceil(textWidth) + (margin * 2);
const svgHeight = Math.ceil(textHeight) + (margin * 2);
// Centrer le texte dans le SVG
const offsetX = margin - bbox.x1;
const offsetY = margin - bbox.y1;
// Ajuster le chemin avec les offsets
const adjustedPath = font.getPath(loremText, offsetX, offsetY, fontSize);
const svgPathData = adjustedPath.toPathData(2);
if (!svgPathData || svgPathData.trim().length === 0) {
throw new Error('Empty path data après génération');
}
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svgWidth} ${svgHeight}" width="${svgWidth}" height="${svgHeight}">
<path d="${svgPathData}" fill="currentColor"/>
</svg>`;
// Valider la qualité du SVG
const validation = validateSVGQuality(svg, fontFamily);
if (!validation.valid) {
throw new Error(`SVG de mauvaise qualité: ${validation.issues.join(', ')}`);
}
return {
svg,
width: svgWidth,
height: svgHeight,
text: loremText,
measuredWidth: textWidth,
fontMetrics: {
unitsPerEm: font.unitsPerEm,
ascender: font.ascender,
descender: font.descender
}
};
} catch (error) {
console.error(`❌ Error generating sentence SVG for ${fontFamily}:`, error.message);
return null;
}
}
/**
* Traite une famille de polices
*/
async function processFontFamily(fontId, fontData, currentIndex, totalFamilies) {
const fontDir = path.join(FONTS_DIR, fontId);
try {
// Chercher le fichier TTF principal (peut être .ttf ou .truetype)
const files = await fs.readdir(fontDir);
const ttfFile = files.find(f => f.endsWith('.ttf') || f.endsWith('.truetype'));
if (!ttfFile) {
throw new Error('TTF file not found');
}
const fontPath = path.join(fontDir, ttfFile);
// Générer le SVG de la lettre A
const letterResult = await generateLetterASVG(fontPath, fontData.family);
if (!letterResult) {
throw new Error('Failed de génération du SVG de la lettre A');
}
// Sauvegarder le SVG de la lettre A
const letterSvgPath = path.join(SVGS_DIR, `${fontId}_a.svg`);
await fs.writeFile(letterSvgPath, letterResult.svg, 'utf-8');
// Générer le SVG de la phrase
const sentenceResult = await generateSentenceSVG(fontPath, fontData.family);
if (!sentenceResult) {
throw new Error('Failed de génération du SVG de la phrase');
}
// Sauvegarder le SVG de la phrase
const sentenceSvgPath = path.join(SVGS_DIR, `${fontId}_sentence.svg`);
await fs.writeFile(sentenceSvgPath, sentenceResult.svg, 'utf-8');
return {
success: true,
fontId,
fontFamily: fontData.family,
letterSvg: letterSvgPath,
sentenceSvg: sentenceSvgPath,
letterDimensions: {
width: letterResult.width,
height: letterResult.height
},
sentenceDimensions: {
width: sentenceResult.width,
height: sentenceResult.height,
measuredWidth: sentenceResult.measuredWidth
},
fontMetrics: letterResult.fontMetrics,
sentenceFontMetrics: sentenceResult.fontMetrics
};
} catch (error) {
return {
success: false,
fontId,
fontFamily: fontData.family,
error: error.message
};
}
}
/**
* Fonction principale
*/
async function main() {
try {
console.log(chalk.blue.bold('🎨 Generating SVGs pour toutes les polices...\n'));
// Check that file d'index existe
try {
await fs.access(FONT_INDEX_PATH);
} catch {
throw new Error(`Fichier d'index non trouvé : ${FONT_INDEX_PATH}`);
}
// Create output directory
await fs.mkdir(SVGS_DIR, { recursive: true });
console.log(chalk.green(`📁 Directory created : ${SVGS_DIR}`));
// Read file d'index des polices
console.log(chalk.yellow('📖 Reading file font-index.json...'));
const fontIndexData = JSON.parse(await fs.readFile(FONT_INDEX_PATH, 'utf8'));
const fontIds = Object.keys(fontIndexData);
console.log(chalk.cyan(`📊 ${fontIds.length} familles de polices trouvées\n`));
const results = [];
let successCount = 0;
let errorCount = 0;
// Traiter chaque famille de polices
for (let i = 0; i < fontIds.length; i++) {
const fontId = fontIds[i];
const fontData = fontIndexData[fontId];
console.log(chalk.magenta(`\n🔤 [${i + 1}/${fontIds.length}] Traitement de "${fontData.family}" (${fontId})`));
// Démarrer la barre de progression
progressBar.start(2, 0, { fontName: fontData.family });
const result = await processFontFamily(fontId, fontData, i, fontIds.length);
progressBar.update(2, { fontName: fontData.family });
progressBar.stop();
results.push(result);
if (result.success) {
successCount++;
console.log(chalk.green(`✅ SVGs générés pour "${fontData.family}"`));
console.log(chalk.blue(` - Lettre A: ${result.letterDimensions.width}x${result.letterDimensions.height}`));
console.log(chalk.blue(` - Phrase: ${result.sentenceDimensions.width}x${result.sentenceDimensions.height} (largeur mesurée: ${result.sentenceDimensions.measuredWidth.toFixed(1)}px)`));
} else {
errorCount++;
console.log(chalk.red(`❌ Error for "${fontData.family}": ${result.error}`));
}
// Afficher le progrès global
const progress = ((i + 1) / fontIds.length) * 100;
console.log(chalk.blue(`📈 Progrès global : ${i + 1}/${fontIds.length} familles (${progress.toFixed(1)}%)`));
console.log(chalk.blue(`📊 Success: ${successCount}, Erreurs: ${errorCount}\n`));
}
// Créer le manifest des résultats
const manifest = {};
const successfulResults = results.filter(r => r.success);
for (const result of successfulResults) {
manifest[result.fontFamily] = {
id: result.fontId,
family: 'sans-serif', // Par défaut
images: {
A: `svgs/${result.fontId}_a.svg`,
sentence: `svgs/${result.fontId}_sentence.svg`
},
svg: {
A: {
path: `svgs/${result.fontId}_a.svg`,
width: result.letterDimensions.width,
height: result.letterDimensions.height,
viewBox: `0 0 ${result.letterDimensions.width} ${result.letterDimensions.height}`
},
sentence: {
path: `svgs/${result.fontId}_sentence.svg`,
width: result.sentenceDimensions.width,
height: result.sentenceDimensions.height,
viewBox: `0 0 ${result.sentenceDimensions.width} ${result.sentenceDimensions.height}`,
measuredWidth: result.sentenceDimensions.measuredWidth
}
},
fontMetrics: result.fontMetrics,
sentenceFontMetrics: result.sentenceFontMetrics
};
}
// Sauvegarder le manifest
const manifestPath = path.join(__dirname, 'output', 'font_manifest.json');
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
console.log(chalk.green.bold('🎉 Génération des SVGs terminée !'));
console.log(chalk.cyan('📊 Summary :'));
console.log(chalk.white(` - ${successCount} familles traitées avec succès`));
console.log(chalk.white(` - ${errorCount} erreurs`));
console.log(chalk.white(` - ${successCount * 2} fichiers SVG générés`));
console.log(chalk.white(` - Dossier de sortie : ${SVGS_DIR}`));
console.log(chalk.white(` - Manifest : ${manifestPath}`));
if (errorCount > 0) {
console.log(chalk.red('\n❌ Polices avec erreurs :'));
results
.filter(r => !r.success)
.forEach(r => console.log(chalk.red(` - ${r.fontFamily}: ${r.error}`)));
}
} catch (error) {
console.error(chalk.red('❌ Erreur :'), error.message);
process.exit(1);
}
}
// Lancer le script
main();