All checks were successful
Build & Deploy / deploy (push) Successful in 13s
Neues render_format-kind 'postkarte'. generatePostcardSVG setzt den Text auf die linke Kartenhälfte, Absender (klein, oben) und Empfänger (Anschriftzone unten) auf die rechte Hälfte; oben rechts bleibt das Briefmarkenfeld frei. Ein SVG je Karte (ein Plotter-Durchlauf), geschrieben als letter_NNN.svg. - page-layout: kind je Format mitführen + getFormatKind(). - svg-generator: generatePostcardSVG (Body/Absender/Empfänger über Marker [[pk_abs]]/[[pk_adr]] getrennt, damit alle Teile scriptalisiert werden). - order-/preview-/directus-controller: type 'postcard' dispatchen, Marker in der Scriptalisierung erhalten, Postkarten-Dokument aus render_format-kind bauen. - directus-service: kind aus render_formats mitladen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
158 lines
5.9 KiB
JavaScript
158 lines
5.9 KiB
JavaScript
const PX_PER_MM = 3.78; // 96dpi
|
||
const FONT_SIZE_PX = 26;
|
||
|
||
// ── Formate: Defaults (Fallback) + datengetriebener Cache ───────────────────
|
||
// Die Werte kommen zentral aus Directus (Collection render_formats) und werden
|
||
// im Speicher gehalten (stündlich / manuell aufgefrischt via applyRenderFormats).
|
||
// Ist Directus nicht erreichbar oder ein Format nicht hinterlegt, greifen die
|
||
// eingebauten Defaults – die Generierung läuft also immer.
|
||
const DEFAULT_LINE_LIMITS = { a4: 27, a6p: 14, a6l: 11 };
|
||
const DEFAULT_FORMAT_LAYOUTS = {
|
||
a4: { marginTopMm: 25, marginLeftMm: 20, lineHeightFactor: 1.35 },
|
||
a6p: { marginTopMm: 8, marginLeftMm: 8, lineHeightFactor: 1.3 },
|
||
a6l: { marginTopMm: 8, marginLeftMm: 8, marginRightMm: 14, lineHeightFactor: 1.2 },
|
||
din_lang: { marginTopMm: 50, marginLeftMm: 10, lineHeightFactor: 1.2 },
|
||
c6: { marginTopMm: 60, marginLeftMm: 10, lineHeightFactor: 1.2 },
|
||
};
|
||
const DEFAULT_PAGE_FORMATS = {
|
||
a4: { widthMm: 210, heightMm: 297 },
|
||
a6p: { widthMm: 105, heightMm: 148 },
|
||
a6l: { widthMm: 148, heightMm: 105 },
|
||
din_lang: { widthMm: 220, heightMm: 110 },
|
||
c6: { widthMm: 162, heightMm: 114 },
|
||
};
|
||
// Art des Formats (brief/umschlag/postkarte) – steuert, WAS generiert wird.
|
||
const DEFAULT_KINDS = { a4: 'brief', a6p: 'brief', a6l: 'brief', din_lang: 'umschlag', c6: 'umschlag' };
|
||
|
||
// Live-Cache (wird von applyRenderFormats ersetzt). Start = Defaults.
|
||
let LINE_LIMITS = { ...DEFAULT_LINE_LIMITS };
|
||
let FORMAT_LAYOUTS = { ...DEFAULT_FORMAT_LAYOUTS };
|
||
let PAGE_FORMATS = { ...DEFAULT_PAGE_FORMATS };
|
||
let KINDS = { ...DEFAULT_KINDS };
|
||
|
||
/**
|
||
* Übernimmt Format-Definitionen aus Directus (render_formats) in den Cache.
|
||
* Baut immer auf den Defaults auf, damit Teilangaben nichts kaputt machen.
|
||
* rows: [{ key, page_width_mm, page_height_mm, margin_top_mm, margin_left_mm,
|
||
* margin_right_mm, line_height_factor, line_limit }]
|
||
*/
|
||
function applyRenderFormats(rows) {
|
||
const pf = { ...DEFAULT_PAGE_FORMATS };
|
||
const fl = { ...DEFAULT_FORMAT_LAYOUTS };
|
||
const ll = { ...DEFAULT_LINE_LIMITS };
|
||
const kn = { ...DEFAULT_KINDS };
|
||
for (const r of Array.isArray(rows) ? rows : []) {
|
||
const k = String(r.key || '').toLowerCase();
|
||
if (!k) continue;
|
||
if (r.kind) kn[k] = String(r.kind).toLowerCase();
|
||
if (r.page_width_mm && r.page_height_mm) {
|
||
pf[k] = { widthMm: Number(r.page_width_mm), heightMm: Number(r.page_height_mm) };
|
||
}
|
||
const alt = fl[k] || {};
|
||
const layout = {
|
||
marginTopMm: Number(r.margin_top_mm ?? alt.marginTopMm ?? 25),
|
||
marginLeftMm: Number(r.margin_left_mm ?? alt.marginLeftMm ?? 20),
|
||
lineHeightFactor: Number(r.line_height_factor ?? alt.lineHeightFactor ?? 1.35),
|
||
};
|
||
const mr = r.margin_right_mm ?? alt.marginRightMm;
|
||
if (mr != null && mr !== '') layout.marginRightMm = Number(mr);
|
||
fl[k] = layout;
|
||
if (r.line_limit != null && r.line_limit !== '') ll[k] = Number(r.line_limit);
|
||
}
|
||
PAGE_FORMATS = pf;
|
||
FORMAT_LAYOUTS = fl;
|
||
LINE_LIMITS = ll;
|
||
KINDS = kn;
|
||
}
|
||
|
||
// Art eines Formats (brief/umschlag/postkarte). Unbekannt → 'brief'.
|
||
function getFormatKind(format) {
|
||
return KINDS[String(format).toLowerCase()] || 'brief';
|
||
}
|
||
|
||
// mm in Pixel umrechnen
|
||
function mmToPx(mm) {
|
||
return mm * PX_PER_MM;
|
||
}
|
||
|
||
// ── Refresh bei unbekanntem Format ──────────────────────────────────────────
|
||
// Wird ein Format angefragt, das (noch) nicht im Cache liegt, meldet page-layout
|
||
// das einmalig an einen Handler (den der Server auf refreshRenderFormats setzt).
|
||
// So werden neu in Directus angelegte Formate sofort nachgeladen, ohne die
|
||
// stündliche Aktualisierung abzuwarten. Für DIESEN Aufruf greift noch der
|
||
// Default – der nächste Aufruf hat das Format.
|
||
let missHandler = null;
|
||
function setFormatMissHandler(fn) { missHandler = fn; }
|
||
function signalMiss(key) {
|
||
const k = String(key || '').toLowerCase();
|
||
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 */ }
|
||
}
|
||
|
||
// Layout für Format holen
|
||
function getLayoutForFormat(format) {
|
||
const normalizedFormat = String(format).toLowerCase();
|
||
const layoutConfig = FORMAT_LAYOUTS[normalizedFormat];
|
||
if (!layoutConfig) signalMiss(normalizedFormat);
|
||
|
||
if (!layoutConfig) {
|
||
// Fallback auf a4
|
||
return {
|
||
marginTopPx: mmToPx(FORMAT_LAYOUTS.a4.marginTopMm),
|
||
marginLeftPx: mmToPx(FORMAT_LAYOUTS.a4.marginLeftMm),
|
||
lineHeightPx: FONT_SIZE_PX * FORMAT_LAYOUTS.a4.lineHeightFactor,
|
||
};
|
||
}
|
||
|
||
return {
|
||
marginTopPx: mmToPx(layoutConfig.marginTopMm),
|
||
marginLeftPx: mmToPx(layoutConfig.marginLeftMm),
|
||
marginRightPx: layoutConfig.marginRightMm ? mmToPx(layoutConfig.marginRightMm) : null,
|
||
lineHeightPx: FONT_SIZE_PX * layoutConfig.lineHeightFactor,
|
||
};
|
||
}
|
||
|
||
// Seiten-Dimensionen holen
|
||
function getPageDimensions(format) {
|
||
const normalizedFormat = String(format).toLowerCase();
|
||
if (!PAGE_FORMATS[normalizedFormat]) signalMiss(normalizedFormat);
|
||
const dimensions = PAGE_FORMATS[normalizedFormat] || PAGE_FORMATS.a4;
|
||
|
||
return {
|
||
widthMm: dimensions.widthMm,
|
||
heightMm: dimensions.heightMm,
|
||
widthPx: mmToPx(dimensions.widthMm),
|
||
heightPx: mmToPx(dimensions.heightMm),
|
||
};
|
||
}
|
||
|
||
// XML Escaping
|
||
function escapeXml(str) {
|
||
return String(str)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>');
|
||
}
|
||
|
||
// Zeilenlimit für Format holen
|
||
function getLineLimit(format) {
|
||
const normalizedFormat = String(format).toLowerCase();
|
||
if (!LINE_LIMITS[normalizedFormat]) signalMiss(normalizedFormat);
|
||
return LINE_LIMITS[normalizedFormat] || LINE_LIMITS.a4;
|
||
}
|
||
|
||
module.exports = {
|
||
PX_PER_MM,
|
||
FONT_SIZE_PX,
|
||
mmToPx,
|
||
getLayoutForFormat,
|
||
getPageDimensions,
|
||
getLineLimit,
|
||
getFormatKind,
|
||
escapeXml,
|
||
applyRenderFormats,
|
||
setFormatMissHandler,
|
||
};
|