前后端生成PDF合同

概述过程: 前端绘制html模板,后端拿到html模板使用FreeMaker生成ftl文件,如有图表则引入JFreeChart,后端代码处理后生成PDF并下载


前言

展业过程中,会涉及多方合同的签订,签订的合同又会因为主体,材料等差异进行定制化。 生成合同的方案有很多种,经过尝试后,推荐以下:


1 前端工作内容

  1.1 根据合同文档编写html文件

代码先上:

<!DOCTYPE html>
<html lang="en">
<head>
    <title></title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: SimSun;
        }

        section {
            display: block;
            margin: 20px 10px;
        }

        .title {
            text-align: center;
        }

        .preface p{
            line-height: 30px;
        }

        .contract p {
            padding:5px 0px;
        }


        .preface p.content {
            text-indent: 2em;
        }

        .preface h3.content {
            text-indent: 2em;
        }

        section>table {
            table-layout: fixed;
            width: 100%;
            margin: 20px 0px;
            text-align: left;
            word-wrap: break-word;
            border-collapse: collapse;
        }

        section table td {
            padding: 10px;
        }

        td>div,
        td>h4 {
            line-height: 30px;
        }

        section table tr {
            page-break-inside: avoid;
            page-break-after: auto;
        }

        .new_page {
            page-break-before: always;
        }

    </style>
</head>

<body>
<section class="title">
    <h2>设备购销合同</h2>
</section>
<section class="preface">
    <p>合同编号:</p>
    <p> 甲方(供方):</p>
    <p> 乙方(需方):<span style="text-decoration: underline;">${officeName}</span></p>
    <p>
        根据《中华人民共和国民法典》及相关法律法规,本着平等自愿、等价有偿、诚实信用的原则,甲乙双方协商一致达成如下协议:
    </p>
    <h3>第一条 货物品名、数量、单价、金额、合同总价</h3>
</section>
<section class="count-info">
    <table border="1" cellspacing="0" cellpadding="0">
        <tr>
            <td>序号</td>
            <td>物料编码</td>
            <td>产品铭牌</td>
            <td>产品名称</td>
            <td>规格型号</td>
            <td>数量</td>
            <td>销售单价(元/瓦)</td>
            <td>销售总价(元)</td>
        </tr>
        <#list tableData as ad>
            <tr>
                <td>${ad.orderNo}</td>
                <td>${ad.itemCode}</td>
                <td>${ad.itemBrand}</td>
                <td>${ad.itemName}</td>
                <td>${ad.itemSpec}</td>
                <td>${ad.number}</td>
                <td>${ad.unitPrice}</td>
                <td>${ad.totalPrice}</td>
            </tr>
        </#list>
        <tr>
            <td colspan="8">
                合同总价:${totalPriceString}元,大写人民币 ${totalPriceChinese}(含税)
            </td>
        </tr>
        <tr>
            <td colspan="8">
                <div>备注:1、合同货物符合本合同产品规格书约定的质量标准。</div>
                <div>2、合同计价以设备标称功率为准。</div>
            </td>
        </tr>
    </table>
</section>
<section class="contract" style="margin-bottom:0;padding:0;">
    <h3>第二条 付款方式</h3>
    <p>2.1 付款方式:本合同签订之日起【七】个自然日内乙方向甲方支付合同总额的100%。</p>
    <p>2.2 除另有约定,甲乙双方各自承担因执行本合同所发生的银行费用及各项税费。</p>
    <h3>
        第三条 风险转移及所有权保留
    </h3>
    <p>3.1 本合同货物之毁损、灭失的风险自货物签收之日起由乙方承担。</p>
    <p>3.2 在乙方按本合同要求付清全部合同货款之后,货物所有权转移给乙方。</p>
    <p>3.3 在乙方未按本合同要求付清全部合同货款之前,甲方保留本合同未付款部分标的物之所有权。此时未经甲方书面同意,乙方不得擅自转卖本合同标的物,也不得将本合同标的物以任何形式为第三方设定担保物权。</p>
    <h3>
        第四条 设备质量要求
    </h3>
    <p>
        4.1 质量要求:按厂家的认证标准。
    </p>
  
 
    <p class="content" style="margin-bottom:0;padding:0;">(以下无正文)</p>

</section>
<section class="new_page" >
    <p class="content">(本页无正文,为《设备购销合同》签字盖章页)</p>
    <table style="margin:0">
        <tr>
            <td>甲方:</td>
            <td>乙方(盖章):</td>
        </tr>
        <tr>
            <td>法定代表人: </td>
            <td>法定代表人:</td>
        </tr>
    </table>
    <table border="1" cellspacing="0" cellpadding="0">
        <tr>
            <td>联系人:</td>
            <td>联系人: ${receiverContactName}</td>
        </tr>
        <tr>
            <td>电话:</td>
            <td>电话:${receiverContactPhone}</td>
        </tr>
        <tr>
            <td>电子邮件</td>
            <td>电子邮件:</td>
        </tr>
        <tr>
            <td>地址:</td>
            <td>地址:${receiverAddress}</td>
        </tr>
        <tr>
            <td>邮编:</td>
            <td>邮编:</td>
        </tr>
        <tr>
            <td>开户银行: </td>
            <td>开户银行:${bank}</td>
        </tr>
        <tr>
            <td>账号: 88</td>
            <td>账号:${bankAccount}</td>
        </tr>
        <tr>
            <td>纳税人识别号:</td>
            <td>纳税人识别号:${unifiedSocialCreditCode}</td>
        </tr>
        <tr>
            <td>
                <span style="margin-left:70px">年</span><span style="margin-left:40px">月</span><span
                        style="margin-left:40px">日</span>
            </td>
            <td style="text-align: left;">
                <span style="margin-left:70px">年</span><span style="margin-left:40px">月</span><span
                        style="margin-left:40px">日</span>
            </td>
        </tr>
    </table>
