lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ tests export fusszeilenbreite.test.ts

6,0 KB Rohdatei
tests/export/fusszeilenbreite.test.ts — 173 Zeilen
1 import { inflateSync } from 'node:zlib';
2 import { describe, expect, it } from 'vitest';
3 import { jsPDF } from 'jspdf';
4 import { buildProjectPdf, DEFAULT_PDF_OPTIONS } from '@/services/export/pdf';
5 import { buildSignalPlan } from '@/domain/plan/signalPlan';
6 import { validateProject } from '@/domain/validation';
7 import { createStandardIntersectionProject } from '@/domain/model/factory';
8 import type { Project } from '@/domain/model/project';
9
10 /*
11 * BEFUND 5 (Fassung 5.11.0): Die linke Fusszeile ging ohne Breitenbegrenzung an
12 * doc.text.
13 *
14 * Was der Anwender sonst erlebt: Er traegt in der Ansicht "Projekt" unter
15 * "Bearbeiter" die uebliche Buerobezeichnung statt eines Personennamens ein -
16 * das Feld hat keine Laengengrenze. Auf JEDEM Blatt der Unterlage schiebt sich
17 * die Zeile "Ausgedruckt am ... Bearbeiter: ..." dann unter die rechtsbuendige
18 * Angabe "Seite x von y" und laeuft schliesslich ueber die Blattkante hinaus.
19 * Unlesbar wird dabei ausgerechnet die Angabe, die die Vollstaendigkeit des
20 * Schriftstuecks belegt.
21 *
22 * Beide KOPFzeilen sind seit Fassung 5.9.0 (Befund 43) auf `gekuerztAufBreite`
23 * umgestellt; die Fusszeile derselben Schleife ging leer aus.
24 */
25
26 const STICHTAG = new Date('2026-01-01T12:00:00Z');
27
28 const PAGE_MARGIN = 36;
29 const BLATTBREITE = 841.89;
30 /** Rechter Rand des Satzspiegels. */
31 const SATZSPIEGEL_RECHTS = BLATTBREITE - PAGE_MARGIN;
32 /** Grundlinie der Fusszeile, von der Blattunterkante aus gemessen. */
33 const FUSSZEILE_Y = PAGE_MARGIN + 6;
34
35 /**
36 * Eine Buerobezeichnung, wie sie in einer Anordnungsunterlage vorkommt -
37 * 192 Zeichen. Ab 146 Zeichen ueberdeckt sie die Seitenangabe, ab 171 laeuft
38 * sie ueber die Blattkante.
39 */
40 const BUERO =
41 'Dipl.-Ing. Maximilian von Wertheim-Schoenbrunn, Ingenieurbuero fuer ' +
42 'Verkehrsplanung und Verkehrstechnik GmbH & Co. KG, Musterstadt am Berge, ' +
43 'Abteilung Lichtsignalanlagen und Verkehrsmanagement';
44
45 interface Stueck {
46 readonly blatt: number;
47 readonly x: number;
48 readonly y: number;
49 readonly text: string;
50 }
51
52 function entpacke(roh: string, von: number): string | null {
53 const anfang = roh.indexOf('stream', von);
54 if (anfang < 0) return null;
55 const start = anfang + (roh.startsWith('stream\r\n', anfang) ? 8 : 7);
56 const ende = roh.indexOf('endstream', start);
57 if (ende < 0) return null;
58 try {
59 return inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1');
60 } catch {
61 return null;
62 }
63 }
64
65 function textstuecke(bytes: Uint8Array): readonly Stueck[] {
66 const roh = new TextDecoder('latin1').decode(bytes);
67 const raus: Stueck[] = [];
68 let blatt = 0;
69
70 for (const treffer of roh.matchAll(/\/Type \/Page\b[^>]*?\/Contents (\d+) 0 R/g)) {
71 const objekt = roh.indexOf(`\n${treffer[1]!} 0 obj`);
72 const inhalt = objekt < 0 ? null : entpacke(roh, objekt);
73 if (inhalt === null) continue;
74 blatt += 1;
75 let x = 0;
76 let y = 0;
77 let abstand = 0;
78 for (const roheZeile of inhalt.split('\n')) {
79 const zeile = roheZeile.trim();
80 const tl = /^(-?[\d.]+) TL$/.exec(zeile);
81 if (tl) {
82 abstand = Number(tl[1]);
83 continue;
84 }
85 const td = /^(-?[\d.]+) (-?[\d.]+) Td$/.exec(zeile);
86 if (td) {
87 x = Number(td[1]);
88 y = Number(td[2]);
89 continue;
90 }
91 let rest = zeile;
92 while (rest.startsWith('T*')) {
93 y -= abstand;
94 rest = rest.slice(2).trim();
95 }
96 const tj = /^\(((?:[^()\\]|\\.)*)\)\s*Tj$/.exec(rest);
97 if (tj) raus.push({ blatt, x, y, text: tj[1]!.replace(/\\([()\\])/g, '$1') });
98 }
99 }
100 return raus;
101 }
102
103 /** Gemessene Breite - unabhaengig vom Pruefling, mit derselben Schrift. */
104 function breite(text: string): number {
105 const doc = new jsPDF({ orientation: 'landscape', unit: 'pt', format: 'a4' });
106 doc.setFont('helvetica', 'normal');
107 doc.setFontSize(8);
108 return doc.getTextWidth(text);
109 }
110
111 function unterlage(planner: string): Uint8Array {
112 const basis = createStandardIntersectionProject('Musterkreuzung', STICHTAG);
113 const projekt: Project = { ...basis, meta: { ...basis.meta, planner } };
114 const plan = buildSignalPlan(projekt);
115 return buildProjectPdf(
116 projekt,
117 plan,
118 validateProject(projekt, plan, STICHTAG),
119 DEFAULT_PDF_OPTIONS,
120 STICHTAG,
121 );
122 }
123
124 /** Die beiden Fusszeilentexte je Blatt, von links nach rechts. */
125 function fusszeilen(bytes: Uint8Array): Map<number, readonly Stueck[]> {
126 const je = new Map<number, Stueck[]>();
127 for (const s of textstuecke(bytes)) {
128 if (Math.abs(s.y - FUSSZEILE_Y) > 0.5) continue;
129 const bisher = je.get(s.blatt) ?? [];
130 bisher.push(s);
131 je.set(s.blatt, bisher);
132 }
133 for (const [blatt, stuecke] of je)
134 je.set(
135 blatt,
136 [...stuecke].sort((a, b) => a.x - b.x),
137 );
138 return je;
139 }
140
141 describe('Befund 5 - Bearbeiterangabe in der Fusszeile', () => {
142 it('haelt die Bearbeiterangabe von der Seitenangabe und vom Satzspiegelrand frei', () => {
143 const je = fusszeilen(unterlage(BUERO));
144
145 expect(je.size, 'Vorbedingung: mehrere Blaetter mit Fusszeile').toBeGreaterThan(3);
146
147 for (const [blatt, stuecke] of je) {
148 expect(stuecke.length, `Blatt ${blatt}: Bearbeiterangabe und Seitenangabe`).toBe(2);
149 const links = stuecke[0]!;
150 const seite = stuecke[1]!;
151 expect(seite.text, `Blatt ${blatt}: rechts steht die Seitenangabe`).toMatch(/^Seite \d+ von/);
152
153 const ende = links.x + breite(links.text);
154 expect(
155 ende,
156 `Blatt ${blatt}: "${links.text}" endet bei ${ende.toFixed(2)}, ` +
157 `"${seite.text}" beginnt bei ${seite.x.toFixed(2)}`,
158 ).toBeLessThanOrEqual(seite.x);
159 expect(ende, `Blatt ${blatt}: endet bei ${ende.toFixed(2)}`).toBeLessThanOrEqual(
160 SATZSPIEGEL_RECHTS,
161 );
162 }
163 });
164
165 it('laesst eine kurze Bearbeiterangabe unveraendert stehen', () => {
166 const je = fusszeilen(unterlage('O. Willerding'));
167 const erstes = je.get(1) ?? [];
168
169 expect(erstes.length).toBe(2);
170 expect(erstes[0]!.text).toContain('Bearbeiter: O. Willerding');
171 expect(erstes[0]!.text, 'kurze Angaben werden nicht gekuerzt').not.toContain('...');
172 });
173 });