java实现pdf水印详解,支持换行、旋转且样式不会错乱,贼透彻
业务需求
在客户点击链接评价完后生成相应评价数据的pdf文件,并且添加水印,水印添加完成后进行第三方签名。水印名称为项目名称,字体默认,字号12,颜色浅灰色,水印文字排列为8 * 3(一行3个水印,一页8行),水印30度旋转,且支持文字换行,换行后文字居中。
代码实现思路
因为是公司项目,为了方便整理成了3步骤实现,第一步生成评价数据的pdf文件,第二步添加水印,第三步进行第三方签名,这篇文章主要分析水印相关的实现和详细步骤拆分,看完自己就可以根据需求定制水印,想放哪儿就放哪儿,怎么好看你就能怎么整
。
效果图
本地代码实现
一、前期准备
- 依赖:
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.4.3</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
- 一个pdf文件
二、pdf水印生成简单样例(基础)
public static void main(String[] args) throws Exception {
ReducePdfTestService reducePdfService = new ReducePdfTestService();
reducePdfService.setPdfWaterMarForFileTest("D:/file/todest/",
"test.pdf", "测试水印");
}
/**
* 给已经输出的pdf文件添加水印
* @param filePath
* @param fileName
* @param
* @throws Exception
*/
public void setPdfWaterMarForFileTest(String filePath, String fileName , String waterText) throws Exception {
// 输入PDF文件路径
String inputFile = filePath + fileName;
// 输出PDF文件路径
String outputFile = filePath +"temp_" + fileName;
// 创建PdfReader对象读取原始PDF文件
PdfReader reader = new PdfReader(inputFile);
// 创建PdfStamper对象,用于添加水印
PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(outputFile));
// 用于添加水印的主要对象
PdfContentByte waterMar;
// 创建水印字体
BaseFont base = BaseFont.createFont("STSongStd-Light", "UniGB-UCS2-H", BaseFont.EMBEDDED);
// 设置水印透明度
PdfGState gs = new PdfGState();
// 设置填充字体不透明度
gs.setFillOpacity(0.12f);
gs.setStrokeOpacity(0.3f);
// 获取pdf页数
int n = reader.getNumberOfPages();
// 添加水印到每一页
for (int i = 1; i <= n; i++) {
waterMar = stamper.getOverContent(i);
// 水印的设置方法,主要用于初始化字体数据和前期准备
waterMar.saveState();
waterMar.restoreState();
waterMar.setGState(gs);
waterMar.setFontAndSize(base, 12);
// 开始设置水印
waterMar.beginText();
// 第一个为字体的对齐方式,第二个是水印文本,第三个是水印x坐标,第四个为y坐标,最后一个参数为旋转度数
waterMar.showTextAligned(Element.ALIGN_LEFT, waterText, 0, 0, 0);
// 设置水印结束
waterMar.endText();
}
// 关闭PdfStamper和PdfReader对象
stamper.close();
reader.close();
}
这里简单做一下说明,上面的代码是pdf生成水印的基础要素,关于其中每一步都有详细的说明,在setPdfWaterMarForFileTest
方法中的第一步有两个文件路径,有的小伙伴可能会有疑惑,outputFile
这个路径的存在是因为我们在生成途中可能会有错误,保险起见用一个新的文件,不破坏原有的文件,保证代码的顺利运行(注意:因为还有第三步的第三方签名,如果破坏源文件,那里出现错误将是致命的)。
效果图
三、showTextAligned方法解读
最关键也是最迷惑人的其实是showTextAligned
这个方法,其中的五个参数只有第二个水印文本是最直观的,其余四个参数都需要我们仔细研究才能实现我们想要的效果,下面对这个方法的五个参数进行详细解释。
这段取自官方的java支持文档,可以很清晰的看到五个参数。
- int alignment
第一个参数中文解释为对齐方式,入参是int类型,这里官方对不同的对齐方式是有一个常量类进行对应的Element
,里面有10中常量标识对齐,每一个都有对应的解释,这里只介绍最常用的三种。
- ALIGN_LEFT:左对齐
- ALIGN_CENTER:居中对齐
- ALIGN_RIGHT:右对齐
虽然这三个看起来很好理解,其实里面大有门道,不理解透彻根本无法自定义水印的样式,下面将五个参数的定义说完后我们通过实验解析每一个参数的原理及使用。
- String text
这个不做多余解释,就是水印的文字,在换行时我们会详细说明使用。 - float x
水印的x坐标,也就是横坐标,类似我们小时候学过的坐标系 - float y
水印的y坐标,也就是纵坐标 - float rotation
水印文字的旋转角度
四、根据需求实现水印
回到我们最开始的例子,水印展示的位置是在左下角,这里涉及到一个知识点。
itext底层使用画布画出了我们所看到的pdf,水印是怎么绘画的,大家联想一下坐标系,其实pdf左下角的那个点就是坐标系的
原点
,showTextAligned方法的x和y参数都是基于这个原点进行位置的定位,因为例子中给的x和y都是0,所以默认就是从原点位置开始绘制水印。
文字展示的不全面,经过测试发现无论是中文还是英文数字都会存在这个问题,暂时没有对这块儿更好的解释,个人猜测应该是遮挡,不过我们有解决方案。
为了之后代码更好的理解,我们需要用到以下代码
// 获取水印字体的宽高
JLabel label = new JLabel();
FontMetrics metrics;
int textH = 0;
int textW = 0;
label.setText(waterText);
metrics = label.getFontMetrics(label.getFont());
//字符串的高, 只和字体有关
textH = metrics.getHeight();
System.out.println("字符串的高度 = " + textH);
//字符串的宽
textW = metrics.stringWidth(label.getText());
System.out.println("字符串的宽度 = " + textW);
// 获取页面的宽高
Rectangle pageSizeWithRotation = reader.getPageSizeWithRotation(1);
float height = pageSizeWithRotation.getHeight();
float width = pageSizeWithRotation.getWidth();
System.out.println("页面宽度 = " + width);
System.out.println("页面高度 = " + height);
经过运行得出了下面的结果。
可以看到水印字体的高度是16,那么我们可以稍微改造一下代码,让水印的高度为文字高度的一半,这样会好看许多。
waterMar.showTextAligned(Element.ALIGN_LEFT, waterText, 0, (float)(textH / 2), 0);
如果想要继续调整位置可以参考在进行增加和减少,不过看到这的老铁就有疑惑,这有什么难得,不不不,x和y坐标如果只是在一行中,且旋转角度为0的情况下很简单,最恶心的是,如果你的对齐方式或旋转角度一旦发生变化,那就不好使了哦,如下:
waterMar.showTextAligned(Element.ALIGN_Right, waterText, 0, (float)(textH / 2), 0);
右对齐水印就没了
waterMar.showTextAligned(Element.ALIGN_CENTER, waterText, 0, (float) textH / 2, 0);
居中对齐水印就剩一半了
这里又涉及到一个知识点,对齐方式默认是按照x坐标进行对齐的,现在我们的x坐标是0,如果是右对齐,文字自然就看不到了,如果是居中对齐,就只会显示文字的一半。
了解这个知识点主要是为了方便我们后面出现水印字体过长的时候,需要换行显示,换行后文字到底是居中还是居左的需求。
接下来实现一行多个水印字体,我们已经知道怎么画了,之后就是动态改变x和y的坐标就可以实现多个水印,for循环是最好的工具,我们直接根据需求一行显示3个进行改造。
// 开始设置水印
waterMar.beginText();
for (int x = 0; x < 3; x++) {
// 每次增加230px和水印宽度的一半,加水印宽度的一半是让第一个水印距离页面左侧有一些填充,更加美观
float xFloat = x * 230 + (float)(textW / 2);
System.out.println("x坐标为 = " + xFloat);
// x坐标为 = 24.0 x坐标为 = 254.0 x坐标为 = 484.0
waterMar.showTextAligned(Element.ALIGN_LEFT, waterText, xFloat, (float) textH / 2, 0);
}
// 设置水印结束
waterMar.endText();
效果图
之后进行整个页面的水印效果,其实就是在上面的基础上再嵌套一个for循环,动态的改变y的坐标值即可。
// 开始设置水印
waterMar.beginText();
for (int y = 0; y < 8; y++) {
for (int x = 0; x < 3; x++) {
// 每次增加230px和水印宽度的一半,加水印宽度的一半是让第一个水印距离页面左侧有一些填充,更加美观
float xFloat = x * 230 + (float)(textW / 2);
System.out.println("x坐标为 = " + xFloat);
// 每次增加110px和水印字体高度的2倍,加字体高度2倍也是为了让第一个水印距离页面底部有一些填充
float yFloat = y * 110 + (float) textH * 2;
System.out.println("y坐标为 = " + yFloat);
waterMar.showTextAligned(Element.ALIGN_LEFT, waterText, xFloat, yFloat, 0);
}
}
// 设置水印结束
waterMar.endText();
效果图
之后我们悄悄将最后一个参数改为30,对应需求的旋转30度,基本就实现了效果。
你以为到这儿就完了吗?其实好戏才刚刚开始
从实际需求出发,现在的写法有很多问题,第一:如果水印字数过多样式会错乱且没有支持换行;第二:代码过于死板,不灵活,假如现在业务说要求字号变大或者旋转角度、对齐方式改变,我们依旧需要动态调整里面的参数,也就是x和y的坐标值,如果要求排列方式变化那就更麻烦了。
这是水印字体过长时出现的情况,样式错乱,接下来处理换行。
思路:实际上还是对x和y坐标的修改,我们指定一下一行允许展示的最多字数(初版,后期会用宽度进行计算),按照字数进行分割,在加一层for循环即可实现。
分割字符串的方法
public static String[] stringToStringArray(String text, int length) {
//检查参数是否合法
if (StringUtils.isEmpty(text)) {
return null;
}
if (length <= 0) {
return null;
}
// 首尾空格去除
text = text.trim();
//获取整个字符串可以被切割成字符子串的个数
int n = (text.length() + length - 1) / length;
String[] splitArr = new String[n];
for (int i = 0; i < n; i++) {
if (i < (n - 1)) {
splitArr[i] = text.substring(i * length, (i + 1) * length);
} else {
splitArr[i] = text.substring(i * length);
}
}
return splitArr;
}
改造的水印方法
// 开始设置水印
waterMar.beginText();
String[] marTextArray = stringToStringArray(waterText, 15);
for (int y = 0; y < 8; y++) {
for (int x = 0; x < 3; x++) {
for (int t = 0; t < marTextArray.length; t++) {
// 115为横向偏移量,这里使用的是居中对齐,必须加入一定偏移量保证边缘不会出现水印
// 10为水印多行时偏移量,调整范围在8-16,适当调整可以增加行间距
// 87为纵向偏移量,和115原理类似
float xFloat = x * 190 + 115 + t * 10;
System.out.println("x坐标为 = " + xFloat);
float yFloat = y * 100 + 87 - t * 10;
System.out.println("y坐标为 = " + yFloat);
waterMar.showTextAligned(Element.ALIGN_CENTER, marTextArray[t], xFloat, yFloat, 30);
}
}
}
// 设置水印结束
waterMar.endText();
做一下简单解读:190和100实际上是每个水印之间的横向和纵向间隙,这两个数字是我目前调整的最合适的数字,而10这个数字也是经过测试得出的,因为在多行之后仅仅只调整成字符高度的一半不太美观,这里适当增加,会使得段落之间有间距,比较好看。
到这里已经解决了大部分伙伴的疑惑,接下来是对整个方法进行封装,也就是解决不动态的问题,因为之前公司业务已经明确了水印的要求,而且字数不会超过三行,所以目前的写法已经完全够用了,但是测试发现如果超过4行样式会稍微有所变化,但整体不会影响。
到这里就结束了,因为itext的这个版本相对较低,现在已经有了更高版本或者更为便捷的其它工具,所以对更深层次的封装就不予说明了,不过这些对于我们基础的开发已经够用了,毕竟很少有公司会对水印有着那么高的要求。