2012-06-02 99 views
3

我有一個視圖返回一個包含多個頁面的pdf(使用iTextSharp),但現在我必須對其進行更改,以便每個頁面都是一個單獨的pdf(具有它自己的唯一標題)和返回一個zip文件。MVC3將多個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?

在此先感謝!

回答

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循環中調用此作者委託,則需要獲取與委託中的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; 
} 

我沒有試過這個,但它應該工作。這與你寫的內容相比具有更高的內存效率。缺點是它比使用命名管道和幾個匿名函數要複雜得多。

這使得僅當拉鍊含量是進入> 1MB範圍感。如果你的拉鍊比這更小,那麼你可以按照我上面展示的第一種方式來做。


附錄

爲什麼你不依賴於匿名方法中的bla價值?

有兩個關鍵點。首先,foreach循環定義了一個名爲bla的變量 ,該變量取值不同,每次都通過 循環。看起來很明顯,但值得明確說明它 。

其次,匿名方法被傳遞作爲參數到 的ZipFile.AddEntry()方法,它不會在當時運行 foreach循環運行。事實上,匿名方法被重複調用 ,一次爲每個條目添加,在 ZipFile.Save()時。如果你指的bla匿名 方法中,它得到最後一個值分配給bla,因爲 是價值bla當時ZipFile.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()時才被調用。

+0

+1謝謝你給出了一個很好的細分和高效的代碼!你能否擴展一下爲什麼我不能依賴循環中'bla'的值。 –

+0

是的,我在上面的答案末尾加上了解釋。如果你想了解更多,你應該閱讀關閉。 http://stackoverflow.com/a/428624/48082 – Cheeso

+0

謝謝!我真的很感謝你的答案的詳細程度! –

1

我會推薦使用SharpZipLib壓縮成一個標準的zip文件。將文件放入臨時文件夾並使用FastZip類進行壓縮。

+0

由於此項目的限制,我無法創建文件並將它們即使暫時存儲在文件夾中。我需要在內存中動態創建文件並將其作爲文件流返回。 –

+0

我看,SharpZipLib確實支持流媒體,但我沒有用它來輸入。應該能夠通過使用他們的基本類來完成它。 – Turnkey

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(); 

我不知道你是怎麼得到的信息被ziped,所以我認爲這個文件就可以了:)

/// <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; 
} 
+0

我的評論太大而不能發表評論,所以我把它們放在一個答案中。 http://stackoverflow.com/a/10891136/48082 – Cheeso

+0

你怎麼得到blas?哇是 – Beginner

+0

布拉和布拉斯剛剛彌補了這一職位。這是你想要的pdf內容。在我的情況下,它是數據庫中的一個模型,但它可以是字符串或其他。 –