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(` `) } 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 = `\n\n \n${paths.join('\n')}\n \n` 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, }