本文介紹在Java語言環境下,使用POI為Excel打水印的解決方案,具體的代碼編寫以及相關的注意事項。
需求描述:
要求通過系統下載的Excel都帶上公司的水印,列寬調整為合適的寬度,並且設置為不可編輯,即只讀。
即:
1:加水印;
2:調整列寬將單元格內容顯示全;
3:設置只讀;
解決方案思路介紹:
三點需求比較來說,以第一點比較復雜,同時網上關於POI為Excel加水印的資料非常少,而這些資料又多數是相互之間Copy得來,干貨較少。
一:使用模版Excel的形式設置水印:
目前網上關於POI為Excel打水印,主要是通過模版的形式來實現,即先准備一份打了水印的模版Excel,說白了就是在這個模版Excel中的一個Sheet中添加一些藝術字,調整下透明度還有字體,角度等,模仿水印,然后加載該模版,再將內容輸出到該模版中,以達到為Excel添加水印的目的,詳細可參考下面這份博客(不追溯該博客是否是原始版本):http://jsonchar.blog.163.com/blog/static/17601614120106135519213/
但這種方式有如下問題:
1:通過手動添加水印,水印數量固定:通過人工在一個Sheet中添加水印,個數必然固定,當實際內容行數不定,列數不定,會導致水印不能覆蓋內容;
2:當無法確定下載的Excel會有多少個Sheet的時候,無法保證輸出的多個Sheet都有水印:產生這種情況是因為,這種實現方式的根本原理是讀取已經存在的模版Excel內的Sheet,並將內容輸出到該Sheet中,也就是說如果模版內的Sheet內有水印則輸出內容有水印,如果該Sheet沒有水印則輸出內容沒有水印。畢竟通過手工添加水印,准備模版的方式,你不能准備無限多個模版Sheet,准備一個兩個也就到頭了。
3:你無法通過程序拷貝模版Sheet:你可能會問了,那我准備一個模版Sheet,在其中盡量多的打上水印,麻煩一點橫向縱向多大點,然后再通過程序拷貝該模版Sheet,想其中輸出內容的形式不行嗎?很遺憾,不行,翻查POI的API你就會發現創建Sheet的方式就那么兩個,即通過workbook進行創建,雖然sheet(無論類型)的構造函數是public的,但是你無法將你構造好的sheet添加到workbook中。當然,workbook也提供了cloneSheet的api,但是如果你的模版水印是通過藝術字或者圖片的形式打上去的,那么也將無法拷貝。
所以這種方案,適合於輸出的內容Sheet頁個數固定,行數可控的形式,因為只有這些固定下來了,在提供模版Excel的時候才能做出合適的模版,在固定的sheet頁,大致的行上打上水印。
二:使用程序將水印圖片動態打到Sheet上:
因為上面方案的問題,我選擇通過程序將提前准備好的水印圖片打印到Sheet上的方案,這種方案的主要步驟為:
1:將需要輸出的內容正常輸出到Excel中,輸出的Excel假定將存在多個Sheet,每個Sheet中內容的行數也不一定;
2:獲取workbook對象,提取其中的多個sheet;
3:調用工具類,傳入workbook和每一個sheet,根據提供好的水印圖片將睡衣圖片打到每一個sheet上,水印圖片在sheet上的位置,個數等可配置;
要求:
1:水印圖片是png格式,無背景,且水印主體顏色要盡可能淺,且細,避免遮擋excel上的信息;
2:使用的POI的版本是3.9,pom文件配置如下:
<!-- poi --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>3.9</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>3.9</version> </dependency>
步驟1和步驟2不提供代碼,網上有很多,直接貼步驟三的代碼,如下:
1 import java.awt.image.BufferedImage; 2 import java.io.ByteArrayOutputStream; 3 import java.io.IOException; 4 import java.io.InputStream; 5 6 import javax.imageio.ImageIO; 7 8 import org.apache.poi.ss.usermodel.ClientAnchor; 9 import org.apache.poi.ss.usermodel.Drawing; 10 import org.apache.poi.ss.usermodel.Picture; 11 import org.apache.poi.ss.usermodel.Sheet; 12 import org.apache.poi.ss.usermodel.Workbook; 13 14 /** 15 * Excel水印工具類 16 * @author gang.wang 17 * 2017年4月23日 18 */ 19 public class ExcelWaterRemarkUtils { 20 21 /** 22 * 為Excel打上水印工具函數 23 * 請自行確保參數值,以保證水印圖片之間不會覆蓋。 24 * 在計算水印的位置的時候,並沒有考慮到單元格合並的情況,請注意 25 * @param wb Excel Workbook 26 * @param sheet 需要打水印的Excel 27 * @param waterRemarkPath 水印地址,classPath,目前只支持png格式的圖片, 28 * 因為非png格式的圖片打到Excel上后可能會有圖片變紅的問題,且不容易做出透明效果。 29 * 同時請注意傳入的地址格式,應該為類似:"\\excelTemplate\\test.png" 30 * @param startXCol 水印起始列 31 * @param startYRow 水印起始行 32 * @param betweenXCol 水印橫向之間間隔多少列 33 * @param betweenYRow 水印縱向之間間隔多少行 34 * @param XCount 橫向共有水印多少個 35 * @param YCount 縱向共有水印多少個 36 * @param waterRemarkWidth 水印圖片寬度為多少列 37 * @param waterRemarkHeight 水印圖片高度為多少行 38 * @throws IOException 39 */ 40 public static void putWaterRemarkToExcel(Workbook wb, Sheet sheet, String waterRemarkPath, int startXCol, int startYRow, 41 int betweenXCol, int betweenYRow, int XCount, int YCount, 42 int waterRemarkWidth, int waterRemarkHeight) throws IOException{ 43 44 //校驗傳入的水印圖片格式 45 if(!waterRemarkPath.endsWith("png") && !waterRemarkPath.endsWith("PNG")){ 46 throw new RuntimeException("向Excel上面打印水印,目前支持png格式的圖片。"); 47 } 48 49 //加載圖片 50 ByteArrayOutputStream byteArrayOut = new ByteArrayOutputStream(); 51 InputStream imageIn = Thread.currentThread().getContextClassLoader().getResourceAsStream(waterRemarkPath); 52 if(null == imageIn || imageIn.available() < 1){ 53 throw new RuntimeException("向Excel上面打印水印,讀取水印圖片失敗(1)。"); 54 } 55 BufferedImage bufferImg = ImageIO.read(imageIn); 56 if(null == bufferImg) { 57 throw new RuntimeException("向Excel上面打印水印,讀取水印圖片失敗(2)。"); 58 } 59 ImageIO.write(bufferImg,"png",byteArrayOut); 60 61 //開始打水印 62 Drawing drawing = sheet.createDrawingPatriarch(); 63 64 //按照共需打印多少行水印進行循環 65 for(int yCount=0; yCount<YCount; yCount++){ 66 //按照每行需要打印多少個水印進行循環 67 for(int xCount=0; xCount<XCount; xCount++){ 68 //創建水印圖片位置 69 int xIndexInteger = startXCol + (xCount * waterRemarkWidth) + (xCount * betweenXCol); 70 int yIndexInteger = startYRow + (yCount * waterRemarkHeight) + (yCount * betweenYRow); 71 72 /* 73 * 參數定義: 74 * 第一個參數是(x軸的開始節點); 75 * 第二個參數是(是y軸的開始節點); 76 * 第三個參數是(是x軸的結束節點); 77 * 第四個參數是(是y軸的結束節點); 78 * 第五個參數是(是從Excel的第幾列開始插入圖片,從0開始計數); 79 * 第六個參數是(是從excel的第幾行開始插入圖片,從0開始計數); 80 * 第七個參數是(圖片寬度,共多少列); 81 * 第8個參數是(圖片高度,共多少行); 82 */ 83 ClientAnchor anchor = drawing.createAnchor(0, 0, Short.MAX_VALUE, Integer.MAX_VALUE, xIndexInteger, yIndexInteger, waterRemarkWidth, waterRemarkHeight); 84 Picture pic = drawing.createPicture(anchor, wb.addPicture(byteArrayOut.toByteArray(), Workbook.PICTURE_TYPE_PNG)); 85 pic.resize(); 86 } 87 } 88 } 89 }
函數參數的定義介紹的很詳細了,不再過多介紹,說說需要注意的地方:
1:一定要先向Excel中寫內容,然后再打日志,要不然圖片有可能會拉伸導致失真;
2:最好使用Png格式的圖片,避免造成打上的圖片背景變紅的問題,網上有這種問題的解決方案,這里不再贅述;
3:該函數默認是將圖片放在了項目的classpath目錄下;
至此,使用poi向Excel上面打水印的功能就完成了!!!可能會有寫遮擋,但也實在無法避免了,目前還沒有發現類似於設置圖片透明度,文字環繞效果,圖片置底的api~~
接下來再說兩個小點:
1:怎樣實現調整列寬:
Sheet提供了默認的自動設置列寬的api,即根據cell中內容的寬度,自動調整列寬(sheet.autoSizeColumnt),但是經過測試,在SXSSFWorkbook和SXSSFSheet中,會遇到個別列特別窄的問題,就是計算的行寬不太准,而且遇到了公式,數字也有可能計算的不太准,因此我是自己計算的列寬,並設置的。
首先在程序循環向Excel中寫內容的時候,自行設置一個Map存儲每一列的最大列寬,具體的函數很簡單,就是一個比較大小的函數,舉例如下:
1 private void getMaxColLength(Map<Integer,Integer> colMaxLength, Integer colIndex, Integer length){ 2 Integer oriLength = 0; 3 if(colMaxLength.containsKey(colIndex)){ 4 oriLength = colMaxLength.get(colIndex); 5 } 6 7 if(length > oriLength){ 8 colMaxLength.put(colIndex, length); 9 } else { 10 colMaxLength.put(colIndex, oriLength); 11 } 12 }
得到的每一列的最大寬度(調用字符串的length()函數),需要和Excel中的寬度進行換算,換算的公式是寬度*256,即程序求得的每一列的寬度乘以256,在實際使用中,大多數情況需要多乘以一些,因為實測中,如果只乘以256,會使得列正正好好的匹配內容寬度,遇到漢字還有可能遮擋一部分,因此最好將這個基數擴大些:
sheet.setColumnWidth(colIndex, length * 480);
2:如何設置Excel只讀,或者說是不可編輯:
當Excel中的數據敏感,希望客戶不能修改內容時,除了使用水印的形式,還需要配合不可修改功能,我的實現是為Excel設置一個密碼,保證該Excel不可修改,試圖修改將會被要求輸入密碼,只要密碼夠復雜應該沒有破解的可能,比如你設置一個隨機的UUID,並且不記錄該UUID,這樣就沒有人知道該密碼了,保證安全性。在設置了密碼之后用戶依然可以通過拷貝的方法,將Excel中的內容拷貝到其他sheet或者Excel中去,這點需要注意,具體的代碼很簡單,如下:
sheet.protectSheet(UUID.randomUUID().toString());
到這,本文要介紹的主體內容就介紹完了,實現了一個通過POI,向Excel中打水印圖片的主體功能。希望對大家有所幫助。