윈도우에서 사용하는 비트맵 데이터는 크게 두가지 방식으로 나뉩니다.
장치 의존적인 DDB와 장치 독립적인 DIB...
DDB는 윈도우 내부에서 비트맵을 다룰때 사용하는 방식으로 사용자가 직접 DDB 데이터를 건드릴 수는 없게 되어있습니다.
DIB는 BMP 파일 등에서 사용하는 비트맵 형식으로 사용자가 비트맵 데이터를 조작하려면 일단 DIB로 변환을 해야 합니다. 파일로 저장하려고 해도 DIB로 변경을 해야 합니다.
사설은 이 정도로 하고...
윈도우에서 비트맵 데이터 조작을 위해... 즉, DDB와 DIB간의 변환을 위해 지원하는 API 함수들은 다음과 같은 것들이 있습니다.
DDB-->DIB :GetDIBits
DIB-->DDB :SetDIBits, SetDIBitsToDevice, StretchDIBits
DIB<-->DDB : CreateDIBSection
하나씩 살펴보기로 합니다.
1) GetDIBits
함수 원형은 다음과 같습니다.
Declare Function GetDIBits Lib "gdi32" Alias "GetDIBits" (ByVal aHDC As Long, ByVal hBitmap As Long, ByVal nStartScan As Long, ByVal nNumScans As Long, lpBits As Any, lpBI As BITMAPINFO, ByVal wUsage As Long) As Long
사용방법은 다음과 같습니다.
GetDIBits (원본 비트맵을 가진 DC의 핸들, 원본 비트맵의 핸들, 시작 스캔라인, 총 스캔라인 수, 비트맵 데이터를 저장할 위치, 비트맵의 정보를 가진 구조체, 팔레트 처리 방법)
리턴값은 수행한 스캔라인의 갯수가 반환되어야 합니다. 0이 반환되면 작업이 실패했음을 의미합니다.
첫번째 인수는 픽쳐박스의 경우 hDC 속성값을 지정하면 됩니다.
두번째 인수는 픽쳐박스의 경우 Image나 Picture 속성값을 지정하면 됩니다.
세번째 인수는 몇번째 라인부터 데이터를 읽어올지를 결정합니다. 보통 0을 지정하면 되지만 임의의 값을 지정할 수 있습니다.
다만, 스캔라인은 비트맵의 제일 아랫줄부터 위쪽으로 올라가면서 값이 증가한다는 점을 주의해야 합니다.
네번째 인수는 총 몇줄의 데이터를 읽어올지를 결정합니다. 보통 비트맵의 세로폭 값을 지정하면 되지만 임의의 값을 지정할 수 있습니다.
다섯번째 인수는 읽어온 비트맵 데이터를 저장할 메모리의 첫번째 데이터 위치를 지정하면 됩니다. 보통 배열의 첫번째 요소를 지정하면 됩니다.
여기에 지정할 배열은 미리 읽어올 데이터의 크기만큼 공간을 확보해 놓아야 합니다.
데이터의 크기를 계산하는 방법은 몇 비트 컬러로 변환해서 읽어오느냐에 따라 달라지는데 다음과 같은 공식으로 계산합니다.
8비트:(Width+3) \ 4 * Height * 4
16비트:(Width+1) \2 * Height * 4
24비트:(Width*3+3) \4 * Height * 4
32비트:Width * Height * 4
편한대로 상황에 맞게 크기를 적당히 구해주면 됩니다.
여섯번째 인수는 비트맵 정보를 가진 구조체를 지정하면 되는데 구조체의 구조는 다음과 같습니다.
Public Type BITMAPINFOHEADER '40 bytes
biSize As Long
biWidth As Long
biHeight As Long
biPlanes As Integer
biBitCount As Integer
biCompression As Long
biSizeImage As Long
biXPelsPerMeter As Long
biYPelsPerMeter As Long
biClrUsed As Long
biClrImportant As Long
End Type
Public Type RGBQUAD
rgbBlue As Byte
rgbGreen As Byte
rgbRed As Byte
rgbReserved As Byte
End Type
Public Type BITMAPINFO
bmiHeader As BITMAPINFOHEADER
bmiColors As RGBQUAD
End Type
BITMAPINFO 구조체는 헤더 부분과 팔레트 데이터 부분으로 구성되어 있습니다.
BITMAPINFOHEADER가 헤더 정보를 담은 구조체이고 RGBQUAD가 팔레트 정보를 담은 구조체입니다. 보통 배열로 선언해서 사용합니다.
그러나, 16비트 컬러 이상인 경우는 팔레트 정보를 사용하지 않으므로 BITMAPINFOHEADER 구조체의 이름을 BITMAPINFO로 바꿔서 사용해도 무방합니다.
BITMAPINFOHEADER 구조체에서 반드시 지정해야 할 부분을 살펴보면...
biSize = 헤더의 크기입니다. 40(&h28)으로 지정해야만 합니다. 표준 비트맵은 헤더 크기가 무조건 40입니다.
biPlanes = 플레인 수입니다. 무조건 1로 지정합니다.
biBitCount = 몇 비트 컬러를 사용할지 지정합니다. 흑백=1, 16컬러=4, 256컬러=8, 하이컬러=16, 트루컬러=24나 32를 지정합니다.
biWidth = 비트맵의 가로폭을 지정합니다.
biHeight = 비트맵의 세로폭을 지정합니다. 음수로 지정할 경우 비트맵 데이터의 라인 순서를 반대로 취급합니다.
biSizeImage = 비트맵 데이터의 크기입니다. 이건 지정해도 그만 안해도 그만입니다. 지정 안해도 속도 저하 같은거 없습니다.
나머지 요소는 모조리 0으로 지정하면 됩니다. 다만, 8비트 컬러 이하인 경우에 필요한 요소는 다음과 같습니다.
biClrUsed As Long = 팔레트에서 실제 사용할 색상수를 지정합니다. 0으로 지정하면 팔레트의 모든 요소를 사용합니다.
biClrImportant As Long = 팔레트에서 필수적으로 사용해야 하는 색상수를 지정합니다.
일곱번째 인수는 팔레트 처리방법인데... 보통 DIB_RGB_COLORS(=0)를 지정하면 됩니다. 256컬러인 경우 DIB_PAL_COLORS(=1)를 지정하면 시스템 팔레트가 아닌 논리 팔레트를 사용합니다.
이 함수의 역할은 지정한 비트맵으로부터 비트맵 데이터를 DIB 형식으로 변환하여 지정한 메모리 영역에 복사합니다.
시스템 상황에 따라 다르겠지만 속도는 그리 빠른 편은 아닙니다. BitCount가 높으면 높을수록 속도는 더 느려집니다.
그리고, BITMAPINFO에서 지정한 Width, Height와 비트맵의 실제 Width, Height가 다를 경우는 작업에 실패할 수 있습니다.
스캔라인 값을 지정하면 특정한 라인 범위의 데이터만 읽어오는게 가능하지만 이 함수는 비트맵 전체 데이터를 한번에 읽어오는 목적으로 쓰고 일부만 다루는 것은 다소 불편합니다.
2) SetDIBits
함수 원형은 다음과 같습니다.
Declare Function SetDIBits Lib "gdi32" Alias "SetDIBits" (ByVal hdc As Long, ByVal hBitmap As Long, ByVal nStartScan As Long, ByVal nNumScans As Long, lpBits As Any, lpBI As BITMAPINFO, ByVal wUsage As Long) As Long
GetDIBits와 사용법이 거의 동일합니다. 단지 DIB 데이터를 DDB로 변환해서 비트맵에 출력한다는 점이 다를 뿐...
SetDIBits도 스캔라인 값을 지정해서 일부만 출력할 수 있고 BITMAPINFO의 Width, Height와 비트맵의 실제 Width,Height가 다를 경우 작업에 실패할 수 있습니다.
속도는 GetDIBits에 비해서 비약적으로 빠릅니다. 다만, DDB용 함수인 Bitblt보다 5배에서 10배정도 느립니다.
덤으로, 16비트 이상 컬러로 작업할때의 장단점들을 살펴보면...
16비트 : 데이터 양이 적으므로 가장 속도가 빠릅니다. 다만, 색의 RGB값을 추출하는 과정이 간단하지 않습니다.
24비트 : 색의 RGB값을 간단히 추출 가능하지만 한 픽셀이 3바이트이므로 픽셀의 위치값 계산이 간단하지 않습니다.
32비트 : 색의 RGB값 추출도 쉽고 위치값 계산도 간단하지만 가장 속도가 느립니다.
3) SetDIBitsToDevice, StretchDIBits
함수 원형은 다음과 같습니다.
Declare Function SetDIBitsToDevice Lib "gdi32" Alias "SetDIBitsToDevice" (ByVal hdc As Long, ByVal x As Long, ByVal y As Long, ByVal dx As Long, ByVal dy As Long, ByVal SrcX As Long, ByVal SrcY As Long, ByVal Scan As Long, ByVal NumScans As Long, Bits As Any, BitsInfo As BITMAPINFO, ByVal wUsage As Long) As Long
Declare Function StretchDIBits Lib "gdi32" Alias "StretchDIBits" (ByVal hdc As Long, ByVal x As Long, ByVal y As Long, ByVal dx As Long, ByVal dy As Long, ByVal SrcX As Long, ByVal SrcY As Long, ByVal wSrcWidth As Long, ByVal wSrcHeight As Long, lpBits As Any, lpBitsInfo As BITMAPINFO, ByVal wUsage As Long, ByVal dwRop As Long) As Long
이 함수들은 각기 Bitblt와 StretchBlt의 DIB판이라고 볼 수 있습니다.
SetDIBitsToDevice 함수는 SetDIBits 함수에서 특정 위치에 특정 영역만큼의 데이터만 출력할 수 있다는 점이 다릅니다. 그만큼 속도는 SeyDIBits보다 느립니다.
SetDIBitsToDevice의 인수들을 살펴보면...
x,y : 대상 비트맵에 출력할 위치 설정
dx,dy : 출력을 위한 비트맵 영역 설정
SrcX, SrcY : 원본 데이터로부터 출력할 위치 설정
Bits : 비트맵 데이터의 위치
dwRop : Bitbltdml ROP 설정과 같다.
StretchDIBits의 경우는 원본 hdc 대신 원본 비트맵 데이터를 사용한다는 것을 빼면 StretchBlt와 동일하므로 생략합니다.
4) CreateDIBSection
함수 원형은 다음과 같습니다.
Declare Function CreateDIBSection Lib "gdi32" Alias "CreateDIBSection" (ByVal hDC As Long, pBitmapInfo As BITMAPINFO, ByVal iUsage As Long, lplpVoid As Long, ByVal handle As Long, ByVal dw As Long) As Long
DIBSection은 DIB이면서 DDB이기도 한 형태입니다. 그래서 DIB 데이터를 조작할 수 있으면서 Bitblt 같은 DDB용 함수도 사용 가능합니다.
각 인수들을 살펴보면...
첫번째 인수는 hdc를 지정합니다. API책에는 iUsage=DIB_PAL_COLORS가 아닌 경우는 아무 의미도 없다고 되어있네요.
두번째 인수는 비트맵 정보입니다. 위에서 언급한 내용을 참고하면 됩니다.
세번째 인수는 팔레트 처리방법으로 이 값이 DIB_PAL_COLORS인 경우 지정된 hdc에서 사용하는 팔레트를 사용합니다.
네번째 인수는 DIB 데이터가 있는 위치를 반환받을 Long형 변수를 지정하면 됩니다. 반환된 값은 비트맵 데이터가 위치한 곳의 주소입니다. DIBSection은 필요한 메모리 영역을 알아서 확보한 뒤에 메모리의 핸들을 반환합니다.
다섯번째 인수는 DIBSection과 연동할 파일매핑 객체의 핸들값을 지정합니다. 사용하지 않을 경우 0을 지정하면 됩니다.
여섯번째 인수는 DIBSection과 연동할 파일매핑 객체의 오프셋을 지정합니다. 사용하지 않을 경우 0을 지정합니다.
DIBSection은 위와 같이 파일매핑 객체와 연동해서 쓸 수도 있고 그냥 포인터 변수로 접근해서 쓸 수도 있습니다. 하지만 어느 방법이든 비베에서 쓰기에는 어렵겠지요.(비베의 파일 관리 기능은 파일매핑 같은거 지원 안합니다.)
그래서 비베에서 DIBSection을 쓰기 위한 편법이 있습니다.
바로, 비베의 배열 구조인 SAFEARRAY의 헤더를 건드려서 마치 DIBSection이 사용하는 메모리 영역을 비베의 배열인 것처럼 속여서 배열을 다루듯이 편리하게 접근할 수 있게 하는 것입니다. 다만, 구체적인 방법은 생략합니다.
더 간단한 방법으로는 필요한 크기만큼의 배열을 선언한 후에 CopyMemory로 DIBSection의 데이터를 배열로 옮겨 작업하고 이후 배열의 내용을 DIBSection으로 전송하는 것입니다. 이건 다소 메모리 낭비는 있지만 사용법 자체는 간단합니다.
마지막으로 GetDIBits/SetDIBits와 CreateDIBSection/Bitblt의 속도를 비교해보면...
DIBSection쪽이 속도가 더 빠릅니다. '미세하게' 말이지요. 그리 큰 차이는 없습니다.
(...라고 언급했습니다만... 시험을 해본 결과 DIB Section이 속도의 편차가 가장 적습니다.)
GetDIBits가 속도가 느리다지만 DIBSection을 대상으로 하는 Bitblt의 속도 역시 느립니다.
SetDIBits와 DIBSection을 원본으로 하는 Bitblt의 속도는 그리 큰 차이가 나지 않습니다.
다만, DIBSection은 Bitblt를 사용할 수 있으므로 임의의 영역만 복사한다거나 하는것이 자유롭습니다. GetDIBits나 SetDIBits는 위에서 언급했듯이 그런 면에서 제약이 있습니다. 사용법 면에서는 DIBSection이 훨씬 복잡하고 까다롭겠습니다만...
SAFEARRAY를 조작하는 방법은 DIBSection을 사용하는 소스들을 찾아보면 나올겁니다.
ps.부록으로 SAFEARRAY 구조체에 대해 설명합니다.
Private Type SAFEARRAYBOUND
cElements As Long
lLbound As Long
End Type
Private Type SAFEARRAY2D
cDims As Integer
fFeatures As Integer
cbElements As Long
cLocks As Long
pvData As Long
Bounds(0 To 1) As SAFEARRAYBOUND
End Type
위는 2차원 배열일때의 경우입니다.
cDims : 배열의 차원에 대한 정보
fFeatures : 무슨 용도인지는 알 수 없음. 다만, 정적 배열과 동적 배열의 경우 수치가 다름.
cbElements : 배열 요소 하나당 데이터 크기.
cLocks : 용도를 알 수 없음.
pvData : 배열의 실제 데이터가 들어있는 메모리의 위치.
Bounds : 배열의 각 차원의 첨자 범위 정보
cElements : 배열의 총 요소수
lLbound : 배열의 최소 첨자값
Bounds 배열의 첨자값의 경우 숫자가 적을수록 고차원, 많을수록 저차원의 첨자 범위를 가리킵니다.
위의 경우는 Bounds(0)이 2차원, Bounds(1)이 1차원의 첨자 범위를 가리킵니다.
Bounds의 첨자값을 적당히 수정하면 몇차원의 배열이든 적용이 가능합니다.
댓글
댓글 리스트-
작성자빗방울 작성시간 08.10.11 짝짝짝(싸다구가 아니라 박수임)
-
작성자수학쟁이 작성시간 08.10.27 바로, 비베의 배열 구조인 SAFEARRAY의 헤더를 건드려서 마치 DIBSection이 사용하는 메모리 영역을 비베의 배열인 것처럼 속여서 배열을 다루듯이 편리하게 접근할 수 있게 하는 것입니다. 다만, 구체적인 방법은 생략합니다. → 이건 하우투뱅크의 지상현님이 모자이크 관련 프로젝트에서 썼던 방법이기도 하죠. 저 역시 여기서 힌트를 얻는 아이디어긴 한데... 상당히 재미있는 방법이더군요. ㅎㅎ
-
작성자수학쟁이 작성시간 08.10.27 cLocks → 배열을 고정하기 위해 존재하는 필드입니다.
-
작성자수학쟁이 작성시간 08.10.27 그리고 윈도우는 웃긴 것이, Little Endian방식을 사용하면서 '장치 독립적인 DIB' 라고 한 것은 조금 문제가 있다고 생각되는 부분이라고 개인적으로 보고 있습니다. Sun Sparc 과 같은 Big Endian을 사용하는 장치에서는 이를 호환하지 않으니 문제가 충분히 될 수 있죠. 역시 DIB도 DDB(Device-Dependant Bitmap) 인 것입니다.(탕)