springboot生成pdf方案之dot/html/图片转pdf三种方式

文章目录

前言:随着客户对报告审美的提升,需求也越来越五彩斑斓~ 原有的dot模板已经满足不了他们了!这篇文章主打列出各种方案及适用场景,带部分demo。

pdf生成方案

dot转pdf

自定义.dot文档,模板中插入书签占位,使用aspose-words转换为pdf

ps:这个收费=_=|| 公司之前有项目用了,所以没探索其他实现方案

html转pdf

探索了三个框架,openhtmltopdfaspose-pdfflying-saucer-pdf-openpdf,下面分别对这些方案进行描述。

openhtmltopdf

源码地址

明晃晃的优点:

  1. 开源&&免费

但有两个不得不忽视的缺点:

  1. 中文乱码,官网issue中有人提单了含有中文字符的html输出pdf有乱码 #129,按照解决方案并不能修复,所以block了
  2. 仅仅支持简单的CSS

aspose-pdf

官网:Creating a complex PDF,虽然和aspose-words都是aspose家的,但他们分开收费!!!

优点:

  1. 内置14种字体 - 中文支持度非常高
  2. 支持加密/解密、数字签名、权限控制
  3. 文档完善、社区活跃度高

缺点:

  1. 贵!!!经费不足不考虑
  2. 对CSS3中部分样式不支持,例如aspect-ratio,需要后端一点点排查再让前端调整。。。 (太难了)
实践

注意: 这个库不是在maven中央仓库中管理,需要加一个仓库配置https://releases.aspose.com/java/repo/

xml 复制代码
<dependency>
    <groupId>com.aspose</groupId>
    <artifactId>aspose-pdf</artifactId>
    <version>23.6</version>
</dependency>
java 复制代码
import com.aspose.pdf.Document;
import com.aspose.pdf.HtmlLoadOptions;
import com.aspose.pdf.SaveFormat;

public void generatePdf(String name) {	
	// 1. 准备数据模型
    Map<String, Object> data = new HashMap<>();
    data.put("name", name);
    data.put("date", LocalDate.now().toString());

    String html = freeMarkerService.getTemplate2String("report.ftl", data);
    
    try (FileOutputStream fileOutputStream = new FileOutputStream("E:\\test\\report.pdf");
    		InputStream stream = new ByteArrayInputStream(html.getBytes("UTF-8"));) {
    	// 加载静态资源
    	HtmlLoadOptions loadOptions = new HtmlLoadOptions("src/main/resources/static/report");
    	loadOptions.setEmbedFonts(true);
    	
    	Document document = new Document(stream, loadOptions);
    	document.save(fileOutputStream, SaveFormat.Pdf);
        
    } catch (Exception e) {
    	log.error("[generatePdf] html转pdf失败", e);
    }
}

playwright

由前端提供的html文件,里面包含的CSS样式太复杂了,没办法只能用webkit这种方式渲染样式才不会有大的偏差~
优点:

  1. 渲染质量ok,基本上和html展示一致
  2. 开源免费
  3. 跨平台支持,docker中也可运行

缺点:

  1. 初次运行要下载浏览器
  2. 资源消耗大,每个转换需要100-300M内存
  3. Java版是对Node.js版的封装
实践

maven

xml 复制代码
<dependency>
    <groupId>com.microsoft.playwright</groupId>
    <artifactId>playwright</artifactId>
    <version>1.52.0</version>
</dependency>

业务代码(强制将输出A4纸张大小):

java 复制代码
package com.lizzy.mp.service;

import java.io.FileOutputStream;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;

import org.springframework.stereotype.Service;

import com.microsoft.playwright.Browser;
import com.microsoft.playwright.BrowserType;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.Playwright;
import com.microsoft.playwright.options.Margin;

import lombok.extern.slf4j.Slf4j;

@Service
@Slf4j
public class PlaywrightPdfService {

	@Resource
	private FreeMarkerService freeMarkerService;
	
    private Playwright playwright;
	
    private Browser browser;
    
    @PostConstruct
    public void init() {
        playwright = Playwright.create();
        browser = playwright.chromium().launch(new BrowserType.LaunchOptions()
            .setHeadless(true)
            .setArgs(Stream.of("--disable-dev-shm-usage").collect(Collectors.toList())));
    }
    
    public void generatePdf(String name) {
        
        // 1. 准备数据模型
        Map<String, Object> data = new HashMap<>();
        data.put("name", name);
        data.put("date", LocalDate.now().toString());

        String htmlContent = freeMarkerService.getTemplate2String("report2.ftl", data);
        
        try (Page page = browser.newPage();
             FileOutputStream fileOutputStream = new FileOutputStream("E:\\test\\report.pdf");) {
            page.setContent(htmlContent);
            byte[] bytes = page.pdf(new Page.PdfOptions()
            		.setMargin(new Margin().setTop("0cm").setBottom("0cm").setLeft("0cm").setRight("0cm"))
            		.setPrintBackground(true)
            		.setFormat("A4"));
  
            fileOutputStream.write(bytes);

        } catch (Exception e) {
            log.error("[generatePdf] HTML转PDF失败", e);
        }
        log.info("[generatePdf] HTML转PDF成功");
    }
    
