代码改变世界

使用 Libpng 配合 GDI 完成对 png 图片的解析与显示

2013-04-22 05:30  wid  阅读(8417)  评论(9编辑  收藏  举报

 

使用 Libpng 配合 GDI 完成对 png 图片的解析与显示

 

第一步: 使用 libpng 完成对 png 图像的解析

在上一篇 《VC6 下 libpng 库的编译与初步使用》 中我们已经完成了对 libpng 库的编译与配置, 今天就来用它来实现对 png 图片进行解析并且将解析到的图片数据通过 Windows GDI 显示到窗口中。

在这之前, 我们先做个假设:

  • 1. 所使用的图片确实为 png 格式

做假设目的是为了减少演示代码的复杂度, 因为代码中没有对传入的图片是否为png进行检测, 进行过多的错误处理这不符合演示代码的书写习惯。



开始进行图片得读取:

 1     long ReadPngData( const char *szPath, int *pnWidth, int *pnHeight, unsigned char **cbData )
 2     {
 3         FILE *fp = NULL;
 4         long file_size = 0, pos = 0, mPos = 0;
 5         int color_type = 0, x = 0, y = 0, block_size = 0;
 6 
 7         png_infop info_ptr;
 8         png_structp png_ptr;
 9         png_bytep *row_point = NULL;
10 
11         fp = fopen( szPath, "rb" );
12         if( !fp )    return FILE_ERROR;            //文件打开错误则返回 FILE_ERROR
13 
14         png_ptr  = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0);        //创建png读取结构
15         info_ptr = png_create_info_struct(png_ptr);        //png 文件信息结构
16         png_init_io(png_ptr, fp);                //初始化文件 I\O
17         png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_EXPAND, 0);                //读取png文件
18         
19         *pnWidth  = png_get_image_width( png_ptr, info_ptr );        //获得图片宽度
20         *pnHeight = png_get_image_height( png_ptr, info_ptr );        //获得图片高度
21         color_type = png_get_color_type( png_ptr, info_ptr );        //获得图片颜色类型
22         file_size = (*pnWidth) * (*pnHeight) * 4;                    //计算需要存储RGB(A)数据所需的内存大小
23         *cbData = (unsigned char *)malloc(file_size);            //申请所需的内存, 并将传入的 cbData 指针指向申请的这块内容
24 
25         row_point = png_get_rows( png_ptr, info_ptr );            //读取RGB(A)数据
26 
27         block_size = color_type == 6 ? 4 : 3;                    //根据是否具有a通道判断每次所要读取的数据大小, 具有Alpha通道的每次读4字节, 否则读3字节
28 
29         //将读取到的RGB(A)数据按规定格式读到申请的内存中
30         for( x = 0; x < *pnHeight; x++ )
31             for( y = 0; y < *pnWidth*block_size; y+=block_size )
32             {
33                 (*cbData)[pos++] = row_point[x][y + 2];        //B
34                 (*cbData)[pos++] = row_point[x][y + 1];        //G
35                 (*cbData)[pos++] = row_point[x][y + 0];        //R
36                 (*cbData)[pos++] = row_point[x][y + 3];        //Alpha
37             }
38 
39         png_destroy_read_struct(&png_ptr, &info_ptr, 0);
40         fclose( fp );
41 
42         return file_size;
43     }

 

上面我们定义了一个 ReadPngData 的函数, 看一下这个函数的参数:

        long ReadPngData(
            const char *szFileName,        //png文件路径
            int *pnWidth,                //指针类型, 用于传出图片宽度
            int *pnHeight,                //指针类型, 用于传出图片高度
            unsigned char **cbData        //二级指针, 用于指向所申请的用于存储RGB(A)的内存地址
        )

 

在代码中, 还根据获得的色彩类型对每次需要读取的字节数做了简单的判断

    block_size = color_type == 6 ? 4 : 3; //根据是否具有a通道判断每次所要读取的数据大小, 具有Alpha通道的每次读4字节, 否则读3字节

 

该语句使用了问号表达式代替了 if..else 进行判断, 减少了代码的行数。

接下来就是使用两个 for 循环完成对RGB(A)数据的读取工作, 在这里, 你也可以通过不同的 RGB(A) 之间的运算, 将图像呈现出不同的显示效果。

现在, cbData指针指向的内存中就是我们所需的 RGB(A) 数据了, 有了这份数据, 就可以完成显示。该函数已被封装为 .h 头文件, 使用可以直接调用, 完成的封装代码如下: gdipng.h

