SpringBoot集成PDFBox實現(xiàn)電子簽章的代碼詳解
Apache PDFBox 是一個開源的 Java 庫,用于處理 PDF 文檔。它提供了一系列強大的功能,包括創(chuàng)建、渲染、拆分、合并、加密、解密 PDF 文件,以及從 PDF 中提取文本和元數(shù)據(jù)等。PDFBox 支持 PDF 1.7 標(biāo)準(zhǔn),并且兼容大多數(shù)現(xiàn)代 PDF 格式和特性。
1、使用 Maven 集成 PDFBox
在 pom.xml 文件中引入依賴
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.24</version> <!-- 請檢查最新的版本 -->
</dependency>2、編寫工具類
package cn.iocoder.yudao.module.contract.service.content;
import com.fasterxml.jackson.databind.ObjectMapper;
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.image.PDImageXObject;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.springframework.http.ResponseEntity;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
public class PDFBoxUtil {
/**
* 加載 PDF 文檔
*/
public static PDDocument loadPdf(byte[] input) throws IOException {
return PDDocument.load(input);
}
/**
* 添加印章到 PDF 文檔中
*
* @param document PDF 文檔對象
* @param imageByteArray 印章圖像的二進(jìn)制數(shù)據(jù)
* @param x 橫坐標(biāo)
* @param y 縱坐標(biāo)
* @param h 高度
* @param pageIdx 頁碼
* @throws IOException 異常
*/
public static void addStampToPdf(PDDocument document, byte[] imageByteArray, int x, int y, int h, int pageIdx) throws IOException {
// 加載簽章圖像
PDImageXObject pdImage = PDImageXObject.createFromByteArray(document, imageByteArray, "簽章");
// 獲取 PDF 文檔的第一個頁面
PDPage page = document.getPage(pageIdx);
// 計算簽章圖像的尺寸
float desiredHeight = h; // 目標(biāo)高度
float scale = desiredHeight / pdImage.getHeight();
// 創(chuàng)建一個內(nèi)容流以添加簽章
try (PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
// 在 PDF 頁面上繪制簽章圖像
contentStream.drawImage(pdImage, x, y, pdImage.getWidth() * scale, pdImage.getHeight() * scale);
}
// 可選:也可以向 PDF 添加一個簽名字段
// addSignatureField(document);
}
/**
* 將 BufferedImage 轉(zhuǎn)換為字節(jié)數(shù)組
*
* @param image 要轉(zhuǎn)換的圖像
* @return 字節(jié)數(shù)組
*/
private static byte[] imageToBytes(BufferedImage image) {
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
ImageIO.write(image, "png", os);
return os.toByteArray();
} catch (IOException e) {
throw new RuntimeException("Failed to convert image to bytes", e);
}
}
/**
* 裁剪圖像
*
* @param image 要裁剪的圖像
* @param page PDF 頁面
* @param x 開始裁剪的橫坐標(biāo)
* @param y 開始裁剪的縱坐標(biāo)
* @param w 需要裁剪的寬度
* @param h 需要裁剪的高度
* @return 裁剪后的圖片
*/
private static BufferedImage cropImage(BufferedImage image, PDPage page, int x, int y, int w, int h) {
PDRectangle mediaBox = PDRectangle.A4; // 使用默認(rèn)的 A4 大小
// 將 PDF 單位轉(zhuǎn)換為圖像坐標(biāo)
int width = (int) (mediaBox.getWidth() * (image.getWidth() / page.getMediaBox().getWidth()));
int height = (int) (mediaBox.getHeight() * (image.getHeight() / page.getMediaBox().getHeight()));
// 裁剪圖像
return image.getSubimage(x, y, width - w, height - h);
}
/**
* 將 PDF 轉(zhuǎn)換為多個圖片
*
* @param pdfBytes PDF 二進(jìn)制數(shù)據(jù)
* @param dpi DPI 值
* @return 裁剪后的圖片列表
* @throws IOException 異常
*/
public static List<byte[]> convertPdfToImages(byte[] pdfBytes, int numberOfPages, int dpi, int x, int y, int w, int h) throws IOException {
List<byte[]> croppedImages = new ArrayList<>();
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(pdfBytes))) {
PDFRenderer renderer = new PDFRenderer(document);
if (numberOfPages == 0) {
numberOfPages = document.getNumberOfPages();
}
for (int i = 0; i < numberOfPages; i++) {
// 渲染頁面
BufferedImage image = renderer.renderImageWithDPI(i, dpi); // 300 DPI
// 裁剪圖像
BufferedImage croppedImage = cropImage(image, document.getPage(i), x, y, w, h);
byte[] croppedImageBytes = imageToBytes(croppedImage);
croppedImages.add(croppedImageBytes);
}
}
return croppedImages;
}
/**
* 將 PDF 轉(zhuǎn)換為 Base64 編碼的 JSON
*
* @param fileContent PDF 二進(jìn)制數(shù)據(jù)
* @param x 開始裁剪的橫坐標(biāo)
* @param y 開始裁剪的縱坐標(biāo)
* @param w 需要裁剪的寬度
* @param h 需要裁剪的高度
* @return Base64 編碼的 JSON
* @throws Exception 異常
*/
public static ResponseEntity<String> convertPdfToBase64(byte[] fileContent, int x, int y, int w, int h) throws Exception {
List<byte[]> imageBytesList = convertPdfToImages(fileContent, 0, 300, x, y, w, h);
List<String> base64Images = new ArrayList<>();
for (byte[] imageBytes : imageBytesList) {
String base64Image = Base64.getEncoder().encodeToString(imageBytes);
base64Images.add(base64Image);
}
ObjectMapper mapper = new ObjectMapper();
String jsonResult = mapper.writeValueAsString(base64Images);
return ResponseEntity.ok().body(jsonResult);
}
}3、編寫控制器用于瀏覽器直接打開
第五步會編寫控制器用于在 VUE 前端預(yù)覽 PDF 文件
/**
* 測試添加數(shù)字簽名
*
* @param filename 文件名
* @param x x坐標(biāo)
* @param y y坐標(biāo)
* @param h 高度
* @param i 寬度
*/
@GetMapping("/stamp/{filename}/p")
@Parameter(name = "x", description = "添加簽名的 x 坐標(biāo)", required = true, example = "x")
@Parameter(name = "y", description = "添加簽名的 y 坐標(biāo)", required = true, example = "y")
@Parameter(name = "h", description = "簽名的顯示高度", required = true, example = "h")
@Parameter(name = "i", description = "簽名所在頁數(shù)下標(biāo)", required = true, example = "i")
public ResponseEntity<ByteArrayResource> stampTest(@PathVariable String filename, @RequestParam("x") Integer x, @RequestParam("y") Integer y,
@RequestParam("h") Integer h, @RequestParam("i") Integer i) throws Exception {
// 從數(shù)據(jù)庫中獲取文件內(nèi)容,這里需要修改為你們自己的獲取方式來獲取源 PDF 文件的字節(jié)數(shù)組
byte[] fileContent = fileApi.getFileContent(4L, filename);
ByteArrayOutputStream out = new ByteArrayOutputStream();
// 添加數(shù)字簽名
try (PDDocument document = PDFBoxUtil.loadPdf(fileContent)) {
// 這里需要修改為你們自己的獲取方式來獲取簽名文件的字節(jié)數(shù)組
byte[] imageByteArray = fileApi.getFileContent(4L, "2c095928083c5ee82e6e229089892191d7790a3a42616dfd5a49daae68c27f41.png");
PDFBoxUtil.addStampToPdf(document, imageByteArray, x, y, h, i);
document.save(out);
} catch (IOException e) {
e.printStackTrace();
}
// 創(chuàng)建 ByteArrayResource
ByteArrayResource resource = new ByteArrayResource(out.toByteArray());
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + filename + "\"")
.contentType(MediaType.APPLICATION_PDF)
.body(resource);
}4、瀏覽器測試
直接打開連接http://IP:端口/你們自己的控制器前綴/stamp/文件名/p?x=100&y=200&h=80&i=1進(jìn)行測試

