Files
skrift-backend/src/lib/page-layout.js
s4luorth 29021281bb
All checks were successful
Build & Deploy / deploy (push) Successful in 11s
Greifrand auch fuer A5 + DIN lang (lange Seite)
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>
2026-09-30 11:41:55 +02:00

190 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
// 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,
};