PDF文件替换内容(电子签章),依赖免费pdfbox
首先提前准备,压入如下依赖
<!-- https://mvnrepository.com/artifact/org.apache.pdfbox/pdfbox -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.16</version>
</dependency>
正文开始
创建:
CoordinateDTO 坐标bean,用来存替换文字的坐标位置
PdfBoxKeyWordPosition 工具类,解析pdf文件,获取关键字坐标
ReportUtils 工具类,替换文件内容,可用于电子签章
源码如下:
(注:源码中的package...和import...需要修改为项目实际位置)
CoordinateDTO.java
/*** @FileName: CoordinateDTO.java* @creator yongzhizean* @date 2023年2月17日 下午5:16:52* @editor* @Description:* @version V1.0*/
package ...包位置;import lombok.Data;/*** @ClassName: CoordinateDTO * @Description: 坐标* @author yongzhizean* @date 2023年2月17日 下午5:16:52* @version V1.0*/
@Data
public class CoordinateDTO {/*** 关键字在PDF中的X坐标*/private Float x;/*** 关键字在PDF中的Y坐标*/private Float y;/*** 关键字在PDF中的页码*/private Integer pageNum;/*** 关键字在PDF中的显示出来的长度*/private Float length;
}
PdfBoxKeyWordPosition.java
/*** @FileName: PdfBoxKeyWordPosition.java* @creator yongzhizean* @date 2023年2月17日 下午5:16:52* @editor* @Description:* @version V1.0*/
package ...自己包位置;import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.TextPosition;import ...CoordinateDTO位置;import lombok.extern.slf4j.Slf4j;import java.io.*;
import java.util.ArrayList;
import java.util.List;/*** @ClassName: PdfBoxKeyWordPosition* @Description: 解析pdf文件,获取关键字坐标* @author yongzhizean* @date 2023年2月17日 下午5:16:52* @version V1.0*/
@Slf4j
public class PdfBoxKeyWordPosition extends PDFTextStripper {/*** 关键字字符数组*/private char[] key;/*** 关键字字符数组*/private boolean flag;/*** 坐标集合*/private List<CoordinateDTO> coordinates = new ArrayList<CoordinateDTO>();/*** 当前页坐标集合*/private List<CoordinateDTO> pageList = new ArrayList<CoordinateDTO>();/*** 使用字符流* * @param keyWords* @param document* @param flag* @throws IOException*/public PdfBoxKeyWordPosition(String keyWords, PDDocument document, boolean flag) throws IOException {super();super.setSortByPosition(true);this.document = document;this.flag = flag;char[] key = new char[keyWords.length()];for (int i = 0; i < keyWords.length(); i++) {key[i] = keyWords.charAt(i);}this.key = key;}/*** * 获取坐标信息* @date 2023年2月17日 下午5:22:34* @author yongzhizean* @return* @return List<CoordinateDTO>*/public List<CoordinateDTO> getCoordinate() {try {int pages = document.getNumberOfPages();for (int i = 1; i <= pages; i++) {super.setSortByPosition(true);super.setStartPage(i);super.setEndPage(i);Writer dummy = new OutputStreamWriter(new ByteArrayOutputStream());super.writeText(document, dummy);for (CoordinateDTO li : pageList) {li.setPageNum(i);}coordinates.addAll(pageList);pageList.clear();}} catch (Exception e) {log.error("获取pdf关键字坐标失败:{}", e);} finally {pageList.clear();if (flag) {try {if (document != null) {document.close();}} catch (IOException e) {log.error("关闭文件失败:{}", e);}} else {log.info("不关闭文件");}}return coordinates;}/*** 获取坐标信息*/@Overrideprotected void writeString(String string, List<TextPosition> textPositions) throws IOException {for (int i = 0; i < textPositions.size(); i++) {String str = textPositions.get(i).getUnicode();// 找到 key 中第一位所在位置if (str.equals(String.valueOf(key[0]))) {int count = 0;for (int j = 0; j < key.length; j++) {String s = "";try {s = textPositions.get(i + j).getUnicode();} catch (Exception e) {s = "";}// 判断key 中每一位是否和文本中顺序对应,一旦不等说明 关键字与本段落不等,则停止本次循环if (s.equals(String.valueOf(key[j]))) {count++;} else if (count > 0) {break;}}// 判断 key 中字 在文本是否连续,是则获取坐标if (count == key.length) {CoordinateDTO coordinate = new CoordinateDTO();TextPosition tp = textPositions.get(i);// X坐标 直接获取的字体位置 ,也可以加上了字体的长度 tp.getX()+ + tp.getFontSize()Float x = tp.getX();// Y坐标 减去的字体的长度 tp.getPageHeight() - tp.getY() - 4 * tp.getFontSize()Float y = tp.getPageHeight() - tp.getY();coordinate.setX(x);coordinate.setY(y);coordinate.setLength(tp.getFontSize());pageList.add(coordinate);}}}}
}
ReportUtils.java
/*** @FileName: ReportUtils.java* @creator yongzhizean* @date 2023年2月17日 下午5:16:52* @editor* @Description:* @version V1.0*/
package ...包位置;import ...CoordinateDTO位置;
import ...PdfBoxKeyWordPosition位置;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.pdfbox.contentstream.operator.Operator;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSString;
import org.apache.pdfbox.pdfparser.PDFStreamParser;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.springframework.stereotype.Service;import java.awt.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Iterator;
import java.util.List;/*** * @ClassName: ReportUtils * @Description: pdf电子签章* @author yongzhizean* @date 2023年2月17日 下午5:16:52* @version V2.0*/
@Service
@Slf4j
public class ReportUtils {/*** * 生成新的报表* * @date 2023年2月17日 下午4:59:15* @author yongzhizean* @param url* 报表、pdf文档的地址* @param imageUrl* 签章图片地址* @param key* 需要签章的文字* @return* @throws Exception* @return byte[]*/public byte[] getNewReport(String url, String imageUrl, String key) throws Exception {ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();try {log.info("访问报表Url:" + url);// 根据url获取文件流InputStream is = getFile(url);PDDocument doc = PDDocument.load(is);// 获取需要签章的文字坐标PdfBoxKeyWordPosition pdf = new PdfBoxKeyWordPosition(key, doc, false);List<CoordinateDTO> wordsPcoordinates = pdf.getCoordinate();log.info("获取新的textLocalDTO 带坐标:" + wordsPcoordinates.toString());// 替换坐标中的文字为空,并且赋值图片try {// 防止替换的文字盖住章或签字,先进行所有的替换,再进行所有的赋值for (CoordinateDTO coordinate : wordsPcoordinates) {// 获取需要替换的页面PDPage page = doc.getPage(coordinate.getPageNum() - 1);// 原本想根据key删除页面文本,但是deleteKey(page, textLocalDTO.getKey())未生效// 生成一个白色的矩形盖住需要替换的文本,实现替换功能PDPageContentStream contentStream = new PDPageContentStream(doc, page,PDPageContentStream.AppendMode.APPEND, true, true);contentStream.setNonStrokingColor(new Color(255, 255, 255));contentStream.addRect(coordinate.getX() - 1, coordinate.getY() - 1,coordinate.getLength() * key.length() + 1, coordinate.getLength() + 1);contentStream.fill();contentStream.close();}// 文件地址为空,签章文字坐标未找到返回原文件if (wordsPcoordinates.size() == 0 || StringUtils.isBlank(imageUrl)) {return toByteArray(is);}// 获取图片流InputStream imageInputStream = getFile(imageUrl);byte[] imageBtye = toByteArray(imageInputStream);// 指定页面,指定位置插入图片for (CoordinateDTO coordinate : wordsPcoordinates) {// 获取需要替换的页面PDPage page = doc.getPage(coordinate.getPageNum() - 1);PDPageContentStream contentStream = new PDPageContentStream(doc, page,PDPageContentStream.AppendMode.APPEND, true, true);PDImageXObject pdImage = PDImageXObject.createFromByteArray(doc, imageBtye, null);// 图片大小为80,找到替换位置x\y轴减去一半,保证在需要签章位置在中间contentStream.drawImage(pdImage, coordinate.getX() - 40, coordinate.getY() - 40, 80, 80);contentStream.close();}doc.save(byteArrayOutputStream);byte[] pdfBytes = byteArrayOutputStream.toByteArray();return pdfBytes;} catch (Exception e) {throw e;} finally {if (doc != null) {doc.close();}}} catch (Exception e) {throw e;}}/*** * 获取url后 下载文件* @date 2023年2月17日 下午5:16:28* @author yongzhizean* @param url* @return* @throws Exception* @return InputStream*/private InputStream getFile(String url) throws Exception {// 创建不同的 文件夹 目录InputStream inputStream = null;String fileName = url.substring(url.lastIndexOf("/") + 1);String newUrl = url.substring(0, url.lastIndexOf("/") + 1) + URLEncoder.encode(fileName, "utf-8");try {// 建立链接URL httpUrl = new URL(newUrl);HttpURLConnection conn = null;conn = (HttpURLConnection) httpUrl.openConnection();log.info("文件获取完毕,文件大小为:" + conn.getContentLength());// 获取网络输入流inputStream = httpUrl.openStream();} catch (Exception e) {log.error("文件获取失败:" + e.getMessage(), e);throw e;}return inputStream;}/**** InputStream 转换成byte[]* @date 2023年2月17日 下午5:17:53* @author yongzhizean* @param input* @return* @throws IOException* @return byte[]*/private byte[] toByteArray(InputStream input) throws IOException {ByteArrayOutputStream output = new ByteArrayOutputStream();byte[] buffer = new byte[1024 * 4];int n = 0;while (-1 != (n = input.read(buffer))) {output.write(buffer, 0, n);}return output.toByteArray();}/*** * 删除标记,未成功,不了解啥原因* @date 2023年2月17日 下午5:18:14* @author hushizhao* @param page* @param key* @throws IOException* @return void*/@SuppressWarnings("unused")private void deleteKey(PDPage page, String key) throws IOException {try {// 流对象来接收当前page的内容Iterator<PDStream> contents = page.getContentStreams();// PDF流对象剖析器(这将解析一个PDF字节流并提取操作数,等等)while (contents.hasNext()) {PDStream content = contents.next();// PDF流对象剖析器(这将解析一个PDF字节流并提取操作数,等等)PDFStreamParser parser = new PDFStreamParser(content.toByteArray());parser.parse();// 用list存流中的所有标记List<Object> tokens = parser.getTokens();for (int j = 0; j < tokens.size(); j++) {// 创建一个object对象去接收标记Object next = tokens.get(j);if (next instanceof Operator) {Operator op = (Operator) next;if (op.getName().equals("Tj")) {// COSString对象>>创建java字符串的一个新的文本字符串。COSString previous = (COSString) tokens.get(j - 1);// 将此字符串的内容作为PDF文本字符串返回。String string = previous.getString();// replaceAll>>替换字符string = string.replaceAll(key, "");System.out.println(string.getBytes("UTF-8"));// 重置COSString对象,设置字符编码格式previous.setValue(string.getBytes("UTF-8"));} else if (op.getName().equals("TJ")) {// COSArray是pdfbase对象数组,作为PDF文档的一部分COSArray previous = (COSArray) tokens.get(j - 1);// 循环previousfor (int k = 0; k < previous.size(); k++) {// 这将从数组中获取一个对象,这将取消引用该对象// 如果对象为cosnull,则返回nullObject arrElement = previous.getObject(k);if (arrElement instanceof COSString) {// COSString对象>>创建java字符串的一个新的文本字符串。COSString cosString = (COSString) arrElement;// 将此字符串的内容作为PDF文本字符串返回。String string = cosString.getString();// 替换string = string.replaceAll(key, "");log.info("替换字符1" + string);// 重置COSString对象cosString.setValue(string.getBytes("UTF-8"));}}}}}}} catch (Exception e) {log.error("pdf文本替换成空失败:" + e.getMessage(), e);throw e;}}
}
有大神解决了deleteKey方法不能用的话,欢迎评论指点,谢谢!