@@ -18,6 +18,10 @@ const {
const SIG _CANVAS _W = 600 ;
const SIG _CANVAS _W = 600 ;
const SIG _CANVAS _H = 160 ;
const SIG _CANVAS _H = 160 ;
// Mindestabstand des Adressblocks zum rechten Rand (mm). Der Plotter braucht rechts
// Greiffläche – gilt einheitlich für Postkarte UND Umschlag-Empfänger (mit Absender).
const RAND _RECHTS _MM = 15 ;
/**
/**
* Extrahiert Path- und Polyline-Elemente aus einem SVG-String und gibt sie als
* Extrahiert Path- und Polyline-Elemente aus einem SVG-String und gibt sie als
* umgeformte <g>-Gruppe zurück, die an Position (x, y) mit Skalierung scale eingebettet ist.
* umgeformte <g>-Gruppe zurück, die an Position (x, y) mit Skalierung scale eingebettet ist.
@@ -297,7 +301,12 @@ function generateEnvelopeSVG(text, format, type, options = {}) {
// type === 'recipient' / 'free'. Ist ein Absender gesetzt, sitzt der
// type === 'recipient' / 'free'. Ist ein Absender gesetzt, sitzt der
// Empfänger UNTEN RECHTS (klassisches Kuvert), sonst wie bisher oben links.
// Empfänger UNTEN RECHTS (klassisches Kuvert), sonst wie bisher oben links.
const hatAbsender = ! ! String ( options . sender || '' ) . trim ( ) ;
const hatAbsender = ! ! String ( options . sender || '' ) . trim ( ) ;
const maxWidthPx = Math . min ( widthPx - marginLeftPx - 20 , widthPx * ( hatAbsender ? 0.36 : 0.6 ) ) ;
// Empfänger unten rechts (mit Absender): Startpunkt zuerst festlegen, dann die
// Textbreite so begrenzen, dass rechts der Plotter-Mindestabstand frei bleibt.
const startXEmpf = Math . round ( widthPx * 0.55 ) ;
const maxWidthPx = hatAbsender
? Math . max ( mmToPx ( 30 ) , ( widthPx - mmToPx ( RAND _RECHTS _MM ) ) - startXEmpf )
: Math . min ( widthPx - marginLeftPx - 20 , widthPx * 0.6 ) ;
lines = wrapTextToLinesByWidth ( {
lines = wrapTextToLinesByWidth ( {
text : String ( text || '' ) ,
text : String ( text || '' ) ,
@@ -309,7 +318,7 @@ function generateEnvelopeSVG(text, format, type, options = {}) {
if ( hatAbsender ) {
if ( hatAbsender ) {
// Empfänger unten rechts – bewusst über Höhen-/Breiten-Anteile, damit der
// Empfänger unten rechts – bewusst über Höhen-/Breiten-Anteile, damit der
// große Kuvert-Rand (marginTop bis ~50 % der Höhe) nicht alles nach oben zieht.
// große Kuvert-Rand (marginTop bis ~50 % der Höhe) nicht alles nach oben zieht.
startX = Math . round ( widthPx * 0.60 ) ;
startX = startXEmpf ;
const untenBaseline = heightPx * 0.80 ; // unterste Zeile ~20 % über der Unterkante
const untenBaseline = heightPx * 0.80 ; // unterste Zeile ~20 % über der Unterkante
startY = Math . round ( untenBaseline - Math . max ( 0 , lines . length - 1 ) * lineHeightPx ) ;
startY = Math . round ( untenBaseline - Math . max ( 0 , lines . length - 1 ) * lineHeightPx ) ;
} else {
} else {
@@ -428,18 +437,25 @@ function splitPostcardText(text) {
}
}
/**
/**
* Postkarte (einseitig): Text auf der LINKEN Hälfte, Absender + Empfänger auf der
* Postkarte (einseitig) nach dem Plotter-Maßblatt (Dialogpost A6, Ursprung oben
* RECHTEN Hälfte, Briefmarkenfeld (leerer Bereich) oben rechts. Für A6/DIN-lang-
* links, Maße in mm):
* Postkarten – ein SVG je Karte (ein Plotter-Durchlauf). Der Text kommt bereits
* • Absender: x 2– 72, y 2– 12 (2 Zeilen, klein)
* scriptalisiert an und enthält die Marker [[pk_abs]] / [[pk_adr]].
* • 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 = { } ) {
function generatePostcardSVG ( text , format , options = { } ) {
const fontKey = normalizeFontKey ( options . font ) ;
const fontKey = normalizeFontKey ( options . font ) ;
const fontConfig = SVG _FONT _CONFIG [ fontKey ] ;
const fontFile = SVG _FONT _CONFIG [ fontKey ] . file ;
const realistic = options . realisticHandwriting ? ? false ;
const realistic = options . realisticHandwriting ? ? false ;
const { widthPx , heightPx , widthMm , heightMm } = getPageDimensions ( format ) ;
const { widthPx , heightPx , widthMm : W , heightMm : H } = getPageDimensions ( format ) ;
const { marginTopPx , marginLeftPx , lineHeightPx: baseLineHeightPx } = getLayoutForFormat ( format ) ;
const { lineHeightPx : baseLineHeightPx } = getLayoutForFormat ( format ) ;
const { body , sender , recipient } = splitPostcardText ( text ) ;
const { body , sender , recipient } = splitPostcardText ( text ) ;
@@ -447,50 +463,98 @@ function generatePostcardSVG(text, format, options = {}) {
const fontSizePx = FONT _SIZE _PX * fontScale ;
const fontSizePx = FONT _SIZE _PX * fontScale ;
const lineHeightPx = baseLineHeightPx * fontScale ;
const lineHeightPx = baseLineHeightPx * fontScale ;
// Zonen: linke Hälfte Text, rechte Hälfte Adresse; Briefmarke oben rechts frei .
// Zonen in mm (Maßblatt), für andere Größen an den Rändern verankert .
const textLeft = marginLeftPx ;
const maxYmm = H - 17 ; // Codierzone (unten 15 mm) frei → A6: 88
const textRight = widthPx * 0.48 ;
const frankBottomMm = 40 ; // Frankierzone-Unterkante (Briefmarke)
const textWidth = Math . max ( mmToPx ( 30 ) , textRight - textLeft ) ;
const frankLeftMm = W - 74 ; // Frankierzone links → A6: 74
const addrLeft = widthPx * 0.5 2;
const addrRightMm = W - 2; // A6: 146
const addrRight = widthPx - mmToPx ( 8 ) ;
const addrLeftMm = addrRightMm - 59.4 ; // A6: 86,6
const addrWidth = Math . max ( mmToPx ( 30 ) , addrRight - addrLeft ) ;
const addrTopMm = 42 ;
const stampW = mmToPx ( 25 ) ;
const textLeftMm = 2 ;
const stampX = widthPx - mmToPx ( 6 ) - stampW ;
const textUpperRightMm = frankLeftMm - 2 ; // A6: 72
const textLowerRightMm = addrLeftMm - 5.2 ; // A6: 81,4
const trennXmm = ( textLowerRightMm + addrLeftMm ) / 2 ; // A6: 84
const paths = [ ] ;
const paths = [ ] ;
let bodyLineCount = 0 ;
let bodyLineCount = 0 ;
let bodyLineLimit = 1 ;
let overflow = false ;
// Text links .
// Variabler (L-förmiger) Textfluss: oben schmal (bis Frankierzone), unten breit .
if ( body ) {
const breiteBeiY = ( yPx ) => ( yPx < mmToPx ( frankBottomMm )
const bodyLines = wrapTextToLinesByWidth ( { text : body , maxWidthPx : textWidth , fontFileName : fontConfig . file , fontSizePx } ) ;
? mmToPx ( textUpperRightMm - textLeftMm )
bodyLineCount = bodyLines . length ;
: mmToPx ( textLowerRightMm - textLeftMm ) ) ;
paths . push ( ... layoutTextToPaths ( {
function flowVariabel ( txt , size , baseline0 , lh ) {
lines : bodyLines , fontFileName : fontConfig . file , fontSizePx ,
const paras = String ( txt ) . split ( /\r?\n/ ) ;
startX : textLeft , startY : marginTopPx + fontSizePx , lineHeightPx , realisticHandwriting : realistic ,
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 ;
}
}
// Absender klein oben in der Adresszone, links neben dem Briefmarkenfeld .
// Absender oben l inks – ZUERST, damit der Text knapp darunter beginnen kann .
// Größe folgt der „Absender-Größe" aus dem manuellen Druck (options.senderScale).
let senderBottomPx = mmToPx ( 2 ) ;
if ( sender ) {
if ( sender ) {
const senderScale = Math . max ( 0.5 , Math . min ( 2 , Number ( options . senderScale ) || 1 ) ) ;
const senderScale = Math . max ( 0.5 , Math . min ( 1.8 , Number ( options . senderScale ) || 1 ) ) ;
const senderSize = FONT _SIZE _PX * 0.7 * senderScale ;
const senderSize = FONT _SIZE _PX * 0.55 * senderScale ;
const senderLH = l ineHeightPx * ( senderSize / fontSizePx ) ;
const senderLH = ( baseL ineHeightPx / FONT _SIZE _PX ) * senderSize ;
const senderWidth = Math . max ( mmToPx ( 30 ) , Math . min ( addrWidth , stampX - addrLeft - mmToPx ( 3 ) ) ) ;
const senderLines = wrapTextToLinesByWidth ( { text : sender , maxWidthPx : mmToPx ( textUpperRightMm - textLeftMm ) , fontFileName : fontFile , fontSizePx : senderSize } ) . slice ( 0 , 2 ) ;
const senderLines = wrapTextToLinesByWidth ( { text : sender , maxWidthPx : senderWidth , fontFileName : fontConfig . file , fontSizePx : senderSize } ) ;
const senderStartY = mmToPx ( 2 ) + senderSize ;
paths . push ( ... layoutTextToPaths ( {
paths . push ( ... layoutTextToPaths ( {
lines : senderLines , fontFileName : fontConfig . f ile , fontSizePx : senderSize ,
lines : senderLines , fontFileName : fontF ile , fontSizePx : senderSize ,
startX : addrLeft , startY : mmToPx ( 8 ) + senderSize , lineHeightPx : senderLH , realisticHandwriting : realistic ,
startX : mmToPx ( textLeftMm ) , startY : senderStartY , lineHeightPx : senderLH , realisticHandwriting : realistic ,
} ) ) ;
senderBottomPx = senderStartY + Math . max ( 0 , senderLines . length - 1 ) * senderLH + senderSize * 0.3 ;
}
// Text links (L-förmig). Beginn nur ETWAS unter dem Absender (≈2 mm); ohne
// Absender wie im Maßblatt bei y≈14 mm. Nur DER Text kann „zu lang" sein.
const baseline0 = ( sender ? senderBottomPx + mmToPx ( 2 ) : mmToPx ( 14 ) ) + fontSizePx ;
bodyLineLimit = Math . max ( 1 , Math . floor ( ( mmToPx ( maxYmm ) - baseline0 ) / lineHeightPx ) + 1 ) ;
if ( body ) {
const bodyLines = flowVariabel ( body , fontSizePx , baseline0 , lineHeightPx ) ;
bodyLineCount = bodyLines . length ;
if ( bodyLineCount > bodyLineLimit ) overflow = true ;
paths . push ( ... layoutTextToPaths ( {
lines : bodyLines , fontFileName : fontFile , fontSizePx ,
startX : mmToPx ( textLeftMm ) , startY : baseline0 , lineHeightPx , realisticHandwriting : realistic ,
} ) ) ;
} ) ) ;
}
}
// Empfänger in der unteren Hälfte der Adresszone (klassische Anschriftzone).
// Empfänger im sicheren Adressfeld (rechts unten). Größe folgt der „Schriftgröße
// Kuvert" (options.recipientScale). Passt der Block nicht in die 46-mm-Zone, wird
// er automatisch verkleinert – so bleibt die Codierzone IMMER frei.
if ( recipient ) {
if ( recipient ) {
const recLines = wrapTextToLinesByWidth ( { text : recipient , maxWidthPx : addrWidth , fontFileName : fontConfig . file , fontSizePx } ) ;
const feldOben = mmToPx ( addrTopMm ) ;
const recBottom = heightPx * 0.90 ;
const feldUnten = mmToPx ( maxYmm ) ;
const recStartY = recBottom - Math . max ( 0 , recLines . length - 1 ) * lineHeightPx ;
const feldBreite = mmToPx ( addrRightMm - addrLeftMm ) ;
const recScale = Math . max ( 0.6 , Math . min ( 1.6 , Number ( options . recipientScale ) || 1 ) ) ;
const recBaseSize = FONT _SIZE _PX * recScale ;
const recBaseLH = baseLineHeightPx * recScale ;
let recSize = recBaseSize ;
let recLH = recBaseLH ;
let recLines = [ ] ;
for ( const scale of [ 1 , 0.9 , 0.8 , 0.72 , 0.65 , 0.6 ] ) {
recSize = recBaseSize * scale ;
recLH = recBaseLH * scale ;
recLines = wrapTextToLinesByWidth ( { text : recipient , maxWidthPx : feldBreite , fontFileName : fontFile , fontSizePx : recSize } ) ;
const blockUnten = feldOben + recSize + Math . max ( 0 , recLines . length - 1 ) * recLH + recSize * 0.3 ;
if ( blockUnten <= feldUnten ) break ;
}
paths . push ( ... layoutTextToPaths ( {
paths . push ( ... layoutTextToPaths ( {
lines : recLines , fontFileName : fontConfig . f ile , fontSizePx ,
lines : recLines , fontFileName : fontF ile , fontSizePx : recSize ,
startX : addrLeft , startY : recStartY , lineHeightPx , realisticHandwriting : realistic ,
startX : mmToPx ( addrLeftMm ) , startY : feldOben + recSize , lineHeightPx : recLH , realisticHandwriting : realistic ,
} ) ) ;
} ) ) ;
}
}
@@ -506,18 +570,20 @@ function generatePostcardSVG(text, format, options = {}) {
/> ` )
/> ` )
. join ( '\n ' ) ;
. join ( '\n ' ) ;
// Trennstrich: einfache Kugelschreiber-Linie (Single-Stroke, keine Füllung) –
// gleiche Strichstärke wie die Handschrift, damit die Ballpoint-Optik greift.
const trennstrich = ` <path d="M ${ mmToPx ( trennXmm ) . toFixed ( 1 ) } ${ mmToPx ( addrTopMm ) . toFixed ( 1 ) } L ${ mmToPx ( trennXmm ) . toFixed ( 1 ) } ${ mmToPx ( maxYmm ) . toFixed ( 1 ) } " stroke="#000000" stroke-width="0.6" fill="none" stroke-linecap="round" vector-effect="non-scaling-stroke"/> ` ;
const svg = ` <svg xmlns="http://www.w3.org/2000/svg"
const svg = ` <svg xmlns="http://www.w3.org/2000/svg"
width=" ${ widthMm } mm"
width=" ${ W } mm"
height=" ${ heightMm } mm"
height=" ${ H } mm"
viewBox="0 0 ${ widthPx } ${ heightPx } ">
viewBox="0 0 ${ widthPx } ${ heightPx } ">
<rect x="0" y="0" width=" ${ widthPx } " height=" ${ heightPx } " fill="#FFFFFF" />
<rect x="0" y="0" width=" ${ widthPx } " height=" ${ heightPx } " fill="#FFFFFF" />
${ trennstrich }
${ pathElements }
${ pathElements }
</svg> ` ;
</svg> ` ;
// Überlauf: passt der Text in die linke Hälfte (Höhe)?
return { svg : svg . trim ( ) , overflow , lineCount : bodyLineCount , lineLimit : bodyLineLimit } ;
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 } ;
}
}
/**
/**