import { readFileSync } from 'node:fs'; import { jsPDF } from 'jspdf'; import { describe, expect, it, vi } from 'vitest'; import { farbanteil, halbdurchsichtig } from '@/render/farbe'; import { PdfSurface } from '@/render/pdfSurface'; /** * Halbdurchsichtige Farben auf Papier. * * BEFUND AUS DEM BETRIEB (11.09.2026, mit Ausdruck): Das Lageplanblatt druckte * den Arbeitsbereich als SCHWARZEN KLOTZ und die Haltlinien als schwarze * Balken. Beide sind auf dem Bildschirm blass - `#c2410c22` und `#b3060455`, * also achtstellige Farben mit Alphakanal. Der Bildschirm kann das, jsPDF * nicht: `setDrawColor` versteht die achte Stelle nicht und faellt auf Schwarz * zurueck. Aus einer 13 Prozent deckenden Flaeche wurde die deckendste, die es * gibt, und sie verdeckte gerade die Fahrlinien, um die es auf dem Blatt geht. * * Der Fehler ist AELTER als der Arbeitsbereich - die Haltlinien traf er, seit * es das Blatt gibt. Er fiel nur nicht auf, weil ein schwarzer Strich wie ein * gewollter schwarzer Strich aussieht; eine schwarze Flaeche tut das nicht. * * ZWEITE BEHEBUNG (Fassung 5.40.0). Die erste verrechnete die Farbe ueber * Weiss und gab das Ergebnis deckend an jsPDF. Das war richtig, solange unter * der Zeichnung nur Papier liegt. Seit dem Luftbildblatt liegt dort ein Bild, * und eine deckende blasse Flaeche verdeckt es vollstaendig - derselbe Fehler, * nur umgefaerbt. jsPDF KANN Transparenz, ueber den Grafikzustand; das war * beim ersten Mal nicht nachgesehen worden. Diese Datei bewacht seitdem den * einen Mechanismus, der beide Faelle traegt. */ describe('Eine Farbe wird in Grundfarbe und Deckung zerlegt', () => { it('laesst eine gewoehnliche Farbe unangetastet und voll deckend', () => { expect(farbanteil('#1b2230')).toEqual({ grund: '#1b2230', deckung: 1 }); expect(farbanteil('#000000')).toEqual({ grund: '#000000', deckung: 1 }); }); it('trennt die achte Stelle ab', () => { expect(farbanteil('#c2410cff')).toEqual({ grund: '#c2410c', deckung: 1 }); expect(farbanteil('#c2410c00')).toEqual({ grund: '#c2410c', deckung: 0 }); }); it('liest die Deckung des Arbeitsbereichs als rund 13 Prozent', () => { const { grund, deckung } = farbanteil('#c2410c22'); expect(grund).toBe('#c2410c'); expect(deckung).toBeCloseTo(0.133, 3); }); it('liest die Deckung der Haltlinie als rund 33 Prozent', () => { // Der aeltere Teil desselben Fehlers. expect(farbanteil('#b3060455').deckung).toBeCloseTo(0.333, 3); }); it('laesst eine unbrauchbare Angabe durch, statt sie zu erfinden', () => { // Was nicht als Farbe mit Alphakanal zu lesen ist, geht unveraendert an // jsPDF - dort ist die Behandlung unbekannter Angaben zu Hause und nicht // hier. expect(farbanteil('rot')).toEqual({ grund: 'rot', deckung: 1 }); expect(farbanteil('#abc')).toEqual({ grund: '#abc', deckung: 1 }); }); it('beantwortet die Frage nach dem Alphakanal fuer die Zeichnung mit', () => { // Die Zeichenbeschreibung des Blattes braucht dieselbe Auskunft: Sie // unterlegt nur DECKENDE Striche (siehe lageplanDrawing.ts). expect(halbdurchsichtig('#c2410c22')).toBe(true); expect(halbdurchsichtig('#b3060455')).toBe(true); expect(halbdurchsichtig('#1b2230')).toBe(false); expect(halbdurchsichtig('#c2410cff')).toBe(false); }); }); /** Ein Dokument in der Einrichtung der Planunterlage. */ function dokument(): jsPDF { return new jsPDF({ orientation: 'landscape', unit: 'pt', format: 'a4' }); } function alsText(doc: jsPDF): string { return Buffer.from(doc.output('arraybuffer')).toString('latin1'); } describe('Die Deckung erreicht das PDF als Grafikzustand', () => { it('schreibt fuer den Arbeitsbereich einen Zustand mit rund 13 Prozent', () => { const doc = dokument(); const flaeche = new PdfSurface(doc, 36, 36, 700, 300); flaeche.line(10, 10, 300, 10, '#c2410c22', 12); const text = alsText(doc); expect(text).toContain('/ExtGState'); expect(text, 'Fuellung halbdurchsichtig').toMatch(/\/ca 0\.13/); expect(text, 'Strich halbdurchsichtig').toMatch(/\/CA 0\.13/); }); it('laesst ein Dokument ohne halbdurchsichtige Farbe ganz ohne Grafikzustand', () => { // Sonst traegt jede Unterlage einen Eintrag mit, den sie nicht braucht. const doc = dokument(); const flaeche = new PdfSurface(doc, 36, 36, 700, 300); flaeche.line(10, 10, 300, 10, '#1b2230', 2); flaeche.text('Zufahrt Nord', 10, 40, { size: 8, color: '#1b2230' }); flaeche.rect(10, 50, 40, 20, '#c8cfdb'); expect(alsText(doc)).not.toContain('/ExtGState'); }); it('gibt jsPDF die Farbe ohne die achte Stelle', () => { // Sonst faellt jsPDF wieder auf Schwarz zurueck - der urspruengliche // Befund. const doc = dokument(); const gesetzt: unknown[] = []; vi.spyOn(doc, 'setDrawColor').mockImplementation((...args: unknown[]) => { gesetzt.push(args[0]); return doc; }); new PdfSurface(doc, 36, 36, 700, 300).line(10, 10, 300, 10, '#c2410c22', 12); expect(gesetzt).toEqual(['#c2410c']); }); it('setzt den Grafikzustand danach wieder auf deckend zurueck', () => { /* * DIE GEFAEHRLICHE RICHTUNG DIESES UMBAUS: Bliebe der Zustand stehen, waere * der GANZE Rest des Dokuments halbdurchsichtig - Zwischenzeitenmatrix, * Signalzeitenplan, Unterschriftenfeld. Der Ausdruck saehe aus wie ein * Entwurf, und niemand kaeme auf das Lageplanblatt als Ursache. */ const doc = dokument(); const stufen: number[] = []; vi.spyOn(doc, 'setGState').mockImplementation((zustand: unknown) => { stufen.push((zustand as { opacity: number }).opacity); return doc; }); const flaeche = new PdfSurface(doc, 36, 36, 700, 300); flaeche.line(10, 10, 300, 10, '#c2410c22', 12); flaeche.line(10, 20, 300, 20, '#1b2230', 2); expect(stufen, 'gesetzt, dann zurueckgenommen').toEqual([0x22 / 255, 1]); }); it('nimmt den Zustand auch zurueck, wenn das Zeichnen fehlschlaegt', () => { const doc = dokument(); const stufen: number[] = []; vi.spyOn(doc, 'setGState').mockImplementation((zustand: unknown) => { stufen.push((zustand as { opacity: number }).opacity); return doc; }); vi.spyOn(doc, 'line').mockImplementation(() => { throw new Error('Ausgabe abgebrochen'); }); const flaeche = new PdfSurface(doc, 36, 36, 700, 300); expect(() => { flaeche.line(10, 10, 300, 10, '#c2410c22', 12); }).toThrow('Ausgabe abgebrochen'); expect(stufen.at(-1), 'zuletzt wieder deckend').toBe(1); }); }); describe('Keine Zeichenfarbe erreicht jsPDF mit Alphakanal', () => { it('gibt jede Farbstelle der PDF-Flaeche die zerlegte Grundfarbe weiter', () => { /* * ZWEI ZULAESSIGE SCHREIBWEISEN, und beide zerlegen. * * Der Regelfall ist `setXColor(grund)`: `mitDeckung` hat die Farbe zerlegt * und reicht die Grundfarbe herein. Seit dem Kreisbefehl (Fassung 5.42.0) * gibt es einen zweiten Fall - `circle` traegt ZWEI Farben, Fuellung und * Umriss, und `mitDeckung` fuehrt nur eine. Die zweite wird an Ort und * Stelle ueber `farbanteil(...).grund` zerlegt. * * Was die Regel sachlich verlangt, ist in beiden Faellen erfuellt: Keine * achtstellige Farbe erreicht jsPDF. Geprueft wird die Form, weil sie sich * pruefen laesst - und kommt eine zweite Form hinzu, gehoert sie hierher * und nicht als Ausnahmevermerk in den Quelltext. */ const quelle = readFileSync('src/render/pdfSurface.ts', 'utf8'); const ungefiltert = quelle .split('\n') .filter((zeile) => /set(Draw|Fill|Text)Color\(/.test(zeile)) .filter((zeile) => !/set(Draw|Fill|Text)Color\(grund\)/.test(zeile)) .filter((zeile) => !/set(Draw|Fill|Text)Color\(farbanteil\(\w+\)\.grund\)/.test(zeile)); expect(ungefiltert, 'Farbe ohne Zerlegung an jsPDF').toEqual([]); }); it('gibt es diese Stellen ueberhaupt', () => { const quelle = readFileSync('src/render/pdfSurface.ts', 'utf8'); const stellen = quelle.split('\n').filter((z) => /set(Draw|Fill|Text)Color\(/.test(z)); expect(stellen.length, 'keine Farbstelle gefunden').toBeGreaterThanOrEqual(5); }); it('fuehren die Zeichenbeschreibungen weiterhin achtstellige Farben', () => { // Sie sind erlaubt und noetig - der Bildschirm braucht sie. Dieser Fall // haelt fest, dass es sie gibt, damit die beiden darueber etwas zu pruefen // haben. const quelle = readFileSync('src/render/lageplanDrawing.ts', 'utf8'); expect(quelle).toMatch(/#[0-9a-f]{6}[0-9a-f]{2}'/); }); });