</section>
<section class="new_page">
    <h2 style="text-align: center;">收货确认单</h2>
</section>
<section>
    <table border="1" cellspacing="0" cellpadding="0">
        <tr>
            <td colspan="4">发货方</td>
            <td colspan="4">
            </td>
        </tr>
        <tr>
            <td colspan="4">
                收货方
            </td>
            <td colspan="4">
                ${officeName}
            </td>
        </tr>
        <tr>
            <td colspan="4">
                收货地址
            </td>
            <td colspan="4">
                ${receiverAddress}
            </td>
        </tr>
        <tr>
            <td colspan="4">
                收货联系人/联系方式
            </td>
            <td colspan="4">
                ${receiverContactName}/${receiverContactPhone}
            </td>
        </tr>
        <tr>
            <td colspan="8">
                产品清单
            </td>
        </tr>
        <tr>
            <td>
                序号
            </td>
            <td>
                物料编码
            </td>
            <td>
                产品品牌
            </td>
            <td>
                产品名称
            </td>
            <td>
                规格型号
            </td>
            <td>
                数量
            </td>
            <td>
                销售单价
            </td>
            <td>
                销售总价
            </td>
        </tr>
        <#list tableData as ad>
            <tr>
                <td>${ad.orderNo}</td>
                <td>${ad.itemCode}</td>
                <td>${ad.itemBrand}</td>
                <td>${ad.itemName}</td>
                <td>${ad.itemSpec}</td>
                <td>${ad.number}</td>
                <td>${ad.unitPrice}</td>
                <td>${ad.totalPrice}</td>
            </tr>
        </#list>
        <tr>
            <td colspan="6">
                <div>□收货方已收到上述产品,对产品外观、数量、单价及总价确认无误</div>
                <div>□到货产品的数量不符(勾选此项请添加描述和照片,并在2日内联系发货方)</div>
                <div>□到货产品的规格不符(勾选此项请添加描述和照片,并在2日内联系发货方)</div>
                <div>□到货产品的外包装不符(勾选此项请添加描述和照片,并在2日内联系发货方)</div>
                <div>□到货产品损坏或缺陷(勾选此项请另外提交详细的说明和照片,并在5日内联系发货方)</div>
                <div>□其他:______________</div>
            </td>
            <td colspan="2" style="vertical-align: top;">
                不符合项详细描述:
            </td>
        </tr>
        <tr style="height:120px">
            <td colspan="2">
                收货人/签收人签名并捺印
            </td>
            <td colspan="2">

            </td>
            <td rowspan="2" colspan="2">
                收货方公章
            </td>
            <td colspan="2" rowspan="2">

            </td>
        </tr>
        <tr>
            <td colspan="4">收货日期</td>
        </tr>
        <tr>
            <td colspan="8">
                <h4>注意:</h4>
                <h4>收货方应真实、准确、全面填写本表,签字并盖章后3日内将原件邮寄回发货方。</h4>
                <h4>收货方负责建立相应收发统计数据,并保留签收货物的凭证。否则,发生争议时发货方有权按照发货时的金额主张赔偿。</h4>
                <h4>其他:______________</h4>
            </td>
        </tr>
        <tr>
            <td colspan="8">
                <h4>备注</h4>
            </td>
        </tr>
    </table>
</section>
</body>

</html>

编写注意点:

1、按照.html文件格式进行编写

2、必须使用原生html标签:

文本标签推荐使用:p、h1~5、div、span

表格标签:table、tr、th、td

3、踩坑标签:(后续开发中如有遇到其他标签问题,可继续添加提示)

checkbox 复选框使用无效

input 为自闭合标签,但是FreeMaker识别有误,会报错提示input应该存在</input>,建议采用div绘制作为替代,或者复制下述示例的 ”□“

4、变量使用 ${变量名}进行插值

5、表格数据渲染须注意格式,进行变量list的循环渲染 <#list tableData as ad> <tr> <td>${ad.orderNo}</td> <td>${ad.itemCode}</td> <td>${ad.itemBrand}</td> <td>${ad.itemName}</td> <td>${ad.itemSpec}</td> <td>${ad.number}</td> <td>${ad.unitPrice}</td> <td>${ad.totalPrice}</td> </tr> </#list>

6、样式部分尽可能使用css基础样式,scss及其他C3样式可能会存在识别问题

7、需要另起一页排布的内容时,可以在样式中添加"page-break-before: always;"进行解决

8、表格防止被撕裂的样式

table tr { page-break-inside: avoid; page-break-after: auto; }

9、为确保合同中汉字的展示无误,须在指定字体

