65.9K
CodeProject 正在变化。 阅读更多。
Home

VB.NET 中的 ImageMagick

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (23投票s)

2007 年 3 月 3 日

22分钟阅读

viewsIcon

430304

downloadIcon

33006

ImageMagick 是一个功能强大的图像处理工具,支持多种格式。在本文中,我们将开发一个 C++ 版的 ImageMagick 包装器,以将 Magick++ 的功能暴露给 .NET 应用程序。

引言

ImageMagick 是一个功能强大的图像处理工具,可识别多种图像格式,包括网络上常用的 JPG、GIF、PNG 格式。它的一些处理功能包括应用图形效果,例如炭笔画、高斯模糊、曝光反转(以及在 Adobe Photoshop 等专业图像处理软件中看到的一些效果)。

ImageMagick 通过一组命令行可执行文件公开其功能,可以通过指定开关和选项来应用效果。但它也通过其 MagickCore 和 MagickWand C 库以及 Magick++ C++ 库公开其功能,以便开发人员可以在其软件中利用其功能。ImageMagick 社区还开发了 Perl、Python、Java 等其他语言的接口。

在此练习中,我们创建了一个 .NET ImageMagick 包装器,作为对 ImageMagick 功能的研究。我们还尝试创建了一个 VB.NET 应用程序,该应用程序调用 .NET 包装器来访问 ImageMagick 功能。尽管已经存在一个 ImageMagick .NET 包装器,但我们创建了一个更接近 Magick++ 可用类的模型。由于 Magick++ 在 C++ 类和标准模板库中公开了 ImageMagick,因此在 .NET 和 C++ 之间封送类型变得不那么困难。

在开始之前,我们想强调一下 ImageMagick 现在支持的格式。(请注意,某些格式需要安装额外的插件。)