    @PreDestroy
    public void cleanup() {
        if (browser != null) {
            browser.close();
        }
        if (playwright != null) {
            playwright.close();
        }
    }
}

图片转pdf

项目中前后端共用一个html模板,前端会有预览功能,于是乎讨论出一个方案:前端直接将html生成图片,后端将图片转成pdf,这样后端就不用care样式问题了!

网上解决方案很多,作者只调研了Apache PDFBox。

Apache PDFBox实践

maven:

xml 复制代码
<dependency>
   <groupId>org.apache.pdfbox</groupId>
    <artifactId>pdfbox</artifactId>
    <version>3.0.3</version>
</dependency>

业务代码,说明:

  • 方法convert中生成的pdf打开后50%展示都很大
  • 方法convertForA4中进行了限制,打开后100%还原
    ps:最根本的解决方法还是控制css样式为A4
java 复制代码
package com.lizzy.mp.service;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.springframework.stereotype.Service;

import lombok.extern.slf4j.Slf4j;

@Service
@Slf4j
public class Image2PdfService {
	
	public void convert() {
		
		// 创建PDF文档
        try (PDDocument document = new PDDocument()) {
            // 加载图片
            PDImageXObject pdImage = PDImageXObject.createFromFile("E:\\report_page-0001.jpg", document);
            
            // 创建页面,大小与图片相同
            PDPage page = new PDPage(new PDRectangle(pdImage.getWidth(), pdImage.getHeight()));
            document.addPage(page);
            
            // 将图片写入PDF
            try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
                contentStream.drawImage(pdImage, 0, 0);
            }
            
            // 保存PDF
            document.save("E:\\test\\report0.pdf");
        } catch (IOException e) {
        	log.error("[convert] 图片转pdf失败,错误原因:{}", e.getMessage(), e);
        }
		
	}
	
	public void convertForA4() {
		String imagePath = "E:\\report_page-0001.jpg";
        String outputPdfPath = "E:\\test\\report0.pdf";

        try (PDDocument document = new PDDocument()) {
            // 读取图片
            BufferedImage image = ImageIO.read(new File(imagePath));
            if (image == null) throw new IOException("无法读取图片");

            PDImageXObject pdImage = PDImageXObject.createFromFile(imagePath, document);

            // 创建A4页面
            PDRectangle a4 = PDRectangle.A4;
            PDPage page = new PDPage(a4);
            document.addPage(page);

            // 原始图片尺寸
            float imageWidth = image.getWidth();
            float imageHeight = image.getHeight();

            // A4尺寸
            float pageWidth = a4.getWidth();
            float pageHeight = a4.getHeight();

            // 缩放比例(等比缩放)
            float scale = Math.min(pageWidth / imageWidth, pageHeight / imageHeight);
            float drawWidth = imageWidth * scale;
            float drawHeight = imageHeight * scale;

            // 居中坐标
            float x = (pageWidth - drawWidth) / 2;
            float y = (pageHeight - drawHeight) / 2;

            // 写入图像
            try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
                contentStream.drawImage(pdImage, x, y, drawWidth, drawHeight);
            }

            // 保存PDF
            document.save(outputPdfPath);
        } catch (IOException e) {
            System.err.println("图片转PDF失败: " + e.getMessage());
            e.printStackTrace();
        }
	}
}

框架场景匹配

框架名称 CSS样式支持度 是否开源 中文支持 使用难易 体积大小 说明
Aspose.word 中等 ❌,收费高 试用版有水印 简单 50+MB
Aspose.pdf CSS3(动画等不支持) ❌,收费高 试用版有水印 中等
openhtmltopdf CSS2.1(基本支持) 需显示引入字体 简单 小,3~5MB
playwright 非常高 浏览器原生支持 需运行浏览器依赖 大,依赖Chromium
apache pdfbox - - - - 只使用图像转pdf,无需控制样式

后记

因项目背景,对报告pdf的生成要求蛮高,所以得不停尝试各种解决方案,推荐获取思路的网址~

ps:思考要记录,不然会忘记~

相关推荐
程序员的记录9 分钟前
AI 实战 - 文档处理(pdf/work/md/txt...)
pdf
StockTV1 小时前
印度股票实时数据 NSE和BSE的实时行情、K 线及指数数据
java·开发语言·spring boot·python
橘子海全栈攻城狮2 小时前
【最新源码】养老院系统管理A013
java·spring boot·后端·web安全·微信小程序
敖正炀2 小时前
反模式与排查宝典:Spring Boot 自动配置与核心机制的常见陷阱
spring boot
Muyuan19982 小时前
22.让 RAG Agent 更像真实产品:聊天页面优化、PDF 上传、知识库重建与检索片段展示
python·django·pdf·fastapi
直奔標竿2 小时前
Java开发者AI转型第二十六课!Spring AI 个人知识库实战(五)——联网搜索增强实战
java·开发语言·人工智能·spring boot·后端·spring
吴爃4 小时前
Spring Boot 项目在 K8S 中的打包、部署与运维发布实践
运维·spring boot·kubernetes
a8a3024 小时前
Laravel8.x新特性全解析
java·spring boot·后端
白露与泡影4 小时前
Spring Boot 完整流程
java·spring boot·后端