【技术实战】Java实现导出为效果最好的PDF文件

前言

生成 PDF 文件的方法有很多,都会有几个卡点,就是样式和字体的问题,这里直接推荐 PlayWright 可以将网页转化为 pdf 文件,效果和网页效果一样,美得很
先给出 playwright 官网,https://playwright.dev/docs/intro ,依靠 Nodejs 环境,通过模拟浏览器操作并输出为 pdf文件;官网申明了运行的环境参数,所以出现的各种问题,先从这些条件上找找问题

Node.js 18+
Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL).
macOS 13 Ventura, or later.
Debian 12, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture.

开整

首先声明这是一个Java 项目,所以会用到maven,现引入依赖

 <dependency>
            <groupId>com.microsoft.playwright</groupId>
     <artifactId>playwright</artifactId>
      <version>1.43.0</version> 
</dependency>

新建一个 pdf的工具类,其他地方使用直接调用;PdfUtil.java

import com.microsoft.playwright.*;
import com.microsoft.playwright.options.LoadState;
import com.microsoft.playwright.options.Margin;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;

/**
 * pdf 工具
 * @author xiaofei
 * @date 2024/12/17 17:00
 */
@Slf4j
public class PdfUtil {

     /**
     * 将网页生成 pdf
     * @auth xiaofei
     * @date 2024/12/20 11:39
     */
    public static String generateHtmlToPdf(String url, String fileName,OutputStream outputStream) {
        log.info("开始生成网页的 pdf 文件,url={}",url);
        try (Playwright playwright = Playwright.create()) {
            Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(true));
            BrowserContext context = browser.newContext();
            //new Browser.NewContextOptions()
            //                    .setViewportSize(375, 667) // 设置模拟 iPhone 6/7/8 的视口
            //                    .setUserAgent("Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1")
            Page page = context.newPage();
            page.navigate(url);
            // 修改根节点 <html> 的 font-size,设置为 16px(这是 1rem 对应的大小)
            page.evaluate("document.documentElement.style.fontSize = '25px';");
            //等待加载完成后
            page.waitForLoadState(LoadState.NETWORKIDLE);
            // 等待字体加载(如果页面依赖于 Web 字体)
            page.evaluate("document.fonts.ready.then(() => { window.isFontLoaded = true; });");
            page.waitForFunction("window.isFontLoaded === true");
            //手动等待 10秒
            page.waitForTimeout(10000);
            // 设置视口大小
            //page.setViewportSize(1920, 1080); // 设置视口大小为 1920x1080
            // 临时文件路径
            if (StringUtils.isEmpty(fileName)){
                fileName = "webPage.pdf";
            }
            fileName = "/data/logs/manage/"+fileName;
            //fileName =" /Users/macbook/Project/logs/manage/"+fileName;
            File tempFile = new File(fileName);
            if (tempFile == null || !tempFile.exists()){
                //tempFile.createNewFile();
            }
            // 输出为 PDF
            page.pdf(new Page.PdfOptions()
                    .setPath(Paths.get(tempFile.getAbsolutePath()))  // 设置输出 PDF 的路径
                    .setFormat("A4")  // 设置纸张格式
                    .setPrintBackground(true)  // 确保背景和图像被打印
                    //.setMargin(new Margin().setTop("10px").setBottom("10px").setLeft("10px").setRight("10px"))  // 设置边距
            );
            //输出到流
            if (outputStream != null){
                try {
                    byte[] pdfData = Files.readAllBytes(tempFile.toPath());
                    outputStream.write(pdfData);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }

            // 删除临时文件
            //tempFile.delete();

            // 获取网页加载后的 HTML
            //content = page.content();
            browser.close();
        }
        log.info("完成生成网页的 pdf 文件");
        return fileName;
    }

    public static void main(String[] args) {
        System.out.println("Screenshot begin!");
        String url = "https://baidu.com";
        String outputPath = "baidu.pdf";
        PdfUtil.generateHtmlToPdf(url, outputPath,null);
    }

提供一个 controller 类对外使用 :PdfToolController.java

import util.PdfUtil;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.util.Map;

/**
 * pdf 工具
 * @author xiaofei
 * @date 2024/12/20 14:37
 */
@RestController
@RequestMapping("/webapi/pdfTool")
public class PdfToolController {

    /**
     * 获取 网页pdf
     */
    @GetMapping("/getHtmlToPdf")
    public void htmlToPdf(HttpServletResponse response, String url, String fileName) throws IOException {

        PdfUtil.generateHtmlToPdf(url, fileName,response.getOutputStream());
    }

