itext实现pdf自动定位合同签订

需要实现如下效果(最终效果)

itext实现pdf自动定位合同签订

思考

需求方的要求就是实现签订合同,实现方法不限,但过程中又提出需要在签章的过程中把签订日期的文字也打上去,这就有点坑了~

一开始的想法是想办法定位需要签名的位置,事实上同类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(); } } }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zyjxzw.html