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:
@@ -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