View Code - gdipng.h
#pragma once

//////////////////////////////////////////////////////////////////////////

#include <stdio.h>
#include "png.h"

//////////////////////////////////////////////////////////////////////////

#define        FILE_ERROR        -1

//////////////////////////////////////////////////////////////////////////

long ReadPngData( const char *szFileName, int *pnWidth, int *pnHeight, unsigned char **cbData );

//////////////////////////////////////////////////////////////////////////

long ReadPngData( const char *szPath, int *pnWidth, int *pnHeight, unsigned char **cbData )
{
    FILE *fp = NULL;
    long file_size = 0, pos = 0, mPos = 0;
    int color_type = 0, x = 0, y = 0, block_size = 0;

    png_infop info_ptr;
    png_structp png_ptr;
    png_bytep *row_point = NULL;

    fp = fopen( szPath, "rb" );
    if( !fp )    return FILE_ERROR;            //文件打开错误则返回 FILE_ERROR

    png_ptr  = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0);        //创建png读取结构
    info_ptr = png_create_info_struct(png_ptr);        //png 文件信息结构
    png_init_io(png_ptr, fp);                //初始化文件 I\O
    png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_EXPAND, 0);                //读取png文件
    
    *pnWidth  = png_get_image_width( png_ptr, info_ptr );        //获得图片宽度
    *pnHeight = png_get_image_height( png_ptr, info_ptr );        //获得图片高度
    color_type = png_get_color_type( png_ptr, info_ptr );        //获得图片色彩深度
    file_size = (*pnWidth) * (*pnHeight) * 4;                    //计算需要存储RGB(A)数据所需的内存大小
    *cbData = (unsigned char *)malloc(file_size);            //申请所需的内容, 并将 *cbData 指向申请的这块内容

    row_point = png_get_rows( png_ptr, info_ptr );            //读取RGB(A)数据

    block_size = color_type == 6 ? 4 : 3;                    //根据是否具有a通道判断每次所要读取的数据大小, 具有Alpha通道的每次读4位, 否则读3位

    //将读取到的RGB(A)数据按规定格式读到申请的内存中
    for( x = 0; x < *pnHeight; x++ )
        for( y = 0; y < *pnWidth*block_size; y+=block_size )
        {
            (*cbData)[pos++] = row_point[x][y + 2];        //B
            (*cbData)[pos++] = row_point[x][y + 1];        //G
            (*cbData)[pos++] = row_point[x][y + 0];        //R
            (*cbData)[pos++] = row_point[x][y + 3];        //alpha
        }

    png_destroy_read_struct(&png_ptr, &info_ptr, 0);
    fclose( fp );

    return file_size;
}

//////////////////////////////////////////////////////////////////////////

 

第二步: 通过 GDI 实现 png 的显示

这里准备了两张png图片, 一个作为背景(bk.png), 一张作为前景(op.png), 图片来源于互联网, 前景图具有 Alpha通道, 用于实现部分透明效果。图片如下:

图: 背景图片 bk.png 图: 前景图片 op.png

1. 创建一个Win32窗口(已折叠)

View Code - WinMain
#include <windows.h>
#include "gdipng.h"

//////////////////////////////////////////////////////////////////////////

LRESULT CALLBACK WndProc( HWND, UINT, WPARAM, LPARAM );

//////////////////////////////////////////////////////////////////////////

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdline, int iCmdShow )
{
    TCHAR szAppName[] = TEXT("GdiPng");

    HWND        hwnd;
    MSG            msg;
    WNDCLASS    wndclass;

    wndclass.cbClsExtra            = 0;
    wndclass.cbWndExtra            = 0;
    wndclass.hbrBackground        = GetSysColorBrush( BLACK_BRUSH );
    wndclass.hCursor            = LoadCursor( NULL, IDC_ARROW );
    wndclass.hIcon                = LoadIcon( NULL, IDI_APPLICATION );
    wndclass.hInstance            = hInstance;
    wndclass.lpfnWndProc        = WndProc;
    wndclass.lpszClassName        = szAppName;
    wndclass.lpszMenuName        = NULL;
    wndclass.style                = CS_HREDRAW | CS_VREDRAW;

    if( !RegisterClass(&wndclass) )
    {
        MessageBox( NULL, TEXT("窗口类注册失败!"), TEXT("应用程序错误"), MB_OK | MB_ICONERROR );
        return 0;
    }

    hwnd = CreateWindow(
        szAppName,
        TEXT("通过 GDI 实现 png 的显示 - Demo"),
        WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
        800, 600,
        NULL, NULL, hInstance, NULL
    );

    ShowWindow( hwnd, iCmdShow );
    UpdateWindow( hwnd );

    while( GetMessage(&msg, NULL, 0, 0) )
    {
        TranslateMessage( &msg );
        DispatchMessage( &msg );
    }

    return msg.wParam;
}

 

