java使用pdfbox实现PDF页面拼接

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;
    }
}


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值