All checks were successful
Build & Deploy / deploy (push) Successful in 11s
A5 hoch (a5p) rechts, A5 quer (a5l) + DIN lang (dinlang_karte) unten - analog A6. Gleicher margin_right/margin_bottom-Mechanismus, Zeilen werden geometrisch gedeckelt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
190 lines
8.0 KiB
JavaScript
190 lines
8.0 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, 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, '<')
|
||
.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,
|
||
};
|