扩展名 模式 描述 注释
ART R PFS: 1st Publisher 最初在 Macintosh (MacPaint?) 上使用的格式,后来用于 PFS: 1st Publisher 剪贴画。
AVI R Microsoft 音频/视频交错
AVS RW AVS X 图像
BMP RW Microsoft Windows 位图
CGM R 计算机图形元文件 需要 ralcgm 来渲染 CGM 文件。
CIN RW 柯达 Cineon 图像格式 使用 -set 指定图像伽马或黑白点(例如 -set gamma 1.7, -set reference-black 95, -set reference-white 685)。
CMYK RW 原始青色、品红色、黄色和黑色样本 使用 -size 和 -depth 指定图像的宽度、高度和深度。要指定单精度浮点格式,请使用 -depth 32 -define quantum:format=floating-point。将深度设置为 64 以获取双精度浮点格式。
CMYKA RW 原始青色、品红色、黄色、黑色和 Alpha 样本 使用 -size 和 -depth 指定图像的宽度、高度和深度。要指定单精度浮点格式,请使用 -depth 32 -define quantum:format=floating-point。将深度设置为 64 以获取双精度浮点格式。
CUR R Microsoft 光标图标
CUT R DR Halo
DCM R 医学数字成像和通信 (DICOM) 图像 医疗界用于 X 射线等图像。
DCX RW ZSoft IBM PC 多页画笔图像
DIB RW Microsoft Windows 设备无关位图 DIB 是没有 BMP 头部的 BMP 文件。用于支持 WMF 等复合格式中的嵌入图像。
DJVU R
DNG R 数字负片
DOT R 图形可视化 使用 -define 指定布局引擎(例如 -define dot:layout-engine=neato)。
DPX RW SMPTE 数字电影交换 使用 -set 指定图像伽马或黑白点(例如 -set gamma 1.7, -set reference-black 95, -set reference-white 685)。
EMF R Microsoft 增强型图元文件 (32 位) 仅适用于 Microsoft Windows。
EPDF RW 封装便携式文档格式
EPI RW Adobe 封装 PostScript 交换格式 需要 Ghostscript 才能读取。
EPS RW Adobe 封装 PostScript 需要 Ghostscript 才能读取。
EPS2 W Adobe Level II 封装 PostScript 需要 Ghostscript 才能读取。
EPS3 W Adobe Level III 封装 PostScript 需要 Ghostscript 才能读取。
EPSF RW Adobe 封装 PostScript 需要 Ghostscript 才能读取。
EPSI RW Adobe 封装 PostScript 交换格式 需要 Ghostscript 才能读取。
EPT RW
OTB RW 空中位图
P7 RW Xv 的 Visual Schnauzer 缩略图格式
PALM RW Palm 像素图
PAM W 通用二维位图格式
PBM RW 便携式位图格式(黑白)
PCD RW Photo CD 写入的最大分辨率为 768x512 像素,因为更大的图像需要哈夫曼压缩(不支持)。
PCDS RW Photo CD 使用 sRGB 颜色表解码。
PCL W HP 页面控制语言 用于输出到 HP 激光打印机。
PCX RW ZSoft IBM PC 画笔文件
PDB RW Palm 数据库 ImageViewer 格式
PDF RW 便携式文档格式 需要 Ghostscript 才能读取。默认情况下,ImageMagick 将页面大小设置为 MediaBox。但是,某些 PDF 文件具有小于 MediaBox 的 CropBox,并且可能在 CropBox 之外包含空白、注册或裁剪标记。要强制 ImageMagick 使用 CropBox 而不是 MediaBox,请使用 -define(例如 -define pdf:use-cropbox=true)。
PFA R Postscript Type 1 字体 (ASCII) 作为文件打开会返回预览图像。
PFB R Postscript Type 1 字体 (二进制) 作为文件打开会返回预览图像。
PGM RW 便携式灰度图格式(灰度)
PICON RW 个人图标
PICT RW Apple Macintosh QuickDraw/PICT 文件
PIX R Alias/Wavefront RLE 图像格式
PNG RW 便携式网络图形 需要 libpng-1.0.2 或更高版本,推荐 libpng-1.2.5 或更高版本。
PNM RW 便携式任何图 PNM 是一系列支持便携式位图 (PBM)、灰度图 (PGM) 和像素图 (PPM) 的格式。没有与 pnm 本身关联的文件格式。如果 PNM 用作输出格式说明符,则 ImageMagick 会自动选择最合适的格式来表示图像。默认是写入格式的二进制版本。使用 -compress none 写入格式的 ASCII 版本。
PPM RW 便携式像素图格式(彩色)
PS RW Adobe PostScript 文件 需要 Ghostscript 才能读取。
PS2 RW Adobe Level II PostScript 文件 需要 Ghostscript 才能读取。
PS3 RW Adobe Level III PostScript 文件 需要 Ghostscript 才能读取。
PSD RW Adobe Photoshop 位图文件
PTIF RW 金字塔编码 TIFF 多分辨率 TIFF,包含图像的 successively smaller versions,直至图标大小。在读取时可以通过 -size 选项指定所需的子图像大小。
PWP R Seattle File Works 多图像文件
RAD R 辐射图像文件 需要安装 Radiance 软件包中的 ra_ppm。
RGB RW 原始红色、绿色和蓝色样本 使用 -size 和 -depth 指定图像的宽度、高度和深度。要指定单精度浮点格式,请使用 -depth 32 -define quantum:format=floating-point。将深度设置为 64 以获取双精度浮点格式。
RGBA RW 原始红色、绿色、蓝色和 Alpha 样本 使用 -size 和 -depth 指定图像的宽度、高度和深度。要指定单精度浮点格式,请使用 -depth 32 -define quantum:format=floating-point。将深度设置为 64 以获取双精度浮点格式。
RLA R Alias/Wavefront 图像文件
RLE R 犹他行程编码图像文件
SCT R Scitex 连续色调图片
SFW R Seattle File Works 图像
SGI RW Irix RGB 图像
SHTML W 超文本标记语言客户端图像映射 用于根据蒙太奇输出或支持平铺图像(如 MIFF)的格式写入 HTML 可点击图像映射。
SUN RW SUN 光栅文件
SVG RW 可伸缩矢量图形 需要 libxml2 和 freetype-2。请注意,SVG 是一个非常复杂的规范,因此支持尚不完整。
TGA RW Truevision Targa 图像 也称为 ICB、VDA 和 VST 格式。
TIFF RW 标记图像文件格式 也称为 TIF。需要 tiff-v3.6.1.tar.gz 或更高版本。使用 -define 指定每条纹的行数(例如 -define tiff:rows-per-strip=8)。要指定有符号格式,请使用 -define quantum:format signed。要指定单精度浮点格式,请使用 -depth 32 -define quantum:format=floating-point。将深度设置为 64 以获取双精度浮点格式。使用 -define tiff:photometric=min-is-black 或 -define tiff:photometric=min-is-white 切换双级图像的光度解释。例如,使用 -define tiff:alpha=unassociated 将额外样本指定为关联或非关联 Alpha。
TIM R PSX TIM 文件
TTF R TrueType 字体文件 需要 freetype 2。作为文件打开会返回预览图像。如果您不希望在缩放到设备像素后提示字形轮廓,请使用 -set(例如 -set type:hinting off)。
TXT RW 原始文本文件
UIL W X-Motif UIL 表
UYVY RW 交错 YUV 原始图像 使用 -size 和 -depth 命令行选项指定宽度和高度。使用 -sampling-factor 设置所需的子采样(例如 -sampling-factor 4:2:2)。
VICAR RW VICAR 光栅文件格式
VIFF RW Khoros 可视化图像文件格式
WBMP RW 无线位图 仅支持未压缩的单色。
WMF R Windows 元文件 需要 libwmf。默认情况下,使用元文件标头指定的尺寸渲染 WMF 文件。使用 -density 选项调整输出分辨率,从而调整输出大小。默认输出分辨率为 72DPI,因此 -density 144 会生成比默认大小大两倍的图像。使用 -background color 指定 WMF 背景颜色(默认白色)或 -texture filename 指定背景纹理图像。
WPG R Word Perfect 图形文件
X RW 显示或导入图像到或从 X11 服务器 使用 -define 从根窗口获取图像(例如 -define x:screen=true)。
XBM RW X Windows 系统位图,仅限黑白 X Windows 系统用于存储单色图标。
XCF R GIMP 图像
XPM RW X Windows 系统像素图 也称为 PM。X Windows 系统用于存储彩色图标。
XWD RW X Windows 系统窗口转储 X Windows 系统用于保存/显示屏幕转储。
YCbCr RW 原始 Y、Cb 和 Cr 样本 使用 -size 和 -depth 指定图像宽度、高度和深度。
YCbCrA RW 原始 Y、Cb、Cr 和 Alpha 样本 使用 -size 和 -depth 指定图像宽度、高度和深度。
YUV RW CCIR 601 4:1:1 使用 -size 和 -depth 命令行选项指定宽度、高度和深度。使用 -sampling-factor 设置所需的子采样(例如 -sampling-factor 4:2:2)。

