概述过程: 前端绘制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转PDF
、PDF
加工(添加页眉、页脚、水印、背景等)的方法。其中若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文件。
总结
以上