     /**
     * 获取 网页pdf
     */
    @PostMapping("/postHtmlToPdf")
    public void postHtmlToPdf(HttpServletResponse response,@RequestBody Map<String,Object> params) throws IOException {
        String url = (String) params.get("url");
        String fileName = (String) params.get("fileName");
        PdfUtil.generateHtmlToPdf(url, fileName,response.getOutputStream());
    }
}

重点来了,就是部署问题;需要有 nodejs环境,可以手动安装;playwright需要的浏览器驱动等内容会在服务第一次使用时自动下载,可以不用管

# 安装 nvm (Node 版本管理器)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
# 下载并安装 Node.js(可能需要重启终端)
nvm install 22  # docker容器内支持的最低版本
# 验证环境中是否存在正确的 Node.js 版本
node -v  # 应该打印 `v22.12.0`
# 验证环境中是否存在正确的 npm 版本
npm -v  # 应该打印 `10.9.0`

如果是docker环境,我这里提供一个 dockerfile,特别说明需要高版本的 JDK,不然有一些坑,可以自己测试下试试,我之前用过 jdk8,总是出现一些问题,估计是操作系统版本的问题,所以我改用 jdk17

# 选 择 一 个 包 含  JDK 的 基 础 镜 像 , 这 里 选 择 的 是  openjdk:17-jdk-slim
FROM openjdk:17-jdk-slim


# 设 置 工 作 目 录
WORKDIR /app

# -------------------
# 安 装  Node.js 部 分
# -------------------

# 1. 安 装  curl
RUN apt-get update && apt-get install -y curl

# 2. 下 载  Node.js 安 装 脚 本  (这 里 下 载 的 是  LTS 版 本 )
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -

# 3. 安 装  Node.js 和  npm
RUN apt-get install -y nodejs

# 4. ( 可 选 ) 验 证  Node.js 和  npm 安 装
RUN node -v
RUN npm -v

# 安 装  Playwright 依 赖 以 及 系 统 依 赖
#RUN apt-get install playwright chromium
RUN apt-get install -y libglib2.0-0 libnss3 libnspr4 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2  libatspi2.0-0 libx11-6 libxcomposite1 libxrandr2 \
     libxrender1 libxtst6 ca-certificates fonts-noto-cjk  libxdamage1 libxfixes3 libgbm1 libxkbcommon0 libpango-1.0-0 libcairo2 libasound2 \
      libx11-xcb1  libxcursor1  libgtk-3-0 libpangocairo-1.0-0 libcairo-gobject2  libgdk-pixbuf-2.0-0

ENV LANG C.UTF-8
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo "Asia/Shanghai" > /etc/timezone

COPY *.jar app.jar
CMD ["java", "-jar", "app.jar"]

如果是通过 docker-compose 构建的 docker镜像,提供一个 docker-compose.yml

pdftool:
    image: pdftool
    container_name: tool
    ports:
        - "8181:8181"
    volumes:
        - /data/logs/tool:/data/logs/tool

所以整个构建过程是如下,如果是 Jenkins 方式,只需要配置一个任务就是了,这里提供一个手动构建的过程

1、停止服务
docker stop tool
2、创建镜像
 docker build -t pdftool .
3、 运行应用
docker-compose -f docker-compose.yml up -d 

4、执行操作
curl http://localhost:8181/webapi/pdftool/getHtmlToPdf?url=https://baidu.cn&fileName=test.pdf 

5、查看日志
docker logs tool 

6、进入容器
docker exec -u root -it tool bash

可以在浏览器中执行,或者通过 Apifox 中操作;这里推荐用 post请求,因为网页链接中还有参数等,如果 get请求就会有问题

http://localhost:8181/webapi/pdftool/getHtmlToPdf?url=https://baidu.cn&fileName=test.pdf

如果将这个功能作为一个服务,其他应用调用,可以这么实现;可以同时输出到文件和输出流中


import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import org.apache.commons.io.FileUtils;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
 * pdf 工具
 * @author xiaofei
 * @date 2024/12/17 17:00
 */
public class PdfUtil {
 /**
  * 将网页生成 pdf
  * @auth xiaofei
  * @date 2024/12/20 11:39
  */
public static void generateUrlToPdf(String url, String fileName,OutputStream outputStream){
        //调用第三方服务转化
        Map<String, Object> params = new HashMap<>();
        params.put("url", url);
        params.put("fileName", fileName);
 
        OkHttpClient client = new OkHttpClient().newBuilder()
                .connectTimeout(10, TimeUnit.SECONDS)  // 设置连接超时时间为10秒
                .readTimeout(30, TimeUnit.SECONDS)     // 设置读取超时时间为30秒
                .writeTimeout(15, TimeUnit.SECONDS)    // 设置写入超时时间为15秒
                .build();
        okhttp3.MediaType mediaType = okhttp3.MediaType.parse("application/json");
        RequestBody body = RequestBody.create(mediaType, JSONObject.toJSONString(params));

        okhttp3.Request request = new okhttp3.Request.Builder()
                .url("https://localhost:8181/webapi/pdfTool/postHtmlToPdf")
                .method("POST", body)
                .addHeader("Content-Type", "application/json")
                .addHeader("Accept", "*/*")
                .addHeader("Connection", "keep-alive")
                .build();
        try (okhttp3.Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                throw new IOException("Unexpected code " + response);
            }
            ResponseBody responseBody = response.body();
            if (responseBody != null && outputStream != null) {
                try (InputStream inputStream = responseBody.byteStream()) {
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    byte[] buffer = new byte[1024];
                    int bytesRead;
                    while ((bytesRead = inputStream.read(buffer)) != -1) {
                        baos.write(buffer, 0, bytesRead);
                    }             
                    // 将内容写入到文件
                    try (FileOutputStream fos = new FileOutputStream(fileName)) {
                        fos.write(baos.toByteArray());
                    }
                    // 将内容写入到输出流
                    baos.writeTo(outputStream);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws IOException {
        System.out.println("Screenshot begin!");
        String url = "http://baidu.com";
        String outputPath = "baidu.pdf";
        PdfUtil.generateUrlToPdf(url, outputPath,null);
    }

结尾

至此整个功能就完成了,最大的问题就是环境问题导致 playwright没有正常安装,导致功能无法跑通;多试多折腾,总会找到办法,如果你在使用中也遇到一些问题仍然无法解决,就在评论区标注下,大家一起想办法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值