编译源代码

我们已尽力使源代码的编译尽可能简单。但您的机器上需要设置以下环境

  1. 您必须有一个可用的 C: 盘。
  2. 您必须有 Visual Studio .NET 2005。
    如果您没有,您仍然可以使用 Visual Studio 2005 Express Editions (C++ 和 Visual Basic),但您可能需要拆分 .SLN 文件。

由于某种原因,ImageMagick 6.3.2 无法从与执行应用程序相同的目录加载 IM_MOD*.DLL。相反,它总是引用注册表中的 IM_MOD*.DLL 文件路径。这可能会给那些希望在托管服务器上使用 ImageMagick 但无法访问托管服务器注册表的用户带来问题。因此,我获取了 ImageMagick 6.3.2 源代码并对其进行了非常小的修改,使其能够从执行应用程序目录读取 IM_MOD*.DLL。因此,在此更新版本中,您不再需要设置注册表中的任何内容。

要安装,请下载 ImageMagick-Source.zip、ImageMagick-DLLs-Q8.zip、ImageMagick-DLLs-Q16.zip 并将它们解压缩到同一文件夹位置。(我们为单独的文件道歉,因为 CodeProject 无法将它们全部作为单个大 zip 文件接受)如果您已正确解压缩它们,您的目录结构应该如下所示

 Folder
  +-DLLs
  |  +-DebugQ8
  |  +-DebugQ16
  +-ImageMagickNET
  |  +-include8
  |  +-include16
  |  +-lib8
  |  +-lib16
  +-VBForms
     +-My Project
