2 Commits

Author SHA1 Message Date
s4luorth
42a0d2d8b1 Kuvertbeschriftung: Absender oben links + Empfaenger unten rechts (opt-in)
All checks were successful
Build & Deploy / deploy (push) Successful in 22s
generateEnvelopeSVG rendert bei gesetztem Absender den Absender klein oben links und
den Empfaenger unten rechts (sonst unveraendert). Absender wird ueber die Auftrags-/
Vorschau-Pipeline durchgereicht (metadata.envelopeSender, envelope_sender), fetchOrder
laedt das Feld, Kuvert wird bei Absender frisch gerendert statt 1:1 aus der Vorschau.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-01 15:01:07 +02:00
s4luorth
25ffb3727b Scriptalizer: nach Zeichenlaenge batchen (32000-Limit)
All checks were successful
Build & Deploy / deploy (push) Successful in 19s
Fixe 18er-Batches sprengen bei langen/mehrseitigen Texten das InputText-Limit der
Scriptalizer-API (32000 Zeichen) -> 'Input length limit 32000 exceeded'. Jetzt werden
Batches nach kumulativer Zeichenlaenge (max 28000, Sicherheitsabstand) gebildet, die
Anzahl 18 bleibt als zusaetzlicher Deckel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 10:00:58 +02:00
6 changed files with 70 additions and 22 deletions

View File

@@ -167,6 +167,8 @@ async function generateFromDirectus(req, res, next) {
previewSession: order.preview_session || null,
// Eingegebener Brieftext → als brieftext.txt im Ausgabeordner ablegen.
rawText: order.text_template || null,
// Absender fürs Kuvert (manuelle Aufträge): oben links, Empfänger unten rechts.
envelopeSender: order.envelope_sender || null,
},
});

View File

