Convert graphs to SVG

This commit is contained in:
zzz
2024-07-25 11:52:07 +00:00
parent 04390dd8fd
commit 3cc1eb7cc3
6 changed files with 970 additions and 10 deletions

View File

@@ -37,6 +37,9 @@
encoding="UTF-8"
includes="**/*.java" >
<compilerarg line="${javac.compilerargs}" />
<classpath>
<pathelement location="../../../core/java/build/i2p.jar" />
</classpath>
</javac>
</target>

View File

@@ -0,0 +1,431 @@
package net.i2p.rrd4j;
import java.text.AttributedCharacterIterator;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.awt.*;
import java.awt.font.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.PathIterator;
import static java.awt.geom.PathIterator.*;
import java.awt.image.*;
import java.awt.image.renderable.RenderableImage;
/**
* Very simple SVGGraphics2D, only enough for basic rrd4j use, without dependencies.
* Plus a few things that rrd4j doesn't use, but not much.
* Unsupported things will throw UnsupportedOperationExceptions.
*
* Supports custom RenderingHints for id and class on top-level svg element.
* Supports custom RenderingHints for id, class, title, and arbitrary attributes
* on all drawn elements.
* Supports custom RenderingHints for inner SVG on all drawn elements except text.
*
* No standard Java AWT hints are supported.
* Antialiasing is done automatically.
* Antialiasing hints have no effect.
*
* License: Apache 2.0 (same as rrd4j)
*
* @since 0.9.64
* @author zzz
*/
public class SimpleSVGGraphics2D extends Graphics2D {
//// hints - all strings except for ATTMAP ////
/**
* On the top svg element.
* Value is a string and will be XML-escaped when rendering.
*/
public static final RenderingHints.Key KEY_SVG_ID = new RHKey(1);
/**
* On the top svg element.
* Value is a string and will be XML-escaped when rendering.
*/
public static final RenderingHints.Key KEY_SVG_CLASS = new RHKey(2);
/**
* On the top svg element.
* Value is a string and will be XML-escaped when rendering.
*/
public static final RenderingHints.Key KEY_SVG_TITLE = new RHKey(3);
/**
* On the next element drawn, one-shot, will be removed after rendering.
* Value is a string and will be XML-escaped when rendering.
*/
public static final RenderingHints.Key KEY_ELEMENT_ID = new RHKey(4);
/**
* On the next element drawn, one-shot, will be removed after rendering.
* Value is a string and will be XML-escaped when rendering.
*/
public static final RenderingHints.Key KEY_ELEMENT_CLASS = new RHKey(5);
/**
* Value is a Map of String to String of extra attributes on the next element drawn, one-shot, will be removed after rendering.
* Map keys must be XML-escaped by caller if necessary.
* Map values will be XML-escaped when rendering.
*/
public static final RenderingHints.Key KEY_ELEMENT_ATTMAP = new RHKey(6);
/**
* On the next element drawn, one-shot, will be removed after rendering.
* Value is a string and will be XML-escaped when rendering.
*/
public static final RenderingHints.Key KEY_ELEMENT_TITLE = new RHKey(7);
/**
* Put "inside" the next element drawn, one-shot, will be removed after rendering.
* Value is an XML string and must be XML-escaped by caller if necessary.
*/
public static final RenderingHints.Key KEY_ELEMENT_INNERSVG = new RHKey(8);
private final StringBuilder buf;
private final SimpleSVGMaker svg;
private final Map<Object,Object> hints = new HashMap<Object,Object>();
private AffineTransform transform = new AffineTransform();
private final FontRenderContext frctx = new FontRenderContext(transform, true, true);
private final int width, height;
private final Rectangle origclip;
// null unless different from origclip
private Rectangle clip;
private String clipID;
private Color bgcolor = Color.WHITE;
private Paint paint = Color.BLACK;
private Stroke stroke = new BasicStroke(1);
private Font font = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
private boolean started;
public SimpleSVGGraphics2D(int width, int height) {
this.width = width;
this.height = height;
origclip = new Rectangle(0, 0, width, height);
buf = new StringBuilder(16*1024);
svg = new SimpleSVGMaker(buf);
}
public String getSVG() {
stop();
String rv = buf.toString();
dispose();
return rv;
}
private void start() {
if (!started) {
String id = (String) hints.remove(KEY_SVG_ID);
String cl = (String) hints.remove(KEY_SVG_CLASS);
svg.startSVG(width, height, bgcolor, id, cl);
started = true;
}
}
private void stop() {
if (started) {
if (!transform.isIdentity()) {
svg.endGroup();
transform = frctx.getTransform();
}
svg.endSVG();
clip = null;
clipID = null;
started = false;
}
}
public void dispose() { buf.setLength(0); }
//// API bypass ////
/**
* Graphics2D API bypass, advanced use only
*/
public SimpleSVGMaker getMaker() { start(); return svg; }
/**
* Graphics2D API bypass, advanced use only
*/
public void append(String s) { start(); buf.append(s).append('\n'); }
//// draws/fills used by rrd4j ////
public void drawLine(int x1, int y1, int x2, int y2) {
start();
svg.drawLine(x1, y1, x2, y2, (Color) paint, (BasicStroke) stroke, clipID, hints);
}
public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) {
start();
svg.drawPolyline(xPoints, yPoints, nPoints, (Color) paint, (BasicStroke) stroke, clipID, hints);
}
public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) {
start();
String id = (String) hints.remove(KEY_ELEMENT_ID);
String cl = (String) hints.remove(KEY_ELEMENT_CLASS);
svg.fillPolygon(xPoints, yPoints, nPoints, (Color) paint, clipID, hints);
}
public void fillRect(int x, int y, int width, int height) {
if (!started) {
// rrd4j calls this first with the background color, it does not call setBackground()
if (x == 0 && y == 0 && width == this.width && height == this.height) {
// disable setting the background color, this is it
bgcolor = null;
}
start();
}
svg.drawRect(x, y, width, height, null, (Color) paint, null, clipID, hints);
}
//// text ////
public void drawString(String str, int x, int y) {
start();
svg.drawText(str, x, y, (Color) paint, font, clipID, hints);
}
public void drawString(String str, float x, float y) { drawString(str, (int) x, (int) y); }
public FontRenderContext getFontRenderContext() { return frctx; }
//// supported things not used by rrd4j ////
/**
* Circles only for now, must be width == height and arcAngle == 360
* Otherwise throws UnsupportedOperationException
* TODO
*/
public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
if (width != height || arcAngle != 360)
throw new UnsupportedOperationException("circles only!");
start();
int r = width / 2;
svg.drawCircle(x + r, y + r, r, (Color) paint, null, (BasicStroke) stroke, clipID, hints);
}
/**
* Circles only for now, must be width == height and arcAngle == 360
* Otherwise throws UnsupportedOperationException
* TODO
*/
public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) {
if (width != height || arcAngle != 360)
throw new UnsupportedOperationException("circles only!");
start();
int r = width / 2;
svg.drawCircle(x + r, y + r, r, null, (Color) paint, (BasicStroke) stroke, clipID, hints);
}
public void clearRect(int x, int y, int width, int height) {
boolean wasStarted = started;
if (!wasStarted) {
start();
} else {
// don't do it twice at the start
String id = (String) hints.remove(KEY_ELEMENT_ID);
String cl = (String) hints.remove(KEY_ELEMENT_CLASS);
svg.drawRect(x, y, width, height, null, bgcolor, null, clipID, hints);
}
}
public void draw(Shape s) {
drawOrFill(s, true, false);
}
public void fill(Shape s) {
drawOrFill(s, false, true);
}
/**
* Lines only for now
* Will draw a closed shape, open shapes will become closed.
* Arcs will throw UnsupportedOperationException
*/
private void drawOrFill(Shape s, boolean draw, boolean fill) {
int[] x = new int[16];
int[] y = new int[16];
int i = 0;
float[] coords = new float[6];
for (PathIterator it = s.getPathIterator(frctx.getTransform()); !it.isDone(); it.next()) {
int type = it.currentSegment(coords);
switch(type) {
case SEG_MOVETO:
case SEG_LINETO:
if (i >= x.length) {
x = Arrays.copyOf(x, x.length * 2);
y = Arrays.copyOf(y, y.length * 2);
}
x[i] = (int) coords[0];
y[i++] = (int) coords[1];
break;
case SEG_CLOSE:
break;
case SEG_CUBICTO:
case SEG_QUADTO:
throw new UnsupportedOperationException("Unsupported curved shape");
default:
throw new UnsupportedOperationException("Unsupported type " + type);
}
}
if (draw)
drawPolyline(x, y, i);
else
fillPolygon(x, y, i);
}
//// clips ////
public void setClip(int x, int y, int width, int height) {
setClip(new Rectangle(x, y, width, height));
}
public void setClip(Shape clip) {
if (clip.equals(this.clip))
return;
if (this.clip == null && clip.equals(origclip))
return;
Rectangle newclip;
if (clip instanceof Rectangle)
newclip = (Rectangle) clip;
else
newclip = clip.getBounds();
if (clip.equals(origclip)) {
this.clip = null;
clipID = null;
return;
}
// define new clip, save the Rectangle and ID
clipID = svg.defineClipPath(newclip);
this.clip = newclip;
}
//// transforms ////
public void translate(int x, int y) { translate((double) x, (double) y); }
public void translate(double tx, double ty) {
AffineTransform ntx = (AffineTransform) transform.clone();
ntx.translate(tx, ty);
setTransform(ntx);
}
public void rotate(double theta) {
AffineTransform ntx = (AffineTransform) transform.clone();
ntx.rotate(theta);
setTransform(ntx);
}
public void rotate(double theta, double x, double y) {
AffineTransform ntx = (AffineTransform) transform.clone();
ntx.rotate(theta, x, y);
setTransform(ntx);
}
public void scale(double sx, double sy) {
AffineTransform ntx = (AffineTransform) transform.clone();
ntx.scale(sx, sy);
setTransform(ntx);
}
public void shear(double shx, double shy) {
AffineTransform ntx = (AffineTransform) transform.clone();
ntx.shear(shx, shy);
setTransform(ntx);
}
public void setTransform(AffineTransform tx) {
// For each transform, we close the previous group if non-identity,
// and start a new group with a transform containing the new combined transform.
// We don't 'stack' groups with each individual transform.
if (transform.equals(tx))
return;
if (!transform.isIdentity())
svg.endGroup();
if (!tx.isIdentity()) {
String matrix = String.format(Locale.US, "matrix(%.3f %.3f %.3f %.3f %.3f %.3f)",
tx.getScaleX(), tx.getShearY(),
tx.getShearX(), tx.getScaleY(),
tx.getTranslateX(), tx.getTranslateY());
svg.startGroup(null, null, "transform", matrix);
}
transform = tx;
}
public AffineTransform getTransform() { return transform; }
//// setters ////
public void setFont(Font font) { this.font = font; }
public void setPaint(Paint paint) { this.paint = paint; }
public void setStroke(Stroke stroke) { this.stroke = stroke; }
//// we support these but unused by rrd4j ////
public void setBackground(Color color) { bgcolor = color; }
public Color getBackground() { return bgcolor; }
public Shape getClip() { return clip; }
public Rectangle getClipBounds() { return clip; }
public void setColor(Color color) { paint = color; }
public Color getColor() { return (Color) paint; }
public Font getFont() { return font; }
public Paint getPaint() { return paint; }
public Stroke getStroke() { return stroke; }
//// Hints ////
private static class RHKey extends RenderingHints.Key {
public RHKey(int k) {
super(k);
}
public boolean isCompatibleValue(Object o) {
if (intKey() == 6)
return o instanceof Map;
return o instanceof String;
}
}
public void addRenderingHints(Map<?,?> hints) { this.hints.putAll(hints); }
public Object getRenderingHint(RenderingHints.Key hintKey) { return hints.get(hintKey); }
public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) { hints.put(hintKey, hintValue); }
public void setRenderingHints(Map<?,?> hints) { this.hints.clear(); addRenderingHints(hints); }
//// unsupported things ////
//// all do nothing or throw ////
public void clipRect(int x, int y, int width, int height) { throw new UnsupportedOperationException(); }
public void clip(Shape s) { throw new UnsupportedOperationException(); }
public void copyArea(int x, int y, int width, int height, int dx, int dy) { throw new UnsupportedOperationException(); }
public Graphics create() { throw new UnsupportedOperationException(); }
public void drawGlyphVector(GlyphVector g, float x, float y) { throw new UnsupportedOperationException(); }
public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) { throw new UnsupportedOperationException(); }
public boolean drawImage(Image img, AffineTransform xform, ImageObserver obs) { throw new UnsupportedOperationException(); }
public boolean drawImage(Image img, int x, int y, ImageObserver obs) { throw new UnsupportedOperationException(); }
public boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver obs) { throw new UnsupportedOperationException(); }
public boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver obs) { throw new UnsupportedOperationException(); }
public boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, ImageObserver obs) { throw new UnsupportedOperationException(); }
public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, ImageObserver obs) { throw new UnsupportedOperationException(); }
public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, Color bgcolor, ImageObserver obs) { throw new UnsupportedOperationException(); }
public void drawOval(int x, int y, int width, int height) { throw new UnsupportedOperationException(); }
public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) { throw new UnsupportedOperationException(); }
public void drawRenderableImage(RenderableImage img, AffineTransform xform) { throw new UnsupportedOperationException(); }
public void drawRenderedImage(RenderedImage img, AffineTransform xform) { throw new UnsupportedOperationException(); }
public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { throw new UnsupportedOperationException(); }
public void drawString(AttributedCharacterIterator iterator, float x, float y) { throw new UnsupportedOperationException(); }
public void drawString(AttributedCharacterIterator iterator, int x, int y) { throw new UnsupportedOperationException(); }
public void fillOval(int x, int y, int width, int height) { throw new UnsupportedOperationException(); }
public void fillPolyline(int[] xPoints, int[] yPoints, int nPoints) { throw new UnsupportedOperationException(); }
public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { throw new UnsupportedOperationException(); }
public Composite getComposite() { return null; }
public GraphicsConfiguration getDeviceConfiguration() { return null; }
public FontMetrics getFontMetrics(Font f) { return null; }
public RenderingHints getRenderingHints() { return null; }
public boolean hit(Rectangle rect, Shape s, boolean onStroke) { return false; }
public void setComposite(Composite comp) { throw new UnsupportedOperationException(); }
public void setPaintMode() {}
public void setXORMode(Color color) { throw new UnsupportedOperationException(); }
public void transform(AffineTransform tx) {}
}

