pdfbox使用中遇到的坑,无法渲染,渲染倒置问题,帮你少走弯路
创始人
2025-05-30 10:51:20
📢📢📢📣📣📣
哈喽!大家好,我是「奇点」,江湖人称 singularity。刚工作几年,想和大家一同进步🤝🤝
一位上进心十足的【Java ToB端大厂领域博主】!😜😜😜
喜欢java和python,平时比较懒,能用程序解决的坚决不手动解决😜😜😜

✨ 如果有对【java】感兴趣的【小可爱】,欢迎关注我

❤️❤️❤️感谢各位大可爱小可爱!❤️❤️❤️

由于最近项目后端由我负责,导致最近没有时间更新文章,这里对大家说声抱歉,后续我会慢慢学会对事件的管理,做好工作和分享的协调,争取多分享一些文章给大家,也欢迎大家能和我一起,学习进步。

项目最近使用pdfbox,由于这方面经验不足,由于用itext的用户很多,pdfbox的文章相对较少,但是由于itext开源协议的问题,所以项目使用的pdfbox,但是pdfbox国内的相关文章也相对较少,由于项目中的什么牛鬼神蛇用户都有,在pdf的渲染过程中也走了不是弯路,同时也遇到了各种乱起八糟的问题,这里给大家进行一下总结也工大家参考。

首先将遇到的需求和问题列下来

  1. 正常渲染pdf,根据前端传的位置,在pdf中设置变量值

  1. pdf渲染时需要自动换行(这个由于仓促只是思路)

问题

  • pdf渲染的内容部分pdf会出现倒置的问题

  • pdf部分文件会出现缩放的问题

  • pdf中的文件部分会出现渲染不出来的问题

妈呀,这么多莫名其妙的问题,真是叫人头疼,奈何用户较多,这里只能按照用户的需求,将这些问题一一解决,mmp,公司什么时候能为员工考虑一下。md,简单吐槽一下,奈何大环境这样也只能在这抱怨一下,希望不会影响大家的心态。

好了言归正传,还是进入今天的正文,pdfbox的使用。

渲染这部分相当来说是比较简单的,网上的文章也相对较多,这里只是进行简单的描述,将核心的代码给大家提供出来。

@Data
@ToString
public class ReplaceRegion {/*** 唯一标识*/private String id;/*** 替换内容*/private String replaceText;/*** x坐标*/private Float x;/*** y坐标*/private Float y;/*** 宽度*/private Float w;/*** 高度*/private Float h;/*** 字体属性*/private FontValue fontValue;
}

上面是整个体会的内容对象,有id和替换文本的位置和文字信息

