feat: F4.8d the documentation mode's report as a PDF, as Java's
ReportRun reads Java's report back with iText: every piece of text with its place, font, size and colour. The editor lays the report out by iText's rules (A4, leading 1.5, loose chunks in the current line, tables 80 % wide with padding 2, fixed traffic columns, page breaks) and draws it with pdf-lib in the PDF's own Helvetica, widths without kerning as iText measures. All device pages of Internet_Komplett, the first page and the data exchange of einfaches_rechnernetz_komplett match Java piece by piece; Java's iText reads our PDF with the same text in the same places. The sidebar gains Java's "Report"; the frames come from the room by a short subscription. Not adopted: true/false for DHCP (Java's own Ja/Nein), hash table orders, an image running off the page, the report writing back into the model. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1731,3 +1731,53 @@ Gemessen (04.10.2026):
|
||||
Falle der Sonde: Das Druckpanel nimmt Javas Geräte-Etiketten aus der
|
||||
Oberfläche (eine Swing-Komponente hat nur einen Elternteil); erst
|
||||
`updateViewport` legt sie zurück.
|
||||
|
||||
### Dokumentation: „Report“ (F4.8d)
|
||||
|
||||
`ReportRun` öffnet ein Projekt wie Javas Menü, lässt es auf Wunsch eine
|
||||
Weile laufen (alle Knoten `starten`, dann `beenden`; der Lauscher behält die
|
||||
Rahmen wie nach einer Simulation) und schreibt mit
|
||||
`ReportGenerator.generateReport` Javas PDF. Das liest es mit iTexts eigenem
|
||||
Parser zurück: jedes Textstück mit Ort, Schrift, Größe (aus dem privaten
|
||||
Grafikzustand per Reflection) und Farbe, jedes Bild mit Ort und Größe, Titel
|
||||
und Ersteller. Drei Projekte (`einfaches_rechnernetz_komplett` nach 3 s
|
||||
Lauf, `Internet_Komplett_…_DE`, die Fixture `documentation.fls`) und ein
|
||||
leeres (`packages/editor/test/report.java.json`). Mit `--read` liest es ein
|
||||
beliebiges PDF allein, mit SHA-256: so liest Javas iText den Bericht des
|
||||
Editors (`report.ours.java.json`).
|
||||
|
||||
Gemessen (04.10.2026):
|
||||
|
||||
* A4, 36 Rand; Titel Helvetica-Bold 18 in (30, 50, 120), mittig; darunter
|
||||
der Pfad in Helvetica 8; „1. Übersicht“ mit Javas Bild in Seitenbreite,
|
||||
eine halbe Zeile unter der Überschrift; neue Seite „2. Komponenten“; neue
|
||||
Seite „3. Datenaustausch“, nur wenn der Lauscher Rahmen hat.
|
||||
* Überschriften Helvetica-Bold 14/12/10 in Dunkelgrau (64), Ebene 1 und 2
|
||||
nummeriert („2.1. “), Ebene 3 nicht. Zeilenabstand das 1,5-Fache der
|
||||
Schrift; lose Textstücke (`Chunk`) laufen in derselben Zeile mit dem
|
||||
Abstand des letzten Absatzes, bis `Chunk.NEWLINE` sie beendet.
|
||||
* Tabellen: 80 % der Breite, links, Spalten im Verhältnis (180:360,
|
||||
3 × 180, 180:60:260, 4 × 180); Zellen mit 2 Innenabstand, Zeilen so weit
|
||||
auseinander wie die Schrift groß ist; eine Zeile, die unter den Rand
|
||||
reicht, beginnt die nächste Seite (ohne Kopfzeile). Datenaustausch: feste
|
||||
Spalten 20/60/80/80/40/50/180, mittig, Helvetica 8, Kopf hellblau (230,
|
||||
240, 255), jede zweite Zeile grau (240).
|
||||
* Je Gerät mit Karten (nicht Switch, nicht Modem): Gateway, DNS-Server, bei
|
||||
Rechnern „DHCP zur Konfiguration verwenden“ als `true`/`false`; Karten;
|
||||
bei Rechnern „Installiert: “ und die Programme (oder „-“), in der
|
||||
Reihenfolge einer HashMap; mit DNS-Server dessen Einträge;
|
||||
Weiterleitungstabelle (nur Zeilen mit einer Adresse als Ziel); beim
|
||||
Heimrouter die Portweiterleitung, wenn eine Zeile ein bekanntes Protokoll
|
||||
hat. Datenaustausch je Karte mit Rahmen („Name - Adresse“), die Zeilen
|
||||
von `Lauscher.getDaten(id, true, 0)`, in der Reihenfolge einer HashMap.
|
||||
* Leeres Projekt: Titel ohne Namen, kein Pfad; das leere 20 × 35-Bild wird
|
||||
auf Seitenbreite 915 hoch und läuft über den Rand einer eigenen Seite.
|
||||
* Java-Fehler am Rande: der Bericht schreibt die Protokolle der
|
||||
Portweiterleitung („6“ → „TCP“) ins Modell zurück und gibt eine
|
||||
Debug-Zeile auf der Konsole aus.
|
||||
|
||||
Fallen der Sonde: iText legt Absätze und Tabellen in eigene Ebenen, die
|
||||
Zeichenreihenfolge im PDF ist darum nicht die der Seite (der Test vergleicht
|
||||
nach Ort). Die Uhrzeiten des Datenaustauschs sind Javas Wanduhr; der Test
|
||||
nimmt Javas Zeilen aus Javas Seiten, damit der Satz verglichen wird, nicht
|
||||
der Lauf.
|
||||
|
||||
@@ -21,7 +21,7 @@ if [ ! -f "$upstream/target/filius.jar" ]; then
|
||||
fi
|
||||
classpath="$upstream/target/filius.jar:$upstream/target/lib/*"
|
||||
mkdir -p "$here/.build"
|
||||
javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$here/src/ExampleRun.java" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java" "$here/src/KonfigCases.java" "$here/src/EmailRun.java" "$here/src/MailCases.java" "$here/src/EmailClientRun.java" "$here/src/TerminalCases.java" "$here/src/TerminalRun.java" "$here/src/FileCases.java" "$here/src/GnutellaRun.java" "$here/src/NewDeviceRun.java" "$here/src/SheetRun.java" "$here/src/TerminalWindowRun.java" "$here/src/InstallRun.java" "$here/src/BrowserWindowRun.java" "$here/src/MailWindowRun.java" "$here/src/SaveAfterRunRun.java" "$here/src/TextEditorWindowRun.java" "$here/src/FileExplorerWindowRun.java" "$here/src/EchoWindowRun.java" "$here/src/DnsWindowRun.java" "$here/src/FirewallWindowRun.java" "$here/src/GnutellaWindowRun.java" "$here/src/ViewersRun.java" "$here/src/ExchangeWindowRun.java" "$here/src/DocuRun.java" "$here/src/ExportRun.java"
|
||||
javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$here/src/ExampleRun.java" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java" "$here/src/KonfigCases.java" "$here/src/EmailRun.java" "$here/src/MailCases.java" "$here/src/EmailClientRun.java" "$here/src/TerminalCases.java" "$here/src/TerminalRun.java" "$here/src/FileCases.java" "$here/src/GnutellaRun.java" "$here/src/NewDeviceRun.java" "$here/src/SheetRun.java" "$here/src/TerminalWindowRun.java" "$here/src/InstallRun.java" "$here/src/BrowserWindowRun.java" "$here/src/MailWindowRun.java" "$here/src/SaveAfterRunRun.java" "$here/src/TextEditorWindowRun.java" "$here/src/FileExplorerWindowRun.java" "$here/src/EchoWindowRun.java" "$here/src/DnsWindowRun.java" "$here/src/FirewallWindowRun.java" "$here/src/GnutellaWindowRun.java" "$here/src/ViewersRun.java" "$here/src/ExchangeWindowRun.java" "$here/src/DocuRun.java" "$here/src/ExportRun.java" "$here/src/ReportRun.java"
|
||||
# FILIUS writes its settings below $HOME; keep them out of the real one.
|
||||
mkdir -p "$here/.build/home"
|
||||
# Removed first: a crashed run must not leave the previous result standing.
|
||||
@@ -510,6 +510,24 @@ HOME="$here/.build/home" xvfb-run -a java -Duser.language=de -Duser.country=DE -
|
||||
"$root/packages/doc/test/fixtures/documentation.fls" "$upstream/beispiele/modem_beispiel.fls" 2>&1 | grep -v 'logback\|^$\|^\s*at ' || true
|
||||
test -s "$output"
|
||||
|
||||
# FILIUS' „Report“ (ReportRun): the PDF Java writes of three projects (one run
|
||||
# for 3 s, its packet log in it) and an empty one, read back with iText —
|
||||
# every piece of text with its place, font, size and colour
|
||||
# (packages/editor/test/report.test.ts). Then the editor's own report, which
|
||||
# that test writes with UPDATE_REPORT=1, read back the same way, with its SHA-256.
|
||||
output="$root/packages/editor/test/report.java.json"
|
||||
rm -f "$output"
|
||||
HOME="$here/.build/home" xvfb-run -a java -Duser.language=de -Duser.country=DE -cp "$classpath:$here/.build" ReportRun \
|
||||
"$output" "$here/.build/report" "$upstream/beispiele/einfaches_rechnernetz_komplett.fls:3000" \
|
||||
"$upstream/beispiele/Internet_Komplett_mit_eMail_Webserver_Intranet_Portforwarding_Firewall_DHCP_DE.fls" \
|
||||
"$root/packages/doc/test/fixtures/documentation.fls" 2>&1 | grep -v 'logback\|^$\|^\s*at \|^TCP ' || true
|
||||
test -s "$output"
|
||||
(cd "$root" && UPDATE_REPORT=1 npx vitest run packages/editor/test/report.test.ts >/dev/null 2>&1 || true)
|
||||
output="$root/packages/editor/test/report.ours.java.json"
|
||||
rm -f "$output"
|
||||
HOME="$here/.build/home" java -cp "$classpath:$here/.build" ReportRun --read "$output" "$here/.build/report-ours.pdf" 2>&1 | grep -v 'logback\|^$' || true
|
||||
test -s "$output"
|
||||
|
||||
# FILIUS' own examples as Java FILIUS runs them, opened and started as FILIUS
|
||||
# does (ExampleRun): the acceptance of F2 (packages/fls/test/examplesJava.test.ts)
|
||||
# and the classics of F3 (web, email, gnutella; packages/fls/test/classicsJava.test.ts).
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
// FILIUS' „Report“ in the documentation mode measured (ReportGenerator):
|
||||
// each project opened as the menu opens it, run for a while when asked (its
|
||||
// frames in FILIUS' packet log, the Lauscher, as after a simulation), then
|
||||
// the PDF Java writes, read back with iText's own parser: its pages and
|
||||
// their size, every piece of text with its place, font, size and colour,
|
||||
// every image with its place and size, the document's title and creator;
|
||||
// and what the report was made of (the Lauscher's cards, the nodes' cards).
|
||||
// An empty project too (no file). The reference for the editor's report
|
||||
// (packages/editor/test/report.test.ts).
|
||||
//
|
||||
// Usage: java -cp filius.jar:lib/*:classes ReportRun <out.json> <pdf folder> <file.fls>[:<milliseconds to run>]...
|
||||
// java -cp filius.jar:lib/*:classes ReportRun --read <out.json> <file.pdf>... (a PDF read back alone, with its SHA-256)
|
||||
|
||||
import com.itextpdf.text.BaseColor;
|
||||
import com.itextpdf.text.pdf.PdfReader;
|
||||
import com.itextpdf.text.pdf.parser.GraphicsState;
|
||||
import com.itextpdf.text.pdf.parser.ImageRenderInfo;
|
||||
import com.itextpdf.text.pdf.parser.Matrix;
|
||||
import com.itextpdf.text.pdf.parser.PdfReaderContentParser;
|
||||
import com.itextpdf.text.pdf.parser.RenderListener;
|
||||
import com.itextpdf.text.pdf.parser.TextRenderInfo;
|
||||
import com.itextpdf.text.pdf.parser.Vector;
|
||||
import filius.gui.GUIContainer;
|
||||
import filius.gui.documentation.ReportGenerator;
|
||||
import filius.gui.netzwerksicht.GUIKnotenItem;
|
||||
import filius.hardware.NetzwerkInterface;
|
||||
import filius.hardware.knoten.InternetKnoten;
|
||||
import filius.rahmenprogramm.Information;
|
||||
import filius.rahmenprogramm.SzenarioVerwaltung;
|
||||
import filius.rahmenprogramm.nachrichten.Lauscher;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.swing.SwingUtilities;
|
||||
|
||||
public class ReportRun {
|
||||
static float round(float value) {
|
||||
return Math.round(value * 100) / 100f;
|
||||
}
|
||||
|
||||
static String colour(BaseColor color) {
|
||||
return color == null ? null : color.getRed() + "," + color.getGreen() + "," + color.getBlue();
|
||||
}
|
||||
|
||||
/** Every piece of text and every image of a page, in the order the page draws them. */
|
||||
static class Listener implements RenderListener {
|
||||
final List<Object> texts = new ArrayList<>();
|
||||
final List<Object> images = new ArrayList<>();
|
||||
|
||||
public void beginTextBlock() {}
|
||||
|
||||
public void endTextBlock() {}
|
||||
|
||||
public void renderText(TextRenderInfo info) {
|
||||
// Spaces alone (iText writes the ends of lines and Chunk.NEWLINE so) carry nothing.
|
||||
if (info.getText().trim().isEmpty()) return;
|
||||
try {
|
||||
Field field = TextRenderInfo.class.getDeclaredField("gs");
|
||||
field.setAccessible(true);
|
||||
GraphicsState state = (GraphicsState) field.get(info);
|
||||
Vector start = info.getBaseline().getStartPoint();
|
||||
// x|y|font|size|colour|text, one string a piece, so that the file stays small.
|
||||
texts.add(round(start.get(Vector.I1)) + "|" + round(start.get(Vector.I2)) + "|" + info.getFont().getPostscriptFontName() + "|"
|
||||
+ round(state.getFontSize()) + "|" + colour(info.getFillColor()) + "|" + info.getText());
|
||||
} catch (ReflectiveOperationException error) {
|
||||
throw new IllegalStateException(error);
|
||||
}
|
||||
}
|
||||
|
||||
public void renderImage(ImageRenderInfo info) {
|
||||
Matrix matrix = info.getImageCTM();
|
||||
Map<String, Object> image = new LinkedHashMap<>();
|
||||
image.put("x", round(matrix.get(Matrix.I31)));
|
||||
image.put("y", round(matrix.get(Matrix.I32)));
|
||||
image.put("width", round(matrix.get(Matrix.I11)));
|
||||
image.put("height", round(matrix.get(Matrix.I22)));
|
||||
try {
|
||||
image.put("pixels", List.of(info.getImage().getBufferedImage().getWidth(), info.getImage().getBufferedImage().getHeight()));
|
||||
} catch (Exception error) {
|
||||
image.put("pixels", null);
|
||||
}
|
||||
images.add(image);
|
||||
}
|
||||
}
|
||||
|
||||
static Map<String, Object> read(String pdf) throws Exception {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
PdfReader reader = new PdfReader(pdf);
|
||||
Map<String, String> info = reader.getInfo();
|
||||
result.put("title", info.get("Title"));
|
||||
result.put("creator", info.get("Creator"));
|
||||
List<Object> pages = new ArrayList<>();
|
||||
PdfReaderContentParser parser = new PdfReaderContentParser(reader);
|
||||
for (int number = 1; number <= reader.getNumberOfPages(); number++) {
|
||||
Listener listener = parser.processContent(number, new Listener());
|
||||
Map<String, Object> page = new LinkedHashMap<>();
|
||||
page.put("size", List.of(round(reader.getPageSize(number).getWidth()), round(reader.getPageSize(number).getHeight())));
|
||||
page.put("texts", listener.texts);
|
||||
page.put("images", listener.images);
|
||||
pages.add(page);
|
||||
}
|
||||
result.put("pages", pages);
|
||||
reader.close();
|
||||
return result;
|
||||
}
|
||||
|
||||
static Map<String, Object> measure(String name, File pdfFolder) throws Exception {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
// What the report is made of: the Lauscher's cards and every card of the nodes.
|
||||
List<Object> packetLogCards = new ArrayList<>();
|
||||
for (String id : Lauscher.getLauscher().getInterfaceIDs()) {
|
||||
Map<String, Object> card = new LinkedHashMap<>();
|
||||
card.put("mac", id);
|
||||
card.put("rows", Lauscher.getLauscher().getDaten(id, true, 0).length);
|
||||
packetLogCards.add(card);
|
||||
}
|
||||
result.put("packetLogCards", packetLogCards);
|
||||
List<Object> nodes = new ArrayList<>();
|
||||
for (GUIKnotenItem item : GUIContainer.getGUIContainer().getKnotenItems()) {
|
||||
Map<String, Object> node = new LinkedHashMap<>();
|
||||
node.put("name", item.getKnoten().holeAnzeigeName());
|
||||
node.put("type", item.getKnoten().getClass().getSimpleName());
|
||||
List<Object> macs = new ArrayList<>();
|
||||
if (item.getKnoten() instanceof InternetKnoten) {
|
||||
for (NetzwerkInterface nic : ((InternetKnoten) item.getKnoten()).getNetzwerkInterfaces()) macs.add(nic.getMac());
|
||||
}
|
||||
node.put("macs", macs);
|
||||
nodes.add(node);
|
||||
}
|
||||
result.put("nodes", nodes);
|
||||
String pdf = new File(pdfFolder, name.replace(".fls", "") + ".pdf").getPath();
|
||||
ReportGenerator.getInstance().generateReport(pdf);
|
||||
result.put("pdf", read(pdf));
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
// FILIUS' window keeps the JVM alive: end it on any failure too.
|
||||
try {
|
||||
run(args);
|
||||
} catch (Throwable error) {
|
||||
error.printStackTrace();
|
||||
System.exit(1);
|
||||
}
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
interface Step {
|
||||
void run() throws Exception;
|
||||
}
|
||||
|
||||
static void onEdt(Step step) throws Exception {
|
||||
Exception[] failed = new Exception[1];
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
try {
|
||||
step.run();
|
||||
} catch (Exception error) {
|
||||
failed[0] = error;
|
||||
}
|
||||
});
|
||||
if (failed[0] != null) throw failed[0];
|
||||
}
|
||||
|
||||
static void run(String[] args) throws Exception {
|
||||
if (args[0].equals("--read")) {
|
||||
Map<String, Object> read = new LinkedHashMap<>();
|
||||
for (int index = 2; index < args.length; index++) {
|
||||
Map<String, Object> pdf = read(args[index]);
|
||||
byte[] digest = java.security.MessageDigest.getInstance("SHA-256").digest(java.nio.file.Files.readAllBytes(Path.of(args[index])));
|
||||
StringBuilder hex = new StringBuilder();
|
||||
for (byte each : digest) hex.append(String.format("%02x", each));
|
||||
pdf.put("sha256", hex.toString());
|
||||
read.put(Path.of(args[index]).getFileName().toString(), pdf);
|
||||
}
|
||||
try (FileOutputStream out = new FileOutputStream(args[1])) {
|
||||
out.write((FlsOracle.Json.write(read, "") + "\n").getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
return;
|
||||
}
|
||||
GUIContainer.getGUIContainer();
|
||||
File pdfFolder = new File(args[1]);
|
||||
pdfFolder.mkdirs();
|
||||
Map<String, Object> output = new LinkedHashMap<>();
|
||||
Map<String, Object> projects = new LinkedHashMap<>();
|
||||
for (int index = 2; index < args.length; index++) {
|
||||
String[] parts = args[index].split(":");
|
||||
String file = parts[0];
|
||||
long runFor = parts.length > 1 ? Long.parseLong(parts[1]) : 0;
|
||||
String name = Path.of(file).getFileName().toString();
|
||||
onEdt(() -> {
|
||||
// As GUIMainMenu opens a file; the packet log starts empty, as after Lauscher.reset.
|
||||
Information.getInformation().reset();
|
||||
Lauscher.getLauscher().reset();
|
||||
SzenarioVerwaltung.getInstance().laden(file, GUIContainer.getGUIContainer().getKnotenItems(),
|
||||
GUIContainer.getGUIContainer().getCableItems(), GUIContainer.getGUIContainer().getDocuItems());
|
||||
GUIContainer.getGUIContainer().setProperty(null);
|
||||
GUIContainer.getGUIContainer().updateViewport();
|
||||
GUIContainer.getGUIContainer().updateCables();
|
||||
});
|
||||
if (runFor > 0) {
|
||||
// As GUIMainMenu.selectMode starts and ends a simulation: every node in the window's order.
|
||||
for (GUIKnotenItem item : GUIContainer.getGUIContainer().getKnotenItems()) item.getKnoten().getSystemSoftware().starten();
|
||||
Thread.sleep(runFor);
|
||||
for (GUIKnotenItem item : GUIContainer.getGUIContainer().getKnotenItems()) item.getKnoten().getSystemSoftware().beenden();
|
||||
Thread.sleep(500);
|
||||
}
|
||||
Map<String, Object>[] measured = new Map[1];
|
||||
onEdt(() -> measured[0] = measure(name, pdfFolder));
|
||||
projects.put(name, measured[0]);
|
||||
}
|
||||
output.put("projects", projects);
|
||||
|
||||
Map<String, Object>[] empty = new Map[1];
|
||||
onEdt(() -> {
|
||||
GUIContainer container = GUIContainer.getGUIContainer();
|
||||
container.getKnotenItems().clear();
|
||||
container.getCableItems().clear();
|
||||
container.getDocuItems().clear();
|
||||
SzenarioVerwaltung.getInstance().reset();
|
||||
Lauscher.getLauscher().reset();
|
||||
container.updateViewport();
|
||||
empty[0] = measure("empty.fls", pdfFolder);
|
||||
});
|
||||
output.put("empty", empty[0]);
|
||||
|
||||
try (FileOutputStream out = new FileOutputStream(args[0])) {
|
||||
out.write((FlsOracle.Json.write(output, "") + "\n").getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user