如何从 Web API 应用程序返回 PDF

Posted

技术标签:

【中文标题】如何从 Web API 应用程序返回 PDF【英文标题】:How to return a PDF from a Web API application 【发布时间】:2016-07-02 17:48:59 【问题描述】:

我有一个在服务器上运行的 Web API 项目。它应该从两种不同的来源返回 PDF:实际的可移植文档文件 (PDF) 和存储在数据库中的 base64 字符串。我遇到的麻烦是将文档发送回客户端 MVC 应用程序。剩下的就是关于发生的一切以及我已经尝试过的细节。

我编写的代码成功地将这两种格式转换为 C# 代码,然后(返回)为 PDF 格式。我已经成功传输了一个应该代表其中一个文档的字节数组,但我无法让它在浏览器中显示(在新选项卡中)。我总是收到某种“无法显示”的错误。

最近,我做了一种在服务器端查看文档的方法,以确保我至少可以让它做到这一点。它将文档放入代码中并用它创建一个 FileStreamResult,然后它作为(隐式转换)ActionResult 返回。我把它返回到服务器端 MVC 控制器并将其放入一个简单的返回(无视图)中,在浏览器中显示 PDF 就好了。但是,尝试直接访问 Web API 函数只会返回类似于 FileStreamResult 的 JSON 表示形式。

当我试图让它正确地返回到我的客户端 MVC 应用程序时,它告诉我不能直接设置“_buffer”。一些错误消息,大意是返回并抛出到对象中的某些属性是私有的,无法访问。

当转换为 base64 字符串时,上述 PDF 的字节数组表示形式似乎与 FileStreamResult 在 JSON 中返回的“_buffer”字符串的字符数不同。它最后缺少大约 26k 'A'。

关于如何让这个 PDF 正确返回的任何想法?如有必要,我可以提供代码,但必须有一些已知的方法将 PDF 从服务器端 Web API 应用程序返回到客户端 MVC 应用程序并在浏览器中将其显示为网页。

附:我确实知道“客户端”应用程序在技术上并不在客户端。它也将是一个服务器应用程序,但在这种情况下这无关紧要。相对于 Web API 服务器,我的 MVC 应用程序是“客户端”。

代码 获取pdf:

private System.Web.Mvc.ActionResult GetPDF()

    int bufferSize = 100;
    int startIndex = 0;
    long retval;
    byte[] buffer = new byte[bufferSize];
    MemoryStream stream = new MemoryStream();
    SqlCommand command;
    SqlConnection sqlca;
    SqlDataReader reader;

    using (sqlca = new SqlConnection(CONNECTION_STRING))
    
        command = new SqlCommand((LINQ_TO_GET_FILE).ToString(), sqlca);
        sqlca.Open();
        reader = command.ExecuteReader(CommandBehavior.SequentialAccess);
        try
        
            while (reader.Read())
            
                do
                
                    retval = reader.GetBytes(0, startIndex, buffer, 0, bufferSize);
                    stream.Write(buffer, 0, bufferSize);
                    startIndex += bufferSize;
                 while (retval == bufferSize);
            
        
        finally
        
            reader.Close();
            sqlca.Close();
        
    
    stream.Position = 0;
    System.Web.Mvc.FileStreamResult fsr = new System.Web.Mvc.FileStreamResult(stream, "application/pdf");
    return fsr;

从GetPDF获取的API函数:

    [AcceptVerbs("GET","POST")]
    public System.Web.Mvc.ActionResult getPdf()
    
        System.Web.Mvc.ActionResult retVal = GetPDF();
        return retVal;
    

用于显示 PDF 服务器端:

public ActionResult getChart()

    return new PDFController().GetPDF();

随着时间的推移,MVC 应用程序中的代码发生了很大变化。现在的方式是,它没有进入尝试在浏览器中显示的阶段。在此之前它会出错。

public async Task<ActionResult> get_pdf(args,keys)

    JObject jObj;
    StringBuilder argumentsSB = new StringBuilder();
    if (args.Length != 0)
    
        argumentsSB.Append("?");
        argumentsSB.Append(keys[0]);
        argumentsSB.Append("=");
        argumentsSB.Append(args[0]);
        for (int i = 1; i < args.Length; i += 1)
        
            argumentsSB.Append("&");
            argumentsSB.Append(keys[i]);
            argumentsSB.Append("=");
            argumentsSB.Append(args[i]);
        
    
    else
    
        argumentsSB.Append("");
    
    var arguments = argumentsSB.ToString();
    using (var client = new HttpClient())
    
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        var response = await client.GetAsync(URL_OF_SERVER+"api/pdf/getPdf/" + arguments).ConfigureAwait(false);
        jObj = (JObject)JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().Result);
    
    return jObj.ToObject<ActionResult>();

我直接从 Web API 控制器运行该方法得到的 JSON 是:


    "FileStream":
        "_buffer":"JVBER...NjdENEUxAA...AA==",
        "_origin":0,
        "_position":0,
        "_length":45600,
        "_capacity":65536,
        "_expandable":true,
        "_writable":true,
        "_exposable":true,
        "_isOpen":true,
        "__identity":null,
    "ContentType":"application/pdf",
    "FileDownloadName":""

