itext5的水印方案許多年前掘金已有文章介紹(JAVA itextpdf 為PDF添加多種水印),但itext7已經(jīng)問世許久搅窿,至今網(wǎng)絡(luò)上還很難找到介紹這個(gè)巨大變革后的itext7相關(guān)文章欧聘,從包秕铛、類、方法到參數(shù)盗舰、流程都有了許多的不同谨读,在開源的java庫(kù)中,很難找到比itext對(duì)PDF操作更精細(xì)的庫(kù)藕施,唯一值得比較的pdfbox,在性能上也是落后的凸郑,所以其實(shí)選擇空間很有限裳食。
從5到7的升級(jí)并沒有讓這個(gè)庫(kù)變得簡(jiǎn)單,許多地方的繁瑣不亞于從前芙沥,且更不易控制了诲祸。即便如此,升級(jí)也是必須的而昨,itext5隱藏的問題也非常的多救氯,而且也停止更新很久了,安全漏洞也不會(huì)再更新歌憨。
首先就是包的引入着憨,相信現(xiàn)在大多數(shù)人已經(jīng)從maven轉(zhuǎn)為了gradle,下面是gradle的dependencies:
dependencies {
implementation 'com.itextpdf:kernel:7.2.5'
implementation 'com.itextpdf:layout:7.2.5'
}
參數(shù)設(shè)定
以前的itext5文章中第一個(gè)出現(xiàn)的類PdfStamper
在新版中已經(jīng)不存在了务嫡,現(xiàn)在是圍繞PdfDocument
來(lái)操作基礎(chǔ)文件的甲抖。同時(shí)以前作為水印對(duì)象的PdfContentByte
也已不在,現(xiàn)在是使用Canvas
進(jìn)行操作心铃,下面來(lái)看一下基礎(chǔ)的一些參數(shù)設(shè)置:
//水印文本
String text = "Computer Engineering";
//文字大小
int size = 32;
//出入的文檔
PdfDocument pdfDoc = new PdfDocument(new PdfReader("EC0066761.pdf"), new PdfWriter("output.pdf"));
//水印旋轉(zhuǎn)的角度
float angle = (float) Math.toRadians(35);
//字體
PdfFont font = PdfFontFactory.createFont(FontProgramFactory.createFont(StandardFonts.HELVETICA));
//段落
Paragraph paragraph = new Paragraph(text)
.setFont(font).setFontSize(size);
//圖形狀態(tài)參數(shù)
PdfExtGState gs = new PdfExtGState();
//水印自身透明度的設(shè)置
gs.setFillOpacity(0.2f);
//頁(yè)面大小
PageSize pageSize = pdfDoc.getDefaultPageSize();
對(duì)比以前准谚,PdfDocument
的初始化看上去簡(jiǎn)單了些,兩個(gè)參數(shù)看上去比較干凈去扣。不過(guò)緊接著就是一個(gè)坑柱衔,下面的旋轉(zhuǎn)角度請(qǐng)注意,用的是弧度制,所以要用角度制就必須用Math.toRadians
轉(zhuǎn)換一下秀存。在其他地方可能也有這種問題捶码,曾經(jīng)的版本使用角度制羽氮,而新版改用了弧度制或链。用于擴(kuò)展設(shè)定的PdfGState
改為了PdfExtGState
,這個(gè)改變主要是新版支持了ISO-320001標(biāo)準(zhǔn)档押,但移除了一些的方法澳盐,例如setOverPrintStroking(boolean op)
,toPdf(PdfWriter writer,OutputStream os)
令宿,不過(guò)影響不大叼耙。
基本的參數(shù)設(shè)定完畢,接下來(lái)就是一個(gè)分叉口粒没,是生成什么類型的水印筛婉,單個(gè)的居中水印,還是滿屏的水印癞松,無(wú)論是哪種爽撒,以上的參數(shù)設(shè)置都是需要的,下面先看一下單個(gè)水酉烊亍:
單水印
使用下面這段代碼即可生成好單個(gè)屏幕居中的水铀段稹:
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();
循環(huán)和以前一樣,頁(yè)碼都是從第1頁(yè)開始的枫甲,而非第0頁(yè)源武。不過(guò),請(qǐng)注意一個(gè)細(xì)節(jié):上面有兩個(gè)地方都用到了頁(yè)碼想幻,但卻有不同粱栖,用pdfDoc.getPage(0)
會(huì)直接拋出IndexOutOfBoundsException
,但Canvas.showTextAligned(,,,0,)
脏毯,這個(gè)頁(yè)碼參數(shù)用0卻并不會(huì)出錯(cuò)闹究,會(huì)將這個(gè)canvas顯示在第1頁(yè)上。但你可千萬(wàn)別覺得此處的頁(yè)碼是從0開始的抄沮,查看一下此處的源碼就會(huì)發(fā)現(xiàn)跋核,這里只是進(jìn)行了一個(gè)值的判斷,查看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頁(yè)則變?yōu)?頁(yè)叛买,但如果第1頁(yè)砂代,一樣是第1頁(yè),跟上面的.getPage
是不同的率挣,這樣混沌式的兼容實(shí)在是數(shù)不勝數(shù)刻伊,如果要對(duì)這種庫(kù)進(jìn)行封裝,建議不要進(jìn)行這樣混亂的行為,從1開始就請(qǐng)禁止0的輸入
另外捶箱,此處的angle
就是上文所提到的弧度制值智什,上面的參數(shù)設(shè)置已經(jīng)過(guò)了Math.toRadians()
的處理,此處直接使用即可丁屎。將頁(yè)面的長(zhǎng)寬都/2很好理解荠锭,就是定位在中心的位置,至于本身的長(zhǎng)寬的影響itext自身就會(huì)處理好晨川,不需要修正
單水印效果
滿屏水印
滿屏水印是要復(fù)雜一些的证九,平鋪在頁(yè)面上只需要循環(huán)一下頁(yè)面的高寬/水印本身大小,就可以鋪滿屏幕了共虑,但這個(gè)水印本身大小就是一個(gè)麻煩愧怜,因?yàn)槲覀儽仨氁佬D(zhuǎn)后的水印大小,而旋轉(zhuǎn)這個(gè)操作是發(fā)生在showTextAligned
這個(gè)方法執(zhí)行時(shí)的妈拌,但旋轉(zhuǎn)之后拥坛,這個(gè)水印就已經(jīng)出現(xiàn)在PDF上了,這個(gè)流程無(wú)法拆分尘分,那怎么辦呢猜惋?
可能第一時(shí)間會(huì)想到一種辦法,先畫一個(gè)canvas上去音诫,然后獲取這個(gè)canvas的區(qū)域惨奕。但是,這個(gè)獲取區(qū)域?qū)嶋H上就做不到竭钝,因?yàn)槌跏蓟痗anvas本身時(shí)梨撞,它就需要一個(gè)區(qū)域,這時(shí)通常是直接將整個(gè)page大小都給它香罐,所以canvas.getRootArea()
再獲取時(shí)卧波,也只會(huì)獲得和PageSize的一樣大的區(qū)域。所以只有提前計(jì)算好旋轉(zhuǎn)后的水印大小庇茫,計(jì)算分如下三步:
- 計(jì)算出文本的寬度港粱,在itext中這很簡(jiǎn)單,他的
PdfFont.getWidth()
就能算出來(lái)這個(gè)值 - 根據(jù)這個(gè)寬度和設(shè)定的角度用正弦定理計(jì)算出旋轉(zhuǎn)后的高度
- 根據(jù)前兩項(xiàng)(文本的寬度(斜邊長(zhǎng))和旋轉(zhuǎn)后的高度)用勾股定理計(jì)算出旋轉(zhuǎn)后的寬度
有了思路計(jì)算這三個(gè)值就比較簡(jiǎn)單了:
// 文本寬度(用于計(jì)算間隔)
float textWidth = font.getWidth(text, size);
// 用正弦定理計(jì)算出水印高度
float labelHeight = (float) Math.sin(angle) * textWidth;
// 用勾股計(jì)算出旋轉(zhuǎn)后的水印寬度
float labelWidth = (float) Math.sqrt(Math.pow(textWidth, 2) - Math.pow(labelHeight, 2));
有了旋轉(zhuǎn)后的高寬就可以直接循環(huán)鋪滿了
滿屏基礎(chǔ)效果
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();
中間的pX
旦签,pY
都乘1.1是為了水印之間保持一些間距查坪,下面看效果圖:
帶border的平鋪效果
可以在進(jìn)行循環(huán)之前,對(duì)paragraph設(shè)置一下border讓水印變得好看一些:
paragraph.setPadding(12).setBorder(new SolidBorder(ColorConstants.GRAY, 2));
不過(guò)可能很多不喜歡這個(gè)效果宁炫,我也沒有在其他帶水印的PDF中看到過(guò)這樣的效果偿曙。
平行密集平鋪效果
更多的人可能喜歡更緊湊一些的水印,這樣的話就不要設(shè)置邊框了更不好看羔巢,pX
也不能乘1.1倍了望忆,可以改成除以2縮小間距罩阵。因?yàn)檫吘嗫s小了,所以在循環(huán)x值時(shí)启摄,范圍也要再乘以2稿壁,增多水印數(shù)量來(lái)鋪滿:
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();
}
}
只變動(dòng)了兩個(gè)值,即可達(dá)到效果歉备,如下所示
可以將上面labelWidth前后的的 *2,/2 改成 *3,/3 顯得更密集傅是,不過(guò)可能有人不喜歡這個(gè)太過(guò)平行的效果,讓人感覺有些半斜不斜
斜向密集平鋪效果
根據(jù)x的變化來(lái)增加pY
的值威创,同時(shí)和上面pX
的調(diào)整一樣落午,減小高度的邊距,使得整體變得更加傾斜和密集
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的值,對(duì)其進(jìn)行調(diào)整就能鋪出各種各樣的效果界拦,但記得調(diào)整好循環(huán)范圍吸申,鋪滿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();
// 文本寬度(用于計(jì)算間隔)
float textWidth = font.getWidth(text, size);
// 用正弦定理計(jì)算出水印高度
float labelHeight = (float) Math.sin(angle) * textWidth;
// 用勾股計(jì)算出旋轉(zhuǎn)后的水印寬度
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默認(rèn)是沒有中文字體的,要手動(dòng)導(dǎo)入中文字體的otf/ttf文件蚯嫌,這點(diǎn)就不贅述了
最后說(shuō)一個(gè)小問題值得注意哲虾,itext的定位是以左下角為基準(zhǔn)的,并非左上角