天天看點

Skia往SkBitmap上繪圖時畫不出來的問題SkBitmapDevice與kN32_SkColorType解碼圖檔時Red與Blue通道反了

使用SkBitmap作為SkCanvas後端繪圖時畫不出來的問題

用預設條件在采用了Intel Pentium CPU的PC上編譯Skia(參見Windows下從源碼編譯Skia)後,采用SkBitmap作為SkCanvas的後端來繪圖時,遇到了奇怪問題:“無論畫什麼,跟沒畫一個樣”。

代碼如下:

SkImageInfo ii = SkImageInfo::Make(480, 320, kRGBA_8888_SkColorType, kPremul_SkAlphaType);
bitmap.allocPixels(ii, ii.minRowBytes());
SkCanvas canvas(bitmap);
           

經過試驗,發現采用下面的代碼可以繪制成功:

SkImageInfo ii = SkImageInfo::Make(480, 320, kBGRA_8888_SkColorType, kPremul_SkAlphaType);
bitmap.allocPixels(ii, ii.minRowBytes());
SkCanvas canvas(bitmap);
           

是以我覺得可能是顔色類型的問題。就一路跟下去,搞了很久也沒搞明白……SkCanvas的繪圖源碼,一層套一層,看起來比較艱辛……沒看進去,中間繞道通過将SkCanvas繪制好的SkBitmap的像素手動轉換為RGBA達到了目的。不過如果圖檔大,逐像素轉換就很慢……容易的路總是有後患……

SkBitmapDevice與kN32_SkColorType

骨頭還是得啃。我确認了使用SkBitmap作為SkCanvas的後端時,比如上面的代碼,實際上SkCanvas自己配置設定了一個SkBitmapDevice來作為繪圖裝置,繪圖操作會遞交給SkBitmapDevice來完成。

SkBitmapDevice在建立繪圖裝置時校驗了拿到的SkBitmap對象的SkImageInfo屬性,對其中的Alpha Type和Color Type做了檢查。

SkBitmapDevice(SkBitmapDevice.cpp)的構造函數内部會調用valid_for_bitmap_device(),而valid_for_bitmap_device()方法,根據傳入的SkImageInfo(就是建立SkBitmap時設定的那個SkImageInfo)做了過濾,隻針對kAlpha_8_SkColorType、kRGB_565_SkColorType、kN32_SkColorType三種顔色類别建立SkBitmapDevice,而kN32_SkColorType==kBGRA_8888_SkColorType是以,我設定顔色為kRGBA_8888_SkColorType時,建立出來的SkBitmapDevice是無效的,是以怎麼繪制都無效。

valid_for_bitmap_device()的代碼如下:

static bool valid_for_bitmap_device(const SkImageInfo& info,
                                    SkAlphaType* newAlphaType) {
    if (info.width() < 0 || info.height() < 0) {
        return false;
    }

    // TODO: can we stop supporting kUnknown in SkBitmkapDevice?
    if (kUnknown_SkColorType == info.colorType()) {
        if (newAlphaType) {
            *newAlphaType = kUnknown_SkAlphaType;
        }
        return true;
    }

    switch (info.alphaType()) {
        case kPremul_SkAlphaType:
        case kOpaque_SkAlphaType:
            break;
        default:
            return false;
    }

    SkAlphaType canonicalAlphaType = info.alphaType();

    switch (info.colorType()) {
        case kAlpha_8_SkColorType:
            break;
        case kRGB_565_SkColorType:
            canonicalAlphaType = kOpaque_SkAlphaType;
            break;
        case kN32_SkColorType:
            break;
        default:
            return false;
    }

    if (newAlphaType) {
        *newAlphaType = canonicalAlphaType;
    }
    return true;
}
           

另外根據上面的代碼,隻處理了kPremul_SkAlphaType、kOpaque_SkAlphaType兩種Alpha類型,是以我們給SkBitmap指定Alpha類型時,如果是其他的,也不能成功建立SkBitmapDevice。

現在來看為什麼預設編譯出來的kN32_SkColorType==kBGRA_8888_SkColorType。

kN32_SkColorType在SkImageInfo.h中定義:

enum SkColorType {
    kUnknown_SkColorType,
    kAlpha_8_SkColorType,
    kRGB_565_SkColorType,
    kARGB_4444_SkColorType,
    kRGBA_8888_SkColorType,
    kBGRA_8888_SkColorType,
    kIndex_8_SkColorType,
    kGray_8_SkColorType,

