update at 2026-02-08 18:28:39

This commit is contained in:
douboer
2026-02-08 18:28:39 +08:00
parent e2a46e413a
commit 0f5a7f0d85
97 changed files with 22029 additions and 59 deletions

View File

@@ -0,0 +1,188 @@
const { wrapTextByChars } = require('./text-layout')
function formatNumber(value) {
const text = Number(value).toFixed(2).replace(/\.?0+$/, '')
return text || '0'
}
function getGlyphPath(glyph) {
const path = glyph && glyph.path
if (!path || !Array.isArray(path.commands) || !path.commands.length) {
return ''
}
const commands = []
for (const cmd of path.commands) {
switch (cmd.type) {
case 'M':
commands.push(`M${formatNumber(cmd.x)} ${formatNumber(cmd.y)}`)
break
case 'L':
commands.push(`L${formatNumber(cmd.x)} ${formatNumber(cmd.y)}`)
break
case 'Q':
commands.push(`Q${formatNumber(cmd.x1)} ${formatNumber(cmd.y1)} ${formatNumber(cmd.x)} ${formatNumber(cmd.y)}`)
break
case 'C':
commands.push(`C${formatNumber(cmd.x1)} ${formatNumber(cmd.y1)} ${formatNumber(cmd.x2)} ${formatNumber(cmd.y2)} ${formatNumber(cmd.x)} ${formatNumber(cmd.y)}`)
break
case 'Z':
commands.push('Z')
break
default:
break
}
}
return commands.join(' ')
}
function getGlyphBounds(glyph) {
const path = glyph && glyph.path
if (!path || !Array.isArray(path.commands) || !path.commands.length) {
return null
}
let xMin = Infinity
let yMin = Infinity
let xMax = -Infinity
let yMax = -Infinity
for (const cmd of path.commands) {
if (typeof cmd.x === 'number') {
xMin = Math.min(xMin, cmd.x)
xMax = Math.max(xMax, cmd.x)
yMin = Math.min(yMin, cmd.y)
yMax = Math.max(yMax, cmd.y)
}
if (typeof cmd.x1 === 'number') {
xMin = Math.min(xMin, cmd.x1)
xMax = Math.max(xMax, cmd.x1)
yMin = Math.min(yMin, cmd.y1)
yMax = Math.max(yMax, cmd.y1)
}
if (typeof cmd.x2 === 'number') {
xMin = Math.min(xMin, cmd.x2)
xMax = Math.max(xMax, cmd.x2)
yMin = Math.min(yMin, cmd.y2)
yMax = Math.max(yMax, cmd.y2)
}
}
if (xMin === Infinity) {
return null
}
return { xMin, yMin, xMax, yMax }
}
function generateSvgFromFont(options) {
const {
text,
font,
fontSize = 100,
fillColor = '#000000',
letterSpacing = 0,
maxCharsPerLine,
} = options
if (!text || !String(text).trim()) {
throw new Error('文本内容不能为空')
}
const normalizedText = wrapTextByChars(text, maxCharsPerLine)
const scale = fontSize / font.unitsPerEm
const letterSpacingRaw = letterSpacing * font.unitsPerEm
const glyphRuns = []
let minX = null
let minY = null
let maxX = null
let maxY = null
let maxLineAdvance = 0
const ascender = Number.isFinite(font.ascender) ? font.ascender : font.unitsPerEm * 0.8
const descender = Number.isFinite(font.descender) ? font.descender : -font.unitsPerEm * 0.2
const lineAdvance = Math.max(font.unitsPerEm * 1.2, ascender - descender)
const lines = normalizedText.split('\n')
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
const line = lines[lineIndex] || ''
const yPos = -lineIndex * lineAdvance
let xPos = 0
for (const char of Array.from(line)) {
const glyph = font.charToGlyph(char)
glyphRuns.push({ glyph, xPos, yPos })
const bounds = getGlyphBounds(glyph)
if (bounds) {
const adjustedXMin = bounds.xMin + xPos
const adjustedYMin = bounds.yMin + yPos
const adjustedXMax = bounds.xMax + xPos
const adjustedYMax = bounds.yMax + yPos
minX = minX === null ? adjustedXMin : Math.min(minX, adjustedXMin)
minY = minY === null ? adjustedYMin : Math.min(minY, adjustedYMin)
maxX = maxX === null ? adjustedXMax : Math.max(maxX, adjustedXMax)
maxY = maxY === null ? adjustedYMax : Math.max(maxY, adjustedYMax)
}
xPos += (glyph.advanceWidth || 0) + letterSpacingRaw
}
maxLineAdvance = Math.max(maxLineAdvance, xPos)
}
if (minX === null || maxX === null) {
minX = 0
maxX = maxLineAdvance
}
if (minX === null || minY === null || maxX === null || maxY === null) {
throw new Error('未生成有效字形轮廓')
}
const width = (maxX - minX) * scale
const height = (maxY - minY) * scale
if (width <= 0 || height <= 0) {
throw new Error('计算得到的 SVG 尺寸无效')
}
const paths = []
for (const run of glyphRuns) {
const d = getGlyphPath(run.glyph)
if (!d) {
continue
}
const transform = `translate(${formatNumber(run.xPos)} ${formatNumber(run.yPos)})`
paths.push(` <path d=\"${d}\" transform=\"${transform}\"/>`)
}
if (!paths.length) {
throw new Error('未生成任何路径')
}
const viewBox = `${formatNumber(minX)} 0 ${formatNumber(maxX - minX)} ${formatNumber(maxY - minY)}`
const groupTransform = `translate(0 ${formatNumber(maxY)}) scale(1 -1)`
const svg = `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"${viewBox}\" width=\"${formatNumber(width)}\" height=\"${formatNumber(height)}\" preserveAspectRatio=\"xMidYMid meet\">\n <g transform=\"${groupTransform}\" fill=\"${fillColor}\" stroke=\"none\">\n${paths.join('\n')}\n </g>\n</svg>`
const fontName =
(font.names && font.names.fontFamily && (font.names.fontFamily.en || font.names.fontFamily.zh)) ||
(font.names && font.names.fullName && (font.names.fullName.en || font.names.fullName.zh)) ||
'Unknown'
return {
svg,
width,
height,
fontName,
}
}
module.exports = {
generateSvgFromFont,
}