3

我有一个视图,它返回一个包含多个页面的 pdf(使用 iTextSharp),但现在我必须更改它,以便每个页面都是一个单独的 pdf(具有它自己的唯一标题)并返回一个 zip 文件。

我的原始代码如下所示:

public FileStreamResult DownloadPDF()
{
    MemoryStream workStream = new MemoryStream();
    Document document = new Document();
    PdfWriter.GetInstance(document, workStream).CloseStream = false;
    document.Open();

    // Populate pdf items

    document.Close();

    byte[] byteInfo = workStream.ToArray();
    workStream.Write(byteInfo, 0, byteInfo.Length);
    workStream.Position = 0;

    FileStreamResult fileResult = new FileStreamResult(workStream, "application/pdf");
    fileResult.FileDownloadName = "fileName";

    return fileResult;
}

使用 gzip 压缩文件看起来很简单,但我不知道如何 gzip 多个文件并将其作为一个 zip 文件返回。或者我应该使用 gzip 以外的东西,比如 dotnetzip 或 sharpzip?

提前致谢!

4

4 回答 4

11

如果您的解决方案有效,那么最简单的方法就是保持原样。

另一方面,我确实对您使用 DoTNetZip 库有一些评论。

首先,您的代码有点误导。在这个部分:

byte[] byteInfo = workStream.ToArray();                        

zip.Save(workStream);                        

workStream.Write(byteInfo, 0, byteInfo.Length);                        
workStream.Position = 0;                        

...您正在将 workStream 读入一个数组。但是此时,您还没有向 workStream 写入任何内容,因此该数组是空的,长度为零。然后将 zip 保存到工作流中。然后将数组(长度为零)写入同一个工作流。这是一个 NO-OP。最后你重置位置。

您可以将所有这些替换为:

zip.Save(workStream);                        
workStream.Position = 0;                        

这不是 DotNetZip 本身的问题,这只是您对流操作的误解。

好的,接下来,您将不必要地分配临时缓冲区(内存流)。将 MemoryStream 视为只是一个字节数组,上面有一个 Stream 包装器,以支持 Write()、Read()、Seek() 等。本质上,您的代码是将数据写入该临时缓冲区,然后告诉 DotNetZip 将临时缓冲区中的数据读取到其自己的缓冲区中以进行压缩。你不需要那个临时缓冲区。它按照您的方式工作,但它可能更有效。

DotNetZip 有一个AddEntry()接受写入委托的重载。委托是 DotNetZip 调用的一个函数,用于告诉您的应用程序将条目内容写入 zip 存档。您的代码写入未压缩的字节,DotNetZip 压缩并将它们写入输出流。

在该编写器委托中,您的代码直接写入 DotNetZip 流 - 由 DotNetZip 传递给委托的流。没有中间缓冲。很好的效率。

请记住有关关闭的规则。如果您在 for 循环中调用此 writer 委托,则需要有一种方法来检索与委托中的 zipentry 对应的“bla”。委托在被调用之前不会被执行zip.Save()!所以你不能依赖循环中'bla'的值。

public FileStreamResult DownloadPDF() 
{ 
    MemoryStream workStream = new MemoryStream(); 
    using(var zip = new ZipFile()) 
    {
        foreach(Bla bla in Blas) 
        { 
            zip.AddEntry(bla.filename + ".pdf", (name,stream) => {
                    var thisBla = GetBlaFromName(name);
                    Document document = new Document(); 
                    PdfWriter.GetInstance(document, stream).CloseStream = false; 

                    document.Open(); 

                    // write PDF Content for thisBla into stream/PdfWriter 

                    document.Close(); 
                });
        } 

        zip.Save(workStream); 
    }
    workStream.Position = 0; 

    FileStreamResult fileResult = new FileStreamResult(workStream, System.Net.Mime.MediaTypeNames.Application.Zip); 
    fileResult.FileDownloadName = "MultiplePDFs.zip"; 

    return fileResult; 
}

最后,我不是特别喜欢你FileStreamResult从 a 中创建的 a MemoryStream。问题是您的整个 zip 文件都保存在内存中,这对内存使用非常不利。如果您的 zip 文件很大,您的代码会将所有内容保留在内存中。

我对 MVC3 模型知之甚少,无法知道其中是否有对此有所帮助的东西。如果没有,您可以使用匿名管道来反转流的方向,并且无需将所有压缩数据保存在内存中。

这就是我的意思:创建一个FileStreamResult要求您提供一个可读流。如果您使用 MemoryStream,为了使其可读,您需要先对其进行写入,然后返回位置 0,然后再将其传递给FileStreamResult构造函数。这意味着该 zip 文件的所有内容必须在某个时间点连续保存在内存中。

假设您可以向FileStreamResult构造函数提供一个可读流,这将允许读者在您写入它的那一刻准确地阅读。这就是匿名管道流的作用。它允许您的代码使用可写流,而 MVC 代码获取其可读流。

这是它在代码中的样子。

static Stream GetPipedStream(Action<Stream> writeAction) 
{ 
    AnonymousPipeServerStream pipeServer = new AnonymousPipeServerStream(); 
    ThreadPool.QueueUserWorkItem(s => 
    { 
        using (pipeServer) 
        { 
            writeAction(pipeServer); 
            pipeServer.WaitForPipeDrain(); 
        } 
    }); 
    return new AnonymousPipeClientStream(pipeServer.GetClientHandleAsString()); 
} 


