我有一个返回带有多个页面的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?
提前致谢!
答案 0 :(得分: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委托,你需要有一种方法来检索" bla"对应于委托中的zipentry。在调用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
创建MemoryStream
。问题是你的整个zip文件都保存在内存中,这对内存的使用非常困难。如果您的zip文件很大,您的代码将保留内存中的所有内容。
我对MVC3模型知之甚少,不知道其中有什么东西可以帮助解决这个问题。如果没有,您可以use an Anonymous Pipe to invert the direction of the streams,并且无需将所有压缩数据保存在内存中。
这就是我的意思:创建FileStreamResult
要求您提供可读的流。如果你使用MemoryStream,为了使它可读,你需要首先写入它,然后在将它传递给FileStreamResult
构造函数之前回到位置0。这意味着该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
的最后一个值,因为那样
是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,它只是一个类型化的方法。每次循环时,都会创建该Action的新实例,并为bla引用不同的值。直到ZipFile.Save()
时才会调用该操作。
答案 1 :(得分: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;
}
答案 2 :(得分: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]);
}
}
答案 3 :(得分:1)
我建议使用SharpZipLib压缩成标准的zip文件。将文件放入临时文件夹并使用FastZip类制作zip。