You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Batik将SVG转为GraphicsNode时如何使用自定义字体?

问题:Batik转换含自定义字体的SVG为GraphicsNode时出现空指针异常

场景描述

在Java中使用Apache Batik和OpenPDF库将SVG嵌入PDF,流程为先创建GraphicsNode,再转换为ImgTemplate用于PDF文档。但当SVG使用资源文件夹中的自定义字体时,出现空指针异常,不含自定义字体的SVG转换正常。

初始转换代码

private GraphicsNode convert(String svgCode) throws IOException {
    final SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory(XMLResourceDescriptor.getXMLParserClassName());
    final SVGDocument svgDocument = factory.createSVGDocument("", new ByteArrayInputStream(svgCode.getBytes(StandardCharsets.UTF_8)));

    final UserAgent userAgent = new UserAgentAdapter();
    final DocumentLoader loader = new DocumentLoader(userAgent);

    final BridgeContext context = new BridgeContext(userAgent, loader);
    context.setDynamicState(BridgeContext.DYNAMIC);

    final GVTBuilder builder = new GVTBuilder();
    return builder.build(context, svgDocument);
}

首次报错信息

java.lang.NullPointerException: Cannot invoke "java.util.List.iterator()" because "this.srcs" is null

    at org.apache.batik.bridge.FontFace.getFontFamily(FontFace.java:98)
    at org.apache.batik.bridge.CSSFontFace.getFontFamily(CSSFontFace.java:176)
    at org.apache.batik.bridge.SVGFontUtilities.getFontFamily(SVGFontUtilities.java:144)

调试发现是FontFace使用了未初始化srcs元素的构造方法导致。

修改后尝试注册字体的代码

private GraphicsNode convert(String svgCode) throws IOException {
    //Create org.w3c.dom.svg.SVGDocument
    final SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory(XMLResourceDescriptor.getXMLParserClassName());
    final SVGDocument svgDocument = factory.createSVGDocument("", new ByteArrayInputStream(svgCode.getBytes(StandardCharsets.UTF_8)));

    //Create a org.apache.batik.gvt.GraphicsNode from the org.w3c.dom.svg.SVGDocument
    final UserAgent userAgent = new UserAgentAdapter();
    final DocumentLoader loader = new DocumentLoader(userAgent);

    // Notice, that you should use org.apache.batik.bridge.svg12.SVG12BridgeContext.SVG12BridgeContext for the svg version 1.2
    final BridgeContext context = new SVG12BridgeContext(userAgent, loader);
    context.setDynamicState(BridgeContext.DYNAMIC);

    //Register fonts
    final GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
    final Set<Font> loadedFonts = com.custom.FontFactory.getLoadedFonts();
    loadedFonts.forEach(graphicsEnvironment::registerFont);

    final GVTBuilder builder = new GVTBuilder();
    return builder.build(context, svgDocument);
}

com.custom.FontFactory返回已从资源加载的Set<Font>,但仍出现新的空指针异常:

java.lang.NullPointerException: Cannot invoke "java.util.Collection.toArray()" because "c" is null

    at java.base/java.util.LinkedList.addAll(LinkedList.java:419)
    at java.base/java.util.LinkedList.addAll(LinkedList.java:398)
    at java.base/java.util.LinkedList.<init>(LinkedList.java:130)
    at org.apache.batik.bridge.FontFace.createFontFace(FontFace.java:78)

直接通过文件路径注册字体也无效果。


解决方案

Batik处理自定义字体需要通过自定义UserAgent指定字体查找逻辑,仅注册到系统GraphicsEnvironment无法被Batik的桥接上下文识别。以下是可行步骤:

1. 实现自定义UserAgentAdapter

继承UserAgentAdapter,重写资源解析和字体解析方法,让Batik能正确加载自定义字体:

public class CustomUserAgent extends UserAgentAdapter {
    public CustomUserAgent() {}

    @Override
    public String resolveURI(String uri) {
        // 处理SVG中引用的相对路径字体文件,比如"fonts/MyCustomFont.ttf"
        if (uri.startsWith("fonts/")) {
            try {
                return getClass().getResource("/" + uri).toURI().toString();
            } catch (URISyntaxException e) {
                throw new RuntimeException("解析字体URI失败", e);
            }
        }
        return super.resolveURI(uri);
    }

    @Override
    public FontFamilyResolver getFontFamilyResolver() {
        return familyName -> {
            // 手动加载自定义字体并返回Batik所需的FontFamily实例
            try (InputStream fontStream = getClass().getResourceAsStream("/fonts/MyCustomFont.ttf")) {
                Font font = Font.createFont(Font.TRUETYPE_FONT, fontStream);
                // 可选:注册到系统,确保其他场景也能访问该字体
                GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont(font);
                return new FontFamily(familyName, new Font[]{font});
            } catch (IOException | FontFormatException e) {
                throw new RuntimeException("加载自定义字体失败", e);
            }
        };
    }
}

2. 修改转换代码使用自定义UserAgent

替换原有UserAgentAdapter为自定义实现,确保Batik处理字体时调用自定义逻辑:

private GraphicsNode convert(String svgCode) throws IOException {
    final SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory(XMLResourceDescriptor.getXMLParserClassName());
    final SVGDocument svgDocument = factory.createSVGDocument("", new ByteArrayInputStream(svgCode.getBytes(StandardCharsets.UTF_8)));

    // 使用自定义UserAgent
    final UserAgent userAgent = new CustomUserAgent();
    final DocumentLoader loader = new DocumentLoader(userAgent);

    final BridgeContext context = new BridgeContext(userAgent, loader);
    context.setDynamicState(BridgeContext.DYNAMIC);

    final GVTBuilder builder = new GVTBuilder();
    return builder.build(context, svgDocument);
}

3. 确保SVG字体引用正确

SVG中font-family名称需与自定义字体的名称一致,示例:

<style>
    @font-face {
        font-family: 'MyCustomFont';
        src: url('fonts/MyCustomFont.ttf') format('truetype');
    }
    .custom-text { font-family: 'MyCustomFont'; }
</style>

关键说明

  • Batik的字体解析逻辑依赖UserAgent提供的资源定位和字体解析能力,仅注册到系统无法被Batik桥接上下文识别。
  • 自定义FontFamilyResolver直接返回Batik所需的FontFamily实例,避免了默认逻辑中可能出现的空指针问题。

内容的提问来源于stack exchange,提问作者King Midas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 16:01:14