骄阳似火的六月天,被迫在办公室吹着空调瑟瑟发抖。脑海中不停的思索,什么才是程序员真正的宿命。


这次我们要实现一个导出pdf文件的简单需求,基于Spring Boot 2.2.5.RELEASE。

我们用到的依赖如下

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

spring mvc中已经存在了一个AbstractPdfView的抽象类,但是使用到的依赖被网友吐槽太过老旧,所以我们自己写一个,这个抽象类没什么特别,就是将原有的包都换成itext的包。

package com.jason.cloud.view;

import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.PageSize;
import com.itextpdf.text.pdf.PdfWriter;
import org.springframework.web.servlet.view.AbstractView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.util.Map;

/**
 * 重写抽象类,将原有的AbstractPdfView由itext低版本替换为高版本
 * create by Jason
 * Date: 2021/6/2
 * Time: 11:52
 */
public abstract class AbstractTextPdfView extends AbstractView {

    public AbstractTextPdfView() {
        this.setContentType("application/pdf");
    }

    protected boolean generatesDownloadContent() {
        return true;
    }

    protected final void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        ByteArrayOutputStream baos = this.createTemporaryOutputStream();
        Document document = this.newDocument();
        PdfWriter writer = this.newWriter(document, baos);
        this.prepareWriter(model, writer, request);
        this.buildPdfMetadata(model, document, request);
        this.buildPdfDocument(model, document, writer, request, response);
        this.writeToResponse(response, baos);
    }

    protected Document newDocument() {
        return new Document(PageSize.A4);
    }

    protected PdfWriter newWriter(Document document, OutputStream os) throws DocumentException {
        return PdfWriter.getInstance(document, os);
    }

    protected void prepareWriter(Map<String, Object> model, PdfWriter writer, HttpServletRequest request) throws DocumentException {
        writer.setViewerPreferences(this.getViewerPreferences());
    }

    protected int getViewerPreferences() {
        return PdfWriter.ALLOW_PRINTING | PdfWriter.PageLayoutSinglePage;
    }

    protected void buildPdfMetadata(Map<String, Object> model, Document document, HttpServletRequest request) {
    }

    protected abstract void buildPdfDocument(Map<String, Object> var1, Document var2, PdfWriter var3, HttpServletRequest var4, HttpServletResponse var5) throws Exception;


}

再创建一个处理类,用来添加PDF中的文字水印,当然,没有需要的话可以不用。

package com.jason.cloud.view;

import com.itextpdf.text.*;
import com.itextpdf.text.pdf.*;
import org.springframework.core.io.ClassPathResource;

import java.io.IOException;

/**
 * create by Jason
 * Date: 2021/6/7
 * Time: 14:15
 */
public class PdfWaterMarkEventHandler extends PdfPageEventHelper {

