MVC3如何将多个PDF文件打包成一个ZIP文件并返回?

4
我可以帮忙翻译。这段文字是关于编程的,需要将返回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文件。或者我应该使用其他工具,比如dotnetzip或sharpzip吗?

提前感谢!

4个回答

13

如果您的解决方案有效,则最简单的方法就是保持不变。

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

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

byte[] byteInfo = workStream.ToArray();                        

zip.Save(workStream);                        

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

…你正在将workStream读入一个数组中。但此时,您尚未向workStream写入任何内容,因此该数组为空,长度为零。然后,您将zip保存到workstream中。然后,您将零长度的数组写入同一workstream中。这是无操作(NO-OP)。最后,您重置了位置。

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

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

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

其次,您不必要地分配了临时缓冲区(MemoryStreams)。将MemoryStream视为只是一个带有Stream包装器的字节数组,以支持Write()、Read()、Seek()等操作。实际上,您的代码正在将数据写入该临时缓冲区,然后告诉DotNetZip从临时缓冲区中读取数据到其自己的压缩缓冲区。您不需要那个中间缓冲区。它按您所做的方式工作,但可能效率更高。

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

在该writer委托中,你的代码直接写入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; 
}

最后,我不太喜欢你从MemoryStream中创建一个FileStreamResult。问题是整个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时才有意义。如果您的zip文件小于1MB,则可以使用我上面展示的第一种方式。


补充说明

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

有两个关键点。首先,foreach循环定义了一个名为bla的变量,每次 通过循环时都会取一个不同的值。这似乎很明显,但值得明确说明。

其次,匿名方法被作为参数传递给ZipFile.AddEntry()方法,而它不会在foreach循环运行时运行。 事实上,匿名方法会在每个条目被添加时重复调用,即在 ZipFile.Save()执行的时候。如果在匿名方法中引用bla, 它会得到最后一个分配给bla的值,因为那是ZipFile.Save()运行时bla所保存的值。

这是延迟执行导致了困难。

你想要的是foreach循环中每个不同的bla值都可以在匿名函数被调用时访问 - 稍后,在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),它只是一个有类型的方法。每次循环时,都会创建一个新的该操作(Action)的实例,并引用不同的bla值。该操作(Action)直到ZipFile.Save()时才被调用。


+1 谢谢你提供了清晰高效的代码!你能否再详细解释一下为什么我不能依赖于循环中的 'bla' 变量的值呢? - Garrett Fogerlie
是的,我把解释放在了上面答案的末尾。如果你想更多地了解这个问题,你应该阅读一下有关闭包的资料。https://dev59.com/InRC5IYBdhLWcg3wCMrX#428624 - Cheeso
非常感谢!我真的很欣赏你详细的回答! - Garrett Fogerlie
+1 - 很高兴看到DotNetZip开发人员提供如此详细的解释。太棒了!:) - kuujinbo
我知道评论不是用来感谢别人的,但我不在乎。我来这里是为了寻找一个例子,而你不仅给了一个非常好的例子,还解答了我关于这个主题的每一个问题。非常感谢你如此详细的回答,应该有更多的点赞。 - user10251956

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

我的评论太长了,无法作为评论发布,所以我把它们放在了答案里。https://dev59.com/gGTWa4cB1Zd3GeqPEIer#10891136 - Cheeso
Bla和Blas只是为了本篇文章而编造的。这是您想在PDF中包含的内容。在我的情况下,它是数据库中的一个模型,但它可以是字符串或其他任何东西。 - Garrett Fogerlie

2

正如 Turnkey 所说 - SharpZipLib 对于多个文件和内存流非常好用。只需循环遍历需要压缩的文件并将它们添加到归档中即可。以下是示例:

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

1
我建议使用SharpZipLib将文件压缩成标准的zip文件。将文件放入临时文件夹中,然后使用FastZip类来创建zip文件。

由于此项目的限制,我无法创建文件并将它们存储在文件夹中,即使是暂时性的。我需要在内存中动态创建文件,并将其作为文件流返回。 - Garrett Fogerlie
我知道,SharpZipLib支持流式处理,但我还没有用它来处理输入。不过,使用它们的基本类应该是可以做到的。 - Turnkey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接