使用itext7在PDF中实现多种文字水印效果

itext5的水印方案许多年前掘金已有文章介绍(JAVA itextpdf 为PDF添加多种水印),但itext7已经问世许久,至今网络上还很难找到介绍这个巨大变革后的itext7相关文章,从包、类、方法到参数、流程都有了许多的不同,在开源的java库中,很难找到比itext对PDF操作更精细的库,唯一值得比较的pdfbox,在性能上也是落后的,所以其实选择空间很有限。

从5到7的升级并没有让这个库变得简单,许多地方的繁琐不亚于从前,且更不易控制了。即便如此,升级也是必须的,itext5隐藏的问题也非常的多,而且也停止更新很久了,安全漏洞也不会再更新。

首先就是包的引入,相信现在大多数人已经从maven转为了gradle,下面是gradle的dependencies:

dependencies {
    implementation 'com.itextpdf:kernel:7.2.5'
    implementation 'com.itextpdf:layout:7.2.5'
}

参数设定

以前的itext5文章中第一个出现的类PdfStamper在新版中已经不存在了,现在是围绕PdfDocument来操作基础文件的。同时以前作为水印对象的PdfContentByte也已不在,现在是使用Canvas进行操作,下面来看一下基础的一些参数设置:

//水印文本
String text = "Computer Engineering";
//文字大小
int size = 32; 
//出入的文档
PdfDocument pdfDoc = new PdfDocument(new PdfReader("EC0066761.pdf"), new PdfWriter("output.pdf")); 
//水印旋转的角度 
float angle = (float) Math.toRadians(35); 
//字体
PdfFont font = PdfFontFactory.createFont(FontProgramFactory.createFont(StandardFonts.HELVETICA)); 
//段落
Paragraph paragraph = new Paragraph(text)  
    .setFont(font).setFontSize(size);  
//图形状态参数
PdfExtGState gs = new PdfExtGState();  
//水印自身透明度的设置
gs.setFillOpacity(0.2f);  
//页面大小
PageSize pageSize = pdfDoc.getDefaultPageSize();  

对比以前,PdfDocument的初始化看上去简单了些,两个参数看上去比较干净。不过紧接着就是一个坑,下面的旋转角度请注意,用的是弧度制,所以要用角度制就必须用Math.toRadians转换一下。在其他地方可能也有这种问题,曾经的版本使用角度制,而新版改用了弧度制。用于扩展设定的PdfGState改为了PdfExtGState,这个改变主要是新版支持了ISO-320001标准,但移除了一些的方法,例如setOverPrintStroking(boolean op)toPdf(PdfWriter writer,OutputStream os),不过影响不大。

基本的参数设定完毕,接下来就是一个分叉口,是生成什么类型的水印,单个的居中水印,还是满屏的水印,无论是哪种,以上的参数设置都是需要的,下面先看一下单个水印:

单水印

使用下面这段代码即可生成好单个屏幕居中的水印:

for (int i = 1; i <= pdfDoc.getNumberOfPages(); i++) {
        PdfCanvas over = new PdfCanvas(pdfDoc.getPage(i));
        over.setFillColor(ColorConstants.BLACK);
        over.setExtGState(gs);
        Canvas canvas = new Canvas(over, pageSize)
                .showTextAligned(paragraph,
                        pageSize.getWidth() / 2,
                        pageSize.getHeight() / 2, i,
                        TextAlignment.CENTER, VerticalAlignment.MIDDLE, angle);
        canvas.close();
}
pdfDoc.close();

循环和以前一样,页码都是从第1页开始的,而非第0页。不过,请注意一个细节:上面有两个地方都用到了页码,但却有不同,用pdfDoc.getPage(0)会直接抛出IndexOutOfBoundsException,但Canvas.showTextAligned(,,,0,),这个页码参数用0却并不会出错,会将这个canvas显示在第1页上。但你可千万别觉得此处的页码是从0开始的,查看一下此处的源码就会发现,这里只是进行了一个值的判断,查看Github源代码

