纯 .NET C# DirectShow 过滤器






4.99/5 (76投票s)
本文介绍了如何在 .NET 中创建 DirectShow 过滤器,其中包含 BaseClasses 和一些示例。
- 下载示例过滤器二进制文件 - 124.1 KB
- 下载 BaseClasses 和过滤器源代码 - 143.1 KB
- 下载示例应用程序源代码 - 75.8 KB
- 下载示例应用程序二进制文件 - 134.4 KB
引言
在这篇文章中,我将介绍如何用纯 C# 创建 DirectShow 过滤器。我同样用纯 C# 制作了 BaseClasses 库和几个示例,以展示它们可以多么容易地被使用。我认为初级的多媒体开发者可以使用我的库,但要扩展它,你需要了解 COM、封送处理和线程。我建议你阅读我之前的文章,因为我不会重复其中描述的主要内容。
内容概述
过滤器的源代码包含两个项目:BaseClasses 和 ExampleFilters。BaseClasses 库包含以下类:
- BaseEnum- 枚举的基类
- EnumPins-- IEnumPins接口的实现。
- EnumMediaTypes-- IEnumMediaTypes接口的实现。
- BasePin- 引脚的基类。
- BaseInputPin- 输入引脚的基类。
- BaseOutputPin- 输出引脚的基类。
- BaseFilter- 过滤器实现的基类。
- TransformInputPin- 变换过滤器的输入引脚类。
- TransformOutputPin- 变换过滤器的输出引脚类。
- TransformFilter- 变换过滤器的基类。
- TransInPlaceInputPin- 原地变换过滤器的输入引脚类。
- TransInPlaceOutputPin- 原地变换过滤器的输出引脚类。
- TransInPlaceFilter- 原地变换过滤器的基类。
- RenderedInputPin- 渲染过滤器的输入引脚基类。
- AMThread- 线程实现的基类。
- ManagedThread-- AMThread的托管线程实现。
- SourceStream- 基输出流引脚类。
- BaseSourceFilter- 源过滤器实现的基类。
- OutputQueue- 实现用于传递媒体样本的队列的类。
- BasePropertyPage- 用于实现过滤器属性页的类。
- PosPassThru- 处理查找命令,将它们传递给下一个过滤器的类。
- RendererPosPassThru- 处理渲染过滤器查找命令的类。
- RendererInputPin- 为- BaseRendererFilter类实现输入引脚的类。
- MessageDispatcher- 处理用户线程未阻塞通知并分发窗口消息的类。
- AsyncStream-- IAsyncReader接口的- System.IO.Stream实现。
- COMStream-- IStream接口的- System.IO.Stream实现。
- PacketData- 用于描述队列媒体数据的基类。
- PacketsQueue-- PacketData对象的队列类。
- BitStreamReader- 用于从- System.IO.Stream对象读取二进制数据的辅助类。它支持- IStream和- IAsyncReader接口。允许按位、按数组或按结构读取数据。类还支持 golomb SE 和 UE 值。读取通过缓存进行。
- DemuxTrack- 实现分离器轨道的基类。
- FileParser- 实现分离器文件解析支持的基类。
- SplitterInputPin- 实现- BaseSplitterFilter输入引脚的类。
- SplitterOutputPin- 为- BaseSplitterFilter和- BaseFileSourceFilter对象实现输出引脚。
- BaseSplitter- 实现分离器或文件源实现的类核心功能的类。
- BaseSplitterFilter- 实现具有单个输入引脚的分离器过滤器的基类,该引脚支持连接到- IAsyncReader,输出引脚在连接后创建。过滤器支持查找。
- BaseFileSourceFilter- 实现文件源过滤器的基类,它没有输入引脚,但支持- IFileSourceFilter接口,输出引脚在文件加载后创建。
解决方案中的另一个项目包含以下示例过滤器:
- NullInPlaceFilter- 一个原地变换过滤器,可用作基类。
- NullTransformFilter- 一个变换过滤器,可用作基类。
- DumpFilter- 用于将传入数据保存到文件的过滤器。
- TextOverFilter- 一个变换过滤器,在传入视频上显示叠加文本。
- ImageSourceFilter- 推送源过滤器,将加载的图像文件显示为视频流。
- ScreenCaptureFilter- 推送源过滤器,捕获显示并将其作为视频流提供。
- VideoRotationFilter- 一个变换过滤器,将视频旋转 90 度。
- WavDestFilter- 用于将传入的 PCM 数据保存到 WAV 文件的过滤器。
- AudioChannelFilter- 一个变换过滤器,用于合并传入的 PCM 音频数据并将其发送到指定的通道。
- InfTeeFilter- 将传入的样本传递给任何已连接输出引脚的过滤器。
- NullRendererFilter- 实现为标准的“Null Renderer”过滤器的过滤器,它继承自基渲染过滤器并展示了需要覆盖的基本方法。
- WAVESplitterFilter- 实现 WAVE 文件格式的分离器过滤器的过滤器。
- WAVESourceFilter- 实现 WAVE 文件格式的源过滤器的过滤器。
- NetworkSyncFilter和- NetworkSource过滤器是实现组播视频流的示例。
此外,我还包含了带有示例应用程序的解决方案,向您展示了如何使用我的辅助类,并将过滤器嵌入到应用程序中使用,而无需注册。
- DxCapture- 示例展示了如何构建视频捕获应用程序。
- DxGrabber- 示例用法样本抓取器。
- DxPlayer- 基本的音频视频播放器应用程序。
- WavCapture- 示例演示了如何构建音频捕获应用程序,该示例嵌入了 wave out 过滤器。
- WavExtract- 示例展示了如何从媒体文件中提取音频数据并将其保存为 WAVE 格式,该示例嵌入了 wave out 过滤器。
- WavPlay- 示例用法嵌入的 WAVE 源过滤器来播放 .wav 音频文件。
BaseClasses
BaseClasses 还使用了我的类库和 COM 辅助对象中的一些内容。这些对象在我的先前文章中有部分描述。对于跟踪和调试,我建议使用 TRACE、TRACE_ENTER 和 ASSERT 函数;**不要**使用 Debug.Write 和抛出异常——具体原因在之前的文章中有描述。此外,我建议阅读我之前关于线程的描述(因为我不想在这里重复)。我的实现方法与 Microsoft 的原生方法类似,所以我将只描述区别。好的,如果你关注了所有这些方面,我们就开始回顾吧。
过滤器注册
过滤器注册过程与 .NET COM 对象类似。过滤器 DLL **应该**签名。你可以将类型库作为非托管资源嵌入到 DLL 中,但这并非强制。在示例过滤器中,注册是自动执行的,请参阅 *install.bat* 和 *uninstall.bat* 文件以及生成后事件。你可以在项目设置中指定注册,但这可能不起作用。我让过滤器注册非常简单。为此,有一个类属性,带有一个不同的构造函数:
// Class declaration
[AttributeUsage(AttributeTargets.Class)]
public class AMovieSetup : Attribute 
//.............
// Constructors
public AMovieSetup() 
public AMovieSetup(bool _register) 
public AMovieSetup(string _name) 
public AMovieSetup(Merit _merit) 
public AMovieSetup(Merit _merit, Guid _category) 
public AMovieSetup(string _name, Merit _merit) 
public AMovieSetup(string _name, Merit _merit, Guid _category) 
该属性应指定为要注册为 DirectShow 过滤器的类。类应继承自任何基过滤器类,例如 BaseFilter、TransInPlaceFilter、TransformFilter 或 BaseSourceFilter。在属性中,你可以指定过滤器的 name、merit 和 category。如果未指定名称,则将使用过滤器类中指定的名称。默认 Merit 值为 *DoNotUse*,默认注册 category 为 *AmLegacyFiltersCategory*。过滤器的另一个**必需**属性是 **Guid**,它将是过滤器的 CLSID。过滤器 BaseFilter 类中的以下例程展示了如何执行注册和注销:
[ComRegisterFunctionAttribute]
public static void RegisterFunction(Type _type) 
 
