創作內容

8 GP

【程式】讀取圖檔的方法-Windows篇

作者:Shark│2016-11-28 03:57:57│巴幣:212│人氣:3688
介紹一個開發引擎時用的技術。

PNG、JPG是通用的壓縮圖檔格式,不過自己寫編碼、解碼演算法很費工,一般都是借助別人寫好的函式庫。
讀寫圖檔的功能其實Windows API有內建,而且並不難用,寫Windows程式的時候可以利用一下。

本篇只講讀檔,寫檔以後再介紹,不過對Windows API熟悉的話自己看MSDN就能學會了。
函式參數和class成員的意義就請自己查MSDN,這裡不一個一個解釋。一個通用函式庫為了能應用在多樣的情況有很多參數可以填,但簡單的應用只會用到一部分。
我的需求是輸入方法除了給檔名以外,也要能讀取一塊記憶體,就是把檔案原封不動讀到記憶體裡再把指標給解碼函式,因為引擎有自己的包裝格式,不會直接讀硬碟上的檔案。

寫下來之後發現如果想全部解釋清楚要講到很多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,它會傳回兩個變數。
ULONG_PTR token;
GdiplusStartupInput startupInput;
GdiplusStartup(&token, &startupInput, NULL);
想讓函式一次傳回多個值要用指標來做是C/C++的常識,所以宣告兩個變數再取它們的指標傳入。
startupInput之後不會再使用,可以放在函式的區域變數(函式return後就消失),但token以後會用到,要找個地方保留。

接下來產生一個Bitmap物件,用它讀取圖檔,它有好幾個建構子用在不同的情況。
2a.如果輸入是檔名,用這個給檔名的
Bitmap gdiplusBitmap(L"fileName.png", 0);

2b.如果輸入是記憶體,它沒有直接給buffer的建構子,只有一個參數是(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介面,這類物件釋放資源的方法是內部有個計數器記錄有多少物件參考到它,計數器減到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,不過經實測此處要填的只有Scan0和Stride。
bitmapData.Stride是指一列的開頭到下一列開頭相隔幾個byte,可以不等於圖寬度×每個像素的byte數。
LockBits的第三參數是把原圖轉換成什麼格式,可以把BGR的原圖自動加上alpha變成BGRA,但是沒有直接支援灰階格式,如果原圖是灰階那還是要填BGR。

有stride這個值是因為將資料用4 byte對齊效能會比較好,但有些顏色格式不是每個像素4 byte(如BGR不含alpha是24 bit),想讓每列開頭都是4 byte對齊就會有「stride≠寬度×每像素byte數」的情況。

做完這些步驟pixels裡就是各像素的BGRA值了,可以任君取用。
gdiplusBitmap由於不是動態配置記憶體,離開所在的作用域後會自動釋放。

5.程式最後要把GDI+結束
GdiplusShutdown(token);
第一步取得的token要保留到這裡。

build時要連結這兩個library
gdiplus.lib
shlwapi.lib
第一個是GDI+本體,第二個是因為用到SHCreateMemStream函式。



方法2:用WIC(Windows Imaging Component)
適用版本:Windows Vista以後

研究過一些API的感覺是,微軟在Vista把多媒體功能整個翻新,WIC就是這些新系統的其中之一,其他新API還有控制顯卡的Direct3D 10、影音處理的Media Foundation等等,舊的API如GDI+、Direct3D 9只是為了向後相容而保留下來。

1.初始化
include header
#include<wincodec.h>
#include<shlwapi.h>

呼叫這兩個函式產生一個IWICImagingFactory物件
IWICImagingFactory* factory;
CoInitializeEx(NULL, COINIT_MULTITHREADED);
HRESULT hr = CoCreateInstance(CLSID_WICImagingFactory, NULL,
  CLSCTX_INPROC_SERVER, IID_IWICImagingFactory, (void**)&factory);
這兩個函式是關於一個Windows裡的系統:COM,現在先不解釋。
另外WIC(或其他有用到COM的系統)呼叫函式建立物件時,通常函式本身傳回一個錯誤碼代表是什麼原因失敗,如果hr==S_OK就代表執行成功,要建立的物件用一個指標傳回。

之後其他的物件都用這個factory來產生。

再來建立一個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()可以用,但只能取得圖檔原始的格式,不會幫你轉換。

此時可以把物件刪除了。
converter->Release();
frame->Release();
decoder->Release();
跟IStream一樣是繼承IUnknown的物件,呼叫Release()。

5.把WIC系統結束的方法,先刪除factory,再結束COM系統。
factory->Release();
CoUninitialize();

build時要連結這三個library
windowscodecs.lib
shlwapi.lib
ole32.lib
第一個是使用WIC定義的常數,第三個是要用CoInitializeEx、CoCreateInstance、CoUninitialize這三個函式。



艾莉兒:不用帶那麼多裝備,我可以利用周圍的自然之力。

我對艾莉兒的設計理念是保持輕巧,能從作業系統現地調達的功能就不要自己攜帶函式庫,Cyber Sprite程式很複雜但.exe檔能減到只有300多K這是原因之一。
主要是以前換新電腦重架開發環境時,除了Windows SDK以外還要額外裝一堆函式庫,就決定儘量用作業系統內建的功能,免去手動裝一堆東西的工。

研究之後發掘了很多Windows的潛能,像是音檔解碼、Deflate演算法Windows也有內建。
引用網址:https://home.gamer.com.tw/TrackBack.php?sn=3398535
All rights reserved. 版權所有,保留一切權利

相關創作

同標籤作品搜尋:程式|Windows API|Windows程式設計

留言共 2 篇留言

Shark
贊助:104,有人給了100GP嗎?

11-28 20:09

新手方
喜歡那邊 如果不按下去的話可以選擇要給的8b

04-26 20:23

Shark
我知道啊,只是好奇是誰給的。04-28 01:26
我要留言提醒:您尚未登入,請先登入再留言

8喜歡★shark0r 可決定是否刪除您的留言,請勿發表違反站規文字。

前一篇:【進度】NPC:裝甲運輸... 後一篇:「淺談遊戲引擎」觀後感...

追蹤私訊切換新版閱覽

作品資料夾

robert286 ლ(´•д• ̀ლ
ლ(´•д• ̀ლ看更多我要大聲說昨天18:01


face基於日前微軟官方表示 Internet Explorer 不再支援新的網路標準,可能無法使用新的應用程式來呈現網站內容,在瀏覽器支援度及網站安全性的雙重考量下,為了讓巴友們有更好的使用體驗,巴哈姆特即將於 2019年9月2日 停止支援 Internet Explorer 瀏覽器的頁面呈現和功能。
屆時建議您使用下述瀏覽器來瀏覽巴哈姆特:
。Google Chrome(推薦)
。Mozilla Firefox
。Microsoft Edge(Windows10以上的作業系統版本才可使用)

face我們了解您不想看到廣告的心情⋯ 若您願意支持巴哈姆特永續經營,請將 gamer.com.tw 加入廣告阻擋工具的白名單中,謝謝 !【教學】