为何Batik获取的SVG边界不准确?如何修复?
裁剪SVG空白时Batik的GraphicsNode.getSensitiveBounds()不准确的问题及解决方法
问题背景
需要编程裁剪SVG的多余透明边距,使用Apache Batik编写了trim方法,处理第一个SVG时可正常去除空白,但处理第二个SVG时失效,排查发现GraphicsNode.getSensitiveBounds()返回的边界不准确。
实现的trim方法
public static String trim(String svgXml) throws ParserConfigurationException, SAXException, IOException, TransformerFactoryConfigurationError, TransformerException { SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory(XMLResourceDescriptor.getXMLParserClassName()); SVGDocument document = factory.createSVGDocument("file://localhost/c:/", new StringReader(svgXml)); GVTBuilder builder = new GVTBuilder(); BridgeContext context = new BridgeContext(new UserAgentAdapter()); GraphicsNode node = builder.build(context, document); Rectangle2D boundingBox = node.getSensitiveBounds(); String x = String.valueOf(boundingBox.getX()); String y = String.valueOf(boundingBox.getY()); String width = String.valueOf(boundingBox.getWidth()); String height = String.valueOf(boundingBox.getHeight()); String viewBox = String.join(" ", x, y, width, height); Element svgNode = (Element) stream(document.getChildNodes()) .filter(n -> n.getNodeName().equalsIgnoreCase("svg")).findFirst().get(); svgNode.setAttribute("viewBox", viewBox); svgNode.setAttribute("width", width); svgNode.setAttribute("height", height); ByteArrayOutputStream stream = new ByteArrayOutputStream(); Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.transform(new DOMSource(document), new StreamResult(stream)); return stream.toString(); }
测试用SVG示例
可正常处理的SVG
<svg style="border: 1px dashed black" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="150" height="100" viewBox="0 0 150 100" xml:space="preserve"> <desc>Created with Fabric.js 4.4.0</desc> <defs> </defs> <g transform="matrix(1 0 0 1 75 50)"> <g style=""> <g transform="matrix(0.83 0 0 0.83 0 0.75)" id="path3914"> <path style="stroke: rgb(0,0,0); stroke-width: 3; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); fill-opacity: 0; fill-rule: nonzero; opacity: 1;" transform=" translate(-106.37, -106.56)" d="M 173.5258 164.62704 L 39.223861 164.7471 L 106.27086 48.378182 Z" stroke-linecap="round"/> </g> <g transform="matrix(4.99 0 0 4.99 0.5 5.36)" style="" id="text6366"> <text x="-2.82" y="1.18" xml:space="preserve" font-family="Times New Roman" font-size="3.76296" font-style="normal" font-weight="normal" style="stroke: rgb(0,0,0); stroke-width: 0.166666; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1; white-space: pre;">ME</text> </g> <g transform="matrix(4.99 0 0 4.99 0.79 26.98)" style="" id="text397"> <text x="-3.18" y="1.33" xml:space="preserve" font-family="Times New Roman" font-size="4.23334" font-style="normal" font-weight="normal" style="stroke: rgb(0,0,0); stroke-width: 0.2; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1; white-space: pre;">322</text> </g> </g> </g> </svg>
处理失效的SVG
<svg style="border: 1px dashed black" width="50mm" height="50mm" viewBox="0 0 400 400" version="1.1" id="svg5" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <defs id="defs2" /> <g id="layer1"> <path style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:5.56777;stroke-dasharray:none" id="path1025" d="m 132.29166,145.88567 188.95027,327.27148 -377.900552,-10e-6 z" transform="matrix(0.83095221,0,0,0.95950522,90.073162,-97.347212)" /> <text xml:space="preserve" style="font-size:40.8983px;text-align:center;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.885595;stroke-dasharray:none" x="198.31625" y="228.99879" id="text1081"><tspan style="font-size:40.8983px;fill:#000000;fill-opacity:1;stroke-width:0.885595;stroke-dasharray:none" x="198.31625" y="228.99879" id="tspan1443">ME</tspan></text> <text xml:space="preserve" style="font-size:40.8983px;text-align:center;text-anchor:middle;fill:#000000;stroke:#000000;stroke-width:0.885595" x="196.43968" y="269.51633" id="text347"><tspan id="tspan345" style="stroke-width:0.885595" x="196.43968" y="269.51633">373</tspan></text> </g> </svg>
原因分析
GraphicsNode.getSensitiveBounds()的设计目标是返回元素的可交互区域边界,而非视觉渲染的真实边界:
- 对于设置了
text-anchor:middle的文本元素,敏感边界可能基于文本锚点的逻辑范围计算,而非实际文本的视觉宽度 - 对于
fill-opacity:0但有stroke的路径,敏感边界可能未正确包含stroke的宽度,或在处理复杂transform时出现计算偏差 - 该方法优先适配交互场景的点击区域需求,不适合用于视觉裁剪的边界计算
修复方法
核心修改:替换为getBounds()方法
将node.getSensitiveBounds()替换为node.getBounds(),该方法返回元素的视觉几何边界,会正确包含stroke宽度、transform、文本实际渲染范围等所有视觉属性的影响,完全适配裁剪SVG空白的需求。
修改后的代码片段:
// 替换原有的boundingBox获取逻辑 Rectangle2D boundingBox = node.getBounds();
额外优化:处理精度问题
计算出的边界坐标可能包含大量小数位,可通过格式化保留合适的小数位数,避免viewBox出现冗余数值:
// 在获取坐标前添加格式化逻辑 DecimalFormat df = new DecimalFormat("#.##"); String x = df.format(boundingBox.getX()); String y = df.format(boundingBox.getY()); String width = df.format(boundingBox.getWidth()); String height = df.format(boundingBox.getHeight());
需导入java.text.DecimalFormat类。
验证效果
修改后重新处理第二个SVG,getBounds()会正确计算出包含路径stroke和文本的真实视觉边界,生成的新SVG会自动去除多余的透明边距。
内容的提问来源于stack exchange,提问作者Cardinal System
相关产品推荐
相关产品推荐

