java中通過png轉索引圖壓縮圖片

2020-08-12 18:14:51

背景

需求是大大的減小多媒體簡訊圖片的體積,可以發送更多數量的多媒體簡訊圖片,圖片的內容是報表,顏色單一。最開始把透明色去掉了,位深從32->24,但是最近測試發現多媒體簡訊圖片體積還是太大了,這就需要去學習圖片壓縮的知識了

png格式簡介

PNG格式有8位元、24位元、32位元三種形式,其中8位元PNG支援兩種不同的透明形式(索引透明和alpha透明),24位元PNG不支援透明,32位元PNG在24位元基礎上增加了8位元透明通道,因此可展現256級透明程度。位深度越大圖片體積越大,能夠表示的色彩越豐富。

png有種索引彩色模式,採用8位元調色板將RGB彩色影象轉換爲索引彩色影象。影象中儲存的不再是各個畫素的彩色資訊,而是從影象中挑選出來的具有代表性的顏色編號,每一編號對應一種顏色,影象的數據量也因此減少,這對彩色影象的傳播非常有利。

通過索引png,可以大大減小圖片體積,如果圖片中的顏色種類小於256,那麼就可以使用索引格式的PNG,如果圖片原本的顏色大於256種,那麼可以通過向量量化的方法來建立一個索引PNG格式(有失真壓縮)

上面只是一個簡單的介紹,如果要深入瞭解可以看看:
https://www.jianshu.com/p/5ad19825a3d0
https://www.jianshu.com/p/324744087e24

圖片壓縮實操

我這裏的場景非常適合png索引圖模式,因爲圖片內容是表格報表,顏色最多隻有4種,遠遠小於索引圖的256色,如果顏色豐富的那種照片之類的還是用jpg壓縮吧。。。

這裏重點介紹png量化轉索引圖
看網上框架有:tinypng、pngquant、OpenViewerFX6.6.14等等

根據資料顯示,tinypng、pngquant、ImageAlpha、pngnq都是有失真壓縮,基本採用的都是quantization演算法,將32位元的PNG圖片轉換爲8位元的PNG圖片,減少圖片的顏色數;pngcrush、optipng、pngout、advpng都是無失真壓縮,採用的都是基於LZ/Huffman的DEFLATE演算法,減少圖片IDAT
chunk區域的數據。一般有失真壓縮的壓縮率會大大高於無失真壓縮。

我這裏使用開源的pngquant框架和OpenViewerFX6.6.14來操作

使用OpenViewerFX

<dependency>
    <groupId>org.jpedal</groupId>
    <artifactId>OpenViewerFX</artifactId>
    <version>6.6.14</version>
</dependency>
PngCompressor.compress(inputStream, outputStream);

這個的使用極其方便,就一句,input進output出,就是不像pngquant有很多參數可以選,如果只是簡單的壓縮,這個就足夠了
在这里插入图片描述
28k(24位元) -> 12.6k(8位元),發現已經成功的轉爲了索引圖

使用pngquant(JNI呼叫)

使用pngquant框架,這是一款比較流行的png壓縮開源框架,它是一種有失真壓縮,並將其轉爲索引圖,這種轉換大大減小了檔案大小(通常高達70%),並保留了完全的alpha透明度。生成的影象與所有web瀏覽器和操作系統相容。

它是c語言寫的,這裏麻煩一點,需要使用jni的方式呼叫,這裏就當練習下jni使用了

官方參考文件: https://pngquant.org/lib/

首先去https://github.com/ImageOptim/libimagequant下載原始碼,發現它是基於makefile構建的.
在这里插入图片描述
文件說使用make java就可以構建了,dll的話就是make java-dll

本地環境: centOS7.3 64位元、win7 64位元、jdk1.8、gcc9.3.0、GNU make4.3

在linux下構建

在linux下構建比較簡單,直接cd到目錄下去make java

遇到的問題有:
1、configure檔案格式的問題,把內容複製下來重新copy一份即可
在这里插入图片描述
2、bad value錯誤
在这里插入图片描述

make clean
make distclean
./configure --extra-cflags="-fPIC"
make java

帶上fPIC再試一次就可以了

在这里插入图片描述
編譯完成後就有了libimagequant.jnilib檔案
當然,在linux上我們需要的是libimagequant.so而不是libimagequant.jnilib(這是macOS上使用的)
Make java的時候輸出的最後一個命令,我們copy下來修改一下

gcc -g -fno-math-errno -funroll-loops -fomit-frame-pointer -Wall -std=c99 -I. -O3 -DNDEBUG -DUSE_SSE=1 -msse -mfpmath=sse -Wno-unknown-pragmas -fexcess-precision=fast  -fPIC -lm -I'/usr/lib/jvm/java-1.8.0-openjdk/include' -I'/usr/lib/jvm/java-1.8.0-openjdk/include/linux' -I'/usr/lib/jvm/java-1.8.0-openjdk/include/win32' -I'/usr/lib/jvm/java-1.8.0-openjdk/include/darwin' -shared -o libimagequant.so org/pngquant/PngQuant.c libimagequant.a

把目標檔案改爲.so,重新編譯就可以了

在win下構建

