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

SQL Server Reporting Services 2005/2008/2012 的 Zip 渲染扩展

starIconstarIconstarIconstarIconstarIcon

5.00/5 (20投票s)

2012 年 11 月 12 日

CPOL

13分钟阅读

viewsIcon

219567

downloadIcon

3953

如何创建和部署 SSRS 渲染扩展,通过一个功能性的 Zip 渲染扩展来解释 SSRS 2005、2008 (R2) 和 2012。

简介     

在 Reporting Services 的报表管理器中,您可以将报表导出为 Excel、CSV、PDF 等格式。但如果您想为每个 PDF 添加水印呢?或者,将导出的 Excel 的工作表名称从“Sheet2”更改为更有意义的名称?或者,对现有(内置)渲染扩展进行任何类型的处理呢?

本文介绍了如何创建一个 SQL Server Reporting Services (SSRS) 渲染扩展,该扩展可以在报表发送给客户端之前更改其他渲染器的输出——在本例中,它将一个或多个报表压缩到 .zip 文件中。  

此外,还有关于部署和故障排除的通用说明,这些说明对于任何渲染扩展都很有用。我已尽力使内容清晰完整。关于部署的一些文档*确实*存在,但散布在各处(尤其是在 MSDN 上)。本文应能帮助任何人开始使用渲染扩展。  

目录

MSDN 链接列表

背景

在我参与的一个项目中,我创建了一个包含数十兆字节数据的报表。该报表后来被安排每天通过电子邮件发送。这当然太大,不适合通过电子邮件发送,但如果您想将数据发送给客户,通常别无选择。

因此,我开始寻找更小的替代方案。最小的现有导出是 .CSV,但是您只能将结构化为表的导出数据导出到 .CSV 文件中。.CSV 处理起来也不实用,除非您设置了自动数据传输。

然后,我读到了我的同胞撰写的关于渲染扩展的这篇优秀文章:http://www.broes.nl/2011/02/pdf-watermarkbackground-rendering-extension-for-ssrs-part-1/ 这篇文章详细介绍了如何修改导出的 PDF(添加水印)。

因此,我想到,如果您可以修改 PDF,当然也可以将输出内容打包成 zip 文件并返回一个 zip 文件?这个 ZipRenderer 的基本思想是一样的:获取对现有渲染器的引用,让它渲染一个报表。获取输出并对其进行处理。

尽管 broes.nl 上的文章是一个很好的开始(我强烈建议您也阅读它),但仍有许多东西需要弄清楚。例如,2005 年可用的 PdfRenderer 从 2008 年起就不再可用了,所以文章中的方法也不再起作用了。

使用代码

这部分是关于部署的;下面的步骤适用于任何渲染扩展。(不包括那些显然与 ZipRenderer 相关的内容)

构建和部署扩展 