您会注意到其中有一个批处理文件:install.bat。双击 install.bat 执行批处理文件。批处理文件执行以下操作

  1. 创建 DLLs\DebugQ8 的副本到 DLLs\ReleaseQ8,以及 DLLs\DebugQ16 的副本到 DLLs\ReleaseQ16。

完成后,在 Visual Studio .NET 中打开 ImageMagick.NET.sln 文件

  1. 有四种可能的编译配置:DebugQ8、DebugQ16、ReleaseQ8、ReleaseQ16。选择 Q8 或 Q16 进行编译。您之前是否在机器上设置过 ImageMagick 无关紧要。
  2. 重新生成整个解决方案。
  3. 运行 VB 演示。

使用 ImageMagick 的后续版本进行编译

如果您需要使用 ImageMagick 的后续版本编译 .NET 包装器,有几点需要注意。第一种方法

  1. 从以下位置下载 ImageMagick 的最新版本:https://imagemagick.org.cn/script/binary-releases.php。请记住下载 DLL 版本,而**不是**静态版本
  2. 获取最新的 *.h 文件并将其复制到 ImageMagickNET\include8 或 ImageMagickNET\include16 文件夹(取决于像素量化)
  3. 获取最新的 *.lib 文件并将其复制到 ImageMagickNET\lib8 或 ImageMagickNET\lib16
  4. 获取最新的 CORE_RL*.DLL、mfc71、msvcp71、msvcr71.dll 以及 ImageMagick 安装根文件夹中存在的所有其他非 IM_MOD_RL*.DLL。将这些文件复制到 DLLs\DebugQX 和 DLLs\ReleaseQX 文件夹中。VB 项目有一个后期构建事件,它会根据您选择的配置将相应的 DLL 复制到您的应用程序输出文件夹中。(请参阅下面关于 CORE_RL*.DLL 的说明)
  5. 从 ImageMagick 获取 IM_MOD_RL*.DLL 文件和 *.xml 文件,并将其复制到 C:\ImageMagick

以下注意事项对于开发环境的设置很重要,可能对希望将应用程序部署到生产站点的用户有用。

关于 CORE_RL*.DLL 的注意事项

运行应用程序时,请确保 CORE_RL*.DLL、msvcp71.dll、msvcr71.dll 和所有其他非 IM_MOD_RL*.DLL 的正确像素量化版本 Q8/Q16 位于**与您的应用程序相同的文件夹中**。这是为了确保您的应用程序直接从其当前路径加载 DLL,而不是从 ImageMagick 文件夹加载。

您可能会问,我们为什么要这样做?这是因为如果您的机器上同时安装了 Q8 和 Q16 版本的 ImageMagick,Windows 不会自动知道要查找哪个 CORE_RL*.DLL。我们相信它只会在当前应用程序目录中查找 DLL,然后按照 PATH 环境变量中指定的文件夹顺序查找。因此,如果您的机器上同时设置了 Q8 和 Q16,则 Windows 将始终查找 Q8 DLL 或 Q16 DLL。当使用错误的像素量化库时,这会导致您的应用程序崩溃。因此,将 CORE_RL*.DLL 复制到您的应用程序文件夹更安全。