2. 在处理 WM_PAINT 消息时进行显示

整个窗口回调函数部分的代码如下:

 1 LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
 2 {
 3     static HBITMAP hPngFr, hPngBk;            //前景、背景位图句柄
 4     HDC         hdc, hdcFr, hdcBk;            //客户区、前景、背景设备句柄
 5     PAINTSTRUCT ps ;
 6     static fr_x, fr_y;        //前景图宽、高
 7     static bk_x, bk_y;        //背景图宽、高
 8 
 9     static unsigned char *cbFrData=NULL;        //用于指向前景图的RGB(A)数据内存地址
10     static unsigned char *cbBkData=NULL;        //用于指向背景图的RGB(A)数据内存地址
11 
12     BLENDFUNCTION bf = {0};            //BLENDFUNCTION结构变量, 作为函数参数, 用于 AlphaBlend 函数进行 Alpha 的融图
13     bf.BlendOp = AC_SRC_OVER;
14     bf.BlendFlags = 0;
15     bf.AlphaFormat = AC_SRC_ALPHA;
16     bf.SourceConstantAlpha = 255;        //透明度, 范围为 0-255, 0:透明, 255:不透明
17      
18     switch (message)
19     {
20     case WM_CREATE:        //处理窗口创建消息
21         ReadPngData( "op.png", &fr_x, &fr_y, &cbFrData );        //读取前景图 op.png 数据
22         ReadPngData( "bk.png", &bk_x, &bk_y, &cbBkData );        //读取背景如 bk.png 数据
23         hPngFr = CreateBitmap( fr_x, fr_y, 32, 1, cbFrData );    //创建前景位图
24         hPngBk = CreateBitmap( bk_x, bk_y, 32, 1, cbBkData );    //创建背景位图
25         return 0 ;
26           
27     case WM_PAINT:        //处理重绘消息
28         hdc = BeginPaint (hwnd, &ps) ;        //开始重绘
29         
30         hdcFr = CreateCompatibleDC( hdc );        //创建前景设备句柄
31         SelectObject( hdcFr, hPngFr );            //将前景图句柄选入设备环境
32         
33         hdcBk = CreateCompatibleDC( hdc );        //创建背景设备句柄
34         SelectObject( hdcBk, hPngBk );            //将背景图句柄选入设备环境
35           
36         BitBlt( hdc, 0, 0, bk_x, bk_y, hdcBk, 0, 0, SRCCOPY );                    //将背景图进行 BitBlt 操作显示在客户区中
37         AlphaBlend( hdc, 100, 0, fr_x, fr_y, hdcFr, 0, 0, fr_x, fr_y, bf );        //使用 AlphaBlend 函数将前景与背景进行融合
38 
39         DeleteDC( hdcFr );        //删除前景图设备环境句柄
40         DeleteDC( hdcBk );        //删除背景图设备环境句柄
41         EndPaint( hwnd, &ps );    //结束重绘
42         return 0 ;
43           
44     case WM_DESTROY:    //处理撤销窗口消息
45         PostQuitMessage (0) ;
46         return 0 ;
47     }
48 
49     return DefWindowProc (hwnd, message, wParam, lParam) ;
50 }

 

Wnd_Main.c 完整代码(已折叠):

View Code - W_Main.c
#include <windows.h>
#include "gdipng.h"

//////////////////////////////////////////////////////////////////////////

LRESULT CALLBACK WndProc( HWND, UINT, WPARAM, LPARAM );

