PDF拼接功能模块
环境要求
- Java版本: JDK 17或更高版本
- 依赖库:
- Apache PDFBox 3.0.5
- Spring Boot 3.x
- Lombok
Maven依赖
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
功能概述
本模块提供PDF文件拼接功能,支持多种拼接场景:
- 多页PDF拼接: 可将不同PDF文件的页面拼接到同一个新PDF中
- PDF页面的裁切与缩放: 支持裁切PDF页面指定区域和自动缩放适应目标尺寸
- 页面旋转: 支持0°、90°、180°、270°四种旋转角度
- 灵活布局: 可自定义每个PDF片段在目标页面中的位置和大小
代码结构
com.whh.pdf/
├── PdfComposer.java # 主入口类,提供静态方法执行PDF拼接
├── controller/
│ └── PdfComposeController.java # REST API控制器
├── model/
│ ├── PdfComposeRequest.java # 拼接请求模型
│ ├── PdfPage.java # 页面模型
│ └── PdfFragment.java # PDF片段模型
├── service/
│ └── PdfComposeService.java # 核心服务实现
└── util/
└── PdfUtils.java # 工具类
坐标系统说明
- 页面坐标系的原点(0,0)位于左上角
- 所有尺寸和坐标使用毫米作为单位
- X轴向右为正,Y轴向下为正
- 旋转以片段的中心点为轴心进行,旋转后片段在页面上占据的区域不变
使用方法
方法1: 通过REST API调用
发送POST请求到/pdf/compose
接口,请求体为JSON格式的PdfComposeRequest
对象。
方法2: 通过Java代码调用
// 示例1: 使用PdfComposeRequest对象
PdfComposeRequest request = new PdfComposeRequest();
request.setOutputPath("D:/output.pdf");
// ... 配置页面和片段
String outputPath = PdfComposer.compose(request);
// 示例2: 水平拼接两页
String output = PdfComposer.composeTwoHorizontal(
"D:/output.pdf",
"D:/input.pdf", 0, // 左侧PDF文件和页码
"D:/input.pdf", 1 // 右侧PDF文件和页码
);
// 示例3: 垂直拼接两页
String output = PdfComposer.composeTwoVertical(
"D:/output.pdf",
"D:/input.pdf", 0, // 上方PDF文件和页码
"D:/input.pdf", 1 // 下方PDF文件和页码
);
API说明
1. PDF片段 (PdfFragment)
表示要拼接的单个PDF页面片段。
字段 | 类型 | 描述 |
---|---|---|
pdfFilePath | String | PDF文件路径 |
pageIndex | int | PDF页面索引(从0开始) |
x | float | X坐标(毫米) |
y | float | Y坐标(毫米) |
width | float | 宽度(毫米) |
height | float | 高度(毫米) |
rotation | int | 旋转角度(0/90/180/270) |
crop | boolean | 是否裁切 |
cropTop | float | 顶部裁切(毫米) |
cropBottom | float | 底部裁切(毫米) |
cropLeft | float | 左侧裁切(毫米) |
cropRight | float | 右侧裁切(毫米) |
2. PDF页面 (PdfPage)
表示拼接后的单个PDF页面。
字段 | 类型 | 描述 |
---|---|---|
width | float | 页面宽度(毫米) |
height | float | 页面高度(毫米) |
fragments | List | PDF片段列表 |
3. 拼接请求 (PdfComposeRequest)
表示一个完整的PDF拼接请求。
字段 | 类型 | 描述 |
---|---|---|
outputPath | String | 输出PDF路径 |
pages | List | PDF页面列表 |
请求示例
示例1: 水平拼接两页(左右并排)
{
"outputPath": "D:/file/four_page_layout.pdf",
"pages": [
{
"width": 420,
"height": 570,
"fragments": [
{
"pdfFilePath": "D:/file/SL2507060444-06214.pdf",
"pageIndex": 6,
"x": 0,
"y": 0,
"width": 210,
"height": 285,
"rotation": 0
},
{
"pdfFilePath": "D:/file/SL2507060444-06214.pdf",
"pageIndex": 7,
"x": 210,
"y": 0,
"width": 210,
"height": 285,
"rotation": 0
},
{
"pdfFilePath": "D:/file/SL2507060444-06214.pdf",
"pageIndex": 8,
"x": 0,
"y": 285,
"width": 210,
"height": 285,
"rotation": 0
},
{
"pdfFilePath": "D:/file/SL2507060444-06214.pdf",
"pageIndex": 9,
"x": 210,
"y": 285,
"width": 210,
"height": 285,
"rotation": 0
}
]
},
{
"width": 420,
"height": 570,
"fragments": [
{
"pdfFilePath": "D:/file/SL2507060444-06214.pdf",
"pageIndex": 10,
"x": 0,
"y": 0,
"width": 210,
"height": 285,
"rotation": 0
},
{
"pdfFilePath": "D:/file/SL2507060444-06214.pdf",
"pageIndex": 11,
"x": 210,
"y": 0,
"width": 210,
"height": 285,
"rotation": 180
},
{
"pdfFilePath": "D:/file/SL2507060444-06214.pdf",
"pageIndex": 12,
"x": 0,
"y": 285,
"width": 210,
"height": 285,
"rotation": 180
},
{
"pdfFilePath": "D:/file/SL2507060444-06214.pdf",
"pageIndex": 13,
"x": 210,
"y": 285,
"width": 210,
"height": 285,
"rotation": 180
}
]
}
]
}
PdfComposeRequest
package com.whh.pdf.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
/**
* PDF拼接请求模型类
* 包含输出文件路径和所有页面信息
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PdfComposeRequest {
private String outputPath; // PDF输出路径(包含文件名)
@Builder.Default
private List<PdfPage> pages = new ArrayList<>(); // PDF页面列表
/**
* 简便构造方法(仅设置输出路径)
*/
public PdfComposeRequest(String outputPath) {
this.outputPath = outputPath;
this.pages = new ArrayList<>();
}
/**
* 添加一个PDF页面
*
* @param page PDF页面
*/
public void addPage(PdfPage page) {
if (page != null) {
this.pages.add(page);
}
}
/**
* 验证输出路径是否有效
* 检查路径格式是否有效、父目录是否存在、文件是否已存在且是否有权限写入
*
* @return 路径验证结果,null表示验证通过,否则返回错误信息
*/
public String validateOutputPath() {
if (outputPath == null || outputPath.trim().isEmpty()) {
return "输出路径不能为空";
}
try {
// 检查路径格式是否有效
Path path = Paths.get(outputPath);
// 检查父目录是否存在
Path parent = path.getParent();
if (parent != null && !Files.exists(parent)) {
return "输出目录不存在: " + parent;
}
// 检查文件名是否以.pdf结尾
if (!outputPath.toLowerCase().endsWith(".pdf")) {
return "输出文件必须以.pdf结尾";
}
// 检查是否有权限写入
if (Files.exists(path)) {
if (!Files.isWritable(path)) {
return "无法写入输出文件: " + path;
}
// 文件已存在,检查是否可以删除
File file = path.toFile();
if (file.exists() && !file.canWrite()) {
return "无法覆盖已存在的文件: " + path;
}
} else {
// 文件不存在,检查是否可以创建
try {
// 尝试创建文件来测试权限
if (!path.getParent().toFile().canWrite()) {
return "无权限在目录中创建文件: " + path.getParent();
}
} catch (Exception e) {
return "无法创建输出文件: " + e.getMessage();
}
}
return null; // 验证通过
} catch (InvalidPathException e) {
return "无效的文件路径: " + e.getMessage();
} catch (Exception e) {
return "验证文件路径时发生错误: " + e.getMessage();
}
}
/**
* 验证请求是否有效
*
* @return 验证结果,null表示验证通过,否则返回错误信息
*/
public String validate() {
// 验证输出路径
String outputPathError = validateOutputPath();
if (outputPathError != null) {
return outputPathError;
}
// 验证页面列表
if (pages == null || pages.isEmpty()) {
return "页面列表不能为空";
}
// 验证每个页面
for (int i = 0; i < pages.size(); i++) {
PdfPage page = pages.get(i);
if (page == null) {
return "第 " + (i + 1) + " 页不能为null";
}
if (page.getWidth() <= 0 || page.getHeight() <= 0) {
return "第 " + (i + 1) + " 页尺寸无效: " + page.getWidth() + "x" + page.getHeight();
}
if (page.getFragments() == null || page.getFragments().isEmpty()) {
return "第 " + (i + 1) + " 页没有PDF片段";
}
// 验证每个PDF片段
for (int j = 0; j < page.getFragments().size(); j++) {
PdfFragment fragment = page.getFragments().get(j);
if (fragment == null) {
return "第 " + (i + 1) + " 页的第 " + (j + 1) + " 个片段不能为null";
}
if (fragment.getPdfFilePath() == null || fragment.getPdfFilePath().trim().isEmpty()) {
return "第 " + (i + 1) + " 页的第 " + (j + 1) + " 个片段的PDF文件路径不能为空";
}
if (!new File(fragment.getPdfFilePath()).exists()) {
return "第 " + (i + 1) + " 页的第 " + (j + 1) + " 个片段的PDF文件不存在: " + fragment.getPdfFilePath();
}
if (fragment.getPageIndex() < 0) {
return "第 " + (i + 1) + " 页的第 " + (j + 1) + " 个片段的页码不能为负数: " + fragment.getPageIndex();
}
if (fragment.getWidth() <= 0 || fragment.getHeight() <= 0) {
return "第 " + (i + 1) + " 页的第 " + (j + 1) + " 个片段的尺寸无效: " + fragment.getWidth() + "x" + fragment.getHeight();
}
}
// 验证片段是否都在页面范围内
if (!page.validateFragmentsInBounds()) {
return "第 " + (i + 1) + " 页中有片段超出页面范围";
}
}
return null; // 验证通过
}
}
PdfFragment
package com.whh.pdf.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* PDF片段模型,表示要拼接的单个PDF页面片段
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PdfFragment {
private String pdfFilePath; // PDF文件路径
private int pageIndex; // PDF页面索引(从0开始)
private float x; // X坐标(毫米)
private float y; // Y坐标(毫米)
private float width; // 宽度(毫米)
private float height; // 高度(毫米)
private int rotation; // 旋转角度(0, 90, 180, 270)
// 裁切信息
@Builder.Default
private boolean crop = false; // 是否裁切
@Builder.Default
private float cropTop = 0; // 顶部裁切(毫米)
@Builder.Default
private float cropBottom = 0; // 底部裁切(毫米)
@Builder.Default
private float cropLeft = 0; // 左侧裁切(毫米)
@Builder.Default
private float cropRight = 0; // 右侧裁切(毫米)
/**
* 简便构造方法(不带旋转)
*/
public PdfFragment(String pdfFilePath, int pageIndex, float x, float y, float width, float height) {
this.pdfFilePath = pdfFilePath;
this.pageIndex = pageIndex;
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.rotation = 0;
}
/**
* 设置旋转角度
*
* @param rotation 旋转角度(0, 90, 180, 270)
*/
public void setRotation(int rotation) {
// 验证旋转角度为0, 90, 180, 270
if (rotation != 0 && rotation != 90 && rotation != 180 && rotation != 270) {
throw new IllegalArgumentException("旋转角度必须是0, 90, 180或270");
}
this.rotation = rotation;
}
/**
* 设置均匀裁切值,会自动计算上下左右裁切值
*
* @param widthDiff 宽度差值(原宽度 - 目标宽度)
* @param heightDiff 高度差值(原高度 - 目标高度)
*/
public void setEvenCrop(float widthDiff, float heightDiff) {
if (widthDiff < 0 || heightDiff < 0) {
throw new IllegalArgumentException("裁切差值必须大于或等于0");
}
this.cropLeft = widthDiff / 2;
this.cropRight = widthDiff / 2;
this.cropTop = heightDiff / 2;
this.cropBottom = heightDiff / 2;
this.crop = true;
}
}
PdfPage
package com.whh.pdf.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
/**
* PDF页面模型,表示拼接后的单个PDF页面
* 包含页面宽高和所有PDF片段
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PdfPage {
private float width; // 页面宽度(毫米)
private float height; // 页面高度(毫米)
@Builder.Default
private List<PdfFragment> fragments = new ArrayList<>(); // PDF片段列表
/**
* 简便构造方法(带宽高)
*/
public PdfPage(float width, float height) {
this.width = width;
this.height = height;
this.fragments = new ArrayList<>();
}
/**
* 添加一个PDF片段
*
* @param fragment PDF片段
*/
public void addFragment(PdfFragment fragment) {
if (fragment != null) {
this.fragments.add(fragment);
}
}
/**
* 验证片段是否在页面范围内
*
* @return 是否有片段超出页面范围
*/
public boolean validateFragmentsInBounds() {
for (PdfFragment fragment : fragments) {
// 检查片段是否超出页面范围
if (fragment.getX() < 0 || fragment.getY() < 0 || fragment.getX() + fragment.getWidth() > width || fragment.getY() + fragment.getHeight() > height) {
return false;
}
}
return true;
}
}
PdfComposeService
package com.whh.pdf.service;
import com.whh.pdf.model.PdfComposeRequest;
import com.whh.pdf.model.PdfFragment;
import com.whh.pdf.model.PdfPage;
import com.whh.pdf.util.PdfUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.util.Matrix;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
/**
* PDF拼接服务
* 实现PDF页面拼接、裁切和缩放功能
*/
@Slf4j
public class PdfComposeService {
/**
* 执行PDF拼接操作
*
* @param request PDF拼接请求
* @return 输出的PDF文件路径
* @throws Exception 如果拼接过程中发生错误
*/
public String composePdf(PdfComposeRequest request) throws Exception {
// 验证请求
String validationResult = request.validate();
if (validationResult != null) {
throw new IllegalArgumentException(validationResult);
}
// 确保输出路径的父目录存在
Path outputPath = Paths.get(request.getOutputPath());
Path parentDir = outputPath.getParent();
if (parentDir != null && !Files.exists(parentDir)) {
Files.createDirectories(parentDir);
}
// 创建新的输出PDF文档
try (PDDocument outputDocument = new PDDocument()) {
// 缓存已打开的源PDF文档,避免重复打开
Map<String, PDDocument> sourceDocuments = new HashMap<>();
try {
// 处理每个页面
for (PdfPage page : request.getPages()) {
processPdfPage(page, outputDocument, sourceDocuments);
}
// 保存结果
outputDocument.save(request.getOutputPath());
log.info("PDF拼接完成,输出文件: {}, 总页数: {}", request.getOutputPath(), outputDocument.getNumberOfPages());
return request.getOutputPath();
} finally {
// 关闭所有打开的源文档
for (PDDocument doc : sourceDocuments.values()) {
try {
doc.close();
} catch (Exception e) {
log.warn("关闭源文档时发生错误", e);
}
}
}
}
}
/**
* 处理单个PDF页面
*/
private void processPdfPage(PdfPage page, PDDocument outputDocument, Map<String, PDDocument> sourceDocuments) throws IOException {
// 创建新页面
PDRectangle pageSize = PdfUtils.createRectangle(page.getWidth(), page.getHeight());
PDPage outputPage = new PDPage(pageSize);
outputDocument.addPage(outputPage);
// 创建内容流,用于向页面添加内容
try (PDPageContentStream contentStream = new PDPageContentStream(outputDocument, outputPage, AppendMode.APPEND, true)) {
// 处理页面上的每个片段
for (PdfFragment fragment : page.getFragments()) {
processFragment(fragment, contentStream, outputDocument, sourceDocuments);
}
}
}
/**
* 处理PDF片段,将其绘制到页面上
*/
private void processFragment(PdfFragment fragment, PDPageContentStream contentStream, PDDocument outputDocument, Map<String, PDDocument> sourceDocuments) throws IOException {
// 获取或加载源文档
PDDocument sourceDocument = getSourceDocument(fragment.getPdfFilePath(), sourceDocuments);
// 验证页码
if (fragment.getPageIndex() < 0 || fragment.getPageIndex() >= sourceDocument.getNumberOfPages()) {
throw new IllegalArgumentException(String.format("页码无效: %d, PDF文件 %s 的总页数: %d", fragment.getPageIndex(), fragment.getPdfFilePath(), sourceDocument.getNumberOfPages()));
}
// 获取源页面的尺寸
float[] srcSize = PdfUtils.getPageSizeMm(sourceDocument, fragment.getPageIndex());
float srcWidth = srcSize[0];
float srcHeight = srcSize[1];
// 从源文档导入页面
LayerUtility layerUtility = new LayerUtility(outputDocument);
PDFormXObject pageForm = layerUtility.importPageAsForm(sourceDocument, fragment.getPageIndex());
// 保存图形状态
contentStream.saveGraphicsState();
// 创建变换矩阵
Matrix matrix = new Matrix();
// 移动到目标位置
float x = PdfUtils.mmToPoints(fragment.getX());
float y = PdfUtils.mmToPoints(fragment.getY());
// 计算缩放因子
float targetWidth = PdfUtils.mmToPoints(fragment.getWidth());
float targetHeight = PdfUtils.mmToPoints(fragment.getHeight());
float srcWidthPt = PdfUtils.mmToPoints(srcWidth);
float srcHeightPt = PdfUtils.mmToPoints(srcHeight);
// 计算裁切和缩放
float srcX = 0;
float srcY = 0;
float srcWidthAfterCrop = srcWidth;
float srcHeightAfterCrop = srcHeight;
// 如果需要裁切
if (fragment.isCrop()) {
// 应用裁切
srcX = PdfUtils.mmToPoints(fragment.getCropLeft());
srcY = PdfUtils.mmToPoints(fragment.getCropBottom());
srcWidthAfterCrop = srcWidth - fragment.getCropLeft() - fragment.getCropRight();
srcHeightAfterCrop = srcHeight - fragment.getCropTop() - fragment.getCropBottom();
// 重新计算源尺寸(点)
srcWidthPt = PdfUtils.mmToPoints(srcWidthAfterCrop);
srcHeightPt = PdfUtils.mmToPoints(srcHeightAfterCrop);
log.info("应用裁切: 左={}, 右={}, 上={}, 下={}, 裁切后尺寸: {}x{} mm", fragment.getCropLeft(), fragment.getCropRight(), fragment.getCropTop(), fragment.getCropBottom(), srcWidthAfterCrop, srcHeightAfterCrop);
}
// 计算缩放因子
float scaleX = targetWidth / srcWidthPt;
float scaleY = targetHeight / srcHeightPt;
// 设置变换矩阵
// 1. 移动到目标位置(左下角)
matrix.translate(x, y);
// 2. 考虑旋转(绕左下角旋转)
if (fragment.getRotation() != 0) {
// 先移到旋转中心点
matrix.translate(targetWidth / 2, targetHeight / 2);
// 旋转
matrix.rotate(Math.toRadians(fragment.getRotation()));
// 移回
if (fragment.getRotation() == 90 || fragment.getRotation() == 270) {
// 对于90度和270度旋转,交换宽高
matrix.translate(-targetHeight / 2, -targetWidth / 2);
} else {
matrix.translate(-targetWidth / 2, -targetHeight / 2);
}
}
// 3. 缩放
matrix.scale(scaleX, scaleY);
// 应用变换
contentStream.transform(matrix);
// 如果需要裁切,绘制部分页面
if (fragment.isCrop()) {
// 计算裁切区域
float cropX = srcX;
float cropY = srcY;
float cropWidth = srcWidthPt;
float cropHeight = srcHeightPt;
// 应用裁切矩形
contentStream.addRect(cropX, cropY, cropWidth, cropHeight);
contentStream.clip();
}
// 绘制页面表单
contentStream.drawForm(pageForm);
// 恢复图形状态
contentStream.restoreGraphicsState();
// 记录日志
log.info("已处理片段: PDF={}, 页码={}, 位置=({}, {}), 尺寸={}x{}, 旋转={}°", fragment.getPdfFilePath(), fragment.getPageIndex(), fragment.getX(), fragment.getY(), fragment.getWidth(), fragment.getHeight(), fragment.getRotation());
}
/**
* 获取源PDF文档,如果已经加载则从缓存获取,否则加载新的
*/
private PDDocument getSourceDocument(String pdfFilePath, Map<String, PDDocument> sourceDocuments) throws IOException {
// 如果文档已加载,则从缓存获取
if (sourceDocuments.containsKey(pdfFilePath)) {
return sourceDocuments.get(pdfFilePath);
}
// 加载新文档
File pdfFile = new File(pdfFilePath);
if (!pdfFile.exists()) {
throw new IllegalArgumentException("PDF文件不存在: " + pdfFilePath);
}
PDDocument document = Loader.loadPDF(pdfFile);
sourceDocuments.put(pdfFilePath, document);
log.info("加载PDF文件: {}, 页数: {}", pdfFilePath, document.getNumberOfPages());
return document;
}
}
PdfUtils
package com.whh.pdf.util;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import java.io.File;
import java.io.IOException;
/**
* PDF工具类,提供PDF相关的静态实用方法
*/
public class PdfUtils {
// 毫米转换为PDF点数的常量
public static final double MM_TO_POINTS = 2.8346457;
/**
* 将毫米单位转换为PDF点单位
*
* @param mm 毫米值
* @return 点值
*/
public static float mmToPoints(float mm) {
return (float) (mm * MM_TO_POINTS);
}
/**
* 将PDF点单位转换为毫米单位
*
* @param points 点值
* @return 毫米值
*/
public static float pointsToMm(float points) {
return (float) (points / MM_TO_POINTS);
}
/**
* 获取PDF页面的尺寸(毫米)
*
* @param document PDF文档
* @param pageIndex 页面索引
* @return 包含宽度和高度的数组 [width, height],单位毫米
* @throws IOException 如果无法读取页面
*/
public static float[] getPageSizeMm(PDDocument document, int pageIndex) throws IOException {
PDPage page = document.getPage(pageIndex);
PDRectangle mediaBox = page.getMediaBox();
float width = pointsToMm(mediaBox.getWidth());
float height = pointsToMm(mediaBox.getHeight());
return new float[]{width, height};
}
/**
* 获取PDF文档的页数
*
* @param pdfPath PDF文件路径
* @return 页数
* @throws IOException 如果无法读取文件
*/
public static int getPdfPageCount(String pdfPath) throws IOException {
try (PDDocument document = Loader.loadPDF(new File(pdfPath))) {
return document.getNumberOfPages();
}
}
/**
* 从PDF页面导入为Form XObject
*
* @param sourceDocument 源PDF文档
* @param targetDocument 目标PDF文档
* @param pageIndex 页面索引
* @return 导入的Form XObject
* @throws IOException 如果导入失败
*/
public static PDFormXObject importPageAsForm(PDDocument sourceDocument, PDDocument targetDocument, int pageIndex) throws IOException {
LayerUtility layerUtility = new LayerUtility(targetDocument);
PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, pageIndex);
return form;
}
/**
* 创建一个新的PDRectangle(毫米单位)
*
* @param widthMm 宽度(毫米)
* @param heightMm 高度(毫米)
* @return 创建的PDRectangle
*/
public static PDRectangle createRectangle(float widthMm, float heightMm) {
float widthPt = mmToPoints(widthMm);
float heightPt = mmToPoints(heightMm);
return new PDRectangle(widthPt, heightPt);
}
/**
* 计算目标尺寸的PDRectangle,根据源尺寸和目标尺寸计算裁切或缩放
*
* @param srcWidth 源宽度(毫米)
* @param srcHeight 源高度(毫米)
* @param targetWidth 目标宽度(毫米)
* @param targetHeight 目标高度(毫米)
* @param isCrop 是否裁切
* @param cropTop 顶部裁切(毫米)
* @param cropBottom 底部裁切(毫米)
* @param cropLeft 左侧裁切(毫米)
* @param cropRight 右侧裁切(毫米)
* @return 计算后的目标PDRectangle和源矩形在目标矩形中的位置信息 [targetRect, srcX, srcY, srcWidth, srcHeight]
*/
public static Object[] calculateTargetRectangle(float srcWidth, float srcHeight, float targetWidth, float targetHeight, boolean isCrop, float cropTop, float cropBottom, float cropLeft, float cropRight) {
PDRectangle targetRect = createRectangle(targetWidth, targetHeight);
float srcX = 0;
float srcY = 0;
// 如果需要裁切,则计算裁切后的源尺寸和位置
if (isCrop) {
srcX = mmToPoints(cropLeft);
srcY = mmToPoints(cropTop);
srcWidth -= (cropLeft + cropRight);
srcHeight -= (cropTop + cropBottom);
}
// 无论是否裁切,都可能需要缩放
float scaleX = targetWidth / srcWidth;
float scaleY = targetHeight / srcHeight;
// 保持纵横比不变,使用较小的缩放比例
float scale = Math.min(scaleX, scaleY);
// 计算缩放后的源尺寸(点)
float scaledSrcWidth = mmToPoints(srcWidth) * scale;
float scaledSrcHeight = mmToPoints(srcHeight) * scale;
// 计算在目标中的居中位置(点)
float centeredX = (mmToPoints(targetWidth) - scaledSrcWidth) / 2;
float centeredY = (mmToPoints(targetHeight) - scaledSrcHeight) / 2;
return new Object[]{targetRect, srcX, srcY, scaledSrcWidth, scaledSrcHeight, centeredX, centeredY};
}
}
PdfComposeController
package com.whh.pdf.controller;
import com.whh.base.domain.AjaxResult;
import com.whh.pdf.model.PdfComposeRequest;
import com.whh.pdf.service.PdfComposeService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* PDF拼版控制器
*/
@RestController
@RequestMapping("/pdf")
@Slf4j
public class PdfComposeController {
@Resource
private PdfComposeService pdfComposeService;
/**
* 执行PDF拼接操作
*
* @param request PDF拼接请求
* @return 拼接结果
*/
@PostMapping("/compose")
public AjaxResult composePdf(@RequestBody PdfComposeRequest request) {
try {
log.info("接收到PDF拼接请求: {}", request);
// 验证请求
String validationResult = request.validate();
if (validationResult != null) {
return AjaxResult.error(validationResult);
}
// 执行拼接
String outputPath = pdfComposeService.composePdf(request);
// 返回结果
return AjaxResult.success("PDF拼接成功", outputPath);
} catch (Exception e) {
log.error("PDF拼接失败", e);
return AjaxResult.error("PDF拼接失败: " + e.getMessage());
}
}
}
注意事项
- 所有坐标和尺寸单位均为毫米
- 页面索引从0开始计算
- 旋转是以片段中心为轴心进行的
- 确保输出路径的父目录存在
- 裁切参数仅在
crop=true
时生效