关于 IM_MOD_RLL*.DLL 的注意事项

当您从安装程序设置最新的 ImageMagick 时,安装程序将自动在您的机器上设置一些注册表项。这些 DLL 负责解码和编码各种受支持的图像文件格式。现在,当 CORE_RL*.DLL 需要加载其中一个时,它**不会**(至少在 6.3.2 版本中)在当前应用程序文件夹中搜索它们。相反,它会在注册表项中 CoderModulesPath 键指定的文件夹中搜索它们。(这就是为什么我们必须重新编译 ImageMagick 源代码以使其能够在当前执行文件夹下查找)ImageMagick 注册表项,Q8 和 Q16 各一组,定义如下

[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"

托管 C++ 扩展

我们将通过托管扩展将 Magick++ 接口暴露给 .NET。但是由于 Magick++ 是用 C++ 编写的,我们将使用 C++ 创建 .NET 接口。请注意,我们使用的 C++ 托管扩展仅适用于 .NET Framework 2.0,因此文件只能在 Visual Studio 2005 上编译。

网上已经有很多文章深入介绍了托管 C++ 扩展,所以我们不会过多地介绍细节。在本文中,我们将仅简要介绍冰山一角,足以让我们在 C++ 中创建一个包装器。

创建包装类

我们有兴趣为 Magick++ 库的 Image 类 (Magick::Image) 创建包装类。以下是我们如何在 Image.h 中声明头文件

#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);

    };
}

请注意,在第二个构造函数中,.NET Image 类接受 System::String,这相当于 .NET String 类。但是,等效的 Magick::Image 构造函数具有以下签名

    Image(std::string imageSpec);

std::string 类是 C++ STL 字符串格式。现在,这立即给我们带来了问题,因为 C++ STL 字符串类和 .NET String 类显然不兼容。这就是封送器必须介入的地方。

封送字符串

将 .NET 类型转换为等效 C++ 类型的大部分封送工作涉及字符串,至少在我们这里是这样。所以我们专门编写了一个单独的类,以一种相当抽象的方式处理封送。这是我们设计用于处理字符串封送的 Marshaller 类。Marshaller.h 声明如下

#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);
    };
}

它仅由 3 个静态方法组成。

第一个是 StdStringToSystemString,其职责是将 C++ STL 字符串转换为 .NET 字符串。

另外两个将 .NET 字符串转换为 C++ STL 字符串。大多数情况下,我们将使用最后一个方法将字符串从 .NET 传递到 ImageMagick 库,因为它是最简单的方法,并且处理 StringToHGlobalAnsi 所涉及的所有工作都得到了抽象。但是,使用最后一个方法有一个小小的警告,因为此方法需要内存中静态的字符串池。我们选择创建一个包含 8 个字符串的池,这些字符串以循环方式用于封送。

Marshaller.cpp 实现如下

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]);

        
    }
}

实现图像构造函数

现在我们已经实现了 Marshaller,我们可以继续实现 Image 类的构造函数。

    ///-------------------------------------------------------------------
    /// 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));
    }

实现加载和保存

现在基本的构造函数和析构函数已经准备就绪,我们可以继续实现 Magick::Image 类的许多其他功能。

我们首先包装加载和保存方法。Image.h 现在实现如下

#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_);


    }
}

再次注意,我们使用了 Marshaller:SystemStringToStdString 方法进行封送。Magick::Image 类有一些用于加载和保存文件的方法,但其中我们只关注两个基本方法

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

我们尽量避免对方法签名和调用方式进行重大更改,只是将方法名称的首字母大写。这是为了尽可能保留 Magick::Image 类的原始接口,这样我们就不必重新编写所有接口文档。

Read 和 Write 函数在 Image.cpp 中实现如下。

    ///-------------------------------------------------------------------
    /// 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));
    }

