找回密码
 立即注册
查看: 2228|回复: 0

Java生成PDF的若干坑

  • TA的每日心情
    开心
    2022-1-29 18:58
  • 签到天数: 8 天

    [LV.3]偶尔看看II

    184

    主题

    414

    回帖

    585

    VC币

    星辰大海

    Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20

    积分
    549470

    崭露头角活跃达人新人登场

    Seekladoom 发表于 2020-11-10 18:07:53 | 显示全部楼层 |阅读模式
    本帖最后由 Seekladoom 于 2020-11-11 01:35 编辑

    转自知乎
    https://zhuanlan.zhihu.com/p/34519204

    前言
    在工作中,有使用Java生成大量的电子凭证,也就是PDF。在之前的文章中,有写过,如何通过Java生成PDF。这里就不再描述,写这篇文章主要是为了记录我在使用Java生成PDF过程中犯过的错,以及踩过的坑。

    内存溢出
    之前在使用生成方案的时候,我们一开始采用的是《PD4ML》框架,这个框架相对来说,要小巧些。但是不足的是:它是闭源的。在批量生成PDF文件时会出现内存泄漏的现象。每每我们在分析内存原因时,都止步于下面这一段代码:
    1. private void createPdfByHtml(String html, File file) throws Exception{
    2.         FileOutputStream fos = new FileOutputStream(file);  
    3.         ...
    4.         // 这一行
    5.         pd4ml.render(sr, fos, new URL("http://"), "UTF-8");  
    6.     }
    复制代码
    因此,我们不得不考虑换一个第三方库。目前我们使用的是开源库《iText》,到目前为止,稳定运行。
    (我们在选择第三方框架时,尽量选择开源的,因为在出现问题后,我们可以通过查看源代码分析,甚至可以修改源代码进行优化)。


    字体路径
    这个问题是这样的,我们生成PDF时为了支持中文字体,需要通过路径加载指定的字体文件。而开发环境是windows,测试/生产环境均是Linux。字体放在
    1. /resources/fonts/
    复制代码


    因为:
    1.在Window下获取Thread.currentThread()为null值
    2.在Linux下获取this.class.getClass().getResource(“/“).getPath();获取为null值
    所以在加载路径时,我们通过下述方法进行了兼容处理。
    1. /**
    2.      * 应用场景:
    3.      * 1.在windows下,使用Thread.currentThread()获取路径时,出现空对象,导致不能使用
    4.      * 2.在linux下,使用PdfUtils.class获取路径为null,
    5.      * 获取字体路径
    6.      * @return
    7.      */
    8.     private static String getFontPath(){
    9.         String path="";
    10.         // 1. 生产环境路径
    11.         ClassLoader classLoader= Thread.currentThread().getContextClassLoader();
    12.         URL url = (classLoader==null)?null:classLoader.getResource("/");
    13.         String threadCurrentPath = (url==null)?"":url.getPath();
    14.         // 2. 如果线程获取为null,则使用当前PdfUtils.class加载路径
    15.         if(StringUtil.isBlank(threadCurrentPath)){
    16.              path = PdfUtils.class.getClass().getResource("/").getPath();
    17.         }
    18.         // 3.拼接字体路径
    19.         StringBuffer stringBuffer = new StringBuffer(path);
    20.         stringBuffer.append("/fonts/SIMKAI.TTF");
    21.         path = stringBuffer.toString();
    22.         logger.info("getFontPath threadCurrentPath: {}  path: {}",threadCurrentPath,path);
    23.         return path;
    24.     }
    复制代码
    从这个方面也能体现程序员中的一些常见现象:
    1.为什么在我电脑上是好的,在你电脑上不行,一定是你电脑问题。
    2.不应该啊,在我电脑上是好的。绝对没问题。
    小伙伴们知道这个梗吗? 这也是很多程序员使用与生产环境一致的系统作为开发机的原因。


    字符编码
    问题是这样的。是由于在生成PDF文件时转换字节流时没有指定编码导致的: 如下所示:
    1. public static void createdPdfByItextHtml(String htmlContent,File file){
    2.             ...
    3.             try {
    4.                 inputStream= new ByteArrayInputStream(htmlContent.getBytes());
    5.                 outputStream = new FileOutputStream(file);
    6.             ...
    复制代码
    此处省略掉部分代码,此时htmlContent.getBytes()该方法没有指定默认的编码导致的。具体的原因,在《初探JDK源码之默认字符集》这篇文章中有详细的描述。


    生僻字
    这个坑是这样的,因为正式公文的字体是:Kaiti_GB2312的字体。我们使用过一段时间发现,Kaiti_GB2312字体对于生僻字支持不友好。
    例如:陈垚,通过Kaiti_GB2312生成后就只能显示为: 陈
    其中字就没有正常显示。这个问题是非常严重的。如果电子凭证中姓名错误,也就代表,这份电子凭证是没有法律效应的。在经过测试后,我们最终使用了Kaiti字体,代替了Kaiti_GB2312。(这个坑,还真的不容易发现)


    枚举传递类型
    如果Dubbo接口中使用枚举作为参数时,如果只有单方面更新枚举的值。会导致序列化出错的问题。也就是说:调用方在更新枚举后,如果接受方不同时更新枚举,会导致接收方的参数为空对象。也就是说:
    调用方在更新枚举后,如果接受方不同时更新枚举,会导致接收方的参数为空对象。
    注意点:Dubbo服务调用之间,尽量不要使用enum类型。


    严格的HTML语义标签
    使用过itext的朋友应该知道。HTML内容是需要严格的标签的。也就是说,一个开始标签,对应一个结束标签。如果不匹配,则会生成失败。(像<img src="www.baidu.com"/>) 标签除外。
    错误的html:
    1. <html>
    2.   <head>
    3.      <title></title>
    4.   </head>
    5.   <body>
    6.     <span>Hello World!</span></span>
    7.   </body>
    8. </html>
    复制代码

    上面这段html内容,就多了一个</span>结束标签,就属于非严格的HTM语义标签。
    执行后,就会有下面这类错误:
    1. com.itextpdf.tool.xml.exceptions.RuntimeWorkerException: Invalid nested tag span found, expected closing tag body.
    2.     at com.itextpdf.tool.xml.XMLWorker.endElement(XMLWorker.java:134)
    3.     at com.itextpdf.tool.xml.parser.XMLParser.endElement(XMLParser.java:396)
    复制代码

    最后
    有些系统,看起来容易。做起来可并非易事。在实现期间可能会遇到各种各样的奇葩问题。但恰恰,就是这些怪问题。给我们积累了经验。我一直觉得,在写代码方面,是一个“倒霉”的人,因为各种奇怪的问题,都能让我遇到。
    公众号内回复:【PDF】,即可获取项目。记得更换字体哦!

    相关阅读:
    知乎搜索“开源 PDF 嵌入字体 Java”的结果:

    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有账号?立即注册

    x
    一个对动画组来说真正耐用的中文字体应该具备哪些条件?
    https://bbs.acgrip.com/forum.php?mod=viewthread&tid=5310

    使用思源字体在Aegisub中制作字幕时的一些常见问题
    https://bbs.acgrip.com/forum.php?mod=viewthread&tid=3805
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    快速回复 返回顶部 返回列表