打开适用于您环境的相应项目。2012 年的项目可用于 SSRS 2008 和 2012。由于这是一个普通的 .NET 程序集项目,因此不需要 BIDS。   

  1. 添加对以下项的引用

    Microsoft.ReportingServices.ExcelRendering.dll
    Microsoft.ReportingServices.ProcessingCore.dll
    Microsoft.ReportingServices.Interfaces.dll

    SSRS 2005 项目还需要
    Microsoft.ReportingServices.CsvRendering.dll
    Microsoft.ReportingServices.ImageRendering.dll 

    所有这些都在 SQL Server 机器上的 [SQL Reporting Services 文件夹]\ReportServer\Bin\ 中。我发现最简单的方法是将该 bin 文件夹的全部内容复制到开发机器上,以便进行研究和将来在扩展中使用。

    ExcelRendering 和 ProcessingCore 会因 .NET 版本较新而发出警告,您可以安全地忽略它。(请参阅 http://support.microsoft.com/kb/2722683

  2. 为了实现 zip 功能,我们需要 DotNetZip 库:http://dotnetzip.codeplex.com/releases 下载最新的 Runtime,从 bin/Release 文件夹之一中提取 Ionic.Zip.dll(zip-v#.# 包含普通的 Zip 压缩),然后添加对 Ionic.Zip.dll 的引用。
  3. 构建项目。
  4. 将 ZipRenderer.dll 和 Ionic.Zip.dll 从输出文件夹移动到 [SQL Reporting Services 文件夹]\ReportServer\Bin\。
  5. 您可能会注意到输出文件夹中还有很多其他的 dll。这些都与报表服务器相关,不需要复制到报表服务器。(实际上,它们最初就源自那里)。

服务器配置 

现在需要进行一些配置,以便我们的扩展能够显示在报表管理器中。多亏了这篇文章,这并不难。 http://www.broes.nl/2011/02/pdf-watermark-background-rendering-extension-for-ssrs-part-2/

  1. 将以下内容添加到 [SQL Reporting Services 文件夹]\ReportServer\rsreportserver.config 中,位于 </Render> 标签的正上方
    <Extension Name="ZIPEXCEL" Type="ZipRenderer.ZipRenderingProvider,ZipRenderer">
        <Configuration>
            <DeviceInfo>
                <ZipRenderer description ="Zipped Excel">
                    <SubRenderers>
                        <SubRenderer extension="xls" format="EXCEL" />
                    </SubRenderers>
                </ZipRenderer>
            </DeviceInfo>
        </Configuration>
    </Extension>

    (请注意 <DeviceInfo> 内的自定义标签。

  2. 并将以下内容添加到 [SQL Reporting Services 文件夹]\ReportServer\rssrvpolicy.config: 
  3. <CodeGroup
        class="UnionCodeGroup"
        version="1"
        PermissionSetName="FullTrust"
        Name="Zip Renderer"
        Description="This code group grants Zip Renderer code full trust.">
    <IMembershipCondition
       class="UrlMembershipCondition"
       version="1"
       Url="[SQL Reporting Services folder]\ReportServer\bin\ZipRenderer.dll" />
    </CodeGroup>

    将此 XML 放在配置文件中,具体说明请参见 MSDN

    对于您开发的扩展和自定义程序集,建议将您的自定义代码组直接放在 URL 成员关系 "$CodeGen$/*" 的现有条目下方。

    这是一个 UrlMembershipCondition,它使用 DLL 的路径或 URL。请记住将上面的 Url 更改为指向 ZipRenderer.dll 的正确路径!

    您也可以使用 StrongNameMembershipCondition,请再次查看 Broes 的文章第 2 部分。这需要更多的操作才能设置好。如果您想以这种方式部署您的程序集,请查看那里。 

  4. 最后,重新启动 Reporting Services 服务以应用对 .config 文件的更改。

    *较新版本的 SSRS 会检测到对配置文件所做的更改,并自动重新启动/重新配置服务。这在应用程序事件日志中会显示为一条信息事件,例如“RSReportServer.config 文件已被修改”。

故障排除

现在,如果一切设置正确,您应该会在运行报表时在导出下拉菜单中看到名为“Zipped excel”的扩展。如果情况是这样,恭喜您,您已成功部署了该扩展。单击导出链接,您将下载一个包含 .xls 文件的 zip 文件。

现在,很多事情都可能出错——我认为我都遇到了——如果它没有显示或不起作用,这里有一些提示:

  • 如果扩展根本不显示,最可能的原因是配置文件未正确修改(指向 ZipRenderer.dll 的路径错误)、文件未保存到磁盘或未应用。确保它们已保存,然后重新启动 SQL Server Reporting Services。在 SSRS 2005 中,重新启动 IIS 中的整个网站可能也有帮助。
  • 还请检查应用程序事件日志以获取错误消息,例如“报表服务器(MS SQL Server)无法加载 ZIPEXCEL 扩展。”,这将为您指明正确的方向。在错误事件附近的 ASP.NET 警告事件的堆栈跟踪中可能包含有用信息。
  • 检查 [Reporting Services 文件夹]\LogFiles 中的最新日志文件。按修改日期排序并打开最新的日志文件。然后向下滚动并查找堆栈跟踪。
  • 如果选择 ZipExcel 时出现服务器错误页面,您也可以在页面本身中找到详细信息。如果未显示错误详细信息,请在服务器上打开报表页面以查看错误详细信息。  

调试  

如果您在 SSRS 计算机上运行 Visual Studio,则调试非常简单,但这在 MSDN 上并未记录。(页面 Debugging Delivery Extensions 最接近)   

首先,将 ZipRenderer.pdb 文件复制到服务器。否则,调试器将抱怨符号未加载。要开始调试:  

  • 以管理员身份运行 Visual Studio 并打开项目  
  • 选择“工具”->“附加到进程”。
  • 如果需要,请选中“显示所有用户的进程”和“显示所有会话的进程” 
  • 找到并选择托管 ZipRenderer DLL 的可执行文件  
    • SSRS 2008+:ReportServicesService.exe
    • IIS6:w3wp.exe
    • IIS7:aspnet_wp.exe
  • 附加。

如果您无法在服务器上运行 Visual Studio,则可以使用远程调试。MSDN 上对此进行了说明: 远程调试 设置。这需要多少工作取决于您的服务器配置。我将其留给其他专门的文章来解释。 

兴趣点 

现在……终于到了项目中的所有精巧代码部分了。代码中实际的 zip 过程相当小——关键在于获取渲染器和正确的数据流。

内置的 ExcelRenderer

问题 1:读取内置 ExcelRenderer 的输出 

起初我最关心的问题之一是如何获取其他内置渲染器(尤其是 ExcelRenderer)的输出。在 SSRS 2005、2008 和 2012 中,Excel 渲染器在 Microsoft.ReportingServices.ExcelRendering 命名空间中是公开的。

一切顺利,您可以将 ExcelRenderer 添加到您的引用中,实例化它并让它渲染一些报表。但是:ExcelRenderer 的行为不佳;它会关闭 CreateAndRegisterStream 回调函数提供的 MemoryStream。无法获取结果。即使对 MemoryStream 调用 ToArray() 也无效:只返回 Excel 文件的一部分,其余部分被截断,因此如果您尝试写入它,最终会得到一个损坏的文件。

我不是 Stream 专家,但我认为它之所以这样行为,是因为 ExcelRenderer 调用 CreateAndRegisterStream 多次;每次调用一次用于每个工作表或嵌入的图像。

解决方案:UnclosableMemoryStream

在花费了很多时间之后,我决定直接继承 MemoryStream 来阻止 ExcelRenderer 关闭*我的* MemoryStreams。一个简单的解决方案,但效果很好!

class UnclosableMemoryStream: MemoryStream
{
    private bool allowClose; 
    public bool AllowClose
    {
        get { return allowClose; }
        set { allowClose = value; }
    }
    public override void Close()
    {
        if (AllowClose)
            base.Close();
    }
}

此流在您允许之前不会关闭。因此,在准备好此类后,您可以将其实例传递给 CreateAndRegisterStream 回调函数中的 ExcelRenderer,并且以后仍然可以读取内容。

问题 2:多个流

解决了 MemoryStream 问题后,还需要处理 ExcelRenderer 所需的多个流。

渲染器的 CreateAndRegisterStream 仅被调用一次,其参数值为 StreamOper.CreateAndRegister。这是最终包含所有 Excel 内容的主 Stream,所以我们将它存储为 RegisteredStream

下面是 CreateAndRegisterStream 的 Excel 版本。

(注意:实际的 UnclosableMemoryStream 实例在包装类 CreateAndRegisterStreamUnclosableMemoryStream 中创建。此类还存储 CreateAndRegisterStream 调用所需的参数,供以后在 Render 方法中使用。)

// Intermediate CreateAndRegisterStream method that matches the delegate
// Microsoft.ReportingServices.Interfaces.CreateAndRegisterStream
// It will return a reference to a new MemoryStream, so we can get to
// the results of the intermediate render-step later.
public Stream IntermediateCreateAndRegisterStreamExcel(
    string name,
    string extension,
    Encoding encoding,
    string mimeType,
    bool willSeek,
    StreamOper operation)
{
    //Create a stream container and store every parameter in it
    CreateAndRegisterStreamStream crss = 
      new CreateAndRegisterStreamUnclosableMemoryStream(
      name, extension, encoding, mimeType, willSeek, operation);
    //Store stream container
    intermediateStreams.Add(crss);
    if (operation == StreamOper.CreateAndRegister)
        //Create the main stream. Contents of this stream are returned later
        RegisteredStream = crss.Stream;
    
    return crss.Stream;
} 

来自配置文件的自定义 DeviceInfo

您可能已经注意到,包括现有渲染器在内的所有渲染器都可以从 rsreportserver.config 进行配置。读取您自己渲染器配置并应用它的地方是在 IExtension.SetConfiguration 中: 

/// <summary>
/// Process XML data stored in the configuration file
/// </summary>
/// <param name="configuration">The XML string from the configuration file that contains extension configuration data.</param>
void IExtension.SetConfiguration(string configuration) 

参数“configuration”包含 rsreportserver.config 中 <Configuration> 标签的 Inner XML。

IRenderingExtension.Render 方法还有一个名为 deviceInfo 的配置参数,它是一个 NameValueCollection。DeviceInfo 也是 rsreportserver.config 中的一个配置项,但更简化:它包含 <DeviceInfo> 的每个直接子项及其 XmlNode.InnerText 作为值。

有关 DeviceInfo 的更多信息,请参阅 MSDN 此处此处  
更新:增加了对 zip 模块设置的 deviceInfo 参数的支持。请参阅本章下面的章节。 

对于 zip 渲染器,我设置了一个配置格式,允许将多个 SubRenderer(我称之为它们)包装到 zip 文件中,并且它们可以拥有自己独立的 DeviceInfo 配置。每个 <SubRenderer> 的内容会原样传递给它们各自的 Renderer 对象,并用作配置字符串。

一个配置示例

  <Extension Name="ZIPEXCELPDF" Type="ZipRenderer.ZipRenderingProvider, ZipRenderer"> 
    <Configuration> 
      <DeviceInfo> 
        <ZipRenderer description="Zipped Excel + PDF"> 
          <SubRenderers>
            <SubRenderer extension="xls" format="EXCEL" />
            <SubRenderer extension="pdf" format="PDF">
              <DeviceInfo>
                <StartPage>3</StartPage>
                <EndPage>4</EndPage>
              </DeviceInfo>
            </SubRenderer>
          </SubRenderers>
        </ZipRenderer>
      </DeviceInfo>
    </Configuration>
  </Extension> 

以下是它的解释方式

  • description="Zipped Excel + PDF" 是您在报表管理器导出列表中看到的名称
  • <Subrenderers> 告诉扩展使用以下 SubRenderer
    • SubRenderer: format EXCEL: 使用 ExcelRenderer,使用“xls”作为文件扩展名
      • (无附加 DeviceInfo)
    • SubRenderer: format PDF: 使用 PdfRenderer,使用“pdf”作为文件扩展名
      • 调用 pdfrenderer 时添加附加 DeviceInfo:从第 3 页开始,渲染到第 4 页(是的,我知道,这很荒谬,但仅用于演示)

解析 XML 的代码

/// <summary>
/// Process XML data stored in the configuration file
/// </summary>
/// <param name="configuration">The XML string from the configuration file that contains extension configuration data.</param>
void IExtension.SetConfiguration(string configuration)
{
    this.configuration = configuration;
    // Create the document and load the Configuration element    
    XmlDocument doc = new XmlDocument();
    try
    {
        doc.LoadXml(configuration);
        //Check for the DeviceInfo element
        if (doc.DocumentElement.Name == "DeviceInfo")
        {
            //Find the ZipRenderer node
            XmlNode zipRendererNode = doc.DocumentElement.SelectSingleNode("ZipRenderer");
            if (zipRendererNode == null)
                throw new System.Configuration.ConfigurationErrorsException(
                    "Missing ZipRenderer node in configuration", doc.DocumentElement);
            //Read this extension's description
            description = zipRendererNode.Attributes["description"].Value;
            //Read all of the SubRenderers configured
            subRenderers = new List<SubRenderer>();
            foreach (XmlNode zippedReportNode in zipRendererNode.SelectNodes("SubRenderers/SubRenderer"))
            {
                try
                {
                    //Try and create a SubRenderer
                    subRenderers.Add(SubRenderer.CreateSubRenderer(
                        zippedReportNode.Attributes["format"].Value,
                        zippedReportNode.Attributes["extension"].Value,
                        zippedReportNode.InnerXml));
                }
                catch (System.Configuration.ConfigurationErrorsException ex)
                {
                    //An invalid SubRenderer format was used.
                    throw new System.Configuration.ConfigurationErrorsException(
                        String.Format("The SubReport format {0} could not be found", 
                                      zippedReportNode.Attributes["format"].Value), 
                        ex, zippedReportNode);
                    //Tried this but fails in 2008 / 2012. The ZipRenderer
                    // and the ReportViewer renderer will keep waiting for each other. Deadlock.
                    //TODO: Replace by WebServiceSubRenderer.
                    //subRenderers.Add(new ReportViewerSubRenderer(
                    //        zippedReportNode.Attributes["format"].Value));
                }
            }
        }
    }
    catch (XmlException ex)
    {
        throw new System.Configuration.ConfigurationErrorsException(
                    "Failed to read configuration data: " + ex.Message, ex);
    }
}  

实际的 SubRenderer 是在 SubRenderer.CreateSubRenderer(…) 中确定的。

请注意,SubRenderer 节点内的 InnerXml 会原样传递给 SubRenderer;这可能包含额外的 DeviceInfoSubRenderer 负责解析自己的配置字符串。

更新:zip 过程的 DeviceInfo   

根据读者的建议,包含的 ziprenderer 现在还支持 Ionic.zip 的以下设置:

CompressionLevelCompressionMethod、 Strategy、 Comment、 EnableZip64、 IgnoreCase、 EncryptionPassword
请关注链接以了解每个配置项的可用值,或查看 Ionic.Zip Zip 参考
配置项不是必需的,但是如果您启用 Encryption,则还必须输入 Password 值。

配置示例:  

<Extension Name="ZIPEXCELOPENXML" Type="ZipRenderer.ZipRenderingProvider,ZipRenderer">
    <Configuration>
        <DeviceInfo>
            <CompressionLevel>BestCompression</CompressionLevel>
            <Encryption>WinZipAes256</Encryption>
            <Password>testpass</Password>
            <ZipRenderer description ="Zipped Excel 2012">
                <SubRenderers>
                    <SubRenderer extension="xlsx" format="EXCELOPENXML">
                    </SubRenderer>
                </SubRenderers>
            </ZipRenderer>
        </DeviceInfo>
    </Configuration>
</Extension>

读取设置的代码(在 ZipRenderingProvider.Render 中)

(...)
//Create a Zip output and tell it to keep the provided stream open - we use it outside the using clause
using (ZipOutputStream zipOutput = new ZipOutputStream(outputMemoryStream, true))
{       
    //Read zip deviceinfo
    try 
    {
        if (deviceInfo["CompressionLevel"] != null)
            zipOutput.CompressionLevel = (Ionic.Zlib.CompressionLevel)Enum.Parse(
              typeof(Ionic.Zlib.CompressionLevel), deviceInfo["CompressionLevel"], true);
        if (deviceInfo["CompressionMethod"] != null)
            zipOutput.CompressionMethod = (Ionic.Zip.CompressionMethod)Enum.Parse(
              typeof(Ionic.Zip.CompressionMethod), deviceInfo["CompressionMethod"], true);
        if (deviceInfo["Strategy"] != null)
            zipOutput.Strategy = (Ionic.Zlib.CompressionStrategy)Enum.Parse(
              typeof(Ionic.Zlib.CompressionStrategy), deviceInfo["Strategy"], true);
        if (deviceInfo["Comment"] != null)
            zipOutput.Comment = deviceInfo["Comment"];
        if (deviceInfo["EnableZip64"] != null)
            zipOutput.EnableZip64 = (Ionic.Zip.Zip64Option)Enum.Parse(
              typeof(Ionic.Zip.Zip64Option), deviceInfo["EnableZip64"], true);
        if (deviceInfo["IgnoreCase"] != null)
            zipOutput.IgnoreCase = Boolean.Parse(deviceInfo["IgnoreCase"]);
        if (deviceInfo["Encryption"] != null)
            zipOutput.Encryption = (Ionic.Zip.EncryptionAlgorithm)Enum.Parse(
              typeof(Ionic.Zip.EncryptionAlgorithm), deviceInfo["Encryption"], true);
        if (deviceInfo["Password"] != null)
            zipOutput.Password = deviceInfo["Password"];
    } 
    catch (Exception ex)
    {
        throw new System.Configuration.ConfigurationErrorsException(
          "Invalid DeviceInfo configuration value", ex);
    }

    foreach (SubRenderer sr in subRenderers)
    {<span style="font-size: 9pt;">
(...)</span>

SSRS 2008 R2 / 2012 中的内置 PDFRenderer

在 SSRS 2005 中,您可以像使用 ExcelRenderer 一样使用内置的 PdfRenderer,因为它也是公开的。

但在 SSRS 2008 (R2) / 2012 中就没那么幸运了。Microsoft 已将 PdfRenderer 设为内部密封类,因此您必须诉诸反射方法才能访问它。是的,您没听错。以下是执行此操作所需的代码。

// Initialize the PDF renderer. it is an internal sealed class but we can still get to it using..Reflection! 
if (pdfRendererType == null)
{
    //1. Load the ImageRendering Assembly
    //Use a disassembler tool like ILSpy to find The AssemblyName, type and constructor
    //methods for other internal renderers in their respective .dlls
    //This is code for the Sql Server 2012 assembly.
    //Replace Version=11.0.0.0 by Version=10.0.0.0 and you are good to go for 2008.
    Assembly IR = Assembly.Load(new AssemblyName("Microsoft.ReportingServices.ImageRendering, 
                    Version=11.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91"));
    //2. Read the PdfRenderer type from the Assembly
    pdfRendererType = IR.GetType("Microsoft.ReportingServices.Rendering.ImageRenderer.PDFRenderer");
}
//3. Create an instance of type PdfRenderer. PdfRenderer inherits from
// IRenderingExtension which is a public interface so cast it to IRenderingExtension
renderer = (IRenderingExtension)pdfRendererType.GetConstructor(BindingFlags.Public | 
             BindingFlags.Instance, null, Type.EmptyTypes, null).Invoke(null);
                    
// /Reflection
// phew..  

这里的诀窍是,最终 pdfRenderer 被强制转换为接口 IRenderingExtension。除非您想使用 pdfRenderer 特有的函数,否则反射到此为止。这很好。  

有了这段代码,在 SSRS 2008/2012 中实现 PDF 水印就成为可能!  

通用 SubRenderer  

对于每个内置渲染器,我们需要获取对 SSRS 渲染器类的引用。此外,还有各种关于流的问题需要解决。您可能会想:难道不能告诉报表服务器创建报表,然后将其取回吗? 

嗯,我们可以:通过调用 ReportExecutionService 或报表服务器 URL 访问。但是,服务器已经在 IRenderingExtension.Render 方法的 report 参数中为我们提供了报表数据。什么都不做而重新运行整个报表,岂不是一种浪费? 

ReportViewer 来拯救(还是不救?)  

您可能已经在报表管理器中注意到,导出报表时没有延迟。这是因为它没有重新运行整个报表:它将 SessionID 传递给 SSRS 引擎。这样 SSRS 就知道它需要重用已显示的数据,并只需以不同方式进行渲染。  这在代码中也是可能的,请参阅 ReportViewerSubRenderer 类(修改自此博客评论)。它会生成一个 HttpWebRequest,如下所示:  

/Reports/Reserved.ReportViewerWebControl.axd?ReportSession=4obc0z550
  su1si2em2oxv445&Culture=1043&CultureOverrides=False&UICulture=9&UICultureOverrides=
  False&ReportStack=1&ControlID=ab4010a279a44dffbe1b15e6c7182d7b&OpType=Export&
  FileName=Report1&ContentDisposition=OnlyHtmlInline&Format=EXCEL

有一个问题。其实有两个

在订阅中,没有可用的 SessionID。没有 SessionID,ReportViewerWebControl 无法用于渲染报表,它不知道要渲染哪个报表…… 

而且,虽然这在 SSRS 2005 中(从报表管理器)起作用,但在 SSRS 2008 中,引擎似乎变得更智能了,并且会阻止对 ReportViewerWebControl 的附加调用;上述请求将超时。我假设这是为了防止重复请求,以保护服务器资源。  

这是一张图表来说明这一点——请记住,这都是基于观察,并且可能不反映实际  过程。

解决方案 

如果您将 Web 服务/URL 访问与报表数据缓存一起使用,它仍然可能是一个可行的选择。只需将缓存过期时间设置为 5 分钟之类的短时间,足以让报表数据被存储和重用。只需继承 SubRenderer 并在 CreateSubRenderer 中指向您的类。——我将来可能会自己添加它。 

除此之外,内置渲染器是最有效的解决方案。您只需单独编码每个渲染器。

如果我们可以像在Report.Render(format, deviceInfo) 方法中访问交付扩展一样,一切都会容易得多…… 

历史

  • 2012 年 11 月 11 日 v1.0:发布第一篇文章! 
  • 2012 年 1 月 26 日 v1.1:增加了对 CompressionLevel 等 Zip 选项的支持 
© . All rights reserved.