一、需求描述
实现合同模板自动生成合同,再添加电子签名。如图:
二、实现步骤
使用java开源itext库对pdf进行操作,主要分为:
1 html模板+model数据,通过freemarker进行渲染,生成pdf1。
2 查找指定电子签名的位置,然后再在指定位置插入签名图片,生成最终pdf。
备注:模板使用的是html形式,电子签名格式使用的是Base64格式。
准备html合同模板
合同模板,在公众中获取。添加java后端依赖
完整的依赖包,点击蓝字,在公众号中获取。html模板+model数据,生成pdf的工具类
注:
① FONT是黑体,每个window电脑基本上都有,目录都是一样的。FONT需要使用fontResolver.addFont(FONT…添加进去,不然可能汉字一偏空白。
② freeMarkDir是合同模板放置的位置,根据自己情况修改。
③ 使用的编码最好是"UTF-8",换一套环境后可能乱码。
public class JavaToPdfHtmlFreeMarker {private final static Logger logger = LoggerFactory.getLogger(JavaToPdfHtmlFreeMarker.class);private static final String FONT = "C:\\Windows\\Fonts\\simhei.ttf";private static final String freeMarkDir = "D:\\app\\contractTemplate\\";private static Configuration freemarkerCfg = null;static {freemarkerCfg =new Configuration();//freemarker的模板目录try {freemarkerCfg.setDirectoryForTemplateLoading(new File(freeMarkDir));freemarkerCfg.setEncoding(Locale.CHINA, "UTF-8");} catch (IOException e) {e.printStackTrace();}}public static void createPdf1(String content,String dest) throws IOException, DocumentException, com.lowagie.text.DocumentException {// String LOGO_PATH = "file:///C:/Users/86132/Desktop/";ITextRenderer render = new ITextRenderer();ITextFontResolver fontResolver = render.getFontResolver();fontResolver.addFont(FONT, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);// 解析html生成pdfrender.setDocumentFromString(content);//解决图片相对路径的问题// render.getSharedContext().setBaseURL(LOGO_PATH);render.layout();render.createPDF(new FileOutputStream(dest));}/*** freemarker渲染html*/public static String freeMarkerRender(Map<String, Object> data, String htmlTmp) {Writer out = new StringWriter();try {// 获取模板,并设置编码方式Template template = freemarkerCfg.getTemplate(htmlTmp,"UTF-8");template.setEncoding("UTF-8");// logger.info("template info --->" + template);// 合并数据模型与模板template.process(data, out); //将合并后的数据和模板写入到流中,这里使用的字符流// logger.info("template info --->" + template);// logger.info("out info --->" + out);out.flush();return out.toString();} catch (Exception e) {e.printStackTrace();} finally {try {out.close();} catch (IOException ex) {ex.printStackTrace();}}return null;}}
调用工具类生成pdf
// 模板名称String HTML = "contract.html";// 生成的pdf存放路径String DEST = "D:\\app\\user_contract\\";// 生成的pdf名称String fileName = "合同D0511SDNQB1.pdf"public static void main(String[] args) throws IOException, DocumentException, InterruptedException {Map<String,Object> data = new HashMap();data.put("合同编号","D0511SDNQB");data.put("甲方姓名","小哈");data.put("签字日期",".05.13");String content = JavaToPdfHtmlFreeMarker.freeMarkerRender(data,HTML);JavaToPdfHtmlFreeMarker.createPdf(content,DEST);}
找指定电子签名的位置,然后再在指定位置插入签名图片工具类
package weixin.charging.mercurius.weixin.service.impl.e_contractUtils;import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.util.ArrayList;import java.util.List;import com.itextpdf.awt.geom.Rectangle2D.Float;import com.itextpdf.text.pdf.PdfDictionary;import com.itextpdf.text.pdf.PdfName;import com.itextpdf.text.pdf.PdfReader;import com.itextpdf.text.pdf.parser.*;public class PdfKeywordFinder {/*** @description: 查找插入签名图片的最终位置,因为是插入签名图片,所以之前的关键字应只会出现一次* 这里直接使用了第一次查找到关键字的位置,并返回该关键字之后的坐标位置* @return: float[0]:页码,float[1]:最后一个字的x坐标,float[2]:最后一个字的y坐标*/public static float[] getAddImagePositionXY(String pdfName, String keyword) throws IOException {float[] temp = new float[3];List<float[]> positions = PdfKeywordFinder.findKeywordPostions(pdfName, keyword);// PdfReader pdfReader = new PdfReader(pdfName);// Rectangle pageSize = pdfReader.getPageSize(1);// temp[0]=positions.get(0)[0];// temp[1]=positions.get(0)[1]*pageSize.getWidth();// temp[2]=positions.get(0)[2]*pageSize.getHeight();temp[0] = positions.get(0)[0];temp[1] = positions.get(0)[1] + (keyword.length() * positions.get(0)[3]);temp[2] = positions.get(0)[2] - positions.get(0)[3];return temp;}/*** findKeywordPostions* 返回查找到关键字的首个文字的左上角坐标值** @param pdfName* @param keyword* @return List<float [ ]> : float[0]:pageNum float[1]:x float[2]:y* @throws IOException*/public static List<float[]> findKeywordPostions(String pdfName, String keyword) throws IOException {File pdfFile = new File(pdfName);byte[] pdfData = new byte[(int) pdfFile.length()];FileInputStream inputStream = new FileInputStream(pdfFile);//从输入流中读取pdfData.length个字节到字节数组中,返回读入缓冲区的总字节数,若到达文件末尾,则返回-1inputStream.read(pdfData);inputStream.close();List<float[]> result = new ArrayList<>();List<PdfPageContentPositions> pdfPageContentPositions = getPdfContentPostionsList(pdfData);for (PdfPageContentPositions pdfPageContentPosition : pdfPageContentPositions) {List<float[]> charPositions = findPositions(keyword, pdfPageContentPosition);if (charPositions == null || charPositions.size() < 1) {continue;}result.addAll(charPositions);}return result;}private static List<PdfPageContentPositions> getPdfContentPostionsList(byte[] pdfData) throws IOException {PdfReader reader = new PdfReader(pdfData);List<PdfPageContentPositions> result = new ArrayList<>();int pages = reader.getNumberOfPages();for (int pageNum = 1; pageNum <= pages; pageNum++) {float width = reader.getPageSize(pageNum).getWidth();float height = reader.getPageSize(pageNum).getHeight();PdfRenderListener pdfRenderListener = new PdfRenderListener(pageNum, width, height);//解析pdf,定位位置PdfContentStreamProcessor processor = new PdfContentStreamProcessor(pdfRenderListener);PdfDictionary pageDic = reader.getPageN(pageNum);PdfDictionary resourcesDic = pageDic.getAsDict(PdfName.RESOURCES);try {processor.processContent(ContentByteUtils.getContentBytesForPage(reader, pageNum), resourcesDic);} catch (IOException e) {reader.close();throw e;}String content = pdfRenderListener.getContent();List<CharPosition> charPositions = pdfRenderListener.getcharPositions();List<float[]> positionsList = new ArrayList<>();for (CharPosition charPosition : charPositions) {float[] positions = new float[]{charPosition.getPageNum(), charPosition.getX(), charPosition.getY(), charPosition.getCharWidth()};positionsList.add(positions);}PdfPageContentPositions pdfPageContentPositions = new PdfPageContentPositions();pdfPageContentPositions.setContent(content);pdfPageContentPositions.setPostions(positionsList);result.add(pdfPageContentPositions);}reader.close();return result;}private static List<float[]> findPositions(String keyword, PdfPageContentPositions pdfPageContentPositions) {List<float[]> result = new ArrayList<>();String content = pdfPageContentPositions.getContent();List<float[]> charPositions = pdfPageContentPositions.getPositions();for (int pos = 0; pos < content.length(); ) {int positionIndex = content.indexOf(keyword, pos);if (positionIndex == -1) {break;}float[] postions = charPositions.get(positionIndex);result.add(postions);pos = positionIndex + 1;}return result;}private static class PdfPageContentPositions {private String content;private List<float[]> positions;public String getContent() {return content;}public void setContent(String content) {this.content = content;}public List<float[]> getPositions() {return positions;}public void setPostions(List<float[]> positions) {this.positions = positions;}}private static class PdfRenderListener implements RenderListener {private int pageNum;private float pageWidth;private float pageHeight;private StringBuilder contentBuilder = new StringBuilder();private List<CharPosition> charPositions = new ArrayList<>();public PdfRenderListener(int pageNum, float pageWidth, float pageHeight) {this.pageNum = pageNum;this.pageWidth = pageWidth;this.pageHeight = pageHeight;}@Overridepublic void beginTextBlock() {}@Overridepublic void renderText(TextRenderInfo textRenderInfo) {List<TextRenderInfo> characterRenderInfos = textRenderInfo.getCharacterRenderInfos();for (TextRenderInfo renderInfo : characterRenderInfos) {String word = renderInfo.getText();if (word.length() > 1) {word = word.substring(word.length() - 1, word.length());}Float rectangle = renderInfo.getAscentLine().getBoundingRectange();float x = (float) rectangle.getMinX();float y = (float) rectangle.getMinY();float charWidth = (float) (rectangle.getMaxX() - rectangle.getMinX());//也可以返回坐标相对于pdf页面大小的百分比float xPercent = Math.round(x / pageWidth * 10000) / 10000f;float yPercent = Math.round((1 - y / pageHeight) * 10000) / 10000f;CharPosition charPosition = new CharPosition(pageNum, x, y, charWidth);charPositions.add(charPosition);contentBuilder.append(word);}}@Overridepublic void endTextBlock() {}@Overridepublic void renderImage(ImageRenderInfo renderInfo) {}public String getContent() {return contentBuilder.toString();}public List<CharPosition> getcharPositions() {return charPositions;}}private static class CharPosition {private int pageNum = 0;private float x = 0;private float y = 0;private float charWidth = 0;//单个文字的宽度public CharPosition(int pageNum, float x, float y, float charWidth) {this.pageNum = pageNum;this.x = x;this.y = y;this.charWidth = charWidth;}public int getPageNum() {return pageNum;}public float getX() {return x;}public float getY() {return y;}public float getCharWidth() {return charWidth;}@Overridepublic String toString() {return "[pageNum=" + this.pageNum + ",x=" + this.x + ",y=" + this.y + "]";}}}
在pdf上添加签名
public static void main(String[] args) throws IOException {// base64格式的图片String signData = 'data:image/png;base64,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';// 生成电子签字String oldPdfUrl= DEST+"合同D0511SDNQB1.pdf";String newPdfUrl= DEST+"合同D0511SDNQB.pdf";//查找签名位置float[] position= PdfKeywordFinder.getAddImagePositionXY(oldPdfUrl,"承租人(乙方)签章:");//Read file using PdfReaderPdfReader pdfReader = new PdfReader(oldPdfUrl);System.out.println("x:"+position[1]+" y:"+position[2]);PdfStamper pdfStamper = new PdfStamper(pdfReader, new FileOutputStream(newPdfUrl));String imgUrl = Base64ImgConvert.convertBase64ToPng(signData, contractNo);Image image = Image.getInstance(imgUrl);//Fixed Positioningimage.scaleAbsolute(100, 50);//Scale to new height and new width of imageimage.setAbsolutePosition(position[1], position[2]);System.out.println("pages:"+pdfReader.getNumberOfPages());PdfContentByte content = pdfStamper.getUnderContent((int) position[0]);content.addImage(image);pdfStamper.close();}
如有错误,欢迎指正!
微信公众号,欢迎关注交流!