天天看點

一般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;下面我們将一張原圖用下面的幾句代碼分别調用不同的參數生成圖檔看看:

·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······15001.for(int i=1;i<=13;i++){  

02.        tempImage=new BufferedImage(width, height, i);  

03.        g2D = (Graphics2D) tempImage.getGraphics();  

04.        g2D.drawImage(sourceImage, 0, 0, null);  

05.        ImageIO.write(tempImage, "png", new File("cut/c_com_"+i+".png"));  

06.    } 

    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:

一般PNG圖檔壓縮的Java實作

     壓縮後的圖檔:

一般PNG圖檔壓縮的Java實作
一般PNG圖檔壓縮的Java實作
一般PNG圖檔壓縮的Java實作
一般PNG圖檔壓縮的Java實作
一般PNG圖檔壓縮的Java實作
一般PNG圖檔壓縮的Java實作
一般PNG圖檔壓縮的Java實作
一般PNG圖檔壓縮的Java實作
一般PNG圖檔壓縮的Java實作
一般PNG圖檔壓縮的Java實作
一般PNG圖檔壓縮的Java實作
一般PNG圖檔壓縮的Java實作
一般PNG圖檔壓縮的Java實作

      從圖檔看到,黑白照片最小,不過這不是我們想要,排除;最後一張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生成的圖檔背景卻是不透明的,自然而然的我們想到了把不透明的背景替換成透明的不就行 了。·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······15001. 

06. private static BufferedImage getConvertedImage(BufferedImage image){  

07.     int width=image.getWidth();  

08.     int height=image.getHeight();  

09.     BufferedImage convertedImage=null;  

10.     Graphics2D g2D=null;  

11.     //采用帶1 位元組alpha的TYPE_4BYTE_ABGR,可以修改像素的布爾透明  

12.     convertedImage=new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);  

13.     g2D = (Graphics2D) convertedImage.getGraphics();  

14.     g2D.drawImage(image, 0, 0, null);  

15.     //像素替換,直接把背景顔色的像素替換成0  

16.     for(int i=0;i<width;i++){  

17.         for(int j=0;j<height;j++){  

18.             int rgb=convertedImage.getRGB(i, j);  

19.             if(isBackPixel(rgb)){  

20.                 convertedImage.setRGB(i, j,0);  

21.             }  

22.         }  

23.     }  

24.     g2D.drawImage(convertedImage, 0, 0, null);  

25.     return convertedImage;  

26. } 

    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);

        //像素替換,直接把背景顔色的像素替換成0

        for(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)用于判斷目前像素是否為背景像素:view plaincopy to clipboardprint?

·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······15001. 

06.private static boolean isBackPixel(int pixel){  

07.    int back[]={-16777216};  

08.    for(int i=0;i<back.length;i++){  

09.        if(back[i]==pixel) return true;  

10.    }  

11.    return false;  

12.} 

    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;

    }

   經轉化後的圖檔如下:

一般PNG圖檔壓縮的Java實作

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

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

·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······15001. 

07.public static BufferedImage compressImage(BufferedImage sourceImage) throws IOException{  

08.    if(sourceImage==null) throw new NullPointerException("空圖檔");  

09.    BufferedImage cutedImage=null;  

10.    BufferedImage tempImage=null;  

11.    BufferedImage compressedImage=null;  

12.    Graphics2D g2D=null;  

13.    //圖檔自動裁剪  

14.    cutedImage=cutImageAuto(sourceImage);  

15.    int width=cutedImage.getWidth();  

16.    int height=cutedImage.getHeight();  

17.    //圖檔格式為555格式  

18.    tempImage=new BufferedImage(width, height, BufferedImage.TYPE_USHORT_555_RGB);  

19.    g2D = (Graphics2D) tempImage.getGraphics();  

20.    g2D.drawImage(sourceImage, 0, 0, null);  

21.    compressedImage=getConvertedImage(tempImage);  

22.    //經過像素轉化後的圖檔  

23.    compressedImage=new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);  

24.    g2D = (Graphics2D) compressedImage.getGraphics();  

25.    g2D.drawImage(tempImage, 0, 0, null);  

26.    int pixel[]=new int[width*height];  

27.    int sourcePixel[]=new int[width*height];  

28.    int currentPixel[]=new int[width*height];  

29.    sourcePixel=cutedImage.getRGB(0, 0, width, height, sourcePixel, 0, width);  

30.    currentPixel=tempImage.getRGB(0, 0, width, height, currentPixel, 0, width);  

