Monday, March 21, 2005

 

Some notes on GDIPlus

I.
An old essay written in 2001, first published on www.vccode.com

先转一段NorthTibet的话。“GDI+是GDI图形库的一个增强版本,C++可以使用这个库。它内建于Windows XP 和Microsoft .NET,而对于Windows 98、Windows NT和Windows 2000,则有一个可重新发布的版本。GDI+是一个C++ API。它用C++类和C++方法。”

首先要说的是,原来的VC6(比如我用的)是不支持GDI+的,所以您要使用的话,必须更新您的SDK。连到下面的地方就行了,您可以选择更新那些内容

http://www.microsoft.com/msdownload...msdk/sdkupdate/

VC7自带,您就可以省了。
如果想用VC6.0但又要用GDI+库,可以用提供zgrong单独的GDI+库,只有900多K

rar文件在http://www.vccode.com/forum/attachm...?s=&postid=4907

装完了之后,您可以发现include目录下面多了很多东西比如base64什么的,反正改改Visual Studio的include目录,您就可以编译了。下面借着再引用NorthTibet的一段,没办法不想敲字

“为了使用GDI+,你必须包含(#include)文件,并将工程链接到gdiplus.lib库,这两个文件包含在最新的Windows SDK中。”“但有两个障碍要解决:第一个是你必须对GDI库进行初始化以及最后的终止释放操作,与其说它是个障碍,还不如说它是GDI+本身的需要,在哪里进行这两个操作呢?最佳的地方莫过于在程序的 InitInstance 和 ExitInstance函数中: //初始化 GDI
class CMyApp : public CWinApp {
protected:
GdiplusStartupInput m_gdiplusStartupInput;
ULONG_PTR m_gdiplusToken;
…….
};
//释放GDI
BOOL CMyApp::InitInstance()
{
VERIFY(GdiplusStartup(&m_gdiplusToken,
&m_gdiplusStartupInput, NULL)==Ok);
…….
}
int CMyApp::ExitInstance()
{
GdiplusShutdown(m_gdiplusToken);
return CWinApp::ExitInstance();
}
CMyApp::m_gdiplusToken 是一个很神奇的东东,它来自GdiplusStartup 并被传递到GdiplusShutdown。m_gdiplusStartupInput 是一个结构,它包含某些GDI+的启动参数。缺省构造函数建立一个智能的缺省值,它又一次证明了C++比C更好。一旦你启动GDI+,就可以使用它了。”

借着您就可以声明很多类,关键是Image,Bitmap等类,他们的属性,方法用自动补全就可以看的很清楚了。MSDN提供一个还不错的启蒙教程(当然和以往一样,N多地方模糊不清),跟着趟一遍混水,就都知道了

Codeproject的GDI+目录是一定要去的

http://www.codeproject.com/vcpp/gdiplus/

另外还有一个特别需要注意的问题,就是如何让GDI+和VC的Debug模式兼容。

方法一:

By the way, if you count this error
"error C2660: 'new' : function does not take 3 parameters"

You can find and remove the line "#define new DEBUG_NEW" on the top of file.

#ifdef _DEBUG
//#define new DEBUG_NEW <-- remove it
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

Or choose a better way as following:
Instead of doing that, because it turns off the DEBUG version of "new" for the entire file, I would suggest just turning it off for the line(s) of code that need to use the real version of new:


#if defined( _DEBUG )
#undef new
#endif
//
// Code That Uses "Placement New" (For Example...)
//
#if defined( _DEBUG )
#define new DEBUG_NEW
#endif

方法二:
Instead of doing that, you can add "::" in front of the operater "new".

(from msdn)
new Operator

http://msdn.microsoft.com/library/en-us/vclang/html/_pluslang_new_Operator.asp

Summary: C++ Language Reference new Operator Grammar new-expression: [::] new [placement] new-type-name [new-initializer] [::] new [placement] ( type-name ) [new-initializer] The new keyword allocates memory for an object or array of objects of type-name from the free store and
Category = MSDN - library

方法三:目前我最喜欢的方法
使用 http://www.codeproject.com/vcpp/gdiplus/gdiplush.asp
提供的GDI+help文件

OK, that's all!

II.
Access image data

作者:Bob Powell
出处:http://www.bobpowell.net/lockingbits.htm

(按:下面几篇都是很基本的GDI+讲义)

Many image processing tasks and even file type conversions, say from 32 bit-per-pixel to 8 bit-per-pixel can be speeded up by accessing the pixel data array directly, rather than relying on GetPixel and SetPixel or other methods.

You will be aware that .NET is a managed code system which most often uses managed data so it's not often that we need to gain access to bytes stored in memory anymore however, image manipulation is one of the few times when managed data access is just too slow and so we need to delve once again into the knotty problems of finding the data and manipulating it.

Before I start on the subject in hand, I'll just remind you that the methods used to access any unmanaged data will be different depending on the language in which your program is written. C# developers have the opportunity, via the unsafe keyword and use of pointers, to access data in memory directly. Visual basic programmers should access such data through the Marshal class methods which may also show a small performance loss.

Lock up your bits
The Bitmap class provides the LockBits and corresponding UnlockBits methods which enable you to fix a portion of the bitmap pixel data array in memory, access it directly and finally replace the bits in the bitmap with the modified data. LockBits returns a BitmapData class that describes the layout and position of the data in the locked array.

The BitmapData class contains the following important properties;

Scan0 The address in memory of the fixed data array
Stride The width, in bytes, of a single row of pixel data. This width is a multiple, or possiblysub-multiple, of the pixel dimensions of the image and may be padded out to include a few more bytes. I'll explain why shortly.
PixelFormat The actual pixel format of the data. This is important for finding the right bytes
Width The width of the locked image
Height The height of the locked image

The Stride property, as shown in figure 1, holds the width of one row in bytes. The size of a row however may not be an exact multiple of the pixel size because for efficiency, the system ensures that the data is packed into rows that begin on a four byte boundary and are padded out to a multiple of four bytes. This means for example that a 24 bit per pixel image 17 pixels wide would have a stride of 52. The used data in each row would take up 3*17 = 51 bytes and the padding of 1 byte would expand each row to 52 bytes or 13*4 bytes. A 4BppIndexed image of 17 pixels wide would have a stride of 12. Nine of the bytes, or more properly eight and a half, would contain data and the row would be padded out with a further 3 bytes to a 4 byte boundary.

The data carrying portion of the row, as has been suggested above, is laid out according to the pixel format. A 24 bit per pixel image containing RGB data would have a new pixel every 3 bytes, a 32 bit per pixel RGBA every four bytes. Pixel formats that contain more than one pixel per byte, such as the 4 bit per pixel Indexed and 1 bit per pixel indexed, have to be processed carefully so that the pixel required is not confused with it's neigbour pixels in the same byte.

Finding the right byte.
Because the stride is the width of a row, to index any given row or Y coordinate you can multiply the stride by the Y coordinate to get the beginning of a particular row. Finding the correct pixel within the row is possibly more difficult and depends on knowing the layout of the pixel formats. The following examples show how to access a particular pixel for a given pixel format.

Format32BppArgb Given X and Y coordinates, the address of the first element in the pixel is Scan0+(y * stride)+(x*4). This Points to the blue byte. The following three bytes contain the green, red and alpha bytes.

Format24BppRgb Given X and Y coordinates, the address of the first element in the pixel is Scan0+(y*Stride)+(x*3). This points to the blue byte which is followed by the green and the red.

Format8BppIndexed Given the X and Y coordinates the address of the byte is Scan0+(y*Stride)+x. This byte is the index into the image palette.

Format4BppIndexed Given X and Y coordinates the byte containing the pixel data is calculated as Scan0+(y*Stride)+(x/2). The corresponding byte contains two pixels, the upper nibble is the leftmost and the lower nibble is the rightmost of two pixels. The four bits of the upper and lower nibble are used to select the colour from the 16 colour palette.

Format1BppIndexed Given the X and Y coordinates, the byte containing the pixel is calculated by Scan0+(y*Stride)+(x/8). The byte contains 8 bits, each bit is one pixel with the leftmost pixel in bit 8 and the rightmost pixel in bit 0. The bits select from the two entry colour palette.

Iterating through the pixels
For pixel formats with one or more bytes per pixel, the formula is simple and can be accomplished by looping through all Y and X values in order. The code in the following listings sets the blue component of a 32 bit per pixel image to 255. In both cases bm is a bitmap previously created.

BitmapData bmd=bm.LockBits(new Rectangle(0, 0, 10, 10), System.Drawing.Imaging.ImageLockMode.ReadOnly, bm.PixelFormat);

int PixelSize=4;



for(int y=0; y< bmd.Height; y++)

{

byte* row=(byte *)bmd.Scan0+(y*bmd.Stride);

for(int x=0; x
{

row[x*PixelSize]=255;

}

}

In VB this operation would be treated a little differently because VB has no knowledge of pointers and requires the use of the marshal class to access unmanaged data.

Dim x As Integer

Dim y As Integer

Dim PixelSize As Integer = 4

Dim bmd As BitmapData = bm.LockBits(new Rectangle(0, 0, 10, 10), System.Drawing.Imaging.ImageLockMode.ReadOnly, bm.PixelFormat)



For y = 0 To bmd.Height - 1

For x = 0 To bmd.Width - 1

Marshal.WriteByte(bmd.Scan0, (bmd.Stride * y) + (4 * x) , 255)

Next

Next

Sub-byte pixels.
The Format4BppIndexed and Format1BppIndexed pixel formats mentioned earlier both have more than one pixel stored in a byte. In such cases, it's up to you to ensure that changing the data for one pixel does not effect the other pixel or pixels held in that byte.

The method for indexing a 1 bit per pixel image is shown in the GDI+ FAQ article "Converting an RGB image to 1 bit-per-pixel monochrome." It relies on using bitwise logical operations And and Or to reset or set specific bits in the byte. After using the formula shown above for 1 bit per pixel images, the lower 3 bits of the X coordinate is used to select the bit required. The listings below show this process in C# and VB. In both examples bmd is a bitmap data extracted from a 1 bit per pixel image.

C# code uses pointers and requires compiling with unsafe code



byte* p=(byte*)bmd.Scan0.ToPointer();

int index=y*bmd.Stride+(x>>3);

byte mask=(byte)(0x80>>(x&0x7));

if(pixel)

p[index]|=mask;

else

p[index]&=(byte)(mask^0xff);

VB code uses the marshal class

Dim mask As Byte = 128 >> (x And 7)

Dim offset As Integer = (y * bmd.Stride) + (x >> 3)

Dim currentPixel As Byte = Marshal.ReadByte(bmd.Scan0, offset)

If pixel = True Then

Marshal.WriteByte(bmd.Scan0, offset, currentPixel Or mask)

Else

Marshal.WriteByte(bmd.Scan0, offset, CByte(currentPixel And (mask Xor 255)))

End If

Note, it's quite valid to use the Marshal class from C# code. I used pointers because it offers the best performance.

Accessing individual pixels in a 4 bit per pixel image is handled in a similar manner. The upper and lower nibble of the byte must be dealt with separately and changing the contents of the odd X pixels should not effect the even X pixels. The code below shows how to perform this in C# and VB.

C#

int offset = (y * bmd.Stride) + (x >> 1);

byte currentByte = ((byte *)bmd.Scan0)[offset];

if((x&1) == 1)

{

currentByte &= 0xF0;

currentByte |= (byte)(colorIndex & 0x0F);

}

else

{

currentByte &= 0x0F;

currentByte |= (byte)(colorIndex << 4);

}

((byte *)bmd.Scan0)[offset]=currentByte;





VB

Dim offset As Integer = (y * bmd.Stride) + (x >> 1)

Dim currentByte As Byte = Marshal.ReadByte(bmd.Scan0, offset)

If (x And 1) = 1 Then

currentByte = currentByte And &HF0

currentByte = currentByte Or (colorIndex And &HF)

Else

currentByte = currentByte And &HF

currentByte = currentByte Or (colorIndex << 4)

End If

Marshal.WriteByte(bmd.Scan0, offset, currentByte)

Using LockBits and UnlockBits
The LockBits method takes a rectangle which may be the same size or smaller than the image being processed, a PixelFormat which is usually the same as that of the image being processed and a ImageLockMode value that specifies whether the data is read-only, write-only, read-write or a user allocated buffer. This last option cannot be used from C# or VB because the method overload for LockBits that specifies a user buffer is not included in the GDI+ managed wrapper.

It is very important that when all operations are complete the BitmapData is put back into the bitmap with the UnlockBits method. The snippet of code below illustrates this.

Dim bmd As BitmapData = bm.LockBits(New Rectangle(0, 0, 10, 10), ImageLockMode.ReadWrite, bm.PixelFormat)

' do operations here

bm.UnlockBits(bmd)

Summary
That just about covers the aspects of accessing the most popular and most difficult pixel formats directly. Using these techniques instead of the GetPixel and SetPixel methods provided by Bitmap will show a marked performance boost to your image processing and image format conversion routines.

III.
When to dispose of GDI+ resources

作者:Bob Powell
出处:http://www.bobpowell.net/disposing_of_resources.htm
http://www.bobpowell.net/creategraphics.htm

GDI+ is still an unmanaged technology but .NET provides us with managed wrappers that are lifetime managed by the Garbage Collector. This means that when the pen object goes out of scope, it will be marked for garbage collection and eventually reclaimed by the system, freeing up its resources as it goes.

This kind of technique is fine for quick demos and so on but if you're serious about your graphics you should behave nicely and preemptively dispose of GDI+ objects when you're done with them.

Explicitly disposing of an object frees its unmanaged resources and allows them to be recycled more quickly and efficiently.

The proper usage is to create, use and dispose of an item. in the right place. For example...

void override OnPaint(PaintEventArgs e)

{

Pen p=new Pen(Color.Black,1);

e.Graphics.DrawLine(p,0,0,10,10);

p.Dispose();

}

If your application uses a lot of pens or brushes you can manage them by assigning them when the application first runs and disposing of them when the application closes during the Dispose method.

不过,应该深入了解什么时候Dispose,该不该Dispose。例如下面的情况

The process of drawing on a Windows Forms application is managed by the Graphics object which is created during the Paint event. Internally, the system responds to the WM_PAINT message in the traditional manner by calling BeginPaint to obtain a device context, wrapping this device context with a managed Graphics class and passing this class to your drawing code in the PaintEventArgs which are provided first to the OnPaint protected method and then, in turn, to the Paint event. Generally, a correctly structured Windows Forms application or control should never need to obtain the Graphics object in any other way if the intention is to draw upon that object.

Windows Forms controls and forms provide the CreateGraphics method specifically to enable you to access information about the graphics abilities of the system and perform simple tasks such as measuring text. This methods ease of use however, provides many opportunities for you to misuse the event driven structure of a Windows Forms application and consequently has undesirable side effects such as graphics that disappear or which appear incomplete.

There are two cases in which inexperienced programmers consistently misuse the CreateGraphics method. They are:

When servicing a paint event CreateGraphics is used instead of the PaintEventArgs.Graphics property.
When attempting to perform custom drawing the MyControl.CreateGraphics method is used to hijack the screen real-estate of a particular control, most often a PictureBox, upon which graphics will be plastered by brute force.
Both of these methods can have serious consequences because, in the first instance the graphics provided to the paint event may not be the same as that provided by the CreateGraphics call. Specifically, in the case of double buffered controls or forms the Graphics object returned during the paint event refers to an in-memory bitmap and not to the Graphics object owned by the window. In the second instance, CreateGraphics is used most often in response to a button click or mouse event to asynchronously draw onto another control. This causes problems because at a later date the control will update itself and remove all of the carefully constructed output effectively erasing the desired graphics.

The following table will provide you with guidelines on how to obtain the graphics object and what to do with it once you've got it.

Context Where to obtain Draw? Dispose?
The Paint Event handler From the PaintEventArguments provided Yes No
The OnPaint protected override From the PaintEventArguments provided Yes No
The OnPaintBackground protected override From the PaintEventArguments provided Yes No
Outside of the Paint, OnPaint or OnPaint background when using MeasureString CreateGraphics No Yes
Outside of Paint, OnPaint or OnPaintBackground when you need information such as DPI of the screen CreateGraphics No Yes
When drawing on a Bitmap Graphics.FromImage Yes Yes

IV.
GDIplus Coordinate system conversion

作者:Bob Powell
出处:http://www.bobpowell.net/coordinates.htm

The resolution independent nature of GDI+ can cause problems when trying to convert from mouse coordinates to the current GraphicsUnit setting or from world units to pixels. When the page units are set to GraphicsUnit.Pixel, everything is fine but as soon as you begin using a real-world measurement such as points, or inches the conversion between the mouse position in pixels and the page position in inches is not immediately clear. Add to this the fact that the view of the page may have been scrolled and you have a potential head-scratcher that can eat up hours of development time.

What you need is a simple formula that will enable you to translate from screen-pixel units to page units and back again whatever the page-unit setting and whatever the current scroll position settings.

The basic formula to convert from mouse position to page-units is:

page unit position = (mousepos-scrollpos) * scaling factor (where scaling factor is 1/resolution)

The formula for converting page position to screen position is:

worldpos-(scrollpos*scalingfactor)

These formulae are good for both the X and the Y values but you must remember to calculate the X and Y resolutions independently because they are not guaranteed to be the same.

The following listing defines an application that manages scaling, scrolling, mouse input and paint output to any one of the three Page Unit settings Pixel, Millimeter and Inch.

V.
Save a modified image

作者:Bob Powell
出处:http://www.bobpowell.net/imagefileconvert.htm

(按:虽然我用的是C++,本能的避开了这一问题,不过还是应该了解一下)

A question that pops up all the time is:
"I want to open an image file and then save that same file in a different format or back to the same filename after making modifications and I keep getting A generic error occurred in GDI"

This problem arises because GDI+ tries to be IO efficient by reading in only the headers of an image, so that you can see its width, height, colour depth etc. It leaves the file open and locked so that it can go get the actual bits when you want to look at them.

The file remains locked even after you''ve read all the bits in, for the lifetime of the Image object in fact, and therefore you cannot write back to that file with a modified version of the image.

To have access to the file for writing, you must dispose of the original image, which makes it difficult to save it afterwards.

The trick is to do the following:

Open the image file
create a temporary in-memory image the same size as the original
obtain a Graphics object for the temporary image
Draw the original image onto the temporary one
dispose of the original
do any drawing you''d like on the image using the Graphics you have
Dispose of the Graphics
Write the temporary file to any filename and any format you please including the same name and format if you wish.
The following application combines a file-format converter with a picture time-stamper that shows off this technique.

Click "Choose files" to select one or more image files.
Choose the output format in the dropdown
Decide whether you want a timestamp or not and click "Convert"
If you choose jpeg files as input, a timestamp and jpeg files as output you will open the files, stamp them and write them back to the same filename again (Don''t do this with files that you want to keep pristine!)

核心代码如下

//open the file

Image i = Image.FromFile(s);

//create temporary

Image t=new Bitmap(i.Width,i.Height);

//get graphics

Graphics g=Graphics.FromImage(t);

//copy original

g.DrawImage(i,0,0);

//close original

i.Dispose();

if(this.checkBox1.Checked==true)

{

//draw on temporary

Font f = new Font("Verdana",30);

g.DrawString(DateTime.Now.ToShortDateString()+":"+DateTime.Now.ToShortTimeString(),

f,

Brushes.Red,

10,10,

StringFormat.GenericTypographic);

f.Dispose();

}

g.Dispose();

ImageFormat fmt = ImageFormat.Bmp;

switch(this.comboBox1.Text)

{

case "JPG":

case "JPEG":

fmt=ImageFormat.Jpeg;

break;

case "GIF":

fmt=ImageFormat.Gif;

break;

case "PNG":

fmt=ImageFormat.Png;

break;

}

//save the file. Even if its the same filename

t.Save(Path.GetFileNameWithoutExtension(s)+"."+this.comboBox1.Text,fmt);

}

}

}

}

}