[ComUnregisterFunctionAttribute] 
public static void UnregisterFunction(Type _type)
过滤器声明示例
// Here name not specified and will be used name setted in constructor 
// Category will be used by default AmLegacyFilters an merit - do not use 
[ComVisible(true)] 
[Guid("eeb3eef7-0592-491b-b7d4-8c65763c79c6")] // Filter CLSID 
[AMovieSetup(true)] // We should register 
public class NullInPlaceFilter : TransInPlaceFilter 
{ 
    public NullInPlaceFilter() 
    : base("CSharp Null InPlace Filter") {} // this name will be used 
//.............. 
}
与原生 BaseClasses 的另一个区别是引脚访问。用于 Pins 属性和初始化的是 OnInitializePins 例程,它在 BaseFilter 类中是抽象的。
protected abstract int OnInitializePins();
除了引脚初始化,你还可以通过以下辅助例程动态操作引脚:
public int AddPin(BasePin _pin) 
public int RemovePin(BasePin _pin) 
注册过滤器在 GraphEdit 中的样子

要创建自己的过滤器,只需继承自任何基过滤器类,指定必需的属性并添加功能即可。
自定义过滤器注册
在某些情况下,可能需要执行自定义过滤器注册,无论是否包含基础注册。为此,添加了一些方法。例如,你可以使用这些方法来为你的分离器/源过滤器注册文件扩展名。
protected virtual int BeforeInstall(ref RegFilter2 _reginfo,ref IFilterMapper2 _mapper2)
{
    return NOERROR;
}
protected virtual int AfterInstall(HRESULT hr,ref RegFilter2 _reginfo, ref IFilterMapper2 _mapper2)
{
    return NOERROR;
}
protected virtual int BeforeUninstall(ref IFilterMapper2 _mapper2)
{
    return NOERROR;
}
protected virtual int AfterUninstall(HRESULT hr, ref IFilterMapper2 _mapper2)
{
    return NOERROR;
}
你可以在注册过滤器之前重新配置 _reginfo 变量,并在 BeforeInstall 方法中为某些字段分配内存。并在 AfterInstall 方法中释放分配的内存。如果 BeforeInstall 返回失败 - 过滤器未注册,AfterInstall 不会被调用。AfterInstall 接收注册的 HRESULT 值,因此你可以检查过滤器是否成功注册并执行其他注册功能。
属性页
创建过滤器属性页也非常简单。你应该创建一个 Window Form 并让它继承自 BasePropertyPage 类,而不是 Windows.Form。你创建的窗体也需要 Guid 属性,因为这也是 COM 对象。BasePropertyPage 类具有与原生 BaseClasses 中的基类相同的可覆盖方法。要将你的属性页分配给过滤器,你应该在你的过滤器上指定一个属性。该属性看起来像这样:
[ComVisible(true)]
[AttributeUsage(AttributeTargets.Class)]
public class PropPageSetup : Attribute
//........
public PropPageSetup(Guid _guid)
public PropPageSetup(Guid _guid1,Guid _guid2)
public PropPageSetup(Guid _guid1,Guid _guid2,Guid _guid3)
public PropPageSetup(Type _type)
public PropPageSetup(Type _type1, Type _type2)
public PropPageSetup(Type _type1,Type _type2,Type _type3)
该属性有几个构造函数。指定属性页到过滤器的示例:
[ComVisible(true)]
[Guid("701F5A6E-CE48-4dd8-A619-3FEB42E4AC77")] 
[AMovieSetup(true)] 
[PropPageSetup(typeof(AudioChannelForm),typeof(AboutForm))] 
public class AudioChannelFilter : TransformFilter, IAudioChannel 
AudioChannelFilter 的属性页外观

