java拾遗

Java生成pdf文件或jpg图片

2020-09-08  本文已影响0人  砒霜拌辣椒

在一些业务场景中,需要生成pdf文件或者jpg图片,有时候还需要带上水印。我们可以事先用freemarker定义好html模板,然后把模板转换成pdfjpg文件。

同时freemarker模板还支持变量的定义,在使用时可以填充具体的业务数据。

1、Maven导包

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.4.RELEASE</version>
</parent>
<dependencies>
    <!-- freemarker -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-support</artifactId>
    </dependency>
    <dependency>
        <groupId>org.freemarker</groupId>
        <artifactId>freemarker</artifactId>
    </dependency>
    <!-- pdf核心包 -->
    <dependency>
        <groupId>com.itextpdf</groupId>
        <artifactId>itextpdf</artifactId>
        <version>5.5.12</version>
    </dependency>
    <!-- 适配中文字体 -->
    <dependency>
        <groupId>com.itextpdf</groupId>
        <artifactId>itext-asian</artifactId>
        <version>5.2.0</version>
    </dependency>
    <!-- html转pdf -->
    <dependency>
        <groupId>com.itextpdf.tool</groupId>
        <artifactId>xmlworker</artifactId>
        <version>5.5.12</version>
    </dependency>
    <!-- pdf转图片 -->
    <dependency>
        <groupId>org.apache.pdfbox</groupId>
        <artifactId>pdfbox</artifactId>
        <version>2.0.5</version>
    </dependency>
</dependencies>

2、接口定义

2.1、请求

@Data
public class GeneratePdfReq {
    /**
     * 生成pdf文件的绝对路径
     */
    @NotBlank(message = "生成pdf文件的绝对路径不能为空")
    @Pattern(regexp = "^.*(\\.pdf|\\.jpg)$", message = "生成的文件必须以.pdf或.jpg结尾")
    private String absolutePath;
    /**
     * 使用html模板的绝对路径
     */
    @NotBlank(message = "使用的模板路径不能为空")
    private String templateName;
    /**
     * 渲染模板的业务数据
     */
    private Object dataModel;
    /**
     * 水印信息
     */
    private WaterMarkInfo waterMarkInfo;
    /**
     * pdf文件的宽,默认A4
     */
    private float width = 595;
    /**
     * pdf文件的高,默认A4
     */
    private float height = 842;
}

2.2、水印

@Data
public class WaterMarkInfo {
    /**
     * 如果为null设置水印时会报错
     */
    private String waterMark = "";
    /**
     * 水印透明度,值越小透明度越高
     */
    private float opacity = 0.2F;
    /**
     * 水印字体,如果乱码设置为本地宋体字体:fonts/simsun.ttc,1
     */
    private String fontName = "STSong-Light";
    /**
     * 水印编码格式,如果乱码设置为:BaseFont.IDENTITY_H
     */
    private String encoding = "UniGB-UCS2-H";
    /**
     * 字体大小
     */
    private float fontSize = 24;
    /**
     * 横坐标在页面宽度的百分比,左下角为原点
     */
    private float x = 50;
    /**
     * 纵坐标在页面高度的百分比,左下角为原点
     */
    private float y = 40;
    /**
     * 水印旋转角度
     */
    private float rotation = 45;
}

2.3、响应

@Data
public class GeneratePdfResp {
    /**
     * 生成pdf的绝对路径
     */
    private String absolutePath;
}

3、应用代码

3.1、渲染freemarker模板获取html网页

@Service("freeMarkerService")
@Slf4j
public class FreeMarkerServiceImpl implements FreeMarkerService {
    @Autowired
    private FreeMarkerConfigurer freeMarkerConfigurer;

    /**
     * 渲染html后获取整个页面内容
     *
     * @param templatePath 模板路径
     * @param dataModel    业务数据,一般以map形式传入
     * @return
     */
    @Override
    public String getHtml(String templatePath, Object dataModel) {
        log.info("开始将模板{}渲染为html,业务数据{}", templatePath, JSONUtil.toJsonPrettyStr(dataModel));
        Configuration cfg = freeMarkerConfigurer.getConfiguration();
        cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); // freemaker异常时仍旧抛出,统一异常处理
        cfg.setClassicCompatible(true);// 不需要对null值预处理,否则需要在模板取值时判断是否存在,不然报错
        StringWriter stringWriter = new StringWriter();
        try {
            // 设置模板所在目录,绝对路径方式,不打进jar包
//            cfg.setDirectoryForTemplateLoading(new File(templatePath).getParentFile());
//            Template temp = cfg.getTemplate(new File(templatePath).getName());

            // 相对路径设置模板所在目录,模板打进jar包,默认就是resources目录下的/templates目录。
            cfg.setClassForTemplateLoading(this.getClass(), "/templates");
            Template temp = cfg.getTemplate(templatePath);
            temp.process(dataModel, stringWriter);
        } catch (Exception e) {
            log.error(PdfErrorCode.PDF_TEMPLATE_RENDER_FAIL.getDesc(), e);
            throw new PdfBizException(PdfErrorCode.PDF_TEMPLATE_RENDER_FAIL);
        }
        return stringWriter.toString();
    }
}

