pdfbox使用中遇到的坑,无法渲染,渲染倒置问题,帮你少走弯路

📢 📢 📢 📣 📣 📣
哈喽!大家好,我是「奇点」,江湖人称 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<Integer, List<ReplaceRegion>> replaceRegionMap) throws IOException {
        try {
            //对当前文件的字体进行缓存
            Map<String,PDType0Font> fontCache = new ConcurrentHashMap<>();
            for (Entry<Integer, List<ReplaceRegion>> 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<Integer, List<ReplaceRegion>> regions) {
        //要替换的文本区域数据信息
        Map<Integer, List<ReplaceRegion>> replaceRegionMap = new ConcurrentHashMap<>(CAPACITY);
        for (Map.Entry<Integer, List<ReplaceRegion>> mapEntry : regions.entrySet()) {
            List<ReplaceRegion> 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<Integer, List<ReplaceRegion>> regions, byte[] pdfBytes) {
        //要替换的文本区域数据信息
        Map<Integer, List<ReplaceRegion>> 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<Integer, List<ReplaceRegion>> mapEntry : regions.entrySet()) {
            List<ReplaceRegion> 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<Integer, List<ReplaceRegion>> 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<Integer, List<ReplaceRegion>> replaceRegionMap) throws IOException {
        try {
            //对当前文件的字体进行缓存
            Map<String,PDType0Font> fontCache = new ConcurrentHashMap<>();
            for (Entry<Integer, List<ReplaceRegion>> 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<Integer, List<ReplaceRegion>> replaceRegionMap) throws IOException {
        try {
            //对当前文件的字体进行缓存
            Map<String,PDType0Font> fontCache = new ConcurrentHashMap<>();
            for (Entry<Integer, List<ReplaceRegion>> 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<String> 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<String> 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<String> getStrList(String inputString, int length,
                                          int size) {
        List<String> list = new ArrayList<String>();
        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中的问题,让你少走弯路,我就心满意足了

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

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

智达教育‍

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值