文件分离器和文件源实现
在这一部分,我将简要概述类库,因为原生 BaseClasses 中没有类似的东西,但这可以帮助你实现现有格式的解复用器,甚至创建你自己的媒体文件格式。解复用器的基类是 BaseSplitterFilter 和 BaseFileSourceFilter,选择哪一个取决于你所需的功能。大多数情况下,你不需要修改这些基类。但是对于你的文件格式,你应该至少创建 2 个类,并让它们继承自 FileParser 和 DemuxTrack。
FileParser - 用于执行文件检查和初始化轨道的基类。在此,你应该重写至少两个方法。CheckFile - 在这里,你应该验证你是否可以解析给定的文件或流。**注意**,该方法在文件或流实现时都会被调用,如果是文件源,第一次调用时只初始化了 m_sFileName 变量,第二次(如果第一次失败)调用时则使用 m_Stream,这是一个 BitStreamReader 类的对象。另一个需要重写的方法是抽象的 LoadTracks - 在这里,你应该初始化轨道(继承自 DemuxTrack 的类)并将它们放入 m_Tracks 列表中。在此方法中,你还可以初始化文件持续时间(以纳秒为单位)(m_rtDuration 变量)。解析工作分为 2 种模式:单解析线程模型 - 存在一个主线程,并在其中执行解复用;多线程解析模型 - 在此,每个引脚负责解析数据。**注意**:访问位流以读取数据是线程安全的。要指定使用哪种模式,请在 FileParser 构造函数中指定布尔参数 - bRequireDemuxThread - true 表示单线程模型(默认)。关于这 2 种模式以及如何选择,我计划在另一篇文章中描述,这里只进行实现概述。
单线程模型 - 要使用此模型,你应该在 FileParser 类中重写另外 2 个方法。SeekToTime - 用于支持查找 - 在此方法中,你应该根据指定的时间设置读取位置。ProcessDemuxPackets - 用于解复用数据并将其放入轨道队列的主要方法。在此,你应该创建一个 PacketData 对象,用数据填充它,并通过调用 AddToCache 方法将其放入 DemuxTrack。**注意**:一旦队列已满,此方法将阻塞直到样本传递到过滤器。**注意**:如果存在关键的非阻塞来避免挂起,则 DemuxTrack 类中有一个 Reset 方法,并且所有线程都会因在 BaseSplitter 中设置退出事件而自动退出。但我建议你不要手动使用任何这些,因为这些都由类处理。
多线程模型 - 在此模型中,每个轨道读取和传递数据,并且可能不使用队列。要实现此模型,你应该在继承自 DemuxTrack 的类中重写至少 2 个方法:GetNextPacket - 返回填充的 PacketData 对象,或者在 EOS 的情况下返回 null;以及 SeekToTime - 与单模型中的目的相同,但在此处直接在每个轨道上执行查找。
DemuxTrack - 每个轨道的基类。你应该重写其抽象方法 GetMediaType。其他方法可以根据你选择的模型或特定的轨道实现进行重写。
PacketData - 用于指定轨道媒体数据的基类。它可以包含实际数据或指向文件位置的指针。无论如何,你可以为你自己的提供媒体数据的方式重写此类,在这种情况下,你应该重写 Dispose 方法来清理资源,以及可能在 DemuxTrack 类中重写 FillSampleBuffer 方法以从你的类填充媒体样本缓冲区。
PacketsQueue - 单线程模型中使用的样本队列类。该类允许从队列添加和删除 PacketData 对象,并发出队列已满或为空的信号。队列可以按时间戳对样本进行排序 - 当你在过滤器中执行解复用和解码时会用到此功能。默认缓存大小为 2 秒 - 在引脚激活后分配。
BitStreamReader - 用于从给定流读取数据的类。辅助类允许线程安全地按对象、位、字节或 golomb 值读取数据。
在示例中有 2 个过滤器向你展示了解复用器实现的基础知识:WAVESplitterFilter 和 WAVESourceFilter。但它极大地简化了创建你自己的类,我计划在后续文章中更详细地描述这些类,例如,我制作了一个 Windows Media Splitter,在下一张图中可以看到(是的,单个文件中存在 4 个流 - 只是与我自己的多路复用器一起操作 - 但分离器可以轻松处理)。