public FileStreamResult DownloadPDF() 
{
    var readable = 
        GetPipedStream(output => { 

            using(var zip = new ZipFile()) 
            {
                foreach(Bla bla in Blas) 
                { 
                    zip.AddEntry(bla.filename + ".pdf", (name,stream) => {
                        var thisBla = GetBlaFromName(name);
                        Document document = new Document(); 
                        PdfWriter.GetInstance(document, stream).CloseStream = false; 

                        document.Open(); 

                        // write PDF Content for thisBla to PdfWriter

                        document.Close(); 
                    });
                } 

                zip.Save(output); 
            }
        }); 

    var fileResult = new FileStreamResult(readable, System.Net.Mime.MediaTypeNames.Application.Zip); 
    fileResult.FileDownloadName = "MultiplePDFs.zip"; 

    return fileResult; 
}

我还没有尝试过,但它应该可以工作。与您编写的内容相比,这有一个优势,即内存效率更高。缺点是它相当复杂,使用命名管道和几个匿名函数。

仅当 zip 内容在 >1MB 范围内时,这才有意义。如果你的拉链比那个小,那么你可以按照我上面展示的第一种方式来做。


附录

为什么不能依赖bla匿名方法内的值?

有两个关键点。首先,foreach 循环定义了一个名为 的变量bla,它在每次循环中都取不同的值。看起来很明显,但值得明确说明。

其次,匿名方法作为参数传递给该ZipFile.AddEntry()方法,并且不会在 foreach 循环运行时运行。事实上,匿名方法被重复调用,每次添加的条目一次,在 ZipFile.Save(). bla如果您在匿名方法中引用,它将获得分配给的最后一个值bla,因为该值blaZipFile.Save()运行时保持不变。

造成困难的是延迟执行。

您想要的是bla在调用匿名函数时可以访问 foreach 循环中的每个不同值 - 稍后,在 foreach 循环之外。您可以使用实用方法 ( GetBlaForName()) 来做到这一点,就像我在上面展示的那样。您也可以使用额外的闭包来执行此操作,如下所示:

Action<String,Stream> GetEntryWriter(Bla bla)
{
   return new Action<String,Stream>((name,stream) => {
     Document document = new Document();  
     PdfWriter.GetInstance(document, stream).CloseStream = false;  

     document.Open();  

     // write PDF Content for bla to PdfWriter 

     document.Close();  
  };
}

foreach(var bla in Blas)
{
  zip.AddEntry(bla.filename + ".pdf", GetEntryWriter(bla));
}

GetEntryWriter返回一个方法——实际上是一个动作,它只是一个类型化的方法。每次循环时,都会创建该 Action 的一个新实例,并且它为 bla 引用不同的值。该动作直到 时才被调用ZipFile.Save()

于 2012-06-05T03:20:50.460 回答
3

我最终使用DotNetZip而不是 SharpZipLib,因为解决方案更简单。这是我最终做的事情,它工作正常,但是如果有人有任何建议/更改,我很乐意在这里。

public FileStreamResult DownloadPDF()
{
    MemoryStream workStream = new MemoryStream();
    ZipFile zip = new ZipFile();

    foreach(Bla bla in Blas)
    {
        MemoryStream pdfStream = new MemoryStream();
        Document document = new Document();
        PdfWriter.GetInstance(document, pdfStream).CloseStream = false;

        document.Open();

        // PDF Content

        document.Close();
        byte[] pdfByteInfo = pdfStream.ToArray();
        zip.AddEntry(bla.filename + ".pdf", pdfByteInfo);
        pdfStream.Close();
    }

    zip.Save(workStream);
    workStream.Position = 0;

    FileStreamResult fileResult = new FileStreamResult(workStream, System.Net.Mime.MediaTypeNames.Application.Zip);
    fileResult.FileDownloadName = "MultiplePDFs.zip";

    return fileResult;
}
于 2012-06-03T13:17:54.903 回答
2

正如交钥匙所说 - SharpZipLib 非常适合多个文件和内存流。只需 foreach 您需要压缩的文件并将它们添加到存档。这是示例:

        // Save it to memory
        MemoryStream ms = new MemoryStream();
        ZipOutputStream zipStream = new ZipOutputStream(ms);

        // USE THIS TO CHECK ZIP :)
        //FileStream fileOut = File.OpenWrite(@"c:\\test1.zip");
        //ZipOutputStream zipStream = new ZipOutputStream(fileOut);

        zipStream.SetLevel(0);

        // Loop your pages (files)
        foreach(string filename in files)
        {
            // Create and name entry in archive
            FileInfo fi = new FileInfo(filename);
            ZipEntry zipEntry = new ZipEntry(fi.Name);
            zipStream.PutNextEntry(zipEntry);

            // Put entry to archive (from file or DB)
            ReadFileToZip(zipStream, filename);

            zipStream.CloseEntry();

        }

        // Copy from memory to file or to send output to browser, as you did
        zipStream.Close();

我不知道您如何获取要压缩的信息,所以我认为该文件没问题:)

    /// <summary>
    /// Reads file and puts it to ZIP stream
    /// </summary>
    private void ReadFileToZip(ZipOutputStream zipStream, string filename)
    {
        // Simple file reading :)
        using(FileStream fs = File.OpenRead(filename))
        {
            StreamUtils.Copy(fs, zipStream, new byte[4096]);
        }
    }
于 2012-06-02T17:51:42.323 回答
1

我建议使用 SharpZipLib 压缩成标准的 zip 文件。将文件放入临时文件夹并使用 FastZip 类制作 zip。

于 2012-06-02T13:54:52.420 回答