minify-font
Version:
A lightweight font subsetting and conversion tool | 轻量级字体裁剪和转换工具
286 lines (248 loc) • 9.35 kB
JavaScript
import { minifyFont } from '../src/minify-font.mjs'
import { extname, dirname } from 'node:path'
import { mkdir } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { realpathSync } from 'node:fs'
import { TOP_USED_500_CHARS, TOP_USED_2500_CHARS, COMMONLY_USED_CHARS } from 'top-used-chars'
// Character collection mapping
const COLLECTIONS = {
top500: TOP_USED_500_CHARS,
top2500: TOP_USED_2500_CHARS,
commonlyUsed: COMMONLY_USED_CHARS,
}
async function runCLI() {
const args = process.argv.slice(2)
if (args.length === 0) {
console.log('Usage: minify-font <input-font-path> [options]')
console.log('')
console.log('Options:')
console.log(' -c, --collection <name> Use predefined character collection:')
console.log(' top500, top2500 (default), commonlyUsed')
console.log(' -w, --words <words> Custom characters to include (can combine with -c)')
console.log(
' -o, --output <output> Output file or directory (default: same as input with .min suffix)'
)
console.log(' -f, --formats <formats> Comma-separated formats to generate (default: ttf,woff,woff2)')
console.log(' --input-options <json> Input font options as JSON string')
console.log(' --output-options <json> Output font options as JSON string')
console.log(' -h, --help Show this help message')
console.log(' -v, --version Show version')
console.log('')
console.log('Examples:')
console.log(' minify-font font.ttf')
console.log(' minify-font font.ttf -c top500')
console.log(' minify-font font.ttf -c commonlyUsed -o output/')
console.log(' minify-font font.ttf -w "Hello World"')
console.log(' minify-font font.ttf -c top1000 -w "额外字符" -o minified.woff2')
console.log(' minify-font font.ttf -f woff2')
console.log(' minify-font font.ttf -f ttf,woff -o dist/')
process.exit(0)
}
let input = null
let words = null
let collection = 'top2500' // default collection
let output = null
let formats = null
let inputOptions = {}
let outputOptions = {}
// Simple argument parsing
for (let i = 0; i < args.length; i++) {
const arg = args[i]
switch (arg) {
case '-h':
case '--help':
console.log('Usage: minify-font <input-font-path> [options]')
console.log('')
console.log('Options:')
console.log(' -c, --collection <name> Use predefined character collection:')
console.log(' top500, top2500 (default), commonlyUsed')
console.log(' -w, --words <words> Custom characters to include (can combine with -c)')
console.log(
' -o, --output <output> Output file or directory (default: same as input with .min suffix)'
)
console.log(
' -f, --formats <formats> Comma-separated formats to generate (default: ttf,woff,woff2)'
)
console.log(' --input-options <json> Input font options as JSON string')
console.log(' --output-options <json> Output font options as JSON string')
console.log(' -h, --help Show this help message')
console.log(' -v, --version Show version')
console.log('')
console.log('Examples:')
console.log(' minify-font font.ttf')
console.log(' minify-font font.ttf -c top500')
console.log(' minify-font font.ttf -c commonlyUsed -o output/')
console.log(' minify-font font.ttf -w "Hello World"')
console.log(' minify-font font.ttf -c top1000 -w "额外字符" -o minified.woff2')
console.log(' minify-font font.ttf -f woff2')
console.log(' minify-font font.ttf -f ttf,woff -o dist/')
process.exit(0)
case '-v':
case '--version':
console.log('minify-font version 1.0.0')
process.exit(0)
case '-c':
case '--collection':
collection = args[++i]
if (!COLLECTIONS[collection]) {
console.error(
`Error: Invalid collection "${collection}". Available: ${Object.keys(COLLECTIONS).join(', ')}`
)
process.exit(1)
}
break
case '-w':
case '--words':
words = args[++i]
break
case '-o':
case '--output':
output = args[++i]
break
case '-f':
case '--formats':
formats = args[++i]
break
case '--input-options':
try {
inputOptions = JSON.parse(args[++i])
} catch (e) {
console.error('Invalid JSON for --input-options:', e.message)
process.exit(1)
}
break
case '--output-options':
try {
outputOptions = JSON.parse(args[++i])
} catch (e) {
console.error('Invalid JSON for --output-options:', e.message)
process.exit(1)
}
break
default:
if (arg.startsWith('-')) {
console.error(`Unknown option: ${arg}`)
process.exit(1)
}
if (!input) {
input = arg
} else {
console.error('Multiple input files not supported')
process.exit(1)
}
break
}
}
if (!input) {
console.error('Error: Input font path is required')
process.exit(1)
}
try {
console.log(`\nProcessing: ${input}`)
// Get characters to include
let text = COLLECTIONS[collection] || TOP_USED_2500_CHARS
console.log(`Using character collection: ${collection} (${text.length} chars)`)
// If custom words provided, combine with collection
if (words) {
// Combine and deduplicate characters
const combinedChars = new Set([...text, ...words])
text = Array.from(combinedChars).join('')
console.log(`Added custom words, total: ${text.length} unique chars`)
}
// Determine formats to generate
let outputFormats = []
const hasOutputExtension = output && extname(output)
if (formats) {
// User specified formats explicitly
outputFormats = formats.split(',').map(f => f.trim())
} else if (hasOutputExtension) {
// User specified a specific output file with extension
outputFormats = [extname(output).slice(1)]
} else {
// Default: generate ttf, woff, woff2
outputFormats = ['ttf', 'woff', 'woff2']
}
// Generate output paths for each format
const outputPaths = []
const inputBaseName = input.replace(/\.[^/.]+$/, '')
const inputFileName = input.replace(/^.*[\\/]/, '').replace(/\.[^/.]+$/, '')
for (const format of outputFormats) {
let outputPath
if (hasOutputExtension) {
// User specified a specific file, replace extension with current format
outputPath = output.replace(/\.[^/.]+$/, `.${format}`)
} else if (output) {
// Output is a directory
outputPath = `${output}/${inputFileName}.min.${format}`
} else {
// No output specified, use input location with .min suffix
outputPath = `${inputBaseName}.min.${format}`
}
outputPaths.push(outputPath)
}
// Ensure output directory exists for all files
for (const outputPath of outputPaths) {
const outputDir = dirname(outputPath)
await mkdir(outputDir, { recursive: true })
}
// Generate all formats
console.log(`\nGenerating ${outputFormats.length} format(s): ${outputFormats.join(', ')}`)
const generatedFiles = []
for (let i = 0; i < outputFormats.length; i++) {
const format = outputFormats[i]
const outputPath = outputPaths[i]
process.stdout.write(` ${format}... `)
await minifyFont({
input,
output: outputPath,
text,
inputOptions,
outputOptions,
})
console.log('✓')
generatedFiles.push(outputPath)
}
// Output all generated file paths
console.log('\n✓ Generated successfully:')
generatedFiles.forEach(file => console.log(` ${file}`))
// Generate @font-face CSS with correct syntax
const formatMap = {
woff2: 'woff2',
woff: 'woff',
ttf: 'truetype',
}
// Sort files by format preference (woff2 > woff > ttf)
const sortedFiles = [...generatedFiles].sort((a, b) => {
const order = { woff2: 0, woff: 1, ttf: 2 }
const extA = a.split('.').pop()
const extB = b.split('.').pop()
return (order[extA] || 3) - (order[extB] || 3)
})
console.log(`\nCSS @font-face:`)
console.log(`@font-face {
font-family: '${inputFileName}';
src: ${sortedFiles
.map(file => {
const ext = file.split('.').pop()
return `url(${file}) format('${formatMap[ext] || ext}')`
})
.join(',\n ')};
}`)
} catch (error) {
console.error('\n✗ Error minifying font:', error.message)
if (error.stack) {
console.error(error.stack)
}
process.exit(1)
}
}
// Export for testing
export { runCLI }
// Auto-run CLI when executed directly
// Resolve symlinks (important for global npm installs where argv[1] is a symlink)
const __filename = fileURLToPath(import.meta.url)
const executedPath = process.argv[1] ? realpathSync(process.argv[1]) : null
if (executedPath === __filename) {
runCLI()
}