View File

@@ -0,0 +1,75 @@
package net.i2p.rrd4j;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.io.IOException;
import java.io.OutputStream;
import org.rrd4j.graph.ImageWorker;
/**
* rrd4j adapter for SimpleSVGGraphics2D
*
* Requires: rrd4j 3.10 or higher
* Ref: https://github.com/rrd4j/rrd4j/issues/165
*
* Usage:
* No ImageIO/BufferedImage/ImageWriter required!
*
*<pre>
* RRDGraph graph = new RrdGraph(graphdef, new SimpleSVGImageWorker(width, height));
* outputstream.write(graph.getRrdGraphInfo().getBytes());
*</pre>
*
* License: Apache 2.0 (same as rrd4j)
*
* @since 0.9.64
* @author zzz
*/
public class SimpleSVGImageWorker extends ImageWorker {
private SimpleSVGGraphics2D g2d;
private AffineTransform initialAffineTransform;
private int imgWidth;
private int imgHeight;
public SimpleSVGImageWorker(int width, int height) {
resize(width, height);
}
protected void resize(int width, int height) {
imgWidth = width;
imgHeight = height;
g2d = new SimpleSVGGraphics2D(imgWidth, imgHeight);
initialAffineTransform = g2d.getTransform();
setG2d(g2d);
}
protected void reset(Graphics2D g2d) {
g2d.setTransform(initialAffineTransform);
g2d.setClip(0, 0, imgWidth, imgHeight);
}
protected void makeImage(OutputStream os) throws IOException {
os.write(g2d.getSVG().getBytes("UTF-8"));
}
/**
* Overridden because the SVG format essentially strips leading/trailing spaces,
* causing alignment issues in ValueAxis with the %x.y number formatting.
* Consecutive spaces within text are also probably collapsed, that is not addressed here.
*/
@Override
protected void drawString(String text, int x, int y, Font font, Paint paint) {
super.drawString(text.trim(), x, y, font, paint);
}
/**
* Overridden because the SVG format essentially strips leading/trailing spaces,
* causing alignment issues in ValueAxis with the %x.y number formatting.
* Consecutive spaces within text are also probably collapsed, that is not addressed here.
*/
@Override
protected double getStringWidth(String text, Font font) {
return super.getStringWidth(text.trim(), font);
}
}