在win下構建可以用於本地開發測試,所以我這裏打一個dll
Win下使用cygwin就可以使用make命令構建了,當然,用visual studio也可以,還能避免一些坑。。。
Cygwin是一個可原生執行於Windows系統上的POSXI相容環境,使用它就可以用到常用的ls、pwd、make、gcc等命令,也是推薦的c++開發工具鏈

遇到的問題有:
1、Configure檔案格式不對,這個解決辦法跟linux下一樣

2、__int64定義錯誤
這種解決方法是不對滴,爲了引出問題還是記錄一下過程
在这里插入图片描述
發現報錯了,因爲gnu中沒有 __int64,而參照的win版本的jdk又有這個玩意

當時的解決方法是修改jni_md.h的34行,加上#ifdef else語句判斷一下當前環境。或者gcc編譯參數中加入宏定義-D__int64=’long long’
我直接修改了編譯參數,在Makefile裏面

java-dll:
	$(MAKE) CFLAGS="$(CFLAGS)  -DIMAGEQUANT_EXPORTS" $(JNIDLL)
$(JNIDLL) $(JNIDLLIMP): $(JAVAHEADERS) $(OBJS) org/pngquant/PngQuant.c
	$(CC) -D__int64='long long' -fPIC -shared -I. $(JAVAINCLUDE) -o $(JNIDLL) $^ $(LDFLAGS) -Wl,--out-implib,$(JNIDLLIMP),--output-def,$(JNIDLLDEF)

在這兩處加上-D__int64=’long long’,給__int64這種型別一個宏定義,編譯倒是成功編譯了,就是執行時有錯誤

3、jni執行時error
這一步是在程式執行中報錯的,可以先看下面 下麪jin的使用再看這步報錯

#
# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x0000000180136797, pid=9848, tid=0x0000000000001c3c
#
# JRE version: Java(TM) SE Runtime Environment (8.0_191-b12) (build 1.8.0_191-b12)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.191-b12 mixed mode windows-amd64 compressed oops)
# Problematic frame:
# C  [cygwin1.dll+0xf6797]
……
Stack: [0x0000000002580000,0x0000000002680000],  sp=0x000000000267f6a0,  free space=1021k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
C  [cygwin1.dll+0xf6797]
C  [cygwin1.dll+0x9431f]

Java frames: (J=compiled Java code, j=interpreted, Vv=VM code)
j  org.pngquant.PngQuant.liq_attr_create()J+0
j  org.pngquant.PngQuant.<init>()V+5
j  com.ai.base.tool.pngquant.PngQuantTest.main([Ljava/lang/String;)V+4
v  ~StubRoutines::call_stub

在呼叫create的時候直接error了,說是cygwin1.dll裏面報錯,這個cygwin1.dll是POSIX系統呼叫的模擬層,使用cygwin構建的所有程式都依賴了cygwin1.dll,所以這裏不是關鍵。
在看下面 下麪,報錯是在liq_attr_create方法中,就是編譯出來的dll有問題,不能正常執行。
解決方法是:換用MinGW編譯器,MinGW相比CygWin/gcc來講,更加貼近win32。因爲它幾乎支援所有的Win32API。

直接修改configure檔案,大概在17行的位置

# make gcc default compiler unless CC is already set
CC=${CC:-x86_64-w64-mingw32-gcc}

把編譯器換成這個,我這裏是用的cygwin的元件,如果沒有的話,去setup一遍cygwin,把這個元件選擇安裝上
然後重新編譯一次,編譯過後體積大了100多k,不過終於能正常執行了

在这里插入图片描述
關於編譯好的pngquant動態鏈接庫檔案,文章最後有下載

JNI呼叫

Jni這裏就不多介紹了
在pngquant原始碼中,lib\org\pngquant目錄下找到java檔案
這個makefile指令碼會呼叫javah生成標頭檔案
在这里插入图片描述
它長這個樣子,可以發現是javah自動生成的,下面 下麪的方法名是根據:Java+包名+類名+方法名生成的
所以如果生成好了,在專案中參照時不要修改包名,不然找不到方法

如果遇到找不到方法,那麼就確認一下動態鏈接庫的方法名是否正確
在这里插入图片描述
通過nm命令檢視,它可以列舉檔案中的符號,當然也可以看到方法名
在这里插入图片描述
我這裏的包結構是這樣的
在这里插入图片描述
Win下System.loadLibrary的是imagequant.dll(不帶lib字首),linux下就是libimagequant.so(帶lib字首)

public class PngQuantTest {
    public static void main(String[] args) {
        BufferedImage newImg;
        PngQuant pngQuant = new PngQuant();
        try {
            newImg = pngQuant.getRemapped(ImageIO.read(new File("D:\\test\\92.png")));
            ImageIO.write(newImg, "png", new File("D:\\test\\new_92.png"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        pngQuant.close();
    }
}

最後寫一個測試方法,執行新增jvm參數: -Djava.library.path=./lib,然後測試成功!

關於jni還有個比jni效能好一點的javacpp有興趣可以看看
https://github.com/bytedeco/javacpp

關於pngquant的jni動態鏈接庫檔案下載

https://download.csdn.net/download/w57685321/12706816