1.前言

前一段时间遇到一个制作Pdf的业务,自己下来摸索了一下,基本上解决。将其中遇到的几个问题及解决方法做以记录,仅供大家参考。

首先在这里对于刚接触该类型业务的同学说明下,ItexPdf支持使用模板生成pdf和直接绘画pdf。我个人的理解在于,假如你涉及的业务生成的pdf是具有固定格式或者模快的文字及其图片等内容,我这里建议使用模板,只需要将不一致的地方改成文本域,然后进行文字填充就ok了。如果涉及的业务不能有模块化可以提取出来东西,我这边建议直接从开头一步一步去绘画。本文主要介绍使用pdf模板生成文件,直接绘画因为涉及pdf样式不好归纳,不再做陈述。

2.Maven依赖


        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itextpdf</artifactId>
            <version>5.5.6</version>
        </dependency>
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itext-asian</artifactId>
            <version>5.2.0</version>
        </dependency>

如果pdf内容不涉及汉字,只需要引入上面那个jar包就行。如果内容有汉字,还需加上itext-asian依赖。

3.模板制作

制作pdf模板,最好能有一份已制作完成的pdf文件,并在其基础上进行修改。若没有只能先在word或者Adobe Acrobat Pro DC中制作。

在Adobe Acrobat 中点击新建,创建一份空白的pdf页面。此时注意,如果有对pdf页面大小精确要求的业务,请在Adobe Acrobat中装上Prinetct PDF ToolBox插件或者使用裁剪功能,可以对页面大小做一个设计。这里我新建了一个空白的页面,并使用裁剪页面对纸张大小做了一下调整,调整后的纸张大小为150mmX200mm。

点击右边的准备表单,并点击页面上方的文本域,在表单的对应位置加上文本域,文本域用于替换页面中变动的地方,可以双击文本域,对当前文本域的字体,样式,大小,对其方式等属性做以调整。(我这里因为实际业务使用的模板复杂的多,就简单的画了几个文本域,只是做演示)

 

4.Pdf生成

创建实体对象。

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Person implements Serializable {
    private static final long serialVersionUID = -284358572067776571L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String address1;
    private String address2;
    private String city;
    private byte[] dataMatrix;
}

定义一个工具类,并在里面定义生成pdf文件的方法。(大概原理就是根据文本域名称读取各个文本域在模板中的位置,然后构建实体对象对进行文本域填充) 

@Component
public class FileUtils {
    /**
     * 定义模板路径,一般存在OSS上或项目路径下
     */
    private static final String TEMPLATEADDRESS = "C:\\Users\\Administrator\\Desktop\\temp.pdf";