5、編寫控制器用于在 VUE 前端預(yù)覽 PDF 文件
我這邊在預(yù)覽的時候不想保留邊距、頁眉、頁腳的數(shù)據(jù),所以有裁剪參數(shù),不需要的話需要自行修改
/**
* 根據(jù)合約名稱獲取合約 PDF 文件,并返回圖片的 Base64 編碼
*
* @param filename合約標(biāo)識
* @return 圖片的 Base64 編碼
*/
@GetMapping(value = "/get/{filename}", produces = MediaType.IMAGE_PNG_VALUE)
@Parameter(name = "x", description = "每一頁開始裁剪的 x 橫坐標(biāo)", required = true, example = "x")
@Parameter(name = "y", description = "每一頁開始裁剪的 y 縱坐標(biāo)", required = true, example = "y")
@Parameter(name = "h", description = "每一頁需要裁剪掉的高度 h", required = true, example = "h")
@Parameter(name = "w", description = "每一個需要裁剪掉的寬度 w", required = true, example = "w")
public ResponseEntity<String> getPageImage(@PathVariable String filename, @RequestParam("x") int x, @RequestParam("y") int y,
@RequestParam("h") int h, @RequestParam("w") int w) {
// 從數(shù)據(jù)庫中獲取文件內(nèi)容,這里需要修改為你們自己的獲取方式來獲取源 PDF 文件的字節(jié)數(shù)組
byte[] fileContent = fileApi.getFileContent(4L, filename);
try {
return PDFBoxUtil.convertPdfToBase64(fileContent, x, y, w, h);
} catch (IOException e) {
throw new RuntimeException("獲取 PDF 文件截圖異常", e);
} catch (Exception e) {
throw new RuntimeException("讀取 PDF 文件異常", e);
}
}6、編寫 VUE 代碼
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<div v-if="formLoading">{{message}}</div>
<div id="pdf-container">
</div>
</Dialog>
</template>
<script setup lang="ts">
defineOptions({ name: 'ContentWXPreview' })
const dialogVisible = ref(false) // 彈窗的是否展示
const dialogTitle = ref('') // 彈窗的標(biāo)題
const formLoading = ref(false) // 表單的加載中
const message = ref('數(shù)據(jù)正在加載請稍后 ... ...')
/** 打開彈窗 */
const open = async (title: string, code: string) => {
dialogVisible.value = true
dialogTitle.value = title + '_預(yù)覽'
formLoading.value = true
try {
fetch('http://IP:端口/你們自己的控制器前綴/stamp/文件名/p?x=250&y=188&w=520&h=385', {
method: 'GET',
headers: {
'Content-Type': 'application/octet-stream'
}
})
.then(response => response.text())
.then(base64Images => {
const container = document.getElementById('pdf-container')
if (container) {
container.innerHTML = '' // 清空容器
const images = JSON.parse(base64Images)
images.forEach(base64Image => {
let img = document.createElement('img')
img.src = `data:image/png;base64,${base64Image}`
container.appendChild(img)
})
}
formLoading.value = false
})
} finally {
formLoading.value = false
}
}
defineExpose({ open }) // 提供 open 方法,用于打開彈窗
</script>
<style lang="scss">
#pdf-container {
display: flex;
flex-direction: column;
align-items: center;
}
#pdf-container > img {
max-width: 100%;
}
</style>7、預(yù)覽顯示

