java使用pdfbox实现pdf拼接完整版

发布于:2025-07-25 ⋅ 阅读:(16) ⋅ 点赞:(0)

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文件拼接功能,支持多种拼接场景:

  1. 多页PDF拼接: 可将不同PDF文件的页面拼接到同一个新PDF中
  2. PDF页面的裁切与缩放: 支持裁切PDF页面指定区域和自动缩放适应目标尺寸
  3. 页面旋转: 支持0°、90°、180°、270°四种旋转角度
  4. 灵活布局: 可自定义每个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());
    }
  }


}

注意事项

  1. 所有坐标和尺寸单位均为毫米
  2. 页面索引从0开始计算
  3. 旋转是以片段中心为轴心进行的
  4. 确保输出路径的父目录存在
  5. 裁切参数仅在crop=true时生效

网站公告

今日签到

点亮在社区的每一天
去签到