const fs = require('fs'); const path = require('path'); const config = require('../config'); // Cache je Fontdatei const FONT_CACHE = {}; // Attribut aus einem Tag lesen (mit Wortgrenze, um z.B. 'd' vs 'id' zu unterscheiden) function getAttr(tag, name) { const re = new RegExp(`\\b${name}="([^"]*)"`); const m = tag.match(re); return m ? m[1] : null; } // SVG-Font parsen und Metriken plus Glyphen extrahieren function parseSvgFont(fontFileName) { if (FONT_CACHE[fontFileName]) return FONT_CACHE[fontFileName]; const fontPath = path.join(config.paths.fonts, fontFileName); const svg = fs.readFileSync(fontPath, 'utf8'); const fontFaceMatch = svg.match(//); if (!fontFaceMatch) { throw new Error(`Kein in ${fontFileName} gefunden`); } const fontFaceTag = fontFaceMatch[0]; const unitsPerEm = parseFloat(getAttr(fontFaceTag, 'units-per-em') || '1000'); const ascent = parseFloat(getAttr(fontFaceTag, 'ascent') || '0'); const descent = parseFloat(getAttr(fontFaceTag, 'descent') || '0'); const glyphRegex = //g; const glyphs = {}; let m; while ((m = glyphRegex.exec(svg)) !== null) { const tag = m[0]; const unicode = getAttr(tag, 'unicode'); const d = getAttr(tag, 'd'); const adv = parseFloat(getAttr(tag, 'horiz-adv-x') || '0'); if (!unicode || !d) continue; glyphs[unicode] = { d, adv, }; } const fontData = { unitsPerEm, ascent, descent, glyphs, }; FONT_CACHE[fontFileName] = fontData; return fontData; } /** * Breite einer Textzeile in Pixeln messen. */ function measureTextWidthPx({ text, fontFileName, fontSizePx }) { const { unitsPerEm, glyphs } = parseSvgFont(fontFileName); const scale = fontSizePx / unitsPerEm; let x = 0; for (const ch of String(text || '')) { const glyph = glyphs[ch]; if (!glyph) { // Fallback für Leerzeichen / unbekannte Zeichen const spacePx = fontSizePx * 0.4; x += spacePx; continue; } const advPx = (glyph.adv || unitsPerEm * 0.5) * scale; x += advPx; } return x; } /** * Text nach verfügbarer Breite umbrechen. * Garantiert: keine Zeile wird breiter als maxWidthPx. */ function wrapTextToLinesByWidth({ text, maxWidthPx, fontFileName, fontSizePx, }) { const paragraphs = String(text || '').split(/\r?\n/); const allLines = []; const measure = (t) => measureTextWidthPx({ text: t, fontFileName, fontSizePx }); for (const para of paragraphs) { const words = para.split(/\s+/).filter(Boolean); if (words.length === 0) { allLines.push(''); continue; } let currentLine = ''; for (const word of words) { // Wort alleine zu breit → Zeichenweise brechen if (measure(word) > maxWidthPx) { if (currentLine) { allLines.push(currentLine); currentLine = ''; } let buf = ''; for (const ch of word) { const candidate = buf + ch; if (measure(candidate) > maxWidthPx && buf) { allLines.push(buf); buf = ch; } else { buf = candidate; } } if (buf) currentLine = buf; continue; } if (!currentLine) { currentLine = word; continue; } const candidate = `${currentLine} ${word}`; if (measure(candidate) <= maxWidthPx) { currentLine = candidate; } else { allLines.push(currentLine); currentLine = word; } } if (currentLine) { allLines.push(currentLine); } } return allLines; } /** * Textzeilen in Pfade mit Transform-Matrix umwandeln. */ function layoutTextToPaths({ lines, fontFileName, fontSizePx, startX, startY, lineHeightPx, }) { const { unitsPerEm, glyphs } = parseSvgFont(fontFileName); const scale = fontSizePx / unitsPerEm; const resultPaths = []; let baselineY = startY; for (const line of lines) { let x = startX; for (let i = 0; i < line.length; i++) { const ch = line[i]; const glyph = glyphs[ch]; if (!glyph) { // Leerzeichen mit festem Abstand const spacePx = fontSizePx * 0.4; x += spacePx; continue; } const d = glyph.d; const advPx = (glyph.adv || unitsPerEm * 0.5) * scale; // Einfache Transform-Matrix ohne Rotation const transform = `matrix(${scale},0,0,${-scale},${x},${baselineY})`; resultPaths.push({ d, transform, }); x += advPx; } baselineY += lineHeightPx; } return resultPaths; } module.exports = { measureTextWidthPx, wrapTextToLinesByWidth, layoutTextToPaths };