213 lines
4.6 KiB
JavaScript
213 lines
4.6 KiB
JavaScript
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(/<font-face[\s\S]*?>/);
|
|
if (!fontFaceMatch) {
|
|
throw new Error(`Kein <font-face> 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 = /<glyph[\s\S]*?\/>/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
|
|
};
|