3.2、将html网页转pdf,并添加水印

@Service("pdfService")
@Slf4j
public class PdfServiceImpl implements PdfService {
    public static final String FONT_PATH = "fonts/simsun.ttc,1";

    @Autowired
    private WaterMarkerService waterMarkerService;

    /**
     * html页面内容转pdf,并给每页附上水印
     *
     * @param html          html页面内容
     * @param width         pdf的宽
     * @param height        pdf的高
     * @param waterMarkInfo 水印信息
     * @return
     */
    @Override
    public byte[] html2Pdf(String html, float width, float height, WaterMarkInfo waterMarkInfo) {
        log.info("=================开始将html转换为pdf=================");
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        this.html2Pdf(html, width, height, out);
        byte[] bytes = out.toByteArray();
        // 设置水印
        if (waterMarkInfo != null) {
            bytes = waterMarkerService.addWaterMarker(bytes, waterMarkInfo);
        }
        return bytes;
    }

    /**
     * html转pdf
     *
     * @param html   html页面内容
     * @param width  pdf的宽
     * @param height pdf的高
     * @param out    输出流,pdf文件用此流输出,需要pdf文档关闭后流中才会有数据
     */
    @Override
    @SneakyThrows
    public void html2Pdf(String html, float width, float height, OutputStream out) {
        @Cleanup Document document = new Document(new RectangleReadOnly(width, height)); // 默认A4纵向
        // 这里需要关闭document才能让生成的pdf字节数据刷到输出流中
        PdfWriter writer = PdfWriter.getInstance(document, out); // 关闭可能导致生成的pdf显示异常(Chrome)
        document.open();
        // 设置字体,这里统一用simsun.ttc即宋体
        XMLWorkerFontProvider asianFontProvider = new XMLWorkerFontProvider() {
            @Override
            public Font getFont(String fontname, String encoding, boolean embedded, float size, int style, BaseColor color, boolean cached) {
                Font font;
                try {
                    font = new Font(BaseFont.createFont(FONT_PATH, BaseFont.IDENTITY_H, BaseFont.EMBEDDED));
                } catch (Exception e) {
                    log.error(PdfErrorCode.SET_PDF_FONT_FAIL.getDesc(), e);
                    throw new PdfBizException(PdfErrorCode.SET_PDF_FONT_FAIL);
                }
                font.setStyle(style);
                font.setColor(color);
                if (size > 0) {
                    font.setSize(size);
                }
                return font;
            }
        };

        // 生成pdf
        try {
            XMLWorkerHelper.getInstance().parseXHtml(writer, document, new ByteArrayInputStream(html.getBytes("UTF-8")), null, Charset.forName("UTF-8"), asianFontProvider);

            // 如果系统已经装有simsun.ttc字体,则不需要单独设置字体也不需要itext-asian jar包
//            XMLWorkerHelper.getInstance().parseXHtml(writer, document, new ByteArrayInputStream(html.getBytes("UTF-8")), null, Charset.forName("UTF-8"));
        } catch (RuntimeWorkerException e) {
            log.error(PdfErrorCode.HTML_CONVERT2PDF_FAIL.getDesc(), e);
            throw new PdfBizException(PdfErrorCode.HTML_CONVERT2PDF_FAIL);
        }
    }
}

添加水印实现类

@Service("waterMarkerService")
@Slf4j
public class WaterMarkerServiceImpl implements WaterMarkerService {

    /**
     * 给pdf文件每页添加水印
     *
     * @param source        pdf文件的字节数组形式
     * @param waterMarkInfo 水印信息
     * @return
     */
    @Override
    public byte[] addWaterMarker(byte[] source, WaterMarkInfo waterMarkInfo) {
        log.info("开始设置水印数据{}", JSONUtil.toJsonPrettyStr(waterMarkInfo));
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        this.addWaterMarker(source, waterMarkInfo, out);
        return out.toByteArray();
    }