View File

@@ -0,0 +1,453 @@
package net.i2p.rrd4j;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Rectangle;
import net.i2p.data.DataHelper;
import static net.i2p.rrd4j.SimpleSVGGraphics2D.*;
/**
* Create full or partial SVG images, without dependencies.
* Does not extend or use Graphics2D or ImageWriter.
*
* Each drawn element can be passed an optional CSS ID and/or classes,
* for easy styling and manipulation via CSS or js.
* All parameters are set as attributes, not as inline style,
* so a separate CSS style may easily override them.
* If inline style is desired, add it with the KEY_ELEMENT_ATTMAP hint.
*
* Unlike in Graphics2D, the border and fill for an object may be drawn in
* the same call, with separate colors.
*
* There is no state here other than the StringBuffer;
* there is no concept of current Color or Stroke or Font;
* caller must keep track of current Colors, Stroke, and Font, and pass them in
* on every draw() call, and/or overridden via CSS.
*
* License: Apache 2.0 (same as rrd4j)
*
* @since 0.9.64
* @author zzz
*/
public class SimpleSVGMaker {
private final StringBuilder buf;
private int clipid;
public SimpleSVGMaker(StringBuilder buf) {
this.buf = buf;
}
/**
* Start svg tag
* @param bgcolor null for none
* @param id CSS id or null for none
* @param clz CSS class or null for none
*/
public void startSVG(int width, int height, Color bgcolor, String id, String clz) {
buf.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" +
// "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n" +
"<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" ");
addIDClass(id, clz);
addString("viewBox", "0 0 " + width + ' ' + height);
addInt("width", width);
addInt("height", height);
buf.append(">\n");
if (bgcolor != null && bgcolor.getAlpha() > 0)
drawRect(0, 0, width, height, null, bgcolor, null, null, Collections.emptyMap());
}
/**
* End svg tag
*/
public void endSVG() {
buf.append("</svg>\n");
}
/**
* Start group
* @param id CSS id or null for none
* @param clz CSS class or null for none
* @param att an attribute to add att=val, e.g. "transform", or null for none
* @param val an attribute to add att=val, or null for none
*/
public void startGroup(String id, String clz, String att, String val) {
buf.append("<g ");
addIDClass(id, clz);
if (att != null && val != null)
addString(att, val);
buf.append(">\n");
}
/**
* End group
*/
public void endGroup() {
buf.append("</g>\n");
}
/**
* Define clip path
* @return a unique ID to pass to draw() calls
*/
public String defineClipPath(Rectangle clip) {
buf.append("<clipPath ");
String rv = "clip-" + hashCode() + '-' + (clipid++);
addIDClass(rv, null);
buf.append("><rect ");
addInt("x", (int) clip.getX());
addInt("y", (int) clip.getY());
addInt("width", (int) clip.getWidth());
addInt("height", (int) clip.getHeight());
buf.append("/></clipPath>\n");
return rv;
}
/**
* Draw circle centered on x,y with a radius given
* @param border null for none
* @param fill null for none
* @param clipid as returned from defineClipID() or null for none
* @param id CSS id or null for none
* @param clz CSS class or null for none
*/
public void drawCircle(int x, int y, int radius, Color border, Color fill, BasicStroke stroke, String clipid, Map<Object, Object> hints) {
buf.append("<circle ");
addAttributes(hints);
addClipPath(clipid);
addInt("cx", x);
addInt("cy", y);
addInt("r", radius);
addStroke("fill", fill, null);
addStroke("stroke", border, stroke);
String title = (String) hints.remove(KEY_ELEMENT_TITLE);
String inner = (String) hints.remove(KEY_ELEMENT_INNERSVG);
if (title != null || inner != null) {
buf.append(">\n");
addInner(title, inner);
buf.append("</circle>\n");
} else {
buf.append("/>\n");
}
}
/**
* Draw square centered on x,y with a width/height given
* @param border null for none
* @param fill null for none
* @param clipid as returned from defineClipID() or null for none
* @param id CSS id or null for none
* @param clz CSS class or null for none
*/
public void drawSquare(int x, int y, int sz, Color border, Color fill, BasicStroke stroke, String clipid, Map<Object, Object> hints) {
drawRect(x - (sz/2), y - (sz/2), sz, sz, border, fill, stroke, clipid, hints);
}
/**
* Draw rect
* @param border null for none
* @param fill null for none
* @param id CSS id or null for none
* @param clz CSS class or null for none
*/
public void drawRect(int x, int y, int width, int height, Color border, Color fill, BasicStroke stroke, String clipid, Map<Object, Object> hints) {
buf.append("<rect ");
addAttributes(hints);
addClipPath(clipid);
addInt("x", x);
addInt("y", y);
addInt("width", width);
addInt("height", height);
addStroke("fill", fill, null);
addStroke("stroke", border, stroke);
buf.append("shape-rendering=\"crispEdges\" ");
String title = (String) hints.remove(KEY_ELEMENT_TITLE);
String inner = (String) hints.remove(KEY_ELEMENT_INNERSVG);
if (title != null || inner != null) {
buf.append(">\n");
addInner(title, inner);
buf.append("</rect>\n");
} else {
buf.append("/>\n");
}
}
/**
* Draw line
* @param color null to let CSS do it
* @param clipid as returned from defineClipID() or null for none
* @param id CSS id or null for none
* @param clz CSS class or null for none
*/
public void drawLine(int x1, int y1, int x2, int y2, Color color, BasicStroke stroke, String clipid, Map<Object, Object> hints) {
buf.append("<line ");
addAttributes(hints);
addClipPath(clipid);
addInt("x1", x1);
addInt("y1", y1);
addInt("x2", x2);
addInt("y2", y2);
addStroke("stroke", color, stroke);
// don't do this for diagonal lines, it kills antialiasing
if (x1 == x2 || y1 == y2)
buf.append("shape-rendering=\"crispEdges\" ");
String title = (String) hints.remove(KEY_ELEMENT_TITLE);
String inner = (String) hints.remove(KEY_ELEMENT_INNERSVG);
if (title != null || inner != null) {
buf.append(">\n");
addInner(title, inner);
buf.append("</line>\n");
} else {
buf.append("/>\n");
}
}
/**
* Draw polyline
* @param color null to let CSS do it
* @param clipid as returned from defineClipID() or null for none
* @param id CSS id or null for none
* @param clz CSS class or null for none
*/
public void drawPolyline(int[] x, int[] y, int sz, Color color, BasicStroke stroke, String clipid, Map<Object, Object> hints) {
if (sz < 2)
return;
buf.append("<path ");
addAttributes(hints);
addClipPath(clipid);
buf.append("d=\"M");
buf.append(x[0]).append(',').append(y[0]);
for (int i = 1; i < sz; i++) {
// use relative coords to save a little space
buf.append('l');
buf.append(x[i] - x[i-1]).append(',').append(y[i] - y[i-1]);
}
buf.append("\" ");
addStroke("stroke", color, stroke);
buf.append("fill=\"none\" ");
// this is good for the horizontal/vertical paths drawn by rrd4j,
// but not so great for diagonal path segments
// Take our cue from the first segment
if (x[0] == x[1] || y[0] == y[1])
buf.append("shape-rendering=\"crispEdges\" ");
String title = (String) hints.remove(KEY_ELEMENT_TITLE);
String inner = (String) hints.remove(KEY_ELEMENT_INNERSVG);
if (title != null || inner != null) {
buf.append(">\n");
addInner(title, inner);
buf.append("</path>\n");
} else {
buf.append("/>\n");
}
}
/**
* Fill polygon
* @param color null to let CSS do it
* @param clipid as returned from defineClipID() or null for none
* @param id CSS id or null for none
* @param clz CSS class or null for none
*/
public void fillPolygon(int[] x, int[] y, int sz, Color color, String clipid, Map<Object, Object> hints) {
if (sz < 2)
return;
buf.append("<path ");
addAttributes(hints);
addClipPath(clipid);
buf.append("d=\"M");
buf.append(x[0]).append(',').append(y[0]);
for (int i = 1; i < sz; i++) {
// use relative coords to save a little space
buf.append('l');
buf.append(x[i] - x[i-1]).append(',').append(y[i] - y[i-1]);
}
buf.append("Z\" ");
addStroke("fill", color, null);
buf.append("stroke=\"none\" ");
// see above
if (x[0] == x[1] || y[0] == y[1])
buf.append("shape-rendering=\"crispEdges\" ");
String title = (String) hints.remove(KEY_ELEMENT_TITLE);
String inner = (String) hints.remove(KEY_ELEMENT_INNERSVG);
if (title != null || inner != null) {
buf.append(">\n");
addInner(title, inner);
buf.append("</path>\n");
} else {
buf.append("/>\n");
}
}
/**
* Draw text
* @param color null to let CSS do it
* @param font null to let CSS do it
* @param clipid as returned from defineClipID() or null for none
* @param id CSS id or null for none
* @param clz CSS class or null for none
*/
public void drawText(String text, int x, int y, Color color, Font font, String clipid, Map<Object, Object> hints) {
buf.append("<text ");
addAttributes(hints);
addClipPath(clipid);
addInt("x", x);
addInt("y", y);
addStroke("fill", color, null);
if (font != null) {
addString("font-family", font.getFamily());
buf.append("font-size=\"").append(font.getSize()).append("px\" ");
if (font.isBold())
buf.append("font-weight=\"bold\" ");
if (font.isItalic())
buf.append("font-style=\"italic\" ");
}
buf.append("text-rendering=\"optimizeLegibility\">").append(DataHelper.escapeHTML(text));
String title = (String) hints.remove(KEY_ELEMENT_TITLE);
if (title != null)
addInner(title, null);
buf.append("</text>\n");
}
private void addInt(String key, int val) {
buf.append(key).append("=\"").append(val).append("\" ");
}
private void addString(String key, String val) {
buf.append(key).append("=\"").append(DataHelper.escapeHTML(val)).append("\" ");
}
/**
* @param id CSS id or null for none
* @param clz CSS class or null for none
*/
private void addIDClass(String id, String clz) {
if (id != null)
addString("id", id);
if (clz != null)
addString("class", clz);
}
private void addAttributes(Map<Object, Object> hints) {
String id = (String) hints.remove(KEY_ELEMENT_ID);
if (id != null)
addString("id", id);
String clz = (String) hints.remove(KEY_ELEMENT_CLASS);
if (clz != null)
addString("class", clz);
Map<?,?> atts = (Map) hints.remove(KEY_ELEMENT_ATTMAP);
if (atts != null) {
for (Map.Entry e : atts.entrySet()) {
addString((String) e.getKey(), (String) e.getValue());
}
}
}
/**
* @param type "fill" or "stroke"
* @param color null to let CSS do it
* @param stroke null to omit for fill
*/
private void addStroke(String type, Color color, BasicStroke stroke) {
buf.append(type);
if (color != null) {
// todo getRGB() #tohex & 0xffffff
//buf.append("=\"rgb(").append(color.getRed())
// .append(',').append(color.getGreen())
// .append(',').append(color.getBlue())
// .append(")\" ");
buf.append("=\"#").append(String.format(Locale.US, "%06x", color.getRGB() & 0xffffff))
.append("\" ");
int alpha = color.getAlpha();
if (alpha < 255) {
buf.append(type).append("-opacity=\"")
.append(String.format(Locale.US, "%.2f", alpha / 255f))
.append("\" ");
}
} else {
// default is black opaque, so fixup for none
buf.append("=\"none\" ");
}
if (stroke != null) {
int width = (int) stroke.getLineWidth();
if (width > 0) {
if (width != 1)
buf.append(type).append("-width=\"").append(width).append("\" ");
float[] dash = stroke.getDashArray();
if (dash != null && dash.length > 1) {
buf.append("stroke-dasharray=\"");
for (int i = 0; i < dash.length; i++) {
buf.append((int) dash[i]);
if (i != dash.length - 1)
buf.append(' ');
}
buf.append("\" ");
}
}
}
}
/**
* @param clipid as received from defineClipPath() or null for none
*/
private void addClipPath(String clipid) {
if (clipid != null)
buf.append("clip-path='url(#").append(clipid).append(")' ");
}
/**
* @param title, will be XML escaped here, or null
* @param other full elements, must be XML escaped, or null
*/
private void addInner(String title, String inner) {
if (title != null)
buf.append(" <title>").append(DataHelper.escapeHTML(title)).append("</title>\n");
if (inner != null)
buf.append(" ").append(inner).append("\n");
}
/*
public void main(String[] args) {
StringBuilder buf = new StringBuilder(2048);
SimpleSVGMaker g = new SimpleSVGMaker(buf);
Font f = new Font("Dialog", Font.BOLD, 24);
Color c = new Color(255, 128, 128);
g.startSVG(190, 200, c, "id", "class");
g.startGroup("gid", "class", "transform", "matrix");
c = new Color(255, 0, 0);
BasicStroke s = new BasicStroke(4);
Map<Object,Object> hints = new java.util.HashMap<Object,Object>();
g.drawSquare(100, 36, 17, null, c, s, null, hints);
c = new Color(33, 33, 33, 128);
s = new BasicStroke(8);
g.drawCircle(75, 56, 27, c, null, s, null, hints);
g.drawCircle(100, 100, 110, c, null, s, null, hints);
c = new Color(0, 255, 0);
s = new BasicStroke(2);
g.drawLine(55, 96, 97, 178, c, s, null, hints);
int[] xx = { 10, 20, 30, 40, 150 };
int[] yy = { 81, 92, 113, 184, 29 };
c = new Color(0, 0, 255);
s = new BasicStroke(2);
g.drawPolyline(xx, yy, 5, c, s, null, hints);
Color cc = new Color(128, 128, 0, 128);
Color ccc = new Color(128, 0, 192, 128);
g.drawRect(100, 80, 40, 20, cc, ccc, s, null, hints);
c = new Color(0, 128, 128);
g.drawText("foo", 135, 156, c, f, null, hints);
c = new Color(128, 128, 0);
f = new Font(Font.SANS_SERIF, Font.ITALIC, 20);
g.drawText("bar", 115, 136, c, f, null, hints);
f = new Font(Font.SANS_SERIF, Font.PLAIN, 16);
g.drawText("baz", 115, 176, c, f, null, hints);
g.endGroup();
g.endSVG();
System.out.print(buf.toString());
}
*/
}