// RootElement.showTextAligned 源码
public T showTextAligned(Paragraph p, float x, float y, int pageNumber, 
    TextAlignment textAlign, VerticalAlignment vertAlign, float radAngle) {
    ...
    if (pageNumber == 0)
        pageNumber = 1;
    ...
}

此处如果是0页则变为1页,但如果第1页,一样是第1页,跟上面的.getPage是不同的,这样混沌式的兼容实在是数不胜数,如果要对这种库进行封装,建议不要进行这样混乱的行为,从1开始就请禁止0的输入

另外,此处的angle就是上文所提到的弧度制值,上面的参数设置已经过了Math.toRadians()的处理,此处直接使用即可。将页面的长宽都/2很好理解,就是定位在中心的位置,至于本身的长宽的影响itext自身就会处理好,不需要修正

单水印效果

满屏水印

满屏水印是要复杂一些的,平铺在页面上只需要循环一下页面的高宽/水印本身大小,就可以铺满屏幕了,但这个水印本身大小就是一个麻烦,因为我们必须要知道旋转后的水印大小,而旋转这个操作是发生在showTextAligned这个方法执行时的,但旋转之后,这个水印就已经出现在PDF上了,这个流程无法拆分,那怎么办呢?

可能第一时间会想到一种办法,先画一个canvas上去,然后获取这个canvas的区域。但是,这个获取区域实际上就做不到,因为初始化canvas本身时,它就需要一个区域,这时通常是直接将整个page大小都给它,所以canvas.getRootArea()再获取时,也只会获得和PageSize的一样大的区域。所以只有提前计算好旋转后的水印大小,计算分如下三步:

  1. 计算出文本的宽度,在itext中这很简单,他的PdfFont.getWidth()就能算出来这个值
  2. 根据这个宽度和设定的角度用正弦定理计算出旋转后的高度
  3. 根据前两项(文本的宽度(斜边长)和旋转后的高度)用勾股定理计算出旋转后的宽度

有了思路计算这三个值就比较简单了:

// 文本宽度(用于计算间隔)  
float textWidth = font.getWidth(text, size);  
// 用正弦定理计算出水印高度
float labelHeight = (float) Math.sin(angle) * textWidth;
// 用勾股计算出旋转后的水印宽度
float labelWidth = (float) Math.sqrt(Math.pow(textWidth, 2) - Math.pow(labelHeight, 2));

有了旋转后的高宽就可以直接循环铺满了

满屏基础效果

for (int i = 1; i <= pdfDoc.getNumberOfPages(); i++) {
        PdfCanvas over = new PdfCanvas(pdfDoc.getPage(i));
        over.setFillColor(ColorConstants.BLACK);
        over.setExtGState(gs);
        for (int x = 0; x < pageSize.getWidth() / labelWidth; x++) {
                for (int y = 0; y < pageSize.getHeight() / labelHeight; y++) {
                        float pX = x * labelWidth * 1.1f;
                        float pY = y * labelHeight * 1.1f;
                        Canvas canvas = new Canvas(over, pageSize);
                        canvas.showTextAligned(paragraph,
                                        pX, pY, i,
                                        TextAlignment.CENTER, VerticalAlignment.MIDDLE,
                                        angle);
                        canvas.close();
                }
        }
}
pdfDoc.close();

中间的pXpY都乘1.1是为了水印之间保持一些间距,下面看效果图:

带border的平铺效果

可以在进行循环之前,对paragraph设置一下border让水印变得好看一些:

paragraph.setPadding(12).setBorder(new SolidBorder(ColorConstants.GRAY, 2));

不过可能很多不喜欢这个效果,我也没有在其他带水印的PDF中看到过这样的效果。

平行密集平铺效果

更多的人可能喜欢更紧凑一些的水印,这样的话就不要设置边框了更不好看,pX也不能乘1.1倍了,可以改成除以2缩小间距。因为边距缩小了,所以在循环x值时,范围也要再乘以2,增多水印数量来铺满:

for (int x = 0; x < pageSize.getWidth() / labelWidth * 2; x++) {
        for (int y = 0; y < pageSize.getHeight() / labelHeight; y++) {
                float pX = x * labelWidth / 2;
                float pY = y * labelHeight * 1.1f;
                Canvas canvas = new Canvas(over, pageSize);
                canvas.showTextAligned(paragraph,
                                pX, pY, i,
                                TextAlignment.CENTER, VerticalAlignment.MIDDLE, angle);
                canvas.close();
        }
}

只变动了两个值,即可达到效果,如下所示

可以将上面labelWidth前后的的 *2,/2 改成 *3,/3 显得更密集,不过可能有人不喜欢这个太过平行的效果,让人感觉有些半斜不斜

斜向密集平铺效果

根据x的变化来增加pY的值,同时和上面pX的调整一样,减小高度的边距,使得整体变得更加倾斜和密集

for (int x = 0; x < pageSize.getWidth() / labelWidth * 2; x++) {
        for (int y = 0; y < pageSize.getHeight() / labelHeight * 1.25; y++) {
                float pX = x * labelWidth / 2;
                float pY = y * labelHeight * .8f + x * labelHeight * .2f;
                Canvas canvas = new Canvas(over, pageSize);
                canvas.showTextAligned(paragraph,
                        pX, pY, i,
                        TextAlignment.CENTER, VerticalAlignment.MIDDLE, angle);
                canvas.close();
        }
}

效果如下

只要掌握了labelWidth,labelHeight的值,对其进行调整就能铺出各种各样的效果,但记得调整好循环范围,铺满PDF即可,不要超出太多损失性能。

下面贴下最后一种效果的完整代码:

public static void main(String[] args) throws Exception {
    String text = "Computer Engineering";
    int size = 32;
    PdfDocument pdfDoc = new PdfDocument(new PdfReader("EC0066761.pdf"), new PdfWriter("output.pdf"));
    float angle = (float) Math.toRadians(35);
    PdfFont font = PdfFontFactory.createFont(FontProgramFactory.createFont(StandardFonts.HELVETICA));
    Paragraph paragraph = new Paragraph(text)
            .setFont(font).setFontSize(size);
    PdfExtGState gs = new PdfExtGState();
    gs.setFillOpacity(0.2f);
    PageSize pageSize = pdfDoc.getDefaultPageSize();

    // 文本宽度(用于计算间隔)
    float textWidth = font.getWidth(text, size);
    // 用正弦定理计算出水印高度
    float labelHeight = (float) Math.sin(angle) * textWidth;
    // 用勾股计算出旋转后的水印宽度
    float labelWidth = (float) Math.sqrt(Math.pow(textWidth, 2) - Math.pow(labelHeight, 2));
    for (int i = 1; i <= pdfDoc.getNumberOfPages(); i++) {
            PdfCanvas over = new PdfCanvas(pdfDoc.getPage(i));
            over.setFillColor(ColorConstants.BLACK);
            over.setExtGState(gs);
            for (int x = 0; x < pageSize.getWidth() / labelWidth * 2; x++) {
                    for (int y = 0; y < pageSize.getHeight() / labelHeight * 1.25; y++) {
                            float pX = x * labelWidth / 2;
                            float pY = y * labelHeight * .8f + x * labelHeight * .2f;
                            Canvas canvas = new Canvas(over, pageSize);
                            canvas.showTextAligned(paragraph,
                                    pX, pY, i,
                                    TextAlignment.CENTER, VerticalAlignment.MIDDLE, angle);
                            canvas.close();
                    }
            }
    }
    pdfDoc.close();
}

另外,如果使用汉字水印,要改变PdfFontFactory.createFont处的字体,和旧版一样,itext默认是没有中文字体的,要手动导入中文字体的otf/ttf文件,这点就不赘述了

最后说一个小问题值得注意,itext的定位是以左下角为基准的,并非左上角

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,843评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,538评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,187评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,264评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,289评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,231评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,116评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,945评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,367评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,581评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,754评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,458评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,068评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,692评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,842评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,797评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,654评论 2 354

推荐阅读更多精彩内容