擴展:雖然 PDFBox 很強大,但是在讀取文件、文件識別、文字替換等方面使用起來不是特別方便,需要有一定的學(xué)習(xí)成本。對于我這邊偶爾開發(fā) PDF 文檔處理半路子來說太難了,所以會在SpringBoot集成SpirePDF實現(xiàn)文本替換功能_java_腳本之家 (jb51.net)說明如何使用 Spider.PDF 進(jìn)行文本替換
以上就是SpringBoot集成PDFBox實現(xiàn)電子簽章的代碼詳解的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot PDFBox電子簽章的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot?Validation提示信息國際化配置方式
這篇文章主要介紹了SpringBoot?Validation提示信息國際化配置方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-02-02
多jdk環(huán)境下指定springboot外部配置文件詳解
這篇文章主要為大家介紹了多jdk環(huán)境下指定springboot外部配置文件詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03
在Spring Boot中淺嘗內(nèi)存泄漏的實戰(zhàn)記錄
本文給大家分享在Spring Boot中淺嘗內(nèi)存泄漏的實戰(zhàn)記錄,結(jié)合實例代碼給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2025-04-04
Spring boot項目使用thymeleaf模板過程詳解
這篇文章主要介紹了Spring boot項目使用thymeleaf模板過程詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-07-07

