diff --git a/src/lib/svg-generator.js b/src/lib/svg-generator.js index feef27e..383254f 100644 --- a/src/lib/svg-generator.js +++ b/src/lib/svg-generator.js @@ -437,18 +437,25 @@ function splitPostcardText(text) { } /** - * Postkarte (einseitig): Text auf der LINKEN Hälfte, Absender + Empfänger auf der - * RECHTEN Hälfte, Briefmarkenfeld (leerer Bereich) oben rechts. Für A6/DIN-lang- - * Postkarten – ein SVG je Karte (ein Plotter-Durchlauf). Der Text kommt bereits - * scriptalisiert an und enthält die Marker [[pk_abs]] / [[pk_adr]]. + * Postkarte (einseitig) nach dem Plotter-Maßblatt (Dialogpost A6, Ursprung oben + * links, Maße in mm): + * • Absender: x 2–72, y 2–12 (2 Zeilen, klein) + * • Text: x 2–72, y 14–40 (oben, schmal, links neben der Frankierzone) + * und x 2–81,4, y 40–88 (unten, breiter) – L-förmiger Textfluss + * • Empfänger: x 86,6–146, y 42–88 (sicheres Adressfeld) + * • Trennstrich: x 84, y 42–88, Breite 1,2 mm + * • FREI: Frankierzone (x 74–148, y 0–40) und Codierzone (y 90–105) + * Andere Kartengrößen: Zonen an W/H verankert (Adresse rechts, Text links, + * Frankierzone rechts oben, Codierzone unten frei). Text kommt scriptalisiert + * mit den Markern [[pk_abs]] / [[pk_adr]]. */ function generatePostcardSVG(text, format, options = {}) { const fontKey = normalizeFontKey(options.font); - const fontConfig = SVG_FONT_CONFIG[fontKey]; + const fontFile = SVG_FONT_CONFIG[fontKey].file; const realistic = options.realisticHandwriting ?? false; - const { widthPx, heightPx, widthMm, heightMm } = getPageDimensions(format); - const { marginTopPx, marginLeftPx, lineHeightPx: baseLineHeightPx } = getLayoutForFormat(format); + const { widthPx, heightPx, widthMm: W, heightMm: H } = getPageDimensions(format); + const { lineHeightPx: baseLineHeightPx } = getLayoutForFormat(format); const { body, sender, recipient } = splitPostcardText(text); @@ -456,50 +463,74 @@ function generatePostcardSVG(text, format, options = {}) { const fontSizePx = FONT_SIZE_PX * fontScale; const lineHeightPx = baseLineHeightPx * fontScale; - // Zonen: linke Hälfte Text, rechte Hälfte Adresse; Briefmarke oben rechts frei. - const textLeft = marginLeftPx; - const textRight = widthPx * 0.48; - const textWidth = Math.max(mmToPx(30), textRight - textLeft); - const addrLeft = widthPx * 0.52; - const addrRight = widthPx - mmToPx(RAND_RECHTS_MM); - const addrWidth = Math.max(mmToPx(30), addrRight - addrLeft); - const stampW = mmToPx(25); - const stampX = widthPx - mmToPx(6) - stampW; + // Zonen in mm (Maßblatt), für andere Größen an den Rändern verankert. + const maxYmm = H - 17; // Codierzone (unten 15 mm) frei → A6: 88 + const frankBottomMm = 40; // Frankierzone-Unterkante (Briefmarke) + const frankLeftMm = W - 74; // Frankierzone links → A6: 74 + const addrRightMm = W - 2; // A6: 146 + const addrLeftMm = addrRightMm - 59.4; // A6: 86,6 + const addrTopMm = 42; + const textLeftMm = 2; + const textUpperRightMm = frankLeftMm - 2; // A6: 72 + const textLowerRightMm = addrLeftMm - 5.2; // A6: 81,4 + const trennXmm = (textLowerRightMm + addrLeftMm) / 2; // A6: 84 const paths = []; - let bodyLineCount = 0; + let overflow = false; - // Text links. + // Variabler (L-förmiger) Textfluss: oben schmal (bis Frankierzone), unten breit. + const breiteBeiY = (yPx) => (yPx < mmToPx(frankBottomMm) + ? mmToPx(textUpperRightMm - textLeftMm) + : mmToPx(textLowerRightMm - textLeftMm)); + function flowVariabel(txt, size, baseline0, lh) { + const paras = String(txt).split(/\r?\n/); + const out = []; + let y = baseline0; + for (const para of paras) { + const words = para.split(/\s+/).filter(Boolean); + if (!words.length) { out.push(''); y += lh; continue; } + let cur = ''; + for (const w of words) { + const test = cur ? cur + ' ' + w : w; + const wpx = measureTextWidthPx({ text: test, fontFileName: fontFile, fontSizePx: size }); + if (cur && wpx > breiteBeiY(y)) { out.push(cur); y += lh; cur = w; } else cur = test; + } + if (cur !== '') { out.push(cur); y += lh; } + } + return out; + } + + // Text links (L-förmig). if (body) { - const bodyLines = wrapTextToLinesByWidth({ text: body, maxWidthPx: textWidth, fontFileName: fontConfig.file, fontSizePx }); - bodyLineCount = bodyLines.length; + const baseline0 = mmToPx(14) + fontSizePx; + const bodyLines = flowVariabel(body, fontSizePx, baseline0, lineHeightPx); + const letzteBaseline = baseline0 + Math.max(0, bodyLines.length - 1) * lineHeightPx; + if (letzteBaseline > mmToPx(maxYmm)) overflow = true; paths.push(...layoutTextToPaths({ - lines: bodyLines, fontFileName: fontConfig.file, fontSizePx, - startX: textLeft, startY: marginTopPx + fontSizePx, lineHeightPx, realisticHandwriting: realistic, + lines: bodyLines, fontFileName: fontFile, fontSizePx, + startX: mmToPx(textLeftMm), startY: baseline0, lineHeightPx, realisticHandwriting: realistic, })); } - // Absender klein oben in der Adresszone, links neben dem Briefmarkenfeld. + // Absender oben links (2 Zeilen), klein genug für die 10-mm-Zone. if (sender) { - const senderScale = Math.max(0.5, Math.min(2, Number(options.senderScale) || 1)); - const senderSize = FONT_SIZE_PX * 0.7 * senderScale; - const senderLH = lineHeightPx * (senderSize / fontSizePx); - const senderWidth = Math.max(mmToPx(30), Math.min(addrWidth, stampX - addrLeft - mmToPx(3))); - const senderLines = wrapTextToLinesByWidth({ text: sender, maxWidthPx: senderWidth, fontFileName: fontConfig.file, fontSizePx: senderSize }); + const senderSize = FONT_SIZE_PX * 0.55 * Math.max(0.7, Math.min(1.2, Number(options.senderScale) || 1)); + const senderLH = (baseLineHeightPx / FONT_SIZE_PX) * senderSize; + const senderLines = wrapTextToLinesByWidth({ text: sender, maxWidthPx: mmToPx(textUpperRightMm - textLeftMm), fontFileName: fontFile, fontSizePx: senderSize }).slice(0, 2); paths.push(...layoutTextToPaths({ - lines: senderLines, fontFileName: fontConfig.file, fontSizePx: senderSize, - startX: addrLeft, startY: mmToPx(8) + senderSize, lineHeightPx: senderLH, realisticHandwriting: realistic, + lines: senderLines, fontFileName: fontFile, fontSizePx: senderSize, + startX: mmToPx(textLeftMm), startY: mmToPx(2) + senderSize, lineHeightPx: senderLH, realisticHandwriting: realistic, })); } - // Empfänger in der unteren Hälfte der Adresszone (klassische Anschriftzone). + // Empfänger im sicheren Adressfeld (rechts unten). if (recipient) { - const recLines = wrapTextToLinesByWidth({ text: recipient, maxWidthPx: addrWidth, fontFileName: fontConfig.file, fontSizePx }); - const recBottom = heightPx * 0.90; - const recStartY = recBottom - Math.max(0, recLines.length - 1) * lineHeightPx; + const recLines = wrapTextToLinesByWidth({ text: recipient, maxWidthPx: mmToPx(addrRightMm - addrLeftMm), fontFileName: fontFile, fontSizePx }); + const recBaseline0 = mmToPx(addrTopMm) + fontSizePx; + if (recBaseline0 + Math.max(0, recLines.length - 1) * lineHeightPx > mmToPx(maxYmm)) overflow = true; paths.push(...layoutTextToPaths({ - lines: recLines, fontFileName: fontConfig.file, fontSizePx, - startX: addrLeft, startY: recStartY, lineHeightPx, realisticHandwriting: realistic, + lines: recLines, fontFileName: fontFile, fontSizePx, + startX: mmToPx(addrLeftMm), startY: recBaseline0, lineHeightPx, realisticHandwriting: realistic, })); } @@ -515,18 +546,19 @@ function generatePostcardSVG(text, format, options = {}) { />`) .join('\n '); + // Trennstrich (solide Linie, 1,2 mm breit) zwischen Text und Adressfeld. + const trennstrich = ``; + const svg = ` + ${trennstrich} ${pathElements} `; - // Überlauf: passt der Text in die linke Hälfte (Höhe)? - const available = heightPx - (marginTopPx + fontSizePx) - fontSizePx * 0.5; - const lineLimit = Math.max(1, Math.floor(available / lineHeightPx) + 1); - return { svg: svg.trim(), overflow: bodyLineCount > lineLimit, lineCount: bodyLineCount, lineLimit }; + return { svg: svg.trim(), overflow, lineCount: 0, lineLimit: 0 }; } /**