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, a5p: 19, a5l: 13, a6p: 14, a6l: 11, dinlang_karte: 11 }; const DEFAULT_FORMAT_LAYOUTS = { a4: { marginTopMm: 25, marginLeftMm: 20, lineHeightFactor: 1.35 }, // Greifrand IMMER an der langen (210 mm) Seite: hoch → rechts, quer/DIN lang → unten. a5p: { marginTopMm: 20, marginLeftMm: 15, marginRightMm: 14, lineHeightFactor: 1.35 }, a5l: { marginTopMm: 20, marginLeftMm: 15, marginBottomMm: 14, lineHeightFactor: 1.35 }, dinlang_karte: { marginTopMm: 8, marginLeftMm: 10, marginBottomMm: 14, lineHeightFactor: 1.2 }, // A6-Greifrand IMMER an der langen (148 mm) Seite: // • a6p (hoch, 105×148): lange Seite = rechts → marginRightMm (Breite) // • a6l (quer, 148×105): lange Seite = unten → marginBottomMm (Zeilen) a6p: { marginTopMm: 8, marginLeftMm: 8, marginRightMm: 14, lineHeightFactor: 1.3 }, a6l: { marginTopMm: 8, marginLeftMm: 8, marginBottomMm: 14, lineHeightFactor: 1.2 }, din_lang: { marginTopMm: 50, marginLeftMm: 10, lineHeightFactor: 1.2 }, c6: { marginTopMm: 60, marginLeftMm: 10, lineHeightFactor: 1.2 }, // C4 quer (Querformat-Beschriftung, maschinenseitig quer eingelegt). c4: { marginTopMm: 70, marginLeftMm: 20, lineHeightFactor: 1.2 }, }; const DEFAULT_PAGE_FORMATS = { a4: { widthMm: 210, heightMm: 297 }, a5p: { widthMm: 148, heightMm: 210 }, a5l: { widthMm: 210, heightMm: 148 }, dinlang_karte: { widthMm: 210, heightMm: 105 }, a6p: { widthMm: 105, heightMm: 148 }, a6l: { widthMm: 148, heightMm: 105 }, din_lang: { widthMm: 220, heightMm: 110 }, c6: { widthMm: 162, heightMm: 114 }, c4: { widthMm: 324, heightMm: 229 }, }; // Art des Formats (brief/umschlag/postkarte) – steuert, WAS generiert wird. const DEFAULT_KINDS = { a4: 'brief', a5p: 'brief', a5l: 'brief', a6p: 'brief', a6l: 'brief', dinlang_karte: 'brief', din_lang: 'umschlag', c6: 'umschlag', c4: '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); const mb = r.margin_bottom_mm ?? alt.marginBottomMm; if (mb != null && mb !== '') layout.marginBottomMm = Number(mb); 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, marginBottomPx: layoutConfig.marginBottomMm ? mmToPx(layoutConfig.marginBottomMm) : 0, 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, '>'); } // 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; } // Effektives Zeilenlimit: das kleinere aus (a) konfiguriertem line_limit, // gegenläufig zur Schriftgröße skaliert, und (b) geometrischer Kapazität, die // einen unteren Greifrand (marginBottomMm) frei hält. Dadurch reserviert A6 quer // (marginBottomMm=14) unten Freiraum für den rollengeführten Plotter, ohne dass // die anderen Formate (ohne Bottom-Rand) berührt werden. function getEffectiveLineLimit(format, fontScale = 1) { const fs = Math.max(0.8, Math.min(1.5, Number(fontScale) || 1)); const konfiguriert = Math.max(1, Math.round(getLineLimit(format) / fs)); const { heightPx } = getPageDimensions(format); const { marginTopPx, marginBottomPx, lineHeightPx } = getLayoutForFormat(format); const nutzbarPx = heightPx - marginTopPx - (marginBottomPx || 0); const geo = Math.max(1, Math.floor(nutzbarPx / (lineHeightPx * fs))); return Math.min(konfiguriert, geo); } module.exports = { PX_PER_MM, FONT_SIZE_PX, mmToPx, getLayoutForFormat, getPageDimensions, getLineLimit, getEffectiveLineLimit, getFormatKind, escapeXml, applyRenderFormats, setFormatMissHandler, };