【问题标题】:MVC3 return multiple pdfs as a zip fileMVC3 将多个 pdf 作为 zip 文件返回
【发布时间】:2012-06-07 10:22:05
【问题描述】:

我有一个视图,它返回一个包含多个页面的 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?

提前致谢!

【问题讨论】:

    标签: c# asp.net-mvc-3 file zip itextsharp


    【解决方案1】:

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

    【讨论】:

    • 由于这个项目的限制,我无法创建文件并将它们存储在文件夹中,即使是暂时的。我需要在内存中动态创建文件并将其/它们作为文件流返回。
    • 我明白了,SharpZipLib 确实支持流式传输,但我没有将它用于输入。应该可以通过使用他们的基本类来做到这一点。
    【解决方案2】:

    正如 Turnkey 所说 - 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]);
            }
        }
    

    【讨论】:

      【解决方案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;
      }
      

      【讨论】:

      • 我的 cmets 太大而无法发表评论,所以我将它们放在答案中。 stackoverflow.com/a/10891136/48082
      • Bla and Blas 只是为了这个帖子而弥补的。这是您在 pdf 中想要的内容。在我的例子中,它是数据库中的一个模型,但它可以是字符串或其他任何东西。
      【解决方案4】:

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

      另一方面,我确实有一些关于您使用 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; 
      }
      

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

      我对 MVC3 模型了解得不够多,无法知道其中是否有对此有所帮助的东西。如果没有,您可以use an Anonymous Pipe to invert the direction of the streams,并且无需将所有压缩数据保存在内存中。

      我的意思是:创建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() 运行时保持的值。

      导致困难的是延迟执行。

      您想要的是来自 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 的一个新实例,并且它为 bla 引用不同的值。直到ZipFile.Save() 时才会调用该操作。

      【讨论】:

      • +1 感谢您提供了出色的细分和高效的代码!您能否详细说明为什么我不能从循环中依赖“bla”的值。
      • 是的,我把解释放在上面答案的末尾。如果你想了解更多,你应该阅读闭包。 stackoverflow.com/a/428624/48082
      • 非常感谢!我非常感谢您回答的详细程度!
      • +1 - 很高兴看到一位 DotNetZip 开发人员提供了如此详细的解释。很棒的图书馆。 :)
      • 我知道 cmets 不是为了感谢人,但我不在乎。来这里寻找一个例子,你不仅给出了一个很好的例子,而且你打破了我脑海中关于这个主题的每一个问题。非常感谢您提供如此详细的答案,应该会有更多的支持。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-03-17
      • 2019-03-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多