夜雨聆风学习资料网

ARTICLE · 1209239

【JQUICK-PDF・柱状图】信用报告刚上线,领导指着 PDF 里的乱码问我:为什么柱状图标题全是「口口口」?

【JQUICK-PDF・柱状图】信用报告刚上线,领导指着 PDF 里的乱码问我:为什么柱状图标题全是「口口口」?

官网示例:

  • FORMULAS 示例页:https://www.jquick.org/jquick-pdf/#demo-biz16
  • github 地址:https://github.com/paohaijiao/jquick-pdf

一、故事引入

上个月,我把征信系统的「客户信用评级分布报告」从快照接口迁移到了 jquick-pdf 渲染。代码很简单,一个柱状图,一个模板绑定:

JOption option = new JOption();option.title().text("客户信用评级分布(2026-Q3)");JBar bar = new JBar();bar.name("评级户数").data(320, 580, 410, 190, 85);String svg = JChartRendererFactory.renderChart(        JChartType.BAR, option, new JBar[]{bar},        new JCategoryAxis().data("AAA", "AA", "A", "BBB", "BB"),        new JValueAxis());JPdfConfig config = new JPdfConfig();byte[] bytes = new JQuickPdfFactory(config)        .bind("svg", svg)        .executeResource("report/credit_report_01_bar.txt");

本地跑得好好的,一上测试环境,柱状图区域里全是「口口口」。领导把截图甩我脸上:「图表标题全乱码了,评级也全是方块。」

我第一反应是字体没注册。但奇怪的是——模板里的表格中文正常,偏偏图表里的中文乱码。同一个 PDF,一半正常一半乱码,这问题邪门。

我把现场代码原封不动丢给三个 AI,问它们「为什么 SVG 绑定进模板后中文变方块」。三个 AI 的回答高度一致,而且——全都错了。

二、AI 集体翻车

AI-A 说:「这是 PDF 编码问题,把模板文件转成 UTF-8 with BOM 就好了。」

AI-B 说:「executeResource 内部用了系统默认字符集读模板,Windows 上要加 -Dfile.encoding=UTF-8 启动参数。」

AI-C 说:「SVG 字符串里有中文字体名,需要取消 SVG 里的 font-family,让它继承 PDF 默认字体。」

共同点:三个 AI 都在「字符集 / 编码」层面打转,没有一个人提到 渲染字符串型文本时,图表文本走的是哪条渲染管线。

实际上核心问题根本不是编码。SVG 本身就是 UTF-8 字符串,绑定进模板没有任何编码损失。真正的坑在于——jquick-pdf 里 数据文本和图表文本走了两条完全不同的字体解析路径。

三、底层原理拆解

原生裸框架:数据文本 vs SVG 文本,两套逻辑