我缩短了“_buffer”,因为它太长了。 我目前在 get_pdf(args,keys) 的返回行上收到以下错误消息

异常详细信息:Newtonsoft.Json.JsonSerializationException:无法创建 System.Web.Mvc.ActionResult 类型的实例。类型是接口或抽象类,不能实例化。路径“文件流”。

当我曾经得到一个空白的pdf阅读器(阅读器是空白的。没有文件)时,我使用了这个代码:

public async Task<ActionResult> get_pdf(args,keys)

    byte[] retArr;
    StringBuilder argumentsSB = new StringBuilder();
    if (args.Length != 0)
    
        argumentsSB.Append("?");
        argumentsSB.Append(keys[0]);
        argumentsSB.Append("=");
        argumentsSB.Append(args[0]);
        for (int i = 1; i < args.Length; i += 1)
        
            argumentsSB.Append("&");
            argumentsSB.Append(keys[i]);
            argumentsSB.Append("=");
            argumentsSB.Append(args[i]);
        
    
    else
    
        argumentsSB.Append("");
    
    var arguments = argumentsSB.ToString();
    using (var client = new HttpClient())
    
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/pdf"));
        var response = await client.GetAsync(URL_OF_SERVER+"api/webservice/" + method + "/" + arguments).ConfigureAwait(false);
        retArr = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
    
    var x = retArr.Skip(1).Take(y.Length-2).ToArray();
    /*Response.Clear();
    Response.ClearContent();
    Response.ClearHeaders();
    Response.ContentType = "application/pdf";
    Response.AppendHeader("Content-Disposition", "inline;filename=document.pdf");
    Response.BufferOutput = true;
    Response.BinaryWrite(x);
    Response.Flush();
    Response.End();*/
    return new FileStreamResult(new MemoryStream(x),MediaTypeNames.Application.Pdf);
    

注释掉的是来自其他一些尝试的代码。当我使用该代码时,我正在从服务器返回一个字节数组。它看起来像:

JVBER...NjdENEUx

【问题讨论】:

显示一些示例代码、具体错误信息等 @Mr.T 我刚刚添加了所有我想添加到我的编辑中的内容。 你为什么把它标记为 Web API 和 MVC?您说的是 Web API,但您的所有代码似乎都是 MVC。 那是不是 Web API。那只是MVC。 Web API 2 和 MVC 5 彼此不同。据我所知,您实际上根本没有使用 Web API。您以与 Web API 类似的方式使用 MVC,但这并不能使其成为 Web API。 在 MVC 5 中,您不能混合搭配 Web API 和 MVC。 WebAPI 控制器继承自 ApiController 并且不是 MVC 控制器类的子类型。所以这真的没有意义。 【参考方案1】:

一些用于返回 PDF (Web Api) 的服务器端代码。

[HttpGet]
[Route("documents/docid")]
public HttpResponseMessage Display(string docid) 
    HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.BadRequest);
    var documents = reader.GetDocument(docid);
    if (documents != null && documents.Length == 1) 
        var document = documents[0];
        docid = document.docid;
        byte[] buffer = new byte[0];
        //generate pdf document
        MemoryStream memoryStream = new MemoryStream();
        MyPDFGenerator.New().PrintToStream(document, memoryStream);
        //get buffer
        buffer = memoryStream.ToArray();
        //content length for use in header
        var contentLength = buffer.Length;
        //200
        //successful
        var statuscode = HttpStatusCode.OK;
        response = Request.CreateResponse(statuscode);
        response.Content = new StreamContent(new MemoryStream(buffer));
        response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
        response.Content.Headers.ContentLength = contentLength;
        ContentDispositionHeaderValue contentDisposition = null;
        if (ContentDispositionHeaderValue.TryParse("inline; filename=" + document.Name + ".pdf", out contentDisposition)) 
            response.Content.Headers.ContentDisposition = contentDisposition;
        
     else 
        var statuscode = HttpStatusCode.NotFound;
        var message = String.Format("Unable to find resource. Resource \"0\" may not exist.", docid);
        var responseData = responseDataFactory.CreateWithOnlyMetadata(statuscode, message);
        response = Request.CreateResponse((HttpStatusCode)responseData.meta.code, responseData);
    
    return response;

在我的视图中你可以做这样的事情

<a href="api/documents/1234" target = "_blank" class = "btn btn-success" >View document</a>

这将调用 web api 并在浏览器的新选项卡中打开 PDF 文档。

这是我基本上做同样事情的方式,但来自 MVC 控制器

