Click here to Skip to main content
15,867,308 members
Articles / Web Development / ASP.NET
Article

ImageMagick in VB.NET

Rate me:
Please Sign up or sign in to vote.
4.78/5 (27 votes)
22 May 200722 min read 416.8K   32.9K   137   62
ImageMagick is a powerful image manipulation that supports a wide variety of formats. In this article we develop an ImageMagick Wraper in C++ to expose functionalities in Magick++ to a .NET application.

Introduction

Image 1

ImageMagick is a powerful image manipulation tool that recognizes a wide range of image formats, including your usual JPG, GIF, PNG formats commonly used on the web. Among some of its manipulation capabilities are the ability to apply graphical effects such as charcoal, gaussian blur, solarization, (and some effects that you'd see in professional image manipulation software such as Adobe Photoshop).

ImageMagick exposes its function via a set of command-line executables, and effects can be applied by specifying switches and options. But it also exposes its functionalities through its MagickCore and MagickWand C libraries, as well as its Magick++ C++ library, so that developers can take advantage of its functionalities within their software. Interfaces for other languages such as Perl, Python, Java, etc. have also been developed by the ImageMagick community.

In this exercise we have taken to creating a .NET ImageMagick wrapper as a study of ImageMagick functionalities. We have also attempted to create a VB.NET application that calls the .NET wrapper to access the ImageMagick functionalities. Although there already exists an ImageMagick .NET wrapper, we've created one that models the Magick++ classes available closer than the existing one. As the Magick++ exposes the ImageMagick in C++ classes as well as Standard Template Libraries, marshalling types between .NET and C++ becomes a little less daunting.

Before we get started, we would like to highlight the formats that ImageMagick now supports. (Please note that some formats require additional plugins to be installed.)

ExtModeDescriptionNotes
ARTRPFS: 1st PublisherFormat originally used on the Macintosh (MacPaint?) and later used for PFS: 1st Publisher clip art.
AVIRMicrosoft Audio/Visual Interleaved
AVSRWAVS X image
BMPRWMicrosoft Windows bitmap
CGMRComputer Graphics MetafileRequires ralcgm to render CGM files.
CINRWKodak Cineon Image FormatUse -set to specify the image gamma or black and white points (e.g. -set gamma 1.7, -set reference-black 95, -set reference-white 685).
CMYKRWRaw cyan, magenta, yellow, and black samplesUse -size and -depth to specify the image width, height, and depth. To specify a single precision floating-point format, use -depth 32 -define quantum:format=floating-point. Set the depth to 64 for a double precision floating-point format.
CMYKARWRaw cyan, magenta, yellow, black, and alpha samplesUse -size and -depth to specify the image width, height, and depth. To specify a single precision floating-point format, use -depth 32 -define quantum:format=floating-point. Set the depth to 64 for a double precision floating-point format.
CURRMicrosoft Cursor Icon
CUTRDR Halo
DCMRDigital Imaging and Communications in Medicine (DICOM) imageUsed by the medical community for images like X-rays.
DCXRWZSoft IBM PC multi-page Paintbrush image
DIBRWMicrosoft Windows Device Independent BitmapDIB is a BMP file without the BMP header. Used to support embedded images in compound formats like WMF.
DJVUR
DNGRDigital Negative
DOTRGraph VisualizationUse -define to specify the layout engine (e.g. -define dot:layout-engine=neato).
DPXRWSMPTE Digital Moving Picture ExchangeUse -set to specify the image gamma or black and white points (e.g. -set gamma 1.7, -set reference-black 95, -set reference-white 685).
EMFRMicrosoft Enhanced Metafile (32-bit)Only available under Microsoft Windows.
EPDFRWEncapsulated Portable Document Format
EPIRWAdobe Encapsulated PostScript Interchange formatRequires Ghostscript to read.
EPSRWAdobe Encapsulated PostScriptRequires Ghostscript to read.
EPS2WAdobe Level II Encapsulated PostScriptRequires Ghostscript to read.
EPS3WAdobe Level III Encapsulated PostScriptRequires Ghostscript to read.
EPSFRWAdobe Encapsulated PostScriptRequires Ghostscript to read.
EPSIRWAdobe Encapsulated PostScript Interchange formatRequires Ghostscript to read.
EPTRW
OTBRWOn-the-air Bitmap
P7RWXv's Visual Schnauzer thumbnail format
PALMRWPalm pixmap
PAMWCommon 2-dimensional bitmap format
PBMRWPortable bitmap format (black and white)
PCDRWPhoto CDThe maximum resolution written is 768x512 pixels since larger images require huffman compression (which is not supported).
PCDSRWPhoto CDDecode with the sRGB color tables.
PCLWHP Page Control LanguageFor output to HP laser printers.
PCXRWZSoft IBM PC Paintbrush file
PDBRWPalm Database ImageViewer Format
PDFRWPortable Document FormatRequires Ghostscript to read. By default, ImageMagick sets the page size to the MediaBox. Some PDF files, however, have a CropBox that is smaller than the MediaBox and may include white space, registration or cutting marks outside the CropBox. To force ImageMagick to use the CropBox rather than the MediaBox, use -define (e.g. -define pdf:use-cropbox=true).
PFARPostscript Type 1 font (ASCII)Opening as file returns a preview image.
PFBRPostscript Type 1 font (binary)Opening as file returns a preview image.
PGMRWPortable graymap format (gray scale)
PICONRWPersonal Icon
PICTRWApple Macintosh QuickDraw/PICT file
PIXRAlias/Wavefront RLE image format
PNGRWPortable Network GraphicsRequires libpng-1.0.2 or later, libpng-1.2.5 or later recommended.
PNMRWPortable anymapPNM is a family of formats supporting portable bitmaps (PBM) , graymaps (PGM), and pixmaps (PPM). There is no file format associated with pnm itself. If PNM is used as the output format specifier, then ImageMagick automatically selects the most appropriate format to represent the image. The default is to write the binary version of the formats. Use -compress none to write the ASCII version of the formats.
PPMRWPortable pixmap format (color)
PSRWAdobe PostScript fileRequires Ghostscript to read.
PS2RWAdobe Level II PostScript fileRequires Ghostscript to read.
PS3RWAdobe Level III PostScript fileRequires Ghostscript to read.
PSDRWAdobe Photoshop bitmap file
PTIFRWPyramid encoded TIFFMulti-resolution TIFF containing successively smaller versions of the image down to the size of an icon. The desired sub-image size may be specified when reading via the -size option.
PWPRSeattle File Works multi-image file
RADRRadiance image fileRequires that ra_ppm from the Radiance software package be installed.
RGBRWRaw red, green, and blue samplesUse -size and -depth to specify the image width, height, and depth. To specify a single precision floating-point format, use -depth 32 -define quantum:format=floating-point. Set the depth to 64 for a double precision floating-point format.
RGBARWRaw red, green, blue, and alpha samplesUse -size and -depth to specify the image width, height, and depth. To specify a single precision floating-point format, use -depth 32 -define quantum:format=floating-point. Set the depth to 64 for a double precision floating-point format.
RLARAlias/Wavefront image file
RLERUtah Run length encoded image file
SCTRScitex Continuous Tone Picture
SFWRSeattle File Works image
SGIRWIrix RGB image
SHTMLWHypertext Markup Language client-side image mapUsed to write HTML clickable image maps based on a the output of montage or a format which supports tiled images such as MIFF.
SUNRWSUN Rasterfile
SVGRWScalable Vector GraphicsRequires libxml2 and freetype-2. Note that SVG is a very complex specification so support is still not complete.
TGARWTruevision Targa imageAlso known as formats ICB, VDA, and VST.
TIFFRWTagged Image File FormatAlso known as TIF. Requires tiff-v3.6.1.tar.gz or later. Use -define to specify the rows per strip (e.g. -define tiff:rows-per-strip=8). To specify a signed format, use -define quantum:format signed. To specify a single-precision floating-point format, use -depth 32 -define quantum:format=floating-point. Set the depth to 64 for a double-precision floating-point format. Use -define tiff:photometric=min-is-black or -define tiff:photometric=min-is-white to toggle the photometric interpretation for a bilevel image. Specify the extra samples as associated or unassociated alpha with, for example, -define tiff:alpha=unassociated.
TIMRPSX TIM file
TTFRTrueType font fileRequires freetype 2. Opening as file returns a preview image. Use -set if you do not want to hint glyph outlines after their scaling to device pixels (e.g. -set type:hinting off).
TXTRWRaw text file
UILWX-Motif UIL table
UYVYRWInterleaved YUV raw imageUse -size and -depth command line options to specify width and height. Use -sampling-factor to set the desired subsampling (e.g. -sampling-factor 4:2:2).
VICARRWVICAR rasterfile format
VIFFRWKhoros Visualization Image File Format
WBMPRWWireless bitmapSupport for uncompressed monochrome only.
WMFRWindows MetafileRequires libwmf. By default, renders WMF files using the dimensions specified by the metafile header. Use the -density option to adjust the output resolution, and thereby adjust the output size. The default output resolution is 72DPI so -density 144 results in an image twice as large as the default. Use -background color to specify the WMF background color (default white) or -texture filename to specify a background texture image.
WPGRWord Perfect Graphics File
XRWdisplay or import an image to or from an X11 serverUse -define to obtain the image from the root window (e.g. -define x:screen=true).
XBMRWX Windows system bitmap, black and white onlyUsed by the X Windows System to store monochrome icons.
XCFRGIMP image
XPMRWX Windows system pixmapAlso known as PM. Used by the X Windows System to store color icons.
XWDRWX Windows system window dumpUsed by the X Windows System to save/display screen dumps.
YCbCrRWRaw Y, Cb, and Cr samplesUse -size and -depth to specify the image width, height, and depth.
YCbCrARWRaw Y, Cb, Cr, and alpha samplesUse -size and -depth to specify the image width, height, and depth.
YUVRWCCIR 601 4:1:1Use -size and -depth command line options to specify width, height, and depth. Use -sampling-factor to set the desired subsampling (e.g. -sampling-factor 4:2:2).

Compiling the Source Codes

We have tried to make it as easy as possible to compile the sources. But we need the following environment on your machine to be set up:

  1. You must have a C: available.
  2. You must have Visual Studio .NET 2005 available.
    If you do not, you could still use Visual Studio 2005 Express Editions (C++ and Visual Basic), but you may have to break up the .SLN file.

For some reason, ImageMagick 6.3.2 is unable to load the IM_MOD*.DLLs from the same directory as the executing application. Instead it always refers to the registry for the path to the IM_MOD*.DLL files. This may cause problems for those who wish to use ImageMagick on their hosting servers, but have no access to the hosting server's registry. For that matter, I've taken the ImageMagick 6.3.2 source code and modified it very slightly to allow it to read the IM_MOD*.DLLs from the executing application directory. As a result, in this updated release, you no longer need to set up anything in the registry.

To install, download the ImageMagick-Source.zip, ImageMagick-DLLs-Q8.zip, ImageMagick-DLLs-Q16.zipand unzip them to the same folder location. (We apologize for the separate files since CodeProject was unable to accept all of them as one big zip file) If you have unzipped them correctly, your directory structure should end up like this:

Folder
 +-DLLs
 |  +-DebugQ8
 |  +-DebugQ16
 +-ImageMagickNET
 |  +-include8
 |  +-include16
 |  +-lib8
 |  +-lib16
 +-VBForms
    +-My Project
You will notice one batch file there: install.bat. Double click on install.bat to execute the batch file. The batch file does the following:

  1. Create a duplicate copy of DLLs\DebugQ8 to DLLs\ReleaseQ8, and DLLs\DebugQ16 to DLLs\ReleaseQ16.

Once you are done, open up the ImageMagick.NET.sln file in Visual Studio .NET:

  1. There are four configurations possible for compilation: DebugQ8, DebugQ16, ReleaseQ8, ReleaseQ16. Choose either of Q8 or Q16 for compilation. It does not matter if you have previously set up ImageMagick on your machine.
  2. Rebuild the entire solution.
  3. Run the VB Demo.

Compiling with Subsequent Releases of ImageMagick

There are a few things to take note should you need to compile the .NET Wrapper with subsequent releases of ImageMagick. The first way:

  1. Download the latest release of ImageMagick from: http://www.imagemagick.org/script/binary-releases.php. Remember to download on the DLLs version and not the static versions
  2. Obtain the latest *.h files and copy them to the ImageMagickNET\include8 or ImageMagickNET\include16 folders (depending on the pixel quantization)
  3. Obtain the latest *.lib files and copy them to the ImageMagickNET\lib8 or ImageMagickNET\lib16
  4. Obtain the latest CORE_RL*.DLLs, mfc71, msvcp71, msvcr71.dll and all other non-IM_MOD_RL*.DLLs that exist in the root folder of the ImageMagick installation. Copy these into the DLLs\DebugQX and DLLs\ReleaseQX folder. The VB project has a post-build event that copies the appropriate DLLs based on the configuration you chose into your application output folder. (See notes on the CORE_RL*.DLLs below)
  5. Obtain the IM_MOD_RL*.DLL files and the *.xml files from ImageMagick and copy them to the C:\ImageMagick

The following notes are important for the setting up of the development environment, and maybe useful for those who wish to deploy their application onto production sites.

Notes on the CORE_RL*.DLLs

When running your application, make sure that the correct pixel quantization version Q8/Q16 of the CORE_RL*.DLLs, msvcp71.dll, msvcr71.dll and all other non-IM_MOD_RL*.DLLs reside on the same folder as your application. This is to ensure that your application loads the DLLs directly from its current path, instead of loading it from the ImageMagick folder.

Why do we do this? You may ask. This is because if you have both the Q8 and the Q16 installations of ImageMagick set up on your machine, Windows does not automatically know which CORE_RL*.DLLs to look for. It only looks for the DLLs, we believe, first in the current application directory, and then in the order of the folders specified in the PATH environment variable. Thus, if you have both Q8 and Q16 set up on your machine, then Windows will always look for either the Q8 DLLs everytime or Q16 DLLs everytime. This will cause your application to crash when the wrong pixel quantization libraries are used. It is thus safer to copy the CORE_RL*.DLLs to your application folder.

Notes on the IM_MOD_RLL*.DLLs

When you set up the latest ImageMagick from the installers, the installers will automatically set up on your machine some registry entries. These DLLs are responsible for decoding and encoding the various supported image file formats. Now, when the CORE_RL*.DLLs needs to load up one of these it does NOT (at least in version 6.3.2) search for them in the current application folder. Instead it searches for them in the folders specified by the CoderModulesPath key in the registry entries. (Which is why we had to recompile the ImageMagick sources to enable it to look under the currrent executing folder) The ImageMagick registry entries, one set for Q8 and Q16 each, are defined as follows:

[HKEY_LOCAL_MACHINE\SOFTWARE\ImageMagick\6.3.2\Q:8]
"BinPath"="C:\\Program Files\\ImageMagick-6.3.2-Q8"
"ConfigurePath"="C:\\Program Files\\ImageMagick-6.3.2-Q8\\config"
"LibPath"="C:\\Program Files\\ImageMagick-6.3.2-Q8"
"CoderModulesPath"="C:\\Program Files\\ImageMagick-6.3.2-Q8\\modules\\coders"
"FilterModulesPath"="C:\\Program Files\\ImageMagick-6.3.2-Q8\\modules\\filters"

[HKEY_LOCAL_MACHINE\SOFTWARE\ImageMagick\6.3.2\Q:16]
"BinPath"="C:\\Program Files\\ImageMagick-6.3.2-Q16"
"ConfigurePath"="C:\\Program Files\\ImageMagick-6.3.2-Q16\\config"
"LibPath"="C:\\Program Files\\ImageMagick-6.3.2-Q16"
"CoderModulesPath"="C:\\Program Files\\ImageMagick-6.3.2-Q16\\modules\\coders"
"FilterModulesPath"="C:\\Program Files\\ImageMagick-6.3.2-Q16\\modules\\filters"

Image 2

Managed C++ Extensions

We will be exposing the Magick++ interface to .NET. But since Magick++ is in C++, we will be creating the .NET interface with C++, with the Managed Extensions. Please note that the C++ Managed Extensions that we are using will only work on .NET Framework 2.0, and thus the files will compile only on Visual Studio 2005.

A wide variety of articles on the web already cover Managed C++ Extensions in-depth, so we will not go too much into the details. In this article, we will only briefly cover the tip of the iceberg, sufficient for us to create a wrapper in C++.

Creating the Wrapper Class

We are interested in creating the wrapper class for the Image class of the Magick++ library (Magick::Image). Here's how we declare the header in Image.h:

C++
#include "Magick++.h"

using namespace System;
using namespace System::Runtime::InteropServices;

namespace ImageMagickNET 
{ 
    public ref class Image
    {
        // Create a pointer reference to the Magick::Image
        // class, the very object that we are going to wrap
        //
        Magick::Image*  image;

    protected:
        // Here, we create the destructors 
        //
        ~Image() { this->!Image(); }
        !Image() 
        { 
            if( image!=NULL && !isReferenceOnly ) 
            { 
                delete image; 
                image = NULL; 
            } 
        }
    
    public:
        // Here, we create the constructors.
        // We have created two whose method signature matches
        // that of the Magick::Image class' constructors.
        // Although there are many more constructors available,
        // we created only these two enough for us to perform
        // simple manipulation.
        //
        Image();
        Image(System::String^ imageSpec);

    };
}

Notice in the second constructor, the .NET Image class accepts a System::String, which is the equivalent of the .NET String class. However, the equivalent Magick::Image constructor has the following signature:

C++
Image(std::string imageSpec);

The std::string class is the C++ STL string format. Now, this immediately poses a problem to us because the C++ STL string class and the .NET String class is clearly incompatible. This is where the Marshaller has to enter.

Marshalling Strings

A bulk of the marshalling work to convert a .NET type to an equivalent C++ type involves strings, at least, in our case here. So we wrote a separate class specifically to handle the marshalling in a fairly abstract manner. This is the Marshaller class that we designed to handle the string marshalling. The Marshaller.h is declared as follows:

C++
#pragma once

using namespace System;
using namespace System::Runtime::InteropServices;

namespace ImageMagickNET 
{
    public ref class Marshaller 
    {
    private:
        
    protected:
        
    public:
        static System::String^ StdStringToSystemString(const std::string& s);
        static std::string& SystemStringToStdString(System::String^ s, 
            std::string& s2);
        static std::string& SystemStringToStdString(System::String^ s);
    };
}

It consists of only 3 static methods.

The first of which is the StdStringToSystemString, whose responsibility is to convert the C++ STL string to a .NET string.

The other 2 converts a .NET string to a C++ STL string. Most of the time we will be using the last method to pass in strings from .NET to the ImageMagick library, as it is the easiest and all work involved in dealing with the StringToHGlobalAnsi gets abstracted away. There's a small caveat, however, for using the last method, as this method requires a static pool of strings in memory. We've opted to create a pool of 8 strings that are used for marshaling on a round-robin basis.

The Marshaller.cpp is implemented as follows:

C++
namespace ImageMagickNET
{
    int stringPoolCounter = 0;
    std::string strings[8]; 

    ///-------------------------------------------------------------------
    /// Converts a STL string to a CLR string.
    ///
    /// When the conversion is complete, returns the CLR string to the
    /// caller.
    ///-------------------------------------------------------------------
    System::String^ Marshaller::StdStringToSystemString(const std::string& s)
    {
        return gcnew System::String(s.c_str());
    }

    ///-------------------------------------------------------------------
    /// Converts a CLR string to a STL string.
    ///
    /// When the conversion completes, returns the STL string through the
    /// referenced parameter s2.
    ///-------------------------------------------------------------------
    std::string& Marshaller::SystemStringToStdString(System::String^ s, 
        std::string &s2)
    {
        const char* chars = (const char*)(Marshal::StringToHGlobalAnsi(s))
            .ToPointer();
        s2 = chars;
        Marshal::FreeHGlobal(IntPtr((void*)chars));
        return s2;
    }


    ///-------------------------------------------------------------------
    /// Converts a CLR string to a STL string, using a pool of internally
    /// declared strings (up to 8 of them)
    ///-------------------------------------------------------------------
    std::string& Marshaller::SystemStringToStdString(System::String^ s)
    {
        int c = stringPoolCounter;
        stringPoolCounter = (stringPoolCounter+1) % 8;
        return Marshaller::SystemStringToStdString(s, strings[c]);

        
    }
}

Implementing the Image Constructors

Now that we've implemented the Marshaller, we can proceed to implement the constructors of the Image class.

C++
///-------------------------------------------------------------------
/// Constructor
///-------------------------------------------------------------------
Image::Image()
{
    // Here we construct a new Magick::Image class
    // and assign the reference to it from our class
    //
    image = new Magick::Image();
}


///-------------------------------------------------------------------
/// Constructor
/// Loads the file from the specified file path.
///-------------------------------------------------------------------
Image::Image(System::String^ imageSpec)
{
    // Here we construct a new Magick::Image class
    // and we use the Marshaller's SystemStringToStdString
    // method to assist us convert the .NET string to the
    // C++ STL string.
    //
    image = new Magick::Image(
        Marshaller::SystemStringToStdString(imageSpec));
}

Implementing the Loading and Saving

Now that the basic constructors and destructors are ready, we can go on to implement the many other functionalities of the Magick::Image class.

We shall first wrap the load and save methods. The Image.h is now implemented as follows:

C++
#include "Magick++.h"

using namespace System;
using namespace System::Runtime::InteropServices;

namespace ImageMagickNET 
{ 
    public ref class Image
    {
        // Create a pointer reference to the Magick::Image
        // class, the very object that we are going to wrap
        //
        Magick::Image*  image;

    protected:
        // Here, we create the destructors 
        //
        ~Image() { this->!Image(); }
        !Image() 
        { 
            if( image!=NULL && !isReferenceOnly ) 
            { 
                delete image; 
                image = NULL; 
            } 
        }
    
    public:
        // Here, we create the constructors.
        // We have created two whose method signature matches
        // that of the Magick::Image class' constructors.
        // Although there are many more constructors available,
        // we created only these two enough for us to perform
        // simple manipulation.
        //
        Image();
        Image(System::String^ imageSpec);


        //----------------------------------------------------------------
        // IO
        //----------------------------------------------------------------

        // read
        void            Read(String^ imageSpec_);

        // write
        void             Write(String^ imageSpec_);


    }
}

Notice again that we made use of the Marshaller:SystemStringToStdString method for marshalling. The Magick::Image class has a few methods for loading and saving files, but out of those let's concentrate on the basic two methods:

C++
Magick::Image::read(std::string imageSpec_)
Magick::Image::write(std::string imageSpec_)

We have tried to avoid making major changes to the method signatures, and the way they are being called except to capitalize the first letter of the method name. This is to retain, as much as possible, the original interfaces of the Magick::Image classes, so that we don't have to re-document all our interfaces again.

And the Read and Write functions are implemented in Image.cpp as follows.

C++
///-------------------------------------------------------------------
/// Load an image from the specified file path.
///-------------------------------------------------------------------
void Image::Read(System::String^ imageSpec)
{
    // call the read method by simply calling
    // image->read and passing in the C++ STL string
    //
    image->read(Marshaller::SystemStringToStdString(imageSpec));
}


///-------------------------------------------------------------------
/// Save an image to the specified file path.
///-------------------------------------------------------------------
void Image::Write(System::String^ imageSpec)
{
    // call the read method by simply calling
    // image->write and passing in the C++ STL string
    //
    image->write(Marshaller::SystemStringToStdString(imageSpec));
}

And viola! We now have an Image class that we can export to .NET to do simple file loading and saving.

Of course we aren't yet as satisfied, because there are a lot more functionality encapsulated in the Magick::Image that we need to expose to our .NET program. But before we expose them all, some methods in the Magick::Image class require references to objects of classes found in the Magick:: namespace. Examples of classes are: Magick::Geometry, Magick::Color. Some methods also require an enumerated type to be passed in as a parameter. This means that the classes and the enumerated types must be defined in our .NET wrapper.

Setting Up Enumerator Classes

We will be setting up plenty of enumerator classes as we proceed to implement the entire interface of our .NET Image class, but we shall highlight just one, since implementing the rest can be done in the exact same way.

The MagickLib:: namespace in the ImageMagick library has this enumerated type called PaintMethod. It allows the user to specify the method of flood filling and painting related operations on an image. It is declared in C++ this way:

C++
typedef enum
{
    UndefinedMethod,
    PointMethod,
    ReplaceMethod,
    FloodfillMethod,
    FillToBorderMethod,
    ResetMethod
} PaintMethod;

When we implement this in .NET, we used the .NET enumeration class, as follows.

C++
public enum class PaintMethod
{
    UndefinedMethod = MagickLib::UndefinedMethod,
    PointMethod = MagickLib::PointMethod,
    ReplaceMethod = MagickLib::ReplaceMethod,
    FloodfillMethod = MagickLib::FloodfillMethod,
    FillToBorderMethod = MagickLib::FillToBorderMethod,
    ResetMethod = MagickLib::ResetMethod
};

As a general rule, it is advisable to map each item in the enumeration to their corresponding constant declared in the C++ library. This is so as to avoid breaking changes that may occur, when the order of the enumeration is different in a newer release.

Setting Up Other Classes

As highlighted in the earlier section, the Magick::Image class requires as parameters references to classes like Magick::Color, Magick::Geometry. Here, we will be implemented the Color class as an example, and the rest of the classes can be implemented the same way.

The implementation of the Magick::Geometry class, is essentially, the same way we had wrapped the Magick::Image class. When implementing the wrapper, we try to match as many functions as the Magick::Geometry class exposes. But to shorten the code we put up for illustration here, we only picked a few methods. And this is how we did it:

C++
public ref class Geometry
{
internal:
    // Create a pointer reference to the Magick::Geometry
    // class, the very object that we are going to wrap
    //
    // Notice that this is declared in the internal section
    // which is very important, because this is where other
    // classes within our .NET wrapper will access the
    // object.
    //
    Magick::Geometry*  geometry;

protected:
    ~Geometry() { this->!Geometry(); }
    !Geometry() { if( geometry!=NULL ) { delete geometry; geometry = NULL; } }

public:
    // constructors
    Geometry ( );
    Geometry (
        unsigned int width_,
        unsigned int height_,
        unsigned int xOff_,
        unsigned int yOff_,
        bool xNegative_,
        bool yNegative_ );
    Geometry ( System::String ^geometry_ );
    Geometry ( Geometry^ geometry_ );

    // Width
    void          Width ( unsigned int width_ );
    unsigned int  Width ( void );

    // Height
    void          Height ( unsigned int height_ );
    unsigned int  Height ( void );

};

And then, we implement the class itself in Geometry.cpp.

C++
#include "stdafx.h"
#include "Geometry.h"

using namespace System;
using namespace System::Runtime::InteropServices;

namespace ImageMagickNET
{
    ///-------------------------------------------------------------------
    /// Constructor
    ///-------------------------------------------------------------------
    Geometry::Geometry ( )
    {
        geometry = new Magick::Geometry();
    }

    ///-------------------------------------------------------------------
    /// Constructor
    ///-------------------------------------------------------------------
    Geometry::Geometry ( 
        unsigned int width,
        unsigned int height,
        unsigned int xOff,
        unsigned int yOff,
        bool xNegative,
        bool yNegative )
    {
        geometry = new Magick::Geometry(width, height, xOff, yOff, 
            xNegative, yNegative);
    }

    ///-------------------------------------------------------------------
    /// Constructor
    ///-------------------------------------------------------------------
    Geometry::Geometry ( System::String^ pGeometry )
    {
        std::string geometryStr;
        geometry = new Magick::Geometry(
            Marshaller::SystemStringToStdString(pGeometry, geometryStr));
    }

    ///-------------------------------------------------------------------
    /// Constructor
    ///-------------------------------------------------------------------
    Geometry::Geometry ( Geometry^ pGeometry )
    {
        geometry = new Magick::Geometry(*(pGeometry->geometry));
    }


    ///-------------------------------------------------------------------
    // Width
    ///-------------------------------------------------------------------
    void          Geometry::Width ( unsigned int width_ )
    {
        geometry->width( width_ );
    }

    unsigned int  Geometry::Width ( void )
    {
        return geometry->width();
    }
    
    ///-------------------------------------------------------------------
    // Height
    ///-------------------------------------------------------------------
    void          Geometry::Height ( unsigned int height_ )
    {
        geometry->height( height_ );
    }

    unsigned int  Geometry::Height ( void )
    {
        return geometry->height();
    }
}

Back to the Image Wrapper

Now, back to the Image Wrapper, we should be implementing as many methods as the Magick::Image class has, using all the techniques we have learnt above about wrapping. But we will focus on this one method, that uses reference to the Magick::Geometry and the Magick::Color class.

The method signature in Magick::Image is as follows:

C++
void         resize( Magick::Geometry &geometry_ );

When we implement it in the Image.h, it will become like this:

C++
namespace ImageMagickNET 
{ 
    public ref class Image
    {
    public:
    void         Resize( Geometry^ geometry_ );
    };
}

When implementing this in the Image.cpp file, we must resolve the .NET-wrapped Geometry class to the C++ equivalent. In this case, we don't use the Marshaller anymore, but the reference object wrapped within the .NET Geometry class. This is how we do it:

C++
///-------------------------------------------------------------------
// Resize image to specified size.
///-------------------------------------------------------------------
void            Image::Resize ( Geometry ^geometry_ )
{
    image->resize( *(geometry_->geometry) );
}

The .NET Image class wrapper exposes a lot of functionality from the Magick::Image object, though not all. But it serves as a good start to anyone who would like to use the ImageMagick libraries from .NET.

In the following sections we will be discussing on how to use this library in a VB.NET windows forms application.

Using ImageMagick from VB.NET

Using the ImageMagick wrapper that we developed for .NET from VB turns out to be fairly straightforward.

  1. In the same solution as your ImageMagick .NET wrapper, create a VB.NET Windows Application project in Visual Studio 2005. Select Other Languages > Visual Basic > Windows > Windows Application. Give the application a name and click on OK.
  2. Add a reference from your VB.NET project to the ImageMagick .NET wrapper project.
  3. In the .vb file, put the following line at the top of the file:
    VB
    Imports ImageMagickNET
    
  4. Drop a MenuStrip object on the form and construct the following menu:
    VB
    File
        Load...
        Save...
    
  5. Rename the form to be called Convert, and we create the Convert_Load event and call the following in the Convert_Load event:
    VB
    '-------------------------------------------------------------------
    ' Load event
    '-------------------------------------------------------------------
    Private Sub Convert_Load(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles MyBase.Load
        ' initialize the ImageMagick library here
        MagickNet.InitializeMagick(Application.ExecutablePath)
    End Sub
    
  6. Drop the OpenFileDialog and the SaveFileDialog onto the form.
  7. Create the menu Click event for the Load menu, and in the event, show a dialog and then load the file specified by the user in the open file dialog:
    Image 3
    VB
    '-------------------------------------------------------------------
    ' Load file from the path specified.
    '-------------------------------------------------------------------
    Private Sub LoadFile(ByVal filename As String)
        ' we load all formats as a list of images
        ' (single frame images such as JPG will
        ' only contain one image in the list)
        imageList = New ImageMagickNET.ImageList()
        imageList.ReadImages(filename)
        Me.Text = filename
    
        ' Once the file is loaded, refresh the
        ' picturebox to show the image
        PictureBox1.Refresh()
    End Sub
    
    '-------------------------------------------------------------------
    ' Open the file dialog and load an image.
    '-------------------------------------------------------------------
    Private Sub LoadToolStripMenuItem_Click(_
        ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles LoadToolStripMenuItem.Click
        ' Open the open file dialog
        If OpenFileDialog1.ShowDialog() = _
            Windows.Forms.DialogResult.OK Then
            ' If the user clicks OK, then load up the file
            LoadFile(OpenFileDialog1.FileName)
    
            ' Set the filename for the Save dialog, so that the user
            ' can choose to save it back to the same file name.
            SaveFileDialog1.FileName = OpenFileDialog1.FileName
        End If
    End Sub
    
    The LoadFile subroutine here bears a little explanation. We did not use the Image object in our .NET Wrapper, but an ImageList object that we created. This ImageList object wraps a C++ STL list. But our real intent is so that we are able to read multi-frame images. Multi-frame images are images that have more than one frame in it and normally these frames are cycled to give the impression of animation. GIF files are one of the few formats with multiple frames. So when you call ImageList.ReadImages and pass in a GIF file as a parameter, all frames in that GIF will be loaded up. Contrast this with using the Image.Read method, only the first frame of the GIF file will be loaded. When you load single-frame formats like JPG, PNG using the ImageList.ReadImages method, the ImageList will only have one frame in it.
  8. Create the menu Click event for the Save menu, and in the event, show the save file dialog and save the file.
    VB
    '-------------------------------------------------------------------
    ' Save file from the path specified.
    '-------------------------------------------------------------------
    Private Sub SaveFile(ByVal filename As String)
        ' write all images into a single file
        '
        ' Notice that we pass in true as the second parameter
        ' to indicate that we want an adjoin file, ie, all frames
        ' in one file.
        '
        ' If we pass in false, each frame will be saved as a separate
        ' as according to the Magick++ documentation.
        '
        imageList.WriteImages(filename, True)
    End Sub
    
    '-------------------------------------------------------------------
    ' Open the Save file dialog and save the image.
    '-------------------------------------------------------------------
    Private Sub SaveToolStripMenuItem_Click(_
        ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles SaveToolStripMenuItem.Click
        ' Open the save file dialog
        If SaveFileDialog1.ShowDialog() = _
            Windows.Forms.DialogResult.OK Then
            ' If the user clicks OK, we do a check to see if the file
            ' format is a JPG. If so, show the
            ' compression quality dialog
            '
            If (SaveFileDialog1.FileName.ToUpper().EndsWith(".JPG")) Then
                ConvertQuality.ShowDialog()
                SetQuality()
            End If
    
            ' We save the file
            SaveFile(SaveFileDialog1.FileName)
        End If
    End Sub
    
  9. Compile the application. But before the application can be run, make sure of the following things:
    1. Be sure that you have the ImageMagick Q8 DLL version installed on your machine.
    2. Be sure to copy the ImageMagick Q8 DLLs (available in the source code package) into the output bin folder of your application.

Now with this simple application, you can load and save files in different formats.

Setting Compression Quality

Often when saving a JPG file, the user would be required to specify the compression quality. A better compression (that is, a smaller output file) will result poorer visual quality; in contrast, a worse compress (or a bigger file) will result be better visual quality. And the Image .NET wrapper exposes this compression quality as a property.

  1. So we will create a new form called ConvertQuality.
  2. Drop a scroll bar, two labels (one whose Caption='Quality', the other is meant to be updated with the scroll bar value when it changes), and an OK button onto the form.
    Image 4
  3. Set the scroll bar's minimum property to 1, and maximum property to 100.
  4. Create the following event to update the label when the scrollbar is changed.
    VB
    '-------------------------------------------------------------------
    ' Set the label to the value of the scroll bar when it changes
    '-------------------------------------------------------------------
    Private Sub HScrollBar1_ValueChanged(ByVal sender As System.Object,_
        ByVal e As System.EventArgs) Handles HScrollBar1.ValueChanged
        Label2.Text = HScrollBar1.Value.ToString()
    End Sub
    
  5. Expose a public integer variable called Quality.
    VB
    Public Quality As Integer
    
  6. Create the following events to set the public variable Quality and close the form when the button is clicked.
    VB
    '-------------------------------------------------------------------
    ' Close and return to the caller
    '-------------------------------------------------------------------
    Private Sub Button1_Click(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles Button1.Click
        Me.Quality = HScrollBar1.Value
        Me.Close()
    End Sub
    
  7. We will add a subroutine to set the quality of all images in the ImageList in the main Convert form. The Images can be access using the For Each syntax, as the ImageList exposes the enumerator.
    VB
    '-------------------------------------------------------------------
    ' Set the compression quality of the image.
    '-------------------------------------------------------------------
    Private Sub SetQuality()
        ' Set quality for all images in the list
        ' but normally set quality only applies to JPG or
        ' coders that use the quality property.
        For Each image As ImageMagickNET.Image In imageList
            image.Quality(ConvertQuality.Quality)
        Next
    End Sub
    
  8. Now going back to the Convert form, we are going to make some minor changes to the SaveToolStripMenuItem_Click event:
    VB
    '--------------------------------------------------------------------
    ' Open the Save file dialog and save the image.
    '-------------------------------------------------------------------
    Private Sub SaveToolStripMenuItem_Click(_
        ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles SaveToolStripMenuItem.Click
        ' Open the save file dialog
        If SaveFileDialog1.ShowDialog() = _
            Windows.Forms.DialogResult.OK Then
            ' If the user clicks OK, we do a check to see if the file
            ' format is a JPG. If so, show the compression quality dialog
            '
            If (SaveFileDialog1.FileName.ToUpper().EndsWith(".JPG")) Then
                ConvertQuality.ShowDialog()
                SetQuality()
            End If
    
            ' We save the file
            SaveFile(SaveFileDialog1.FileName)
        End If
    End Sub
    
    Notice that we try to detect the output format. If it is a JPG file, then we pop up the ConvertQuality form as a prompt for the user to set the compression quality. After the user sets it, the compression quality is set for all frames in the image.
  9. We have also included a simple feature to display the loaded image onto the form. We created a PictureBox onto the form, and hooked its Paint event. In this paint event, we call a method ToBitmap() on the first frame of the images, and then paint this bitmap onto the PictureBox. Additionally, we have ensured that the PictureBox took the size of the Form's client width and height in the event of any resize.
    VB
    '--------------------------------------------------------------------
    ' Resize form
    '-------------------------------------------------------------------
    Private Sub MenuStrip1_Resize(ByVal sender As System.Object, _
        ByVal e As System.EventArgs)
        _Handles MenuStrip1.Resize
        PictureBox1.Height = Me.ClientSize.Height - MenuStrip1.Height
        PictureBox1.Width = Me.ClientSize.Width
    End Sub
    
    '-------------------------------------------------------------------
    ' PictureBox1 Paint event
    '-------------------------------------------------------------------
    Private Sub PictureBox1_Paint(ByVal sender As System.Object,_
        ByVal e As _
        System.Windows.Forms.PaintEventArgs) Handles PictureBox1.Paint
        ' just draw the first image (regardless of how many there are in
        ' the list)
        ' of course, it would be trivial to show animated GIF by cycling
        ' through all images in the list, via a timer.
        '
        For Each image As ImageMagickNET.Image In imageList
            If image.Columns > PictureBox1.Width Or image.Rows > _
                PictureBox1.Height Then
                PictureBox1.SizeMode = PictureBoxSizeMode.Zoom
            Else
                PictureBox1.SizeMode = PictureBoxSizeMode.CenterImage
            End If
    
            ' This is where we can attach the
            ' .NET Bitmap constructed from
            ' the image to our PictureBox
            PictureBox1.Image = image.ToBitmap
            Exit For
        Next
    
    End Sub
    

In the source code that we have provided, an operation to resize the image is available. But more image operations can certainly be added.

Running .NET Wrapper on Other Platforms

The C++ wrapper has not yet been tested on Mono and all platforms that Mono supports. We are keen to know if anyone has endeavored compiling the C++ codes against ImageMagick in their respective platforms and producing a .NET wrapper on those platforms. If it is possible at all, do let us know and share your experience with us, positive, or negative.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Singapore Singapore
bubble2k is a software architect in an IT consulting firm. He does in his free time recreational development with C# and Flash, for games, multimedia, and emulation. He also does web and digital graphics design.

Dubble Design
I Want More Money, I Need More Money

Comments and Discussions

 
GeneralCompile DLL Pin
PQSIK20-Dec-08 13:44
PQSIK20-Dec-08 13:44 
GeneralRe: Compile DLL Pin
PQSIK15-Jan-09 17:47
PQSIK15-Jan-09 17:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.