先看调用链。JQuickPdfFactory.executeResource() 本质上是三件事:

  1. 从 classpath 读模板文件(credit_report_01_bar.txt),解析成 DOM 树;
  2. 模板里出现 {svg} 都是内存字符串操作,不存在编码损失。转 BOM 只会让 <pdf> 根节点多一个不可见字符。
  3. 「file.encoding 启动参数」
    :错。这是 JDK 18 之前的经典坑,但它影响的是 new String(byte[]) 这种隐式编码转换。绑定 SVG 走的是 Map<String,Object>,与系统字符集无关。
  4. 「去掉 SVG 的 font-family」
    :方向沾边但结论错。SVG 里原本就有字体声明,删掉声明 SVG 解析器就会用默认字体(通常是不含中文的 Helvetica),乱码依旧。
  5. 真正该辨析的 API 差异是:bind() 是纯数据注入,executeResource() 是模板渲染。乱码发生在渲染阶段,跟绑定无关。修编码、修启动参数,都是在错误的层打补丁。

    五、延伸隐藏坑

    这个坑有三个衍生版本:

    1. Linux 服务器上「好了」
      :很多生产环境是 UTF-8 locale,某些 SVG 解析器在不同系统上字体回退行为不同,本地 Windows 复现不了,上线才发现。别用环境差异当遮羞布。
    2. 数字和英文永远正常
      :图表里的数字标签走的是 SVG 默认字体,ASCII 字符都有兜底,所以很多报表「只坏中文」,一眼看不出是字体问题还是编码问题。
    3. Spring 单例工厂的并发残留
      :JQuickPdfFactory 内部有可变 context(变量绑定表)。Spring 单例共享一个实例时,多线程并发 bind() 会互相覆盖变量——这是比乱码更严重的线上隐患,后面单独写一篇。

    六、推荐正确写法

    原生环境,按下面这样写,一次成型:

    // 1. 先配置字体,再建工厂JPdfConfig config = new JPdfConfig();config.fontConfig().registerFont("custom-mid", "STSongStd-Light",        "/fonts/stsong.ttf");   // 注册图表 SVG 能认到的字体// 2. 图表:SVG 文本显式指定已注册字体族JOption option = new JOption();option.title().text("客户信用评级分布(2026-Q3)")      .fontFamily("custom-mid");          // 关键:让 SVG 文本用注册字体option.legend().fontFamily("custom-mid");JBar bar = new JBar();bar.name("评级户数").data(320, 580, 410, 190, 85);String svg = JChartRendererFactory.renderChart(        JChartType.BAR, option,        new JBar[]{bar},        new JCategoryAxis().data("AAA", "AA", "A", "BBB", "BB"),        new JValueAxis());// 3. 模板仅做数据注入,编码全靠 JVM 内部字符串JPdfConfig renderConfig = new JPdfConfig();byte[] bytes = new JQuickPdfFactory(renderConfig)        .bind("svg", svg)        .executeResource("report/credit_report_01_bar.txt");

    改造思路就一条:让图表文本的字体解析路径和表格文本保持同源。不要靠「碰巧先把字体注册了」这种隐式时序,要在生成 SVG 之前就把字体显式挂进 option。

    七、实战验证方案

    别信嘴炮,用证据说话。给 JTemplatePdfBoxRenderer 挂上日志,观察两段文本的字体解析结果:

    # 打开 jquick-pdf 渲染日志(logback 或 log4j 配置)<logger name="com.github.paohaijiao.visitor.render" level="DEBUG"/>

    日志里会看到两类记录:

    • resolveTextFont(text=客户信用评级分布, resolved=STSongStd-Light)
       —— 表格文本正确命中内置字体;
    • resolveSvgFontFamily(family=Helvetica, glyph=客户, missing=true)
       —— 图表 SVG 命中默认字体、缺字形。

    再看字节码层面:把乱码 PDF 用 PDFBox 打开,PDType0Font 的字形映射表里 客户 的码点对应 .notdef,而正常 PDF 对应真实字形索引。内存地址实证:透传 -Dorg.apache.pdfbox.io.RandomAccessRead 断点,观察字体对象 font.getFontDescriptor() 的 identity 是否与注册实例一致。

    八、结尾感悟

    这个 bug 让我重新信了一条老话:框架的「默认能用」,从来不是承诺,只是巧合的积累。表格中文能用,不代表图表中文能用——两条渲染管线,两种字体来源,中间隔着一个 SVG 解析器。

    把「现象」和「层」对应清楚,比记住一百个修复技巧更值钱。

    如果你也被这类「一半正常一半乱码」的报表坑过,或者踩过 jquick-pdf 别的隐性坑,欢迎点赞、在看、转发,评论区聊聊你的翻车现场。


    📎 附:完整源码与模板(可直接复现)

    完整 Java 测试代码

     JOption option = new JOption();        option.title().text("信用评级指标对比").subtext("2020-2023年度(单位:万元)");        option.tooltip().trigger(JTrigger.axis);        JCategoryAxis xAxis = new JCategoryAxis();        xAxis.data("2020年", "2021年", "2022年", "2023年");        option.xAxis(xAxis);        option.yAxis(new JValueAxis());        JBar revenue = new JBar();        revenue.name("营业收入").data(8560, 12340, 18920, 25680);        JBar profit = new JBar();        profit.name("净利润").data(1240, 2150, 3890, 5420);        option.series(revenue, profit);        String svg = JChartRendererFactory.renderChart(JChartType.BAR, option);        File dir = new File(path + "creditreport");        if (!dir.exists()) {            dir.mkdirs();        }        FileOutputStream fileOutputStream = new FileOutputStream(path + "creditreport/01_bar.pdf");        JPdfConfig config = new JPdfConfig();        // 开启 flex 行布局:图表与其右侧的指标说明面板在宽度允许时并排,而不是换行堆叠。        config.getLayoutConfig().setFlexLayout(true);        JQuickPdfFactory factory = new JQuickPdfFactory(config);        factory.bind("svg", svg);        byte[] bytes = factory.executeResource("report/credit_report_01_bar.txt");        fileOutputStream.write(bytes);        fileOutputStream.close();

    完整模板文件(txt)

    <pdf><body>  <divstyle="textAlignment:center; marginBottom:5px; paddings:16px; background:#3E6B9D; color:white; borderRadius:4px">    <h1style="textAlignment:center;color:white; marginBottom:8px; fontSize:20; bold:true">'企业信用评级报告'</h1>    <pstyle="textAlignment:center;color:rgba(255,255,255,0.9); fontSize:11; margin:2px">'报告编号: CR-2023-08975 | 评估日期: 2023年11月15日'</p>    <pstyle="textAlignment:center;color:rgba(255,255,255,0.9); fontSize:11; margin:2px">'评估机构: 四川省企业信用评估中心'</p>  </div>  <divstyle="marginBottom:3px;borderRadius:4px;">    <divstyle="padding:12px">      <h2style="color:#2c3e50; marginTop:0; marginBottom:8px; fontSize:14; bold:true">'四川省泡海椒科技有限公司'</h2>      <tablestyle="width:600px;verticalAlignment:center; fontSize:10">        <tr>          <tdstyle="backgroundColor:#f8f9fa;padding:5px; textAlign:left;width:150px;">'统一信用代码'</td>          <tdstyle="backgroundColor:#f8f9fa;padding:5px;width:150px;">'91110108MA01XX1234'</td>          <tdstyle="backgroundColor:#f8f9fa; padding:5px; textAlign:left;width:150px;">'成立日期'</td>          <tdstyle="padding:5px; width:150px;">'2018年5月20日'</td>        </tr>        <tr>          <tdstyle="backgroundColor:#f8f9fa; padding:5px; textAlign:left;">'注册资本'</td>          <tdstyle="padding:5px;">'1000万元人民币'</td>          <tdstyle="backgroundColor:#f8f9fa; padding:5px; textAlign:left;">'法定代表人'</td>          <tdstyle="padding:5px;">'泡海椒'</td>        </tr>        <tr>          <tdstyle="backgroundColor:#f8f9fa; padding:5px; textAlign:left;">'所属行业'</td>          <tdstyle="padding:5px;">'软件和信息技术服务业'</td>          <tdstyle="backgroundColor:#f8f9fa; padding:5px; textAlign:left;">'企业规模'</td>          <tdstyle="padding:5px;">'中型(150-200人)'</td>        </tr>      </table>    </div>  </div>  <divstyle="margin-bottom:15px">    <h2style="color:#3498db; borderBottom:1px solid #3498db; paddingBottom:4px; fontSize:13; marginBottom:10px">'历年财务数据(单位:万元)'</h2>    <tablestyle="width:600px; fontSize:10; marginTop:8px">      <tr>        <thstyle="backgroundColor:#3498db; color:white; padding:6px; textAlign:center; border:1px solid #dee2e6">'财务指标'</th>        <thstyle="backgroundColor:#3498db; color:white; padding:6px; textAlign:center; border:1px solid #dee2e6">'2020年'</th>        <thstyle="backgroundColor:#3498db; color:white; padding:6px; textAlign:center; border:1px solid #dee2e6">'2021年'</th>        <thstyle="backgroundColor:#3498db; color:white; padding:6px; textAlign:center; border:1px solid #dee2e6">'2022年'</th>        <thstyle="backgroundColor:#3498db; color:white; padding:6px; textAlign:center; border:1px solid #dee2e6">'2023年'</th>        <thstyle="backgroundColor:#3498db; color:white; padding:6px; textAlign:center; border:1px solid #dee2e6">'增长率'</th>      </tr>      <tr>        <tdstyle="backgroundColor:#f8f9fa; padding:5px; textAlign:left; border:1px solid #dee2e6; bold:true">'营业收入'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6">'8,560'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6">'12,340'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6">'18,920'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6">'25,680'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6; color:#4CAF50; bold:true">'+44.2%'</td>      </tr>      <tr>        <tdstyle="backgroundColor:#f8f9fa; padding:5px; textAlign:left; border:1px solid #dee2e6; bold:true">'净利润'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6">'1,240'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6">'2,150'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6">'3,890'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6">'5,420'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6; color:#4CAF50; bold:true">'+63.8%'</td>      </tr>      <tr>        <tdstyle="backgroundColor:#f8f9fa; padding:5px; textAlign:left; border:1px solid #dee2e6; bold:true">'总资产'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6">'15,800'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6">'22,450'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6">'31,200'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6">'42,800'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6; color:#4CAF50; bold:true">'+39.5%'</td>      </tr>      <tr>        <tdstyle="backgroundColor:#f8f9fa; padding:5px; textAlign:left; border:1px solid #dee2e6; bold:true">'净资产'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6">'9,200'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6">'13,800'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6">'19,500'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6">'26,400'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6; color:#4CAF50; bold:true">'+42.1%'</td>      </tr>      <tr>        <tdstyle="backgroundColor:#f8f9fa; padding:5px; textAlign:left; border:1px solid #dee2e6; bold:true">'毛利率'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6">'42.5%'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6">'45.8%'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6">'48.2%'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6">'51.6%'</td>        <tdstyle="padding:5px; textAlign:right; border:1px solid #dee2e6; color:#4CAF50; bold:true">'+9.1%'</td>      </tr>    </table>  </div>  <divstyle="margin-bottom:15px">    <h2style="color:#3498db; border-bottom:1px solid #3498db; padding-bottom:4px; font-size:13px; margin-bottom:10px">'关键经营指标'</h2>    <liststyle="list-style-type:none; padding-left:0; font-size:11px">      <listyle="margin-bottom:8px; padding:10px; background-color:#f8f9fa; border-left:4px solid #4CAF50; border-radius:0 4px 4px 0">        '营收增长率:连续三年保持40%以上增长,2023年达到25,680万元'      </li>      <listyle="margin-bottom:8px; padding:10px; background-color:#f8f9fa; border-left:4px solid #2196F3; border-radius:0 4px 4px 0">        '利润率提升:净利率从14.5%提升至21.1%,盈利能力显著增强'      </li>      <listyle="margin-bottom:8px; padding:10px; background-color:#f8f9fa; border-left:4px solid #FF9800; border-radius:0 4px 4px 0">        '研发投入:研发费用年均增长38.9%,占营收比例达13.3%'      </li>      <listyle="margin-bottom:8px; padding:10px; background-color:#f8f9fa; border-left:4px solid #9C27B0; border-radius:0 4px 4px 0">        '人才结构:技术研发人员占比42%,硕士以上学历占35%'      </li>      <listyle="margin-bottom:8px; padding:10px; background-color:#f8f9fa; border-left:4px solid #E91E63; border-radius:0 4px 4px 0">        '客户质量:服务15家上市公司,客户续约率92%,大客户满意度4.8/5.0'      </li>      <listyle="margin-bottom:8px; padding:10px; background-color:#f8f9fa; border-left:4px solid #009688; border-radius:0 4px 4px 0">        '资产质量:资产负债率维持在38%左右,流动比率2.8,偿债能力良好'      </li>    </list>  </div>  <divstyle="margin-bottom:15px">    <h2style="color:#3498db; border-bottom:1px solid #3498db; padding-bottom:4px; font-size:13px; margin-bottom:10px">'信用评级条形图'</h2>    <divstyle="display:flex; justify-content:space-between; margin-top:10px">      <svgstyle="width:340px;height:340px">${svg}</svg>      <divstyle="width:290px">        <divstyle="backgroundColor:#f8f9fa; padding:15px; borderRadius:4px">          <h3style="color:#2c3e50; margin-top:0; fontSize:12; marginBottom:8px">'评级指标说明'</h3>          <liststyle="list-style-type:none; padding-left:0; font-size:10px">            <listyle="margin-bottom:6px; padding:5px; border-left:3px solid #3498db">              <spanstyle="bold:true; color:#2c3e50">'财务状况(90分):' </span>              '营收稳定增长'            </li>            <listyle="margin-bottom:6px; padding:5px; border-left:3px solid #2ecc71">              <spanstyle="bold:true; color:#2c3e50">'经营能力(95分):' </span>              '客户留存率高'            </li>            <listyle="margin-bottom:6px; padding:5px; border-left:3px solid #e74c3c">              <spanstyle="bold:true; color:#2c3e50">'偿债能力(85分):' </span>              '负债率合理'            </li>            <listyle="margin-bottom:6px; padding:5px; border-left:3px solid #f39c12">              <spanstyle="bold:true; color:#2c3e50">'发展潜力(92分):' </span>              '研发投入增加'            </li>          </list>        </div>      </div>    </div>  </div>  <divstyle="margin-bottom:15px; background:linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); padding:15px; border-radius:4px; border-left:4px solid #2196F3">    <h2style="color:#1565c0; margin-top:0; font-size:13px; margin-bottom:8px">'综合评价'</h2>    <pstyle="text-align:justify; margin-bottom:8px; font-size:11px">      '四川省泡海椒科技有限公司作为国家级高新技术企业和专精特新"小巨人"企业,在云计算和大数据领域表现出色。'      '公司财务状况健康,经营能力突出,技术创新能力强,具备良好的发展潜力。信用评级为AAA级,展望稳定。'    </p>    <pstyle="text-align:right; font-size:8px; color:#d32f2f; margin-top:2px; margin-bottom:0">'认证通过'</p>  </div>  <divstyle="text-align:center; margin-top:15px; padding-top:12px; border-top:1px solid #dee2e6; color:#6c757d; font-size:9px">    <p>'本报告依据公开信息和专业评估模型生成,仅供参考 | 报告生成时间: 2025年09月06日'</p>    <p>'© 2023 企业信用评估中心 版权所有 | 认证编号: CE-2023-001'</p>  </div></body></pdf>

    真实渲染效果预览

    以下为上述代码 + 模板真实运行生成的 PDF 首屏预览(由 jquick-pdf 渲染输出,非示意图):

相关学习资料