//////////////////////////////////////////////////////////////////////////

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdline, int iCmdShow )
{
    TCHAR szAppName[] = TEXT("GdiPng");

    HWND        hwnd;
    MSG            msg;
    WNDCLASS    wndclass;

    wndclass.cbClsExtra            = 0;
    wndclass.cbWndExtra            = 0;
    wndclass.hbrBackground        = GetSysColorBrush( BLACK_BRUSH );
    wndclass.hCursor            = LoadCursor( NULL, IDC_ARROW );
    wndclass.hIcon                = LoadIcon( NULL, IDI_APPLICATION );
    wndclass.hInstance            = hInstance;
    wndclass.lpfnWndProc        = WndProc;
    wndclass.lpszClassName        = szAppName;
    wndclass.lpszMenuName        = NULL;
    wndclass.style                = CS_HREDRAW | CS_VREDRAW;

    if( !RegisterClass(&wndclass) )
    {
        MessageBox( NULL, TEXT("窗口类注册失败!"), TEXT("应用程序错误"), MB_OK | MB_ICONERROR );
        return 0;
    }

    hwnd = CreateWindow(
        szAppName,
        TEXT("通过 GDI 实现 png 的显示 - Demo"),
        WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
        800, 600,
        NULL, NULL, hInstance, NULL
    );

    ShowWindow( hwnd, iCmdShow );
    UpdateWindow( hwnd );

    while( GetMessage(&msg, NULL, 0, 0) )
    {
        TranslateMessage( &msg );
        DispatchMessage( &msg );
    }

    return msg.wParam;
}

//////////////////////////////////////////////////////////////////////////

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    static HBITMAP hPngFr, hPngBk;            //前景、背景位图句柄
    HDC         hdc, hdcFr, hdcBk;            //客户区、前景、背景设备句柄
    PAINTSTRUCT ps ;
    static fr_x, fr_y;        //前景图宽、高
    static bk_x, bk_y;        //背景图宽、高

    static unsigned char *cbFrData=NULL;        //用于指向前景图的RGB(A)数据内存地址
    static unsigned char *cbBkData=NULL;        //用于指向背景图的RGB(A)数据内存地址

    BLENDFUNCTION bf = {0};            //BLENDFUNCTION结构变量, 作为函数参数, 用于 AlphaBlend 函数进行 Alpha 的融图
    bf.BlendOp = AC_SRC_OVER;
    bf.BlendFlags = 0;
    bf.AlphaFormat = AC_SRC_ALPHA;
    bf.SourceConstantAlpha = 255;        //透明度, 范围为 0-255, 0:透明, 255:不透明
     
    switch (message)
    {
    case WM_CREATE:        //处理窗口创建消息
        ReadPngData( "op.png", &fr_x, &fr_y, &cbFrData );        //读取前景图 op.png 数据
        ReadPngData( "bk.png", &bk_x, &bk_y, &cbBkData );        //读取背景如 bk.png 数据
        hPngFr = CreateBitmap( fr_x, fr_y, 32, 1, cbFrData );    //创建前景位图
        hPngBk = CreateBitmap( bk_x, bk_y, 32, 1, cbBkData );    //创建背景位图
        return 0 ;
          
    case WM_PAINT:        //处理重绘消息
        hdc = BeginPaint (hwnd, &ps) ;        //开始重绘
        
        hdcFr = CreateCompatibleDC( hdc );        //创建前景设备句柄
        SelectObject( hdcFr, hPngFr );            //将前景图句柄选入设备环境
        
        hdcBk = CreateCompatibleDC( hdc );        //创建背景设备句柄
        SelectObject( hdcBk, hPngBk );            //将背景图句柄选入设备环境
          
        BitBlt( hdc, 0, 0, bk_x, bk_y, hdcBk, 0, 0, SRCCOPY );                    //将背景图进行 BitBlt 操作显示在客户区中
        AlphaBlend( hdc, 100, 0, fr_x, fr_y, hdcFr, 0, 0, fr_x, fr_y, bf );        //使用 AlphaBlend 函数将前景与背景进行融合
        //TransparentBlt( hdc, 200, 0, fr_x, fr_y, hdcFr, 0, 0, fr_x, fr_y, RGB(255, 255, 255) );        //对纯色背景的透明操作

        DeleteDC( hdcFr );        //删除前景图设备环境句柄
        DeleteDC( hdcBk );        //删除背景图设备环境句柄
        EndPaint( hwnd, &ps );    //结束重绘
        return 0 ;
          
    case WM_DESTROY:    //处理撤销窗口消息
        PostQuitMessage (0) ;
        return 0 ;
    }

    return DefWindowProc (hwnd, message, wParam, lParam) ;
}

 

运行效果如下:

 

关于 AlphaBlend 函数