    public static ByteArrayOutputStream generetePdf(Person person) throws IOException, DocumentException {
        /*创建一个pdf读取对象*/
        PdfReader reader = new PdfReader(TEMPLATEADDRESS);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        /*创建pdf模板,参数reader  bos*/
        PdfStamper ps = new PdfStamper(reader, bos);
        /*定义字体*/
        BaseFont baseFont = BaseFont.createFont("STSongStd-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
        ArrayList<BaseFont> fontArrayList = new ArrayList<>();
        fontArrayList.add(baseFont);
        /*读取模板文本域并封装数据(注意名字应与文本域中的名字一致)*/
        AcroFields s = ps.getAcroFields();
        /*定义字体,若不定义,会使用模板中的文本域设置的字体*/
        s.setSubstitutionFonts(fontArrayList);
        s.setField("name", person.getName());
        s.setField("address1", person.getAddress1());
        s.setField("address2", person.getAddress2());
        s.setField("city", person.getCity());
        /*插入图片*/
        int pageNo = s.getFieldPositions("dataMatrix").get(0).page;
        Rectangle signRect = s.getFieldPositions("dataMatrix").get(0).position;
        float x = signRect.getLeft();
        float y = signRect.getBottom();
        Image image = Image.getInstance(person.getDataMatrix());
        /*获取操作的页面*/
        PdfContentByte under = ps.getOverContent(pageNo);
        /*根据域的大小缩放图片*/
        image.scaleToFit(signRect.getWidth(), signRect.getHeight());
        /*添加图片*/
        image.setAbsolutePosition(x, y);
        under.addImage(image);
        /*这里true表示pdf可编辑*/
        ps.setFormFlattening(true);
        /*关闭PdfStamper*/
        ps.close();
        return bos;
    }

写一个测试方法,构建实体对象,并调用生成pdf的方法。

  @Test
    void contextLoads() throws IOException, DocumentException {
        /*创建文件存放位置*/
        FileOutputStream file=new FileOutputStream("C:\\Users\\Administrator\\Desktop\\1.pdf");
        /*读取图片地址*/
        File picFile=new File("C:\\Users\\Administrator\\Desktop\\addr.png");
        Person person=Person.builder()
                .name("李明")
                .address1("人民南路一段86号")
                .address2("天府广场")
                .city("四川省成都市锦江区")
                .dataMatrix(FileUtils.File2byte(picFile))
                .build();
        file.write(FileUtils.generetePdf(person).toByteArray());
    }

生成结果如下:

5.字体缩放

在使用模板生成pdf的过程中,因为文本域的长度是有所限制的,若填充文字超出文本域长度,默认不会显示超出部分。想要对文本域中填充的全部文字进行显示,就只能对字体大小做以调整。

假设当前我要将"Tianfu Square, 86 Section 1, Renmin South Road, Jinjiang District, Chengdu City, Sichuan Province, China"这条信息填入到address2所对应的文本域中,因为长度超出了文本域的长度,生成效果如下:

这里定义一个方法,检查文本域填充文字的长度。里面有三个参数,第一个参数就是填充的信息,第二个参数是当前文本域的名称,第三个参数就是表单信息。

这里切记,里面的字体要和上面生成文件的字体一致,若上面没有定义字体,也需要和pdf模板中表单默认的字体名一致,因为不同字体计算出占用文本域的宽度是不一样的,若字体不相同会导致缩放效果无法达成。

  public float checkAddrLength(String addr, String fileName, AcroFields form) throws IOException, DocumentException {
        BaseFont baseFont = BaseFont.createFont("STSongStd-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
        float fontSize = 12f;
        Rectangle position = form.getFieldPositions(fileName).get(0).position;
        float textBoxWidth = position.getWidth();
        /*文本框高度*/
        float textBoxHeight = position.getHeight();
        /*文本单行行高*/
        float ascent = baseFont.getFontDescriptor(baseFont.ASCENT, fontSize);
        /*baseFont渲染后的文字宽度*/
        float textWidth = baseFont.getWidthPoint(addr, fontSize);

        /*文本框高度只够写一行,并且文字宽度大于文本框宽度,则缩小字体*/
        if (textBoxHeight < ascent * 1.6) {
            while (textWidth > textBoxWidth) {
                fontSize--;
                textWidth = baseFont.getWidthPoint(addr, fontSize);
            }
        }
        return fontSize;
    }

修改生成pdf方法,加上s.setFieldProperty()方法。

        s.setField("name", person.getName());
        s.setFieldProperty("name", "textsize", checkAddrLength(person.getName(), "name", s), null);
        s.setField("address1", person.getAddress1());
        s.setFieldProperty("address1", "textsize", checkAddrLength(person.getAddress1(), "address1", s), null);
        s.setField("address2", person.getAddress2());
        s.setFieldProperty("address2", "textsize", checkAddrLength(person.getCity(), "address2", s), null);
        s.setField("city", person.getCity());
        s.setFieldProperty("city", "textsize", checkAddrLength(person.getCity(), "city", s), null);

 缩放效果如下:

这里提一句,若对文本域值长度无法预估,建议对字体大小做一个判断,最好缩放的幅度不大于4。如果不判断缩放的大小,若字符串长度很长,可能会缩放到很小导致完全看不到,又或者缩放的幅度很大可能会导致页面字体大小差距过大,显得很突兀。

6.半自动换行

半自动换行和上面的字体缩放一样,都是为了解决文本域字体超出的问题。首先解释这里为什么叫半自动换行,因为其实使用模板来说是无法真正简单做到超长换行的。只能通过对文本域宽度的调整,使其达到换行的目的,但是我个人理解如果字体长度限制在一个可控的范围内,如像上面举到的地址例子一样,一行不够的话,两行肯定是够了的。若无法对字体长度进行控制,可能有些十几行都有了,有些只有一两行,这种需求可能使用这个方法难以实现。

半自动换行也就是在字体缩放的基础上,因为通过设置使用字体及大小能准确得到当前文本值是否已经超出文本域的范围,若已经超出范围,对当前文本域宽度进行调整,且对当前文本域以下的文本域都进行位置的调整,完成宽度拓张。这种方法的优点就是不用调整字体大小,可以使页面字体大小保持一致,缺点就是若文本值超长,计算移位间距相对复杂,或者需要调整文本域下还有很多未赋值的文本域,逐个去调整可能会很麻烦。大概原理如同数组从中间插入或删除数据,对当前位置做了调整之后,当前位置之后的所有数据都会调整下标,效率较慢。

1.调整模板

使用Adobe Acrobat打开模板,右击对应文本域,在选项处勾选“多行”。

2.改写checkAddr()方法,只需判断当前文字是否超出文本域。

    public static boolean checkAddrLength(String addr, String fileName, AcroFields form) throws IOException, DocumentException {
        float fontSize = 12f;
        boolean flag = false;
        BaseFont baseFont = BaseFont.createFont("STSongStd-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
        float textWidth = baseFont.getWidthPoint(addr, fontSize);
        Rectangle position = form.getFieldPositions(fileName).get(0).position;
        float textBoxWidth = position.getWidth();
        if (textWidth > textBoxWidth) {
            flag = true;
        }
        return flag;
    }

3.修改生成方法

        s.setField("address1", person.getAddress1());
        //若当前文本域无法满足
        if (checkAddrLength(person.getAddress2(), "address2", s)) {
            /*获取当前文本框的尺寸,返回的数据依次为左上右下(0,1,2,3)*/
            PdfArray rect1 = s.getFieldItem("address2").getValue(0).getAsArray(PdfName.RECT);
            rect1.set(1, new PdfNumber(rect1.getAsNumber(1).intValue() - 16));
        }
        s.setField("address2", person.getAddress2());

我这里只对"address2"所对应的文本域做了调整,至于这个“16”,取决于你们设置当前文本与的宽度是多少。可以对rect1打断点,查看返回的数组大小差额,方便调整具体宽度。

换行后效果如下:

对于半自动换行,我没有做过多的研究,因为当前项目要求不能换行。若由此需求的朋友可以再深究一下,看存在多行的情况下如何去实现换行功能。

 

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