|
|
#!/usr/bin/env node |
|
|
|
|
|
import fs from 'fs/promises'; |
|
|
import path from 'path'; |
|
|
import { fileURLToPath } from 'url'; |
|
|
import sharp from 'sharp'; |
|
|
import cliProgress from 'cli-progress'; |
|
|
import chalk from 'chalk'; |
|
|
|
|
|
const __filename = fileURLToPath(import.meta.url); |
|
|
const __dirname = path.dirname(__filename); |
|
|
|
|
|
|
|
|
const SVGS_DIR = path.join(__dirname, 'output', 'svgs'); |
|
|
const PNGS_DIR = path.join(__dirname, 'output', 'pngs'); |
|
|
const PNG_SIZE = 40; |
|
|
|
|
|
|
|
|
const progressBar = new cliProgress.SingleBar({ |
|
|
format: chalk.cyan('{bar}') + ' | {percentage}% | {value}/{total} | {fontName}', |
|
|
barCompleteChar: '\u2588', |
|
|
barIncompleteChar: '\u2591', |
|
|
hideCursor: true |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function analyzePNGQuality(pngPath) { |
|
|
try { |
|
|
const image = sharp(pngPath); |
|
|
const { data, info } = await image.raw().toBuffer({ resolveWithObject: true }); |
|
|
|
|
|
const { width, height, channels } = info; |
|
|
const totalPixels = width * height; |
|
|
|
|
|
let blackPixels = 0; |
|
|
let whitePixels = 0; |
|
|
let grayPixels = 0; |
|
|
let transparentPixels = 0; |
|
|
|
|
|
|
|
|
for (let i = 0; i < data.length; i += channels) { |
|
|
const r = data[i]; |
|
|
const g = data[i + 1]; |
|
|
const b = data[i + 2]; |
|
|
const a = channels === 4 ? data[i + 3] : 255; |
|
|
|
|
|
|
|
|
if (a < 128) { |
|
|
transparentPixels++; |
|
|
} |
|
|
|
|
|
else if (r < 50 && g < 50 && b < 50) { |
|
|
blackPixels++; |
|
|
} |
|
|
|
|
|
else if (r > 200 && g > 200 && b > 200) { |
|
|
whitePixels++; |
|
|
} |
|
|
|
|
|
else { |
|
|
grayPixels++; |
|
|
} |
|
|
} |
|
|
|
|
|
const blackPercentage = (blackPixels / totalPixels) * 100; |
|
|
const whitePercentage = (whitePixels / totalPixels) * 100; |
|
|
const grayPercentage = (grayPixels / totalPixels) * 100; |
|
|
const transparentPercentage = (transparentPixels / totalPixels) * 100; |
|
|
|
|
|
return { |
|
|
totalPixels, |
|
|
blackPixels, |
|
|
whitePixels, |
|
|
grayPixels, |
|
|
transparentPixels, |
|
|
blackPercentage, |
|
|
whitePercentage, |
|
|
grayPercentage, |
|
|
transparentPercentage, |
|
|
hasContent: blackPixels > 0, |
|
|
isMostlyWhite: whitePercentage > 90, |
|
|
hasTransparency: transparentPixels > 0 |
|
|
}; |
|
|
|
|
|
} catch (error) { |
|
|
console.error(`❌ Error during l'analyse de la qualité PNG:`, error.message); |
|
|
return null; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function validatePNGQuality(analysis, fontFamily) { |
|
|
const issues = []; |
|
|
|
|
|
if (!analysis) { |
|
|
issues.push('Impossible d\'analyser le PNG'); |
|
|
return { valid: false, issues }; |
|
|
} |
|
|
|
|
|
|
|
|
if (!analysis.hasContent) { |
|
|
issues.push('Aucun pixel noir trouvé (police invisible)'); |
|
|
return { valid: false, issues }; |
|
|
} |
|
|
|
|
|
|
|
|
if (analysis.isMostlyWhite && analysis.blackPercentage < 1) { |
|
|
issues.push('PNG presque entièrement blanc (police trop petite)'); |
|
|
return { valid: false, issues }; |
|
|
} |
|
|
|
|
|
|
|
|
if (analysis.transparentPercentage > 10) { |
|
|
issues.push('Trop de transparence détectée'); |
|
|
return { valid: false, issues }; |
|
|
} |
|
|
|
|
|
|
|
|
if (analysis.blackPercentage < 0.1) { |
|
|
issues.push('Contenu noir insuffisant (police trop fine)'); |
|
|
return { valid: false, issues }; |
|
|
} |
|
|
|
|
|
|
|
|
if (analysis.blackPercentage > 50) { |
|
|
issues.push('Trop de contenu noir (possiblement corrompu)'); |
|
|
return { valid: false, issues }; |
|
|
} |
|
|
|
|
|
return { valid: true, issues: [] }; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function convertSvgToPng(svgPath, pngPath, fontFamily) { |
|
|
try { |
|
|
const svgBuffer = await fs.readFile(svgPath); |
|
|
|
|
|
await sharp(svgBuffer) |
|
|
.resize(PNG_SIZE, PNG_SIZE, { |
|
|
fit: 'contain', |
|
|
background: { r: 255, g: 255, b: 255, alpha: 1 } |
|
|
}) |
|
|
.flatten({ background: { r: 255, g: 255, b: 255 } }) |
|
|
.png() |
|
|
.toFile(pngPath); |
|
|
|
|
|
|
|
|
const analysis = await analyzePNGQuality(pngPath); |
|
|
const validation = validatePNGQuality(analysis, fontFamily); |
|
|
|
|
|
if (!validation.valid) { |
|
|
console.warn(`⚠️ PNG de mauvaise qualité pour ${fontFamily}: ${validation.issues.join(', ')}`); |
|
|
return { success: false, analysis, issues: validation.issues }; |
|
|
} |
|
|
|
|
|
return { success: true, analysis, issues: [] }; |
|
|
|
|
|
} catch (error) { |
|
|
console.error(`❌ Error during la conversion ${svgPath}:`, error.message); |
|
|
return { success: false, analysis: null, issues: [error.message] }; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function processLetterASVG(svgFile, currentIndex, totalFiles) { |
|
|
const svgPath = path.join(SVGS_DIR, svgFile); |
|
|
|
|
|
|
|
|
const pngFile = svgFile.replace('.svg', '.png'); |
|
|
const pngPath = path.join(PNGS_DIR, pngFile); |
|
|
|
|
|
|
|
|
const fontId = svgFile.replace('_a.svg', ''); |
|
|
const fontFamily = fontId.replace(/-/g, ' '); |
|
|
|
|
|
try { |
|
|
const result = await convertSvgToPng(svgPath, pngPath, fontFamily); |
|
|
|
|
|
if (result.success) { |
|
|
return { |
|
|
success: true, |
|
|
fontFamily, |
|
|
fontId, |
|
|
pngPath: pngPath, |
|
|
dimensions: { |
|
|
width: PNG_SIZE, |
|
|
height: PNG_SIZE |
|
|
}, |
|
|
quality: result.analysis |
|
|
}; |
|
|
} else { |
|
|
return { |
|
|
success: false, |
|
|
fontFamily, |
|
|
fontId, |
|
|
error: `Qualité insuffisante: ${result.issues.join(', ')}`, |
|
|
quality: result.analysis |
|
|
}; |
|
|
} |
|
|
|
|
|
} catch (error) { |
|
|
return { |
|
|
success: false, |
|
|
fontFamily, |
|
|
fontId, |
|
|
error: error.message |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function main() { |
|
|
try { |
|
|
console.log(chalk.blue.bold(`🖼️ Génération des PNG ${PNG_SIZE}x${PNG_SIZE} pour toutes les lettres A...\n`)); |
|
|
|
|
|
|
|
|
await fs.mkdir(PNGS_DIR, { recursive: true }); |
|
|
console.log(chalk.green(`📁 Directory created : ${PNGS_DIR}`)); |
|
|
|
|
|
|
|
|
const svgFiles = await fs.readdir(SVGS_DIR); |
|
|
const letterASvgFiles = svgFiles.filter(file => file.endsWith('_a.svg')); |
|
|
|
|
|
if (letterASvgFiles.length === 0) { |
|
|
throw new Error('Aucun fichier SVG de lettre A trouvé dans ' + SVGS_DIR); |
|
|
} |
|
|
|
|
|
console.log(chalk.cyan(`📁 ${letterASvgFiles.length} fichiers SVG de lettre A trouvés\n`)); |
|
|
|
|
|
const results = []; |
|
|
let successCount = 0; |
|
|
let errorCount = 0; |
|
|
|
|
|
|
|
|
for (let i = 0; i < letterASvgFiles.length; i++) { |
|
|
const svgFile = letterASvgFiles[i]; |
|
|
|
|
|
console.log(chalk.magenta(`\n🔤 [${i + 1}/${letterASvgFiles.length}] Traitement de "${svgFile}"`)); |
|
|
|
|
|
|
|
|
progressBar.start(1, 0, { fontName: svgFile }); |
|
|
|
|
|
const result = await processLetterASVG(svgFile, i, letterASvgFiles.length); |
|
|
|
|
|
progressBar.update(1, { fontName: svgFile }); |
|
|
progressBar.stop(); |
|
|
|
|
|
results.push(result); |
|
|
|
|
|
if (result.success) { |
|
|
successCount++; |
|
|
console.log(chalk.green(`✅ PNG généré : ${result.fontFamily} (${result.dimensions.width}x${result.dimensions.height}) - ${result.quality.blackPercentage.toFixed(1)}% noir`)); |
|
|
} else { |
|
|
errorCount++; |
|
|
console.log(chalk.red(`❌ PNG rejeté : ${result.fontFamily} - ${result.error}`)); |
|
|
} |
|
|
|
|
|
|
|
|
const progress = ((i + 1) / letterASvgFiles.length) * 100; |
|
|
console.log(chalk.blue(`📈 Progrès global : ${i + 1}/${letterASvgFiles.length} fichiers (${progress.toFixed(1)}%)`)); |
|
|
console.log(chalk.blue(`📊 Success: ${successCount}, Erreurs: ${errorCount}\n`)); |
|
|
} |
|
|
|
|
|
console.log(chalk.green.bold('🎉 Génération des PNG terminée !')); |
|
|
console.log(chalk.cyan('📊 Summary :')); |
|
|
console.log(chalk.white(` - ${successCount} PNG générés avec succès`)); |
|
|
console.log(chalk.white(` - ${errorCount} erreurs`)); |
|
|
console.log(chalk.white(` - ${letterASvgFiles.length} fichiers traités`)); |
|
|
console.log(chalk.white(` - Dossier de sortie : ${PNGS_DIR}`)); |
|
|
|
|
|
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(); |
|
|
|