// NOTE: Original return type: FileContentResult, Changed to ActionResult to allow for error results
[Route("docid/Label")]
public ActionResult Label(Guid docid) 
    var timestamp = DateTime.Now;
    var shipment = objectFactory.Create<Document>();
    if (docid!= Guid.Empty) 
        var documents = reader.GetDocuments(docid);
        if (documents.Length > 0)
            document = documents[0];

            MemoryStream memoryStream = new MemoryStream();
            var printer = MyPDFGenerator.New();
            printer.PrintToStream(document, memoryStream);

            Response.AppendHeader("Content-Disposition", "inline; filename=" + timestamp.ToString("yyyyMMddHHmmss") + ".pdf");
            return File(memoryStream.ToArray(), "application/pdf");
         else 
            return this.RedirectToAction(c => c.Details(id));
        
    
    return this.RedirectToAction(c => c.Index(null, null));

希望对你有帮助

【讨论】:

好的,这可行,但我正在尝试删除从前端到后端的链接。前端应该基本上充当隐藏所有业务逻辑的中间人。我这样做的尝试将此文本返回到浏览器“StatusCode:200,ReasonPhrase:'OK',版本:1.1,内容:System.Net.Http.StreamContent,标题: Pragma:no-cache Cache-Control:no-缓存日期:2016 年 3 月 16 日星期三 18:48:38 GMT 服务器:Microsoft-IIS/10.0 X-AspNet-Version:4.0.30319 X-Powered-By:ASP.NET Content-Length:45600 Content-Disposition:inline;文件名=chart.pdf 内容类型:application/pdf 过期:-1 " 我明白了。谢谢!我只需要将内容作为 Stream 读取并将其放入 FileStreamResult 中,隐式转换为 ActionResult 嗨!当我试图简化response.Content = new StreamContent(new MemoryStream(buffer)); 行时,我正在重构您的示例并碰壁 - 您能帮我理解为什么您不能只将之前创建的 MemoryStream 传递给 StreamContent 吗?我试过了,它没有用,但我不明白为什么不。 @SamBartlett 在响应完成之前如何处理该流。我的猜测是它与变量的范围有关。我也猜测您指的是答案的 Web API 版本,因为我没有遇到 MVC 版本的问题 @Nkosi 是的,对不起,我指的是 WebAPI 版本。有趣的是,我也遇到了这个问题。也许我需要阅读更多关于流/内存流的生命周期的信息。【参考方案2】:

我需要从 .NET core 3.1 web api 返回一个 pdf 文件,并找到了这篇优秀的文章:

https://codeburst.io/download-files-using-web-api-ae1d1025f0a9

基本上,你调用:

var bytes = await System.IO.File.ReadAllBytesAsync(pathFileName);
return File(bytes, "application/pdf", Path.GetFileName(pathFileName));

整个代码是:

using Microsoft.AspNetCore.Mvc;
using System.IO;

using Reportman.Drawing;
using Reportman.Reporting;
using System.Text;
using System.Threading.Tasks;

[Route("api/[controller]")]
[ApiController]
public class PdfController : ControllerBase

    [HttpGet]
    [Route("ot/idOT")]
    public async Task<ActionResult> OT(string idOT)
    
        Report rp = new Report();
        rp.LoadFromFile("ot-net.rep"); // File created with Reportman designer
        rp.ConvertToDotNet();

        // FixReport
        rp.AsyncExecution = false;
        PrintOutPDF printpdf = new PrintOutPDF();

        // Perform the conversion from one encoding to the other.
        byte[] unicodeBytes = Encoding.Convert(Encoding.ASCII, Encoding.Unicode, Encoding.ASCII.GetBytes($"Orden de trabajo idOT"));
        string unicodeString = new string(Encoding.Unicode.GetChars(unicodeBytes));
        // todo: convert to unicode
        // e = Encoding.GetEncoding(unicodeString);
        // System.Diagnostics.Trace.WriteLine(e);
        if (rp.Params.Count > 0)
        
            rp.Params[0].Value = unicodeString;
        

        printpdf.FileName = $"ot-idOT.pdf";
        printpdf.Compressed = false;

        if (printpdf.Print(rp.MetaFile))
        
            // Download Files using Web API. Changhui Xu. https://codeburst.io/download-files-using-web-api-ae1d1025f0a9
            var bytes = await System.IO.File.ReadAllBytesAsync(printpdf.FileName);
            return File(bytes, "application/pdf", Path.GetFileName(printpdf.FileName));
        

        return null;
    

对该 API 的调用如下所示:https://localhost:44387/api/pdf/ot/7

Reportman 是一个 pdf 生成器,您可以在以下位置找到: https://reportman.sourceforge.io/

享受吧!

【讨论】:

以上是关于如何从 Web API 应用程序返回 PDF的主要内容,如果未能解决你的问题,请参考以下文章

Flutter Web - 如何选择 PDF 文件并 POST 到 API?

如何从使用 JWT 令牌的 Web 客户端应用程序中注销用户

C#:从 Web 服务返回的字节 [] 在新选项卡中显示 PDF

如何从 ASP.NET 中的 Web 请求返回 pdf?

如何从 ReactJS Web 应用程序向 Django API 发送 POST 请求?

如何使用 javascript 从 Web 服务返回的二进制字符串构建 PDF 文件