该函数用来显示具有Alpha通道的图像, 将两个图像按照图片得Alpha数据以及指定的透明度进行融合。其函数原型:

        BOOL AlphaBlend(
            HDC hdcDest,                 // 目标环境设备句柄
            int nXOriginDest,            // 目标环境设备的x坐标
            int nYOriginDest,            // 目标环境设备的y坐标
            int nWidthDest,              // 目标矩形区域的宽度
            int nHeightDest,             // 目标矩形区域的高度
            HDC hdcSrc,                  // 源设备环境句柄
            int nXOriginSrc,             // 源环境设备的x坐标
            int nYOriginSrc,             // 源环境设备的y坐标
            int nWidthSrc,               // 源矩形区域的宽度
            int nHeightSrc,              // 源矩形区域的高度
            BLENDFUNCTION blendFunction  // BLENDFUNCTION结构, 指定融合的方式
        );

 

注意: 该函数在使用时需要在IDE的对象\模块库中添加 msimg32.lib, 或者使用 #pragma comment(lib, "msimg32.lib") 命令。

 

关于 BLENDFUNCTION 结构

BLENDFUNCTION 结构在 MSDN中的定义:

        typedef struct _BLENDFUNCTION {
            BYTE     BlendOp;                    // 指定为 AC_SRC_OVER
            BYTE     BlendFlags;                 // 必须为 0
            BYTE     SourceConstantAlpha;        // 源位图的透明度, 范围为 0-255, 0:透明, 255:不透明
            BYTE     AlphaFormat;                // 指定为 AC_SRC_ALPHA 
        }BLENDFUNCTION;

 

代码解说:

在这段代码中, 首先在处理 WM_CREATE 消息时对前景png图以及背景png图进行读取, 同时, 也获取到了这两张png图片得宽度和高度, 再通过 CreateBitmap 函数将得到的 RGB(A) 数据转为 BITMAP 型的位图, 将位图句柄 hPngFrhPngBk 指向这两张新创建的位图。

接下来就是在处理 WM_PAINT 消息时进行 png 的绘图操作, 创建前景、背景的设备环境句柄 hdcFrhdcBk, 将位图句柄分别选入到设备环境中。

显示是通过两个函数来完成的, BitBltAlphaBlend。 BltBlt 将 RGB 数据进行输出, 不自动处理 Alpha 数据。 AlphaBlend 函数负责前后背景的融图。

最后就是删除创建的设备环境句柄 DeleteDC( hdcFr ); 、 DeleteDC( hdcBk );。

三、处理纯色背景的png图片

有些 png 不具备a通道, 但是背景为纯色, 如下图中的一座浮岛的png图片(il.png)所示:

通过观察可以发现, 该图片得背景为 RGB(255, 255, 255) 的纯白色背景, 对于这样的图片, 要实现纯色部分的透明显示最简单的办法就是借助 TransparentBlt 函数, 该函数的作用是将图片中的某一种颜色当做透明颜色并与目标设备中的颜色进行融合, 其函数的原型:

        BOOL TransparentBlt(
            HDC hdcDest,        // 目标设备环境的句柄
            int nXOriginDest,   // 目标矩形左上角的x轴坐标
            int nYOriginDest,   // 目标矩形左上角的y轴坐标
            int nWidthDest,     // 目标矩形宽度
            int hHeightDest,    // 目标矩形高度
            HDC hdcSrc,         // 源设备环境句柄
            int nXOriginSrc,    // 源矩形左上角的x轴坐标
            int nYOriginSrc,    // 源标矩形左上角的y轴坐标
            int nWidthSrc,      // 源矩形宽度
            int nHeightSrc,     // 源矩形高度
            UINT crTransparent  // 源位图中的RGB值当作透明颜色
        );

 

现在将 BitBlt 和 AlphaBlend 操作替换为 BitBlt 和 TransparentBlt 操作:

        BitBlt( hdc, 0, 0, bk_x, bk_y, hdcBk, 0, 0, SRCCOPY );
        TransparentBlt( hdc, 200, 0, fr_x, fr_y, hdcFr, 0, 0, fr_x, fr_y, RGB(255, 255, 255) );

 

运行效果如图:

 

这种处理方式有个弊端就是, 当图像内部也用到与将被扣去的颜色值时, 抠出来的效果就变得不符合我们期望值, 这时, 通过 PS 或者其他图像处理软件对图片进行二次处理是个不错的办法。

 

本篇博文中的代码与示例素材下载(含VC6工程文件): https://files.cnblogs.com/mr-wid/gdipng_demo.zip

 

--------------------

 

 

wid, 2013.04.22