feat: F4.8c the documentation mode's image export, as Java's
ExportRun measures Java's print panel (GUIContainer.prepareExportPanel) and image on four projects and an empty one: 10 to spare around, a light grey footer 20 below the lowest element, 20 below it, a transparent background, the documentation painted as in the design. The sidebar gains Java's "Exportieren": the surface painted with Canvas 2D (an SVG with a foreignObject taints the canvas in WebKit) and saved under the project's name; Java's message when saving fails. Not adopted: Java's empty image of an empty project (its footer overflows). The editor takes a projectName; the capture export shares the download helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1699,3 +1699,35 @@ Gemessen (04.10.2026):
|
||||
|
||||
Falle der Sonde: Der Menüpunkt „Normal“ steht in beiden Untermenüs; die
|
||||
Sonde nimmt den ersten (Schriftgröße).
|
||||
|
||||
### Dokumentation: „Exportieren“ (F4.8c)
|
||||
|
||||
`ExportRun` öffnet ein Projekt, wie Javas Menü es öffnet
|
||||
(`Information.reset`, `SzenarioVerwaltung.laden`, `updateViewport`,
|
||||
`updateCables`), holt per Reflection das Druckpanel
|
||||
(`GUIContainer.prepareExportPanel`) und liest dessen Ausschnitt
|
||||
(`getClipX` … `getClipHeight`), die Grenzen seiner drei Ebenen (Netz,
|
||||
Dokumentation, Fußzeile; `GUIMainArea.minX` …) und die Fußzeile. Dann malt es
|
||||
mit `createNetworkImage` das Bild und misst Größe, Eckpixel, die Farben der
|
||||
Fußzeile und die Füllung einer Fläche darin. Vier Projekte
|
||||
(`nat_with_mapped_port`, `zwei_rechnernetze_komplett`, die Fixture
|
||||
`documentation.fls`, `modem_beispiel`) und ein leeres
|
||||
(`packages/editor/test/networkImage.java.json`; Javas Bilder unter
|
||||
`.build/export`).
|
||||
|
||||
Gemessen (04.10.2026):
|
||||
|
||||
* Ausschnitt: alle Geräte, Kabel (die beiden Mitten mit 2 Rand) und
|
||||
Dokumentationselemente, links, oben und rechts 10 Rand. Die Fußzeile steht
|
||||
20 unter dem untersten Element, bündig mit dem linkesten; sie ist so breit
|
||||
wie ihr Text und 20 mehr, so hoch wie ihre Schrift; unter ihr 20 Rand. Ist
|
||||
die Fußzeile breiter als der Inhalt, reicht das Bild 20 über sie hinaus.
|
||||
* Fußzeile: der Pfad der Datei, ohne Datei „Filius“, hellgrau (192).
|
||||
* Hintergrund durchsichtig; Dokumentation unter dem Netz, gemalt wie im
|
||||
Entwurf (Fläche 204 → 215, 201 → 212); nichts ausgewählt.
|
||||
* Leeres Projekt: ein leeres Bild 20 × 35. Die Fußzeile landet durch einen
|
||||
Überlauf (`Integer.MAX_VALUE + 10`) außerhalb. Nicht übernommen.
|
||||
|
||||
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.
|
||||
|
||||
@@ -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"
|
||||
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"
|
||||
# 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.
|
||||
@@ -499,6 +499,17 @@ HOME="$here/.build/home" xvfb-run -a java -Duser.language=de -Duser.country=DE -
|
||||
"$output" "$root/packages/doc/test/fixtures/documentation.fls" 2>&1 | grep -v 'logback\|^$\|^\s*at ' || true
|
||||
test -s "$output"
|
||||
|
||||
# FILIUS' „Exportieren“ (ExportRun): the print panel and the image Java makes
|
||||
# of four projects and an empty one — the part cut out, the footer, the
|
||||
# colours (packages/editor/test/networkImage.test.ts). Java's images go to
|
||||
# .build/export, to look at.
|
||||
output="$root/packages/editor/test/networkImage.java.json"
|
||||
rm -f "$output"
|
||||
HOME="$here/.build/home" xvfb-run -a java -Duser.language=de -Duser.country=DE -cp "$classpath:$here/.build" ExportRun \
|
||||
"$output" "$here/.build/export" "$upstream/beispiele/nat_with_mapped_port.fls" "$upstream/beispiele/zwei_rechnernetze_komplett.fls" \
|
||||
"$root/packages/doc/test/fixtures/documentation.fls" "$upstream/beispiele/modem_beispiel.fls" 2>&1 | grep -v 'logback\|^$\|^\s*at ' || 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,214 @@
|
||||
// FILIUS' „Exportieren“ in the documentation mode measured
|
||||
// (GUIContainer.createNetworkImage, GUIPrintPanel): each project opened as
|
||||
// the menu opens it, then the print panel Java paints — the bounds of every
|
||||
// device, cable and documentation element in it, the clip bounds of its
|
||||
// network, documentation and footer panels, the footer's text, place and
|
||||
// size, the rectangle cut out of the panel — and the image itself: its size,
|
||||
// whether its background is transparent, the colours of its footer, the fill
|
||||
// of a rectangle in it. An empty project too (no element at all). The
|
||||
// reference for the editor's image export (packages/editor/test/networkImage.test.ts).
|
||||
//
|
||||
// Usage: java -cp filius.jar:lib/*:classes ExportRun <out.json> <png folder> <file.fls>...
|
||||
|
||||
import filius.gui.GUIContainer;
|
||||
import filius.gui.netzwerksicht.GUIDocuItem;
|
||||
import filius.gui.netzwerksicht.GUIKabelItem;
|
||||
import filius.gui.netzwerksicht.GUIKnotenItem;
|
||||
import filius.gui.netzwerksicht.GUIMainArea;
|
||||
import filius.gui.netzwerksicht.GUIPrintPanel;
|
||||
import filius.rahmenprogramm.Information;
|
||||
import filius.rahmenprogramm.SzenarioVerwaltung;
|
||||
|
||||
import java.awt.Component;
|
||||
import java.awt.FontMetrics;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
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 java.util.TreeMap;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.SwingUtilities;
|
||||
|
||||
public class ExportRun {
|
||||
static Object get(Object owner, Class<?> type, String name) throws Exception {
|
||||
Field field = type.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
return field.get(owner);
|
||||
}
|
||||
|
||||
static List<Object> bounds(Component component) {
|
||||
Rectangle rect = component.getBounds();
|
||||
return List.of(rect.x, rect.y, rect.width, rect.height);
|
||||
}
|
||||
|
||||
static Map<String, Object> area(GUIMainArea panel) throws Exception {
|
||||
Map<String, Object> shown = new LinkedHashMap<>();
|
||||
for (String name : new String[] { "minX", "maxX", "minY", "maxY" }) {
|
||||
shown.put(name, get(panel, GUIMainArea.class, name));
|
||||
}
|
||||
return shown;
|
||||
}
|
||||
|
||||
static String rgba(int argb) {
|
||||
return ((argb >> 16) & 255) + "," + ((argb >> 8) & 255) + "," + (argb & 255) + "," + ((argb >>> 24) & 255);
|
||||
}
|
||||
|
||||
/** The colours inside a rectangle of the image (in image coordinates), most frequent first, at most five. */
|
||||
static List<Object> colours(BufferedImage image, int x, int y, int width, int height) {
|
||||
Map<String, Integer> counts = new TreeMap<>();
|
||||
for (int row = Math.max(0, y); row < Math.min(image.getHeight(), y + height); row++) {
|
||||
for (int column = Math.max(0, x); column < Math.min(image.getWidth(), x + width); column++) {
|
||||
counts.merge(rgba(image.getRGB(column, row)), 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
List<Map.Entry<String, Integer>> sorted = new ArrayList<>(counts.entrySet());
|
||||
sorted.sort((lhs, rhs) -> rhs.getValue() - lhs.getValue());
|
||||
List<Object> shown = new ArrayList<>();
|
||||
for (Map.Entry<String, Integer> entry : sorted.subList(0, Math.min(5, sorted.size()))) {
|
||||
shown.add(List.of(entry.getKey(), entry.getValue()));
|
||||
}
|
||||
return shown;
|
||||
}
|
||||
|
||||
static Map<String, Object> measure(String name, File pngFolder) throws Exception {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
GUIContainer container = GUIContainer.getGUIContainer();
|
||||
List<Object> nodes = new ArrayList<>();
|
||||
for (GUIKnotenItem item : container.getKnotenItems()) {
|
||||
Map<String, Object> node = new LinkedHashMap<>();
|
||||
node.put("name", item.getKnoten().holeAnzeigeName());
|
||||
node.put("bounds", bounds(item.getImageLabel()));
|
||||
nodes.add(node);
|
||||
}
|
||||
result.put("nodes", nodes);
|
||||
List<Object> cables = new ArrayList<>();
|
||||
for (GUIKabelItem item : container.getCableItems()) cables.add(bounds(item.getKabelpanel()));
|
||||
result.put("cables", cables);
|
||||
List<Object> docs = new ArrayList<>();
|
||||
for (GUIDocuItem item : container.getDocuItems()) {
|
||||
Map<String, Object> doc = new LinkedHashMap<>();
|
||||
doc.put("type", item.getType());
|
||||
doc.put("bounds", bounds(item.asDocuElement()));
|
||||
doc.put("color", item.getColor() == null ? null : rgba(item.getColor().getRGB()));
|
||||
docs.add(doc);
|
||||
}
|
||||
result.put("docs", docs);
|
||||
result.put("path", SzenarioVerwaltung.getInstance().holePfad() == null ? null : name);
|
||||
|
||||
Method prepare = GUIContainer.class.getDeclaredMethod("prepareExportPanel");
|
||||
prepare.setAccessible(true);
|
||||
GUIPrintPanel panel = (GUIPrintPanel) prepare.invoke(container);
|
||||
result.put("panelSize", List.of(panel.getWidth(), panel.getHeight()));
|
||||
result.put("network", area((GUIMainArea) get(panel, GUIPrintPanel.class, "networkPanel")));
|
||||
result.put("documentation", area((GUIMainArea) get(panel, GUIPrintPanel.class, "docuPanel")));
|
||||
GUIMainArea footer = (GUIMainArea) get(panel, GUIPrintPanel.class, "footer");
|
||||
result.put("footerArea", area(footer));
|
||||
JLabel label = (JLabel) get(footer, footer.getClass(), "footer");
|
||||
Map<String, Object> footerLabel = new LinkedHashMap<>();
|
||||
FontMetrics metrics = label.getFontMetrics(label.getFont());
|
||||
footerLabel.put("text", label.getText().equals(SzenarioVerwaltung.getInstance().holePfad()) ? "<path>" : label.getText());
|
||||
footerLabel.put("bounds", bounds(label));
|
||||
footerLabel.put("stringWidth", metrics.stringWidth(label.getText()));
|
||||
footerLabel.put("fontHeight", metrics.getHeight());
|
||||
footerLabel.put("foreground", rgba(label.getForeground().getRGB()));
|
||||
result.put("footer", footerLabel);
|
||||
try {
|
||||
int clipX = panel.getClipX(), clipY = panel.getClipY();
|
||||
result.put("clip", List.of(clipX, clipY, panel.getClipWidth(), panel.getClipHeight()));
|
||||
container.updateViewport();
|
||||
BufferedImage image = container.createNetworkImage();
|
||||
result.put("imageSize", List.of(image.getWidth(), image.getHeight()));
|
||||
result.put("cornerPixel", rgba(image.getRGB(0, 0)));
|
||||
// The footer in image coordinates: the panel's own minus the clip's origin.
|
||||
Rectangle at = label.getBounds();
|
||||
result.put("footerColours", colours(image, at.x - clipX, at.y - clipY, at.width, at.height));
|
||||
for (GUIDocuItem item : container.getDocuItems()) {
|
||||
if (item.getType() != GUIDocuItem.RECT) continue;
|
||||
Rectangle rect = item.asDocuElement().getBounds();
|
||||
// Inside the panel the elements lie 10 to the right and down (EMPTY_BORDER).
|
||||
result.put("rectangleCentre", colours(image, rect.x + rect.width / 2 + 10 - clipX, rect.y + 4 + 10 - clipY, 1, 1));
|
||||
result.put("rectangleCentreColor", rgba(item.getColor().getRGB()));
|
||||
break;
|
||||
}
|
||||
ImageIO.write(image, "png", new File(pngFolder, name.replace(".fls", "") + ".png"));
|
||||
} catch (RuntimeException error) {
|
||||
result.put("error", error.getClass().getSimpleName());
|
||||
}
|
||||
container.updateViewport();
|
||||
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);
|
||||
}
|
||||
|
||||
static void run(String[] args) throws Exception {
|
||||
GUIContainer.getGUIContainer();
|
||||
File pngFolder = new File(args[1]);
|
||||
pngFolder.mkdirs();
|
||||
Map<String, Object> output = new LinkedHashMap<>();
|
||||
Map<String, Object> projects = new LinkedHashMap<>();
|
||||
for (int index = 2; index < args.length; index++) {
|
||||
String file = args[index];
|
||||
String name = Path.of(file).getFileName().toString();
|
||||
Object[] measured = new Object[1];
|
||||
Exception[] failed = new Exception[1];
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
try {
|
||||
// As GUIMainMenu opens a file.
|
||||
Information.getInformation().reset();
|
||||
SzenarioVerwaltung.getInstance().laden(file, GUIContainer.getGUIContainer().getKnotenItems(),
|
||||
GUIContainer.getGUIContainer().getCableItems(), GUIContainer.getGUIContainer().getDocuItems());
|
||||
GUIContainer.getGUIContainer().setProperty(null);
|
||||
GUIContainer.getGUIContainer().updateViewport();
|
||||
GUIContainer.getGUIContainer().updateCables();
|
||||
measured[0] = measure(name, pngFolder);
|
||||
} catch (Exception error) {
|
||||
failed[0] = error;
|
||||
}
|
||||
});
|
||||
if (failed[0] != null) throw failed[0];
|
||||
projects.put(name, measured[0]);
|
||||
}
|
||||
output.put("projects", projects);
|
||||
|
||||
// An empty project: nothing on the surface, no file.
|
||||
Object[] empty = new Object[1];
|
||||
Exception[] failed = new Exception[1];
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
try {
|
||||
GUIContainer container = GUIContainer.getGUIContainer();
|
||||
container.getKnotenItems().clear();
|
||||
container.getCableItems().clear();
|
||||
container.getDocuItems().clear();
|
||||
SzenarioVerwaltung.getInstance().reset();
|
||||
container.updateViewport();
|
||||
empty[0] = measure("empty.fls", pngFolder);
|
||||
} catch (Exception error) {
|
||||
failed[0] = error;
|
||||
}
|
||||
});
|
||||
if (failed[0] != null) throw failed[0];
|
||||
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