diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/access/FilterDefinitionElement.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/access/FilterDefinitionElement.java index c9bace8aa..74ae27d02 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/access/FilterDefinitionElement.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/access/FilterDefinitionElement.java @@ -33,9 +33,14 @@ abstract class FilterDefinitionElement { * Utility method to create a Hash object from a .b32 string */ protected static Hash fromBase32(String b32) throws InvalidDefinitionException { + if (b32.length() != 60) + throw new InvalidDefinitionException("Invalid b32 " + b32); if (!b32.endsWith(".b32.i2p")) throw new InvalidDefinitionException("Invalid b32 " + b32); - b32 = b32.substring(0, b32.length() - 8); - return new Hash(Base32.decode(b32)); + String s = b32.substring(0, 52); + byte[] b = Base32.decode(s); + if (b == null) + throw new InvalidDefinitionException("Invalid b32 " + b32); + return Hash.create(b); } } diff --git a/apps/imagegen/build.gradle b/apps/imagegen/build.gradle index 776a8efe3..536bc6bed 100644 --- a/apps/imagegen/build.gradle +++ b/apps/imagegen/build.gradle @@ -16,6 +16,7 @@ sourceSets { dependencies { implementation project(':core') implementation project(':apps:jetty') + implementation project(':apps:jrobin') implementation fileTree("../jetty/apache-tomcat-${tomcatVersion}") } diff --git a/apps/imagegen/identicon/core/src/main/java/com/docuverse/identicon/NineBlockIdenticonRenderer2.java b/apps/imagegen/identicon/core/src/main/java/com/docuverse/identicon/NineBlockIdenticonRenderer2.java index 559e1a1a8..9a81a378b 100644 --- a/apps/imagegen/identicon/core/src/main/java/com/docuverse/identicon/NineBlockIdenticonRenderer2.java +++ b/apps/imagegen/identicon/core/src/main/java/com/docuverse/identicon/NineBlockIdenticonRenderer2.java @@ -153,7 +153,7 @@ public class NineBlockIdenticonRenderer2 implements IdenticonRenderer { } public BufferedImage render(BigInteger code, int size) { - return renderQuilt(code.intValue(), size); + return renderQuilt(null, code.intValue(), size); } /** @@ -172,10 +172,22 @@ public class NineBlockIdenticonRenderer2 implements IdenticonRenderer { * @return identicon image */ public BufferedImage render(int code, int size) { - return renderQuilt(code, size); + return renderQuilt(null, code, size); } - protected BufferedImage renderQuilt(int code, int size) { + /** + * @param g custom Graphics2D or null + * @since 0.9.64 + */ + public BufferedImage render(Graphics2D g, int code, int size) { + return renderQuilt(g, code, size); + } + + /** + * @param gg custom Graphics2D or null + * @since 0.9.64 + */ + protected BufferedImage renderQuilt(Graphics2D gg, int code, int size) { // ------------------------------------------------- // PREPARE // @@ -220,9 +232,15 @@ public class NineBlockIdenticonRenderer2 implements IdenticonRenderer { // RENDER // - BufferedImage targetImage = new BufferedImage(size, size, - BufferedImage.TYPE_INT_RGB); - Graphics2D g = targetImage.createGraphics(); + BufferedImage targetImage = null; + Graphics2D g; + if (gg != null) { + g = gg; + } else { + targetImage = new BufferedImage(size, size, + BufferedImage.TYPE_INT_RGB); + g = targetImage.createGraphics(); + } g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); @@ -256,7 +274,8 @@ public class NineBlockIdenticonRenderer2 implements IdenticonRenderer { drawPatch(g, 0, blockSize2, blockSize, cornerType, cornerTurn++, cornerInvert, fillColor, strokeColor); - g.dispose(); + if (gg == null) + g.dispose(); return targetImage; } diff --git a/apps/imagegen/imagegen/build.xml b/apps/imagegen/imagegen/build.xml index 68cb47660..39252ef8b 100644 --- a/apps/imagegen/imagegen/build.xml +++ b/apps/imagegen/imagegen/build.xml @@ -8,6 +8,7 @@ + diff --git a/apps/imagegen/imagegen/webapp/src/main/java/net/i2p/imagegen/IdenticonServlet.java b/apps/imagegen/imagegen/webapp/src/main/java/net/i2p/imagegen/IdenticonServlet.java index 63778a4cd..989009ae9 100644 --- a/apps/imagegen/imagegen/webapp/src/main/java/net/i2p/imagegen/IdenticonServlet.java +++ b/apps/imagegen/imagegen/webapp/src/main/java/net/i2p/imagegen/IdenticonServlet.java @@ -19,6 +19,7 @@ import com.docuverse.identicon.NineBlockIdenticonRenderer2; import net.i2p.I2PAppContext; import net.i2p.data.Hash; +import net.i2p.rrd4j.SimpleSVGGraphics2D; import net.i2p.util.ConvertToHash; import net.i2p.util.Log; @@ -67,10 +68,10 @@ public class IdenticonServlet extends HttpServlet { private static final String PARAM_IDENTICON_SIZE_SHORT = "s"; private static final String PARAM_IDENTICON_CODE_SHORT = "c"; private static final String IDENTICON_IMAGE_FORMAT = "PNG"; - private static final String IDENTICON_IMAGE_MIMETYPE = "image/png"; + private static final String IDENTICON_IMAGE_MIMETYPE = "image/svg+xml"; private static final long DEFAULT_IDENTICON_EXPIRES_IN_MILLIS = 24 * 60 * 60 * 1000; private int version = 1; - private final IdenticonRenderer renderer = new NineBlockIdenticonRenderer2(); + private final NineBlockIdenticonRenderer2 renderer = new NineBlockIdenticonRenderer2(); private IdenticonCache cache; private long identiconExpiresInMillis = DEFAULT_IDENTICON_EXPIRES_IN_MILLIS; @@ -147,6 +148,7 @@ public class IdenticonServlet extends HttpServlet { // retrieve image bytes from either cache or renderer if (cache == null || (imageBytes = cache.get(identiconETag)) == null) { +/* ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); RenderedImage image; try { @@ -160,6 +162,20 @@ public class IdenticonServlet extends HttpServlet { } ImageIO.write(image, IDENTICON_IMAGE_FORMAT, byteOut); imageBytes = byteOut.toByteArray(); +*/ + SimpleSVGGraphics2D g = new SimpleSVGGraphics2D(size, size); + try { + renderer.render(g, code, size); + } catch (Throwable t) { + // java.lang.NoClassDefFoundError: Could not initialize class java.awt.GraphicsEnvironment$LocalGE + Log log = I2PAppContext.getGlobalContext().logManager().getLog(IdenticonServlet.class); + //log.logAlways(Log.WARN, "Identicon render failure: " + t); + log.error("Identicon render failure", t); + response.setStatus(403); + return; + } + String s = g.getSVG(); + imageBytes = s.getBytes("UTF-8"); if (cache != null) cache.add(identiconETag, imageBytes); } else { @@ -178,6 +194,7 @@ public class IdenticonServlet extends HttpServlet { // return image bytes to requester response.setContentType(IDENTICON_IMAGE_MIMETYPE); + response.setCharacterEncoding("UTF-8"); response.setHeader("X-Content-Type-Options", "nosniff"); response.setHeader("Accept-Ranges", "none"); response.setContentLength(imageBytes.length); diff --git a/apps/jrobin/java/build.xml b/apps/jrobin/java/build.xml index 89f931ebf..b643d308a 100644 --- a/apps/jrobin/java/build.xml +++ b/apps/jrobin/java/build.xml @@ -37,6 +37,9 @@ encoding="UTF-8" includes="**/*.java" > + + + diff --git a/apps/jrobin/java/src/net/i2p/rrd4j/SimpleSVGGraphics2D.java b/apps/jrobin/java/src/net/i2p/rrd4j/SimpleSVGGraphics2D.java new file mode 100644 index 000000000..3df1ddd96 --- /dev/null +++ b/apps/jrobin/java/src/net/i2p/rrd4j/SimpleSVGGraphics2D.java @@ -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 hints = new HashMap(); + 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) {} +} diff --git a/apps/jrobin/java/src/net/i2p/rrd4j/SimpleSVGImageWorker.java b/apps/jrobin/java/src/net/i2p/rrd4j/SimpleSVGImageWorker.java new file mode 100644 index 000000000..4ddb53e73 --- /dev/null +++ b/apps/jrobin/java/src/net/i2p/rrd4j/SimpleSVGImageWorker.java @@ -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! + * + *
+ *      RRDGraph graph = new RrdGraph(graphdef, new SimpleSVGImageWorker(width, height));
+ *      outputstream.write(graph.getRrdGraphInfo().getBytes());
+ *
+ * + * 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); + } +} diff --git a/apps/jrobin/java/src/net/i2p/rrd4j/SimpleSVGMaker.java b/apps/jrobin/java/src/net/i2p/rrd4j/SimpleSVGMaker.java new file mode 100644 index 000000000..929274641 --- /dev/null +++ b/apps/jrobin/java/src/net/i2p/rrd4j/SimpleSVGMaker.java @@ -0,0 +1,439 @@ +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("\n" + + // "\n" + + "\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("\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("\n"); + } + + /** + * End group + */ + public void endGroup() { + buf.append("\n"); + } + + /** + * Define clip path + * @return a unique ID to pass to draw() calls + */ + public String defineClipPath(Rectangle clip) { + buf.append("\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 hints non-null + */ + public void drawCircle(int x, int y, int radius, Color border, Color fill, BasicStroke stroke, String clipid, Map hints) { + buf.append("\n"); + addInner(title, inner); + buf.append("\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 hints non-null + */ + public void drawSquare(int x, int y, int sz, Color border, Color fill, BasicStroke stroke, String clipid, Map 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 hints non-null + */ + public void drawRect(int x, int y, int width, int height, Color border, Color fill, BasicStroke stroke, String clipid, Map hints) { + buf.append("\n"); + addInner(title, inner); + buf.append("\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 hints non-null + */ + public void drawLine(int x1, int y1, int x2, int y2, Color color, BasicStroke stroke, String clipid, Map hints) { + buf.append("\n"); + addInner(title, inner); + buf.append("\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 hints non-null + */ + public void drawPolyline(int[] x, int[] y, int sz, Color color, BasicStroke stroke, String clipid, Map hints) { + if (sz < 2) + return; + buf.append("\n"); + addInner(title, inner); + buf.append("\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 hints non-null + */ + public void fillPolygon(int[] x, int[] y, int sz, Color color, String clipid, Map hints) { + if (sz < 2) + return; + buf.append("\n"); + addInner(title, inner); + buf.append("\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 hints non-null + */ + public void drawText(String text, int x, int y, Color color, Font font, String clipid, Map hints) { + buf.append("").append(DataHelper.escapeHTML(text)); + String title = (String) hints.remove(KEY_ELEMENT_TITLE); + if (title != null) + addInner(title, null); + buf.append("\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 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) { + // Output RGB or RGBA. getRGB() is ARGB. + buf.append("=\"#").append(String.format(Locale.US, "%06x", color.getRGB() & 0xffffff)); + int alpha = color.getAlpha(); + if (alpha < 255) + buf.append(String.format(Locale.US, "%02x", alpha)); + buf.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(" ").append(DataHelper.escapeHTML(title)).append("\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 hints = new java.util.HashMap(); + 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()); + } +*/ +} diff --git a/apps/jrobin/java/src/net/i2p/rrd4j/package.html b/apps/jrobin/java/src/net/i2p/rrd4j/package.html new file mode 100644 index 000000000..be6978e3c --- /dev/null +++ b/apps/jrobin/java/src/net/i2p/rrd4j/package.html @@ -0,0 +1,7 @@ + + +

+SVG renderer and adapter for rrd4j. Since 0.9.64. +

+ + diff --git a/apps/jrobin/java/src/org/rrd4j/data/CubicSplineInterpolator.java b/apps/jrobin/java/src/org/rrd4j/data/CubicSplineInterpolator.java deleted file mode 100644 index ed012ce8e..000000000 --- a/apps/jrobin/java/src/org/rrd4j/data/CubicSplineInterpolator.java +++ /dev/null @@ -1,179 +0,0 @@ -package org.rrd4j.data; - -import org.rrd4j.core.Util; - -import java.util.Calendar; -import java.util.Date; - -/** - * Class used to interpolate datasource values from the collection of (timestamp, values) - * points using natural cubic spline interpolation. - *

- * - * WARNING: So far, this class cannot handle NaN datasource values - * (an exception will be thrown by the constructor). Future releases might change this. - */ -@SuppressWarnings("deprecation") -public class CubicSplineInterpolator extends Plottable { - private final double[] x; - private final double[] y; - - // second derivates come here - private double[] y2; - - // internal spline variables - private int n, klo, khi; - - /** - * Creates cubic spline interpolator from arrays of timestamps and corresponding - * datasource values. - * - * @param timestamps timestamps in seconds - * @param values corresponding datasource values - * @throws java.lang.IllegalArgumentException Thrown if supplied arrays do not contain at least 3 values, or if - * timestamps are not ordered, or array lengths are not equal, or some datasource value is NaN. - */ - public CubicSplineInterpolator(long[] timestamps, double[] values) { - this.x = new double[timestamps.length]; - for (int i = 0; i < timestamps.length; i++) { - this.x[i] = timestamps[i]; - } - this.y = values; - validate(); - spline(); - } - - /** - * Creates cubic spline interpolator from arrays of Date objects and corresponding - * datasource values. - * - * @param dates Array of Date objects - * @param values corresponding datasource values - * @throws java.lang.IllegalArgumentException Thrown if supplied arrays do not contain at least 3 values, or if - * timestamps are not ordered, or array lengths are not equal, or some datasource value is NaN. - */ - public CubicSplineInterpolator(Date[] dates, double[] values) { - this.x = new double[dates.length]; - for (int i = 0; i < dates.length; i++) { - this.x[i] = Util.getTimestamp(dates[i]); - } - this.y = values; - validate(); - spline(); - } - - /** - * Creates cubic spline interpolator from arrays of GregorianCalendar objects and corresponding - * datasource values. - * - * @param dates Array of GregorianCalendar objects - * @param values corresponding datasource values - * @throws java.lang.IllegalArgumentException Thrown if supplied arrays do not contain at least 3 values, or if - * timestamps are not ordered, or array lengths are not equal, or some datasource value is NaN. - */ - public CubicSplineInterpolator(Calendar[] dates, double[] values) { - this.x = new double[dates.length]; - for (int i = 0; i < dates.length; i++) { - this.x[i] = Util.getTimestamp(dates[i]); - } - this.y = values; - validate(); - spline(); - } - - /** - * Creates cubic spline interpolator for an array of 2D-points. - * - * @param x x-axis point coordinates - * @param y y-axis point coordinates - * @throws java.lang.IllegalArgumentException Thrown if supplied arrays do not contain at least 3 values, or if - * timestamps are not ordered, or array lengths are not equal, or some datasource value is NaN. - */ - public CubicSplineInterpolator(double[] x, double[] y) { - this.x = x; - this.y = y; - validate(); - spline(); - } - - private void validate() { - boolean ok = true; - if (x.length != y.length || x.length < 3) { - ok = false; - } - for (int i = 0; i < x.length - 1 && ok; i++) { - if (x[i] >= x[i + 1] || Double.isNaN(y[i])) { - ok = false; - break; - } - } - if (!ok) { - throw new IllegalArgumentException("Invalid plottable data supplied"); - } - } - - private void spline() { - n = x.length; - y2 = new double[n]; - double[] u = new double[n - 1]; - y2[0] = y2[n - 1] = 0.0; - u[0] = 0.0; // natural spline - for (int i = 1; i <= n - 2; i++) { - double sig = (x[i] - x[i - 1]) / (x[i + 1] - x[i - 1]); - double p = sig * y2[i - 1] + 2.0; - y2[i] = (sig - 1.0) / p; - u[i] = (y[i + 1] - y[i]) / (x[i + 1] - x[i]) - (y[i] - y[i - 1]) / (x[i] - x[i - 1]); - u[i] = (6.0 * u[i] / (x[i + 1] - x[i - 1]) - sig * u[i - 1]) / p; - } - for (int k = n - 2; k >= 0; k--) { - y2[k] = y2[k] * y2[k + 1] + u[k]; - } - // prepare everything for getValue() - klo = 0; - khi = n - 1; - } - - /** - * Calculates spline-interpolated y-value for the corresponding x-value. Call - * this if you need spline-interpolated values in your code. - * - * @param xval x-value - * @return inteprolated y-value - */ - public double getValue(double xval) { - if (xval < x[0] || xval > x[n - 1]) { - return Double.NaN; - } - if (xval < x[klo] || xval > x[khi]) { - // out of bounds - klo = 0; - khi = n - 1; - } - while (khi - klo > 1) { - // find bounding interval using bisection method - int k = (khi + klo) / 2; - if (x[k] > xval) { - khi = k; - } - else { - klo = k; - } - } - double h = x[khi] - x[klo]; - double a = (x[khi] - xval) / h; - double b = (xval - x[klo]) / h; - return a * y[klo] + b * y[khi] + - ((a * a * a - a) * y2[klo] + (b * b * b - b) * y2[khi]) * (h * h) / 6.0; - } - - /** - * {@inheritDoc} - * - * Method overridden from the base class. This method will be called by the framework. Call - * this method only if you need spline-interpolated values in your code. - */ - public double getValue(long timestamp) { - return getValue((double)timestamp); - } - -} diff --git a/apps/jrobin/java/src/org/rrd4j/graph/RrdGraph.java b/apps/jrobin/java/src/org/rrd4j/graph/RrdGraph.java index 96edcc152..b03476bd7 100644 --- a/apps/jrobin/java/src/org/rrd4j/graph/RrdGraph.java +++ b/apps/jrobin/java/src/org/rrd4j/graph/RrdGraph.java @@ -147,8 +147,7 @@ public class RrdGraph implements RrdGraphConstants { drawAxis(); drawText(); drawLegend(); - drawRules(); - drawSpans(); + drawRulesAndSpans(); gator(); drawOverlay(); saveImage(); @@ -219,13 +218,17 @@ public class RrdGraph implements RrdGraphConstants { } } - private void drawRules() { - worker.clip(im.xorigin + 1, im.yorigin - gdef.height - 1, gdef.width - 1, gdef.height + 2); + private void drawRulesAndSpans() { + boolean found = false; for (PlotElement pe : gdef.plotElements) { if (pe instanceof HRule) { HRule hr = (HRule) pe; if (hr.value >= im.minval && hr.value <= im.maxval) { int y = mapper.ytr(hr.value); + if (!found) { + worker.clip(im.xorigin + 1, im.yorigin - gdef.height - 1, gdef.width - 1, gdef.height + 2); + found = true; + } worker.drawLine(im.xorigin, y, im.xorigin + im.xsize, y, hr.color, hr.stroke); } } @@ -233,31 +236,37 @@ public class RrdGraph implements RrdGraphConstants { VRule vr = (VRule) pe; if (vr.timestamp >= im.start && vr.timestamp <= im.end) { int x = mapper.xtr(vr.timestamp); + if (!found) { + worker.clip(im.xorigin + 1, im.yorigin - gdef.height - 1, gdef.width - 1, gdef.height + 2); + found = true; + } worker.drawLine(x, im.yorigin, x, im.yorigin - im.ysize, vr.color, vr.stroke); } } - } - worker.reset(); - } - - private void drawSpans() { - worker.clip(im.xorigin + 1, im.yorigin - gdef.height - 1, gdef.width - 1, gdef.height + 2); - for (PlotElement pe : gdef.plotElements) { - if (pe instanceof HSpan) { + else if (pe instanceof HSpan) { HSpan hr = (HSpan) pe; int ys = mapper.ytr(hr.start); int ye = mapper.ytr(hr.end); int height = ys - ye; + if (!found) { + worker.clip(im.xorigin + 1, im.yorigin - gdef.height - 1, gdef.width - 1, gdef.height + 2); + found = true; + } worker.fillRect(im.xorigin, ys - height, im.xsize, height, hr.color); } else if (pe instanceof VSpan) { VSpan vr = (VSpan) pe; int xs = mapper.xtr(vr.start); int xe = mapper.xtr(vr.end); + if (!found) { + worker.clip(im.xorigin + 1, im.yorigin - gdef.height - 1, gdef.width - 1, gdef.height + 2); + found = true; + } worker.fillRect(xs, im.yorigin - im.ysize, xe - xs, im.ysize, vr.color); } } - worker.reset(); + if (found) + worker.reset(); } private void drawText() { @@ -726,9 +735,15 @@ public class RrdGraph implements RrdGraphConstants { if (c instanceof LegendText) { // draw with BOX worker.fillRect(x, y - box, box, box, gdef.getColor(ElementsNames.frame)); - worker.fillRect(x + 1, y - box + 1, box - 2, box - 2, gdef.getColor(ElementsNames.canvas)); - worker.fillRect(x + 1, y - box + 1, box - 2, box - 2, gdef.getColor(ElementsNames.back)); - worker.fillRect(x + 1, y - box + 1, box - 2, box - 2, ((LegendText) c).legendColor); + Color bc = (Color) gdef.getColor(ElementsNames.back); + Color lc = (Color) ((LegendText) c).legendColor; + // no use drawing unless both the two on top have some transparency + if (bc.getAlpha() < 255 && lc.getAlpha() < 255) + worker.fillRect(x + 1, y - box + 1, box - 2, box - 2, gdef.getColor(ElementsNames.canvas)); + // no use drawing unless the one on top has some transparency + if (lc.getAlpha() < 255) + worker.fillRect(x + 1, y - box + 1, box - 2, box - 2, bc); + worker.fillRect(x + 1, y - box + 1, box - 2, box - 2, lc); worker.drawString(c.resolvedText, x + boxSpace, y, gdef.getFont(FONTTAG_LEGEND), gdef.getColor(ElementsNames.font)); } else { diff --git a/apps/routerconsole/java/src/net/i2p/router/web/HostCheckHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/HostCheckHandler.java index fcda30f56..aad652084 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/HostCheckHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/HostCheckHandler.java @@ -42,14 +42,12 @@ public class HostCheckHandler extends GzipHandler _context = ctx; _portMapper = ctx.portMapper(); _listenHosts = new HashSet(8); - setMinGzipSize(64*1024); + setMinGzipSize(32*1024); if (_context.getBooleanPropertyDefaultTrue(PROP_GZIP)) { addIncludedMimeTypes( - // our js is very small - //"application/javascript", "application/x-javascript", + "application/javascript", "application/x-javascript", "application/xhtml+xml", "application/xml", - // ditto svg - //"image/svg+xml", + "image/svg+xml", "text/css", "text/html", "text/plain" ); } else { diff --git a/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java b/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java index 324ce124e..989b728f2 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java @@ -45,7 +45,7 @@ public class StatSummarizer implements Runnable, ClientApp { private final Log _log; /** list of SummaryListener instances */ private final List _listeners; - private static final int MAX_CONCURRENT_PNG = SystemVersion.isSlow() ? 1 : 3; + private static final int MAX_CONCURRENT_PNG = SystemVersion.isSlow() ? 3 : 8; private final Semaphore _sem; private volatile boolean _isRunning; private volatile Thread _thread; diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryRenderer.java index 5f11698f7..61f705116 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryRenderer.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryRenderer.java @@ -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) { diff --git a/apps/routerconsole/jsp/configui.jsp b/apps/routerconsole/jsp/configui.jsp index 2712635d7..a6e3f922c 100644 --- a/apps/routerconsole/jsp/configui.jsp +++ b/apps/routerconsole/jsp/configui.jsp @@ -1,5 +1,4 @@ <%@page contentType="text/html"%> -<%@page trimDirectiveWhitespaces="true"%> <%@page pageEncoding="UTF-8"%> @@ -55,6 +54,7 @@ input.default {

<%=uihelper._t("Please contribute to the router console translation project! Contact the developers in #i2p-dev on IRC to help.")%> +<%=uihelper._t("See the {0}translation status report{1}.", "", "")%>


" > " > diff --git a/apps/routerconsole/jsp/debug.jsp b/apps/routerconsole/jsp/debug.jsp index 61004bc2b..f6e7a52cb 100644 --- a/apps/routerconsole/jsp/debug.jsp +++ b/apps/routerconsole/jsp/debug.jsp @@ -22,6 +22,7 @@ Router Session Key Manager Client Session Key Managers Router DHT +Translation Status
<% @@ -116,6 +117,27 @@ if (dd == null || dd.equals("0")) { out.print("

Router DHT

"); ctx.netDb().renderStatusHTML(out); +} else if (dd.equals("6")) { + + /* + * Print out the status of the translations + */ + java.io.InputStream is = this.getClass().getResourceAsStream("/net/i2p/router/web/resources/translationstatus.html"); + if (is == null) { + out.println("Translation status not available"); + } else { + java.io.Reader br = null; + try { + br = new java.io.InputStreamReader(is, "UTF-8"); + char[] buf = new char[4096]; + int read; + while ( (read = br.read(buf)) >= 0) { + out.write(buf, 0, read); + } + } finally { + if (br != null) try { br.close(); } catch (java.io.IOException ioe) {} + } + } } %> diff --git a/apps/routerconsole/jsp/themes/console/dark/console.css b/apps/routerconsole/jsp/themes/console/dark/console.css index deea993c7..f792911ca 100644 --- a/apps/routerconsole/jsp/themes/console/dark/console.css +++ b/apps/routerconsole/jsp/themes/console/dark/console.css @@ -7547,6 +7547,25 @@ b.netdb_transport { font-weight: bold; } +.debug_tx_total, .debug_tx_resource, .debug_tx_file { + width: auto; +} + +.debug_tx_total td:nth-child(3), +.debug_tx_total td:nth-child(4), +.debug_tx_resource td:nth-child(3), +.debug_tx_resource td:nth-child(4) { + text-align: right !important; +} + +.debug_tx_center, +.debug_tx_total td:nth-child(2), +.debug_tx_resource td:nth-child(2), +.debug_tx_file td:nth-child(2), +.debug_tx_file td:nth-child(3) { + text-align: center !important; +} + #portmapper td:first-child { font-weight: bold; } diff --git a/apps/routerconsole/jsp/themes/console/light/console.css b/apps/routerconsole/jsp/themes/console/light/console.css index b0989bac9..0814a32b2 100644 --- a/apps/routerconsole/jsp/themes/console/light/console.css +++ b/apps/routerconsole/jsp/themes/console/light/console.css @@ -7647,6 +7647,25 @@ b.netdb_transport { color: #050; } +.debug_tx_total, .debug_tx_resource, .debug_tx_file { + width: auto; +} + +.debug_tx_total td:nth-child(3), +.debug_tx_total td:nth-child(4), +.debug_tx_resource td:nth-child(3), +.debug_tx_resource td:nth-child(4) { + text-align: right; +} + +.debug_tx_center, +.debug_tx_total td:nth-child(2), +.debug_tx_resource td:nth-child(2), +.debug_tx_file td:nth-child(2), +.debug_tx_file td:nth-child(3) { + text-align: center !important; +} + /* end debug */ diff --git a/apps/routerconsole/jsp/viewstat.jsp b/apps/routerconsole/jsp/viewstat.jsp index 58768a005..28761ac26 100644 --- a/apps/routerconsole/jsp/viewstat.jsp +++ b/apps/routerconsole/jsp/viewstat.jsp @@ -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 diff --git a/build.xml b/build.xml index b48fb5975..0e515d8bc 100644 --- a/build.xml +++ b/build.xml @@ -54,6 +54,7 @@ + @@ -290,7 +291,7 @@ --> - + @@ -315,23 +316,23 @@ - + - + - + - + @@ -368,7 +369,7 @@ - + @@ -421,7 +422,7 @@ - + @@ -879,7 +880,7 @@ - + @@ -936,6 +937,9 @@ + + + @@ -1205,6 +1209,8 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1897,6 +1929,15 @@ + + + + + diff --git a/core/java/src/freenet/support/CPUInformation/AMDInfoImpl.java b/core/java/src/freenet/support/CPUInformation/AMDInfoImpl.java index 3c3da8d82..5f53c1dca 100644 --- a/core/java/src/freenet/support/CPUInformation/AMDInfoImpl.java +++ b/core/java/src/freenet/support/CPUInformation/AMDInfoImpl.java @@ -3,7 +3,8 @@ package freenet.support.CPUInformation; /** * Moved out of CPUID.java * - * Ref: http://en.wikipedia.org/wiki/List_of_AMD_CPU_microarchitectures + * Ref: https://en.wikipedia.org/wiki/List_of_AMD_CPU_microarchitectures + * Ref: https://gmplib.org/repo/gmp/file/tip/config.guess * * @since 0.8.7 */ @@ -468,7 +469,7 @@ class AMDInfoImpl extends CPUIDCPUInfo implements AMDCPUInfo } break; - // Zen / Zen+ / Zen2 / Zen3 / Ryzen 3/5/7/9/Threadripper / EPYC + // Zen / Zen+ / Zen2 / Zen3 / Zen4 / Ryzen 3/5/7/9/Threadripper / EPYC // untested case 23: case 25: { diff --git a/core/java/src/freenet/support/CPUInformation/IntelInfoImpl.java b/core/java/src/freenet/support/CPUInformation/IntelInfoImpl.java index f474bff4b..e0951d454 100644 --- a/core/java/src/freenet/support/CPUInformation/IntelInfoImpl.java +++ b/core/java/src/freenet/support/CPUInformation/IntelInfoImpl.java @@ -4,7 +4,8 @@ package freenet.support.CPUInformation; * Moved out of CPUID.java * * Ref: https://software.intel.com/en-us/articles/intel-architecture-and-processor-identification-with-cpuid-model-and-family-numbers - * Ref: http://en.wikipedia.org/wiki/List_of_Intel_CPU_microarchitectures + * Ref: https://en.wikipedia.org/wiki/List_of_Intel_CPU_microarchitectures + * Ref: https://gmplib.org/repo/gmp/file/tip/config.guess * * @since 0.8.7 */ @@ -521,6 +522,8 @@ class IntelInfoImpl extends CPUIDCPUInfo implements IntelCPUInfo case 0xa7: // Rocket Lake case 0x97: // Alder Lake case 0x9a: // Alder Lake + case 0xba: // Raptor Lake + case 0xb7: // Raptor Lake { CPUIDCPUInfo c = new CPUIDCPUInfo(); if (c.hasAVX2() && c.hasBMI1() && c.hasBMI2() && diff --git a/history.txt b/history.txt index e397ade9e..479fecb14 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,14 @@ +2024-07-25 zzz + * Console: + - Add translation status report (Gitlab MR !207) + - Convert graphs to SVG (Gitlab MR !208) + - Enable compression of js and svg files + * i2ptunnel: Additional fixes for invalid entries in filter file (Gitlab #483) + * imagegen: Convert identicons to SVG + * Installer: Fix some izpack5 translated languages + * rrd4j: More optimizations for graph generation + * Util: Add new processors for CPUID + 2024-07-19 2.6.0 (API 0.9.63) released 2024-07-19 idk diff --git a/installer/install5.xml b/installer/install5.xml index 23f69b2c3..cd696bed6 100644 --- a/installer/install5.xml +++ b/installer/install5.xml @@ -34,19 +34,81 @@ https://izpack.atlassian.net/wiki/spaces/IZPACK/pages/491730/GUI+Preferences + + + + + + + + + + @@ -65,10 +127,10 @@ https://izpack.atlassian.net/wiki/spaces/IZPACK/pages/491730/GUI+Preferences diff --git a/installer/lib/izpack5/patches/resources/flags/ind.gif b/installer/lib/izpack5/patches/resources/flags/ind.gif new file mode 100644 index 000000000..ae760382c Binary files /dev/null and b/installer/lib/izpack5/patches/resources/flags/ind.gif differ diff --git a/installer/lib/izpack5/patches/resources/flags/por.gif b/installer/lib/izpack5/patches/resources/flags/por.gif new file mode 100644 index 000000000..07147d9d1 Binary files /dev/null and b/installer/lib/izpack5/patches/resources/flags/por.gif differ diff --git a/installer/lib/izpack5/patches/resources/flags/zho.gif b/installer/lib/izpack5/patches/resources/flags/zho.gif new file mode 100644 index 000000000..4bfb96b26 Binary files /dev/null and b/installer/lib/izpack5/patches/resources/flags/zho.gif differ diff --git a/installer/lib/izpack5/patches/resources/installer/ind.xml b/installer/lib/izpack5/patches/resources/installer/ind.xml new file mode 100755 index 000000000..28af94717 --- /dev/null +++ b/installer/lib/izpack5/patches/resources/installer/ind.xml @@ -0,0 +1,352 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/installer/lib/izpack5/patches/resources/installer/por.xml b/installer/lib/izpack5/patches/resources/installer/por.xml new file mode 100755 index 000000000..4576c5eaa --- /dev/null +++ b/installer/lib/izpack5/patches/resources/installer/por.xml @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/installer/lib/izpack5/patches/resources/installer/zho.xml b/installer/lib/izpack5/patches/resources/installer/zho.xml new file mode 100755 index 000000000..ddf00c236 --- /dev/null +++ b/installer/lib/izpack5/patches/resources/installer/zho.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/installer/resources/checklist.md b/installer/resources/checklist.md index d65ca3257..ca7cf6d65 100644 --- a/installer/resources/checklist.md +++ b/installer/resources/checklist.md @@ -40,6 +40,8 @@ in override.properties to ensure that Android will build correcly; fix any issues. Ensure that JAVA_HOME is unset. +- Verify CI is passing on both Gitlab and Github + ## A day or two before @@ -67,6 +69,8 @@ 5. Update and check in blocklist-tor.txt with `maketorblocklist.sh` +6. Verify CI is passing on both Gitlab and Github + ## On release day diff --git a/installer/tools/java/src/net/i2p/util/TranslationStatus.java b/installer/tools/java/src/net/i2p/util/TranslationStatus.java new file mode 100644 index 000000000..f29ff27be --- /dev/null +++ b/installer/tools/java/src/net/i2p/util/TranslationStatus.java @@ -0,0 +1,449 @@ +package net.i2p.util; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import net.i2p.I2PAppContext; +import net.i2p.util.ObjectCounterUnsafe; + +/** + * Output translation stats by loading ResourceBundles from jars and wars, + * in html or text format. + * + * Bundles only, does not support external resources (html files, man pages, + * Debian po files) or the gettext properties files. + * + * This is run at build time, so output is not tagged or translated. + * + * @since 0.9.64 + */ +public class TranslationStatus { + + private final I2PAppContext _context; + private final boolean _html; + private final StringBuilder buf, buf2; + private final List langs; + private final Set foundLangs; + private final ObjectCounterUnsafe counts; + private final ObjectCounterUnsafe bundles; + + private static final String[] JARS = { "desktopgui.jar", "i2p.jar", + "i2psnark.war", "i2ptunnel.jar", "i2ptunnel.war", + "mstreaming.jar", "router.jar", "routerconsole.jar", + "susidns.war", "susimail.war" }; + + // Java lang codes, see notes below + private static final String[] LANGS = { "ar", "az", "bg", "ca", "cs", "da", "de", "el", "es", "es_AR", + "et", "fa", "fi", "fr", "gl", "hi", "hr", "hu", "in", "it", "iw", + "ja", "ko", "ku", "mg", "nb", "nl", "nn", "pl", "pt", "pt_BR", + "ro", "ru", "sk", "sl", "sq", "sr", "sv", "tk", "tr", "uk", "vi", + "zh", "zh_TW" }; + + private static final String[] FILES = { + "core/java/src/gnu/getopt/MessagesBundle.properties", + "apps/routerconsole/resources/docs/readme.html", // no country variants supported + "installer/resources/eepsite/docroot/help/index.html", + "installer/resources/locale-man/man.po", // non-Java + "installer/resources/locale/po/messages.po", // non-Java + "debian/po/.po" }; // non-Java + + public TranslationStatus(I2PAppContext ctx, boolean html) { + _context = ctx; + _html = html; + buf = new StringBuilder(65536); + buf2 = new StringBuilder(4096); + langs = Arrays.asList(LANGS); + counts = new ObjectCounterUnsafe(); + bundles = new ObjectCounterUnsafe(); + foundLangs = new HashSet(64); + } + +/* + only useful if we bundle this at runtime + + public String getStatus() throws IOException { + File base = _context.getBaseDir(); + File jars = new File(base, "lib"); + File wars = new File(base, "webapps"); + File[] files = new File[JARS.length]; + for (int i = 0; i < JARS.length; i++) { + String f = JARS[i]; + files[i] = new File(f.endsWith(".jar") ? jars : wars, f); + } + return getStatus(files); + } +*/ + + public String getStatus(File[] files) throws IOException { + buf.setLength(0); + buf2.setLength(0); + List classes = new ArrayList(64); + int grandtot = 0; + int resources = 0; + + // pass 1: for each file + for (int i = 0; i < files.length; i++) { + // pass 1A: collect the class names in the file + ZipFile zip = null; + try { + zip = new ZipFile(files[i]); + Enumeration entries = zip.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + String name = entry.getName(); + if (name.contains("messages_") && !name.contains("$")) { + if (name.startsWith("WEB-INF/classes/")) + name = name.substring(16); + classes.add(name); + } + } + Collections.sort(classes); + } catch (IOException ioe) { + ioe.printStackTrace(); + continue; + } finally { + if (zip != null) try { zip.close(); } catch (IOException e) {} + } + + if (classes.isEmpty()) { + System.err.println("No translations found in " + files[i]); + continue; + } + + // pass 1B: setup a classloader, load each class, calculate max strings + // note that to be accurate this requires, for each resource, at least one translation to be at 100% + // this is mostly true or close enough. To do it right would require parsing the English po file, + // since we don't compile it. + URL url; + if (files[i].getName().endsWith(".jar")) { + url = files[i].toURI().toURL(); + } else if (files[i].getName().endsWith(".war")) { + try { + url = (new URI("jar:file:" + files[i] + "!/WEB-INF/classes/")).toURL(); + } catch (URISyntaxException use) { continue; } + } else { + System.err.println("Not a jar/war file: " + files[i]); + continue; + } + URL[] urls = new URL[] { url }; + URLClassLoader cl = new URLClassLoader(urls); + + String pclz = ""; + int max = 0; + List buns = new ArrayList(64); + for (String name : classes) { + name = name.substring(0, name.length() - 6); // .class + int c = name.indexOf('_'); + String clz = name.substring(0, c).replace("/", "."); + if (!clz.equals(pclz)) { + // pass 1C: output a table for the resource + // output goes here, we have to make two passes to find the max + // number of entries to generate a true % + if (!buns.isEmpty()) { + report(pclz, max, buns); + resources++; + } + grandtot += max; + pclz = clz; + max = 0; + buns.clear(); + } + String s = name.substring(c + 1); + String lang; + String country; + Locale loc; + c = s.indexOf("_"); + if (c < 0) { + lang = s; + country = null; + loc = new Locale(lang); + } else { + lang = s.substring(0, c); + country = s.substring(c + 1); + loc = new Locale(lang, country); + } + foundLangs.add(loc); + ResourceBundle bun; + try { + bun = ResourceBundle.getBundle(clz, loc, cl); + } catch (Exception e) { + System.err.println("FAILED loading class " + clz + " lang " + lang + " country " + country); + continue; + } + // in this pass we just calculate the max strings + buns.add(bun); + Set keys = bun.keySet(); + int tot = keys.size() - 1; // exclude header + if (tot > max) + max = tot; + } + if (!buns.isEmpty()) { + report(pclz, max, buns); + grandtot += max; + resources++; + } + classes.clear(); + } + + nl(); + + // pass 2: resources not in jars/wars + resources += nonCompiledStatus(); + nl(); + + // pass 3: output summary table + + // from here down to buf2 so we can output it first + String h = "Translation Summary (" + resources + " resources, " + langs.size() + " languages, " + grandtot + " strings)"; + if (_html) { + buf2.append("

" + h + "

\n"); + buf2.append("

Note: % translated includes compiled resources only

\n"); + } else { + buf2.append(h); + buf2.append("\n\nNote: % translated includes compiled resources only\n\n"); + } + if (_html) { + buf2.append("
LanguageLanguage Code% TranslatedMissing Resources\n"); + } else { + buf2.append("Code\t %TX\tMissing\tLanguage"); + nl2(); + buf2.append("----\t------\t--------\t-------"); + nl2(); + } + List sorted = counts.sortedObjects(); + for (Locale loc : sorted) { + String s = loc.getLanguage(); + String lang = loc.getDisplayLanguage(); + String country = loc.getCountry(); + if (country.length() > 0) { + s += '_' + country; + country = '(' + loc.getDisplayCountry() + ')'; + } + if (_html) + buf2.append(String.format(Locale.US, "
%s %s%s%5.1f%%%d\n", lang, country, s, 100f * counts.count(loc) / grandtot, resources - bundles.count(loc))); + else + buf2.append(String.format("%s\t%5.1f%%\t%s %s\n", s, 100f * counts.count(loc) / grandtot, resources - bundles.count(loc), lang, country)); + } + if (_html) + buf2.append("
"); + nl2(); + nl2(); + if (_html) + buf2.append("

Compiled Resources

\n"); + else + buf2.append("Compiled Resources\n\n"); + String rv = buf2.toString() + buf.toString(); + buf.setLength(0); + buf2.setLength(0); + return rv; + } + + private void report(String clz, int max, List buns) { + nl(); + if (clz.endsWith(".messages")) + clz = clz.substring(0, clz.length() - 9); + if (_html) + buf.append("

Translations for " + clz + " (" + max + " strings, " + buns.size() + " translations)

"); + else + buf.append("\nTranslations for " + clz + " (" + max + " strings, " + buns.size() + " translations)\n"); + nl(); + if (_html) { + buf.append("
Language Language CodeTranslated% Translated"); + } else { + buf.append("Code\t TX\t %TX\tLanguage"); + nl(); + buf.append("----\t----\t------\t--------"); + nl(); + } + Set missing = new TreeSet(langs); + for (ResourceBundle bun : buns) { + //int not = 0; + //int same = 0; + //int tx = 0; + Set keys = bun.keySet(); + int tot = Math.max(0, keys.size() - 1); // exclude header + /* + for (String k : keys) { + try { + String v = bun.getString(k); + if (v.length() == 0) + not++; + else if (v.equals(k)) + same++; + else + tx++; + } catch (MissingResourceException e) { + not++; + } + } + */ + Locale loc = bun.getLocale(); + String lang = loc.getLanguage(); + String country = loc.getCountry(); + String dlang = loc.getDisplayLanguage(); + if (country.length() > 0) { + lang += '_' + country; + country = '(' + loc.getDisplayCountry() + ')'; + } + missing.remove(lang); + counts.add(loc, tot); + bundles.increment(loc); + if (_html) + buf.append(String.format(Locale.US, "
%s %s%s%4d%5.1f%%\n", dlang, country, lang, tot, 100f * tot / max)); + else + buf.append(String.format("%s\t%4d\t%5.1f%%\t%s %s\n", lang, tot, 100f * tot / max, dlang, country)); + } + if (!missing.isEmpty()) { + if (_html) + buf.append("
Not Translated\n"); + else + buf.append("Not translated:\n"); + for (String s : missing) { + String lang; + String country; + Locale loc; + int c = s.indexOf("_"); + if (c < 0) { + lang = s; + country = ""; + loc = new Locale(lang); + } else { + lang = s.substring(0, c); + country = s.substring(c + 1); + loc = new Locale(lang, country); + country = " (" + loc.getDisplayCountry() + ')'; + } + String dlang = loc.getDisplayLanguage(); + if (_html) + buf.append("
").append(dlang).append(country).append("").append(s).append("----\n"); + else + buf.append(s).append("\t--\t--\t").append(dlang).append(country).append('\n'); + } + } + if (_html) + buf.append("
"); + nl(); + } + + private int nonCompiledStatus() { + int rv = 0; + if (_html) { + buf.append("

Other Resources

\n"); + } else { + buf.append("\nOther Resources\n\n"); + } + for (String file : FILES) { + boolean nonJava = file.startsWith("debian/po/") || + file.startsWith("installer/resources/locale-man/") || + file.startsWith("installer/resources/locale/po/"); + boolean noCountries = file.startsWith("apps/routerconsole/resources/docs/"); + int dot = file.lastIndexOf("."); + int slash = file.lastIndexOf("/"); + String pfx = file.substring(slash + 1, dot); + String sfx = file.substring(dot); + String sdir = file.substring(0, slash); + // we assume we're in build/ + File dir = new File("..", sdir); + if (!dir.exists()) + continue; + rv++; + if (_html) { + buf.append("

Translations for " + file + "

\n"); + buf.append("
Language Language CodeTranslated?"); + } else { + buf.append("\nTranslations for " + file + "\n"); + buf.append("Code\tTX\tLanguage\n"); + buf.append("----\t--\t--------\n"); + } + for (String lg : LANGS) { + String njlg = lg; + if (nonJava) { + // non-java (debian, installer, man) undo conversion + if (lg.equals("in")) + njlg = "id"; + if (lg.equals("iw")) + njlg = "he"; + } + String sf; + if (pfx.length() > 0) + sf = pfx + '_' + njlg + sfx; + else + sf = njlg + sfx; + File f = new File(dir, sf); + boolean ok = f.exists(); + String lang; + String country; + Locale loc; + int c = lg.indexOf("_"); + if (c < 0) { + lang = lg; + country = ""; + loc = new Locale(lang); + } else { + lang = lg.substring(0, c); + country = lg.substring(c + 1); + loc = new Locale(lang, country); + country = " (" + loc.getDisplayCountry() + ')'; + } + String dlang = loc.getDisplayLanguage(); + String sok = (noCountries && c >= 0) ? "n/a" : (ok ? (_html ? "✔" : "yes") : (_html ? "--" : "no")); + if (_html) + buf.append("
").append(dlang).append(country).append("").append(lg).append("").append(sok).append("\n"); + else + buf.append(lg).append('\t').append(sok).append('\t').append(dlang).append(country).append("\n"); + if (ok || (noCountries && c >= 0)) + bundles.increment(loc); + if (ok) + foundLangs.add(loc); + } + if (_html) + buf.append("
"); + } + return rv; + } + + private void nl() { + buf.append(_html ? "
\n" : "\n"); + } + + private void nl2() { + buf2.append(_html ? "
\n" : "\n"); + } + + public static void main(String[] args) throws IOException { + boolean html = false; + if (args.length > 0 && args[0].equals("-h")) { + html = true; + args = Arrays.copyOfRange(args, 1, args.length); + } + if (args.length == 0) + args = JARS; + File[] files = new File[args.length]; + for (int i = 0; i < args.length; i++) { + String f = JARS[i]; + files[i] = new File(f); + } + TranslationStatus ts = new TranslationStatus(I2PAppContext.getGlobalContext(), html); + System.out.print(ts.getStatus(files)); + } +} diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 1984cf18c..f3f107529 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -20,7 +20,7 @@ public class RouterVersion { public final static String VERSION = CoreVersion.VERSION; /** for example: "beta", "alpha", "rc" */ public final static String QUALIFIER = ""; - public final static long BUILD = 0; + public final static long BUILD = 1; /** for example "-test" */ public final static String EXTRA = ""; public final static String FULL_VERSION = VERSION + "-" + BUILD + QUALIFIER + EXTRA;