Enhance SVG processing in build-outline.mjs by implementing parallel processing with concurrency limits.

This commit is contained in:
codecalm 2025-12-14 19:36:01 +01:00
parent 670958d52c
commit 40b0b16605
1 changed files with 89 additions and 70 deletions

View File

@ -6,9 +6,26 @@ import crypto from 'crypto'
import { glob } from 'glob' import { glob } from 'glob'
import { optimize } from 'svgo' import { optimize } from 'svgo'
import { fixOutline } from './fix-outline.mjs' import { fixOutline } from './fix-outline.mjs'
import os from 'os'
const DIR = getPackageDir('icons-webfont') const DIR = getPackageDir('icons-webfont')
// Parallel processing with concurrency limit
const parallelLimit = async (items, fn, concurrency = os.cpus().length * 2) => {
const results = []
let index = 0
const worker = async () => {
while (index < items.length) {
const i = index++
results[i] = await fn(items[i], i)
}
}
await Promise.all(Array(Math.min(concurrency, items.length)).fill(null).map(worker))
return results
}
const strokes = { const strokes = {
200: 1, 200: 1,
300: 1.5, 300: 1.5,
@ -24,79 +41,81 @@ const buildOutline = async () => {
for (const strokeName in strokes) { for (const strokeName in strokes) {
const stroke = strokes[strokeName] const stroke = strokes[strokeName]
await asyncForEach(Object.entries(icons), async ([type, icons]) => { for (const [type, typeIcons] of Object.entries(icons)) {
fs.mkdirSync(resolve(DIR, `icons-outlined/${strokeName}/${type}`), { recursive: true }) fs.mkdirSync(resolve(DIR, `icons-outlined/${strokeName}/${type}`), { recursive: true })
filesList[type] = []
// Filter icons first
await asyncForEach(icons, async function ({ name, content, unicode }) { const iconsToProcess = typeIcons.filter(({ name, unicode }) => {
if (compileOptions.includeIcons.length === 0 || compileOptions.includeIcons.indexOf(name) >= 0) { if (!unicode) return false
if (compileOptions.includeIcons.length > 0 && compileOptions.includeIcons.indexOf(name) < 0) return false
if (unicode) { return true
console.log(`Stroke ${strokeName} for:`, name, unicode) })
let filename = `${name}.svg` // Collect filenames for later cleanup
if (unicode) { filesList[type] = iconsToProcess.map(({ name, unicode }) => `u${unicode.toUpperCase()}-${name}.svg`)
filename = `u${unicode.toUpperCase()}-${name}.svg`
} // Process icons in parallel with concurrency limit
let processed = 0
filesList[type].push(filename) const total = iconsToProcess.length
content = content await parallelLimit(iconsToProcess, async ({ name, content, unicode }) => {
.replace('width="24"', 'width="1000"') const filename = `u${unicode.toUpperCase()}-${name}.svg`
.replace('height="24"', 'height="1000"') const filePath = resolve(DIR, `icons-outlined/${strokeName}/${type}/${filename}`)
content = content // Check cache
.replace('stroke-width="2"', `stroke-width="${stroke}"`) if (fs.existsSync(filePath)) {
let cachedContent = fs.readFileSync(filePath, 'utf-8')
const cachedFilename = `u${unicode.toUpperCase()}-${name}.svg`; let cachedHash = ''
cachedContent = cachedContent.replace(/<!--\!cache:([a-z0-9]+)-->/, (m, hash) => {
if (unicode && fs.existsSync(resolve(DIR, `icons-outlined/${strokeName}/${type}/${cachedFilename}`))) { cachedHash = hash
// Get content return ''
let cachedContent = fs.readFileSync(resolve(DIR, `icons-outlined/${strokeName}/${type}/${cachedFilename}`), 'utf-8') })
// Get hash if (crypto.createHash('sha1').update(cachedContent).digest("hex") === cachedHash) {
let cachedHash = ''; processed++
cachedContent = cachedContent.replace(/<!--\!cache:([a-z0-9]+)-->/, function (m, hash) { process.stdout.write(`\rStroke ${strokeName}/${type}: ${processed}/${total} (cached: ${name})`.padEnd(80))
cachedHash = hash; return
return '';
})
// Check hash
if (crypto.createHash('sha1').update(cachedContent).digest("hex") === cachedHash) {
console.log('Cached stroke for:', name, unicode)
return true;
}
}
await outlineStroke(content, {
optCurve: true,
steps: 4,
round: 0,
centerHorizontally: true,
fixedWidth: false,
color: 'black'
}).then(outlined => {
// Fix outline direction (using JS instead of fontforge)
const fixed = fixOutline(outlined)
// Optimize with svgo (in memory, no subprocess)
const optimized = optimize(fixed, { multipass: true }).data
// Prepare final content with hash
const finalContent = optimized.replace(/\n/g, ' ').trim()
const hashString = `<!--!cache:${crypto.createHash('sha1').update(finalContent).digest("hex")}-->`
// Save file (single write instead of 3 file operations)
fs.writeFileSync(
resolve(DIR, `icons-outlined/${strokeName}/${type}/${filename}`),
finalContent + hashString,
'utf-8'
)
}).catch(error => console.log(error))
} }
} }
})
}) // Prepare content
content = content
.replace('width="24"', 'width="1000"')
.replace('height="24"', 'height="1000"')
.replace('stroke-width="2"', `stroke-width="${stroke}"`)
try {
const outlined = await outlineStroke(content, {
optCurve: true,
steps: 4,
round: 0,
centerHorizontally: true,
fixedWidth: false,
color: 'black'
})
// Fix outline direction (using JS instead of fontforge)
const fixed = fixOutline(outlined)
// Optimize with svgo (in memory, no subprocess)
const optimized = optimize(fixed, { multipass: true }).data
// Prepare final content with hash
const finalContent = optimized.replace(/\n/g, ' ').trim()
const hashString = `<!--!cache:${crypto.createHash('sha1').update(finalContent).digest("hex")}-->`
// Save file
fs.writeFileSync(filePath, finalContent + hashString, 'utf-8')
processed++
process.stdout.write(`\rStroke ${strokeName}/${type}: ${processed}/${total} (${name})`.padEnd(80))
} catch (error) {
console.error(`\nError processing ${name}:`, error.message)
}
}, 32) // 32 concurrent tasks
console.log() // New line after progress
}
// Remove old files // Remove old files
await asyncForEach(Object.entries(icons), async ([type, icons]) => { await asyncForEach(Object.entries(icons), async ([type, icons]) => {