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

46

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

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

最近,我制定了一种查看服务器端文档的方法,以确保我至少可以使其执行。它将文档获取到代码中,并创建一个FileStreamResult,然后将其作为(隐式转换)ActionResult返回。我让它返回到服务器端MVC控制器并将其放入简单的返回(无视图)中,在浏览器中正常显示PDF。但是,尝试直接转到Web API函数只会返回看起来像FileStreamResult的JSON表示。

当我尝试使其正确返回到我的客户端MVC应用程序时,它告诉我“_buffer”不能直接设置。类似于所返回并抛入对象中的某些属性是私有的,无法访问的错误消息。

所述PDF的字节数组表示在翻译为base64字符串时,似乎没有与FileStreamResult返回的JSON中的“_buffer”字符串相同数量的字符。它缺少大约26k个'A'。

有什么想法可以使此PDF正确返回吗?如有必要,我可以提供代码,但必须有某种已知方法可以从服务器端Web API应用程序返回PDF到客户端MVC应用程序并在浏览器中显示为Web页面。

P.S. 我知道“客户端”应用程序在技术上不在客户端。它也将是一个服务器应用程序,但这在这种情况下并不重要。相对于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;
}

获取PDF的API函数:GetPDF

    [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的实例。该类型是接口或抽象类,无法实例化。路径'FileStream'。

以前我遇到过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
1
为什么你把这个标记为Web API和MVC?你说Web API,但是你的所有代码都是MVC。 - mason
1
那并不是Web API。那只是MVC。Web API 2和MVC 5彼此独立。据我所知,您实际上根本没有使用Web API。您正在以类似于Web API的方式使用MVC,但这并不意味着它是Web API。 - mason
1
在MVC 5中,您不能混合使用Web API和MVC。WebAPI控制器继承自ApiController类,而不是MVC Controller类的子类型。因此,这并没有什么意义。 - mason
公共类WebServiceController:ApiController - Doctor93
显示剩余7条评论
2个回答

48

一些用于返回 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));
}

希望这可以帮助到您


1
好的,那个可以用,但是我正在尝试从前端到后端删除链接。前端应该基本上充当一个隐藏所有业务逻辑的中间人。我的尝试将此返回给浏览器的文本为“StatusCode: 200, ReasonPhrase: 'OK', Version: 1.1, Content: System.Net.Http.StreamContent, Headers: { Pragma: no-cache Cache-Control: no-cache Date: Wed, 16 Mar 2016 18:48:38 GMT Server: Microsoft-IIS/10.0 X-AspNet-Version: 4.0.30319 X-Powered-By: ASP.NET Content-Length: 45600 Content-Disposition: inline; filename=chart.pdf Content-Type: application/pdf Expires: -1 }”。 - Doctor93
1
我明白了。谢谢!我只需要将内容作为流读取,并将其隐式转换为ActionResult,然后将其抛入FileStreamResult中即可。 - Doctor93
1
@SamBartlett 不知何故,在响应完成之前,该流被处理掉了。我猜测这可能与变量的作用域有关。我还猜测你所提到的是 Web API 版本的答案,因为我在 MVC 版本中没有遇到过这个问题。 - Nkosi
这对我有用,除了我必须更改获取缓冲区的方式: <code>buffer = memoryStream.ToArray();</code> 而不是: <code>buffer = memoryStream.GetBuffer();</code>使用GetBuffer()的问题在于它返回缓冲区分配的所有内存,其中可能包括许多空字节。 - Sgraffite
我需要从.NET Core 3.1 Web API返回一个PDF文件,并找到了这篇优秀的文章:https://codeburst.io/download-files-using-web-api-ae1d1025f0a9生成的代码如下: - Chesare
显示剩余2条评论

3
我需要从 .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/

祝使用愉快!


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