探究 LoadImage 内部的图像缩放函数

最近研究界面编程涉及到了位图缩放。但发现系统提供的 StretchDIBits 缩放效果很不理想。但又研究发现 LoadImage 这个 API 内部实现了缩放功能。下面是 LoadImage 原型。

HANDLE LoadImage(
    HINSTANCE hinst,  // handle of the instance that contains the image
    LPCTSTR lpszName, // name or identifier of image
    UINT uType, // type of image
    int cxDesired, // desired width
    int cyDesired, // desired height
    UINT fuLoad // load flags
   );
设置此函数的 cxDesired cyDesired 参数实现缩放,效果较好。但是这个函数只能从文件或资源中获取位图。不方便用于已读入内存的位图。

而 StretchDIBits 可以实现但是效果又不理想。这使我有了研究 LoadImage 内部的冲动…… -_-!

首先看了 ReactOS 中的 LoadImage,他对 BITMAP 类型的文件使用 LoadBitmapImage 处理。

      case IMAGE_BITMAP:
         return LoadBitmapImage(hinst, lpszName, fuLoad);

忽略了 cxDesired 和 cyDesired。显然这一点和 Windows 不同。我只好使用OD调试 Windows 的 user32.dll。

我假设 LoadImage 内部使用 StretchDIBits 进行缩放,对 StretchDIBits 下断。果然断了下来。但发觉函数的参数不对。

(原图像宽高均40,cxDesired,cyDesired设置为30)

0012F318   77D1CAB6  /CALL 到 StretchDIBits 来自 user32.77D1CAB0
0012F31C   E8011199  |hDC = E8011199
0012F320   00000000  |XDest = 0
0012F324   00000000  |YDest = 0
0012F328   0000001E  |WidthDest = 1E (30.)
0012F32C   0000001E  |HeightDest = 1E (30.)
0012F330   00000000  |XSrc = 0
0012F334   00000000  |YSrc = 0
0012F338   0000001E  |WidthSrc = 1E (30.)
0012F33C   0000001E  |HeightSrc = 1E (30.)
0012F340   001493C8  |pBits = 001493C8
0012F344   00148FA0  |pBitmapinfo = 00148FA0
0012F348   00000000  |Usage = DIB_RGB_COLORS
0012F34C   00CC0020  \ROP = SRCCOPY

参数中源宽高和目标宽高是相同的,都是缩放后的30,明显在调用这个函数之前就已经处理好了位图数据。

位图缓冲区是 001493C8,重新调试了1次后发现这个地址不变。然后我对这个地址下了硬件访问断点以得到缩放位图数据的函数。运行,断在了下面的位置

77D21C35    52              push    edx
77D21C36    03C8            add     ecx, eax
77D21C38    FF71 03         push    dword ptr [ecx+3]
77D21C3B    FF31            push    dword ptr [ecx]
77D21C3D    FF70 03         push    dword ptr [eax+3]
77D21C40    FF30            push    dword ptr [eax]
77D21C42    E8 EFB7FFFF     call    77D1D436    <<<< 插值算法,根据4个像素点求1个像素点
77D21C47    8BC8            mov     ecx, eax
77D21C49    C1E9 10         shr     ecx, 10
77D21C4C    880F            mov     byte ptr [edi], cl   <<<<< EIP 断在这里
77D21C4E    47              inc     edi
77D21C4F    8827            mov     byte ptr [edi], ah
77D21C51    47              inc     edi
77D21C52    8807            mov     byte ptr [edi], al

用IDA分析这个函数,发现这个函数有很多参数 -_-! SO MANY…

int __stdcall sub_77D21BD0(int a1, int BufferSrc, int LineBytes, int a4, int a5, int a6, int a7, int BufferDst, int a9, int DestWidth, int DestHeight)

有一些参数懒得猜测其意,继续向上分析

交叉参考发现这个函数在一个函数表里……用OD返回到调用他的函数。发现是这样的

77D1D71B    FF10            call    dword ptr [eax]    <<<< 是从这里进入 sub_77D21BD0
77D1D71D    834D FC FF      or      dword ptr [ebp-4], FFFFFFFF
77D1D721    33C0            xor     eax, eax
77D1D723    40              inc     eax
77D1D724    E8 D7AEFFFF     call    77D18600
77D1D729    C2 1000         retn    10

retn 10 可知此函数有 4 个参数。执行到返回。来到了下面的位置

77D1D5DF    56              push    esi
77D1D5E0    53              push    ebx
77D1D5E1    FF75 2C         push    dword ptr [ebp+2C]
77D1D5E4    8943 04         mov     dword ptr [ebx+4], eax
77D1D5E7    8B45 18         mov     eax, dword ptr [ebp+18]
77D1D5EA    FF75 30         push    dword ptr [ebp+30]
77D1D5ED    8943 08         mov     dword ptr [ebx+8], eax
77D1D5F0    66:8B45 FC      mov     ax, word ptr [ebp-4]
77D1D5F4    66:8943 0E      mov     word ptr [ebx+E], ax
77D1D5F8    E8 24000000     call    77D1D621
77D1D5FD    85C0            test    eax, eax  <<<< 返回到这里

对他的上一句 call 下断点重新运行程序。得到4个参数

0012F33C   00146C20
0012F340   00AD0036
0012F344   00146C70
0012F348   00147098

查看内存后我发现了这个函数的原型

