使用itext7在PDF中實(shí)現(xiàn)多種文字水印效果

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ì)算分如下三步:

  1. 計(jì)算出文本的寬度港粱,在itext中這很簡(jiǎn)單,他的PdfFont.getWidth()就能算出來(lái)這個(gè)值
  2. 根據(jù)這個(gè)寬度和設(shè)定的角度用正弦定理計(jì)算出旋轉(zhuǎn)后的高度
  3. 根據(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)的,并非左上角

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末择示,一起剝皮案震驚了整個(gè)濱河市束凑,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌栅盲,老刑警劉巖汪诉,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異谈秫,居然都是意外死亡扒寄,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門拟烫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)该编,“玉大人,你說(shuō)我怎么就攤上這事构灸∩峡剩” “怎么了岸梨?”我有些...
    開封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)稠氮。 經(jīng)常有香客問我曹阔,道長(zhǎng),這世上最難降的妖魔是什么隔披? 我笑而不...
    開封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任赃份,我火速辦了婚禮,結(jié)果婚禮上奢米,老公的妹妹穿的比我還像新娘抓韩。我一直安慰自己,他們只是感情好鬓长,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開白布谒拴。 她就那樣靜靜地躺著,像睡著了一般涉波。 火紅的嫁衣襯著肌膚如雪英上。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天啤覆,我揣著相機(jī)與錄音苍日,去河邊找鬼。 笑死窗声,一個(gè)胖子當(dāng)著我的面吹牛相恃,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播笨觅,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼拦耐,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了屋摇?” 一聲冷哼從身側(cè)響起揩魂,我...
    開封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎炮温,沒想到半個(gè)月后火脉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡柒啤,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年倦挂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片担巩。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡方援,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出涛癌,到底是詐尸還是另有隱情犯戏,我是刑警寧澤送火,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站先匪,受9級(jí)特大地震影響种吸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜呀非,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一坚俗、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧岸裙,春花似錦猖败、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至拟糕,卻和暖如春判呕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背送滞。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留辱挥,地道東北人犁嗅。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像晤碘,于是被迫代替她去往敵國(guó)和親褂微。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容