VI.
RGB and HSL Colour Space Conversions

作者:Bob Powell
出处:http://www.bobpowell.net/RGBHSB.htm

The classic RGB colour space used in GDI+ is excellent for choosing or defining a specific colour as a mixture of primary colour and intensity values but what happens if you want to take a particular colour and make it a bit lighter or a bit darker or change its saturation. For this you need to be able to use the HSL (Hue, Saturation and Luminance) colour space.

The .NET framework actually provides RGB-HSL conversion but for some reason, the conversion of HSL to RGB is not provided as a public method. The Color class has three methods, GetHue, GetSaturation and GetBrightness that provide the basic components of the HSL (or HSB) colour space. The conversion of HSL to RGB is a well known algorithm that you can find in numerous places on the web. Oddly enough, down in the bowels of the ControlPaint class, RGB to HSL is employed when you use the ControlPaint.Light(…) or ControlPaint.DarkDark(…) methods but also unfortunately the scaling of the Light, Dark and DarkDark settings are, to say the least, brutal and offer no finesse.

A specific use for RGB-HSL conversions I have used in the past, is to provide colours that enhance objects drawn in a psuedo-3D or isometric style. In such a drawing, a cube having a specific colour, say red, may be shown with a lighter red on top to simulate a shine on the object and a darker colour on one side to simulate a shadow. Figure1 shows two such cubes with and without shading yet still using red as the basic colour.

