Skip to content

Commit 517faf3

Browse files
committed
Add SVG icon rendering support
1 parent 5aaad05 commit 517faf3

File tree

5 files changed

+204
-3
lines changed

5 files changed

+204
-3
lines changed

src/main/java/edu/rpi/legup/ui/HomePanel.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import edu.rpi.legup.app.LegupPreferences;
55
import edu.rpi.legup.controller.CursorController;
66
import edu.rpi.legup.model.PuzzleExporter;
7+
import edu.rpi.legup.utility.SVGImage;
78
import java.awt.*;
89
import java.awt.event.ActionEvent;
910
import java.awt.event.ActionListener;
@@ -138,10 +139,10 @@ private void initButtons() {
138139

139140
URL button0IconLocation =
140141
ClassLoader.getSystemClassLoader()
141-
.getResource("edu/rpi/legup/images/Legup/homepanel/proof_file.png");
142-
ImageIcon button0Icon = new ImageIcon(button0IconLocation);
142+
.getResource("edu/rpi/legup/images/Legup/homepanel/proof_file.svg");
143+
SVGImage button0Icon = new SVGImage(button0IconLocation);
143144
this.buttons[0].setFocusPainted(false);
144-
this.buttons[0].setIcon(resizeButtonIcon(button0Icon, this.buttonSize, this.buttonSize));
145+
this.buttons[0].setIcon(button0Icon.getIcon(this.buttonSize, this.buttonSize));
145146
this.buttons[0].setHorizontalTextPosition(AbstractButton.CENTER);
146147
this.buttons[0].setVerticalTextPosition(AbstractButton.BOTTOM);
147148
this.buttons[0].addActionListener(CursorController.createListener(this, openProofListener));
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package edu.rpi.legup.utility;
2+
3+
import java.awt.BasicStroke;
4+
import java.awt.Color;
5+
import java.awt.Component;
6+
import java.awt.Graphics;
7+
import java.awt.Graphics2D;
8+
import static java.awt.RenderingHints.KEY_ANTIALIASING;
9+
import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON;
10+
import javax.xml.parsers.DocumentBuilderFactory;
11+
import javax.xml.parsers.DocumentBuilder;
12+
import java.awt.Shape;
13+
import java.awt.Stroke;
14+
import java.awt.geom.AffineTransform;
15+
import java.awt.geom.Area;
16+
import java.awt.geom.Path2D;
17+
import java.awt.geom.RoundRectangle2D;
18+
import java.io.IOException;
19+
import java.net.URL;
20+
import java.util.Map;
21+
import java.util.Optional;
22+
import java.util.Scanner;
23+
import javax.swing.Icon;
24+
import javax.xml.parsers.ParserConfigurationException;
25+
import org.w3c.dom.Document;
26+
import org.w3c.dom.Element;
27+
import org.w3c.dom.NodeList;
28+
import org.xml.sax.SAXException;
29+
30+
public class SVGImage {
31+
Drawable[] items;
32+
float x;
33+
float y;
34+
float width;
35+
float height;
36+
interface Processor {
37+
Shape process(Element node);
38+
}
39+
static Map<String, Color> colors = Map.of(
40+
"white", Color.WHITE,
41+
"black", Color.BLACK,
42+
"red", Color.RED,
43+
"green", Color.GREEN,
44+
"blue", Color.BLUE,
45+
"cyan", Color.CYAN,
46+
"magenta", Color.MAGENTA,
47+
"yellow", Color.YELLOW
48+
);
49+
record ColoredStroke(Stroke stroke, Color color) {}
50+
record Drawable(Optional<Color> fill, Optional<ColoredStroke> stroke, Area shape) {}
51+
record ElementProcessor(String element, Processor processor) implements Map.Entry<String, ElementProcessor> {
52+
Drawable process(Element node) {
53+
Color strokeColor = null;
54+
if(node.hasAttribute("stroke")) {
55+
strokeColor = colors.get(node.getAttribute("stroke").toLowerCase());
56+
}
57+
Float strokeWidth = null;
58+
if(node.hasAttribute("stroke-width")) {
59+
strokeWidth = Float.valueOf(node.getAttribute("stroke-width"));
60+
}
61+
ColoredStroke stroke = null;
62+
if(strokeWidth != null && strokeColor != null) {
63+
stroke = new ColoredStroke(new BasicStroke(strokeWidth), strokeColor);
64+
}
65+
Color fill = null;
66+
if(node.hasAttribute("fill")) {
67+
fill = colors.get(node.getAttribute("fill").toLowerCase());
68+
}
69+
Area area = new Area(processor.process(node));
70+
return new Drawable(Optional.ofNullable(fill), Optional.ofNullable(stroke), area);
71+
}
72+
@Override
73+
public String getKey() {
74+
return element;
75+
}
76+
@Override
77+
public ElementProcessor getValue() {
78+
return this;
79+
}
80+
@Override
81+
public ElementProcessor setValue(ElementProcessor value) {
82+
return this;
83+
}
84+
}
85+
static Map<String, ElementProcessor> processors = Map.ofEntries(
86+
new ElementProcessor("rect", (Element node) -> {
87+
float radiusX = 0;
88+
if(node.hasAttribute("rx")) {
89+
radiusX = Float.parseFloat(node.getAttribute("rx"));
90+
}
91+
float radiusY = 0;
92+
if(node.hasAttribute("ry")) {
93+
radiusX = Float.parseFloat(node.getAttribute("ry"));
94+
}
95+
float x = Float.parseFloat(node.getAttribute("x"));
96+
float y = Float.parseFloat(node.getAttribute("y"));
97+
float width = Float.parseFloat(node.getAttribute("width"));
98+
float height = Float.parseFloat(node.getAttribute("height"));
99+
return new RoundRectangle2D.Float(x, y, width, height, radiusX, radiusY);
100+
}),
101+
new ElementProcessor("path", (Element node) -> {
102+
Path2D path = new Path2D.Float();
103+
Scanner scanner = new Scanner(node.getAttribute("d"));
104+
while(scanner.hasNext()) {
105+
switch(scanner.next()) {
106+
case "M" -> {
107+
path.moveTo(scanner.nextFloat(), scanner.nextFloat());
108+
}
109+
case "Q" -> {
110+
path.quadTo(scanner.nextFloat(), scanner.nextFloat(),
111+
scanner.nextFloat(), scanner.nextFloat());
112+
}
113+
case "L" -> {
114+
path.lineTo(scanner.nextFloat(), scanner.nextFloat());
115+
}
116+
case "V" -> {
117+
path.lineTo(path.getCurrentPoint().getX(), scanner.nextFloat());
118+
}
119+
case "H" -> {
120+
path.lineTo(scanner.nextFloat(), path.getCurrentPoint().getY());
121+
}
122+
case "Z" -> {
123+
path.closePath();
124+
}
125+
}
126+
}
127+
return path;
128+
})
129+
);
130+
public SVGImage(URL url) {
131+
try {
132+
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
133+
DocumentBuilder builder = factory.newDocumentBuilder();
134+
Document doc = builder.parse(url.toString());
135+
String viewBox = doc.getDocumentElement().getAttribute("viewBox");
136+
Scanner scanner = new Scanner(viewBox);
137+
x = scanner.nextFloat();
138+
y = scanner.nextFloat();
139+
width = scanner.nextFloat();
140+
height = scanner.nextFloat();
141+
NodeList nodes = doc.getElementsByTagName("*");
142+
items = new Drawable[nodes.getLength()];
143+
for(int i = 0; i < nodes.getLength(); i++) {
144+
Element node = (Element) nodes.item(i);
145+
ElementProcessor processor = processors.get(node.getNodeName());
146+
if(processor != null) {
147+
items[i] = processor.process(node);
148+
}
149+
}
150+
} catch (ParserConfigurationException | SAXException | IOException e) {
151+
System.out.println(e);
152+
}
153+
}
154+
public Icon getIcon(int width, int height) {
155+
return new Icon() {
156+
@Override
157+
public void paintIcon(Component c, Graphics graphics, int x, int y) {
158+
Graphics2D g = (Graphics2D) graphics;
159+
g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
160+
for(Drawable item : items) {
161+
if(item == null) {
162+
continue;
163+
}
164+
Area shape = (Area) item.shape().clone();
165+
float scaleX = width / SVGImage.this.width;
166+
float scaleY = height / SVGImage.this.height;
167+
AffineTransform transform = AffineTransform.getScaleInstance(scaleX, scaleY);
168+
float shiftX = x - SVGImage.this.x;
169+
float shiftY = y - SVGImage.this.y;
170+
transform.translate(shiftX, shiftY);
171+
shape.transform(transform);
172+
item.fill.ifPresent((Color fill) -> {
173+
g.setColor(fill);
174+
g.fill(shape);
175+
});
176+
item.stroke.ifPresent((ColoredStroke stroke) -> {
177+
g.setColor(stroke.color());
178+
g.setStroke(stroke.stroke());
179+
g.draw(shape);
180+
});
181+
}
182+
}
183+
@Override
184+
public int getIconWidth() {
185+
return width;
186+
}
187+
@Override
188+
public int getIconHeight() {
189+
return height;
190+
}
191+
};
192+
}
193+
}
Binary file not shown.
Lines changed: 7 additions & 0 deletions
Loading
Binary file not shown.

0 commit comments

Comments
 (0)