需求背景

目前项目中有通过 上传word模板,结合用户填写数据,导出渲染后的PDF数据,提升数据安全性。现记录一下使用过程及遇到的问题

项目方案

  1. Adobe Acrobat Pro 打开刚刚制作的pdf文件模板表单,后台读取模板,并替换值,导出即可。
  2. 通过将模板文件替换成 HTML, 并将模板文件放入数据库中,后台从数据库读取模板,并替换值,导出即可。

项目技术

方案一 采用 Adobe Acrobat Pro 软件(注意,需要考虑版权),及后端使用 itextpdf 读取模板和导出模板。
方案二 采用 freemarker 及 flying-saucer-pdf 读取html,并渲染成pdf。

方案对比

方案一: 操作简单,容错性较高。建议 复杂性的可以采用方案一。
方案二: 因为涉及模板转成html,再由html转成pdf,因此可能会存在格式失真,等问题,操作比较复杂,效果比较好。 建议简单点的 可以采用该方案。

制作模板步骤

方案一

1.  下载Adobe Acrobat pro。
2.  准备好 PDF 模板文件。
3. 使用 Adobe Acrobat pro 打开PDF模板文件,选择 表单 -》 添加或者编辑域

java 根据模板下载pdf工具类 java根据模板导出pdf_java 根据模板下载pdf工具类

方案二

1.  准备模板文件word 或者 模板pdf,使用word打开,另存为 Html
2.  将html 作为中转,使用 freemarker 渲染模板文件。

实现方式

方案一

<dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itextpdf</artifactId>
            <version>5.4.3</version>
        </dependency>