public class PdfboxReplace {private static final Integer CAPACITY = 1 << 4;private static final Logger log = LoggerFactory.getLogger(PdfboxReplace.class);/*** 输出流*/private ByteArrayOutputStream output;/*** pdf文本*/private PDDocument document;/*** 文本流*/private PDPageContentStream contentStream;/*** 从第0页开始算*/private static final Integer DECREASE_ONE = 1;/*** 设置字体默认字号*/private int FONT_SIZE = 12;public PdfboxReplace(PDDocument document) {this.document=document;output = new ByteArrayOutputStream();}private PdfboxReplace(byte[] pdfBytes) throws IOException {init(pdfBytes);}private void init(byte[] pdfBytes) throws IOException {log.info("===========[pdf区域替换初始化开始]=============");document = PDDocument.load(pdfBytes,null,null,null, MemoryUsageSetting.setupTempFileOnly());output = new ByteArrayOutputStream();log.info("===========[pdf区域替换初始化完成]=============");}/*** 根据自定区域替换文本** @throws IOException* @throws*/private void process(Map> replaceRegionMap) throws IOException {try {//对当前文件的字体进行缓存Map fontCache = new ConcurrentHashMap<>();for (Entry> entry : replaceRegionMap.entrySet()) {//设置当前操作页码,从第0页开始算PDPage page = document.getPage(entry.getKey() - DECREASE_ONE);contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, false,true);for (ReplaceRegion region : entry.getValue()) {Float cursorX = region.getX();Float cursorY = region.getY();//画矩形,作为背景覆盖,暂时不用//content.setNonStrokingColor(Color.WHITE);//content.addRect(cursorX, cursorY, 100, cursorY + 100);//content.fill();//content.saveGraphicsState();/**添加文字*/contentStream.setNonStrokingColor(Color.BLACK);contentStream.beginText();//设置文字属性String fontKey = region.getFontValue().getSize() + region.getFontValue().getFontStyle();//缓存命中直接走字体缓存PDType0Font font = null;if (fontCache.keySet().contains(fontKey)) {font = fontCache.get(fontKey);}else {InputStream fontInfo = getFontInfo(region.getFontValue());font = PDType0Font.load(document, fontInfo);fontCache.put(fontKey,font);}//设置字号和字体contentStream.setFont(font, region.getFontValue().getSize() != null ? region.getFontValue().getSize() : FONT_SIZE);font.encode("utf8");contentStream.newLineAtOffset(cursorX, cursorY + 3);contentStream.showText(region.getReplaceText());contentStream.saveGraphicsState();contentStream.endText();}contentStream.close();}document.save(output);} catch (Exception e) {log.error("pdf process error:{}{}", e.getMessage(), e);throw new Exception("替换pdf内容异常");} finally {if (contentStream != null) {contentStream.close();}if (document != null) {document.close();}}}/*** 设置参数** @param x* @param y* @param text 替换文字*/public ReplaceRegion replaceText(float x, float y, float w, float h, String text, FontValue fontValue) {//用文本作为别名ReplaceRegion region = new ReplaceRegion(text);region.setH(h);region.setW(w);region.setX(x);region.setY(y);region.setFontValue(this.getFontVale(fontValue));return region;}/*** 获取字体属性** @param fontValue* @return*/public FontValue getFontVale(FontValue fontValue) {if (fontValue != null) {fontValue.setSize(fontValue.getSize() == null ? FONT_SIZE : fontValue.getSize());fontValue.setFontStyle(StringUtils.isBlank(fontValue.getFontStyle()) ? FontEnum.SIM_SUN.getCode() : fontValue.getFontStyle());} else {fontValue = new FontValue();fontValue.setSize(FONT_SIZE);fontValue.setFontStyle(FontEnum.SIM_SUN.getCode());}return fontValue;}/*** 替换pdf文本区域** @param regions  区域参数 key:页码, value:区域参数*/public  byte[] PdfReplaceRegion(Map> regions) {//要替换的文本区域数据信息Map> replaceRegionMap = new ConcurrentHashMap<>(CAPACITY);for (Map.Entry> mapEntry : regions.entrySet()) {List replaceRegionList = new ArrayList<>();if (!CollectionUtils.isEmpty(regions)) {for (ReplaceRegion region : mapEntry.getValue()) {replaceRegionList.add(this.replaceText(region.getX(), region.getY(), region.getW(), region.getH(), region.getReplaceText(), region.getFontValue()));}}replaceRegionMap.put(mapEntry.getKey(), replaceRegionList);}try {//获取生成的pdf流return this.toPdf(replaceRegionMap);} catch (IOException e) {log.error(e.getMessage(), e);throw new Exception("复制转换pdf异常");}}/*** 替换pdf文本区域** @param regions  区域参数 key:页码, value:区域参数* @param pdfBytes 源文件字节码*/public  byte[] PdfReplaceRegion(Map> regions, byte[] pdfBytes) {//要替换的文本区域数据信息Map> replaceRegionMap = new ConcurrentHashMap<>(CAPACITY);PdfboxReplace pdPlacer;try {pdPlacer = new PdfboxReplace(pdfBytes);} catch (IOException e) {log.error(e.getMessage(), e);throw new GlobalException( "替换pdf区域文件错误");}for (Map.Entry> mapEntry : regions.entrySet()) {List replaceRegionList = new ArrayList<>();if (!CollectionUtils.isEmpty(regions)) {for (ReplaceRegion region : mapEntry.getValue()) {replaceRegionList.add(pdPlacer.replaceText(region.getX(), region.getY(), region.getW(), region.getH(), region.getReplaceText(), region.getFontValue()));}}replaceRegionMap.put(mapEntry.getKey(), replaceRegionList);}try {//获取生成的pdf流return pdPlacer.toPdf(replaceRegionMap);} catch (IOException e) {log.error(e.getMessage(), e);throw new Exception("复制转换pdf异常");}}/*** 获取字体流** @param fontValue* @return*/private InputStream getFontInfo(FontValue fontValue) {InputStream resourceAsStream = null;//自定义字体if (fontValue != null && StringUtils.isNotBlank(fontValue.getFontStyle())) {resourceAsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(FontEnum.getValue(fontValue.getFontStyle()));return resourceAsStream;}//默认宋体resourceAsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(FontEnum.getValue(FontEnum.SIM_SUN.getValue()));return resourceAsStream;}/*** 生成新的PDF文件** @param replaceRegionMap 需要替代的数据信息* @return* @throws IOException*/public byte[] toPdf(Map> replaceRegionMap) throws IOException {try {//替代方法this.process(replaceRegionMap);log.info("===========[pdf文件生成成功]=============");return output.toByteArray();} catch (IOException e) {log.error(e.getMessage(), e);throw e;} finally {//关闭资源if (output != null) {output.close();}}}
}

这个是渲染的整个工具类的方法,其中核心的部分是process方法

/*** 根据自定区域替换文本** @throws IOException* @throws*/private void process(Map> replaceRegionMap) throws IOException {try {//对当前文件的字体进行缓存Map fontCache = new ConcurrentHashMap<>();for (Entry> entry : replaceRegionMap.entrySet()) {//设置当前操作页码,从第0页开始算PDPage page = document.getPage(entry.getKey() - DECREASE_ONE);contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, false,true);for (ReplaceRegion region : entry.getValue()) {Float cursorX = region.getX();Float cursorY = region.getY();//画矩形,作为背景覆盖,暂时不用//content.setNonStrokingColor(Color.WHITE);//content.addRect(cursorX, cursorY, 100, cursorY + 100);//content.fill();//content.saveGraphicsState();/**添加文字*/contentStream.setNonStrokingColor(Color.BLACK);contentStream.beginText();//设置文字属性String fontKey = region.getFontValue().getSize() + region.getFontValue().getFontStyle();//缓存命中直接走字体缓存PDType0Font font = null;if (fontCache.keySet().contains(fontKey)) {font = fontCache.get(fontKey);}else {InputStream fontInfo = getFontInfo(region.getFontValue());font = PDType0Font.load(document, fontInfo);fontCache.put(fontKey,font);}//设置字号和字体contentStream.setFont(font, region.getFontValue().getSize() != null ? region.getFontValue().getSize() : FONT_SIZE);font.encode("utf8");contentStream.newLineAtOffset(cursorX, cursorY + 3);//根据自己的实际情况进行调整contentStream.showText(region.getReplaceText());contentStream.saveGraphicsState();contentStream.endText();}contentStream.close();}document.save(output);} catch (Exception e) {log.error("pdf process error:{}{}", e.getMessage(), e);if (contentStream != null) {contentStream.close();}if (document != null) {document.close();}}}

其中这个pdf内容流的构造方法是我遇到坑的问题所在,很多问题就是对这个构造方法不熟悉导致的,这里给大家说明一下,也是后续解决问题的关键,我这里使用的是5个参数的构造方法,

contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, false,true);

public PDPageContentStream(PDDocument document, PDPage sourcePage, AppendMode appendContent, boolean compress, boolean resetContext)

其中最主要的是 AppendMode appendContent和boolean resetContext这个两个参数

AppendMode是一个枚举类

 /*** Overwrite the existing page content streams.*/OVERWRITE, /*** Append the content stream after all existing page content streams.*/APPEND, /*** Insert before all other page content streams.*/PREPEND;

有3个可选值,由于对这些坑的整理,我认为这个pdfbox是一层一层的将流进行叠加渲染整个pdf文件页面进行渲染的

  • OVERWRITE, 这个是覆盖操作,也就是你新增加的变量会将之前的pdf文件中的内容进行覆盖操作,整个页面只会显示你新加的内容(慎用)

  • APPEND 这个是在所有现有页面内容流之后附加内容流。也就是你加的内容是在最后一层进行渲染的,这样的话,我们会在空白处将我们新增加的内容加到pdf中去,这个是我们常用的选项

  • PREPEND 这个是在所有其他页面内容流之前插入。也就是和APPEND相反,会最早将内容插入到pdf文件中,要的的问题就是我们新增加的内容,可能会被特殊的内容覆盖,例如横行等,导致变了和内容渲染不出来,根本原因是覆盖率,不是渲染不出来

其中这个resetContext参数也需要进行设置,这里就得提及一下缩放和倒置问题了,开始如果不设置这个参数等话,用4个构造的方法,这里的默认是false的,就是对容器内容进行重制,这个倒置问题本质上讲就是不通的pdf的坐标原点不通,有的坐标原点在左上角,有点文件的原点在左下角,导致我们进行渲染的时候出现倒置问题。

有的文章说使用APPEND参数,这个确实会将渲染内容的坐标原点重制,但是会出现渲染不出来的问题,真是治聋治哑了,哎,这个问题困扰了我很久,最后还是在stackoverflow解决的,下面是原文

https://stackoverflow.com/questions/27919436/pdfbox-pdpagecontentstreams-append-mode-misbehaving

关于倒置问题有两张方法解决,我是使用的构造方法设置的,当然还可以使用下面方法,通过调用保存和恢复第一个内容流中的图形状态

saveGraphicsState();
// ...
restoreGraphicsState();

自动换行问题

这里的思路就是前端设置一个能够拖动大小的组件,将长告诉后端,后端根据字体和组件宽进行计算,每行有多少字进行自动换行,当然还有一点问题就是符号会有问题,有符号的会比文字使用的更少

 private void process(Map> replaceRegionMap) throws IOException {try {//对当前文件的字体进行缓存Map fontCache = new ConcurrentHashMap<>();for (Entry> entry : replaceRegionMap.entrySet()) {//设置当前操作页码,从第0页开始算PDPage page = document.getPage(entry.getKey() - DECREASE_ONE);contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, false);for (ReplaceRegion region : entry.getValue()) {Float cursorX = region.getX();Float cursorY = region.getY();//画矩形,作为背景覆盖,暂时不用//content.setNonStrokingColor(Color.WHITE);//content.addRect(cursorX, cursorY, 100, cursorY + 100);//content.fill();//content.saveGraphicsState();//TODO 根据长度进行计算每行字数//向下移动的距离也按照变量进行计算//循环渲染List strList = MyStringSpitUtil.getStrList(region.getReplaceText(), 20);for (int i = 0; i < strList.size(); i++) {/**添加文字*/contentStream.setNonStrokingColor(Color.BLACK);contentStream.beginText();//设置文字属性String fontKey = region.getFontValue().getSize() + region.getFontValue().getFontStyle();//缓存命中直接走字体缓存PDType0Font font = null;if (fontCache.keySet().contains(fontKey)) {font = fontCache.get(fontKey);}else {InputStream fontInfo = getFontInfo(region.getFontValue());font = PDType0Font.load(document, fontInfo);fontCache.put(fontKey,font);}//设置字号和字体contentStream.setFont(font, region.getFontValue().getSize() != null ? region.getFontValue().getSize() : FONT_SIZE);font.encode("utf8");contentStream.newLineAtOffset(cursorX, cursorY + 3);
//                        contentStream.showText(region.getReplaceText());contentStream.showText(strList.get(i));contentStream.saveGraphicsState();contentStream.endText();cursorY = cursorY - 20;}
//                    //设置字号和字体
//                    contentStream.setFont(font, region.getFontValue().getSize() != null ? region.getFontValue().getSize() : FONT_SIZE);
//                    font.encode("utf8");
//                    contentStream.newLineAtOffset(cursorX, cursorY + 3);
//                    contentStream.showText(region.getReplaceText());
//                    contentStream.saveGraphicsState();
//                    contentStream.endText();
//                    cursorY=cursorY-20;}contentStream.close();}document.save(output);} catch (Exception e) {log.error("pdf process error:{}{}", e.getMessage(), e);throw new GlobalException(ResultCode.FAIL, com.yonyou.iuap.ucf.common.i18n.MessageUtils.getMessage("P_YS_PF_ECON-SERVICE_0001163006") /* "替换pdf内容异常" */);} finally {if (contentStream != null) {contentStream.close();}if (document != null) {document.close();}}}
public class MyStringSpitUtil {public static List getStrList(String inputString, int length) {int size = inputString.length() / length;if (inputString.length() % length != 0) {size += 1;}return getStrList(inputString, length, size);}/*** 把原始字符串分割成指定长度的字符串列表* @param inputString 原始字符串* @param length 指定长度* @param size  指定列表大小* @return*/public static List getStrList(String inputString, int length,int size) {List list = new ArrayList();for (int index = 0; index < size; index++) {String childStr = substring(inputString, index * length,(index + 1) * length);list.add(childStr);}return list;}/*** 分割字符串,如果开始位置大于字符串长度,返回空* @param str  原始字符串* @param f 开始位置* @param t  结束位置* @return*/public static String substring(String str, int f, int t) {if (f > str.length()) {return null;}if (t > str.length()) {return str.substring(f, str.length());} else {return str.substring(f, t);}}
}

由于时间紧,写的难免会有小bug,希望大家给我指出,我会第一时间进行修改,也希望我写的文章能解决你pdfbox中的问题,让你少走弯路,我就心满意足了

如果觉得本文对你有帮助,欢迎点赞,欢迎关注我,如果有补充欢迎评论交流,我将努力创作更多更好的文章。

相关内容

热门资讯

中信建投2025下半年社服商贸... 智通财经获悉,中信建投证券发布研报称,新消费异军突起的路径各不相同,整体上是供给能够快速响应消费者细...
曾用印度工程师冒充AI,估值1... 曾经,有一位印度工程师,利用虚假手段试图营造出 AI 的假象,以此在市场上获取巨额估值。他或许凭借一...
本轮“即时零售战争”有何不同?... 此时此刻,即使已身处“618”,但目之所及,大众流量仍聚焦于外卖平台的价格战——关心哪个平台的羊毛更...
今日复牌,“国产算力航母”启航... 固态电池装车渐行渐近。6月9日晚间,一批A股宣布复牌,包括市场密集关注的两大科技股中科曙光、海光信息...
员工被通知前1小时还在改bug... 本报(chinatimes.net.cn)记者李明会 北京报道近日,花旗集团宣布推进全球简化工作,作...
沪银期价创历史新高 总沉淀资金... 本报记者 王 宁 6月份以来,受国际白银价格走势强劲影响,国内沪银期价持续走高,截至目前主力7月合约...
高考成绩和主权货币 高考成绩和... 本文来自微信公众号:一知半论,作者:格拉古艾尔,题图来源:视觉中国今年,全国有1335万考生同时走进...
国泰海通证券:6月开始逐步迎来... 国泰海通证券研报表示,从建材需求来看,2024年6月是一个关键的转折时间,2024年6—9月建材需求...
新一轮“造富”,河南赢了? 新一轮“造富”,河南赢了?近年来,河南在经济领域展现出强劲的发展态势,仿佛开启了一场独特的“造富”之...
爆炒乐惠国际:啤酒业卖水人,亲... 斑马消费 杨伟啤酒行情没来,“啤酒影子股”乐惠国际,近期遭遇爆炒,短短10个交易日股价上涨了67%。...
李蓓找了一圈,还是坚持高股息,... 6月9日,一边是新消费、创新药的集体大涨,一边是李蓓的深刻反思:我做错了什么?反思之后,依然坚持自己...
Siri没来!苹果AI终于不画... 北京时间 6 月 10 日凌晨 1 点,苹果准时拉开了一年一度的全球开发者大会(WWDC)的帷幕。但...
会稽山市值反超 “黄酒一哥”黄... 6月9日,会稽山以22.07元/股收盘,总市值达106亿元,总市值超越"黄酒一哥"古越龙山,成功坐上...
万亿化工市场被垄断!但财报里这... 2025年1月,山东省省长周乃翔做了政府工作报告。在这份工作报告中,重点强调了“新型工业化”。大力推...
国家越开放,会促使我们更加进步... 本报记者 胡 健 陈家兴 近日,在深圳华为总部,围绕大众关心的一些热点话题,人民日报记者一行与华为首...
现在真到了“劝退”计算机专业的... 如今,关于是否到了“劝退”计算机专业的讨论众说纷纭。一方面,计算机专业的就业前景看似广阔,薪资待遇较...
暴涨1400%牛股,造就“新首... 大牛股造就大富豪。福布斯实时富豪榜显示,目前,泡泡玛特(09992.HK)创始人王宁家族以208亿美...
加大国有企业技能人才薪酬分配激... 日前,人力资源社会保障部、财政部、国务院国资委联合印发《关于加大国有企业技能人才薪酬分配激励的通知》...
华尔街三巨头唱多美股! 华尔街... 2025.06.10本文字数:1728,阅读时长大约3分钟作者 |第一财经 樊志菁在时隔四个月重新站...
史诗级变革!医疗、养老、教育迎... 重磅利好来了,中国民生保障体系从“兜底型”向“发展型”跃迁。6月9日,据新华社等权威媒体披露,中共中...
美联储调查:5月消费者通胀预期... 5月,美国消费者对未来通胀的预期全面下降,为2024年来首次,其中短期通胀预期降幅最大。周一,美国纽...
盯上“金银珠宝”,幻方量化的“... 幻方量化,一个熟悉又陌生的名字。这家由梁文锋打造的量化大厂,一度在中国量化基金圈占有崇高的地位——行...
iPhone系统最激进更新来了... iPhone 系统最激进更新已降临。此次更新带来诸多变革,界面设计更趋简洁流畅,操作体验大幅提升。新...
盘前必读丨中办国办发布重磅民生... 【财经日历】国新办就进一步保障和改善民生有关政策情况举行新闻发布会。苹果全球开发者大会WWDC25于...
三年来首例“B转A”方案高票通... B股上市27年的杭州汽轮动力集团股份有限公司(下称“杭汽轮”),在多次尝试转A未果后,终于迈出了实质...
经纬早班车|中概股普遍上涨;亚...   【隔夜重磅】  纳斯达克中国金龙指数涨超2%  美东时间周一(9日),美股三大指数收盘涨跌不一,...
华尔街三巨头唱多美股!新高还有... 在时隔四个月重新站上6000点后,周一标普500指数再接再厉。随着美国总统特朗普4月初在关税问题上的...
【美股】美股三大指数收盘涨跌不... 美东时间9日,美股三大指数收盘涨跌不一,纳指、标普500指数小幅拉升,均续创2月底以来收盘新高,道指...
央企控股上市公司回购增持进度条... 近期,央企控股上市公司正加速跑在增持回购路上。据上海证券报记者统计,新增计划方面,截至发稿,年内有3...
新筑股份拟重组战略转型,四川国... 6月9日晚,新筑股份发布重大资产重组预案,公司拟通过“资产出售+资产购买+募集配套资金”,战略性退出...