View File

@@ -19,6 +19,7 @@ import javax.imageio.stream.MemoryCacheImageOutputStream;
import net.i2p.I2PAppContext;
import net.i2p.data.DataHelper;
import net.i2p.rrd4j.SimpleSVGImageWorker;
import net.i2p.router.RouterContext;
import net.i2p.router.util.EventLog;
import static net.i2p.router.web.GraphConstants.*;
@@ -374,7 +375,8 @@ class SummaryRenderer {
RrdGraph graph;
try {
// NPE here if system is missing fonts - see ticket #915
graph = new RrdGraph(def);
SimpleSVGImageWorker svg = new SimpleSVGImageWorker(width, height);
graph = new RrdGraph(def, svg);
} catch (NullPointerException npe) {
_log.error("Error rendering graph", npe);
StatSummarizer.setDisabled(_context);
@@ -385,13 +387,8 @@ class SummaryRenderer {
StatSummarizer.setDisabled(_context);
throw new IOException("Error rendering - disabling graph generation. Missing font?");
}
int totalWidth = graph.getRrdGraphInfo().getWidth();
int totalHeight = graph.getRrdGraphInfo().getHeight();
BufferedImage img = new BufferedImage(totalWidth, totalHeight, BufferedImage.TYPE_USHORT_565_RGB);
Graphics gfx = img.getGraphics();
graph.render(gfx);
ios = new MemoryCacheImageOutputStream(out);
ImageIO.write(img, "png", ios);
out.write(graph.getRrdGraphInfo().getBytes());
out.flush();
_context.statManager().addRateData("graph.renderTime", System.currentTimeMillis() - begin);
} catch (RrdException re) {

View File

@@ -53,8 +53,9 @@ if ( !rendered && ((rs != null) || fakeBw) ) {
rendered = ss.getXML(rate, cout);
}
} else {
response.setContentType("image/png");
response.setHeader("Content-Disposition", "inline; filename=\"" + stat + ".png\"");
response.setContentType("image/svg+xml");
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Disposition", "inline; filename=\"" + stat + ".svg\"");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Accept-Ranges", "none");
// http://jira.codehaus.org/browse/JETTY-1346