SpringBoot集成PDFBox實(shí)現(xiàn)電子簽章的代碼詳解
Apache PDFBox 是一個(gè)開(kāi)源的 Java 庫(kù),用于處理 PDF 文檔。它提供了一系列強(qiáng)大的功能,包括創(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> <!-- 請(qǐng)檢查最新的版本 --> </dependency>
2、編寫(xiě)工具類
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 文檔對(duì)象 * @param imageByteArray 印章圖像的二進(jìn)制數(shù)據(jù) * @param x 橫坐標(biāo) * @param y 縱坐標(biāo) * @param h 高度 * @param pageIdx 頁(yè)碼 * @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 文檔的第一個(gè)頁(yè)面 PDPage page = document.getPage(pageIdx); // 計(jì)算簽章圖像的尺寸 float desiredHeight = h; // 目標(biāo)高度 float scale = desiredHeight / pdImage.getHeight(); // 創(chuàng)建一個(gè)內(nèi)容流以添加簽章 try (PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true, true)) { // 在 PDF 頁(yè)面上繪制簽章圖像 contentStream.drawImage(pdImage, x, y, pdImage.getWidth() * scale, pdImage.getHeight() * scale); } // 可選:也可以向 PDF 添加一個(gè)簽名字段 // 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 頁(yè)面 * @param x 開(kāi)始裁剪的橫坐標(biāo) * @param y 開(kāi)始裁剪的縱坐標(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)換為多個(gè)圖片 * * @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++) { // 渲染頁(yè)面 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 開(kāi)始裁剪的橫坐標(biāo) * @param y 開(kāi)始裁剪的縱坐標(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、編寫(xiě)控制器用于瀏覽器直接打開(kāi)
第五步會(huì)編寫(xiě)控制器用于在 VUE 前端預(yù)覽 PDF 文件
/** * 測(cè)試添加數(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 = "簽名所在頁(yè)數(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ù)庫(kù)中獲取文件內(nèi)容,這里需要修改為你們自己的獲取方式來(lái)獲取源 PDF 文件的字節(jié)數(shù)組 byte[] fileContent = fileApi.getFileContent(4L, filename); ByteArrayOutputStream out = new ByteArrayOutputStream(); // 添加數(shù)字簽名 try (PDDocument document = PDFBoxUtil.loadPdf(fileContent)) { // 這里需要修改為你們自己的獲取方式來(lái)獲取簽名文件的字節(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、瀏覽器測(cè)試
直接打開(kāi)連接http://IP:端口/你們自己的控制器前綴/stamp/文件名/p?x=100&y=200&h=80&i=1進(jìn)行測(cè)試
5、編寫(xiě)控制器用于在 VUE 前端預(yù)覽 PDF 文件
我這邊在預(yù)覽的時(shí)候不想保留邊距、頁(yè)眉、頁(yè)腳的數(shù)據(jù),所以有裁剪參數(shù),不需要的話需要自行修改
/** * 根據(jù)合約名稱獲取合約 PDF 文件,并返回圖片的 Base64 編碼 * * @param filename合約標(biāo)識(shí) * @return 圖片的 Base64 編碼 */ @GetMapping(value = "/get/{filename}", produces = MediaType.IMAGE_PNG_VALUE) @Parameter(name = "x", description = "每一頁(yè)開(kāi)始裁剪的 x 橫坐標(biāo)", required = true, example = "x") @Parameter(name = "y", description = "每一頁(yè)開(kāi)始裁剪的 y 縱坐標(biāo)", required = true, example = "y") @Parameter(name = "h", description = "每一頁(yè)需要裁剪掉的高度 h", required = true, example = "h") @Parameter(name = "w", description = "每一個(gè)需要裁剪掉的寬度 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ù)庫(kù)中獲取文件內(nèi)容,這里需要修改為你們自己的獲取方式來(lá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、編寫(xiě) 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ù)正在加載請(qǐng)稍后 ... ...') /** 打開(kāi)彈窗 */ 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 方法,用于打開(kāi)彈窗 </script> <style lang="scss"> #pdf-container { display: flex; flex-direction: column; align-items: center; } #pdf-container > img { max-width: 100%; } </style>
7、預(yù)覽顯示
擴(kuò)展:雖然 PDFBox 很強(qiáng)大,但是在讀取文件、文件識(shí)別、文字替換等方面使用起來(lái)不是特別方便,需要有一定的學(xué)習(xí)成本。對(duì)于我這邊偶爾開(kāi)發(fā) PDF 文檔處理半路子來(lái)說(shuō)太難了,所以會(huì)在SpringBoot集成SpirePDF實(shí)現(xiàn)文本替換功能_java_腳本之家 (jb51.net)說(shuō)明如何使用 Spider.PDF 進(jìn)行文本替換
以上就是SpringBoot集成PDFBox實(shí)現(xiàn)電子簽章的代碼詳解的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot PDFBox電子簽章的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Maven創(chuàng)建項(xiàng)目過(guò)慢的4種解決辦法
最近經(jīng)常會(huì)遇到一個(gè)困擾,那就是用idea創(chuàng)建maven項(xiàng)目時(shí),速度很慢,本文就來(lái)介紹一下Maven創(chuàng)建項(xiàng)目過(guò)慢的4種解決辦法,感興趣的可以了解一下2021-12-12SpringBoot?Validation提示信息國(guó)際化配置方式
這篇文章主要介紹了SpringBoot?Validation提示信息國(guó)際化配置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02多jdk環(huán)境下指定springboot外部配置文件詳解
這篇文章主要為大家介紹了多jdk環(huán)境下指定springboot外部配置文件詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03在Spring Boot中淺嘗內(nèi)存泄漏的實(shí)戰(zhàn)記錄
本文給大家分享在Spring Boot中淺嘗內(nèi)存泄漏的實(shí)戰(zhàn)記錄,結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2025-04-04java局部變量表的基礎(chǔ)知識(shí)點(diǎn)及實(shí)例
在本篇文章里小編給大家整理的是一篇關(guān)于java局部變量表的基礎(chǔ)知識(shí)點(diǎn)及實(shí)例,有需要的朋友們可以學(xué)習(xí)參考下。2021-06-06java連接Mongodb實(shí)現(xiàn)增刪改查
這篇文章主要為大家詳細(xì)介紹了java連接Mongodb實(shí)現(xiàn)增刪改查,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-03-03Spring boot項(xiàng)目使用thymeleaf模板過(guò)程詳解
這篇文章主要介紹了Spring boot項(xiàng)目使用thymeleaf模板過(guò)程詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07