@@ -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,89 @@ 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 .
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). Nur DER Text kann „zu lang" sein (Nutzer kürzt ihn).
const baseline0 = mmToPx ( 14 ) + fontSizePx ;
bodyLineLimit = Math . max ( 1 , Math . floor ( ( mmToPx ( maxYmm ) - baseline0 ) / lineHeightPx ) + 1 ) ;
if ( body ) {
if ( body ) {
const bodyLines = wrapTextToLinesByWidth ( { text : body , maxWidth Px: textWidth , fontFileName : fontConfig . file , fontSizePx } ) ;
const bodyLines = flowVariabel ( body , fontSize Px, baseline0 , lineHeightPx ) ;
bodyLineCount = bodyLines . length ;
bodyLineCount = bodyLines . length ;
if ( bodyLineCount > bodyLineLimit ) overflow = true ;
paths . push ( ... layoutTextToPaths ( {
paths . push ( ... layoutTextToPaths ( {
lines : bodyLines , fontFileName : fontConfig . f ile , fontSizePx ,
lines : bodyLines , fontFileName : fontF ile , fontSizePx ,
startX : textLeft , startY : marginTopPx + fontSizePx , lineHeightPx , realisticHandwriting : realistic ,
startX : mmToPx ( textLeftMm ) , startY : baseline0 , lineHeightPx , realisticHandwriting : realistic ,
} ) ) ;
} ) ) ;
}
}
// Absender klein oben in der Adresszone, links neben dem Briefmarkenfeld .
// Absender oben l inks (2 Zeilen), klein genug für die 10-mm-Zone .
if ( sender ) {
if ( sender ) {
const senderScal e = Math . max ( 0.5 , Math . min ( 2, Number ( options . senderScale ) || 1 ) ) ;
const senderSiz e = FONT _SIZE _PX * 0.55 * Math . max ( 0.7 , Math . min ( 1. 2, Number ( options . senderScale ) || 1 ) ) ;
const senderSize = FONT _SIZE _PX * 0.7 * senderScal e ;
const senderLH = ( baseLineHeightPx / FONT _SIZE _PX ) * senderSiz e ;
const senderLH = lineHeightPx * ( senderSize / fontSizePx ) ;
const senderLines = wrapTextToLinesByWidth ( { text : sender , maxWidthPx : mmToPx ( textUpperRightMm - textLeftMm ) , fontFileName : fontFile , fontSizePx : senderSize } ) . slice ( 0 , 2 ) ;
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 } ) ;
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 : 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). Passt der Block nicht in die
// 46 mm hohe Zone, wird die Schrift automatisch verkleinert – so bleibt die
// Codierzone (unten) IMMER frei und es gibt keinen „Adresse zu lang"-Fehler.
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 ) ;
let recSize = fontSizePx ;
let recLH = lineHeightPx ;
let recLines = [ ] ;
for ( const scale of [ 1 , 0.9 , 0.8 , 0.72 , 0.65 , 0.6 ] ) {
recSize = fontSizePx * scale ;
recLH = lineHeightPx * 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 +561,19 @@ function generatePostcardSVG(text, format, options = {}) {
/> ` )
/> ` )
. join ( '\n ' ) ;
. join ( '\n ' ) ;
// Trennstrich (solide Linie, 1,2 mm breit) zwischen Text und Adressfeld.
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=" ${ mmToPx ( 1.2 ) . toFixed ( 1 ) } " stroke-linecap="butt" fill="none"/> ` ;
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 } ;
}
}
/**
/**