body { font-family: SimSun;//宋体 }

10、其他tips:

line-height的css样式可能造成后续表格撕裂问题,可不使用;

如果合同需另起一页,注意上一页的最后一个标签(无论是否存在子盒子),须调整样式为

  {

    margin-bottm:0;

    padding-bottom:0;

  }

  以防止,由于内外边距造成的多出空白页的情况;

更多tips,持续维护更新……

2 后端工作内容

获取前端的.html文件,生成.ftl文件,代码处理生成PDF。

 2.1 工具

FreeMarker是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 是一个Java类库。

JFreeChart 由 David Gilbert 于 2000 年创立, 是 Java 开发人员中使用最广泛的图表库。允许创建各种交互式和非交互式图表;可以广泛地定制; 它允许修改图表项目的颜色和绘制,图例,线条或标记的样式。 它会自动绘制轴刻度和图例。可以创建折线图,条形图,面积图,散点图,饼图,甘特图和各种专用图,例如风向图或气泡图。它支持多种输出格式,包括 PNG,JPEG,PDF 和 SVG。

2.2 依赖引入

<!-- JfreeCharts -->
<dependency>
    <groupId>org.jfree</groupId>
    <artifactId>jfreechart</artifactId>
    <version>1.5.4</version>
</dependency>
  2.3 美化工具类ChartConfUtils.java
public class ChartConfUtils {
    private static final String NO_DATA_MSG = "数据加载失败";
    private static final Font FONT = new Font("宋体", Font.PLAIN, 12);
    // 颜色
    public static Color[] CHART_COLORS = {
            new Color(255,188,117),new Color(31,129,188), new Color(92,92,97), new Color(144,237,125),
            new Color(153,158,255), new Color(255,117,153), new Color(253,236,109), new Color(128,133,232),
            new Color(158,90,102),new Color(255, 204, 102) };

    static {
        setChartTheme();
    }

    public ChartConfUtils() {
    }

    /**
     * 中文主题样式 解决乱码
     */
    public static void setChartTheme() {
        // 设置中文主题样式 解决乱码
        StandardChartTheme chartTheme = new StandardChartTheme("CN");
        // 设置标题字体
        chartTheme.setExtraLargeFont(FONT);
        // 设置图例的字体
        chartTheme.setRegularFont(FONT);
        // 设置轴向的字体
        chartTheme.setLargeFont(FONT);
        chartTheme.setSmallFont(FONT);
        chartTheme.setTitlePaint(new Color(51, 51, 51));
        chartTheme.setSubtitlePaint(new Color(85, 85, 85));

        chartTheme.setLegendBackgroundPaint(Color.WHITE);// 设置标注
        chartTheme.setLegendItemPaint(Color.BLACK);//
        chartTheme.setChartBackgroundPaint(Color.WHITE);

        Paint[] OUTLINE_PAINT_SEQUENCE = new Paint[] { Color.WHITE };
        // 绘制器颜色源
        DefaultDrawingSupplier drawingSupplier = new DefaultDrawingSupplier(CHART_COLORS, CHART_COLORS, OUTLINE_PAINT_SEQUENCE,
                DefaultDrawingSupplier.DEFAULT_STROKE_SEQUENCE, DefaultDrawingSupplier.DEFAULT_OUTLINE_STROKE_SEQUENCE,
                DefaultDrawingSupplier.DEFAULT_SHAPE_SEQUENCE);
        chartTheme.setDrawingSupplier(drawingSupplier);

        chartTheme.setPlotBackgroundPaint(Color.WHITE);// 绘制区域
        chartTheme.setPlotOutlinePaint(Color.WHITE);// 绘制区域外边框
        chartTheme.setLabelLinkPaint(new Color(8, 55, 114));// 链接标签颜色
        chartTheme.setLabelLinkStyle(PieLabelLinkStyle.CUBIC_CURVE);

        chartTheme.setAxisOffset(new RectangleInsets(5, 12, 5, 12));
        chartTheme.setDomainGridlinePaint(new Color(192, 208, 224));// X坐标轴垂直网格颜色
        chartTheme.setRangeGridlinePaint(new Color(192, 192, 192));// Y坐标轴水平网格颜色

        chartTheme.setBaselinePaint(Color.WHITE);
        chartTheme.setCrosshairPaint(Color.BLUE);// 不确定含义
        chartTheme.setAxisLabelPaint(new Color(51, 51, 51));// 坐标轴标题文字颜色
        chartTheme.setTickLabelPaint(new Color(67, 67, 72));// 刻度数字
        chartTheme.setBarPainter(new StandardBarPainter());// 设置柱状图渲染
        chartTheme.setXYBarPainter(new StandardXYBarPainter());// XYBar 渲染

        chartTheme.setItemLabelPaint(Color.black);
        chartTheme.setThermometerPaint(Color.white);// 温度计

        ChartFactory.setChartTheme(chartTheme);
    }

    /**
     * 必须设置文本抗锯齿
     */
    public static void setAntiAlias(JFreeChart chart) {
        chart.setTextAntiAlias(false);

    }

    /**
     * 设置图例无边框,默认黑色边框
     */
    public static void setLegendEmptyBorder(JFreeChart chart) {
        LegendTitle legend = chart.getLegend();// 图例对象
        legend.setFrame(new BlockBorder(Color.WHITE));
        legend.setPosition(RectangleEdge.TOP);// 图例所在位置(上、下、左、右)
        legend.setVisible(true);// 是否显示图例
        legend.setItemFont(FONT);// 图例大小
    }
}

  2.4 图表生成

  工具类JfreeChartUtil 用于根据数据集生成图表并保存图片至相对路径template/img


public class JfreeChartUtil {

    /**
     * 图片保存的相对路径地址
     */
    public static final String IMG_PATH = "template/img";

    /**
     * 依据数据生成图表并导出图片至classpath路径
     */
    public static void generateChartsAndExportJpg(String title,
                                                  String xAxisLabel,
                                                  String yAxisLabel,
                                                  DefaultCategoryDataset dataset,
                                                  String outputJpgName) {

        // 主题样式设置
        ChartConfUtils.setChartTheme();
        // 创建JFreeChart对象
        JFreeChart chart = ChartFactory.createBarChart(
                // 图标题
                title,
                // x轴标题
                xAxisLabel,
                // y轴标题
                yAxisLabel,
                //数据集
                dataset,
                //图表方向
                PlotOrientation.VERTICAL,
                true, true, false);
        // 设置图例
        ChartConfUtils.setLegendEmptyBorder(chart);


        //将生成的图片保存到本地
        String contextPath = Objects.requireNonNull(JfreeChartUtil.class.getClassLoader().getResource(IMG_PATH)).getPath();
        String filePath = contextPath + "/"+outputJpgName;
        File lineChart = new File(filePath);
        //生成文件
        try {
            ChartUtils.saveChartAsJPEG(lineChart, chart,1080,540);
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("保存图片"+outputJpgName+"异常");
        }
    }
}

  要在HTML中使用<img>标签插入图片一般有两种方式:

  指定图片文件路径URL

    <img src="path/to/image.jpg" alt="路径插入" width="500" height="600">

  指定图片以Base64 格式插入

    <img src="data:image/jpeg;base64,/9j/4AAQSkZJRgABRU..." alt="base64插入">

  对于前端展示页面,通常使用第一种方式,渲染更快速也更直观;然而在生成模板过程中我们需要用到第二种方式。在HTML 转PDF 的过程中, flying-saucer无法识别图片路径,因此我们需要将图片转为base64格式 。

图片转base64 工具类 ImgBase64Util

/**
 * ImgBase64Util 图片转Base64 工具类
 */
public class ImgBase64Util {

    public final static String IMG_PRE = "data:image/jpg;base64,";

    /**
     * 本地图片转换成base64字符串
     * @param imgFile 图片本地路径
     */
    public static String imageToBase64ByLocal(String imgFile) {// 将图片文件转化为字节数组字符串,并对其进行Base64编码处理


        InputStream fis = null;
        byte[] data = null;

        // 读取图片字节数组
        try {
            fis = new FileInputStream(imgFile);
            //新的 byte 数组输出流,缓冲区容量1024byte
            ByteArrayOutputStream bos = new ByteArrayOutputStream(1024);
            //缓存
            byte[] b = new byte[1024];
            int n;
            while ((n = fis.read(b)) != -1) {
                bos.write(b, 0, n);
            }
            fis.close();
            //改变为byte[]
            data = bos.toByteArray();

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return IMG_PRE+DatatypeConverter.printBase64Binary(data);
    }

    /**
     * base64字符串转换成图片
     * @param imgStr      base64字符串
     * @param imgFilePath 图片存放路径
     */
    public static boolean base64ToImage(String imgStr, String imgFilePath) { // 对字节数组字符串进行Base64解码并生成图片

        if (StringUtils.isEmpty(imgStr)) {
            // 图像数据为空
            return false;
        }

        OutputStream out = null;
        try {

            byte[] b = DatatypeConverter.parseBase64Binary(imgStr);
            for (int i = 0; i < b.length; ++i) {
                if (b[i] < 0) {
                    // 调整异常数据
                    b[i] += 256;
                }
            }

            out = new FileOutputStream(imgFilePath);
            out.write(b);
            out.flush();
            return true;
        } catch (Exception e) {
            return false;
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 数据集转base64图片链接
     * @param title 图标题
     * @param xAxisLabel x轴
     * @param yAxisLabel y轴
     * @param dataset 数据集
     * @param jpgName 图名称.jpg
     * @return 图片的base64链接
     */
    public static String genBase64ImageByChart(String title,
                                               String xAxisLabel,
                                               String yAxisLabel,
                                               DefaultCategoryDataset dataset,
                                               String jpgName){
        // 生成图表并保存图片至本地路径
        JfreeChartUtil.generateChartsAndExportJpg(title,xAxisLabel,yAxisLabel,dataset,jpgName);
        // 获取图表jpg相对路径
        String imgRetPath = JfreeChartUtil.IMG_PATH +"/"+ jpgName;
        // 获取图表jpg绝对路径
        String imgAbsPath = Objects.requireNonNull(ImgBase64Util.class.getClassLoader().getResource(imgRetPath)).getPath();
        // 将图片转为base64返回
        return imageToBase64ByLocal(imgAbsPath);
    }

}

  调用示例

  图表在FTL 模板中的示例:

<section>
    <img src="${recruitStudentImg}" alt="recruitStudentImg" style="width: 90%;margin-left: 5%;margin-right: 15%"/>
</section>

  在java中的调用示例:

HashMap<String, Object> dataMap = new HashMap<>();
// 创建数据
DefaultCategoryDataset dataset = new DefaultCategoryDataset();
dataset.addValue(306, "女", "高一");
dataset.addValue(295, "女", "高二");
dataset.addValue(285, "女", "高三");
dataset.addValue(282, "男", "高一");
dataset.addValue(301, "男", "高二");
dataset.addValue(296, "男", "高三");
// 获取生成的图表
String recruitStudentImg = ImgBase64Util.genBase64ImageByChart("招生人数统计", "年份", "人数", dataset, "recruit_student.jpg");
dataMap.put("recruitStudentImg",recruitStudentImg);
2.5 使用 Flying-saucer-itext5 生成 PDF
  2.5.1什么是 Flying-saucer-itext5

  iText是著名的开放源码站点sourceforge的一个项目,是用于生成PDF文档的一个Java类库。通过iText不仅可以生成PDF或RTF的文档,而且可以将XML、HTML文件转化为PDF文件。 flying sauser 基于iText并做了封装,能进一步解析HTML和CSS。

  2.5.2依赖引入
<!-- html转PDF -->
<dependency>
    <groupId>org.xhtmlrenderer</groupId>
    <artifactId>flying-saucer-pdf-itext5</artifactId>
    <version>9.1.22</version>
</dependency>
  2.5.3 HTML转PDF

  这里使用到工具类 HtmlToPDFUtil


@Component
public class HtmlToPDFUtil {

    /**
     * 使用 freemarker 渲染 HTML
     *
     * @param templateFileName HTML模板路径
     * @param data   渲染的参数
     * @return 返回渲染后的html代码
     */
    public static String render(String templateFileName, Object data) throws Exception {
        Configuration cfg = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
        // 指定FreeMarker模板文件的位置
        cfg.setClassForTemplateLoading(PdfTemplateUtil.class,"/template");
        Template template = cfg.getTemplate(templateFileName, "UTF-8");
        StringWriter writer = new StringWriter();

        // 将数据输出到html中
        template.process(data, writer);
        writer.flush();
        return writer.toString();
    }

    /**
     * 根据html生成pdf的base64格式
     */
    public static String getPDFBase64ByHtml(String html) throws Exception {
        //构建字节输出流
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ITextRenderer renderer = new ITextRenderer();
        // 解决base64图片支持问题
        SharedContext sharedContext = renderer.getSharedContext();
        sharedContext.setReplacedElementFactory(new B64ImgReplacedElementFactory());
        sharedContext.getTextRenderer().setSmoothingThreshold(0);
        ITextFontResolver fontResolver = renderer.getFontResolver();
        // html中设置的字体样式需要参考此处debug后fontResolver的fontFamily的数据的key
        //指定文件字体添加到PDF库,指定字体不作为内部字体,而是外部字体被加载
        fontResolver.addFont("/template/font/simsun.ttc,0", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
        renderer.setDocumentFromString(html);
        renderer.layout();
        renderer.createPDF(baos);

        return Base64.encodeBytes(baos.toByteArray());
    }


    /**
     * 根据pdf的base64格式和路径生成pdf文件
     *  @param base64 pdf的base64格式
     */
    public static ByteArrayOutputStream base64ToPDF(String base64) {
        byte[] bytes = Base64.decode(base64);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try {
            out.write(bytes);
            out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return out;
    }



    /**
     * 合并添加水印 页眉和页脚 水印的方法 (可根据需求自行改动封装)
     *
     * @param pdfBase64        PDF的Base64格式
     * @param isAddWatermark   是否添加水印
     * @param waterMarkName    水印文字
     * @param isAddPageNumbers 是否添加页眉
     * @param headerText       页眉名称
     * @param isAddLogoImg     是否添加Logo
     * @param logoPath         logo图片路径
     * @param newWidth         图片大小宽度如 141
     * @param newHeight        图片大小高度如30
     * @param absoluteX        图片相对位置 如20
     * @param absoluteY        图片相对位置 如-30
     */
    public static String pdfAddWaterHeaderLogo(String pdfBase64
            , boolean isAddWatermark, String waterMarkName
            , boolean isAddPageNumbers, String headerText
            , boolean isAddLogoImg, String logoPath, float newWidth, float newHeight, float absoluteX, float absoluteY
            , boolean isAddWatermarkLogo, String watermarkLogoPath
            , boolean isAddBgImage, String bgImagePath
    ) {
        try {
            byte[] bytes = Base64.decode(pdfBase64);
            /**
             * PdfReader是iText库中的PDF解析器,用于读取和解析PDF文件。通过PdfReader对象,可以获取PDF文件的各种属性和内容,
             * 如总页数、页面尺寸、文本内容等。它提供了方法来打开和关闭PDF文件,并可以从文件路径、InputStream或字节数组等多种方式加载PDF文件。
             *
             * PdfStamper是iText库中的PDF编辑器,用于修改和编辑PDF文件。通过PdfStamper对象,
             * 可以在PDF文件中添加文本、图片、表单域等元素,修改页面内容、添加注释、加密文档等操作。它基于已解析的PdfReader对象,可以将修改后的内容写入新的PDF文件或覆盖原始文件。
             */
            PdfReader reader = new PdfReader(bytes);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            PdfStamper stamper = new PdfStamper(reader, bos);

            int total = reader.getNumberOfPages();

            if (isAddBgImage) {
                addBackgroundImage(stamper, reader, bgImagePath);
            }

            if (isAddLogoImg) {
                addLogoImg(stamper, reader, logoPath, newWidth, newHeight, absoluteX, absoluteY);
            }


            if (isAddPageNumbers) {
                addPageNumbers(stamper, reader, headerText);
            }

            // 添加水印放到最后执行 否则会将页眉页脚进行透明度设置
            if (isAddWatermark) {
                addWatermark(stamper, reader, waterMarkName);
            }

            if (isAddWatermarkLogo) {
                addWatermarkImage(stamper, reader, watermarkLogoPath);
            }


            stamper.close();
            reader.close();
            return Base64.encodeBytes(bos.toByteArray());
        } catch (IOException | DocumentException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 添加水印
     *
     * @param stamper       PdfStamper对象
     * @param reader        PdfReader对象
     * @param waterMarkName 水印文字
     */
    private static void addWatermark(PdfStamper stamper, PdfReader reader, String waterMarkName) throws DocumentException, IOException {
        BaseFont baseFont = BaseFont.createFont("/template/font/simsun.ttc,0", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
        Rectangle pageRect;
        PdfGState gs = new PdfGState();
        // 填充的透明度
        gs.setFillOpacity(0.1f);
        // 描边的透明度
        gs.setStrokeOpacity(0.1f);

        JLabel label = new JLabel();
        FontMetrics metrics;
        int textH = 0;
        int textW = 0;
        label.setText(waterMarkName);
        metrics = label.getFontMetrics(label.getFont());
        textH = metrics.getHeight();
        textW = metrics.stringWidth(label.getText());

        PdfContentByte under;
        for (int i = 1; i <= reader.getNumberOfPages(); i++) {
            pageRect = reader.getPageSizeWithRotation(i);
            under = stamper.getOverContent(i);
            under.saveState();
            under.setGState(gs);
            under.beginText();
            under.setFontAndSize(baseFont, 20);
            //水印颜色
            under.setColorFill(BaseColor.BLACK);

            // 水印文字成30度角倾斜
            for (int height = -5 + textH; height < pageRect.getHeight(); height = height + textH * 4) {
                for (int width = -5 + textW; width < pageRect.getWidth() + textW; width = width + textW * 3) {
                    under.showTextAligned(Element.ALIGN_LEFT, waterMarkName, width - textW, height - textH, 30);
                }
            }
            // 添加水印文字
            under.endText();
        }
    }

    /**
     * 添加页眉 (页脚)
     *
     * @param stamper PdfStamper对象
     * @param reader  PdfReader对象
     * @throws DocumentException
     * @throws IOException
     */
    private static void addPageNumbers(PdfStamper stamper, PdfReader reader, String headerText) throws DocumentException, IOException {
        int total = reader.getNumberOfPages();
        for (int i = 1; i <= total; i++) {
            Rectangle pageRect = reader.getPageSizeWithRotation(i);
            PdfContentByte content = stamper.getOverContent(i);

            BaseFont baseFont = BaseFont.createFont("/template/font/simsun.ttc,0", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
            content.setFontAndSize(baseFont, 20);

            ColumnText.showTextAligned(content, Element.ALIGN_CENTER, new Phrase(headerText, new Font(baseFont, 10)),
                    pageRect.getWidth() / 2, pageRect.getTop() - 20, 0);

            ColumnText.showTextAligned(content, Element.ALIGN_CENTER, new Phrase("页码 " + i + " / " + total, new Font(baseFont, 10)),
                    pageRect.getRight()-50, pageRect.getBottom() + 10, 0);

            ColumnText.showTextAligned(content, Element.ALIGN_CENTER, new Phrase("Copyright © 2023 HollowStars. All rights reserved.", new Font(baseFont, 10)),
                    (pageRect.getLeft() + pageRect.getRight()) / 2, pageRect.getBottom() + 10, 0);

        }
    }



    /**
     * 添加图片logo
     *
     * @param stamper   PdfStamper对象
     * @param reader    PdfReader对象
     * @param logoPath  图片路径
     * @param newWidth  图片大小宽度如 141
     * @param newHeight 图片大小高度如30
     * @param absoluteX 图片相对位置 如20
     * @param absoluteY 图片相对位置 如-30
     * @throws DocumentException
     * @throws IOException
     */
    private static void addLogoImg(PdfStamper stamper, PdfReader reader, String logoPath, float newWidth, float newHeight, float absoluteX, float absoluteY) throws DocumentException, IOException {
        for (int i = 1; i <= reader.getNumberOfPages(); i++) {
            Rectangle pageRect = reader.getPageSizeWithRotation(i);
            PdfContentByte content = stamper.getOverContent(i);

            BaseFont baseFont = BaseFont.createFont("/template/font/simsun.ttc", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
            content.setFontAndSize(baseFont, 20);

            // 添加Logo图片到页眉
            Image logoImage = Image.getInstance(logoPath);
            logoImage.scaleAbsolute(newWidth, newHeight);
            logoImage.setAbsolutePosition(pageRect.getLeft() + absoluteX, pageRect.getTop() + absoluteY);
            content.addImage(logoImage);

        }
    }
    /**
     * 添加水印图片
     *
     * @param stamper       PdfStamper对象
     * @param reader        PdfReader对象
     * @param watermarkPath 水印图片路径
     * @throws DocumentException
     * @throws IOException
     */
    private static void addWatermarkImage(PdfStamper stamper, PdfReader reader, String watermarkPath) throws DocumentException, IOException {
        Image watermarkImage = Image.getInstance(watermarkPath);
        watermarkImage.scaleToFit(400, 400); // 调整水印图片大小

        /**
         * .scaleAbsolute    .scaleToFit 这两个方法 什么区别
         * scaleAbsolute和scaleToFit是iText库中用于缩放图像的两个方法。
         *
         * scaleAbsolute(float width, float height):
         * 该方法用于将图像缩放到指定的绝对宽度和高度。
         * 参数width和height分别指定了缩放后的宽度和高度。
         * 图像将按照指定的宽度和高度进行缩放,可能会导致图像的比例失调。
         *
         * scaleToFit(float width, float height):
         * 该方法用于将图像缩放以适应指定的矩形框的大小。
         * 参数width和height指定了矩形框的宽度和高度。
         * 图像将按照比例缩放,以适应指定的矩形框,同时保持其宽高比。
         * 简而言之,scaleAbsolute方法按照指定的宽度和高度进行缩放,而scaleToFit方法会按照比例缩放以适应指定的矩形框大小。选择使用哪个方法取决于你的需求和图像的要求。
         */

        for (int i = 1; i <= reader.getNumberOfPages(); i++) {
            Rectangle pageRect = reader.getPageSizeWithRotation(i);
            PdfContentByte content = stamper.getOverContent(i);

            // 创建PdfGState对象并设置透明度
            PdfGState gState = new PdfGState();
            gState.setFillOpacity(0.1f); // 水印图片透明度 (0.0f - 1.0f,0.0f 表示完全透明,1.0f 表示完全不透明)

            content.saveState();
            content.setGState(gState);

            // 添加水印图片到页面
            float x = (pageRect.getLeft() + pageRect.getRight() - watermarkImage.getScaledWidth()) / 2;
            float y = (pageRect.getBottom() + pageRect.getTop() - watermarkImage.getScaledHeight()) / 2;
            content.addImage(watermarkImage, watermarkImage.getScaledWidth(), 0, 0, watermarkImage.getScaledHeight(), x, y);

            content.restoreState();
        }
    }

    private static void addBackgroundImage(PdfStamper stamper, PdfReader reader, String bgImagePath) throws DocumentException, IOException {
        Image backgroundImage = Image.getInstance(bgImagePath);
        /* 设置图片的位置 */
        backgroundImage.setAbsolutePosition(0, 0);
        /* 设置图片的大小 */
        backgroundImage.scaleAbsolute(595, 842);


        for (int i = 1; i <= reader.getNumberOfPages(); i++) {
            Rectangle pageRect = reader.getPageSizeWithRotation(i);
            PdfContentByte content = stamper.getOverContent(i);

            // 创建PdfGState对象并设置透明度
            PdfGState gState = new PdfGState();
            gState.setFillOpacity(0.2f);

            content.saveState();
            content.setGState(gState);

            // 添加水印图片到页面
            float x = (pageRect.getLeft() + pageRect.getRight() - backgroundImage.getScaledWidth()) / 2;
            float y = (pageRect.getBottom() + pageRect.getTop() - backgroundImage.getScaledHeight()) / 2;
            content.addImage(backgroundImage, backgroundImage.getScaledWidth(), 0, 0, backgroundImage.getScaledHeight(), x, y);

            content.restoreState();
        }
    }

}

  该工具类包含了FreeMarker渲染HTML、HTML转PDFPDF加工(添加页眉、页脚、水印、背景等)的方法。其中若HTML中<img>标签包含base64图片,需要添加额外B64ImgReplacedElementFactory支持:

public class B64ImgReplacedElementFactory implements ReplacedElementFactory {

    /**
     * 实现createReplacedElement 替换html中的Img标签
     *
     * @param c 上下文
     * @param box 盒子
     * @param uac 回调
     * @param cssWidth css宽
     * @param cssHeight css高
     * @return ReplacedElement
     */
    @Override
    public ReplacedElement createReplacedElement(LayoutContext c, BlockBox box, UserAgentCallback uac,
                                                 int cssWidth, int cssHeight) {
        Element e = box.getElement();
        if (e == null) {
            return null;
        }
        String nodeName = e.getNodeName();
        // 找到img标签
        if (nodeName.equals("img")) {
            String attribute = e.getAttribute("src");
            FSImage fsImage;
            try {
                // 生成itext图像
                fsImage = buildImage(attribute, uac);
            } catch (BadElementException | IOException e1) {
                fsImage = null;
            }
            if (fsImage != null) {
                // 对图像进行缩放
                if (cssWidth != -1 || cssHeight != -1) {
                    fsImage.scale(cssWidth, cssHeight);
                }
                return new ITextImageElement(fsImage);
            }
        }
        return null;
    }

    /**
     * 将base64编码解码并生成itext图像
     *
     * @param srcAttr 属性
     * @param uac 回调
     * @return FSImage
     * @throws IOException io异常
     * @throws BadElementException BadElementException
     */
    protected FSImage buildImage(String srcAttr, UserAgentCallback uac) throws IOException,
            BadElementException {
        FSImage fsImage;
        if (srcAttr.startsWith("data:image/")) {
            String b64encoded = srcAttr.substring(srcAttr.indexOf("base64,") + "base64,".length(),
                    srcAttr.length());
            // 解码
            byte[] decodedBytes = Base64.decode(b64encoded);

            fsImage = new ITextFSImage(Image.getInstance(decodedBytes));
        } else {
            fsImage = uac.getImageResource(srcAttr).getImage();
        }
        return fsImage;
    }

    /**
     * 实现remove
     *
     * @param e 元素
     */
    @Override
    public void remove(Element e) {
    }

    /**
     * 实现reset
     */
    @Override
    public void reset() {
    }

    /**
     * 实现setFormSubmissionListener
     *
     * @param formsubmissionlistener 监听
     */
    @Override
    public void setFormSubmissionListener(FormSubmissionListener formsubmissionlistener) {
    }
}

  此外,封装按模板直接生成PDF的工具类 PdfTemplateUtil


public class PdfTemplateUtil {

    public static ByteArrayOutputStream createPdfByItext(HashMap<String, Object> dataMap, String templateName) throws Exception {

        String html = HtmlToPDFUtil.render(templateName, dataMap);

        String base64 = HtmlToPDFUtil.getPDFBase64ByHtml(html);

        String base64O = HtmlToPDFUtil.pdfAddWaterHeaderLogo(base64
                , true, "仅供内部参阅"
                , true, ""
                , false, "", 141, 30, 20, -30
                , false, ""
                , true, "classpath:template/img/report-background.jpg"
        );

        return HtmlToPDFUtil.base64ToPDF(base64O);


    }
}
  2.6 完整调用示例

  PDF导出接口示例

  此处提供一个导出生成PDF的接口示例供参考

/**
  * 模板PDF导出接口
  */
@ApiOperation(value = "模板PDF导出接口", notes = "模板PDF导出接口", httpMethod = "GET", produces = "application/octet-stream")
@GetMapping(value = "/exportPdf")
public void exportPdf(HttpServletResponse response) throws Exception{
    ByteArrayOutputStream baos = null;
    OutputStream out = null;
    try {
        // 模板中的数据,实际运用从数据库中查询

        SchoolTimeTable.WeekLesson eightAm = new SchoolTimeTable.WeekLesson();
        eightAm.setMon("语文");
        eightAm.setTue("数学");
        eightAm.setWed("英语");
        eightAm.setThu("化学");
        eightAm.setFri("物理");

        SchoolTimeTable.WeekLesson nineAm = new SchoolTimeTable.WeekLesson();
        nineAm.setMon("数学");
        nineAm.setTue("化学");
        nineAm.setWed("语文");
        nineAm.setThu("物理");
        nineAm.setFri("英语");

        SchoolTimeTable.WeekLesson tenAm = new SchoolTimeTable.WeekLesson();
        tenAm.setMon("英语");
        tenAm.setTue("生物");
        tenAm.setWed("语文");
        tenAm.setThu("历史");
        tenAm.setFri("地理");

        SchoolTimeTable.WeekLesson twoPm = new SchoolTimeTable.WeekLesson();
        twoPm.setMon("物理");
        twoPm.setTue("历史");
        twoPm.setWed("数学");
        twoPm.setThu("生物");
        twoPm.setFri("英语");

        SchoolTimeTable.WeekLesson threePm = new SchoolTimeTable.WeekLesson();
        threePm.setMon("历史");
        threePm.setTue("语文");
        threePm.setWed("数学");
        threePm.setThu("物理");
        threePm.setFri("语文");

        List<SchoolTimeTable.WeekLesson> weekLessons1 = new ArrayList<>();
        weekLessons1.add(eightAm);
        weekLessons1.add(nineAm);
        weekLessons1.add(tenAm);

        List<SchoolTimeTable.WeekLesson> weekLessons2 = new ArrayList<>();
        weekLessons2.add(twoPm);
        weekLessons2.add(threePm);

        SchoolTimeTable amData = SchoolTimeTable.builder().period("上午").weekLessons(weekLessons1).lessonsSize(weekLessons1.size()).build();
        SchoolTimeTable pmData = SchoolTimeTable.builder().period("下午").weekLessons(weekLessons2).lessonsSize(weekLessons2.size()).build();

        List<SchoolTimeTable> schoolTimeTables = new ArrayList<>(Collections.singletonList(amData));
        schoolTimeTables.add(pmData);

        HashMap<String, Object> dataMap = new HashMap<>();
        dataMap.put("schoolTimetable",schoolTimeTables);

        // 创建数据
        DefaultCategoryDataset dataset = new DefaultCategoryDataset();
        dataset.addValue(306, "女", "高一");
        dataset.addValue(295, "女", "高二");
        dataset.addValue(285, "女", "高三");
        dataset.addValue(282, "男", "高一");
        dataset.addValue(301, "男", "高二");
        dataset.addValue(296, "男", "高三");
        // 获取生成的图表
        String recruitStudentImg = ImgBase64Util.genBase64ImageByChart("招生人数统计", "年份", "人数", dataset, "recruit_student.jpg");
        dataMap.put("recruitStudentImg",recruitStudentImg);

        baos = PdfTemplateUtil.createPdfByItext(dataMap, "cover.ftl");;
        // 设置响应消息头,告诉浏览器当前响应是一个下载文件
        response.setContentType( "application/x-msdownload");
        // 告诉浏览器,当前响应数据要求用户干预保存到文件中,以及文件名是什么 如果文件名有中文,必须URL编码
        String fileName = URLEncoder.encode("光明中学课表.pdf", "UTF-8");
        response.setHeader( "Content-Disposition", "attachment;filename=" + fileName);
        out = response.getOutputStream();
        baos.writeTo(out);
        baos.close();
    } catch (Exception e) {
        e.printStackTrace();
        throw new Exception("导出失败:" + e.getMessage());
    } finally{
        if(baos != null){
            baos.close();
        }
        if(out != null){
            out.close();
        }
    }
}

其他方案:

纯前端生成( 需接口传输合同相关数据,存在被恶意攻击的风险,且存在单页裁剪问题)

思路:前端绘制合同页面,并通过设置样式将合同内容移出可视区域,js获取dom,使用 html2canvas 将html转换成canvas长图,使用jspdf 将长图剪裁后,转换成PDF文件。


总结

以上

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值