介紹一個開發(fā)引擎時用的技術。
PNG、JPG是通用的壓縮圖檔格式,不過自己寫編碼、解碼演算法很費工,一般都是借助別人寫好的函式庫。
讀寫圖檔的功能其實Windows API有內(nèi)建,而且並不難用,寫Windows程式的時候可以利用一下。
本篇只講讀檔,寫檔以後再介紹,不過對Windows API熟悉的話自己看MSDN就能學會了。
函式參數(shù)和class成員的意義就請自己查MSDN,這裡不一個一個解釋。一個通用函式庫為了能應用在多樣的情況有很多參數(shù)可以填,但簡單的應用只會用到一部分。
我的需求是輸入方法除了給檔名以外,也要能讀取一塊記憶體,就是把檔案原封不動讀到記憶體裡再把指標給解碼函式,因為引擎有自己的包裝格式,不會直接讀硬碟上的檔案。
寫下來之後發(fā)現(xiàn)如果想全部解釋清楚要講到很多Windows API的知識,雖然只要讀到圖檔就好其他先不管的話不難做。
方法1:用GDI+
適用版本:Windows XP以後
GDI+是從Windows XP開始有的,將舊有的GDI包裝一層並加強功能。
1.程式一開始要初始化GDI+。
include header,並啟用Gdiplus名稱空間。
#include<gdiplus.h> #include<shlwapi.h> using namespace Gdiplus; |
呼叫GdiplusStartup,它會傳回兩個變數(shù)。
ULONG_PTR token; GdiplusStartupInput startupInput; GdiplusStartup(&token, &startupInput, NULL); |
想讓函式一次傳回多個值要用指標來做是C/C++的常識,所以宣告兩個變數(shù)再取它們的指標傳入。
startupInput之後不會再使用,可以放在函式的區(qū)域變數(shù)(函式return後就消失),但token以後會用到,要找個地方保留。
接下來產(chǎn)生一個Bitmap物件,用它讀取圖檔,它有好幾個建構子用在不同的情況。
2a.如果輸入是檔名,用這個給檔名的
Bitmap gdiplusBitmap(L"fileName.png", 0); |
2b.如果輸入是記憶體,它沒有直接給buffer的建構子,只有一個參數(shù)是(IStream*, BOOL)的可以用在此情況。
(查MSDN會看到有的是給pixel data的,不過這些要給解碼後的BGRA值,不是原始的圖檔)
所以把這塊記憶體用一個IStream物件包裝起來才能讀取,用SHCreateMemStream。
IStream* stream=SHCreateMemStream((const BYTE*)buffer, bufferSize); //reference=1 Bitmap gdiplusBitmap(stream, 0); //reference增加 stream->Release(); //reference減1 |
(其實我是很久以後才知道有SHCreateMemStream這個函式,以前只知道另一個CreateStreamOnHGlobal,要多一些步驟)
這裡介紹一下reference count,IStream有繼承IUnknown介面,這類物件釋放資源的方法是內(nèi)部有個計數(shù)器記錄有多少物件參考到它,計數(shù)器減到0才刪除而不是直接用delete關鍵字。
stream->Release()是把SHCreateMemStream增加的refernence扣掉,之後Bitmap物件刪除時將它用到的reference扣掉就會自動刪除stream。
有繼承IUnknown的物件都是用這種方法管理,Windows API裡很多這種東西,像之前的Direct3D 11也是,以後還會常常看到。
3.取得圖的尺寸,這樣才知道要malloc多少記憶體
UINT w=gdiplusBitmap.GetWidth(); UINT h=gdiplusBitmap.GetHeight(); UINT32* pixels=(UINT32*)malloc(w*h*4); |
4.再來真正把圖檔解碼了,要用的是LockBits函式,要準備一些資料告訴它範圍和輸出格式。
Rect rect(0,0,w,h); BitmapData bitmapData; bitmapData.Scan0 = pixels; //要把解出來的像素放到哪裡 bitmapData.Stride = w*4; gdiplusBitmap.LockBits(&rect, ImageLockModeRead|ImageLockModeUserInputBuf, PixelFormat32bppARGB, &bitmapData); gdiplusBitmap.UnlockBits(&bitmapData); |
BitmapData還有其他member,不過經(jīng)實測此處要填的只有Scan0和Stride。
bitmapData.Stride是指一列的開頭到下一列開頭相隔幾個byte,可以不等於圖寬度×每個像素的byte數(shù)。
LockBits的第三參數(shù)是把原圖轉(zhuǎn)換成什麼格式,可以把BGR的原圖自動加上alpha變成BGRA,但是沒有直接支援灰階格式,如果原圖是灰階那還是要填BGR。
有stride這個值是因為將資料用4 byte對齊效能會比較好,但有些顏色格式不是每個像素4 byte(如BGR不含alpha是24 bit),想讓每列開頭都是4 byte對齊就會有「stride≠寬度×每像素byte數(shù)」的情況。
做完這些步驟pixels裡就是各像素的BGRA值了,可以任君取用。
gdiplusBitmap由於不是動態(tài)配置記憶體,離開所在的作用域後會自動釋放。
5.程式最後要把GDI+結束
第一步取得的token要保留到這裡。
build時要連結這兩個library
gdiplus.lib
shlwapi.lib
第一個是GDI+本體,第二個是因為用到SHCreateMemStream函式。
方法2:用WIC(Windows Imaging Component)
適用版本:Windows Vista以後
研究過一些API的感覺是,微軟在Vista把多媒體功能整個翻新,WIC就是這些新系統(tǒng)的其中之一,其他新API還有控制顯卡的Direct3D 10、影音處理的Media Foundation等等,舊的API如GDI+、Direct3D 9只是為了向後相容而保留下來。
1.初始化
include header
#include<wincodec.h> #include<shlwapi.h> |
呼叫這兩個函式產(chǎn)生一個IWICImagingFactory物件
IWICImagingFactory* factory; CoInitializeEx(NULL, COINIT_MULTITHREADED); HRESULT hr = CoCreateInstance(CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER, IID_IWICImagingFactory, (void**)&factory); |
這兩個函式是關於一個Windows裡的系統(tǒng):COM,現(xiàn)在先不解釋。
另外WIC(或其他有用到COM的系統(tǒng))呼叫函式建立物件時,通常函式本身傳回一個錯誤碼代表是什麼原因失敗,如果hr==S_OK就代表執(zhí)行成功,要建立的物件用一個指標傳回。
之後其他的物件都用這個factory來產(chǎn)生。
再來建立一個IWICBitmapDecoder物件用來解碼
2a.如果輸入是檔名
IWICBitmapDecoder* decoder; hr = factory->CreateDecoderFromFilename(L"fileName.png", NULL, GENERIC_READ,WICDecodeMetadataCacheOnDemand ,&decoder); |
2b.如果輸入是buffer,跟上面一樣要包在一個IStream物件裡
IStream* stream=SHCreateMemStream((const BYTE*)buffer, bufferSize); IWICBitmapDecoder* decoder; hr = factory->CreateDecoderFromStream(stream,NULL, WICDecodeMetadataCacheOnDemand,&decoder); stream->Release(); |
這裡也一樣,CreateDecoderFromStream傳回錯誤碼,IWICBitmapDecoder是用指標傳回。
建好decoder之後就如GDI+篇一樣,把stream release掉。
3.取得frame再取得frame的尺寸,這樣才知道要malloc多少記憶體
WIC可以用在一個檔案裡有多張圖的情況(如GIF),所以要先取出frame再從它取出pixel data,這裡只取出第一個frame。
IWICBitmapFrameDecode* frame; decoder->GetFrame(0,&frame); UINT w,h; frame->GetSize(&w,&h); UINT32* pixels=(UINT32*)malloc(w*h*4); |
4.把圖檔解碼,要建立一個IWICFormatConverter物件指定輸出格式
IWICFormatConverter* converter; factory->CreateFormatConverter(&converter); converter->Initialize(frame, GUID_WICPixelFormat32bppBGRA, WICBitmapDitherTypeNone, NULL,0, WICBitmapPaletteTypeCustom); converter->CopyPixels(NULL, w*4,w*h*4, (BYTE*)pixels); |
這時候pixels裡就是各像素的BGRA值。
GUID_WICPixelFormat32bppBGRA是指定輸出格式,可以設定byte順序是RGB或BGR、一個像素幾bit、灰階等等,查MSDN裡Native Pixel Formats這一頁可以看到能填的值。
frame其實也有一個frame->CopyPixels()可以用,但只能取得圖檔原始的格式,不會幫你轉(zhuǎn)換。
此時可以把物件刪除了。
converter->Release(); frame->Release(); decoder->Release(); |
跟IStream一樣是繼承IUnknown的物件,呼叫Release()。
5.把WIC系統(tǒng)結束的方法,先刪除factory,再結束COM系統(tǒng)。
factory->Release(); CoUninitialize(); |
build時要連結這三個library
windowscodecs.lib
shlwapi.lib
ole32.lib
第一個是使用WIC定義的常數(shù),第三個是要用CoInitializeEx、CoCreateInstance、CoUninitialize這三個函式。
艾莉兒:不用帶那麼多裝備,我可以利用周圍的自然之力。我對艾莉兒的設計理念是保持輕巧,能從作業(yè)系統(tǒng)現(xiàn)地調(diào)達的功能就不要自己攜帶函式庫,Cyber Sprite程式很複雜但.exe檔能減到只有300多K這是原因之一。
主要是以前換新電腦重架開發(fā)環(huán)境時,除了Windows SDK以外還要額外裝一堆函式庫,就決定儘量用作業(yè)系統(tǒng)內(nèi)建的功能,免去手動裝一堆東西的工。
研究之後發(fā)掘了很多Windows的潛能,像是音檔解碼、Deflate演算法Windows也有內(nèi)建。