diff --git a/README.md b/README.md index a8fbeb3..10b8ae8 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,39 @@ Extremely easy way to create Pdf files from ASP.NET Core New Features ------ +### 4.0.0 + +* Property `SaveOnServerPath` is discontinued. +* Support new delegate. `TryCustomizeAsync()` + * If you want to customize the generated binary file before `OnBuildFileSuccess()`, use this. + * Please return true to continue processing, false to drop with error. + +```csharp + public ActionResult TestInlie() + { + return new ActionAsPdf("Index") + { + ContentDisposition = ContentDisposition.Inline, + TryCustomizeAsync = async (stream, context, fineName) => + { + // some code done. + return true; + + // e.g. + var customizeStream = new MemoryStream(); + await stream.CopyToAsync(customizeStream); + + // ... + stream.SetLength(0); + await customizeStream.CopyToAsync(stream); + + return true; + }, + }; + } +``` + + ### 3.0.0 * Support ASP.NET Core 3.0. Thank you [@vertonghenb](https://github.com/vertonghenb) diff --git a/RotativaCore.sln b/RotativaCore.sln index 441e122..3bce3f1 100644 --- a/RotativaCore.sln +++ b/RotativaCore.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28803.202 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.32407.343 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{053EF92D-D564-4D29-8554-9422F330640C}" EndProject @@ -17,6 +17,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RotativaCore.UnitTests", "s EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RotativaCore.SampleWebApp.3.0", "src\RotativaCore.SampleWebApp.3.0\RotativaCore.SampleWebApp.3.0.csproj", "{1AAF8B2C-F313-46FB-87C6-7B6DF53A4727}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{86815082-22AE-43B1-8816-187A9BC49B2C}" + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + LICENSE = LICENSE + README.md = README.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/src/RotativaCore.SampleWebApp.3.0/Controllers/HomeController.cs b/src/RotativaCore.SampleWebApp.3.0/Controllers/HomeController.cs index 62961d6..5e9636b 100644 --- a/src/RotativaCore.SampleWebApp.3.0/Controllers/HomeController.cs +++ b/src/RotativaCore.SampleWebApp.3.0/Controllers/HomeController.cs @@ -1,249 +1,265 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.WindowsAzure.Storage; -using RotativaCore.Options; -using RotativaCore.SampleWebApp._3._0.Models; - -namespace RotativaCore.SampleWebApp._3._0.Controllers -{ - public class HomeController : Controller - { - public IActionResult Index() - { - return View(); - } - - public IActionResult Privacy() - { - return View(); - } - - [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] - public IActionResult Error() - { - return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); - } - - - public IActionResult TestInlie() - { - return new ActionAsPdf("Index") - { - //FileName = "Test.pdf", - ContentDisposition = ContentDisposition.Inline, - OnBuildFileSuccess = async (bytes, context, fileName) => - { - // some code done. - return true; - - // example. - if (string.IsNullOrEmpty(fileName)) - fileName = $"{Guid.NewGuid()}.pdf"; - - var container = CloudStorageAccount - .Parse(connectionString: null) // Please set your value.If it's null, it will result in an ArgumentNullException(). - .CreateCloudBlobClient() - .GetContainerReference(containerName: null); // Please set your value.If it's null, it will result in an ArgumentNullException(). - - try - { - var blockBlob = container.GetBlockBlobReference(fileName); - blockBlob.Properties.ContentType = "application/pdf"; - await blockBlob.UploadFromByteArrayAsync(bytes, 0, bytes.Length); - } - catch (Exception e) - { - // logging. - return false; // fire InvalidOperationException() - } - - return true; - }, - }; - } - - - public ActionResult TestAttachmentWithA4LandscapeDisableSmartShrinkingViewPortSize1024() - { - return new ActionAsPdf("Index") - { - FileName = "TestWithA4LandscapeDisableSmartShrinkingViewPortSize1024.pdf", - ContentDisposition = ContentDisposition.Attachment, - PageSize = Size.A4, - PageOrientation = Orientation.Landscape, - DisableSmartShrinking = true, - ViewportSize = 1024, - }; - } - - public ActionResult TestImage() - { - return new ActionAsImage("Index") - { - FileName = "TestImage.jpg" - }; - } - - public ActionResult TestImagePng() - { - return new ActionAsImage("Index") - { - FileName = "TestImagePng.png", - Format = ImageFormat.png - }; - } - - public ActionResult TestUrl() - { - // Now I realize that this isn't very expressive example of why this can be useful. - // However imagine that you have your own UrlHelper extensions like UrlHelper.User(...) - // where you create correct URL according to passed conditions, prepare some complex model, etc. - - var urlHelper = new UrlHelper(ControllerContext); - var url = urlHelper.Action("Index", new { name = "Great Friends" }); - - return new UrlAsPdf(url) - { - FileName = "TestUrl.pdf" - }; - } - - public ActionResult TestExternalUrl() - { - // In some cases you might want to pull completely different URL that is not related to your application. - // You can do that by specifying full URL. - - return new UrlAsPdf("http://www.github.com") - { - FileName = "TestExternalUrl.pdf", - PageMargins = new Margins(0, 0, 0, 0) - }; - } - - public ActionResult TestView() - { - // The more usual way of using this would be to have a Model object that you would pass into ViewAsPdf - // and work with that Model inside your View. - // Good example could be an Order Summary page on some fictional E-shop. - - // Probably the biggest advantage of this approach is that you have Session object available. - - ViewBag.Message = string.Format("Hello {0} to ASP.NET Core!", "Super Great Friends"); - return new ViewAsPdf("Index") - { - FileName = "TestView.pdf", - PageSize = Size.A3, - PageOrientation = Orientation.Landscape, - PageMargins = { Left = 0, Right = 0 }, - ContentDisposition = ContentDisposition.Inline - }; - } - - public ActionResult TestViewImage() - { - // The more usual way of using this would be to have a Model object that you would pass into ViewAsImage - // and work with that Model inside your View. - // Good example could be an Order Summary page on some fictional E-shop. - - // Probably the biggest advantage of this approach is that you have Session object available. - - ViewBag.Message = string.Format("Hello {0} to ASP.NET Core!", "Super Great Friends"); - return new ViewAsImage("Index") - { - FileName = "TestViewImage.png", - }; - } - - public ActionResult TestViewWithModel(string id) - { - var model = new TestViewModel - { - DocTitle = id, - DocContent = "This is a test" - }; - - return new ViewAsPdf("TestViewWithModel", model) - { - FileName = "TestViewWithModel.pdf" - }; - } - - public ActionResult TestImageViewWithModel(string id) - { - var model = new TestViewModel - { - DocTitle = id, - DocContent = "This is a test" - }; - - return new ViewAsImage("TestViewWithModel", model) - { - FileName = "TestImageViewWithModel.pdf" - }; - } - - public ActionResult TestPartialViewWithModel(string id) - { - var model = new TestViewModel - { - DocTitle = id, - DocContent = "This is a test with a partial view" - }; - - return new PartialViewAsPdf("TestPartialViewWithModel", model) - { - FileName = "TestPartialViewWithModel.pdf" - }; - } - - public ActionResult TestImagePartialViewWithModel(string id) - { - var model = new TestViewModel - { - DocTitle = id, - DocContent = "This is a test with a partial view" - }; - - return new PartialViewAsImage("TestPartialViewWithModel", model); - } - - public ActionResult ErrorTest() - { - return new ActionAsPdf("SomethingBad") - { - FileName = "ErrorTest.pdf" - }; - } - - public ActionResult SomethingBad() - { - return Redirect("http://thisdoesntexists"); - } - - public ActionResult RouteTest() - { - return new RouteAsPdf("TestRoute") - { - FileName = "RouteTest.pdf" - }; - } - - [Obsolete] - public ActionResult BinaryTest() - { - var pdfResult = new ActionAsPdf("Index") - { - FileName = "BinaryTest.pdf" - }; - - var binary = pdfResult.BuildPdf(this.ControllerContext); - - return File(binary, "application/pdf"); - } - } -} +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.WindowsAzure.Storage; +using RotativaCore.Options; +using RotativaCore.SampleWebApp._3._0.Models; + +namespace RotativaCore.SampleWebApp._3._0.Controllers +{ + public class HomeController : Controller + { + public IActionResult Index() + { + return View(); + } + + public IActionResult Privacy() + { + return View(); + } + + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public IActionResult Error() + { + return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + } + + + public IActionResult TestInlie() + { + return new ActionAsPdf("Index") + { + //FileName = "Test.pdf", + ContentDisposition = ContentDisposition.Inline, + TryCustomizeAsync = async (stream, context, fineName) => + { + // some code done. + return true; + + // e.g. + var customizeStream = new MemoryStream(); + await stream.CopyToAsync(customizeStream); + + // ... + stream.SetLength(0); + await customizeStream.CopyToAsync(stream); + + return true; + }, + OnBuildFileSuccess = async (bytes, context, fileName) => + { + // some code done. + return true; + + // example. + if (string.IsNullOrEmpty(fileName)) + fileName = $"{Guid.NewGuid()}.pdf"; + + var container = CloudStorageAccount + .Parse(connectionString: null) // Please set your value.If it's null, it will result in an ArgumentNullException(). + .CreateCloudBlobClient() + .GetContainerReference(containerName: null); // Please set your value.If it's null, it will result in an ArgumentNullException(). + + try + { + var blockBlob = container.GetBlockBlobReference(fileName); + blockBlob.Properties.ContentType = "application/pdf"; + await blockBlob.UploadFromByteArrayAsync(bytes, 0, bytes.Length); + } + catch (Exception e) + { + // logging. + return false; // fire InvalidOperationException() + } + + return true; + }, + }; + } + + + public ActionResult TestAttachmentWithA4LandscapeDisableSmartShrinkingViewPortSize1024() + { + return new ActionAsPdf("Index") + { + FileName = "TestWithA4LandscapeDisableSmartShrinkingViewPortSize1024.pdf", + ContentDisposition = ContentDisposition.Attachment, + PageSize = Size.A4, + PageOrientation = Orientation.Landscape, + DisableSmartShrinking = true, + ViewportSize = 1024, + }; + } + + public ActionResult TestImage() + { + return new ActionAsImage("Index") + { + FileName = "TestImage.jpg" + }; + } + + public ActionResult TestImagePng() + { + return new ActionAsImage("Index") + { + FileName = "TestImagePng.png", + Format = ImageFormat.png + }; + } + + public ActionResult TestUrl() + { + // Now I realize that this isn't very expressive example of why this can be useful. + // However imagine that you have your own UrlHelper extensions like UrlHelper.User(...) + // where you create correct URL according to passed conditions, prepare some complex model, etc. + + var urlHelper = new UrlHelper(ControllerContext); + var url = urlHelper.Action("Index", new { name = "Great Friends" }); + + return new UrlAsPdf(url) + { + FileName = "TestUrl.pdf" + }; + } + + public ActionResult TestExternalUrl() + { + // In some cases you might want to pull completely different URL that is not related to your application. + // You can do that by specifying full URL. + + return new UrlAsPdf("http://www.github.com") + { + FileName = "TestExternalUrl.pdf", + PageMargins = new Margins(0, 0, 0, 0) + }; + } + + public ActionResult TestView() + { + // The more usual way of using this would be to have a Model object that you would pass into ViewAsPdf + // and work with that Model inside your View. + // Good example could be an Order Summary page on some fictional E-shop. + + // Probably the biggest advantage of this approach is that you have Session object available. + + ViewBag.Message = string.Format("Hello {0} to ASP.NET Core!", "Super Great Friends"); + return new ViewAsPdf("Index") + { + FileName = "TestView.pdf", + PageSize = Size.A3, + PageOrientation = Orientation.Landscape, + PageMargins = { Left = 0, Right = 0 }, + ContentDisposition = ContentDisposition.Inline + }; + } + + public ActionResult TestViewImage() + { + // The more usual way of using this would be to have a Model object that you would pass into ViewAsImage + // and work with that Model inside your View. + // Good example could be an Order Summary page on some fictional E-shop. + + // Probably the biggest advantage of this approach is that you have Session object available. + + ViewBag.Message = string.Format("Hello {0} to ASP.NET Core!", "Super Great Friends"); + return new ViewAsImage("Index") + { + FileName = "TestViewImage.png", + }; + } + + public ActionResult TestViewWithModel(string id) + { + var model = new TestViewModel + { + DocTitle = id, + DocContent = "This is a test" + }; + + return new ViewAsPdf("TestViewWithModel", model) + { + FileName = "TestViewWithModel.pdf" + }; + } + + public ActionResult TestImageViewWithModel(string id) + { + var model = new TestViewModel + { + DocTitle = id, + DocContent = "This is a test" + }; + + return new ViewAsImage("TestViewWithModel", model) + { + FileName = "TestImageViewWithModel.pdf" + }; + } + + public ActionResult TestPartialViewWithModel(string id) + { + var model = new TestViewModel + { + DocTitle = id, + DocContent = "This is a test with a partial view" + }; + + return new PartialViewAsPdf("TestPartialViewWithModel", model) + { + FileName = "TestPartialViewWithModel.pdf" + }; + } + + public ActionResult TestImagePartialViewWithModel(string id) + { + var model = new TestViewModel + { + DocTitle = id, + DocContent = "This is a test with a partial view" + }; + + return new PartialViewAsImage("TestPartialViewWithModel", model); + } + + public ActionResult ErrorTest() + { + return new ActionAsPdf("SomethingBad") + { + FileName = "ErrorTest.pdf" + }; + } + + public ActionResult SomethingBad() + { + return Redirect("http://thisdoesntexists"); + } + + public ActionResult RouteTest() + { + return new RouteAsPdf("TestRoute") + { + FileName = "RouteTest.pdf" + }; + } + + [Obsolete] + public ActionResult BinaryTest() + { + var pdfResult = new ActionAsPdf("Index") + { + FileName = "BinaryTest.pdf" + }; + + var binary = pdfResult.BuildPdf(this.ControllerContext); + + return File(binary, "application/pdf"); + } + } +} diff --git a/src/RotativaCore/AsResultBase.cs b/src/RotativaCore/AsResultBase.cs index 99c90fc..0ce759f 100644 --- a/src/RotativaCore/AsResultBase.cs +++ b/src/RotativaCore/AsResultBase.cs @@ -116,21 +116,28 @@ public string CookieName public string CustomSwitches { get; set; } - [Obsolete(@"Use BuildFile(ActionContext) method instead and use the resulting binary data to do what needed.")] - public string SaveOnServerPath { get; set; } - /// /// Set 'Content-Disposition' Response Header. If you set 'FileName', this value is ignored and "attachment" is forced. /// public ContentDisposition ContentDisposition { get; set; } + +#pragma warning disable CS1998 + /// + /// If you want to customize the generated binary file before `OnBuildFileSuccess()`, use this. + /// Please return true to continue processing, false to drop with error. + /// + public Func> TryCustomizeAsync { get; set; } = async (stream, applicationContext, fileName) => true; +#pragma warning restore CS1998 + + #pragma warning disable CS1998 /// /// If you want to save the generated binary file to an external source such as Azure BLOB, please use this. /// Please return true to continue processing, false to drop with error. /// - public Func> OnBuildFileSuccess { get; set; } = async (byteArray, applicationContext, fileName) => true; + public Func> OnBuildFileSuccess { get; set; } = async (stream, applicationContext, fileName) => true; #pragma warning restore CS1998 @@ -221,25 +228,56 @@ public byte[] BuildFile(ActionContext context) if (context == null) throw new ArgumentNullException("context"); - if (WkhtmlPath == string.Empty) - { - var location = Assembly.GetEntryAssembly().Location; - var directory = Path.GetDirectoryName(location); - WkhtmlPath = Path.Combine(directory, "WkHtmlToPdf"); - } + SetupWkhtmlPath(); var fileContent = CallTheDriver(context); - if (string.IsNullOrEmpty(SaveOnServerPath) == false) - File.WriteAllBytes(SaveOnServerPath, fileContent); + var pdfStream = new MemoryStream(); + + pdfStream.Write(fileContent, 0, fileContent.Length); + if (!TryCustomizeAsync?.Invoke(pdfStream, context, FileName).Result ?? true) + throw new InvalidOperationException($"{nameof(TryCustomizeAsync)} returned false."); - if (!OnBuildFileSuccess?.Invoke(fileContent, context, FileName).Result ?? true) + if (!OnBuildFileSuccess?.Invoke(pdfStream.ToArray(), context, FileName).Result ?? true) throw new InvalidOperationException($"{nameof(OnBuildFileSuccess)} returned false."); - return fileContent; + return pdfStream.ToArray(); } + public async Task BuildFileAsync(ActionContext context) + { + if (context == null) + throw new ArgumentNullException("context"); + + SetupWkhtmlPath(); + + var fileContent = CallTheDriver(context); + + var pdfStream = new MemoryStream(); + + await pdfStream.WriteAsync(fileContent, 0, fileContent.Length); + + if (TryCustomizeAsync != null && !await TryCustomizeAsync.Invoke(pdfStream, context, FileName)) + throw new InvalidOperationException($"{nameof(TryCustomizeAsync)} returned false."); + + if (OnBuildFileSuccess != null && !await OnBuildFileSuccess.Invoke(pdfStream.ToArray(), context, FileName)) + throw new InvalidOperationException($"{nameof(OnBuildFileSuccess)} returned false."); + + return pdfStream; + } + + private void SetupWkhtmlPath() + { + if (WkhtmlPath == string.Empty) + { + var location = Assembly.GetEntryAssembly()?.Location; + var directory = Path.GetDirectoryName(location); + WkhtmlPath = Path.Combine(directory, "WkHtmlToPdf"); + } + } + + public override void ExecuteResult(ActionContext context) { var fileContent = BuildFile(context); @@ -249,6 +287,18 @@ public override void ExecuteResult(ActionContext context) response.Body.WriteAsync(fileContent, 0, fileContent.Length); } + public override async Task ExecuteResultAsync(ActionContext context) + { + var fileContentStream = await BuildFileAsync(context); + + var response = PrepareResponse(context.HttpContext.Response); + + fileContentStream.Position = 0; + + await fileContentStream.CopyToAsync(response.Body); + } + + private static string SanitizeFileName(string name) { var invalidChars = Regex.Escape(new string(Path.GetInvalidPathChars()) + new string(Path.GetInvalidFileNameChars())); diff --git a/src/RotativaCore/RotativaCore.csproj b/src/RotativaCore/RotativaCore.csproj index 0f41a15..1c57344 100644 --- a/src/RotativaCore/RotativaCore.csproj +++ b/src/RotativaCore/RotativaCore.csproj @@ -19,15 +19,14 @@ false false - 3.0.0 + 4.0.0 @arichika (arichika.taniguchi) RotativaCore Convert HTML to PDF by WkHtmlToPdf for ASP.NET Core on Azure Web Apps (Windows). Forked from Rotativa. Target for .NETStandard 2.0. MIT netcore;aspnetcore;pdf;wkhtmltopdf; - This is the initial version of the port and "Preview Release". It is not fully tested. -Support new event. OnBuildFileSuccess Func<T>. For example, use this event to save the PDF to BLOB. + This is beta software always. It is not fully tested. https://github.com/arichika/RotativaCore https://github.com/arichika/RotativaCore