In addition to making a chosen colour lighter or darker, you may wish to choose a range of colour values that all have a similar brightness. Not an easy task where RGB is concerned but trivial using the HSL colour space. Similarly, you may wish to choose random colours but ensure that they have a uniform level of saturation to ensure bright, non-pastel colours for ease of visibility. Once again, a task eminently possible if using HSL colour definitions.

If you''re interested in further reading on the differences between colour spaces, this link will take you to an excellent article on the Apple site which explains them nicely.

To accompany this FAQ entry I have provided the source code shown in listing 1. It provides an RGB-HSL and HSL-RGB conversion plus a few methods that assist in setting or modifying a brightness.

核心代码如下

public class RGBHSL

{



public class HSL

{

public HSL()

{

_h=0;

_s=0;

_l=0;

}



double _h;

double _s;

double _l;



public double H

{

get{return _h;}

set

{

_h=value;

_h=_h>1 ? 1 : _h<0 ? 0 : _h;

}

}



public double S

{

get{return _s;}

set

{

_s=value;

_s=_s>1 ? 1 : _s<0 ? 0 : _s;

}

}



public double L

{

get{return _l;}

set

{

_l=value;

_l=_l>1 ? 1 : _l<0 ? 0 : _l;

}

}

}

public static Color HSL_to_RGB(HSL hsl)

{

double r=0,g=0,b=0;

double temp1,temp2;



if(hsl.L==0)

{

r=g=b=0;

}

else

{

if(hsl.S==0)

{

r=g=b=hsl.L;

}

else

{

temp2 = ((hsl.L<=0.5) ? hsl.L*(1.0+hsl.S) : hsl.L+hsl.S-(hsl.L*hsl.S));

temp1 = 2.0*hsl.L-temp2;



double[] t3=new double[]{hsl.H+1.0/3.0,hsl.H,hsl.H-1.0/3.0};

double[] clr=new double[]{0,0,0};

for(int i=0;i<3;i++)

{

if(t3[i]<0)

t3[i]+=1.0;

if(t3[i]>1)

t3[i]-=1.0;



if(6.0*t3[i] < 1.0)

clr[i]=temp1+(temp2-temp1)*t3[i]*6.0;

else if(2.0*t3[i] < 1.0)

clr[i]=temp2;

else if(3.0*t3[i] < 2.0)

clr[i]=(temp1+(temp2-temp1)*((2.0/3.0)-t3[i])*6.0);

else

clr[i]=temp1;

}

r=clr[0];

g=clr[1];

b=clr[2];

}

}



return Color.FromArgb((int)(255*r),(int)(255*g),(int)(255*b));



}

public static HSL RGB_to_HSL (Color c)

{

HSL hsl = new HSL();



hsl.H=c.GetHue()/360.0; // we store hue as 0-1 as opposed to 0-360

hsl.L=c.GetBrightness();

hsl.S=c.GetSaturation();



return hsl;

}

VII.
Multi-Page TIFF files

作者:Bob Powell
出处:http://www.bobpowell.net/generating_multipage_tiffs.htm
http://www.bobpowell.net/addframes.htm
http://www.bobpowell.net/discoverproperties.htm

Creating a multi-page TIFF file from several images is not a complex operation but it's confusing to get things in the right order. This article will clearly demonstrate that technique and provide a test application that creates multi-page TIFF files automatically when you drop images onto a form.

Rather than delve immediately into the code, take a moment to understand the process.

Images can be saved in a number of formats. To determine which one is used we can specify the encoder used to save the data. Image encoders can be complex and so the framework provides a bunch of parameters that can be used in the context of a certain encoders to alter the way images are saved.

Tiff files probably employ the most complex and diverse image formats so, not surprisingly, the encoders for TIFF files are similarly complex and diverse. A multiple page TIFF is effectively a bunch of images stored one-after-the-other in a single file. The files may be of different formats and even have different physical sizes. These individual images, or frames, may be accessed by selecting the frame to be viewed and drawing it as you would any other image. The process of creating such a file is controlled by using a single image as a master frame and adding other images to it in sequence.

The steps taken to create the multi-frame image are:

Get an encoder for saving with

Obtain the TIFF codec info.

Create a parameter list. This needs 1 parameter in it.

Place the MultiFrame encoder value in the parameter list

Save the first frame using the encoder and parameters

Change the encoder value in the list to FrameDimensionPage

Use first of the master frame's overloaded SaveAdd methods to add subsequent images. Repeat this step for as many images as you want to add.

Change the encoder value in the list to Flush

Use the second of the master frames overloaded SaveAdd methods to flush, save and close the image.

Taking these steps in order, the code to make the process work looks something like this:

1.

Encoder enc=Encoder.SaveFlag;

2.

ImageCodecInfo info=null;

foreach(ImageCodecInfo ice in ImageCodecInfo.GetImageEncoders())

if(ice.MimeType=="image/tiff")

info=ice;

3.

EncoderParameters ep=new EncoderParameters(1);

4.

ep.Param[0]=new EncoderParameter(enc,(long)EncoderValue.MultiFrame);

5.

MasterBitmap.Save(myFileName,info,ep);

6.

ep.Param[0]=new EncoderParameter(enc,(long)EncoderValue.FrameDimensionPage);

7.

MasterBitmap.SaveAdd(myExtraPage1,ep);

MasterBitmap.SaveAdd(myExtraPage2,ep);

MasterBitmap.SaveAdd(myExtraPage3,ep);

8.

ep.Param[0]=new EncoderParameter(enc,(long)EncoderValue.Flush);

9.

MasterBitmap.SaveAdd(ep);

To demonstrate this process the following Windows Forms application enables you to drag any number of image files onto the form. You will be asked for a filename and a multi-page TIFF with all the files dragged in will be saved.

核心代码如下

//get the codec for tiff files

ImageCodecInfo info=null;

foreach(ImageCodecInfo ice in ImageCodecInfo.GetImageEncoders())

if(ice.MimeType=="image/tiff")

info=ice;



//use the save encoder

Encoder enc=Encoder.SaveFlag;



EncoderParameters ep=new EncoderParameters(1);

ep.Param[0]=new EncoderParameter(enc,(long)EncoderValue.MultiFrame);



Bitmap pages=null;



int frame=0;



foreach(string s in sa)

{

if(frame==0)

{

pages=(Bitmap)Image.FromFile(s);

//save the first frame

pages.Save(dlg.FileName,info,ep);

}

else

{

//save the intermediate frames

ep.Param[0]=new EncoderParameter(enc,(long)EncoderValue.FrameDimensionPage);

Bitmap bm=(Bitmap)Image.FromFile(s);

pages.SaveAdd(bm,ep);

}



if(frame==sa.Length-1)

{

//flush and close.

ep.Param[0]=new EncoderParameter(enc,(long)EncoderValue.Flush);

pages.SaveAdd(ep);

}

frame++;

完整应用参见
http://www.bobpowell.net/addframes.htm
http://www.bobpowell.net/discoverproperties.htm

VIII.
Setting compression level when saving JPEG

作者:Bob Powell
出处:http://www.bobpowell.net/jpeg_compression.htm

(按:好文章,解决了我的一个老问题,程序又要升级啦)

Images are serialized by an encoder specially adapted for the image format. Certain encoders, such as the JPEG encoder, can be instructed to alter the method serialization by the use of encoder parameters which specify the characteristics of the data written to the file or stream. The EncoderParameter class provides encapsulation for these different settings and may be applied to the specific image encoder before an image is saved.

In the case of Jpeg images, you can write files with differing levels of compression by using the specialized Quality encoder and a suitable compression setting as shown in the code in the following listing.

//Load a bitmap from file

Bitmap bm=(Bitmap)Image.FromFile("mypic.jpg");

//Get the list of available encoders

ImageCodecInfo[] codecs=ImageCodecInfo.GetImageEncoders();

//find the encoder with the image/jpeg mime-type

ImageCodecInfo ici=null;

foreach(ImageCodecInfo codec in codecs)

{

if(codec.MimeType=="image/jpeg")

ici=codec;

}



//Create a collection of encoder parameters (we only need one in the collection)

EncoderParameters ep=new EncoderParameters();

//We'll save images with 25%, 50%, 75% and 100% quality as compared with the original

for(int x=25;x<101;x+=25)

{

//Create an encoder parameter for quality with an appropriate level setting

ep.Param[0]=new EncoderParameter(Encoder.Quality,(long)x);

//Save the image with a filename that indicates the compression quality used

bm.Save("C:\\quality"+x.ToString()+".jpg",ici,ep);

}

(按:我的blog现在已经没什么人看了,呵呵,自语自乐)

IX.
Creating and Display Transparent GIF Images

1.
作者:Bob Powell
出处:http://www.bobpowell.net/giftransparency.htm

This process is easy enough when you know how but is nevertheless a little more complex than it should be.

To save a GIF with a transparency key you need to modify the colour palette of the image. There are a few problems associated with this. If you're creating an image, you'll be using a true colour format such as 24 or 32 bit per pixel non indexed. This is because you cannot obtain a Graphics object using Graphics.FromImage for any images with an indexed pixel format. Saving such an image as a GIF file will create a standard spread palette for you with the range of standard colours.

Unfortunately, at no point during the save process are you given the opportunity to choose a transparent colour so you need to take the saved image and re-save it with a modified palette.

This in itself presents a problem because once a GIF image has been created, even though you can get hold of and manipulate the palette using the Bitmap.Palette property, GDI+ refuses to save the image with anything other than its original palette.

To work around these limitations it's necessary to create a new, blank 8 bit per pixel, indexed palette image, modify it's bitmap to be the same as the original images, copy all the pixel data from the original to the new and then save the new image.

As a demonstration of this process, and to provide a useful tool, the code in listing 1 is an application that enables you to load a GIF image, choose a transparent colour and save the GIF. panel1_Click is the method with the actual GIF manipulation.

核心代码如下

//Creates a new GIF image with a modified colour palette

if(cp!=null)

{

//Create a new 8 bit per pixel image

Bitmap bm=new Bitmap(_gifImage.Width,_gifImage.Height,PixelFormat.Format8bppIndexed);

//get it's palette

ColorPalette ncp=bm.Palette;



//copy all the entries from the old palette removing any transparency

int n=0;

foreach(Color c in cp.Entries)

ncp.Entries[n++]=Color.FromArgb(255,c);



//Set the newly selected transparency

ncp.Entries[CurrentEntry]=Color.FromArgb(0,cp.Entries[CurrentEntry]);

//re-insert the palette

bm.Palette=ncp;



//now to copy the actual bitmap data

//lock the source and destination bits

BitmapData src=((Bitmap)_gifImage).LockBits(new Rectangle(0,0,_gifImage.Width,_gifImage.Height),ImageLockMode.ReadOnly,_gifImage.PixelFormat);

BitmapData dst=bm.LockBits(new Rectangle(0,0,bm.Width,bm.Height),ImageLockMode.WriteOnly,bm.PixelFormat);



//uses pointers so we need unsafe code.

//the project is also compiled with /unsafe

unsafe

{

//steps through each pixel

for(int y=0;y<_gifImage.Height;y++)

for(int x=0;x<_gifImage.Width;x++)

{

//transferring the bytes

((byte *)dst.Scan0.ToPointer())[(dst.Stride*y)+x]=((byte *)src.Scan0.ToPointer())[(src.Stride*y)+x];

}

}



//all done, unlock the bitmaps

((Bitmap)_gifImage).UnlockBits(src);

bm.UnlockBits(dst);



//clear out the picturebox

this.pictureBox1.Image=null;

_gifImage.Dispose();

//set the new image in place

_gifImage=bm;

cp=_gifImage.Palette;

this.pictureBox1.Image=_gifImage;

}

如果用PictureBox,Transparent GIF Images的显示是很容易的,那么VC里面呢?看下面的工作。

2.
作者:wb
出处:http://www.codeproject.com/vcpp/gdiplus/imageexgdi.asp

Hi!

I spend a whole day, trying to make a transparent gif moving, perhaps this saves someones time

if you add a gif with transparency, it dont repait the backgrdound so I loaded a gif with white (255,255,255)
background and used this:


bool ImageEx::DrawFrameGIF()
{
....
imAtr.SetColorKey(Color(255,255,255),Color(255,255,255),ColorAdjustTypeBitmap);
graphics.DrawImage(this,Rect(m_pt.x, m_pt.y, hmWidth, hmHeight), 0, 0, hmWidth, hmHeight,UnitPixel,&imAtr);
.....
}

but.. this version DrawImage() always uses the first frame in the gif.
finaly, I used the systems background color, to replace the
"transparent" color of the gif...

bool ImageEx::DrawFrameGIF()
{
....
ImageAttributes imAtr;
BYTE r=GetRValue(GetSysColor(COLOR_BTNFACE)); BYTE g=GetGValue(GetSysColor(COLOR_BTNFACE));
BYTE b=GetBValue(GetSysColor(COLOR_BTNFACE));
ColorMap cMap;
cMap.oldColor = Color(255, 254, 254,254);
cMap.newColor = Color(255, r, g, b);

Bitmap memBMP(hmWidth,hmHeight);
Graphics *pMemG = Graphics::FromImage(&memBMP);
//this DrawImage() uses the SELECTED frame of the gif
pMemG->DrawImage(this, 0, 0, hmWidth, hmHeight);
//this DrawImage() uses alway the first frame
graphics.DrawImage(&memBMP,Rect(m_pt.x, m_pt.y, hmWidth, hmHeight), 0, 0, hmWidth, hmHeight,UnitPixel,&imAtr);

with this "double buffer" aproach, you may also use
the ImageAttributes.SetColorKey() method which adds
transparency to a bitmap, but.... then some parts of
the image still dont repaint

3.
There is also an article on the Microsoft site that explains how to optimize colour palettes using octtree colour quantization.

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnaspp/html/colorquant.asp

(按:嘻嘻,才发现发贴过500哪,自己庆祝一下)

X.
Convert 1 bit-per-pixel monochrome

作者:Bob Powell
出处:http://www.bobpowell.net/onebit.htm

(按:这个地方真是好,解决了不少我悬而未决的问题。原文是多色转单色,其实我以前是被单色转多色后处理给卡住了)

One bit per pixel images are essentially an indexed format image having two entries in the colour palette. Unlike a GIF or other 256 colour indexed format which uses a single byte to access one of up-to 256 colours in the palette, the one bit per pixel format uses only a single bit to select colour A or colour B from the palette.
Often used in Fax formats, one bit per pixel images are very compact with one byte of information representing eight pixels.

Very often, a more complex RGB image can be used for the basic raw-image that is used to define the 1 bpp array but this operation in GDI+ and .NET systems is a little more complex than one would hope because you cannot simply get the Graphics object for an indexed image and draw on it. In order to do this conversion, you must manipulate the individual bits of the image by locking it's byte array and selecting the correct bit for the desired pixel.

The LockBits method can be used to obtain a BitmapData object which contains the address of the bitmap in memory, the dimensions and particularly, the stride of the image. All images are stored in an array that fills memory to a four-byte boundary so the stride is used to calculate where each successive line begins in the array. A specific line may be addressed by the formula LineAddress=stride*linenumber and NOT LineAddress = pixeldepth*imagewidth*linenumber as many people assume.

For our single bit per pixel image, the pixel indexing is further complicated by the need to select a specific bit in a byte that contains up to 8 pixels (it may have less than 8 pixels because it may be on the end of a scan-line). To perform this selection, a bit mask is used and rotated into position so that the correct bit may be set or reset as desired.

The simple application shown in listing 1 enables you to load any image into a PictureBox control situated on the left of the form. Once loaded, the image is scanned pixel by pixel to check the brightness of the colours contained in the image. If a pixel is darker than 50% brightness, a black pixel is set in the corresponding pixel of a black and white, single bit per pixel image stored in the rightmost PictureBox on the form. Between the two PictureBox controls, a Splitter enables you to see more or less of the original image for comparison with the converted one.

Note in particular how the single bit per pixel image is created in the pictureBox1_Click handler and how the SetIndexedPixel method is used to access the individual pixel bits in the monochrome image.

To compile this source you will need to allow the use of unsafe code blocks in the compiler or on the command line.

核心代码如下

protected unsafe void SetIndexedPixel(int x,int y,BitmapData bmd, bool pixel)

{

byte* p=(byte*)bmd.Scan0.ToPointer();

int index=y*bmd.Stride+(x>>3);

byte mask=(byte)(0x80>>(x&0x7));

if(pixel)

p[index]|=mask;

else

p[index]&=(byte)(mask^0xff);

}

Bitmap img = (Bitmap)Image.FromFile(dlg.FileName);

this.pictureBox1.Image=img;



bm=new Bitmap(this.pictureBox1.Image.Width,this.pictureBox1.Image.Height,PixelFormat.Format1bppIndexed);

BitmapData bmd=bm.LockBits(new Rectangle(0, 0, bm.Width, bm.Height), ImageLockMode.ReadWrite, PixelFormat.Format1bppIndexed );



for(int y=0;y< img.Height;y++)

{

for(int x=0;x
{

if(img.GetPixel(x,y).GetBrightness()>0.5f)

this.SetIndexedPixel(x,y,bmd,true);

}

}



bm.UnlockBits(bmd);



<< Home

This page is powered by Blogger. Isn't yours?