    kLastEnum_SkColorType = kGray_8_SkColorType,

#if SK_PMCOLOR_BYTE_ORDER(B,G,R,A)
    kN32_SkColorType = kBGRA_8888_SkColorType,
#elif SK_PMCOLOR_BYTE_ORDER(R,G,B,A)
    kN32_SkColorType = kRGBA_8888_SkColorType,
#else
    #error "SK_*32_SHFIT values must correspond to BGRA or RGBA byte order"
#endif
};
           

kN32_SkColorType實際上是根據位元組序決定的一個值,用到的宏SK_PMCOLOR_BYTE_ORDER在SkPostConfig.h中定義:

#ifdef SK_CPU_BENDIAN
    #  define SK_PMCOLOR_BYTE_ORDER(C0, C1, C2, C3)     \
        (SK_ ## C3 ## 32_SHIFT == 0  &&             \
         SK_ ## C2 ## 32_SHIFT == 8  &&             \
         SK_ ## C1 ## 32_SHIFT == 16 &&             \
         SK_ ## C0 ## 32_SHIFT == 24)
#else
    #  define SK_PMCOLOR_BYTE_ORDER(C0, C1, C2, C3)     \
    (SK_ ## C0 ## 32_SHIFT == 0  &&             \
     SK_ ## C1 ## 32_SHIFT == 8  &&             \
     SK_ ## C2 ## 32_SHIFT == 16 &&             \
     SK_ ## C3 ## 32_SHIFT == 24)
#endif
           

因為我的主機是小端位元組序,是以SK_PMCOLOR_BYTE_ORDER是:

#  define SK_PMCOLOR_BYTE_ORDER(C0, C1, C2, C3)     \
    (SK_ ## C0 ## 32_SHIFT == 0  &&             \
     SK_ ## C1 ## 32_SHIFT == 8  &&             \
     SK_ ## C2 ## 32_SHIFT == 16 &&             \
     SK_ ## C3 ## 32_SHIFT == 24)
           

這個宏又用到了SK_A32_SHIFT、SK_R32_SHIFT、SK_G32_SHIFT、SK_B32_SHIFT這三個宏。分析SkPostConfig.h可知,預設編譯時,這三個宏的值在這裡定義:

#ifdef SK_BUILD_FOR_WIN
#  ifndef WIN32_LEAN_AND_MEAN
#    define WIN32_LEAN_AND_MEAN
#    define WIN32_IS_MEAN_WAS_LOCALLY_DEFINED
#  endif
#  ifndef NOMINMAX
#    define NOMINMAX
#    define NOMINMAX_WAS_LOCALLY_DEFINED
#  endif
#
#  include <windows.h>
#
#  ifdef WIN32_IS_MEAN_WAS_LOCALLY_DEFINED
#    undef WIN32_IS_MEAN_WAS_LOCALLY_DEFINED
#    undef WIN32_LEAN_AND_MEAN
#  endif
#  ifdef NOMINMAX_WAS_LOCALLY_DEFINED
#    undef NOMINMAX_WAS_LOCALLY_DEFINED
#    undef NOMINMAX
#  endif
#
#  ifndef SK_A32_SHIFT
#    define SK_A32_SHIFT 24
#    define SK_R32_SHIFT 16
#    define SK_G32_SHIFT 8
#    define SK_B32_SHIFT 0
#  endif
#
#endif
           

SK_A32_SHIFT=24、SK_R32_SHIFT=16、SK_G32_SHIFT=8、SK_B32_SHIFT=0,是以,SkImageInfo.h中,SK_PMCOLOR_BYTE_ORDER(B,G,R,A)展開後如下:

SK_B32_SHIFT == 0 && SK_G32_SHIFT == 8 && SK_R32_SHIFT == 16 && SK_A32_SHIFT == 24
           

這個表達式的值是 true ,是以kN32_SkColorType==kBGRA_8888_SkColorType。

我想用kRGBA_8888_SkColorType,需要在編譯時修改SK_A32_SHIFT、SK_R32_SHIFT、SK_G32_SHIFT、SK_B32_SHIFT這三個宏的值為下面的樣子:

SK_A32_SHIFT=24
SK_B32_SHIFT=16
SK_G32_SHIFT=8
SK_R32_SHIFT=0
           

這樣編譯出來的庫,kN32_SkColorType==kRGBA_8888_SkColorType。

有兩種方法。

    1. 編譯前修改CFLAGS等變量

生成ninja編譯腳本前,在cmd.exe裡執行下面的指令即可:

set "CFLAGS=-DSK_A32_SHIFT=24 -DSK_B32_SHIFT=16 -DSK_G32_SHIFT=8 -DSK_R32_SHIFT=0"
  set "CPPFLAGS=-DSK_A32_SHIFT=24 -DSK_B32_SHIFT=16 -DSK_G32_SHIFT=8 -DSK_R32_SHIFT=0"
  set "CXXFLAGS=-DSK_A32_SHIFT=24 -DSK_B32_SHIFT=16 -DSK_G32_SHIFT=8 -DSK_R32_SHIFT=0"
           

這樣編譯後,SkBitmapDevice在校驗Color Type時,kN32_SkColorType==kRGBA_8888_SkColorType,我們傳遞的SkBitmap的SkImageInfo的顔色類型為kRGBA_8888_SkColorType,就能成功建立繪圖裝置。不過,要使用kBGRA_8888_SkColorType的顔色類型的SkBitmap作為SkCanvas的後端繪圖裝置就不行了。

還有一點要注意:因為編譯時通過CFLAGS傳遞的SK_A32_SHIFT、SK_R32_SHIFT、SK_G32_SHIFT、SK_B32_SHIFT三個宏的值,并沒有被記錄到頭檔案裡,是以使用Skia的頭檔案時,檢測出來的SK_A32_SHIFT、SK_R32_SHIFT、SK_G32_SHIFT、SK_B32_SHIFT還是錯的,kN32_SkColorType也是錯的哈。要想避免有隐患,請在VS工程裡設定這三個宏的值和編譯時一緻。

    1. 修改SkUserConfig.h

如果不想在編譯時通過環境變量修改SK_A32_SHIFT、SK_R32_SHIFT、SK_G32_SHIFT、SK_B32_SHIFT,也可以修改SkUserConfig.h(在include\config目錄内),這樣的話,改動也被記錄下來了,因為SkTypes.h順序包含了SkPreConfig.h、SkUserConfig.h、SkPostConfig.h,改動在哪裡都是有效的,還省去了設定VS工程。SkUserConfig.h中有對這個檔案的說明。

解碼圖檔時Red與Blue通道反了

解決了問題之後,新的問題又來:解碼圖檔時Red與Blue通道反了。

(⊙o⊙)…還得繼續戰鬥啊。

硬着頭皮讀了半天源碼,以png為例,楞沒發現怎麼回事兒。後來都想用下面的方法繞過去了:把解碼出來的圖檔資料逐像素R、B交換。

總是想走容易的路。

再後來使勁實驗,去看SkImageDecoder.cpp、SkImageDecoder_libpng.cpp,漫天添加日志資訊檢視圖檔解碼器的建立流程,花了一天多時間,終于明白了問題在那裡(參見Skia圖檔編解碼子產品分析):

原來雖然我定義了SK_A32_SHIFT、SK_R32_SHIFT、SK_G32_SHIFT、SK_B32_SHIFT,把kN32_SkColorType調整過來了,讓SkBitmapDevice能建立kRGBA_8888_SkColorType格式的繪圖裝置了,但上面三個宏,并不能影響圖檔解碼,因為解碼器用的是Windows平台的COM元件,不受這三個宏影響,預設解碼出來的就是BGRA(參見SkImageDecoder_WIC::decodeStream方法)!

是以,隻好分析解碼器選擇流程,才有了Skia圖檔編解碼子產品分析,發現png、gif、jpeg等根本沒編譯。那麼把它們編譯進去就好了。

有兩個辦法:

  • 直接修改out\Release\obj\gyp\images.ninja檔案,添加相關解碼器的cpp檔案
  • 修改skia\gyp\images.gyp,把win平台下的條件編譯改一下

我用的第一種,硬把png等加上了,遇到各種錯,再改,好歹過來了……skia\gyp\images.gyp中說編譯順序會影響解碼器選擇,是以,加進去也不行,最後隻好直接調用CreatePNGImageDecoder之類的方法,沒用SkImageDecoder::DecodeXXX了。

現在看第二種應該更好些,Skia建構系統會自己來處理各種依賴,還能使用統一的SkImageDecoder接口,不過會因為個人的特殊需求污染Skia的編譯腳本……

就這樣吧。

其他參考文章詳見我的專欄:【CEF與PPAPI開發】。