瞧!我们现在有一个 Image 类,可以将其导出到 .NET 以进行简单的文件加载和保存。

当然,我们还不满足,因为 Magick::Image 中封装了更多我们需要暴露给 .NET 程序的功能。但在我们暴露所有这些功能之前,Magick::Image 类中的一些方法需要引用 Magick:: 命名空间中类的对象。类的示例有:Magick::GeometryMagick::Color。一些方法还需要传递枚举类型作为参数。这意味着这些类和枚举类型必须在我们的 .NET 包装器中定义。

设置枚举器类

在实现 .NET Image 类的整个接口时,我们将设置大量枚举器类,但我们只突出显示一个,因为其余的可以完全以相同的方式实现。

ImageMagick 库中的 MagickLib:: 命名空间具有一个名为 PaintMethod 的枚举类型。它允许用户指定图像上的洪水填充和绘画相关操作的方法。它在 C++ 中这样声明

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

当我们在 .NET 中实现它时,我们使用了 .NET 枚举类,如下所示。

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

通常,建议将枚举中的每个项映射到 C++ 库中声明的相应常量。这是为了避免在较新版本中枚举顺序不同时可能发生的重大更改。

设置其他类

正如前一节中强调的,Magick::Image 类需要引用 Magick::ColorMagick::Geometry 等类作为参数。在这里,我们将以 Color 类为例进行实现,其余的类也可以用相同的方式实现。

Magick::Geometry 类的实现,本质上,与我们包装 Magick::Image 类的方式相同。在实现包装器时,我们尝试匹配 Magick::Geometry 类公开的尽可能多的函数。但为了缩短我们在此处用于说明的代码,我们只挑选了几个方法。我们是这样做的

    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 );
        
    };

然后,我们在 Geometry.cpp 中实现类本身。

#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();
    }
}

回到图像包装器

现在,回到图像包装器,我们应该使用上面学到的所有包装技术,实现与 Magick::Image 类一样多的方法。但我们将专注于使用 Magick::GeometryMagick::Color 类的引用对象的这个方法。

Magick::Image 中的方法签名如下

    void         resize( Magick::Geometry &geometry_ );

当我们在 Image.h 中实现它时,它将变成这样

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

在 Image.cpp 文件中实现此功能时,我们必须将 .NET 包装的 Geometry 类解析为等效的 C++ 类。在这种情况下,我们不再使用 Marshaller,而是使用 .NET Geometry 类中包装的引用对象。我们是这样做的

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

.NET Image 类包装器公开了 Magick::Image 对象的许多功能,尽管不是所有功能。但对于任何希望从 .NET 使用 ImageMagick 库的人来说,它都是一个很好的起点。

在接下来的部分中,我们将讨论如何在 VB.NET Windows Forms 应用程序中使用此库。

从 VB.NET 使用 ImageMagick

