前言
生成 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没有正常安装,导致功能无法跑通;多试多折腾,总会找到办法,如果你在使用中也遇到一些问题仍然无法解决,就在评论区标注下,大家一起想办法



1万+

被折叠的 条评论
为什么被折叠?