31.    for(int i=0;i<currentPixel.length;i++){  

32.        if(i==0 || i==currentPixel.length-1){  

33.            pixel[i]=0;  

34.        //内部像素  

35.        }else if(i>width && i<currentPixel.length-width){  

36.            int bef=currentPixel[i-1];  

37.            int now=currentPixel[i];  

38.            int aft=currentPixel[i+1];  

39.            int up=currentPixel[i-width];  

40.            int down=currentPixel[i+width];  

41.            //背景像素直接置為0  

42.            if(isBackPixel(now)){  

43.                pixel[i]=0;  

44.            //邊框像素和原圖一樣  

45.            }else if((!isBackPixel(now) && isBackPixel(bef))  

46.                    ||(!isBackPixel(now) && isBackPixel(aft))  

47.                    ||(!isBackPixel(now) && isBackPixel(up))  

48.                    ||(!isBackPixel(now) &&isBackPixel(down))  

49.                   ){  

50.                pixel[i]=sourcePixel[i];  

51.            //其他像素和555壓縮後的像素一樣  

52.            }else{  

53.                pixel[i]=now;  

54.            }  

55.        //邊界像素  

56.        }else{  

57.            int bef=currentPixel[i-1];  

58.            int now=currentPixel[i];  

59.            int aft=currentPixel[i+1];  

60.            if(isBackPixel(now)){  

61.                pixel[i]=0;  

62.            }else if((!isBackPixel(now) && isBackPixel(bef))  

63.                    ||(!isBackPixel(now) && isBackPixel(aft))){  

64.                pixel[i]=sourcePixel[i];  

65.            }else{  

66.                pixel[i]=now;  

67.            }  

68.        }  

69.    }  

70.    compressedImage.setRGB(0, 0, width, height, pixel, 0, width);  

71.    g2D.drawImage(compressedImage, 0, 0, null);  

72.    ImageIO.write(cutedImage, "png", new File("cut/a_cut.png"));  

73.    ImageIO.write(tempImage, "png", new File("cut/b_555.png"));  

74.    ImageIO.write(compressedImage, "png", new File("cut/c_com.png"));  

75.    return compressedImage;  

76.} 

    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];

                //背景像素直接置為0

                if(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);是對原圖進行裁剪,代碼如 下:·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······15001. 

06.public static BufferedImage cutImageAuto(BufferedImage image){  

07.    Rectangle area=getCutAreaAuto(image);  

08.    return image.getSubimage(area.x, area.y,area.width, area.height);  

09.}  

10. 

11. 

16.private static Rectangle getCutAreaAuto(BufferedImage image){  

17.    if(image==null) throw new NullPointerException("圖檔為空");  

18.    int width=image.getWidth();  

19.    int height=image.getHeight();  

20.    int startX=width;  

21.    int startY=height;  

22.    int endX=0;  

23.    int endY=0;  

24.    int []pixel=new int[width*height];  

25. 

26.    pixel=image.getRGB(0, 0, width, height, pixel, 0, width);  

27.    for(int i=0;i<pixel.length;i++){  

28.        if(isCutBackPixel(pixel[i])) continue;  

29.        else{  

30.            int w=i%width;  

31.            int h=i/width;  

32.            startX=(w<startX)?w:startX;  

33.            startY=(h<startY)?h:startY;  

34.            endX=(w>endX)?w:endX;  

35.            endY=(h>endY)?h:endY;  

36.        }  

37.    }  

38.    if(startX>endX || startY>endY){  

39.        startX=startY=0;  

40.        endX=width;  

41.        endY=height;  

42.    }  

43.    return new Rectangle(startX, startY, endX-startX, endY-startY);  

44.}  

45. 

46. 

51.private static boolean isCutBackPixel(int pixel){  

52.    int back[]={0,8224125,16777215,8947848,460551,4141853,8289918};  

53.    for(int i=0;i<back.length;i++){  

54.        if(back[i]==pixel) return true;  

55.    }  

56.    return false;  

57.} 

    public static BufferedImage cutImageAuto(BufferedImage image){

        Rectangle area=getCutAreaAuto(image);

        return image.getSubimage(area.x, area.y,area.width, area.height);

    }

    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);

    }

    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;

    }

   改善後得到的圖檔:

一般PNG圖檔壓縮的Java實作

   實際上,這種方法隻适用于圖檔顔色分明(邊框顔色分明,背景顔色唯一),黑色像素不多的圖檔。一些比較特殊的圖檔就得特殊處理了,如以下圖檔:

一般PNG圖檔壓縮的Java實作
一般PNG圖檔壓縮的Java實作

       壓縮後       

一般PNG圖檔壓縮的Java實作
一般PNG圖檔壓縮的Java實作

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

http://hi.baidu.com/bluesnake/blog/item/79c39ddda37f2f205882dd54.html