1.导入maven依赖
<!-- https://mvnrepository.com/artifact/org.apache.pdfbox/pdfbox -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>3.0.5</version>
</dependency>
2.相关代码
1.实体类
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
/**
* 表示要拼接的大PDF中的一页
* 每页由多个小PDF组件组成
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PdfPage {
/** 生成pdf的页码 */
private int pageIndex;
/** 页面宽度 */
private float pageWidth;
/** 页面高度 */
private float pageHeight;
/** 小PDF组件列表 */
@Builder.Default
private List<PdfInfo> pdfInfos = new ArrayList<>();
/**
* 添加一个PDF组件
* @param pdfInfo PDF组件信息
*/
public void addPdfInfo(PdfInfo pdfInfo) {
if (this.pdfInfos == null) {
this.pdfInfos = new ArrayList<>();
}
this.pdfInfos.add(pdfInfo);
}
}
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.File;
/**
* PDF组件信息类,表示一个PDF页面的信息
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PdfInfo {
/** pdf文件 */
private File pdfFile;
/** 页码 */
private int pageIndex;
/** 中心坐标x */
private float centralXCoordinates;
/** 中心坐标y */
private float centralYCoordinates;
/** 长 */
private float width;
/** 宽 */
private float height;
/** 旋转角度 */
private int rotationAngle;
}
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
/**
* 表示要生成的完整PDF文档
* 由多个PDF页面组成
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PdfDocument {
/** 保存路径 */
private String outputPath;
/** 输出文件名*/
private String outputFileName;
/** 每页的信息 */
@Builder.Default
private List<PdfPage> pages = new ArrayList<>();
/**
* 添加一个页面
* @param page PDF页面
*/
public void addPage(PdfPage page) {
if (this.pages == null) {
this.pages = new ArrayList<>();
}
this.pages.add(page);
}
/**
* 获取完整的输出文件路径
* @return 输出文件路径
*/
public String getFullOutputPath() {
return outputPath + "/" + outputFileName;
}
}
2.关键代码
import org.apache.pdfbox.Loader;
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.form.PDFormXObject;
import org.apache.pdfbox.util.Matrix;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.whh.pdf.model.PdfDocument;
import org.whh.pdf.model.PdfInfo;
import org.whh.pdf.model.PdfPage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* PDF合并服务
* 负责将多个PDF紧密无缝拼接成一个PDF
*/
@Service
public class PdfMergeService {
private static final Logger logger = LoggerFactory.getLogger(PdfMergeService.class);
// 毫米转换为PDF点数的常量
private static final double MM_TO_POINTS = 2.8346457;
// 允许的旋转角度集合
private static final Set<Integer> ALLOWED_ROTATION_ANGLES = Set.of(0, 90, 180, 270);
/**
* 验证旋转角度
* @param angle 要验证的角度
* @throws IllegalArgumentException 如果角度不是允许的值
*/
private void validateRotationAngle(int angle) {
if (!ALLOWED_ROTATION_ANGLES.contains(angle)) {
throw new IllegalArgumentException(
String.format("不支持的旋转角度: %d。只允许以下角度: %s",
angle, ALLOWED_ROTATION_ANGLES)
);
}
}
/**
* 合并PDF
*
* @param pdfDocument PDF文档信息
* @return 是否成功
*/
public boolean mergePdf(PdfDocument pdfDocument) {
if (pdfDocument == null || pdfDocument.getPages() == null || pdfDocument.getPages().isEmpty()) {
logger.error("PDF文档信息为空或没有页面信息");
return false;
}
// 创建一个列表来存储所有打开的源文档
List<PDDocument> sourceDocuments = new ArrayList<>();
try (PDDocument document = new PDDocument()) {
// 处理每一页
for (PdfPage pdfPage : pdfDocument.getPages()) {
// 如果页面尺寸未设置,自动计算
if (pdfPage.getPageWidth() <= 0 || pdfPage.getPageHeight() <= 0) {
throw new RuntimeException("页面尺寸未设置或无效");
}
// 创建页面 - 使用精确的点单位计算
float pageWidthPoints = (float) (pdfPage.getPageWidth() * MM_TO_POINTS);
float pageHeightPoints = (float) (pdfPage.getPageHeight() * MM_TO_POINTS);
PDRectangle pageSize = new PDRectangle(pageWidthPoints, pageHeightPoints);
PDPage page = new PDPage(pageSize);
document.addPage(page);
logger.info("创建页面, 尺寸: {}x{} 毫米, {}x{} 点",
pdfPage.getPageWidth(), pdfPage.getPageHeight(),
pageWidthPoints, pageHeightPoints);
// 处理页面上的每个PDF组件
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
for (PdfInfo pdfInfo : pdfPage.getPdfInfos()) {
// 验证旋转角度
validateRotationAngle(pdfInfo.getRotationAngle());
importPdf(document, contentStream, pdfInfo, sourceDocuments, pageHeightPoints);
}
}
}
// 确保输出目录存在
File outputDir = new File(pdfDocument.getOutputPath());
if (!outputDir.exists()) {
outputDir.mkdirs();
}
// 保存文档
document.save(pdfDocument.getFullOutputPath());
logger.info("PDF合并成功,保存到: {}", pdfDocument.getFullOutputPath());
return true;
} catch (IllegalArgumentException e) {
logger.error("PDF合并参数错误: {}", e.getMessage());
return false;
} catch (IOException e) {
logger.error("合并PDF时发生错误", e);
return false;
} finally {
// 关闭所有源文档
for (PDDocument sourceDoc : sourceDocuments) {
try {
sourceDoc.close();
} catch (IOException e) {
logger.error("关闭源文档时发生错误", e);
}
}
}
}
/**
* 导入PDF页面,确保精确定位以实现紧密拼接
*
* @param document 目标文档
* @param contentStream 内容流
* @param pdfInfo PDF信息
* @param sourceDocuments 源文档列表,用于在方法外部统一关闭
* @param pageHeight 页面高度(用于Y坐标转换)
* @throws IOException 如果发生IO错误
*/
private void importPdf(PDDocument document, PDPageContentStream contentStream, PdfInfo pdfInfo,
List<PDDocument> sourceDocuments, float pageHeight) throws IOException {
PDDocument sourceDoc = Loader.loadPDF(pdfInfo.getPdfFile());
// 将源文档添加到列表中,以便在方法外部关闭
sourceDocuments.add(sourceDoc);
if (pdfInfo.getPageIndex() >= sourceDoc.getNumberOfPages()) {
logger.warn("PDF文件 {} 没有第 {} 页", pdfInfo.getPdfFile().getName(), pdfInfo.getPageIndex() + 1);
return;
}
// 获取源页面
PDPage sourcePage = sourceDoc.getPage(pdfInfo.getPageIndex());
// 创建表单XObject
PDFormXObject form = new PDFormXObject(document);
form.setBBox(sourcePage.getBBox());
form.setResources(sourcePage.getResources());
form.setFormType(1);
// 复制页面内容到表单
try (OutputStream formOutput = form.getStream().createOutputStream()) {
try (InputStream contents = sourcePage.getContents()) {
if (contents != null) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = contents.read(buffer)) != -1) {
formOutput.write(buffer, 0, bytesRead);
}
}
}
}
// 获取源PDF的尺寸(毫米)和中心点坐标
float sourceWidthMM = pdfInfo.getWidth(); // 原始宽度(毫米)
float sourceHeightMM = pdfInfo.getHeight(); // 原始高度(毫米)
float centerXMM = pdfInfo.getCentralXCoordinates(); // 中心点X坐标(毫米)
float centerYMM = pdfInfo.getCentralYCoordinates(); // 中心点Y坐标(毫米)
int rotationAngle = pdfInfo.getRotationAngle(); // 旋转角度
// 转换为点单位
float sourceWidthPoints = sourceWidthMM * (float)MM_TO_POINTS;
float sourceHeightPoints = sourceHeightMM * (float)MM_TO_POINTS;
float centerXPoints = centerXMM * (float)MM_TO_POINTS;
float centerYPoints = pageHeight - centerYMM * (float)MM_TO_POINTS; // 转换Y坐标(PDF坐标系原点在左下角)
// 计算源文档的边界框尺寸
float sourceBBoxWidth = form.getBBox().getWidth();
float sourceBBoxHeight = form.getBBox().getHeight();
// 保存图形状态
contentStream.saveGraphicsState();
// 移动到中心点
contentStream.transform(Matrix.getTranslateInstance(centerXPoints, centerYPoints));
// 应用旋转
if (rotationAngle != 0) {
contentStream.transform(Matrix.getRotateInstance(Math.toRadians(rotationAngle), 0, 0));
}
// 计算缩放比例
float scaleX = sourceWidthPoints / sourceBBoxWidth;
float scaleY = sourceHeightPoints / sourceBBoxHeight;
// 计算偏移量 - 基于原始尺寸的一半
float offsetX = -sourceWidthPoints / 2;
float offsetY = -sourceHeightPoints / 2;
// 应用偏移
contentStream.transform(Matrix.getTranslateInstance(offsetX, offsetY));
// 应用缩放
contentStream.transform(Matrix.getScaleInstance(scaleX, scaleY));
// 绘制表单
contentStream.drawForm(form);
contentStream.restoreGraphicsState();
// 记录日志
logger.debug("PDF部分放置: 文件={}, 页面={}, 中心点=({}, {})mm, 旋转角度={}°, 宽度={}mm, 高度={}mm",
pdfInfo.getPdfFile().getName(), pdfInfo.getPageIndex() + 1,
centerXMM, centerYMM, rotationAngle, sourceWidthMM, sourceHeightMM);
}
}
3.示例代码
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.whh.pdf.model.PdfDocument;
import org.whh.pdf.model.PdfInfo;
import org.whh.pdf.model.PdfPage;
import org.whh.pdf.service.PdfMergeService;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* PDF测试应用
* 用于测试PDF拼接功能
*/
@SpringBootApplication
public class PdfTestApp {
public static void main(String[] args) {
SpringApplication.run(PdfTestApp.class, args);
}
@Bean
public CommandLineRunner testPdfMerge(PdfMergeService pdfMergeService) {
return args -> {
// 创建测试PDF
PdfDocument pdfDocument = createTestPdfDocument();
// 合并PDF
boolean success = pdfMergeService.mergePdf(pdfDocument);
if (success) {
System.out.println("PDF合并成功,保存到: " + pdfDocument.getFullOutputPath());
} else {
System.err.println("PDF合并失败");
}
};
}
/**
* 创建测试PDF文档
* @return PDF文档对象
*/
private PdfDocument createTestPdfDocument() {
// 创建PDF文档
PdfDocument pdfDocument = new PdfDocument();
pdfDocument.setOutputPath("D:/output");
pdfDocument.setOutputFileName("merged.pdf");
// 创建页面 - 不再手动设置页面尺寸,由服务自动计算
PdfPage pdfPage = new PdfPage();
pdfPage.setPageIndex(0);
pdfPage.setPageHeight(90);
pdfPage.setPageWidth(54);
// 创建PDF信息列表
List<PdfInfo> pdfInfos = new ArrayList<>();
// 上面两个横放矩形 (宽90, 高54)
PdfInfo pdfInfo1 = new PdfInfo();
pdfInfo1.setPdfFile(new File("D:\\work\\demo\\pdf\\SL2504240281-08785-名片-(90×54)5盒双名片覆膜1K1M.pdf"));
pdfInfo1.setPageIndex(0);
pdfInfo1.setCentralXCoordinates(27);
pdfInfo1.setCentralYCoordinates(45);
pdfInfo1.setWidth(90); // 原始宽度90mm
pdfInfo1.setHeight(54); // 原始高度54mm
pdfInfo1.setRotationAngle(90);
pdfInfos.add(pdfInfo1);
// 设置页面的PDF信息列表
pdfPage.setPdfInfos(pdfInfos);
// 将页面添加到文档
List<PdfPage> pages = new ArrayList<>();
pages.add(pdfPage);
pdfDocument.setPages(pages);
return pdfDocument;
}
}