public static void main(String[] args) {
        // 模板路径
        String templatePath = "C:" + File.separator + "Users" + File.separator + "Administrator" + File.separator + "Desktop" + File.separator + "模板1.pdf";
        // 生成的新文件路径
        String savePath = "C:" + File.separator + "Users" + File.separator + "Administrator" + File.separator + "Desktop" + File.separator + "test2.pdf";
        Map<String, Object> o = new HashMap<>();
        Map<String, String> dataMap = new HashMap<>();
        Map<String, String> imageMap = new HashMap<>();
        dataMap.put("salaryYear","2020");
        dataMap.put("salaryMonth","11");
        dataMap.put("salaryDay","27");

        o.put("dataMap", dataMap);
        o.put("imageMap", imageMap);
        PdfReader reader;
        FileOutputStream out;
        ByteArrayOutputStream bos;
        PdfStamper stamper;
        try {
            com.itextpdf.text.pdf.BaseFont bf = com.itextpdf.text.pdf.BaseFont.createFont("C:" + File.separator + "Windows" + File.separator + "Fonts" + File.separator + "simsun.ttc,0", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
            Font FontChinese = new Font(bf, 5, Font.NORMAL);
            out = new FileOutputStream(savePath);// 输出流
            reader = new PdfReader(templatePath);// 读取pdf模板
            bos = new ByteArrayOutputStream();
            stamper = new PdfStamper(reader, bos);
            AcroFields form = stamper.getAcroFields();
            //文字类的内容处理
            Map<String, String> datemap = (Map<String, String>) o.get("dataMap");
            form.addSubstitutionFont(bf);
            float fontSize = 10.5f;
            for (String key : datemap.keySet()) {
                String value = datemap.get(key);
                form.setFieldProperty(key, "textfont", bf, null);
                form.setFieldProperty(key, "textsize", fontSize, null);
                form.setField(key, value);
            }
            //图片类的内容处理
            Map<String, String> imgmap = (Map<String, String>) o.get("imageMap");
            for (String key : imgmap.keySet()) {
                String value = imgmap.get(key);
                String imgpath = value;
                int pageNo = form.getFieldPositions(key).get(0).page;
                Rectangle signRect = form.getFieldPositions(key).get(0).position;
                float x = signRect.getLeft();
                float y = signRect.getBottom();
                //根据路径读取图片
                Image image = Image.getInstance(imgpath);
                //获取图片页面
                PdfContentByte under = stamper.getOverContent(pageNo);
                //图片大小自适应
                image.scaleToFit(signRect.getWidth(), signRect.getHeight());
                //添加图片
                image.setAbsolutePosition(x, y);
                under.addImage(image);
            }
            stamper.setFormFlattening(true);// 如果为false,生成的PDF文件可以编辑,如果为true,生成的PDF文件不可以编辑
            stamper.close();
            Document doc = new Document();
            Font font = new Font(bf, 32);
            PdfCopy copy = new PdfCopy(doc, out);
            doc.open();
            PdfImportedPage importPage = copy.getImportedPage(new PdfReader(bos.toByteArray()), 1);
            copy.addPage(importPage);
            doc.close();

        } catch (IOException e) {
            System.out.println(e);
        } catch (DocumentException e) {
            System.out.println(e);
        }
    }

方案二

<dependency>
        <groupId>org.freemarker</groupId>
        <artifactId>freemarker</artifactId>
        <version>${freemarker.version}</version>
        </dependency>
        <dependency>
            <groupId>org.xhtmlrenderer</groupId>
            <artifactId>flying-saucer-pdf</artifactId>
            <version>9.0.9</version>
        </dependency>
        <!-- parse DOM -->
        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.11.3</version>
        </dependency>
@SneakyThrows
    @Test
    public void exportHtmlToPdf() {
 Map<String, Object> businessData = new HashMap<>();
        String formatContent = formatContent(“html”, businessData);
        //pdf生成
        DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();

        ITextRenderer iTextRenderer = new ITextRenderer();
        iTextRenderer.setDocumentFromString(formatContent);
        //设置字体  其他字体需要添加字体库
        ITextFontResolver fontResolver = iTextRenderer.getFontResolver();
        fontResolver.addFont("C:" + File.separator + "Windows" + File.separator + "Fonts" + File.separator + "simsun.ttc", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
        fontResolver.addFont(this.getClass().getClassLoader().getResource("fonts/wingdings2.ttf").getPath(), BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
        fontResolver.addFont(this.getClass().getClassLoader().getResource("fonts/simsunb.ttf").getPath(), BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);

        iTextRenderer.setDocument(builder.parse(new ByteArrayInputStream(formatContent.getBytes())), null);
        iTextRenderer.layout();
        //生成PDF
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        iTextRenderer.createPDF(baos);

        String savePath = "C:" + File.separator + "Users" + File.separator + "Administrator" + File.separator + "Desktop" + File.separator + "test.pdf";

        FileOutputStream fos = new FileOutputStream(savePath);

        fos.write(baos.toByteArray());
        baos.close();
        fos.flush();
        fos.close();
}

private String formatContent(String content, Map<String, Object> businessData) {

        String formatContent = "";
        try {
            Configuration configuration = freeMarkerConfigurationFactory.createConfiguration();
            StringTemplateLoader stringLoader = new StringTemplateLoader();
            stringLoader.putTemplate("sendMessageTemplate", content);
            configuration.setTemplateLoader(stringLoader);
            freemarker.template.Template temp = null;
            temp = configuration.getTemplate("sendMessageTemplate", "utf-8");
            StringWriter stringWriter = new StringWriter(2048);
            temp.process(businessData, stringWriter);
            formatContent = stringWriter.toString();
            stringWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TemplateException e) {
            e.printStackTrace();
        }

        // 自动修复 html 格式
//        return Jsoup.parse(formatContent).html();
        return formatContent;
        }

常见问题

方案一: 常见问题

  1. 多行文本能不能自动换行
  2. 单选框如何勾选

3 . 生成后文件变得很大,模板 100KB,下载后,变成 7M多

问题原因
将字体打包进了原文件中
解决方案

引入字体库

<dependency>
       <groupId>com.itextpdf</groupId>
       <artifactId>itext-asian</artifactId>
       <version>5.2.0</version>
   </dependency>
   
com.itextpdf.text.pdf.BaseFont bf = com.itextpdf.text.pdf.BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);

4 . 如何获取单元格默认字体大小

com.itextpdf.text.pdf.BaseFont bf = com.itextpdf.text.pdf.BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
            reader = new PdfReader(url);// 读取pdf模板
            bos = new ByteArrayOutputStream();
            stamper = new PdfStamper(reader, bos);
            stamper.setFullCompression();
            AcroFields form = stamper.getAcroFields();
            //文字类的内容处理
            form.addSubstitutionFont(bf);
            float fontSize = 10.5f;
            for (Map.Entry<String, Object> entry : businessData.entrySet()) {
                form.setFieldProperty(entry.getKey(), "textfont", bf, null);
                if(form.getFields().get(entry.getKey()) != null
                        && form.getFields().get(entry.getKey()).getValue(0).get(new PdfName("DA")) != null){
                    String numbers = getNumbers(form.getFields().get(entry.getKey()).getValue(0).get(new PdfName("DA")) + "");
                    if(!"0".equals(numbers)){
                        fontSize = CommonUtils.evalFloat(numbers,0.0F);
                    } else {
                        fontSize = 10.5f;
                    }
                }
                form.setFieldProperty(entry.getKey(), "textsize", fontSize, null);
                form.setField(entry.getKey(), entry.getValue() != null ? entry.getValue().toString() : "");
            }
            // 如果为false,生成的PDF文件可以编辑,如果为true,生成的PDF文件不可以编辑
            stamper.setFormFlattening(true);
            stamper.close();

public String getNumbers(String content) {
        Matcher matcher = Pattern.compile("\\d+").matcher(content);
        while (matcher.find()) {
            return matcher.group(0);
        }
        return "";
    }

方案二: 常见问题

1.   元素类型 "meta" 必须由匹配的结束标记 "</meta>" 终止。

问题原因
flying saucer对xml格式要求很严格,因此必须是完整的格式。(尝试使用过 Jsoup.parse(formatContent).html() 修复格式,但是发现 这个问题还是不能修复,目前只能手动改)
问题描述

org.xhtmlrenderer.util.XRRuntimeException: Can't load the XML resource (using TRaX transformer). org.xml.sax.SAXParseException; lineNumber: 55; columnNumber: 4; 元素类型 "meta" 必须由匹配的结束标记 "</meta>" 终止。

	at org.xhtmlrenderer.resource.XMLResource$XMLResourceBuilder.createXMLResource(XMLResource.java:192)
	at org.xhtmlrenderer.resource.XMLResource.load(XMLResource.java:75)
	at org.xhtmlrenderer.pdf.ITextRenderer.setDocumentFromString(ITextRenderer.java:165)
	at org.xhtmlrenderer.pdf.ITextRenderer.setDocumentFromString(ITextRenderer.java:160)

修改前

<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312" >
<meta name="Generator" content="Microsoft Word 15 (filtered)" >
</head>

修改后

<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
<meta name="Generator" content="Microsoft Word 15 (filtered)" />
</head>
2 .  不能识别  
org.xhtmlrenderer.util.XRRuntimeException: Can't load the XML resource (using TRaX transformer). org.xml.sax.SAXParseException: The entity "nbsp" was referenced, but not declared.
org.xhtmlrenderer.resource.XMLResource$XMLResourceBuilder.createXMLResource(XMLResource.java:191)
org.xhtmlrenderer.resource.XMLResource.load(XMLResource.java:71)

解决方案:
将 替换为以下

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  1. 支持中文
//设置字体  其他字体需要添加字体库
        ITextFontResolver fontResolver = iTextRenderer.getFontResolver();
        fontResolver.addFont("C:"+ File.separator +"Windows"+ File.separator +"Fonts"+ File.separator +"simsun.ttc", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);

注意!!!
页面中字体不能使用中文,需要使用英文名称,而且是大小写敏感的!例如宋体的英文名称是 SimSun(注意不是simsun!,首字母都是大写的)

错误写法:font-family:宋体 或者  font-family:simsun

  正确写法:font-family:SimSun 或者 font-family:SimHei

如果生成的pdf中文不显示或者乱码,请确认如下信息:

确保页面中所有内容都指定了字体,最好能指定 body {font-family:…},以防止漏网之鱼。

确保上述所有字体均通过addFont加入,字体名称错误或者字体不存在会抛出异常,很方便,但是没导入的字体不会有任何提示。

确保字体名称正确,不使用中文,大小写正确。

确保html标签都正确,简单的方法是所有内容都去掉,随便写几个中文看看能否正常生成,如果可以,在认真检查html标签,否则再次检查上述几条。

还有就是中文换行的问题了,带有中文而且文字较多存在换行情况时,需要给table加入样式:

table-layout:fixed,然后表格中的td使用%还指定td的宽度。

  1. 生成的模板 右边被截断,显示不全
    html 页面不能不要大于A4格式大小 可以通过 css 中 @page{**} 设置模板大小