itext實現pdf自動定位合同簽訂
阿新 • • 發佈:2018-11-24
需求
- 需要實現如下效果(最終效果)
思考
- 需求方的要求就是實現簽訂合同,實現方法不限,但過程中又提出需要在簽章的過程中把簽訂日期的文字也打上去,這就有點坑了~
- 一開始的想法是想辦法定位需要簽名的位置,事實上同類app實現方式就是這樣,在前端實現簽名位置定位,把位置資訊發給後端,後端就可以很方便把印章放上去。
- 但現實是現在前端不靠譜,暫時不能提供這樣的功能;而且日期資訊的填寫也需要定位,這怎麼辦?使用者不會手動去定位日期的位置,最多會調整下簽名的位置才合理
- 然後我研究了下itext的api,並討論決定尾部簽名部分我們自己做。也就是上圖中的下半部分的所有內容,包括甲方乙方,日期,簽章等都通過程式自動定位上去
- 這樣的想法遇到的難點,首先是y軸的定位問題。首先要找到文件的尾行在哪,在適當的距離進行文字的填寫。我沒有找到可以直接在文件末尾新增文字的api,如果各位知道麻煩指教一下。
步驟
- 因為有上述的問題,我首先考慮要找到尾行的文字才會考慮寫程式碼。通過api研究,可以通過itext的監聽器遍歷文字拿到尾行文字等資訊
- x周位置根據頁面寬度調整
- 文字大小和字型型別問題。字型型別是我現在也沒解決的,我沒找到獲取pdf文件字型型別和大小的api,請指教
- 因為沒找到api所以我用的最笨的方法,通過獲取字型的高度來確定字型大小,這樣的文字寫出來差別不會太大。至於字型,只能認為規定,合同字型統一宋體。
- 過程中還遇到的問題就是字型左邊距對齊問題,很明顯甲乙方在一行上,中間用空格來分割的話會很不標準。所以我最終決定用table,且左右邊簽名和文字分開進行寫入。也就是甲籤的時候寫左半部分,乙籤的時候寫右半部分。當簽完後就是上圖的效果
- 說了這麼多接下來直接上工具程式碼吧,如果要使用,直接把幾個類程式碼複製過去,把字型路徑換成自己的,檔案路徑改下就可以在main方法執行測試了
上程式碼
- PdfParser類,主要實現類,包含了main方法
package com.zhiyis.framework.util.itext; import com.itextpdf.io.font.PdfEncodings; import com.itextpdf.kernel.font.PdfFont; import com.itextpdf.kernel.font.PdfFontFactory; import com.itextpdf.kernel.geom.Rectangle; import com.itextpdf.kernel.geom.Vector; import com.itextpdf.kernel.pdf.PdfDocument; import com.itextpdf.kernel.pdf.PdfReader; import com.itextpdf.kernel.pdf.PdfWriter; import com.itextpdf.kernel.pdf.canvas.parser.EventType; import com.itextpdf.kernel.pdf.canvas.parser.PdfDocumentContentParser; import com.itextpdf.kernel.pdf.canvas.parser.data.IEventData; import com.itextpdf.kernel.pdf.canvas.parser.data.TextRenderInfo; import com.itextpdf.kernel.pdf.canvas.parser.listener.IEventListener; import com.itextpdf.layout.Document; import com.itextpdf.layout.borders.Border; import com.itextpdf.layout.element.Cell; import com.itextpdf.layout.element.Paragraph; import com.itextpdf.layout.element.Table; import com.zhiyis.common.utils.DateUtil; import com.zhiyis.common.utils.Sysconfig; import com.zhiyis.framework.util.FileUtil; import com.zhiyis.framework.util.SignPdf; import lombok.extern.slf4j.Slf4j; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.*; /** * @author laoliangliang * @date 2018/11/23 15:03 */ @Slf4j public class PdfParser { private Sysconfig sysconfig; public PdfParser() { } public PdfParser(Sysconfig sysconfig) { this.sysconfig = sysconfig; } public enum SignType { //甲籤 SIGN_A(1), //乙籤 SIGN_B(2); private Integer type; SignType(Integer type) { this.type = type; } public Integer getType() { return type; } } public static void main(String[] args) { List<String> contents = new ArrayList<>(); contents.add("甲方法定代表人:"); contents.add("聯絡電話:"); contents.add("身份證號碼:"); contents.add(DateUtil.format2str("yyyy 年 MM 月 dd 日")); String input = "/Users/laoliangliang/Downloads/合同模板 (1).pdf"; String tempPath = "/Users/laoliangliang/Downloads/合同模板_signed.pdf"; String filePath = "/Users/laoliangliang/Downloads/31.png"; String fileOut = "/Users/laoliangliang/Downloads/合同模板_signed_signed_signed.pdf"; PdfParser pdfParser = new PdfParser(); // pdfParser.startSign(input, input, fileOut, filePath, SignType.SIGN_A, contents, false); pdfParser.startSign(input, fileOut, tempPath, filePath, SignType.SIGN_B, contents, true); } /** * 甲乙方簽名方法 * * @param rootPath 初始合同pdf路徑 * @param tempPath 基於哪份合同簽章,比如甲方先簽,這裡填的就是初始合同地址;若是乙方籤,這裡填的就是甲方簽過生成的合同地址 * @param outPath 輸出的合同地址,包含檔名 * @param imgPath 簽章圖片地址 * @param signType 甲方簽章還是乙方簽章,輸入列舉型別 * @param contents 簽章處文字內容 * @param already 理論上甲籤的時候是false,表示沒有簽過,乙籤的時候是true,表示甲已經簽過,就算下面高度不夠也不會新增頁面 * 若需求改動,可以乙先簽,那邏輯控制,先簽的false,後籤的true; * 該項錯誤可能導致第二方簽章時新啟一頁簽章 */ public void startSign(String rootPath, String tempPath, String outPath, String imgPath, SignType signType, List<String> contents, boolean already) { String tempRootPath = ""; try { //讀取文章尾部位置 MyRectangle myRectangle = getLastWordRectangle(rootPath); //還沒簽印的,臨時檔案路徑 tempRootPath = rootPath.substring(0, rootPath.length() - 4) + "_temp.pdf"; //新增尾部內容 SignPosition signPosition = addTailSign(myRectangle, tempPath, tempRootPath, signType.getType(), contents, already); InputStream in = PdfParser.class.getClassLoader().getResourceAsStream("keystore.p12"); byte[] fileData = SignPdf.sign("123456", in, tempRootPath, imgPath, signPosition.getX(), signPosition.getY(), signPosition.getPageNum()); FileUtil.uploadFile(fileData, outPath); } catch (Exception e) { log.error("簽名出錯", e); } finally { File file = new File(tempRootPath); if (file.exists()) { boolean flag = file.delete(); if (flag) { log.debug("臨時檔案刪除成功"); } } } } /** * 新增尾部簽名部分(不含簽名或印章) * * @param myRectangle 文件末尾位置和大致資訊 * @param input 輸入文件路徑 * @param output 輸出文件路徑 * @param type 1-甲籤 2-乙籤 * @param content 填寫內容 * @param already 理論上甲籤的時候是false,表示沒有簽過,乙籤的時候是true,表示甲已經簽過,就算下面高度不夠也不會新增頁面 * 若需求改動,可以乙先簽,那邏輯控制,先簽的false,後籤的true * @throws Exception */ private SignPosition addTailSign(MyRectangle myRectangle, String input, String output, Integer type, List<String> content, boolean already) throws Exception { PdfReader reader = new PdfReader(input); PdfWriter writer = new PdfWriter(output); PdfDocument pdf = new PdfDocument(reader, writer); int numberOfPages = pdf.getNumberOfPages(); Document doc = new Document(pdf); String dateFontPath; if (sysconfig == null) { dateFontPath = "/Library/Fonts/simsun.ttc"; }else{ dateFontPath = sysconfig.getProperties().getProperty("date_font_path"); } PdfFont font = PdfFontFactory.createFont(dateFontPath + ",1", PdfEncodings.IDENTITY_H, true); //判斷簽名高度是否夠 int size = content.size(); float maxRecHeight = myRectangle.getMinlineHeight() * size; float v = myRectangle.getBottom() - maxRecHeight; boolean isNewPage = false; if (v <= myRectangle.getMinlineHeight() * 3) { isNewPage = true; if (!already) { pdf.addNewPage(); numberOfPages++; } myRectangle.setBottom(myRectangle.getTop() * 2 - maxRecHeight * 2); } Table table = new Table(1); table.setPageNumber(numberOfPages); float bottom = (myRectangle.getBottom() - maxRecHeight) / 2; float left1; left1 = myRectangle.getLeft() + 30f; if (type == 2) { left1 = left1 + myRectangle.getWidth() / 2 - 15; } myRectangle.setLeft(left1); table.setFixedPosition(left1, bottom, 200); table.setBorder(Border.NO_BORDER); for (String text : content) { Paragraph paragraph = new Paragraph(); paragraph.add(text).setFont(font).setFontSize(myRectangle.getHeight()); Cell cell = new Cell(); cell.add(paragraph); cell.setBorder(Border.NO_BORDER); table.addCell(cell); } doc.add(table); doc.flush(); pdf.close(); return getSignPosition(myRectangle, content, bottom, numberOfPages, isNewPage); } private SignPosition getSignPosition(MyRectangle myRectangle, List<String> content, float bottom, int numberOfPages, boolean isNewPage) { SignPosition signPosition = new SignPosition(); //y軸位置,底部 if (isNewPage) { signPosition.setY(bottom + (content.size() - 2) * myRectangle.getMinlineHeight()); } else { signPosition.setY(bottom + (content.size() - 3) * myRectangle.getMinlineHeight()); } //x軸位置,文字寬度+偏移量 signPosition.setX(myRectangle.getLeft() + content.get(0).length() * myRectangle.getHeight() - 15f); signPosition.setPageNum(numberOfPages); return signPosition; } /** * 拿到文章末尾引數 */ private MyRectangle getLastWordRectangle(String input) throws IOException { PdfDocument pdfDocument = new PdfDocument(new PdfReader(input)); MyEventListener myEventListener = new MyEventListener(); PdfDocumentContentParser parser = new PdfDocumentContentParser(pdfDocument); parser.processContent(pdfDocument.getNumberOfPages(), myEventListener); List<Rectangle> rectangles = myEventListener.getRectangles(); float left = 100000; float right = 0; float bottom = 100000; boolean isTop = true; Rectangle tempRec = null; float minV = 1000; MyRectangle myRectangle = new MyRectangle(); //拿到文字最左最下和最右位置 for (Rectangle rectangle : rectangles) { if (isTop) { myRectangle.setTop(rectangle.getY()); isTop = false; } if (tempRec != null) { float v = tempRec.getY() - rectangle.getY(); if (v < minV && v > 5f) { minV = v; } } tempRec = rectangle; float lt = rectangle.getLeft(); float rt = rectangle.getRight(); float y = rectangle.getBottom(); if (lt < left) { left = lt; } if (rt > right) { right = rt; } if (y < bottom) { bottom = y; } } Rectangle rectangle = rectangles.get(rectangles.size() - 1); float height = rectangle.getHeight(); myRectangle.setHeight(height); myRectangle.setLeft(left); myRectangle.setRight(right); myRectangle.setBottom(bottom); myRectangle.setMinlineHeight(minV); myRectangle.setLineSpace(minV - height); myRectangle.setWidth(right - left); pdfDocument.close(); return myRectangle; } static class MyEventListener implements IEventListener { private List<Rectangle> rectangles = new ArrayList<>(); @Override public void eventOccurred(IEventData data, EventType type) { if (type == EventType.RENDER_TEXT) { TextRenderInfo renderInfo = (TextRenderInfo) data; if ("".equals(renderInfo.getText().trim())) { return; } Vector startPoint = renderInfo.getDescentLine().getStartPoint(); Vector endPoint = renderInfo.getAscentLine().getEndPoint(); float x1 = Math.min(startPoint.get(0), endPoint.get(0)); float x2 = Math.max(startPoint.get(0), endPoint.get(0)); float y1 = Math.min(startPoint.get(1), endPoint.get(1)); float y2 = Math.max(startPoint.get(1), endPoint.get(1)); rectangles.add(new Rectangle(x1, y1, x2 - x1, y2 - y1)); } } @Override public Set<EventType> getSupportedEvents() { return new LinkedHashSet<>(Collections.singletonList(EventType.RENDER_TEXT)); } public List<Rectangle> getRectangles() { return rectangles; } public void clear() { rectangles.clear(); } } }
- MyRectangle 用來存文件尾部資料的實體類
package com.zhiyis.framework.util.itext;
/**
* @author laoliangliang
* @date 2018/11/23 16:11
*/
public class MyRectangle {
private float width;
private float left;
private float right;
private float bottom;
private float top;
private float height;
/**
* 行間間隔
*/
private float lineSpace;
/**
* 最小行間距,從上一行底部到下一行底部的距離
*/
private float minlineHeight;
public float getWidth() {
return width;
}
public void setWidth(float width) {
this.width = width;
}
public float getLeft() {
return left;
}
public void setLeft(float left) {
this.left = left;
}
public float getRight() {
return right;
}
public void setRight(float right) {
this.right = right;
}
public float getBottom() {
return bottom;
}
public void setBottom(float bottom) {
this.bottom = bottom;
}
public float getHeight() {
return height;
}
public void setHeight(float height) {
this.height = height;
}
public float getLineSpace() {
return lineSpace;
}
public void setLineSpace(float lineSpace) {
this.lineSpace = lineSpace;
}
public float getMinlineHeight() {
return minlineHeight;
}
public void setMinlineHeight(float minlineHeight) {
this.minlineHeight = minlineHeight;
}
public float getTop() {
return top;
}
public void setTop(float top) {
this.top = top;
}
}
- SignPosition 簽章位置類
package com.zhiyis.framework.util.itext;
/**
* 簽章位置類
* @author laoliangliang
* @date 18/11/24 下午1:43
*/
public class SignPosition {
private float x;
private float y;
private float width;
private float height;
private Integer pageNum;
public Integer getPageNum() {
return pageNum;
}
public void setPageNum(Integer pageNum) {
this.pageNum = pageNum;
}
public float getX() {
return x;
}
public void setX(float x) {
this.x = x;
}
public float getY() {
return y;
}
public void setY(float y) {
this.y = y;
}
public float getWidth() {
return width;
}
public void setWidth(float width) {
this.width = width;
}
public float getHeight() {
return height;
}
public void setHeight(float height) {
this.height = height;
}
}
- SignPdf 簽章類
package com.zhiyis.framework.util;
import com.itextpdf.text.Image;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfSignatureAppearance;
import com.itextpdf.text.pdf.PdfSignatureAppearance.RenderingMode;
import com.itextpdf.text.pdf.PdfStamper;
import com.itextpdf.text.pdf.security.*;
import com.itextpdf.text.pdf.security.MakeSignature.CryptoStandard;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.io.*;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.util.UUID;
/**
* 簽印
*/
public class SignPdf {
/**
* @param password 祕鑰密碼
* @param inputStream 祕鑰檔案
* @param signPdfSrc 簽名的PDF檔案
* @param signImage 簽名圖片檔案
* @param x x座標
* @param y y座標
* @return
*/
public static byte[] sign(String password, InputStream inputStream, String signPdfSrc, String signImage,
float x, float y,int page) {
File signPdfSrcFile = new File(signPdfSrc);
PdfReader reader = null;
ByteArrayOutputStream signPDFData = null;
PdfStamper stp = null;
try {
BouncyCastleProvider provider = new BouncyCastleProvider();
Security.addProvider(provider);
KeyStore ks = KeyStore.getInstance("PKCS12", new BouncyCastleProvider());
// 私鑰密碼 為Pkcs生成證書是的私鑰密碼 123456
ks.load(inputStream, password.toCharArray());
String alias = (String) ks.aliases().nextElement();
PrivateKey key = (PrivateKey) ks.getKey(alias, password.toCharArray());
Certificate[] chain = ks.getCertificateChain(alias);
reader = new PdfReader(signPdfSrc);
signPDFData = new ByteArrayOutputStream();
// 臨時pdf檔案
File temp = new File(signPdfSrcFile.getParent(), System.currentTimeMillis() + ".pdf");
stp = PdfStamper.createSignature(reader, signPDFData, '\0', temp, true);
stp.setFullCompression();
PdfSignatureAppearance sap = stp.getSignatureAppearance();
sap.setReason("數字簽名,不可改變");
// 使用png格式透明圖片
Image image = Image.getInstance(signImage);
sap.setImageScale(0);
sap.setSignatureGraphic(image);
sap.setRenderingMode(RenderingMode.GRAPHIC);
int size = 120;
// 是對應x軸和y軸座標
float lly = y;
sap.setVisibleSignature(new Rectangle(x, lly, x + size, lly+size), page,
UUID.randomUUID().toString().replaceAll("-", ""));
stp.getWriter().setCompressionLevel(5);
ExternalDigest digest = new BouncyCastleDigest();
ExternalSignature signature = new PrivateKeySignature(key, DigestAlgorithms.SHA512, provider.getName());
MakeSignature.signDetached(sap, digest, signature, chain, null, null, null, 0, CryptoStandard.CADES);
stp.close();
reader.close();
return signPDFData.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (signPDFData != null) {
try {
signPDFData.close();
} catch (IOException e) {
}
}
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
}
}
}
return null;
}
}
- 工具方法
public static boolean uploadFile(byte[] file, String filePath) throws Exception {
String tempPath = filePath.substring(0,filePath.lastIndexOf("/"));
File targetFile = new File(tempPath);
if(!targetFile.exists()) {
boolean out = targetFile.mkdirs();
if(out) {
log.info(filePath + " create success");
} else {
log.info(filePath + " create fail");
}
}
FileOutputStream out1 = new FileOutputStream(filePath);
out1.write(file);
out1.flush();
out1.close();
File f = new File(filePath);
return f.exists();
}
總結
- 公私鑰的生成網上很多就自己去生成吧
- 如果想要測試效果的可以把簽章部分先去掉也可以執行
- 我覺得這篇部落格是我最有含金量的一篇了~我找了很多部落格定位pdf簽章的沒有靠譜的,很多技術實現都很複雜,我最初版本,也就是前面有一篇部落格實現就是改編自網上一篇部落格的,但是有很多問題,程式碼也過於複雜難懂,彎彎繞繞且難以修改增強。
- 我研究了官方最新程式碼結合自己腦洞大開的思路,精簡出了很簡單的三個類,其實排除實體類,真正實現功能就一個PdfParser
- **如果覺得有用給我點個贊哦^_^**