    /**
     * 给pdf文件每页添加水印
     *
     * @param source        pdf文件的字节数组形式
     * @param waterMarkInfo 水印信息
     * @param out           输出流,pdf文件用此流输出,需要pdf文档关闭后流中才会有数据
     */
    @Override
    @SneakyThrows
    public void addWaterMarker(byte[] source, WaterMarkInfo waterMarkInfo, OutputStream out) {
        @Cleanup PdfReader reader = new PdfReader(source);
        // 这里需要关闭PdfStamper才能让生成的pdf字节数据刷到输出流中
        @Cleanup PdfStamper pdfStamper = new PdfStamper(reader, out);
        BaseFont font = BaseFont.createFont(waterMarkInfo.getFontName(), waterMarkInfo.getEncoding(), BaseFont.EMBEDDED);
        PdfGState gs = new PdfGState();
        gs.setFillOpacity(waterMarkInfo.getOpacity());
        // 给每页pdf生成水印
        for (int i = 1; i <= reader.getNumberOfPages(); i++) {
            PdfContentByte waterMarker = pdfStamper.getUnderContent(i);
            waterMarker.beginText();
            // 设置水印透明度
            waterMarker.setGState(gs);
            // 设置水印字体和大小
            waterMarker.setFontAndSize(font, waterMarkInfo.getFontSize());
            // 设置水印位置、内容、旋转角度
            float X = reader.getPageSize(i).getWidth() * waterMarkInfo.getX() / 100;
            float Y = reader.getPageSize(i).getHeight() * waterMarkInfo.getY() / 100;
            waterMarker.showTextAligned(Element.ALIGN_CENTER, waterMarkInfo.getWaterMark(), X, Y, waterMarkInfo.getRotation());
            // 设置水印颜色
            waterMarker.setColorFill(BaseColor.GRAY);
            waterMarker.endText();
        }
    }
}

3.3、整合实现

@Slf4j
@Service("generatePdfService")
public class GeneratePdfServiceImpl implements RestService {
    @Autowired
    private FreeMarkerService freeMarkerService;

    @Autowired
    private PdfService pdfService;

    @Override
    @SneakyThrows
    public GeneratePdfResp service(GeneratePdfReq generatePdfReq) {
        log.info("开始生成pdf文件,请求报文:{}", JSONUtil.toJsonPrettyStr(generatePdfReq));
        /*
        1.根据freemarker模板填充业务数据获取完整的html字符串
         */
        String html = freeMarkerService.getHtml(generatePdfReq.getTemplateName(), generatePdfReq.getDataModel());

        /*
        2.生成pdf文件(内存)
         */
        byte[] bytes = pdfService.html2Pdf(html, generatePdfReq.getWidth(), generatePdfReq.getHeight(), generatePdfReq.getWaterMarkInfo());

        /*
        3.本地保存pdf文件
         */
        File targetFile = new File(generatePdfReq.getAbsolutePath());
        // 上级目录不存在则创建
        if (!targetFile.getParentFile().exists()) {
            targetFile.getParentFile().mkdirs();
        }

        // 根据不同文件名后缀生成对应文件
        if (generatePdfReq.getAbsolutePath().endsWith("pdf")) {
            FileUtils.writeByteArrayToFile(targetFile, bytes);
        } else {
            @Cleanup PDDocument document = PDDocument.load(bytes);
            PDFRenderer renderer = new PDFRenderer(document);
            BufferedImage bufferedImage = renderer.renderImageWithDPI(0, 150);// 只打第一页,dpi越大图片越高清也越耗时
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ImageIO.write(bufferedImage, "jpg", baos);
            FileUtils.writeByteArrayToFile(targetFile, baos.toByteArray());
        }
        log.info("文件本地保存完成,文件路径:[{}]", targetFile.getAbsolutePath());

        /*
        4.组织返回
         */
        GeneratePdfResp generatePdfResp = new GeneratePdfResp();
        generatePdfResp.setAbsolutePath(targetFile.getAbsolutePath());
        return generatePdfResp;
    }
}

3.4、controller

@Slf4j
@RestController
public class PdfController {
    @Autowired
    private RestService generatePdfService;

    @PostMapping(value = "/html2Pdf")
    public GeneratePdfResp html2Pdf(@RequestBody @Validated GeneratePdfReq req) {
        GeneratePdfResp resp = generatePdfService.service(req);
        return resp;
    }
}

4、应用

4.1、freemarker模板(html模板)

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <meta http-equiv="Content-Style-Type" content="text/css"/>
    <style>
        body {
            font-family: SimSun
        }
    </style>
    <title>html模板</title>
</head>
<body>
<div>
    <p style="margin:0pt; orphans:0; text-align:center; widows:0">
        <span style="font-family:SimSun; font-size:16pt">html模板</span><br/>
    </p>
    <p>姓名:${name}</p>
    <p>证件号码:${cardNo}</p>
    <p>日期:${date}</p>
</div>
</body>
</html>

4.2、接口调用生成pdf

postman pdf

5、说明

  1. 根据参数后缀名可以生成pdf或jpg文件,生成的pdf文件默认为A4大小,也可以通过请求参数设置大小。
  2. pdf文件会根据html模板内容大小自动分页。
  3. 如果生成图片,多页不会生成多张图片,可以把高度设置大一些,最后会生成长图。
  4. 水印每页都会自动添加。
  5. 为了提高代码的复用性和可维护性,工程内渲染html模板、生成pdf文件、添加水印都有单独的接口实现。

参考资料

代码地址

上一篇 下一篇

猜你喜欢

热点阅读