Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
832204c3d8 | ||
|
|
3dfcdcd3b5 | ||
|
|
bffb7a1085 | ||
|
|
13f8394512 | ||
|
|
42a0d2d8b1 | ||
|
|
25ffb3727b | ||
|
|
d42a09ab81 |
@@ -167,6 +167,9 @@ 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,
|
||||
envelopeSenderScale: Number(order.envelope_sender_scale) || 1,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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 || '', senderScale: metadata.envelopeSenderScale || 1 }
|
||||
);
|
||||
} else {
|
||||
svgResult = generateLetterSVG(
|
||||
|
||||
@@ -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 || '', senderScale: letter.senderScale || 1 }
|
||||
);
|
||||
if (svgResult.overflow) hasOverflow = true;
|
||||
const filename = `envelope_${pad}.svg`;
|
||||
|
||||
@@ -57,7 +57,6 @@ function applyRenderFormats(rows) {
|
||||
PAGE_FORMATS = pf;
|
||||
FORMAT_LAYOUTS = fl;
|
||||
LINE_LIMITS = ll;
|
||||
signaledMisses.clear();
|
||||
}
|
||||
|
||||
// mm in Pixel umrechnen
|
||||
@@ -72,12 +71,12 @@ function mmToPx(mm) {
|
||||
// stündliche Aktualisierung abzuwarten. Für DIESEN Aufruf greift noch der
|
||||
// Default – der nächste Aufruf hat das Format.
|
||||
let missHandler = null;
|
||||
const signaledMisses = new Set();
|
||||
function setFormatMissHandler(fn) { missHandler = fn; }
|
||||
function signalMiss(key) {
|
||||
const k = String(key || '').toLowerCase();
|
||||
if (!missHandler || !k || signaledMisses.has(k)) return;
|
||||
signaledMisses.add(k);
|
||||
if (!missHandler || !k) return;
|
||||
// Kein „schon gemeldet"-Merker mehr: sonst blieb ein Key hängen, wenn die
|
||||
// Nachlade-Drossel gerade griff. Die Drossel im Handler verhindert Dauerfeuer.
|
||||
try { missHandler(k); } catch { /* egal */ }
|
||||
}
|
||||
|
||||
|
||||
@@ -25,20 +25,39 @@ const SIG_CANVAS_H = 160;
|
||||
function buildSignatureElement(svgString, x, y, scale) {
|
||||
if (!svgString || typeof svgString !== 'string') return '';
|
||||
|
||||
// Nur <path>-Elemente extrahieren (unsere Signatur-SVGs enthalten ausschließlich Paths)
|
||||
const pathRegex = /<path\b([^/]*)\/>/gi;
|
||||
// viewBox der Signatur lesen, damit beliebige Quell-SVGs (z.B. Illustrator-
|
||||
// Export mit eigener viewBox) korrekt normiert werden statt fix auf den
|
||||
// 600×160-Zeichen-Canvas. Ohne viewBox: Fallback auf den Canvas (Alt-Signaturen).
|
||||
let minx = 0;
|
||||
let miny = 0;
|
||||
let vh = SIG_CANVAS_H;
|
||||
const vbM = svgString.match(/viewBox\s*=\s*"([^"]+)"/i);
|
||||
if (vbM) {
|
||||
const p = vbM[1].trim().split(/[\s,]+/).map(Number);
|
||||
if (p.length === 4 && p[2] > 0 && p[3] > 0) { minx = p[0]; miny = p[1]; vh = p[3]; }
|
||||
}
|
||||
// Skala auf die Eigen-Höhe beziehen: Canvas-Signaturen (vh=160) unverändert,
|
||||
// hochgeladene SVGs werden auf dieselbe Zielhöhe eingepasst.
|
||||
const s = scale * (SIG_CANVAS_H / vh);
|
||||
|
||||
// Alle Zeichen-Elemente extrahieren und IMMER als Single-Stroke ausgeben:
|
||||
// fill:none + schwarzer Stroke inline; Klasse/Style/fill/stroke/width/height
|
||||
// entfernen. So gehen per <style>-Klasse (z.B. .cls-1) gesetzte Linien nicht
|
||||
// verloren (sonst fällt der Pfad auf fill:black zurück und rendert gefüllt).
|
||||
const elemRe = /<(path|polyline|line|circle|ellipse|polygon)\b([^>]*?)\/?>/gi;
|
||||
const elements = [];
|
||||
let m;
|
||||
while ((m = pathRegex.exec(svgString)) !== null) {
|
||||
// Stroke-Farbe auf Druckfarbe setzen, stroke-width relativ zur Skalierung anpassen
|
||||
const attrs = m[1]
|
||||
.replace(/stroke="[^"]*"/i, 'stroke="#000000"')
|
||||
.replace(/stroke-width="[^"]*"/i, `stroke-width="${(2.5 / scale).toFixed(2)}"`);
|
||||
elements.push(`<path${attrs}/>`);
|
||||
while ((m = elemRe.exec(svgString)) !== null) {
|
||||
const attrs = m[2].replace(/\s(?:class|style|fill|stroke|stroke-width|width|height)\s*=\s*"[^"]*"/gi, '');
|
||||
elements.push(`<${m[1]}${attrs} fill="none" stroke="#000000"/>`);
|
||||
}
|
||||
if (!elements.length) return '';
|
||||
|
||||
return `<g transform="translate(${x.toFixed(1)}, ${y.toFixed(1)}) scale(${scale.toFixed(4)})">
|
||||
// stroke-width auf der Gruppe (in Quell-Einheiten), damit nach scale ~2.5px herauskommt.
|
||||
const sw = (2.5 / s).toFixed(2);
|
||||
const tx = (x - minx * s).toFixed(1);
|
||||
const ty = (y - miny * s).toFixed(1);
|
||||
return `<g transform="translate(${tx}, ${ty}) scale(${s.toFixed(4)})" stroke-width="${sw}" stroke-linecap="round" stroke-linejoin="round">
|
||||
${elements.join('\n ')}
|
||||
</g>`;
|
||||
}
|
||||
@@ -275,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.36 : 0.6));
|
||||
|
||||
lines = wrapTextToLinesByWidth({
|
||||
text: String(text || ''),
|
||||
@@ -285,8 +306,16 @@ function generateEnvelopeSVG(text, format, type, options = {}) {
|
||||
fontSizePx,
|
||||
});
|
||||
|
||||
startX = marginLeftPx;
|
||||
startY = marginTopPx + fontSizePx;
|
||||
if (hatAbsender) {
|
||||
// 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.
|
||||
startX = Math.round(widthPx * 0.60);
|
||||
const untenBaseline = heightPx * 0.80; // unterste Zeile ~20 % über der Unterkante
|
||||
startY = Math.round(untenBaseline - Math.max(0, lines.length - 1) * lineHeightPx);
|
||||
} else {
|
||||
startX = marginLeftPx;
|
||||
startY = marginTopPx + fontSizePx;
|
||||
}
|
||||
}
|
||||
|
||||
let paths = [];
|
||||
@@ -328,6 +357,28 @@ function generateEnvelopeSVG(text, format, type, options = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
// Absender oben links – nur wenn gesetzt und kein zentrierter Freitext.
|
||||
const absender = String(options.sender || '').trim();
|
||||
if (absender && type !== 'custom') {
|
||||
// Eigene Absender-Größe (Prozent, 1 = 100%). Grundgröße = Empfänger-Grundschrift
|
||||
// (früher fix 0.62 → zu klein). Über options.senderScale prozentual einstellbar.
|
||||
const senderScale = Math.max(0.5, Math.min(2, Number(options.senderScale) || 1));
|
||||
const absSize = FONT_SIZE_PX * senderScale;
|
||||
const absLH = lineHeightPx * (absSize / fontSizePx);
|
||||
const absLines = absender.split('\n').map((l) => l.replace(/\s+$/, ''));
|
||||
const absPaths = layoutTextToPaths({
|
||||
lines: absLines,
|
||||
fontFileName: fontConfig.file,
|
||||
fontSizePx: absSize,
|
||||
// Echt oben links – unabhängig vom großen Kuvert-Rand.
|
||||
startX: Math.round(widthPx * 0.06),
|
||||
startY: Math.round(heightPx * 0.14),
|
||||
lineHeightPx: absLH,
|
||||
realisticHandwriting: options.realisticHandwriting ?? false,
|
||||
});
|
||||
paths = [...paths, ...absPaths];
|
||||
}
|
||||
|
||||
const pathElements = paths
|
||||
.map(
|
||||
(p) => `<path d="${escapeXml(p.d)}"
|
||||
|
||||
@@ -27,10 +27,11 @@ async function refreshRenderFormats() {
|
||||
}
|
||||
}
|
||||
|
||||
// Unbekanntes Format angefragt → sofort nachladen (max. alle 30 s, damit ein
|
||||
// dauerhaft unbekanntes Format kein Dauerfeuer auslöst).
|
||||
// Unbekanntes Format angefragt → sofort nachladen (max. alle 5 s, damit ein
|
||||
// dauerhaft unbekanntes Format kein Dauerfeuer auslöst). Neu in Directus angelegte
|
||||
// Formate sind damit ohne Backend-Neustart binnen weniger Sekunden aktiv.
|
||||
setFormatMissHandler((key) => {
|
||||
if (Date.now() - lastFormatRefresh < 30 * 1000) return;
|
||||
if (Date.now() - lastFormatRefresh < 5 * 1000) return;
|
||||
console.log(`[Formate] Unbekanntes Format "${key}" angefragt – lade Formate nach.`);
|
||||
refreshRenderFormats();
|
||||
});
|
||||
|
||||
@@ -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', 'envelope_sender_scale',
|
||||
'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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user