从 VB 使用我们为 .NET 开发的 ImageMagick 包装器非常简单。

  1. 在您的 ImageMagick .NET 包装器所在的解决方案中,在 Visual Studio 2005 中创建一个 VB.NET Windows 应用程序项目。选择“其他语言”>“Visual Basic”>“Windows”>“Windows 应用程序”。给应用程序命名,然后单击“确定”。
  2. 从您的 VB.NET 项目中添加对 ImageMagick .NET 包装器项目的引用。
  3. 在 .vb 文件的顶部添加以下行
        Imports ImageMagickNET
    
  4. 将 MenuStrip 对象拖放到窗体上,并构建以下菜单
        File
            Load...
            Save...
    
  5. 将窗体重命名为 Convert,我们创建 Convert_Load 事件并在 Convert_Load 事件中调用以下内容
        '-------------------------------------------------------------------
        ' 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. OpenFileDialogSaveFileDialog 拖放到窗体上。
  7. 为加载菜单创建菜单 Click 事件,并在事件中显示一个对话框,然后加载用户在打开文件对话框中指定的文件
        '-------------------------------------------------------------------
        ' 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
    
    这里的 LoadFile 子程序需要一点解释。我们没有在 .NET 包装器中使用 Image 对象,而是使用了我们创建的 ImageList 对象。这个 ImageList 对象包装了一个 C++ STL 列表。但我们的真正目的是为了能够读取多帧图像。多帧图像是包含多个帧的图像,这些帧通常循环播放以产生动画效果。GIF 文件是少数具有多个帧的格式之一。因此,当您调用 ImageList.ReadImages 并传入 GIF 文件作为参数时,该 GIF 中的所有帧都将被加载。与使用 Image.Read 方法相比,只有 GIF 文件的第一帧会被加载。当您使用 ImageList.ReadImages 方法加载 JPG、PNG 等单帧格式时,ImageList 中将只有一帧。
  8. 为保存菜单创建菜单 Click 事件,并在事件中显示保存文件对话框并保存文件。
        '-------------------------------------------------------------------
        ' 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. 编译应用程序。但在运行应用程序之前,请确保以下事项
    1. 确保您的机器上安装了 ImageMagick Q8 DLL 版本。
    2. 确保将 ImageMagick Q8 DLL(在源代码包中提供)复制到应用程序的输出 bin 文件夹中。

现在,有了这个简单的应用程序,您可以加载和保存不同格式的文件。

设置压缩质量

通常在保存 JPG 文件时,会要求用户指定压缩质量。更好的压缩(即更小的输出文件)会导致更差的视觉质量;相反,更差的压缩(或更大的文件)会导致更好的视觉质量。Image .NET 包装器将此压缩质量公开为一个属性。

  1. 因此,我们将创建一个名为 ConvertQuality 的新窗体。
  2. 将一个滚动条、两个标签(一个的 Caption='Quality',另一个用于在滚动条值改变时更新)和一个 OK 按钮拖放到窗体上。
  3. 将滚动条的最小属性设置为 1,最大属性设置为 100。
  4. 创建以下事件以在滚动条更改时更新标签。
        '-------------------------------------------------------------------
        ' 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. 公开一个名为 Quality 的公共整数变量。
        Public Quality As Integer
    
  6. 创建以下事件以设置公共变量 Quality 并在单击按钮时关闭窗体。
        '-------------------------------------------------------------------
        ' 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. 我们将在主 Convert 窗体中添加一个子程序来设置 ImageList 中所有图像的质量。图像可以使用 For Each 语法访问,因为 ImageList 公开枚举器。
        '-------------------------------------------------------------------
        ' 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. 现在回到 Convert 窗体,我们将对 SaveToolStripMenuItem_Click 事件进行一些小的更改
        '--------------------------------------------------------------------
        ' 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
    
    请注意,我们尝试检测输出格式。如果它是 JPG 文件,则会弹出 ConvertQuality 窗体以提示用户设置压缩质量。用户设置后,所有图像帧的压缩质量都会设置好。
  9. 我们还包含了一个简单的功能,可以在窗体上显示加载的图像。我们在窗体上创建了一个 PictureBox,并钩住了它的 Paint 事件。在此 paint 事件中,我们调用图像第一帧上的 ToBitmap() 方法,然后将此位图绘制到 PictureBox 上。此外,我们已确保在任何调整大小事件中,PictureBox 都会占用窗体客户端的宽度和高度。
        '--------------------------------------------------------------------
        ' 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

在我们提供的源代码中,图像调整大小操作是可用的。但当然可以添加更多图像操作。

在其他平台上运行 .NET 包装器

C++ 包装器尚未在 Mono 和 Mono 支持的所有平台上进行测试。我们渴望知道是否有人曾尝试在其各自的平台上针对 ImageMagick 编译 C++ 代码,并在这些平台上生成 .NET 包装器。如果有可能,请告诉我们并与我们分享您的经验,无论是积极的还是消极的。

© . All rights reserved.