@@ -311,8 +311,9 @@ async function runGeneration({ orderNumber, letters, metadata }) {
const filepath = isEnv ? path.join(envelopesDir, filename) : path.join(outputDir, filename);
// 1:1 aus der Vorschau übernehmen, wenn für dieses Dokument eine
// Vorschau-Datei existiert.
if (previewSession) {
// Vorschau-Datei existiert. Ausnahme: Kuvert mit Absender – das wird frisch
// gerendert, damit der Absender (oben links) sicher drauf ist.
if (previewSession && !(isEnv && metadata.envelopeSender)) {
const cached = path.join(previewsRoot, previewSession, filename);
try {
await fs.access(cached);
@@ -329,7 +330,7 @@ async function runGeneration({ orderNumber, letters, metadata }) {
if (isEnv) {
svgResult = generateEnvelopeSVG(
scriptalizedText, letter.format, letter.envelopeType,
{ font: letter.font, realisticHandwriting: letter.realisticHandwriting ?? false, fontScale: letter.fontScale }
{ font: letter.font, realisticHandwriting: letter.realisticHandwriting ?? false, fontScale: letter.fontScale, sender: metadata.envelopeSender || '' }
);
} else {
svgResult = generateLetterSVG(

View File

@@ -167,7 +167,7 @@ async function generateBatch(req, res, next) {
if (letter.type === "envelope") {
const svgResult = generateEnvelopeSVG(
scriptalizedText, letter.format, letter.envelopeType,
{ font: letter.font, realisticHandwriting: letter.realisticHandwriting ?? false, fontScale: letter.fontScale }
{ font: letter.font, realisticHandwriting: letter.realisticHandwriting ?? false, fontScale: letter.fontScale, sender: letter.sender || '' }
);
if (svgResult.overflow) hasOverflow = true;
const filename = `envelope_${pad}.svg`;

View File

@@ -294,8 +294,10 @@ function generateEnvelopeSVG(text, format, type, options = {}) {
startX = 0; // Dummy-Wert, wird für jede Zeile überschrieben
} else {
// type === 'recipient' / 'free': Links, max. 60% der Umschlagsbreite
const maxWidthPx = Math.min(widthPx - marginLeftPx - 20, widthPx * 0.6);
// type === 'recipient' / 'free'. Ist ein Absender gesetzt, sitzt der
// Empfänger UNTEN RECHTS (klassisches Kuvert), sonst wie bisher oben links.
const hatAbsender = !!String(options.sender || '').trim();
const maxWidthPx = Math.min(widthPx - marginLeftPx - 20, widthPx * (hatAbsender ? 0.48 : 0.6));
lines = wrapTextToLinesByWidth({
text: String(text || ''),
@@ -304,8 +306,13 @@ function generateEnvelopeSVG(text, format, type, options = {}) {
fontSizePx,
});
startX = marginLeftPx;
startY = marginTopPx + fontSizePx;
if (hatAbsender) {
startX = Math.max(marginLeftPx, Math.round(widthPx * 0.48));
startY = Math.max(marginTopPx + fontSizePx, heightPx - marginTopPx - lines.length * lineHeightPx + fontSizePx);
} else {
startX = marginLeftPx;
startY = marginTopPx + fontSizePx;
}
}
let paths = [];
@@ -347,6 +354,24 @@ function generateEnvelopeSVG(text, format, type, options = {}) {
});
}
// Absender oben links (kleiner) – nur wenn gesetzt und kein zentrierter Freitext.
const absender = String(options.sender || '').trim();
if (absender && type !== 'custom') {
const absSize = fontSizePx * 0.62;
const absLH = lineHeightPx * 0.62;
const absLines = absender.split('\n').map((l) => l.replace(/\s+$/, ''));
const absPaths = layoutTextToPaths({
lines: absLines,
fontFileName: fontConfig.file,
fontSizePx: absSize,
startX: marginLeftPx,
startY: marginTopPx + absSize,
lineHeightPx: absLH,
realisticHandwriting: options.realisticHandwriting ?? false,
});
paths = [...paths, ...absPaths];
}
const pathElements = paths
.map(
(p) => `<path d="${escapeXml(p.d)}"

View File

@@ -41,7 +41,7 @@ async function directusFetch(path, options = {}) {
async function fetchOrder(orderId) {
const fields = [
'id', 'order_number', 'customer_email', 'font', 'status',
'needs_envelope', 'envelope_labeling', 'envelope_text', 'envelope_format',
'needs_envelope', 'envelope_labeling', 'envelope_text', 'envelope_format', 'envelope_sender',
'text_template', 'motif_mode', 'own_cards', 'entries_count', 'preview_session',
'source_file', 'product.key', 'product.type', 'product.input_mode', 'format.key',
'font_scale_letter', 'font_scale_envelope', 'signatures_manual', 'multi_page', 'realistic',

View File

@@ -141,29 +141,49 @@ async function scriptalizeBatch(texts, font = "tilda", errFrequency = 10) {
config.scriptalizer.fontMap[font.toLowerCase()] ||
config.scriptalizer.fontMap.tilda;
const separator = config.scriptalizer.separator;
const BATCH_SIZE = 18; // 18 Texte pro Scriptalizer API Call (Scriptalizer-API-Limit: 32000 Zeichen pro Call → ca. 1700 Zeichen pro Brief sicher)
const BATCH_SIZE = 18; // Zusätzlicher Deckel: max. 18 Texte pro Call (unabhängig von der Länge).
// Primäre Grenze ist die ZEICHENLÄNGE: die Scriptalizer-API begrenzt den
// InputText auf 32000 Zeichen pro Call ("Input length limit 32000 exceeded").
// Fixe 18er-Batches sprengen das bei langen (mehrseitigen) Texten – daher nach
// kumulativer Länge batchen, mit Sicherheitsabstand.
const MAX_BATCH_CHARS = 28000;
const sepLen = separator.length;
// Batches nach Länge (und max. Anzahl) bilden.
const batches = [];
{
let cur = [];
let curLen = 0;
for (const t of texts) {
const tLen = String(t || "").length;
const projected = curLen + (cur.length ? sepLen : 0) + tLen;
if (cur.length && (projected > MAX_BATCH_CHARS || cur.length >= BATCH_SIZE)) {
batches.push(cur);
cur = [];
curLen = 0;
}
cur.push(t);
curLen += (cur.length > 1 ? sepLen : 0) + tLen;
}
if (cur.length) batches.push(cur);
}
const totalBatches = batches.length;
console.log(
`[Scriptalizer] Processing ${texts.length} texts in batches of ${BATCH_SIZE}`
`[Scriptalizer] Processing ${texts.length} texts in ${totalBatches} batch(es) (max ${BATCH_SIZE} texts / ${MAX_BATCH_CHARS} chars each)`
);
const allScriptalized = [];
// Teile in 25er Batches auf
for (let i = 0; i < texts.length; i += BATCH_SIZE) {
const batch = texts.slice(i, i + BATCH_SIZE);
const batchNumber = Math.floor(i / BATCH_SIZE) + 1;
const totalBatches = Math.ceil(texts.length / BATCH_SIZE);
for (let b = 0; b < batches.length; b += 1) {
const batch = batches[b];
const batchNumber = b + 1;
const combinedText = batch.join(separator);
console.log(
`[Scriptalizer] Processing batch ${batchNumber}/${totalBatches} (${
batch.length
} texts, indices ${i}-${i + batch.length - 1})`
`[Scriptalizer] Processing batch ${batchNumber}/${totalBatches} (${batch.length} texts, ${combinedText.length} chars)`
);
// Kombiniere Texte mit Separator
const combinedText = batch.join(separator);
try {
const result = await callScriptalizer(
combinedText,