1. 程式人生 > >guetzli圖片壓縮php擴充套件

guetzli圖片壓縮php擴充套件

前言:在github的發現一個谷歌開源的jpeg格式圖片的壓縮工具,它可以在主觀質量無損的條件下, 將jpeg圖片減少20%~30%的位元速率,於是當時測試了一下壓縮效果,圖片質量95壓縮率大概在20%~30%。我們司平臺是漫畫閱讀app,整個app除了圖片還是圖片,特別是首頁,用了大量的高清無碼的圖片,於是我想將guetzli用於首頁漫畫封面圖片的壓縮,但是guetzli並沒有提供php的介面,所以當時的實現是將需要壓縮的圖片放入redis佇列,建立一個程序在凌晨基本沒有使用者訪問的時候通過exec呼叫guetzli工具對redis佇列消費處理。(至於為什麼要這樣,請看下面網上大佬的測試報告)

Guetzli資源消耗:

官方文件說,1MPIX的圖片處理需要消耗300M記憶體。

實測一個1MB大小1920x2560的圖片,有4.9MPIX。理論消耗記憶體1474MB,實際消耗1009MB記憶體,實際與理論基本相符。由此看出這個工具是個記憶體消耗大戶,60G記憶體只夠處理200MPIX,也就是同時處理40張左右的1920x2560圖片。

CPU消耗則一直是100%單核佔用。當然實際使用多核機器可以同時跑多個程序。有多少核就能跑多少個guetzli任務。

壓縮圖片耗時跟圖片大小程離散相關,圖片越大,耗時越久

guetzli劣勢:

由於guetzli時效性差,圖片越大壓縮時間越長,而且資源佔用大,簡直就是記憶體消耗大戶。

guetzli優勢:


實測對質量在90或以下的jpg圖片,guetzli輸出的新圖質量不會降低。而實際壓縮率能夠達到平均壓縮率29%。

相容性比較好,輸出的jpeg格式圖片通用性非常高。沒有webp、sharpp那種協議不相容的困擾。

在客戶端jpeg格式的圖片編解碼速度比其他私有協議快很多。

下面說說我們將要做的:

從github下載guetzli的原始碼編譯後會生成一個guetzli的工具,以及一個靜態庫檔案,我們需要做的就是建立一個php擴充套件呼叫這個靜態庫提供的壓縮功能。

動態載入so模組:利用ext_skel工具編譯生成so模組,修改php.ini檔案,動態載入即可
靜態編譯:將編寫的模組靜態編譯到PHP,需要重新編譯PHP

面臨的問題:

注:php擴充套件載入方式可以分為靜態編譯與動態載入。(區別在於,靜態編譯需要重新編譯php才能使用擴充套件,動態載入是將php擴充套件編譯成so動態庫庫,修改php.ini,php在執行時會動態載入,不需要重新編譯php),我們這裡需要將擴充套件編譯成so動態庫。

1、php動態so庫需要依賴guetzli編譯生成的靜態庫,guetzli的靜態庫在編譯的時候生成的目標檔案是地址相關的,而動態庫的一個特點是動態庫是被程序動態載入到一個不確定的地址,所以動態庫編譯時的目標檔案為地址無關的相對路徑,而沒有絕對路徑。在這裡我們的php動態庫需要引用guetzli的靜態庫,所以我們需要修改guetzli的Makefile檔案;

2、guetzli原始碼使用c++編寫的,但是我們的php擴充套件介面是用c進行編寫,由於c函式跟c++函式在編譯的時候對引數的處理不一樣,所以這裡需要匯出一個c介面,供php擴充套件呼叫;

建立php擴充套件模板:

注:本文章不詳細討論php擴充套件開發基礎知識,後面會詳細寫一篇php擴充套件方面的文章;

1、用php原始碼擴充套件目錄ext下的ext_skel工具建立php擴充套件的模板,在ext目錄下執行./ext_skel --extname=myguetzli命令(myguetzli是我們需要建立擴充套件的名稱),這個工具會在etx目錄下建立一個myguetzli目錄,裡面包含我們擴充套件開發所必需的基本檔案。


2、我們需要使用到的是config.m4、myguetzli.c、myguetzli.h這三個檔案


3、我們在myguetzli目錄下建立一個guetzli目錄用於存放guetzli的靜態庫以及匯出的c函式介面檔案


php擴充套件模板有了,我們接下來解決上面提到問題:

第一步:修改guetzli原始碼目錄的Makefile檔案,讓guetzli在make編譯的時候生成的目的碼是地址不相關的。(生成地址不相關的目標檔案,需要在編譯的時候加上-fPIC編譯選項

1、guetzli原始碼目錄下有一個guetzli_static.make,這個是用於編譯guetzli靜態庫的Makefile檔案;

2、在這裡檔案裡找到下面這行,並加上紅色部分的選項,儲存;

3、執行make進行編譯即可。會在bin/Release目錄下生成一個guetzli_static.a的靜態庫檔案;到這裡第一個問題就解決了。

//4、將guetzli_static.a靜態庫拷貝到php擴充套件myguetzli目錄下的guetzli/lib目錄,guetzli原始碼目錄的guetzli目錄裡面的標頭檔案也拷貝到myguetzli目錄下的guetzli目錄

ALL_CXXFLAGS += $(CXXFLAGS) $(ALL_CPPFLAGS) -O3 -g -std=c++11 -fPIC `pkg-config --static --cflags libpng || libpng-config --static --cflags`
第二步:建立介面檔案
1、guetzli原始碼目錄下有一個guetzli.cc檔案,這個是guetzli提供的工具原始碼,寫好了對guetzli壓縮功能的呼叫,
我們在這個檔案裡面增加一個函式,並用c函式的方式編譯這個函式

進入擴充套件myguetzli下的guetzli目錄,建立兩個檔案(guetzli.cpp、guetzli.h)
新建一個guetzli.h檔案,extern告訴編譯器按c函式的方式編譯,這樣才能被c所呼叫


#guetzli.h檔案
#ifdef __cplusplus
extern "C"
{
#endif
int MyGuetzli(char* filename, char* savefilename, int quality, int memlimit_mb);
#ifdef __cplusplus
}
#endif
在guetzli.cc檔案新增MyGuetzli函式
#guetzli.cpp檔案,將guetzli原始碼目錄guetzli.cc檔案的程式碼複製過來,並增加下面的程式碼
int MyGuetzli(char* filename, char* savefilename, int quality, int memlimit_mb)
{
  int verbose = 0;
  // int quality = kDefaultJPEGQuality;
  memlimit_mb = kDefaultMemlimitMB;

  std::string in_data = ReadFileOrDie(filename);
  std::string out_data;

  guetzli::Params params;
  params.butteraugli_target = static_cast<float>(
      guetzli::ButteraugliScoreForQuality(quality));

  guetzli::ProcessStats stats;

  if (verbose) {
    stats.debug_output_file = stderr;
  }

  static const unsigned char kPNGMagicBytes[] = {
      0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n',
  };
  if (in_data.size() >= 8 &&
      memcmp(in_data.data(), kPNGMagicBytes, sizeof(kPNGMagicBytes)) == 0) {
    int xsize, ysize;
    std::vector<uint8_t> rgb;
    if (!ReadPNG(in_data, &xsize, &ysize, &rgb)) {
      fprintf(stderr, "Error reading PNG data from input file\n");
      return 1;
    }
    double pixels = static_cast<double>(xsize) * ysize;
    if (memlimit_mb != -1
        && (pixels * kBytesPerPixel / (1 << 20) > memlimit_mb
            || memlimit_mb < kLowestMemusageMB)) {
      fprintf(stderr, "Memory limit would be exceeded. Failing.\n");
      return 1;
    }
    if (!guetzli::Process(params, &stats, rgb, xsize, ysize, &out_data)) {
      fprintf(stderr, "Guetzli processing failed\n");
      return 1;
    }
  } else {
    guetzli::JPEGData jpg_header;
    if (!guetzli::ReadJpeg(in_data, guetzli::JPEG_READ_HEADER, &jpg_header)) {
      fprintf(stderr, "Error reading JPG data from input file\n");
      return 1;
    }
    double pixels = static_cast<double>(jpg_header.width) * jpg_header.height;
    if (memlimit_mb != -1
        && (pixels * kBytesPerPixel / (1 << 20) > memlimit_mb
            || memlimit_mb < kLowestMemusageMB)) {
      fprintf(stderr, "Memory limit would be exceeded. Failing.\n");
      return 1;
    }
    if (!guetzli::Process(params, &stats, in_data, &out_data)) {
      fprintf(stderr, "Guetzli processing failed\n");
      return 1;
    }
  }

  WriteFileOrDie(savefilename, out_data);
  return 0;
}

 2、在myguetzli擴充套件下的guetzli目錄執行如下程式碼
g++ guetzli.cpp -std=c++11 -fPIC -shared -I/home/liaokw/下載/guetzli -L/home/liaokw/下載/guetzli/bin/Release -lguetzli_static  -o ./lib/libguetzli.so


-I:指定guetzli的標頭檔案
-L:指定guetzli靜態庫
-l:guetzli靜態庫的名稱
-o:生成動態庫儲存路徑


libguetzli.so這個動態庫就是我們擴充套件所要要使用的,這個動態庫主要作為呼叫guetzli的介面


第三步:php擴充套件開發
1、修改config.m4,編寫一些巨集函式,用於檢測以及新增擴充套件所依賴的庫,以及環境(擴充套件依賴於第二部編譯的動態庫,guetzli依賴於png庫)
在config.m4檔案新增以下程式碼


PHP_ARG_WITH(png-dir, for myguetzli support,
Make sure that the comment is aligned:
[  --with-png-dir             Include myguetzli support])

if test "$PHP_PNG_DIR" != "no"; then

  SEARCH_PATH="$PHP_PNG_DIR /usr/lib /usr/local/lib"     # you might want to change this
  SEARCH_FOR="include/png.h"  # you most likely want to change this
  for i in $SEARCH_PATH ; do
    if test -f $i/$SEARCH_FOR; then
      PNG_DIR=$i
      AC_MSG_RESULT(found in $i)
    fi
  done
  if test -z "$PNG_DIR" -o -z "$PNG_DIR/include"; then
    AC_MSG_RESULT([$PNG_DIR not found 1])
    AC_MSG_ERROR([$SEARCH_PATH || $PNG_DIR ||  Please reinstall the png distribution])
  fi
  dnl 標頭檔案路徑
  PHP_ADD_INCLUDE($PNG_DIR/include)

  PHP_CHECK_LIBRARY(png,png_create_read_struct,
  [
    PHP_ADD_LIBRARY_WITH_PATH(png, $PNG_DIR/lib, MYGUETZLI_SHARED_LIBADD)
    AC_DEFINE(HAVE_PNGLIB,1,[libpng yes ])
  ],[
   AC_MSG_ERROR([$PNG_DIR/include no found 2])
  ],[
   -L$PNG_DIR/lib -lpng
  ])
  PHP_SUBST(MYGUETZLI_SHARED_LIBADD)
fi

上面的程式碼主要用於在configure新增一個配置引數--with-png-dir,用於指定png庫所在路徑


if test "$PHP_MYGUETZLI_DIR" != "no"; then
  SEARCH_PATH="$PHP_MYGUETZLI_DIR guetzli"     # you might want to change this
  SEARCH_FOR="/guetzli.h"  # you most likely want to change this
  for i in $SEARCH_PATH ; do
    if test -f $i/$SEARCH_FOR; then
      GUETZLI_DIR=$i
      AC_MSG_RESULT(found in $i)
    fi
  done
  if test -z "$GUETZLI_DIR" -o -z "$GUETZLI_DIR/"; then
    AC_MSG_RESULT([$GUETZLI_DIR not found 1])
    AC_MSG_ERROR([$SEARCH_PATH || $GUETZLI_DIR ||  Please reinstall the zhtmltopdf distribution])
  fi
  dnl 標頭檔案路徑
  PHP_ADD_INCLUDE($GUETZLI_DIR)

  PHP_ADD_LIBRARY_WITH_PATH(guetzli, $GUETZLI_DIR/lib, MYGUETZLI_SHARED_LIBADD)
  AC_DEFINE(HAVE_MYGUETZLILIB,1,[libguetzli yes ])

dnl  PHP_CHECK_LIBRARY(guetzli,MyGuetzli,
dnl  [
dnl    PHP_ADD_LIBRARY_WITH_PATH(guetzli, $GUETZLI_DIR/lib, MYGUETZLI_SHARED_LIBADD)
dnl    AC_DEFINE(HAVE_MYGUETZLILIB,1,[libguetzli yes ])
dnl  ],[
dnl   AC_MSG_ERROR([$GUETZLI_DIR/include no found 2])
dnl  ],[
dnl   -L$GUETZLI_DIR/lib -lguetzli
dnl  ])
  PHP_SUBST(MYGUETZLI_SHARED_LIBADD)
fi

上面的巨集程式碼用於檢測以及新增我們在第二步生成的動態庫
通過執行phpize程式,會將上面一系列巨集函式替換成檢測編譯環境的shell程式碼,並配置我們所需要的依賴庫

2、編輯myguetzli.c擴充套件檔案,需要實現一個myguetzlijpg函式給使用者層呼叫,修改兩處地方以及引用guetzli.h檔案,用於使用介面庫匯出的函式


3、編譯擴充套件並安裝即可使用了

哎,第一次寫文章,邏輯處理的不夠好,寫的也一塌糊塗,漏洞百出;很多東西自己知道但是不知道該如何表達出來;