将过滤器参数保存在注册表中
为了更好地根据之前设置的参数配置过滤器,存在两个辅助方法。它们允许你从系统注册表中读取或写入字符串或数值参数。这些方法位于 BaseFilter 类中。
// Retrieve value from registry for current filter
protected object GetFilterRegistryValue(string _name,object _default)
// Set registry value for current filter
protected bool SetFilterRegistryValue(string _name,object _value)
参数存储在过滤器注册表子键下,因此是个人过滤器数据。**注意**:所有设置在过滤器注销时都会被删除。
将过滤器参数保存在图文件中
除了将参数保存在注册表中,过滤器还允许从保存的图中持久化保存和加载数据。每个过滤器都支持 IPersistStream 接口,并提供一些辅助方法来使用它。
// Set or clear flag that properties are modified
protected HRESULT SetDirty(bool bDirty)
// Get's the size of persistent data
protected virtual long SizeMax()
// Write filter properties to the stream
protected virtual HRESULT WriteToStream(Stream _stream)
// Read filter properties from stream
protected virtual HRESULT ReadFromStream(Stream _stream)
要标记持久化数据已修改,你应该使用 SetDirty 方法。加载和保存信息的参数是 .NET 类型 System.IO.Stream。**注意**:如果你不重写 SizeMax 方法,则流写入可能会执行两次:第一次计算最大输出大小,第二次实际写入数据。在你的过滤器实现中,你应该至少重写 WriteToStream 和 ReadFromStream 方法。
将过滤器嵌入到应用程序中
你可能不知道过滤器可以在应用程序内部创建并插入到过滤器图中。为此,过滤器不注册到注册表中,只有你的应用程序才能使用它。这也可以在 .NET 中实现。如果你的过滤器位于将注册为 COM 库的程序集中,则使你的过滤器带有 [ComVisible(false)] 属性,在应用程序中你可以不使用此属性。此外,我们不需要任何其他注册属性,因此可以删除所有这些,甚至 Guid 属性,因为过滤器注册将不会被调用。请记住,在这种情况下,持久化数据保存将无法工作,包括注册表值。如果需要属性页,它们应该注册为 COM,但我不能确定嵌入过滤器是否需要属性页,因为你可以在应用程序中手动配置所有设置。应用程序中过滤器声明的示例:
public class WAVESourceFilter : BaseSourceFilterTemplate<WaveParser>
{
    public WAVESourceFilter()
        : base("CSharp WAVE Source")
    {
    }
}
一旦过滤器类准备好,你就可以将其创建为特定的 .NET 对象。由于过滤器支持 IBaseFilter 接口,你可以将其插入到 Filter Graph 中并无问题地使用它。
public class WavPlayback : DSFilePlayback
{
    protected override HRESULT OnInitInterfaces()
    {
        // Create Filter
        DSBaseSourceFilter _source = new DSBaseSourceFilter(new WAVESourceFilter());
        // load the file
        _source.FileName = m_sFileName;
        // Add to the filter Graph
        _source.FilterGraph = m_GraphBuilder;
        // Render the output pin
        return _source.OutputPin.Render();
    }
}
在下载的示例应用程序中有 3 个示例(WavCapture、WavExtract 和 WavPlay),它们展示了如何在应用程序中使用嵌入的过滤器。
DirectShowNET 库
BaseClasses **不**使用 DirectShowNET 库,它包含一些相同的接口和结构,但封送方式不同。这是必要且非常重要的,因为在许多情况下,我们需要 IntPtr 而不是实际的接口,以避免我之前文章中描述的线程问题。通过我的魔法类(也来自之前的文章)VTableInterface 访问实际接口。我对其进行了一些修改并增加了更多功能。你也可以在过滤器中使用 DirectShowNET 库,但要记住可能出现的模糊问题。我将我的 directshow 辅助类(用于方便地构建过滤器图)作为单独的可下载内容放在本文中,而不是 DirectShowNET 库。
示例过滤器
NullInPlaceFilter
示例原地变换过滤器,它不修改数据,可以用作开发过滤器的起点。
NullTransformFilter
示例变换过滤器,它仅复制媒体样本而不进行修改。此示例可用作开发自己的变换过滤器的起点。
DumpFilter

