Search code examples
javasvgbatik

Batik - calculating bounds of cubic spline


I'm using Batik to work with SVG images. Specifically I have a scene with a number of shapes and I need to be able to convert each shape to a separate BufferedImage. To do this I use the following code:

SVGDocument document = null;

// Load the document
String parser = XMLResourceDescriptor.getXMLParserClassName();
SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parser);

File file = new File(inPath);
try {
    document = (SVGDocument) f.createDocument(file.toURL().toString());
} catch (MalformedURLException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

// Build the tree and get the document dimensions
UserAgentAdapter userAgentAdapter = new UserAgentAdapter();
BridgeContext bridgeContext = new BridgeContext(userAgentAdapter);

GVTBuilder builder = new GVTBuilder();

GraphicsNode graphicsNode = builder.build(bridgeContext, document);
CanvasGraphicsNode canvasGraphicsNode = (CanvasGraphicsNode)
        graphicsNode.getRoot().getChildren().get(0);

if(canvasGraphicsNode.getChildren().get(i) instanceof ShapeNode) {
   currentNode = (ShapeNode) canvasGraphicsNode.getChildren().get(i);
    convertNodeToImage (currentNode);
}

This is pretty standard. I fire up Batik and get it to parse the SVG file. Here's the convert node to image function:

Rectangle2D bounds;
BufferedImage bufferedImage;
Graphics2D g2d;

// This is supposed to get the bounds of the svg node. i.e. the rectangle which would
// fit perfectly around the shape   
bounds = sn.getSensitiveBounds();

// Transform the shape so it's in the top left hand corner based on the bounds
sn.setTransform(AffineTransform.getTranslateInstance(-bounds.getX(), -bounds.getY()));

// Create a buffered image of the same size as the svg node         
bufferedImage = new BufferedImage((int) bounds.getWidth(), (int) bounds.getHeight(),
                BufferedImage.TYPE_INT_ARGB);

// Paint the node to the buffered image and convert the buffered image to an input       
// stream           
g2d = (Graphics2D) bufferedImage.getGraphics();
sn.paint(g2d);

ByteArrayOutputStream os = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, "png", os);
InputStream is = new ByteArrayInputStream(os.toByteArray());
return is;

This works fine for rectangles and straight line shapes but it doesn't work for splines. For splines the bounds are greater than the rendered spline. I think this is because the getBounds function is including the control points in the bounds calculation. I need to find the bounds of just the spline i.e. if the spline were stroked I'd like to find the bounds of that stroke. I've tried all the getBounds() functions (getSensativeBounds, getGeometryBounds...) and they all give me the same result. So I'm wondering if I've missed something? This is a bug in Batik? Or if there's a workaround?

A workaround I thought of would be to get a list of vertices of the shape and calculate the bounds manually. I've been unable to find how to get a list of the outline vertices however.

Any help would be greatly appreciated.


Solution

  • For anyone who has this problem I found the solution. From the documentation it says that get bounds isn't guaranteed to provide the smallest bounds just some rectangle which completely contains the shape. This means that it's necessary to calculate the bounds manually. A spline is a mathematical definition of a shape i.e. a piecewise continuous function. This means that we have to calculate the spline to a certain accuracy. This is achieved by using a path iterator with a double for the degree of accuracy. This path iterator only returns LINE_TO commands which means it can be used to calculate the actual bounds of the shape:

    BufferedImage bufferedImage;
    Graphics2D g2d;
    
    // Manually calculate the bounds
    double [] vals = new double[7];
    
    double minX = Double.MAX_VALUE;
    double maxX = 0;
    
    double minY = Double.MAX_VALUE;
    double maxY = 0;
    
    // Get a path iterator iterating to a certain level of accuracy
    PathIterator pi = sn.getOutline().getPathIterator(null, 0.01);
    
    while(!pi.isDone()) {
        pi.currentSegment(vals);
    
        if(vals[0] < minX ) {
            minX = vals[0];
        }
        if(vals[0] > maxX ) {
            maxX = vals[0];
        }
        if(vals[1] < minY ) {
            minY = vals[1];
        }
        if(vals[1] > maxY ) {
            maxY = vals[1];
        }
    
        pi.next();
    }
    
    sn.setTransform(AffineTransform.getTranslateInstance(-minX, -minY));
    
    bufferedImage = new BufferedImage((int) (maxX - minX), (int) (maxY - minY),
                    BufferedImage.TYPE_INT_ARGB);
    
    g2d = (Graphics2D) bufferedImage.getGraphics();
    
    sn.paint(g2d);
    
    ByteArrayOutputStream os = new ByteArrayOutputStream();
    ImageIO.write(bufferedImage, "png", os);
    InputStream is = new ByteArrayInputStream(os.toByteArray());