Filipe hat den Bauplan "Willkommen & Anleitung" geschickt und um eine
vollstaendige Pruefung gebeten. Beim Abgleich der Kachelzahlen stimmte
eine Zeile nicht:
Rolle Bauplan App
Community 14 + Willkommen 15 ok
Modi 29 + Willkommen 30 ok
Linke Hand 29 + Willkommen 34 NEIN
Rechte Hand 34 + Willkommen 35 ok
Am laufenden Server nachgemessen: Die linke Hand sah vier Kacheln, die
ins Leere fuehren -- Talente, Personen & Zugaenge, Wer sieht was,
Vertraulich melden. Alle vier antworten mit 302 und leiten sie weg;
aufgaben.html als Gegenprobe mit 200. Der Bauplan spricht ihr genau
diese vier ab. Der Plan hatte recht, die Kachelliste nicht.
WARUM ES PASSIERT IST: Ihre Liste entsteht aus der der rechten Hand
MINUS einer Kachel (Dein Team). Jede Seite, die spaeter nur fuer die
rechte Hand freigegeben wurde, kam damit still bei ihr mit -- die
Rechtetafel wusste es, die Kachelliste nicht.
ABGELEITET STATT GEPFLEGT: Eine Kachel, deren Seite die Rolle nicht
oeffnen darf, erscheint nicht mehr. Gefragt wird dieselbe Tafel, die
auch die Seite selbst schuetzt (`darfSeite`, in workspace.js schon
importiert). Damit kann diese Fehlersorte bei KEINER Rolle
wiederkommen, nicht nur bei dieser einen.
Nachher hat die linke Hand 30 Kacheln -- genau die Zahl, die im
Bauplan steht ("Du siehst dieselben 30 Kacheln wie ein Modi"). Alle
vier Zahlen des Plans stimmen damit. Die uebrigen Rollen sind
unveraendert (gemessen: gast 15, modi 30, hand 35, admin 35).
pruef-sackgassen +1, und die Ergaenzung ist der eigentliche Punkt:
Die Datei heisst "Sackgassen" und hat das NICHT gefunden, weil sie
ihre Rollen von der Anmeldewand nimmt -- und die steht auf der
Agenturadresse. Modi, linke Hand und Gast gibt es dort nicht; sie
wurden nie geprueft. Eine Pruefung, die ihre Faelle aus einer Liste
nimmt, prueft nur, was auf der Liste steht.
Die neue Zeile kommt ohne Browser und ohne Anmeldung aus und fragt
die beiden Stellen, die es wissen: `bereicheFuer` und `darfSeite`.
Damit sind alle Rollen BEIDER Haeuser erfasst. Gegenprobe gefahren:
ohne die Korrektur nennt sie alle vier beim Namen, mit ihr sind
145 Kacheln sauber.
Gegengemessen: pruef-rollen 452, pruef-rechtetafel 19,
pruef-sackgassen 13 -- 0 Fehler.
Co-Authored-By: Claude Opus 5 <[email protected]>
175 lines
6.2 KiB
JavaScript
175 lines
6.2 KiB
JavaScript
/* =====================================================================
|
|
PDF-Text herausholen -- ohne fremde Bibliothek.
|
|
|
|
Gebaut am 06.10.2026, weil auf diesem Rechner weder poppler noch
|
|
eine PDF-Bibliothek liegt und ein Bauplan gelesen werden musste.
|
|
|
|
WIE ES ARBEITET: Alle Stroeme entpacken, die Seiten in ihrer
|
|
Reihenfolge einsammeln, und den Text ueber die ToUnicode-Tabellen
|
|
der jeweiligen Schrift zurueckuebersetzen. Ohne diese Tabellen
|
|
kaeme Buchstabensalat heraus -- die Schriften sind Teilmengen, in
|
|
denen <54> irgendein Zeichen bedeuten kann.
|
|
|
|
AUFRUF: node tools/pdf-text.mjs <datei.pdf> [ziel.txt]
|
|
===================================================================== */
|
|
|
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
import { inflateSync } from "node:zlib";
|
|
|
|
const quelle = process.argv[2];
|
|
const ziel = process.argv[3] || null;
|
|
if (!quelle) { console.error("Aufruf: node tools/pdf-text.mjs <datei.pdf> [ziel.txt]"); process.exit(2); }
|
|
|
|
const roh = readFileSync(quelle);
|
|
const text = roh.toString("latin1");
|
|
|
|
/* ---- 1. Alle Objekte einsammeln ------------------------------------ */
|
|
const objekte = new Map(); // nummer -> { dict, daten }
|
|
{
|
|
const re = /(\d+)\s+(\d+)\s+obj\b/g;
|
|
let m;
|
|
while ((m = re.exec(text))) {
|
|
const nr = Number(m[1]);
|
|
const ab = m.index + m[0].length;
|
|
const bis = text.indexOf("endobj", ab);
|
|
if (bis < 0) continue;
|
|
const koerper = text.slice(ab, bis);
|
|
let daten = null;
|
|
const sm = /(^|[^d])stream\r?\n/.exec(koerper);
|
|
if (sm) {
|
|
const sAb = ab + sm.index + sm[0].length;
|
|
const sBis = text.indexOf("endstream", sAb);
|
|
if (sBis > 0) {
|
|
try { daten = inflateSync(roh.subarray(sAb, sBis)); }
|
|
catch { daten = roh.subarray(sAb, sBis); }
|
|
}
|
|
}
|
|
objekte.set(nr, { dict: sm ? koerper.slice(0, sm.index) : koerper, daten });
|
|
}
|
|
}
|
|
|
|
/* ---- 2. ToUnicode-Tabellen ----------------------------------------- */
|
|
const tabellen = new Map(); // objektnummer -> Map(code -> zeichen)
|
|
|
|
function tabelleLesen(nr) {
|
|
if (tabellen.has(nr)) return tabellen.get(nr);
|
|
const o = objekte.get(nr);
|
|
const karte = new Map();
|
|
tabellen.set(nr, karte);
|
|
if (!o?.daten) return karte;
|
|
const t = o.daten.toString("latin1");
|
|
|
|
/* Einzelzuordnungen: <01> <0041> */
|
|
for (const block of t.match(/beginbfchar([\s\S]*?)endbfchar/g) || []) {
|
|
const re = /<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]+)>/g; let m;
|
|
while ((m = re.exec(block))) karte.set(parseInt(m[1], 16), ausHex(m[2]));
|
|
}
|
|
/* Bereiche: <01> <05> <0041> oder <01> <03> [<0041> <0042> <0043>] */
|
|
for (const block of t.match(/beginbfrange([\s\S]*?)endbfrange/g) || []) {
|
|
const re = /<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]+)>\s*(?:<([0-9A-Fa-f]+)>|\[([\s\S]*?)\])/g;
|
|
let m;
|
|
while ((m = re.exec(block))) {
|
|
const von = parseInt(m[1], 16), bis = parseInt(m[2], 16);
|
|
if (m[3]) {
|
|
const basis = parseInt(m[3], 16);
|
|
for (let c = von; c <= bis && c - von < 65536; c++) {
|
|
karte.set(c, String.fromCodePoint(basis + (c - von)));
|
|
}
|
|
} else if (m[4]) {
|
|
const liste = m[4].match(/<([0-9A-Fa-f]+)>/g) || [];
|
|
liste.forEach((h, i) => karte.set(von + i, ausHex(h.slice(1, -1))));
|
|
}
|
|
}
|
|
}
|
|
return karte;
|
|
}
|
|
|
|
function ausHex(h) {
|
|
let s = "";
|
|
for (let i = 0; i + 3 < h.length + 1; i += 4) {
|
|
const c = parseInt(h.slice(i, i + 4), 16);
|
|
if (!Number.isNaN(c)) s += String.fromCharCode(c);
|
|
}
|
|
return s;
|
|
}
|
|
|
|
/* ---- 3. Seiten in der richtigen Reihenfolge ------------------------ */
|
|
const seiten = [];
|
|
for (const [nr, o] of objekte) {
|
|
if (!/\/Type\s*\/Page[^s]/.test(o.dict)) continue;
|
|
seiten.push({ nr, dict: o.dict });
|
|
}
|
|
seiten.sort((a, b) => a.nr - b.nr);
|
|
|
|
/* ---- 4. Text je Seite ---------------------------------------------- */
|
|
const alles = [];
|
|
for (const [i, seite] of seiten.entries()) {
|
|
/* Welche Schrift heisst wie? /F5 12 0 R */
|
|
const schriften = new Map();
|
|
const fm = /\/Font\s*<<([\s\S]*?)>>/.exec(seite.dict);
|
|
if (fm) {
|
|
const re = /\/(\w+)\s+(\d+)\s+\d+\s+R/g; let m;
|
|
while ((m = re.exec(fm[1]))) {
|
|
const fo = objekte.get(Number(m[2]));
|
|
const tu = fo && /\/ToUnicode\s+(\d+)\s+\d+\s+R/.exec(fo.dict);
|
|
schriften.set("/" + m[1], tu ? tabelleLesen(Number(tu[1])) : new Map());
|
|
}
|
|
}
|
|
|
|
/* Inhalt -- kann ein Verweis oder eine Liste sein. */
|
|
const cm = /\/Contents\s+(?:(\d+)\s+\d+\s+R|\[([^\]]*)\])/.exec(seite.dict);
|
|
let inhalt = "";
|
|
if (cm?.[1]) inhalt = objekte.get(Number(cm[1]))?.daten?.toString("latin1") || "";
|
|
else if (cm?.[2]) {
|
|
for (const r of cm[2].match(/(\d+)\s+\d+\s+R/g) || []) {
|
|
inhalt += (objekte.get(Number(r))?.daten?.toString("latin1") || "") + "\n";
|
|
}
|
|
}
|
|
|
|
let karte = new Map();
|
|
let zeile = "";
|
|
const raus = [];
|
|
/* Jede Textanweisung der Reihe nach. `Td`/`TD`/`Tm`/`T*` mit
|
|
Zeilenvorschub trennt Zeilen -- sonst klebt die ganze Seite
|
|
zusammen und kein Mensch liest es. */
|
|
const re = /\/(\w+)\s+[\d.]+\s+Tf|<([0-9A-Fa-f\s]*)>\s*Tj|\[([\s\S]*?)\]\s*TJ|([-\d.]+)\s+([-\d.]+)\s+Td|T\*|ET/g;
|
|
let m;
|
|
let letztesY = null;
|
|
while ((m = re.exec(inhalt))) {
|
|
if (m[1]) { karte = schriften.get("/" + m[1]) || new Map(); continue; }
|
|
if (m[2] !== undefined) { zeile += entziffern(m[2], karte); continue; }
|
|
if (m[3] !== undefined) {
|
|
for (const st of m[3].match(/<([0-9A-Fa-f\s]*)>/g) || []) {
|
|
zeile += entziffern(st.slice(1, -1), karte);
|
|
}
|
|
/* Grosse Rueckschritte im TJ sind Wortabstaende. */
|
|
continue;
|
|
}
|
|
if (m[4] !== undefined) {
|
|
const y = Number(m[5]);
|
|
if (letztesY !== null && Math.abs(y) > 0.1) { raus.push(zeile); zeile = ""; }
|
|
letztesY = y;
|
|
continue;
|
|
}
|
|
/* T* oder ET */
|
|
if (zeile.trim()) { raus.push(zeile); zeile = ""; }
|
|
}
|
|
if (zeile.trim()) raus.push(zeile);
|
|
alles.push(`\n═══ Seite ${i + 1} ═══\n` + raus.join("\n"));
|
|
}
|
|
|
|
function entziffern(hex, karte) {
|
|
const h = hex.replace(/\s+/g, "");
|
|
let s = "";
|
|
for (let i = 0; i + 1 < h.length + 1; i += 2) {
|
|
const c = parseInt(h.slice(i, i + 2), 16);
|
|
if (Number.isNaN(c)) continue;
|
|
s += karte.get(c) ?? "";
|
|
}
|
|
return s;
|
|
}
|
|
|
|
const fertig = alles.join("\n");
|
|
if (ziel) { writeFileSync(ziel, fertig, "utf8"); console.log(`${seiten.length} Seiten -> ${ziel} (${fertig.length} Zeichen)`); }
|
|
else console.log(fertig);
|