|
|
#!/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); |
|
|
|
|
|
|
|
|
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'); |
|
|
|
|
|
|
|
|
const progressBar = new cliProgress.SingleBar({ |
|
|
format: chalk.cyan('{bar}') + ' | {percentage}% | {value}/{total} | {fontName}', |
|
|
barCompleteChar: '\u2588', |
|
|
barIncompleteChar: '\u2591', |
|
|
hideCursor: true |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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: [] }; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
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'); |
|
|
} |
|
|
|
|
|
|
|
|
const margin = padding; |
|
|
const svgWidth = Math.ceil(textWidth) + (margin * 2); |
|
|
const svgHeight = Math.ceil(textHeight) + (margin * 2); |
|
|
|
|
|
|
|
|
const offsetX = margin - bbox.x1; |
|
|
const offsetY = margin - bbox.y1; |
|
|
|
|
|
|
|
|
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>`; |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function processFontFamily(fontId, fontData, currentIndex, totalFamilies) { |
|
|
const fontDir = path.join(FONTS_DIR, fontId); |
|
|
|
|
|
try { |
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
const letterResult = await generateLetterASVG(fontPath, fontData.family); |
|
|
if (!letterResult) { |
|
|
throw new Error('Failed de génération du SVG de la lettre A'); |
|
|
} |
|
|
|
|
|
|
|
|
const letterSvgPath = path.join(SVGS_DIR, `${fontId}_a.svg`); |
|
|
await fs.writeFile(letterSvgPath, letterResult.svg, 'utf-8'); |
|
|
|
|
|
|
|
|
const sentenceResult = await generateSentenceSVG(fontPath, fontData.family); |
|
|
if (!sentenceResult) { |
|
|
throw new Error('Failed de génération du 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 |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function main() { |
|
|
try { |
|
|
console.log(chalk.blue.bold('🎨 Generating SVGs pour toutes les polices...\n')); |
|
|
|
|
|
|
|
|
try { |
|
|
await fs.access(FONT_INDEX_PATH); |
|
|
} catch { |
|
|
throw new Error(`Fichier d'index non trouvé : ${FONT_INDEX_PATH}`); |
|
|
} |
|
|
|
|
|
|
|
|
await fs.mkdir(SVGS_DIR, { recursive: true }); |
|
|
console.log(chalk.green(`📁 Directory created : ${SVGS_DIR}`)); |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
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})`)); |
|
|
|
|
|
|
|
|
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}`)); |
|
|
} |
|
|
|
|
|
|
|
|
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`)); |
|
|
} |
|
|
|
|
|
|
|
|
const manifest = {}; |
|
|
const successfulResults = results.filter(r => r.success); |
|
|
|
|
|
for (const result of successfulResults) { |
|
|
manifest[result.fontFamily] = { |
|
|
id: result.fontId, |
|
|
family: 'sans-serif', |
|
|
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 |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
main(); |
|
|
|