A6 quer: spaltenweise in ~10er-Bahnen statt pro Zeichen

Statt jede Glyphe einzeln umzusortieren (Rolle rückt pro Zeichen), wird
jetzt in senkrechten Bahnen von ~10 Zeichen Breite gearbeitet: je Zeile
~10 Zeichen als Reihe, dann eine Zeile tiefer in derselben Bahn, erst
danach die nächste Bahn. Rolle spult höchstens eine Bahnbreite zurück,
10er-Blöcke bleiben zusammen (natürlicher, schneller). Zeichenbreite wird
aus dem SVG geschätzt; Bahnbreite über ?group=N einstellbar (Default 10).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
s4luorth
2026-09-24 10:22:10 +02:00
parent 08e12180b6
commit 45e69c0c3a
2 changed files with 26 additions and 11 deletions

View File

@@ -1100,7 +1100,7 @@ const handler = (router, { services, getSchema, logger }) => {
// behalten ihre Position. Das sichtbare Ergebnis ist identisch – nur die // behalten ihre Position. Das sichtbare Ergebnis ist identisch – nur die
// Druckreihenfolge ändert sich, sodass der rollengeführte Plotter monoton in // Druckreihenfolge ändert sich, sodass der rollengeführte Plotter monoton in
// Vorschubrichtung fährt statt je Zeile die Rolle zurückzuspulen (A6 quer). // Vorschubrichtung fährt statt je Zeile die Rolle zurückzuspulen (A6 quer).
function svgSpaltenweise(svg) { function svgSpaltenweise(svg, proReihe = 10) {
// <g>…</g>-Blöcke (z. B. eingebettete Signaturen) als Ganzes schützen – deren // <g>…</g>-Blöcke (z. B. eingebettete Signaturen) als Ganzes schützen – deren
// innere Pfade dürfen NICHT umsortiert werden (sie hängen an einem eigenen // innere Pfade dürfen NICHT umsortiert werden (sie hängen an einem eigenen
// Gruppen-Transform). Signaturen stehen hinter dem Text, daher greedy bis zum // Gruppen-Transform). Signaturen stehen hinter dem Text, daher greedy bis zum
@@ -1110,17 +1110,31 @@ const handler = (router, { services, getSchema, logger }) => {
const RE = /<path\b[^>]*\/>/g; const RE = /<path\b[^>]*\/>/g;
const paths = safe.match(RE); const paths = safe.match(RE);
if (!paths || paths.length < 2) return svg; if (!paths || paths.length < 2) return svg;
const key = (p) => { const pos = (p) => {
const m = p.match(/matrix\(\s*[-\d.]+\s*,\s*[-\d.]+\s*,\s*[-\d.]+\s*,\s*[-\d.]+\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)/); const m = p.match(/matrix\(\s*[-\d.]+\s*,\s*[-\d.]+\s*,\s*[-\d.]+\s*,\s*[-\d.]+\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)/);
if (m) return [parseFloat(m[1]), parseFloat(m[2])]; if (m) return { x: parseFloat(m[1]), y: parseFloat(m[2]) };
const d = p.match(/\bd\s*=\s*"\s*M\s*([-\d.]+)[\s,]+([-\d.]+)/i); const d = p.match(/\bd\s*=\s*"\s*M\s*([-\d.]+)[\s,]+([-\d.]+)/i);
if (d) return [parseFloat(d[1]), parseFloat(d[2])]; if (d) return { x: parseFloat(d[1]), y: parseFloat(d[2]) };
return [0, 0]; return { x: 0, y: 0 };
}; };
const sorted = paths const items = paths.map((p) => ({ p, ...pos(p) }));
.map((p) => ({ p, k: key(p) })) // Zeichenbreite aus den x-Abstaenden aufeinanderfolgender Glyphen INNERHALB
.sort((a, b) => (a.k[0] - b.k[0]) || (a.k[1] - b.k[1])) // einer Zeile schaetzen (x steigt in Leserichtung; Zeilenumbruch = x faellt).
.map((x) => x.p); const gaps = [];
let prevX = null;
for (const it of items) {
if (prevX !== null && it.x - prevX > 0) gaps.push(it.x - prevX);
prevX = it.x;
}
gaps.sort((a, b) => a - b);
const zeichenBreite = gaps.length ? gaps[Math.floor(gaps.length / 2)] : 0;
if (!(zeichenBreite > 0)) return svg; // ohne Schaetzung nicht anfassen
const bahn = Math.max(1, proReihe) * zeichenBreite;
// Bahn (Spalte) primaer, darin Zeile (y) oben->unten, dann x links->rechts.
const sorted = items
.map((it, idx) => ({ it, idx, b: Math.floor(it.x / bahn) }))
.sort((a, b) => (a.b - b.b) || (a.it.y - b.it.y) || (a.it.x - b.it.x) || (a.idx - b.idx))
.map((o) => o.it.p);
let i = 0; let i = 0;
let out = safe.replace(RE, () => sorted[i++]); let out = safe.replace(RE, () => sorted[i++]);
out = out.replace(/G0/, () => bloecke[0]); out = out.replace(/G0/, () => bloecke[0]);
@@ -1146,7 +1160,8 @@ const handler = (router, { services, getSchema, logger }) => {
// Optional: SVG spaltenweise umsortieren (rollenschonend, A6 quer). // Optional: SVG spaltenweise umsortieren (rollenschonend, A6 quer).
if (ziel.endsWith('.svg') && String(req.query.reorder || '') === 'column') { if (ziel.endsWith('.svg') && String(req.query.reorder || '') === 'column') {
try { try {
const umsortiert = svgSpaltenweise(fs.readFileSync(ziel, 'utf8')); const proReihe = Math.max(1, Math.min(100, parseInt(req.query.group, 10) || 10));
const umsortiert = svgSpaltenweise(fs.readFileSync(ziel, 'utf8'), proReihe);
res.setHeader('Content-Type', 'image/svg+xml'); res.setHeader('Content-Type', 'image/svg+xml');
res.setHeader('Content-Disposition', `attachment; filename="${path.basename(ziel)}"`); res.setHeader('Content-Disposition', `attachment; filename="${path.basename(ziel)}"`);
return res.send(umsortiert); return res.send(umsortiert);

View File

@@ -1486,7 +1486,7 @@ const Board = defineComponent({
h('input', { type: 'checkbox', checked: druck.spaltenweise, onChange: (e) => { druck.spaltenweise = e.target.checked; }, style: 'width:18px;height:18px;' }), h('input', { type: 'checkbox', checked: druck.spaltenweise, onChange: (e) => { druck.spaltenweise = e.target.checked; }, style: 'width:18px;height:18px;' }),
h('span', { style: 'font-weight:600;' }, 'Rollenschonend drucken (spaltenweise)'), h('span', { style: 'font-weight:600;' }, 'Rollenschonend drucken (spaltenweise)'),
]), ]),
subdued('Für A6 quer: Zeichen werden spaltenweise (kurze Seite, oben→unten) gedruckt, damit die Rolle nicht je Zeile zurückfährt. Das sichtbare Ergebnis auf der Karte bleibt gleich – nur die Schriftstücke (nicht die Kuverts).', 'margin-top:6px;'), subdued('Für A6 quer: es werden ~10 Zeichen je Reihe geschrieben, dann in derselben Bahn eine Zeile tiefer, erst danach die nächste Spalte. So spult die Rolle nur eine Bahnbreite zurück statt je Zeile die ganze Zeile. Sichtbares Ergebnis bleibt gleich – nur Schriftstücke, nicht Kuverts.', 'margin-top:6px;'),
]) : null, ]) : null,
h('div', { style: 'margin:12px 0 4px;' }, [ h('div', { style: 'margin:12px 0 4px;' }, [
segToggle(busy ? 'queue' : druck.mode, [ segToggle(busy ? 'queue' : druck.mode, [