一般PNG图片压缩的Java实现
????? 由于對資源或網速的要求,在手機游戲或一般的網頁游戲中,希望能對圖片進最大可能的壓縮,以節省資源。最近公司做的項目也有對這方面的需求,于是我在網上逛了半天,希望能發現現成版的Java方法可以使用(用程序來壓縮而不借助于工具,要不然2萬多張的圖片你想累死人?雖然PS有批量功能,它卻無法按原來的路徑存放);失望的是,好像沒發現什么能直接使用代碼,哪怕是提個解決方案也很少。既然網上找不到合適的,那就自己動手,豐衣足食。
????? 關于PNG圖片的格式我在此就不多說,圖片壓縮方面的理論知識我也不在這多此一舉,網上資料一大堆。開門見山,我們的目標是怎樣用Java把PNG圖片盡最大可能的壓縮;當然,不能看出明顯的失真。
????? 一:BufferedImage類
????? 在Java中,關于圖片處理我們自然而然的想到了BufferedImage類,深入了解它,你會發現其實Java已經幫我們做好了圖片壓縮了,只是壓縮完的圖片和我們的需求有一點點偏差.......先看看BufferedImage最常用的構造方法:
?public BufferedImage(int width,int height,int imageType);
????? 構造一個類型為預定義圖像類型之一的 BufferedImage,其中imageType有以下幾種:
BufferedImage.TYPE_INT_RGB:8 位 RGB 顏色分量,不帶alpha通道。
BufferedImage.TYPE_INT_ARGB:8 位 RGBA 顏色分量,帶alpha通道。
BufferedImage.TYPE_INT_ARGB_PRE:8 位 RGBA 顏色分量,已預乘以 alpha。
BufferedImage.TYPE_INT_BGR:8 位 RGB 顏色分量Windows 或 Solaris 風格的圖像,不帶alpha通道。
BufferedImage.TYPE_3BYTE_BGR:8位GBA顏色分量,用3字節存儲Blue、Green和Red三種顏色,不存在alpha。
BufferedImage.TYPE_4BYTE_ABGR:8位RGBA顏色分量,用3字節存儲Blue、Green和Red三種顏色以及1字節alpha。
BufferedImage.TYPE_4BYTE_ABGR_PRE:具有用3字節存儲的Blue、Green和Red三種顏色以及1字節alpha。
BufferedImage.TYPE_USHORT_565_RGB:具有5-6-5RGB顏色分量(5位Red、6位Green、5位Blue)的圖像,不帶alpha。
BufferedImage.TYPE_USHORT_555_RGB:具有5-5-5RGB顏色分量(5位Red、5位Green、5位Blue)的圖像,不帶alpha。
BufferedImage.TYPE_BYTE_GRAY:表示無符號byte灰度級圖像(無索引)。
BufferedImage.TYPE_USHORT_GRAY:表示一個無符號short 灰度級圖像(無索引)。
BufferedImage.TYPE_BYTE_BINARY:表示一個不透明的以字節打包的 1、2 或 4 位圖像。
BufferedImage.TYPE_BYTE_INDEXED:表示帶索引的字節圖像。??
??????其實imageType就是對應著Java內不同格式的壓縮方法,編號分別為1-13;下面我們將一張原圖用下面的幾句代碼分別調用不同的參數生成圖片看看:
for(int i=1;i<=13;i++){tempImage=new BufferedImage(width, height, i);g2D = (Graphics2D) tempImage.getGraphics();g2D.drawImage(sourceImage, 0, 0, null);ImageIO.write(tempImage, "png", new File("cut/c_com_"+i+".png"));}
??????原圖如下,PNG格式,大小24.0KB:
?????壓縮后的圖片:
??? ??? ? ? ?? ?? ??? ? ??
??????從圖片看到,黑白照片最小,不過這不是我們想要,排除;最后一張TYPE_BYTE_INDEXED類型的(其實就是PNG8)是彩色,也不大,但是失真太厲害了,排除;剩下的透明的那幾個大小都一樣,排除;對比剩下背景不透明的那幾張,TYPE_USHORT_555_RGB就是我們要的壓縮類型了。
?????? 二:555格式的位圖
??? 555格式其實是16位位圖中的一種。16位位圖最多有65536種顏色。每個色素用16位(2個字節)表示。這種格式叫作高彩色,或叫增強型16位色,或64K色。16位中,最低的5位表示藍色分量,中間的5位表示綠色分量,高的5位表示紅色分量,一共占用了15位,最高的一位保留,設為0。在555格式下,紅、綠、藍的掩碼分別是:0x7C00、0x03E0、0x001F(在BufferedImage源碼中也有定義)。
????? 三:進一步處理
????? 從圖片效果可以看出,555格式非常接近真彩色了,而圖像數據又比真彩圖像小的多,非常滿足我們的要求。但是我們需要背景是透明的,而用TYPE_USHORT_555_RGB生成的圖片背景卻是不透明的,自然而然的我們想到了把不透明的背景替換成透明的不就行了。 /*** 將背景為黑色不透明的圖片轉化為背景透明的圖片* @param image 背景為黑色不透明的圖片(用555格式轉化后的都是黑色不透明的)* @return 轉化后的圖片*/private static BufferedImage getConvertedImage(BufferedImage image){int width=image.getWidth();int height=image.getHeight();BufferedImage convertedImage=null;Graphics2D g2D=null;//采用帶1 字節alpha的TYPE_4BYTE_ABGR,可以修改像素的布爾透明convertedImage=new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);g2D = (Graphics2D) convertedImage.getGraphics();g2D.drawImage(image, 0, 0, null);//像素替換,直接把背景顏色的像素替換成0for(int i=0;i<width;i++){for(int j=0;j<height;j++){int rgb=convertedImage.getRGB(i, j);if(isBackPixel(rgb)){convertedImage.setRGB(i, j,0);}}}g2D.drawImage(convertedImage, 0, 0, null);return convertedImage;}?
?? 其中的isBackPixel(rgb)用于判斷當前像素是否為背景像素: /*** 判斷當前像素是否為黑色不透明的像素(-16777216)* @param pixel 要判斷的像素* @return 是背景像素返回true,否則返回false*/private static boolean isBackPixel(int pixel){int back[]={-16777216};for(int i=0;i<back.length;i++){if(back[i]==pixel) return true;}return false;}
?? 經轉化后的圖片如下:
??
?? 轉化后稍微大了一點,這個可以接受;要命的是帶了一個黑色邊框。為什么呢?原因很簡單,原圖中邊框部分的像素是介于透明和不透明之間的,而經過555格式壓縮后所有像素都變成了布爾透明,也就是說所有的像素要么是透明的要么就是不透明的。
???最容易想到的方法就是把邊框的像素換成原圖邊框的像素,關鍵在于怎么判斷當前像素是否為圖片的邊框像素,這個算法可能得花費你一定的時間,下面只是我想到的一種實現:
/*** 圖片壓縮* @param sourceImage 要壓縮的圖片* @return 壓縮后的圖片* @throws IOException 圖片讀寫異常*/public static BufferedImage compressImage(BufferedImage sourceImage) throws IOException{if(sourceImage==null) throw new NullPointerException("空圖片");BufferedImage cutedImage=null;BufferedImage tempImage=null;BufferedImage compressedImage=null;Graphics2D g2D=null;//圖片自動裁剪cutedImage=cutImageAuto(sourceImage);int width=cutedImage.getWidth();int height=cutedImage.getHeight();//圖片格式為555格式tempImage=new BufferedImage(width, height, BufferedImage.TYPE_USHORT_555_RGB);g2D = (Graphics2D) tempImage.getGraphics();g2D.drawImage(sourceImage, 0, 0, null);compressedImage=getConvertedImage(tempImage);//經過像素轉化后的圖片compressedImage=new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);g2D = (Graphics2D) compressedImage.getGraphics();g2D.drawImage(tempImage, 0, 0, null);int pixel[]=new int[width*height];int sourcePixel[]=new int[width*height];int currentPixel[]=new int[width*height];sourcePixel=cutedImage.getRGB(0, 0, width, height, sourcePixel, 0, width);currentPixel=tempImage.getRGB(0, 0, width, height, currentPixel, 0, width);for(int i=0;i<currentPixel.length;i++){if(i==0 || i==currentPixel.length-1){pixel[i]=0;//內部像素}else if(i>width && i<currentPixel.length-width){int bef=currentPixel[i-1];int now=currentPixel[i];int aft=currentPixel[i+1];int up=currentPixel[i-width];int down=currentPixel[i+width];//背景像素直接置為0if(isBackPixel(now)){pixel[i]=0;//邊框像素和原圖一樣}else if((!isBackPixel(now) && isBackPixel(bef))||(!isBackPixel(now) && isBackPixel(aft))||(!isBackPixel(now) && isBackPixel(up))||(!isBackPixel(now) &&isBackPixel(down))){pixel[i]=sourcePixel[i];//其他像素和555壓縮后的像素一樣}else{pixel[i]=now;}//邊界像素}else{int bef=currentPixel[i-1];int now=currentPixel[i];int aft=currentPixel[i+1];if(isBackPixel(now)){pixel[i]=0;}else if((!isBackPixel(now) && isBackPixel(bef))||(!isBackPixel(now) && isBackPixel(aft))){pixel[i]=sourcePixel[i];}else{pixel[i]=now;}}}compressedImage.setRGB(0, 0, width, height, pixel, 0, width);g2D.drawImage(compressedImage, 0, 0, null);ImageIO.write(cutedImage, "png", new File("cut/a_cut.png"));ImageIO.write(tempImage, "png", new File("cut/b_555.png"));ImageIO.write(compressedImage, "png", new File("cut/c_com.png"));return compressedImage;}
???其中的cutedImage=cutImageAuto(sourceImage);是對原圖進行裁剪,代碼如下: /*** 圖片自動裁剪* @param image 要裁剪的圖片* @return 裁剪后的圖片*/public static BufferedImage cutImageAuto(BufferedImage image){Rectangle area=getCutAreaAuto(image);return image.getSubimage(area.x, area.y,area.width, area.height);}/*** 獲得裁剪圖片保留區域* @param image 要裁剪的圖片* @return 保留區域*/private static Rectangle getCutAreaAuto(BufferedImage image){if(image==null) throw new NullPointerException("圖片為空");int width=image.getWidth();int height=image.getHeight();int startX=width;int startY=height;int endX=0;int endY=0;int []pixel=new int[width*height];pixel=image.getRGB(0, 0, width, height, pixel, 0, width);for(int i=0;i<pixel.length;i++){if(isCutBackPixel(pixel[i])) continue;else{int w=i%width;int h=i/width;startX=(w<startX)?w:startX;startY=(h<startY)?h:startY;endX=(w>endX)?w:endX;endY=(h>endY)?h:endY;}}if(startX>endX || startY>endY){startX=startY=0;endX=width;endY=height;}return new Rectangle(startX, startY, endX-startX, endY-startY);}/*** 當前像素是否為背景像素* @param pixel* @return*/private static boolean isCutBackPixel(int pixel){int back[]={0,8224125,16777215,8947848,460551,4141853,8289918};for(int i=0;i<back.length;i++){if(back[i]==pixel) return true;}return false;}
?? 改善后得到的圖片:
??
???實際上,這種方法只適用于圖片顏色分明(邊框顏色分明,背景顏色唯一),黑色像素不多的圖片。一些比較特殊的圖片就得特殊處理了,如以下圖片:
?? ??????????壓縮后???????? ????
?? 原因是黑色不透明像素也是圖片實體的一部分,這樣就把它替換成白色透明的了。可以把代碼改一下,但是圖片的大小會增加不少,就是把程序認為是背景顏色的像素替換成原圖片的像素;將compressImage()方法中的第33、43、61行改成 pixel[i]=sourcePixel[i]; 即可。
?
總結
以上是生活随笔為你收集整理的一般PNG图片压缩的Java实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: transformer序列预测示例
- 下一篇: Kettle 将查询SQL导出的 Exc