示例过滤器,用于将传入数据写入指定文件。过滤器支持 IFileSinkFilter 和 IAMFilterMiscFlags。过滤器接受任何传入类型。
TextOverFilter

示例变换过滤器,演示如何在视频流上绘制文本。接受的媒体类型为 RGB32,过滤器不执行间距校正,将通过 ColorSpace Converter 连接到视频渲染器。
VideoRotationFilter

示例变换过滤器,将视频旋转 90 度。接受的媒体类型为 RGB24,过滤器不执行间距校正,将通过 ColorSpace Converter 连接到视频渲染器。
ImageSourceFilter

示例实现推送源过滤器,将加载的图像文件作为视频流提供。过滤器支持 IFileSourceFilter 接口。输出宽度和高度根据加载的图像分辨率设置。默认 FPS 为 20。输出媒体格式为 RGB32。过滤器不执行间距校正,将通过 ColorSpace Converter 连接到视频渲染器。
ScreenCaptureFilter

示例推送源过滤器,通过 GDI 提供当前桌面图像的副本。输出宽度和高度设置为 640x480,过滤器执行缩放以适应该分辨率。默认 FPS 为 20。输出媒体格式为 RGB32。过滤器不执行间距校正,将通过 ColorSpace Converter 连接到视频渲染器。
WavDestFilter