int __stdcall sub_77D1D621(BITMAPINFO *SrcHdr, PVOID *lpSrcBits, BITMAPINFO *DstHdr, PVOID *lpDstBits)

这样。我们就可以使用 sub_77D1D621 这个内部函数缩放位图数据啦。

可是怎么使用呢。由于是user32内部函数,没有导出。在不同的操作系统这个函数的位置是不同的。我当初想过把这个函数抠出来独立实现,可是发现这个代码写的实在是有点搞……里面利用了一些函数表,而且一些代码乱乱的。不好移植,所以干脆就使用特征码的方法搜索吧! -_-!

查看了 win2000/xp/2003/vista 的 user32.dll
,发现除win2000的函数开头有些不一致,其他版本的win大体还是相同的!

77D1D621    6A 28           push    28
77D1D623    68 30D7D177     push    77D1D730
77D1D628    E8 93AFFFFF     call    77D185C0
77D1D62D    33FF            xor     edi, edi
77D1D62F    8B4D 08         mov     ecx, dword ptr [ebp+8]
77D1D632    3979 10         cmp     dword ptr [ecx+10], edi

这是winxp sp3开头的6行代码。由于2、3行涉及到了变量我们使用后三行作为特征码

特征码为 33 FF 8B 4D 08 39 79 10 共8个字节。

下面是搜索函数

GetStretchBits Proc uses esi edi ebx
Local hUser32

invoke GetModuleHandle,CTEXT(”user32.dll”)
mov edi,eax

@@: .if dword ptr [edi] == 4d8bff33h && dword ptr [edi+4] == 10793908h
.if word ptr [edi-12] == 286Ah ;winxp/2003/vista
lea eax,[edi-12]
ret
.endif
.if dword ptr [edi-28h] == 6AEC8B55h ;win2000
lea eax,[edi-28h]
ret
.endif
mov eax,0 ;无法识别
ret
.endif
inc edi
jmp @b

ret
GetStretchBits EndP

对于 40×40 的一个QQ头像,缩放至20×20,与 Photoshop(两次立方平滑)/StretchDIBits 对比效果如下

 result

从左到右依次为

1.源图像
2.user32.LoadImage 内部函数处理
3.Photoshop(两次立方平滑)
4.StretchDIBits

 
下面我给出大致的测试代码片段
测试平台 LENOVO_XP_PRO_SP3_CHS_090416
.586
.Model Flat,StdCall
Option CaseMap :None

Include Windows.inc
Include User32.inc
Include Kernel32.inc
Include Gdi32.inc

IncludeLib User32.lib
IncludeLib Kernel32.lib
IncludeLib Gdi32.lib

.Code
GetStretchBits Proc uses esi edi ebx
  Local hUser32
  
  invoke GetModuleHandle,CTEXT(”user32.dll”)
  mov edi,eax
  
@@:  .if dword ptr [edi] == 4d8bff33h && dword ptr [edi+4] == 10793908h
   .if word ptr [edi-12] == 286Ah
    lea eax,[edi-12]
    ret
   .endif
   .if dword ptr [edi-28h] == 6AEC8B55h
    lea eax,[edi-28h]
    ret
   .endif
   mov eax,0
   ret
  .endif
  inc edi
  jmp @b
  
  ret
GetStretchBits EndP

Start Proc
 Local hFile,dwFileSize,dwRead
 Local lpBI,lpBits
 Local lpSrcBuffer,lpDstBuffer
 Local bi:BITMAPINFOHEADER
 Local hDC
 
 invoke CreateFile,CTEXT(”NEWFACE\24.bmp”),GENERIC_ALL,FILE_SHARE_READ,0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0 ;40×40 24Bit Bitmap
 mov hFile,eax
 invoke GetFileSize,hFile,0
 mov dwFileSize,eax
 invoke LocalAlloc,LPTR,65536
 mov lpSrcBuffer,eax
 invoke ReadFile,hFile,lpSrcBuffer,dwFileSize,addr dwRead,0 ;读入lpSrcBuffer
 invoke CloseHandle,hFile
 
 mov esi,lpSrcBuffer
 add esi,[esi][BITMAPFILEHEADER.bfOffBits]
 mov lpBits,esi  ;定位位图数据区
 
 mov esi,lpSrcBuffer
 add esi,sizeof BITMAPFILEHEADER
 mov lpBI,esi  ;定位 源 BITMAPFILEHEADER
 
 invoke RtlMoveMemory,addr bi,lpBI,sizeof BITMAPINFOHEADER ;复制一份 BITMAPFILEHEADER 当作 目标 BITMAPFILEHEADER
 mov bi.biWidth,20  ;设置缩放宽
 mov bi.biHeight,20  ;设置缩放高
 
 invoke LocalAlloc,LPTR,65536 ;申请目标缓冲区
 mov lpDstBuffer,eax
 
 invoke GetStretchBits  ;获取函数地址
 .if eax
  push lpDstBuffer
  lea ecx,bi
  push ecx
  push lpBits
  push lpBI
  call eax  ;调用之
 .endif
 
 invoke GetDC,0   ;获取屏幕 DC
 mov hDC,eax
 
 invoke StretchDIBits,hDC,0,0,20,20,0,0,20,20,lpDstBuffer,addr bi,DIB_RGB_COLORS,SRCCOPY ;画在屏幕 0,0 处
 invoke ExitProcess,0
Start EndP
End Start

(完)

Continue reading » · Written on: 05-26-09 · No Comments »

Leave a Reply