    @Override
    public void onStartPage(PdfWriter writer, Document document) {
        // 增加水印,且水印在文字下
        PdfContentByte waterMark = writer.getDirectContentUnder();

        BaseFont simSunFont = null;
        try {
            simSunFont = BaseFont.createFont(new ClassPathResource("/font/SimSun.ttf").getPath(), BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
        } catch (DocumentException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        Font defaultFont = new Font(simSunFont, 36f, Font.BOLD, new GrayColor(0.93f));
        
        // 设置水印文字
        Phrase phrase = new Phrase("RPD", defaultFont);

        final int gap = 270;
        for (int i = 1; i < 6; i++) {
            for (int j = 1; j < 6; j++) {
                // 设置对齐方式,内容,坐标,旋转角度
                ColumnText.showTextAligned(waterMark, Element.ALIGN_RIGHT, phrase , gap*i - 70, gap*j - 70, 45);
                ColumnText.showTextAligned(waterMark, Element.ALIGN_RIGHT, phrase , gap*i - 140, gap*j - 140, 45);
                ColumnText.showTextAligned(waterMark, Element.ALIGN_RIGHT, phrase , gap*i, gap*j, 45);
            }
        }

        super.onStartPage(writer, document);
    }

}

我们写一个实现了自己AbstractTextPdfView的类PdfView。

package com.jason.cloud.view;

import com.itextpdf.text.*;
import com.itextpdf.text.pdf.*;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URLEncoder;
import java.util.Map;

public class PdfView extends AbstractTextPdfView {

    // 大标题字号
    private final float FONT_SIZE_TITLE = 16f;

    // 小标题字号
    private final float FONT_SIZE_HEADLINE = 14f;

    // 默认字号
    private final float FONT_SIZE_DEFAULT = 12f;

    // 标题行间距
    private final float LEADING_HEADLINE = 24f;

    // 默认行间距
    private final float LEADING_DEFAULT = 14f;

    // 段落间距
    private final float SPACING_PARAGRAPH = 18f;

    // 标题与内容间距
    private final float SPACING_CONTENT = 12f;

    // 左侧缩进
    private final float INDENTATION_LEFT = 50f;

    @Override
    protected void buildPdfDocument(Map<String, Object> map, Document document, PdfWriter pdfWriter, HttpServletRequest request, HttpServletResponse response) throws Exception {
        String name = (String) map.get("fileName");
        if (StringUtils.isEmpty(name)) {
            name = "pdf";
        }
        String fileName = name + ".pdf";

        // 处理火狐浏览器乱码
        String agent = request.getHeader("User-Agent");
        if (agent != null) {
            if ("firefox".contains(agent.toLowerCase())) {
                response.setHeader("content-disposition", String.format("attachment;filename*=utf-8'zh_cn'%s", URLEncoder.encode(fileName,"utf-8")));
            } else {
                response.setHeader("content-disposition", "attachment;filename=" + URLEncoder.encode(fileName, "utf-8"));
            }
        }

        response.setCharacterEncoding("UTF-8");
        response.setContentType("Application/pdf;charset=utf-8");

        // 这个设置对我来说不管用,使用了下一行的字体设置
//        BaseFont baseFont = BaseFont.createFont("STSong-Light","UniGB-UCS2-H",true);
        // 字体 宋体(这里的设置看文末的说明)
        BaseFont SimSunFont = BaseFont.createFont(new ClassPathResource("/font/SimSun.ttf").getPath(), BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
//        BaseFont SimSunFont = BaseFont.createFont(new ClassPathResource("/font/simsun.ttc").getPath() + ",1", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
        // 设置默认字号
        Font defaultFont = new Font(SimSunFont, FONT_SIZE_DEFAULT);
        // 设置标题字号
        Font headlineFont = new Font(SimSunFont, FONT_SIZE_HEADLINE);
        // 默认黑色
//        defaultFont.setColor(BaseColor.BLACK);

        // 默认A4
//        document.setPageSize(PageSize.A4);
        document.addTitle((String) map.get("title"));
        
        // 加入事件处理,此处用来添加水印
        pdfWriter.setPageEvent(new PdfWaterMarkEventHandler());

        // 打开文档
        document.open();

        // 设置标题内容,字号
        Paragraph title = new Paragraph(new Chunk("需求审核申请单", new Font(SimSunFont, FONT_SIZE_TITLE)));
        // 设置居中
        title.setAlignment(Element.ALIGN_CENTER);
        // 设置下间距
        title.setSpacingAfter(LEADING_HEADLINE);
        document.add(title);

        // 章节,带序号
//        Chapter chapter = new Chapter(title, 1);
//        document.add(chapter);

        /*  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~  */

        // 一级标题
        Paragraph headlineApply = new Paragraph(new Chunk("申请单信息", headlineFont));
        // 设置标题与内容间距
        headlineApply.setSpacingAfter(SPACING_CONTENT);
        document.add(headlineApply);

        // 创建申请单信息表格,5列
        PdfPTable applyInfoTable = new PdfPTable(5);
        // 设置表格占比
        applyInfoTable.setWidthPercentage(100f);
        // 表格宽度占比
        applyInfoTable.setWidths(new int[]{25, 35, 15, 10, 15});

        // 表头
        final String[] applyInfoColumns = new String[] {"申请单号", "申请事项", "所属单位", "申请人", "手机号"};

        for (String infoColumn : applyInfoColumns) {
            // 建立单元格
            PdfPCell cell = new PdfPCell(new Phrase(infoColumn, defaultFont));
            // 设置单元格高度(不要瞎设置,高度不够会影响换行)
//            cell.setFixedHeight(TABLE_CELL_HEIGHT);
            // 设置单元格背景色
            cell.setBackgroundColor(new BaseColor(231,230,230));
            applyInfoTable.addCell(cell);
        }

        // 按照表头顺序填入数据
        PdfPCell cell;
        cell = new PdfPCell(new Phrase("124389759016789", defaultFont));
        applyInfoTable.addCell(cell);

        cell = new PdfPCell(new Phrase("我要申请六十个存档", defaultFont));
        applyInfoTable.addCell(cell);

        cell = new PdfPCell(new Phrase("浣熊市警察局", defaultFont));
        applyInfoTable.addCell(cell);

        cell = new PdfPCell(new Phrase("里昂", defaultFont));
        applyInfoTable.addCell(cell);

        cell = new PdfPCell(new Phrase("18876543210", defaultFont));
        applyInfoTable.addCell(cell);

        // 将数据表加入文档
        document.add(applyInfoTable);

        /*  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~  */

        // 一级标题
        Paragraph headlineDemand = new Paragraph(new Chunk("需求详情", headlineFont));
//        headlineDemand.setSpacingBefore(SPACING_PARAGRAPH);
        document.add(headlineDemand);


        /*  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~  */


        // 加入list
        List list = new List();
        list.setSymbolIndent(12f);
        // 设置item样式
        list.setListSymbol("\u2022");
        // 多写几行,使pdf变成两页
        for (int i = 1; i <= 60; i++) {
            list.add(new ListItem(new Phrase("第" + i + "项", defaultFont)));
        }

        document.add(list);


        document.close();
    }
}

那最后我们在将前端控制器补上

    @GetMapping("/pdf")
    public ModelAndView exportPDF() {
        Map<String, Object> params = new HashMap<>(16);
        params.put("fileName", "pdf文档导出");
        params.put("title", "这是一个神奇的PDF");
        return new ModelAndView(new PdfView(), params);
    }

这里着重说明一点,最初我也是使用下面这一段代码,导出的pdf汉字不会显示(使用预览的时候,不但能看到汉字,还能复制文字,就很奇怪)。这里有可能就是字符集不匹配造成的。

// 这段代码酌情使用
BaseFont.createFont("STSong-Light","UniGB-UCS2-H",BaseFont.NOT_EMBEDDED);

解决方法如下:
C:\Windows\Fonts中找到新宋体 常规(当然前面的宋体常规也可以),复制到工程的/resources/font下,会变成simsun.ttc,并且在代码中获取路径后需加上",1"(据说ttc是多个ttf的聚合,1类似于数组下标),并且逗号和1之间不能有空格。
image.png

或者网上下载了SimSun.ttf(我下载的10M)。
image.png


虚惊一百场
19 声望7 粉丝

1 + 1 = 2