一般PNG圖片壓縮的Java實現

來源:互聯網
上載者:User

      由於對資源或網速的要求,在手機遊戲或一般的網頁遊戲中,希望能對圖片進最大可能的壓縮,以節省資源。最近公司做的項目也有對這方面的需求,於是我在網上逛了半天,希望能發現現成版的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++){<br /> tempImage=new BufferedImage(width, height, i);<br /> g2D = (Graphics2D) tempImage.getGraphics();<br /> g2D.drawImage(sourceImage, 0, 0, null);<br /> ImageIO.write(tempImage, "png", new File("cut/c_com_"+i+".png"));<br /> }

      原圖如下,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產生的圖片背景卻是不透明的,自然而然的我們想到了把不透明的背景替換成透明的不就行了。 /**<br /> * 將背景為黑色不透明的圖片轉化為背景透明的圖片<br /> * @param image 背景為黑色不透明的圖片(用555格式轉化後的都是黑色不透明的)<br /> * @return 轉化後的圖片<br /> */<br /> private static BufferedImage getConvertedImage(BufferedImage image){<br /> int width=image.getWidth();<br /> int height=image.getHeight();<br /> BufferedImage convertedImage=null;<br /> Graphics2D g2D=null;<br /> //採用帶1 位元組alpha的TYPE_4BYTE_ABGR,可以修改像素的布爾透明<br /> convertedImage=new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);<br /> g2D = (Graphics2D) convertedImage.getGraphics();<br /> g2D.drawImage(image, 0, 0, null);<br /> //像素替換,直接把背景顏色的像素替換成0<br /> for(int i=0;i<width;i++){<br /> for(int j=0;j<height;j++){<br /> int rgb=convertedImage.getRGB(i, j);<br /> if(isBackPixel(rgb)){<br /> convertedImage.setRGB(i, j,0);<br /> }<br /> }<br /> }<br /> g2D.drawImage(convertedImage, 0, 0, null);<br /> return convertedImage;<br /> } 

   其中的isBackPixel(rgb)用於判斷當前像素是否為背景像素: /**<br /> * 判斷當前像素是否為黑色不透明的像素(-16777216)<br /> * @param pixel 要判斷的像素<br /> * @return 是背景像素返回true,否則返回false<br /> */<br /> private static boolean isBackPixel(int pixel){<br /> int back[]={-16777216};<br /> for(int i=0;i<back.length;i++){<br /> if(back[i]==pixel) return true;<br /> }<br /> return false;<br /> }

   經轉化後的圖片如下:

  

   轉化後稍微大了一點,這個可以接受;要命的是帶了一個黑色邊框。為什麼呢?原因很簡單,原圖中邊框部分的像素是介於透明和不透明之間的,而經過555格式壓縮後所有像素都變成了布爾透明,也就是說所有的像素要麼是透明的要麼就是不透明的。

   最容易想到的方法就是把邊框的像素換成原圖邊框的像素,關鍵在於怎麼判斷當前像素是否為圖片的邊框像素,這個演算法可能得花費你一定的時間,下面只是我想到的一種實現:

/**<br /> * 圖片壓縮<br /> * @param sourceImage 要壓縮的圖片<br /> * @return 壓縮後的圖片<br /> * @throws IOException 圖片讀寫異常<br /> */<br /> public static BufferedImage compressImage(BufferedImage sourceImage) throws IOException{<br /> if(sourceImage==null) throw new NullPointerException("空圖片");<br /> BufferedImage cutedImage=null;<br /> BufferedImage tempImage=null;<br /> BufferedImage compressedImage=null;<br /> Graphics2D g2D=null;<br /> //圖片自動裁剪<br /> cutedImage=cutImageAuto(sourceImage);<br /> int width=cutedImage.getWidth();<br /> int height=cutedImage.getHeight();<br /> //圖片格式為555格式<br /> tempImage=new BufferedImage(width, height, BufferedImage.TYPE_USHORT_555_RGB);<br /> g2D = (Graphics2D) tempImage.getGraphics();<br /> g2D.drawImage(sourceImage, 0, 0, null);<br /> compressedImage=getConvertedImage(tempImage);<br /> //經過像素轉化後的圖片<br /> compressedImage=new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);<br /> g2D = (Graphics2D) compressedImage.getGraphics();<br /> g2D.drawImage(tempImage, 0, 0, null);<br /> int pixel[]=new int[width*height];<br /> int sourcePixel[]=new int[width*height];<br /> int currentPixel[]=new int[width*height];<br /> sourcePixel=cutedImage.getRGB(0, 0, width, height, sourcePixel, 0, width);<br /> currentPixel=tempImage.getRGB(0, 0, width, height, currentPixel, 0, width);<br /> for(int i=0;i<currentPixel.length;i++){<br /> if(i==0 || i==currentPixel.length-1){<br /> pixel[i]=0;<br /> //內部像素<br /> }else if(i>width && i<currentPixel.length-width){<br /> int bef=currentPixel[i-1];<br /> int now=currentPixel[i];<br /> int aft=currentPixel[i+1];<br /> int up=currentPixel[i-width];<br /> int down=currentPixel[i+width];<br /> //背景像素直接置為0<br /> if(isBackPixel(now)){<br /> pixel[i]=0;<br /> //邊框像素和原圖一樣<br /> }else if((!isBackPixel(now) && isBackPixel(bef))<br /> ||(!isBackPixel(now) && isBackPixel(aft))<br /> ||(!isBackPixel(now) && isBackPixel(up))<br /> ||(!isBackPixel(now) &&isBackPixel(down))<br /> ){<br /> pixel[i]=sourcePixel[i];<br /> //其他像素和555壓縮後的像素一樣<br /> }else{<br /> pixel[i]=now;<br /> }<br /> //邊界像素<br /> }else{<br /> int bef=currentPixel[i-1];<br /> int now=currentPixel[i];<br /> int aft=currentPixel[i+1];<br /> if(isBackPixel(now)){<br /> pixel[i]=0;<br /> }else if((!isBackPixel(now) && isBackPixel(bef))<br /> ||(!isBackPixel(now) && isBackPixel(aft))){<br /> pixel[i]=sourcePixel[i];<br /> }else{<br /> pixel[i]=now;<br /> }<br /> }<br /> }<br /> compressedImage.setRGB(0, 0, width, height, pixel, 0, width);<br /> g2D.drawImage(compressedImage, 0, 0, null);<br /> ImageIO.write(cutedImage, "png", new File("cut/a_cut.png"));<br /> ImageIO.write(tempImage, "png", new File("cut/b_555.png"));<br /> ImageIO.write(compressedImage, "png", new File("cut/c_com.png"));<br /> return compressedImage;<br /> }

   其中的cutedImage=cutImageAuto(sourceImage);是對原圖進行裁剪,代碼如下: /**<br /> * 圖片自動裁剪<br /> * @param image 要裁剪的圖片<br /> * @return 裁剪後的圖片<br /> */<br /> public static BufferedImage cutImageAuto(BufferedImage image){<br /> Rectangle area=getCutAreaAuto(image);<br /> return image.getSubimage(area.x, area.y,area.width, area.height);<br /> }</p><p> /**<br /> * 獲得裁剪圖片保留地區<br /> * @param image 要裁剪的圖片<br /> * @return 保留地區<br /> */<br /> private static Rectangle getCutAreaAuto(BufferedImage image){<br /> if(image==null) throw new NullPointerException("圖片為空白");<br /> int width=image.getWidth();<br /> int height=image.getHeight();<br /> int startX=width;<br /> int startY=height;<br /> int endX=0;<br /> int endY=0;<br /> int []pixel=new int[width*height];</p><p> pixel=image.getRGB(0, 0, width, height, pixel, 0, width);<br /> for(int i=0;i<pixel.length;i++){<br /> if(isCutBackPixel(pixel[i])) continue;<br /> else{<br /> int w=i%width;<br /> int h=i/width;<br /> startX=(w<startX)?w:startX;<br /> startY=(h<startY)?h:startY;<br /> endX=(w>endX)?w:endX;<br /> endY=(h>endY)?h:endY;<br /> }<br /> }<br /> if(startX>endX || startY>endY){<br /> startX=startY=0;<br /> endX=width;<br /> endY=height;<br /> }<br /> return new Rectangle(startX, startY, endX-startX, endY-startY);<br /> }</p><p> /**<br /> * 當前像素是否為背景像素<br /> * @param pixel<br /> * @return<br /> */<br /> private static boolean isCutBackPixel(int pixel){<br /> int back[]={0,8224125,16777215,8947848,460551,4141853,8289918};<br /> for(int i=0;i<back.length;i++){<br /> if(back[i]==pixel) return true;<br /> }<br /> return false;<br /> }

   改善後得到的圖片:

  

   實際上,這種方法只適用於圖片顏色分明(邊框顏色分明,背景顏色唯一),黑色像素不多的圖片。一些比較特殊的圖片就得特殊處理了,如以片:

             壓縮後             

   原因是黑色不透明像素也是圖片實體的一部分,這樣就把它替換成白色透明的了。可以把代碼改一下,但是圖片的大小會增加不少,就是把程式認為是背景顏色的像素替換成原圖片的像素;將compressImage()方法中的第33、43、61行改成 pixel[i]=sourcePixel[i]; 即可。

 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.