|
|
#!/usr/bin/env node |
|
|
|
|
|
import fs from 'fs/promises'; |
|
|
import path from 'path'; |
|
|
import { fileURLToPath } from 'url'; |
|
|
import opentype from 'opentype.js'; |
|
|
|
|
|
const __filename = fileURLToPath(import.meta.url); |
|
|
const __dirname = path.dirname(__filename); |
|
|
|
|
|
|
|
|
const TYPOGRAPHY_BASE = __dirname; |
|
|
const GENERATED_DIR = path.join(TYPOGRAPHY_BASE, 'generated'); |
|
|
const FONTS_DIR = path.join(GENERATED_DIR, 'fonts'); |
|
|
const SVGS_DIR = path.join(GENERATED_DIR, 'svgs'); |
|
|
const FONT_MANIFEST_PATH = path.join(GENERATED_DIR, 'data', 'font_manifest.json'); |
|
|
const TYPOGRAPHY_DATA_PATH = path.join(GENERATED_DIR, 'data', 'typography_data.json'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function updateFontManifest(results) { |
|
|
try { |
|
|
console.log('\n📝 Updating font manifest...'); |
|
|
|
|
|
|
|
|
let manifest = {}; |
|
|
try { |
|
|
const manifestData = await fs.readFile(FONT_MANIFEST_PATH, 'utf-8'); |
|
|
manifest = JSON.parse(manifestData); |
|
|
} catch { |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
let typographyData = []; |
|
|
try { |
|
|
const typographyDataContent = await fs.readFile(TYPOGRAPHY_DATA_PATH, 'utf-8'); |
|
|
const data = JSON.parse(typographyDataContent); |
|
|
typographyData = data.fonts || []; |
|
|
} catch { |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const successfulResults = results.filter(r => r.status === 'success'); |
|
|
|
|
|
for (const result of successfulResults) { |
|
|
const { fontFamily, fontId, svgPath, dimensions, fontMetrics } = result; |
|
|
|
|
|
|
|
|
const typographyEntry = typographyData.find(entry => entry.name === fontFamily); |
|
|
const family = typographyEntry?.family || 'sans-serif'; |
|
|
|
|
|
|
|
|
manifest[fontFamily] = { |
|
|
id: fontId, |
|
|
family: family, |
|
|
images: { |
|
|
A: svgPath, |
|
|
a: svgPath |
|
|
}, |
|
|
svg: { |
|
|
A: { |
|
|
path: svgPath, |
|
|
width: dimensions.width, |
|
|
height: dimensions.height, |
|
|
viewBox: `0 0 ${dimensions.width} ${dimensions.height}` |
|
|
} |
|
|
}, |
|
|
fontMetrics: fontMetrics |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
await fs.mkdir(path.dirname(FONT_MANIFEST_PATH), { recursive: true }); |
|
|
|
|
|
|
|
|
await fs.writeFile(FONT_MANIFEST_PATH, JSON.stringify(manifest, null, 2), 'utf-8'); |
|
|
|
|
|
console.log(`✅ Manifest updated with ${successfulResults.length} fonts`); |
|
|
|
|
|
} catch (error) { |
|
|
console.error('❌ Error updating manifest:', error.message); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
|
|
|
const glyphWidth = bbox.x2 - bbox.x1; |
|
|
const glyphHeight = bbox.y2 - bbox.y1; |
|
|
|
|
|
|
|
|
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); |
|
|
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>`; |
|
|
|
|
|
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; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function fontNameToId(fontName) { |
|
|
return fontName |
|
|
.toLowerCase() |
|
|
.replace(/[^a-z0-9]+/g, '_') |
|
|
.replace(/^_|_$/g, ''); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function generateSVGsForAllFonts() { |
|
|
console.log('🎨 Generating SVGs for all downloaded fonts\n'); |
|
|
|
|
|
try { |
|
|
|
|
|
await fs.mkdir(SVGS_DIR, { recursive: true }); |
|
|
|
|
|
|
|
|
const fontFiles = await fs.readdir(FONTS_DIR); |
|
|
const ttfFiles = fontFiles.filter(file => file.endsWith('.ttf')); |
|
|
|
|
|
if (ttfFiles.length === 0) { |
|
|
console.error('❌ No TTF files found in', FONTS_DIR); |
|
|
process.exit(1); |
|
|
} |
|
|
|
|
|
console.log(`📁 Found ${ttfFiles.length} TTF files`); |
|
|
|
|
|
const results = []; |
|
|
|
|
|
for (let i = 0; i < ttfFiles.length; i++) { |
|
|
const ttfFile = ttfFiles[i]; |
|
|
const fontPath = path.join(FONTS_DIR, ttfFile); |
|
|
|
|
|
|
|
|
const fontId = ttfFile.replace('.ttf', ''); |
|
|
const fontFamily = fontId.replace(/_/g, ' '); |
|
|
|
|
|
console.log(`\n[${i + 1}/${ttfFiles.length}] 🔄 Generating SVG for "${fontFamily}"...`); |
|
|
|
|
|
try { |
|
|
|
|
|
const svgResult = await generateLetterASVG(fontPath, fontFamily); |
|
|
|
|
|
if (!svgResult) { |
|
|
results.push({ |
|
|
fontFamily, |
|
|
fontId, |
|
|
status: 'error', |
|
|
error: 'SVG generation failed' |
|
|
}); |
|
|
continue; |
|
|
} |
|
|
|
|
|
|
|
|
const svgPath = path.join(SVGS_DIR, `${fontId}_a.svg`); |
|
|
await fs.writeFile(svgPath, svgResult.svg, 'utf-8'); |
|
|
|
|
|
console.log(`✅ SVG generated: ${fontFamily} (${svgResult.width}x${svgResult.height})`); |
|
|
|
|
|
results.push({ |
|
|
fontFamily, |
|
|
fontId, |
|
|
status: 'success', |
|
|
svgPath: `/content/embeds/typography/font_svgs/${fontId}_a.svg`, |
|
|
dimensions: { |
|
|
width: svgResult.width, |
|
|
height: svgResult.height |
|
|
}, |
|
|
fontMetrics: svgResult.fontMetrics |
|
|
}); |
|
|
|
|
|
} catch (error) { |
|
|
console.error(`❌ Error for ${fontFamily}:`, error.message); |
|
|
results.push({ |
|
|
fontFamily, |
|
|
fontId, |
|
|
status: 'error', |
|
|
error: error.message |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
await updateFontManifest(results); |
|
|
|
|
|
|
|
|
const successful = results.filter(r => r.status === 'success').length; |
|
|
const errors = results.filter(r => r.status === 'error').length; |
|
|
|
|
|
console.log('\n📊 Final statistics:'); |
|
|
console.log(`✅ SVGs generated successfully: ${successful}`); |
|
|
console.log(`❌ Errors: ${errors}`); |
|
|
console.log(`📋 Total processed: ${results.length}`); |
|
|
|
|
|
if (errors > 0) { |
|
|
console.log('\n❌ Fonts with errors:'); |
|
|
results |
|
|
.filter(r => r.status === 'error') |
|
|
.forEach(r => console.log(` - ${r.fontFamily}: ${r.error}`)); |
|
|
} |
|
|
|
|
|
} catch (error) { |
|
|
console.error('💥 Fatal error:', error.message); |
|
|
process.exit(1); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (import.meta.url === `file://${process.argv[1]}`) { |
|
|
generateSVGsForAllFonts(); |
|
|
} |
|
|
|
|
|
export { generateSVGsForAllFonts, generateLetterASVG }; |