示例过滤器,它将音频流写入 WAV 文件。过滤器支持 IFileSinkFilter 和 IAMFilterMiscFlags。过滤器接受 PCM 媒体类型和 WaveFormatEx 格式类型。
AudioChannelFilter

示例变换过滤器,它将音频输出到指定的通道。过滤器支持 WAVE_FORMAT_PCM 格式的 PCM 输入和 8 或 16 位的 BitsPerSample。输出媒体格式为 WaveFormatExtensible,具有一个通道输出和扬声器配置。过滤器支持自定义接口,通过使用它可以指定输出通道。
public enum AudioChannel : int 
{ 
    FRONT_LEFT = 0x1, 
    FRONT_RIGHT = 0x2, 
    FRONT_CENTER = 0x4, 
    LOW_FREQUENCY = 0x8, 
    BACK_LEFT = 0x10, 
    BACK_RIGHT = 0x20, 
    SIDE_LEFT = 0x200, 
    SIDE_RIGHT = 0x400, 
} 
 
[ComVisible(true)] 
[System.Security.SuppressUnmanagedCodeSecurity, 
Guid("29D64CCD-D271-4390-8CF2-89D445E7814B"), 
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 
public interface IAudioChannel 
{ 
    [PreserveSig] 
    int put_ActiveChannel([In] AudioChannel _channel); 
 
    [PreserveSig] 
    int get_ActiveChannel([Out] out AudioChannel _channel); 
}
InfTeeFilter

示例类似于 Microsoft InfTee 示例 DirectShow 过滤器。
Null Renderer Filter

过滤器继承自 BaseRendererFilter,并且工作方式与标准的“Null Renderer”相同 - 通过丢弃所有接收到的样本而无需显示或渲染它们。
WAVE Splitter Filter

分离器过滤器示例,它解析 WAVE 文件数据并将其传递给下游过滤器。过滤器有一个输入引脚,并支持连接到带有 IAsyncReader 接口的引脚(例如“File Source Async”过滤器)。输出引脚在输入引脚连接后创建。过滤器不会自动注册使用,因此您需要手动构建图。
WAVE Source Filter

源过滤器示例,它解析 WAVE 文件数据并将其传递给下游过滤器。过滤器支持 IFileSourceFilter 并且没有输入引脚。输出引脚在输入引脚连接后创建。过滤器不会自动注册使用,因此您需要手动构建图。
Network Sync 和 Source Filters

示例是 2 个过滤器:发送方和接收方,它们将压缩为 JPEG 的视频数据进行组播。一个 Sync 过滤器可以连接到多个源过滤器。Sync 过滤器继承自 BaseRendrerFilter 并支持 RGB24 输入。一旦图变为活动状态,sync 过滤器就开始组播传入的样本。可以通过 INetworkConfig 接口配置组播设置。
[ComVisible(true)]
[System.Security.SuppressUnmanagedCodeSecurity]
[Guid("96D8A0B7-8EE7-4325-98D4-BB7C66F27B1A")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface INetworkConfig
{
    string IP { get; set; }
    int Port { get; set; }
}
该接口允许指定组播 IP 地址和端口。在 GraphEdit 中,你可以使用属性页进行此操作。

源过滤器一旦添加到过滤器图中,就会在一个单独的线程中开始等待样本。仅当至少一个样本被接收后,输出引脚上的 MediaType 才可用。过滤器还支持 INetworkConfig 接口。**注意**,由于要发送的数据可能很大且在系统级别不受支持,过滤器可能无法在高分辨率下工作。
高级示例
我将作为单独的文章发布更多示例过滤器,因为它们需要审查代码。目前可用的示例包括:
- C# 中的 H.264 CUDA 编码器 DirectShow 过滤器
- C# 中的 DirectShow 虚拟视频捕获源过滤器
- 用 C# 编写 DirectShow 解复用器。第 1 部分 - Windows Media Splitter 示例。
历史
- 2012-07-13 - 初始版本。
- 2012-07-15 - 添加了属性页、OutputQueue 和 InfTeeFilter 示例。
- 2012-08-09 - 进行了一些改进。
- 2012-08-14 - 实现了 BaseRenderer、BaseSplitter 和 BaseFileSource,并添加了几个示例。
- 2012-09-19 - 添加了应用程序示例。
- 2012-10-13 - 解决了 .NET Framework 2.0 中丢失方法的问题。修改了类库以解决其他一些问题。


