diff --git a/README.md b/README.md index 69af67b..8377dc2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ -# Rotativa.AspNetCore for .Net core 3.1, .Net 5, .Net 6, .Net 7 +# Create PDFs and images with .NET -Rotativa for Asp.Net Core, easy Pdf from Razor views for .Net core 3.1, .Net 5, .Net 6, .Net 7. - -Docs are in the making. Should work almost exactly as Rotativa https://github.com/webgio/Rotativa +Use Rotativa to transform a Razor view into a PDF or image. +This package is compatible with .NET Core 3.1, .NET 5, .NET 6, .NET 7 and .NET 8. ## Install with nuget.org: @@ -12,12 +11,12 @@ https://www.nuget.org/packages/Rotativa.AspNetCore Please give feedback! ## Needs configuration -Basic configuration done in Program.cs (.net 6 or 7, beta package): +Basic configuration done in Program.cs (.NET 6 up to 8): ```csharp app.UseRotativa(); ``` -or, if using .Net Core 3.1 and .Net 5: +or, if using .NET Core 3.1 and .NET 5: ```csharp app.UseRotativa(env); @@ -25,6 +24,128 @@ app.UseRotativa(env); Make sure you have a folder with the wkhtmltopdf.exe file accessible by the process running the web app. By default it searches in a folder named "Rotativa" in the root of the web app. If you need to change that use the optional parameter to the Setup call `RotativaConfiguration.Setup(env, "path/relative/to/root")` +## Usage + +This package should work almost exactly as Rotativa https://github.com/webgio/Rotativa. + +Instead of returning a `View()` in your .NET controller, use `new ViewAsPdf()` to return a PDF or use `new ViewAsImage()` to return an image: + +```csharp +public class InvoiceController : Controller +{ + private readonly ILogger _logger; + + public InvoiceController(ILogger logger) + { + _logger = logger; + } + + public IActionResult Index() + { + // Returns the Index view as HTML. + return View(); + } + + public IActionResult Invoice() + { + // Returns the Invoice view as PDF. + return new ViewAsPdf(); + } + + public IActionResult InvoiceImage() + { + // Returns the InvoiceImage view as PDF. + return new ViewAsImage(); + } +} +``` + +You can specify the View that should be transformed into a PDF or image: + +```csharp +return new ViewAsPdf("NameOfView"); +``` + +Pass ViewData as an optional property: + +```csharp +ViewData["Message"] = "Thank you for downloading this PDF."; +return new ViewAsPdf(viewData: ViewData); +``` + +We support partial views as well: + +```csharp +return new ViewAsImage("MyPartialView", isPartialView: true); +``` + +By default Rotativa injects a base url in the head section of the HTML. This can be disabled: + +```csharp +return new ViewAsImage(setBaseUrl: false); +``` + +The settings can be combined as well: + +```csharp +ViewData["Message"] = "Thank you for downloading this PDF."; +return new ViewAsImage("MyPartialView", isPartialView: true, viewData: ViewData, setBaseUrl: false); +``` + +To change the way the PDF or image is generated, you can pass the settings as parameters: + +```csharp +return new ViewAsImage() +{ + Format = ImageFormat.png, + Quality = 90 +}; +``` + +By default the PDF or image is shown to the user in the browser, like HTML. If you want to force the document to be downloaded use the Content-Disposition property: + +```csharp +return new ViewAsPdf() +{ + ContentDisposition = ContentDisposition.Attachment, + FileName = "MyDocument.pdf" +}; +``` + +Each property is documented in the object for easy reference. + +Rotativa uses wkhtmltopdf/wkhtmltoimage behind the scenes. If you want to specify custom switches that are unsupported by Rotativa, you can pass them as well: + +```csharp +return new ViewAsPdf() +{ + CustomSwitches = "--disable-smart-shrinking" +}; +``` + +If you need to write the PDF to the server, you can call `BuildFile` and use the resulting byte array to save the file: + +```csharp +var pdfFile = new ViewAsPdf().BuildFile(this.ControllerContext); +File.WriteAllBytes("wwwroot/output.pdf", pdfFile); +``` + +This is how you save the PDF file to the server before displaying it in the browser: + +```csharp +public IActionResult Invoice() +{ + // Generate the PDF. + var pdfFile = new ViewAsPdf(); + + // Save to the server. + File.WriteAllBytes("wwwroot/output.pdf", pdfFile.BuildFile(this.ControllerContext)); + + // Show in the browser. + return pdfFile; +} +``` + ## Issues and Pull Request Contribution is welcomed. If you would like to provide a PR please add some testing. diff --git a/Rotativa.AspNetCore.DemoApp/Controllers/HomeController.cs b/Rotativa.AspNetCore.DemoApp/Controllers/HomeController.cs index 9c16a0f..f5b36bb 100644 --- a/Rotativa.AspNetCore.DemoApp/Controllers/HomeController.cs +++ b/Rotativa.AspNetCore.DemoApp/Controllers/HomeController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Rotativa.AspNetCore.DemoApp.Models; using System.Diagnostics; +using Rotativa.AspNetCore.Options; namespace Rotativa.AspNetCore.DemoApp.Controllers { @@ -29,6 +30,27 @@ public IActionResult Privacy() return new ViewAsPdf(); } + public IActionResult ContactImage() + { + ViewData["Message"] = "Your contact page image."; + + // Example on how to set custom data. + // For demo purposes we changed the name of the view, and specified that it isn't a partial view. + // IsPartialView is false by default. We add some additional ViewData. + // Using custom options 'Format' and 'Quality' as a demo. + // See AsImageResultBase for more options. + return new ViewAsImage("ContactDemo", isPartialView: false, viewData: ViewData, setBaseUrl: true) + { + Format = ImageFormat.png, + Quality = 90 + }; + } + + public IActionResult PrivacyImage() + { + return new ViewAsImage(); + } + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult Error() { diff --git a/Rotativa.AspNetCore.DemoApp/Rotativa.AspNetCore.DemoApp.csproj b/Rotativa.AspNetCore.DemoApp/Rotativa.AspNetCore.DemoApp.csproj index 6834c82..5693452 100644 --- a/Rotativa.AspNetCore.DemoApp/Rotativa.AspNetCore.DemoApp.csproj +++ b/Rotativa.AspNetCore.DemoApp/Rotativa.AspNetCore.DemoApp.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable diff --git a/Rotativa.AspNetCore.DemoApp/Views/Home/Contact.cshtml b/Rotativa.AspNetCore.DemoApp/Views/Home/Contact.cshtml index b932f6d..6d43261 100644 --- a/Rotativa.AspNetCore.DemoApp/Views/Home/Contact.cshtml +++ b/Rotativa.AspNetCore.DemoApp/Views/Home/Contact.cshtml @@ -14,4 +14,5 @@
Support: Support@example.com
Marketing: Marketing@example.com + Special character test: àéù
diff --git a/Rotativa.AspNetCore.DemoApp/Views/Home/ContactDemo.cshtml b/Rotativa.AspNetCore.DemoApp/Views/Home/ContactDemo.cshtml new file mode 100644 index 0000000..b932f6d --- /dev/null +++ b/Rotativa.AspNetCore.DemoApp/Views/Home/ContactDemo.cshtml @@ -0,0 +1,17 @@ +@{ + ViewData["Title"] = "Contact"; +} +

@ViewData["Title"]

+

@ViewData["Message"]

+ +
+ One Microsoft Way
+ Redmond, WA 98052-6399
+ P: + 425.555.0100 +
+ +
+ Support: Support@example.com
+ Marketing: Marketing@example.com +
diff --git a/Rotativa.AspNetCore.DemoApp/Views/Home/PrivacyImage.cshtml b/Rotativa.AspNetCore.DemoApp/Views/Home/PrivacyImage.cshtml new file mode 100644 index 0000000..af4fb19 --- /dev/null +++ b/Rotativa.AspNetCore.DemoApp/Views/Home/PrivacyImage.cshtml @@ -0,0 +1,6 @@ +@{ + ViewData["Title"] = "Privacy Policy"; +} +

@ViewData["Title"]

+ +

Use this page to detail your site's privacy policy.

diff --git a/Rotativa.AspNetCore.DemoApp/Views/Shared/_Layout.cshtml b/Rotativa.AspNetCore.DemoApp/Views/Shared/_Layout.cshtml index ab09ea3..e998670 100644 --- a/Rotativa.AspNetCore.DemoApp/Views/Shared/_Layout.cshtml +++ b/Rotativa.AspNetCore.DemoApp/Views/Shared/_Layout.cshtml @@ -25,9 +25,15 @@ + + diff --git a/Rotativa.AspNetCore.Tests/OptionFlagTest.cs b/Rotativa.AspNetCore.Tests/OptionFlagTest.cs new file mode 100644 index 0000000..d8d4f63 --- /dev/null +++ b/Rotativa.AspNetCore.Tests/OptionFlagTest.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Rotativa.AspNetCore.Tests +{ + + [Trait("Rotativa.AspNetCore", "testing if the option flags are passed to wkhtmltopdf/wkhtmltoimage in the right way.")] + public class OptionFlagTest + { + StringBuilder verificationErrors; + + public OptionFlagTest() + { + verificationErrors = new StringBuilder(); + } + + [Fact(DisplayName = "should not pass options by default.")] + public void NoOptions_ShouldNotPassOptions() + { + var test = new ViewAsPdf(); + + Assert.Empty(test.GetConvertOptions()); + } + + [Fact(DisplayName = "zoom option flag is outputted in wkhtml format.")] + public void SingleOption_Zoom_ShouldBeFormatted() + { + var test = new ViewAsPdf() + { + Zoom = 1.5 + }; + + Assert.Equal("--zoom 1.5", test.GetConvertOptions()); + } + + [Fact(DisplayName = "boolean option flag are outputted in wkhtml format.")] + public void SingleOption_Boolean_ShouldBeFormatted() + { + var test = new ViewAsPdf() + { + NoImages = true + }; + + Assert.Equal("--no-images", test.GetConvertOptions()); + } + + [Fact(DisplayName = "multiple option flags should be combined to one option string.")] + public void MultipleOption_Boolean_ShouldBeCombined() + { + var test = new ViewAsPdf() + { + IsLowQuality = true, + NoImages = true + }; + + Assert.Equal("-l --no-images", test.GetConvertOptions()); + } + + [Fact(DisplayName = "dictionary options should be repeated for each key")] + public void DictionaryOption_ShouldBeFormatted() + { + var test = new ViewAsPdf() + { + CustomHeaders = new Dictionary + { + { "Header1", "value" }, + { "Header2", "value" }, + } + }; + + Assert.Equal("--custom-header Header1 value --custom-header Header2 value", test.GetConvertOptions()); + } + } +} diff --git a/Rotativa.AspNetCore.Tests/PDFParser.cs b/Rotativa.AspNetCore.Tests/PDFParser.cs index 7dbe475..d17f3c8 100644 --- a/Rotativa.AspNetCore.Tests/PDFParser.cs +++ b/Rotativa.AspNetCore.Tests/PDFParser.cs @@ -1,4 +1,4 @@ -using iTextSharp.text.pdf; +using iText.Kernel.Pdf; using System; using System.Collections.Generic; using System.IO; @@ -43,19 +43,20 @@ public bool ExtractText(string inFileName, string outFileName) { // Create a reader for the given PDF file var reader = new PdfReader(inFileName); + var document = new PdfDocument(reader); //outFile = File.CreateText(outFileName); outFile = new StreamWriter(outFileName, false, System.Text.Encoding.UTF8); Console.Write("Processing: "); int totalLen = 68; - float charUnit = ((float)totalLen) / (float)reader.NumberOfPages; + float charUnit = ((float)totalLen) / (float)document.GetNumberOfPages(); int totalWritten = 0; float curUnit = 0; - for (int page = 1; page <= reader.NumberOfPages; page++) + for (int page = 1; page <= document.GetNumberOfPages(); page++) { - outFile.Write(ExtractTextFromPDFBytes(reader.GetPageContent(page)) + " "); + outFile.Write(ExtractTextFromPDFBytes(document.GetPage(page).GetContentBytes()) + " "); // Write the progress. if (charUnit >= 1.0f) diff --git a/Rotativa.AspNetCore.Tests/PdfTester.cs b/Rotativa.AspNetCore.Tests/PdfTester.cs index ce1a96f..aea4530 100644 --- a/Rotativa.AspNetCore.Tests/PdfTester.cs +++ b/Rotativa.AspNetCore.Tests/PdfTester.cs @@ -1,6 +1,7 @@ -using iTextSharp.text.exceptions; -using iTextSharp.text.pdf; -using iTextSharp.text.pdf.parser; +using iText.Kernel.Exceptions; +using iText.Kernel.Pdf; +using iText.Kernel.Pdf.Canvas.Parser; +using iText.Kernel.Pdf.Canvas.Parser.Listener; using System; using System.Collections.Generic; using System.Linq; @@ -22,12 +23,12 @@ public void LoadPdf(byte[] pdfcontent) { try { - this.pdfReader = new PdfReader(pdfcontent); + this.pdfReader = new PdfReader(new MemoryStream(pdfcontent)); var parser = new PDFParser(); var parsed = parser.ExtractTextFromPDFBytes(pdfcontent); this.PdfIsValid = true; } - catch (InvalidPdfException ex) + catch (PdfException ex) { this.PdfException = ex; this.PdfIsValid = false; @@ -36,10 +37,12 @@ public void LoadPdf(byte[] pdfcontent) public bool PdfContains(string text) { - for (int page = 1; page <= pdfReader.NumberOfPages; page++) + var pdfDocument = new PdfDocument(this.pdfReader); + + for (int page = 1; page <= pdfDocument.GetNumberOfPages(); page++) { var strategy = new SimpleTextExtractionStrategy(); - string currentText = PdfTextExtractor.GetTextFromPage(pdfReader, page, strategy); + string currentText = PdfTextExtractor.GetTextFromPage(pdfDocument.GetPage(page), strategy); currentText = Encoding.UTF8.GetString(ASCIIEncoding.Convert(Encoding.Default, Encoding.UTF8, Encoding.Default.GetBytes(currentText))); if (currentText.Contains(text)) diff --git a/Rotativa.AspNetCore.Tests/Rotativa.AspNetCore.Tests.csproj b/Rotativa.AspNetCore.Tests/Rotativa.AspNetCore.Tests.csproj index 2c35671..7a97c9f 100644 --- a/Rotativa.AspNetCore.Tests/Rotativa.AspNetCore.Tests.csproj +++ b/Rotativa.AspNetCore.Tests/Rotativa.AspNetCore.Tests.csproj @@ -1,7 +1,7 @@ - + - net6.0 + net8.0 enable enable @@ -10,18 +10,24 @@ - - - - - + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all + + + + diff --git a/Rotativa.AspNetCore.Tests/RotativaTests.cs b/Rotativa.AspNetCore.Tests/RotativaIntegrationTests.cs similarity index 77% rename from Rotativa.AspNetCore.Tests/RotativaTests.cs rename to Rotativa.AspNetCore.Tests/RotativaIntegrationTests.cs index 76d53fb..57f7f05 100644 --- a/Rotativa.AspNetCore.Tests/RotativaTests.cs +++ b/Rotativa.AspNetCore.Tests/RotativaIntegrationTests.cs @@ -15,12 +15,12 @@ namespace Rotativa.AspNetCore.Tests { [Trait("Rotativa.AspNetCore", "accessing the demo site home page")] - public class RotativaTests + public class RotativaIntegrationTests { ChromeDriver selenium; StringBuilder verificationErrors; - public RotativaTests() + public RotativaIntegrationTests() { selenium = new ChromeDriver(); //selenium = new InternetExplorerDriver(); @@ -36,62 +36,104 @@ public void Dispose() [Theory(DisplayName = "should return the demo home page")] //[InlineData("http://localhost:64310", "Asp.net core 2.0")] //[InlineData("https://localhost:44375", "Asp.net core 3.1")] - [InlineData("https://localhost:7059", "Asp.net 6")] + //[InlineData("https://localhost:7059", "Asp.net 6")] + [InlineData("https://localhost:56246", "Asp.net 8")] public void Is_the_site_reachable(string url, string site) { selenium.Navigate().GoToUrl(url); + Assert.Equal("Home Page - Rotativa.AspNetCore.Demo", selenium.Title); } [Theory(DisplayName = "can get the PDF from the contact link")] //[InlineData("http://localhost:64310", "Asp.net core 2.0")] //[InlineData("https://localhost:44375", "Asp.net core 3.1")] - [InlineData("https://localhost:7059", "Asp.net 6")] - public void Contact_PDF(string url, string site) + //[InlineData("https://localhost:7059", "Asp.net 6")] + [InlineData("https://localhost:56246", "Asp.net 8")] + public async Task Contact_PDF_ViewData(string url, string site) { selenium.Navigate().GoToUrl(url); + var testLink = selenium.FindElement(By.LinkText("Contact")); var pdfHref = testLink.GetAttribute("href"); - using (var wc = new WebClient()) + + using (var wc = new HttpClient()) { - var pdfResult = wc.DownloadData(new Uri(pdfHref)); + var pdfResult = await wc.GetAsync(new Uri(pdfHref)); var pdfTester = new PdfTester(); - pdfTester.LoadPdf(pdfResult); + pdfTester.LoadPdf(await pdfResult.Content.ReadAsByteArrayAsync()); Assert.True(pdfTester.PdfIsValid); + + // This should be present, as it's set in the viewdata. Assert.True(pdfTester.PdfContains("Your contact page.")); - // pdfTester.PdfContains("admin").Should().Be.True(); } } - //[Fact] - //public void Can_print_the_test_image() - //{ + [Theory(DisplayName = "can get a PDF with special characters")] + //[InlineData("http://localhost:64310", "Asp.net core 2.0")] + //[InlineData("https://localhost:44375", "Asp.net core 3.1")] + //[InlineData("https://localhost:7059", "Asp.net 6")] + [InlineData("https://localhost:56246", "Asp.net 8")] + public async Task Contact_PDF_SpecialCharacters(string url, string site) + { + selenium.Navigate().GoToUrl(url); + var testLink = selenium.FindElement(By.LinkText("Contact")); + var pdfHref = testLink.GetAttribute("href"); - // var testLink = selenium.FindElement(By.LinkText("Test Image")); - // var pdfHref = testLink.GetAttribute("href"); - // using (var wc = new WebClient()) - // { - // var imageResult = wc.DownloadData(new Uri(pdfHref)); - // var image = Image.FromStream(new MemoryStream(imageResult)); - // image.Should().Not.Be.Null(); - // image.RawFormat.Should().Be.EqualTo(ImageFormat.Jpeg); - // } - //} + using (var wc = new HttpClient()) + { + var pdfResult = await wc.GetAsync(new Uri(pdfHref)); + var pdfTester = new PdfTester(); + pdfTester.LoadPdf(await pdfResult.Content.ReadAsByteArrayAsync()); - //[Fact] - //public void Can_print_the_test_image_png() - //{ + Assert.True(pdfTester.PdfIsValid); + Assert.True(pdfTester.PdfContains("àéù")); + } + } - // var testLink = selenium.FindElement(By.LinkText("Test Image Png")); - // var pdfHref = testLink.GetAttribute("href"); - // using (var wc = new WebClient()) - // { - // var imageResult = wc.DownloadData(new Uri(pdfHref)); - // var image = Image.FromStream(new MemoryStream(imageResult)); - // image.Should().Not.Be.Null(); - // image.RawFormat.Should().Be.EqualTo(ImageFormat.Png); - // } - //} + [Theory(DisplayName = "can get the png from the contact image link")] + //[InlineData("http://localhost:64310", "Asp.net core 2.0")] + //[InlineData("https://localhost:44375", "Asp.net core 3.1")] + //[InlineData("https://localhost:7059", "Asp.net 6")] + [InlineData("https://localhost:56246", "Asp.net 8")] + public async Task Can_create_png_image(string url, string site) + { + selenium.Navigate().GoToUrl(url); + + var testLink = selenium.FindElement(By.LinkText("Contact Image")); + var pdfHref = testLink.GetAttribute("href"); + + using (var wc = new HttpClient()) + { + var imageResult = await wc.GetAsync(new Uri(pdfHref)); + var image = Image.FromStream(imageResult.Content.ReadAsStream()); + + Assert.NotNull(image); + Assert.Equal(image.RawFormat, System.Drawing.Imaging.ImageFormat.Png); + } + } + + [Theory(DisplayName = "can get the jpg from the privacy image link")] + //[InlineData("http://localhost:64310", "Asp.net core 2.0")] + //[InlineData("https://localhost:44375", "Asp.net core 3.1")] + //[InlineData("https://localhost:7059", "Asp.net 6")] + [InlineData("https://localhost:56246", "Asp.net 8")] + public async Task Can_create_jpg_image(string url, string site) + { + selenium.Navigate().GoToUrl(url); + + var testLink = selenium.FindElement(By.LinkText("Privacy Image")); + var pdfHref = testLink.GetAttribute("href"); + + using (var wc = new HttpClient()) + { + var imageResult = await wc.GetAsync(new Uri(pdfHref)); + var image = Image.FromStream(imageResult.Content.ReadAsStream()); + + Assert.NotNull(image); + Assert.Equal(image.RawFormat, System.Drawing.Imaging.ImageFormat.Jpeg); + } + } //[Fact] //public void Can_print_the_authorized_pdf() @@ -195,22 +237,6 @@ public void Contact_PDF(string url, string site) // } //} - //[Fact] - //public void Can_print_the_pdf_from_a_view_with_non_ascii_chars() - //{ - - // var testLink = selenium.FindElement(By.LinkText("Test View")); - // var pdfHref = testLink.GetAttribute("href"); - // using (var wc = new WebClient()) - // { - // var pdfResult = wc.DownloadData(new Uri(pdfHref)); - // var pdfTester = new PdfTester(); - // pdfTester.LoadPdf(pdfResult); - // pdfTester.PdfIsValid.Should().Be.True(); - // pdfTester.PdfContains("àéù").Should().Be.True(); - // } - //} - //[Fact] //public void Can_print_the_image_from_a_view_with_non_ascii_chars() //{ diff --git a/Rotativa.AspNetCore.sln b/Rotativa.AspNetCore.sln index ff13c7d..46cdae7 100644 --- a/Rotativa.AspNetCore.sln +++ b/Rotativa.AspNetCore.sln @@ -11,7 +11,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rotativa.AspNetCore.DemoCor EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rotativa.AspNetCore.Demo", "Rotativa.AspNetCore.Demo\Rotativa.AspNetCore.Demo.csproj", "{AC72409F-AAC8-496F-920A-06D92755AC38}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rotativa.AspNetCore.Tests", "Rotativa.AspNetCore.Tests\Rotativa.AspNetCore.Tests.csproj", "{DD6DAABC-D9A0-4744-8FF9-5318EF756524}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rotativa.AspNetCore.Tests", "Rotativa.AspNetCore.Tests\Rotativa.AspNetCore.Tests.csproj", "{DD6DAABC-D9A0-4744-8FF9-5318EF756524}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{837BF8ED-3A93-4648-ABBC-38C0F6F79C66}" + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + LICENSE = LICENSE + README.md = README.md + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -24,6 +31,7 @@ Global {D9D23B29-06C7-4130-84B5-100E9E3DA048}.Release|Any CPU.ActiveCfg = Release|Any CPU {D9D23B29-06C7-4130-84B5-100E9E3DA048}.Release|Any CPU.Build.0 = Release|Any CPU {9E33D9A0-2F7B-467B-8B58-D62D44C9E0E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9E33D9A0-2F7B-467B-8B58-D62D44C9E0E7}.Debug|Any CPU.Build.0 = Debug|Any CPU {9E33D9A0-2F7B-467B-8B58-D62D44C9E0E7}.Release|Any CPU.ActiveCfg = Release|Any CPU {9E33D9A0-2F7B-467B-8B58-D62D44C9E0E7}.Release|Any CPU.Build.0 = Release|Any CPU {68798C66-D7C2-46B1-A232-0CFC7BEDD72E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU diff --git a/Rotativa.AspNetCore/AsImageResultBase.cs b/Rotativa.AspNetCore/AsImageResultBase.cs new file mode 100644 index 0000000..f3de64d --- /dev/null +++ b/Rotativa.AspNetCore/AsImageResultBase.cs @@ -0,0 +1,73 @@ +using Rotativa.AspNetCore.Options; + +namespace Rotativa.AspNetCore +{ + public abstract class AsImageResultBase : AsResultBase + { + /// + /// Set height for cropping + /// + [OptionFlag("-f")] + public ImageFormat? Format { get; set; } + + /// + /// Output image quality (between 0 and 100) (default 94) + /// + [OptionFlag("--quality")] + public int? Quality { get; set; } + + /// + /// Set height for cropping + /// + [OptionFlag("--crop-h")] + public int? CropHeight { get; set; } + + /// + /// Set width for cropping + /// + [OptionFlag("--crop-w")] + public int? CropWidth { get; set; } + + /// + /// Set x coordinate for cropping + /// + [OptionFlag("--crop-x")] + public int? CropX { get; set; } + + /// + /// Set y coordinate for cropping + /// + [OptionFlag("--crop-y")] + public int? CropY { get; set; } + + /// + /// Sets the page width. + /// + /// Set screen width, note that this is used only as a guideline. + [OptionFlag("--width")] + public int? PageWidth { get; set; } + + /// + /// Sets the page height in mm. + /// + /// Has priority over but has to be also specified. + [OptionFlag("--height")] + public int? PageHeight { get; set; } + + protected override byte[] WkhtmlConvert(string switches) + { + return WkhtmltoimageDriver.Convert(this.WkhtmlPath, switches); + } + + protected override string GetContentType() + { + var imageFormat = this.Format; + if (!imageFormat.HasValue) + { + imageFormat = ImageFormat.jpeg; + } + + return string.Format("image/{0}", imageFormat); + } + } +} \ No newline at end of file diff --git a/Rotativa.AspNetCore/AsPdfResultBase.cs b/Rotativa.AspNetCore/AsPdfResultBase.cs index 4c6d3a1..3d67041 100644 --- a/Rotativa.AspNetCore/AsPdfResultBase.cs +++ b/Rotativa.AspNetCore/AsPdfResultBase.cs @@ -11,6 +11,7 @@ protected AsPdfResultBase() { this.PageMargins = new Margins(); } + /// /// Sets the page size. /// @@ -55,7 +56,7 @@ protected override string GetContentType() /// /// Path to wkhtmltopdf binary. /// - [Obsolete("Use WkhtmlPath instead of CookieName.", false)] + [Obsolete("Use WkhtmlPath instead of WkhtmltopdfPath.", false)] public string WkhtmltopdfPath { get @@ -86,7 +87,7 @@ public string WkhtmltopdfPath [OptionFlag("-g")] public bool IsGrayScale { get; set; } - protected override string GetConvertOptions() + public override string GetConvertOptions() { var result = new StringBuilder(); diff --git a/Rotativa.AspNetCore/AsResultBase.cs b/Rotativa.AspNetCore/AsResultBase.cs index 05a1c8a..bd58419 100644 --- a/Rotativa.AspNetCore/AsResultBase.cs +++ b/Rotativa.AspNetCore/AsResultBase.cs @@ -7,6 +7,12 @@ using System.Text.RegularExpressions; using Rotativa.AspNetCore.Options; using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewEngines; + +using Microsoft.AspNetCore.Mvc.ViewFeatures; + #if NET5_0_OR_GREATER using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Http; @@ -20,21 +26,39 @@ namespace Rotativa.AspNetCore { - public abstract class AsResultBase : ViewResult //IActionResult + public abstract class AsResultBase : ViewResult { protected AsResultBase() { this.WkhtmlPath = string.Empty; this.FormsAuthenticationCookieName = ".ASPXAUTH"; + this.IsPartialView = false; + this.SetBaseUrl = true; + this.DontStopSlowScripts = false; + this.NoImages = false; } + /// + /// Determines if the view that is referenced is partial or not. + /// + public bool IsPartialView { get; set; } + + /// + /// Whether we add a base URL when we generate the HTML. + /// + /// + /// This was always on because it wasn't configurable (<= 1.3.2). That's why the default is on. + /// However it's cleaner to allow developers to set it themselves, and only add the BaseUrl when requested. + /// + public bool SetBaseUrl { get; set; } + /// /// This will be send to the browser as a name of the generated PDF file. /// public string FileName { get; set; } /// - /// Path to wkhtmltopdf\wkhtmltoimage binary. + /// Path to wkhtmltopdf / wkhtmltoimage binary. /// public string WkhtmlPath { get; set; } @@ -77,12 +101,36 @@ public string CookieName [OptionFlag("-n")] public bool IsJavaScriptDisabled { get; set; } + /// + /// Indicates whether the page can run JavaScript. + /// + [OptionFlag("--no-stop-slow-scripts")] + public bool DontStopSlowScripts { get; set; } + + /// + /// Specify a user style sheet, to load with every page. + /// + [OptionFlag("--user-style-sheet")] + public string UserStyleSheet { get; set; } + /// /// Minimum font size. /// [OptionFlag("--minimum-font-size")] public int? MinimumFontSize { get; set; } + /// + /// Sets the zoom level. + /// + [OptionFlag("--zoom")] + public double? Zoom { get; set; } + + /// + /// Do not load or print images. + /// + [OptionFlag("--no-images")] + public bool NoImages { get; set; } + /// /// Sets proxy server. /// @@ -101,6 +149,18 @@ public string CookieName [OptionFlag("--password")] public string Password { get; set; } + /// + /// Password to ssl client cert private key. + /// + [OptionFlag("--ssl-key-password")] + public string SslKeyPassword { get; set; } + + /// + /// Path to the ssl client cert public key in OpenSSL PEM format, optionally followed by intermediate ca and trusted certs + /// + [OptionFlag("--ssl-crt-path")] + public string SslCrtPath { get; set; } + /// /// Use this if you need another switches that are not currently supported by Rotativa. /// @@ -115,10 +175,10 @@ public string CookieName protected abstract string GetUrl(Microsoft.AspNetCore.Mvc.ActionContext context); /// - /// Returns properties with OptionFlag attribute as one line that can be passed to wkhtmltopdf binary. + /// Returns properties with OptionFlag attribute as one line that can be passed to wkhtmltopdf / wkhtmltoimage binary. /// - /// Command line parameter that can be directly passed to wkhtmltopdf binary. - protected virtual string GetConvertOptions() + /// Command line parameter that can be directly passed to wkhtmltopdf / wkhtmltoimage binary. + public virtual string GetConvertOptions() { var result = new StringBuilder(); @@ -244,5 +304,72 @@ protected HttpResponse PrepareResponse(HttpResponse response) } protected abstract string GetContentType(); + + /// + /// Get the view out of the context. + /// + /// The action context + /// + /// + protected IView GetView(ActionContext context) + { + // Use current action name if the view name was not provided + if (string.IsNullOrEmpty(ViewName)) + { + ViewName = ((Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor)context.ActionDescriptor).ActionName; + } + + var engine = context.HttpContext.RequestServices.GetService(typeof(ICompositeViewEngine)) as ICompositeViewEngine; + var getViewResult = engine.GetView(executingFilePath: null, viewPath: ViewName, isMainPage: !IsPartialView); + if (getViewResult.Success) + { + return getViewResult.View; + } + + var findViewResult = engine.FindView(context, ViewName, isMainPage: !IsPartialView); + if (findViewResult.Success) + { + return findViewResult.View; + } + + var searchedLocations = getViewResult.SearchedLocations.Concat(findViewResult.SearchedLocations); + var errorMessage = string.Join( + System.Environment.NewLine, + new[] { $"Unable to find view '{ViewName}'. The following locations were searched:" }.Concat(searchedLocations)); + + throw new InvalidOperationException(errorMessage); + } + + protected async Task GetHtmlFromView(ActionContext context) + { + var view = GetView(context); + var html = new StringBuilder(); + + ITempDataProvider tempDataProvider = context.HttpContext.RequestServices.GetService(typeof(ITempDataProvider)) as ITempDataProvider; + var tempDataDictionary = new TempDataDictionary(context.HttpContext, tempDataProvider); + + using (var output = new StringWriter()) + { + var viewContext = new ViewContext( + context, + view, + this.ViewData, + tempDataDictionary, + output, + new HtmlHelperOptions()); + + await view.RenderAsync(viewContext); + + html = output.GetStringBuilder(); + } + + if (!this.SetBaseUrl) + { + return html.ToString(); + } + + string baseUrl = string.Format("{0}://{1}", context.HttpContext.Request.Scheme, context.HttpContext.Request.Host); + return Regex.Replace(html.ToString(), "", string.Format("", baseUrl), RegexOptions.IgnoreCase); + } } } \ No newline at end of file diff --git a/Rotativa.AspNetCore/README.md b/Rotativa.AspNetCore/README.md index 6e80a4b..994787c 100644 --- a/Rotativa.AspNetCore/README.md +++ b/Rotativa.AspNetCore/README.md @@ -1,13 +1,9 @@ -# Rotativa.AspNetCore +# Create PDFs and images with .NET -Rotativa for Asp.Net Core. +Use Rotativa to transform a Razor view into a PDF or image. +This package is compatible with .NET Core 3.1, .NET 5, .NET 6, .NET 7 and .NET 8. -Docs are in the making. Should work almost exactly as Rotativa https://github.com/webgio/Rotativa - -## Development version -This is the first version of Rotativa for Asp.Net Core. - -Install with nuget.org: +## Install with nuget.org: https://www.nuget.org/packages/Rotativa.AspNetCore @@ -15,12 +11,12 @@ https://www.nuget.org/packages/Rotativa.AspNetCore Please give feedback! ## Needs configuration -Basic configuration done in Program.cs (.net 6 or 7): +Basic configuration done in Program.cs (.NET 6 up to 8): ```csharp app.UseRotativa(); ``` -or, if using .Net Core 3.1 and .Net 5: +or, if using .NET Core 3.1 and .NET 5: ```csharp app.UseRotativa(env); @@ -28,10 +24,132 @@ app.UseRotativa(env); Make sure you have a folder with the wkhtmltopdf.exe file accessible by the process running the web app. By default it searches in a folder named "Rotativa" in the root of the web app. If you need to change that use the optional parameter to the Setup call `RotativaConfiguration.Setup(env, "path/relative/to/root")` +## Usage + +This package should work almost exactly as Rotativa https://github.com/webgio/Rotativa. + +Instead of returning a `View()` in your .NET controller, use `new ViewAsPdf()` to return a PDF or use `new ViewAsImage()` to return an image: + +```csharp +public class InvoiceController : Controller +{ + private readonly ILogger _logger; + + public InvoiceController(ILogger logger) + { + _logger = logger; + } + + public IActionResult Index() + { + // Returns the Index view as HTML. + return View(); + } + + public IActionResult Invoice() + { + // Returns the Invoice view as PDF. + return new ViewAsPdf(); + } + + public IActionResult InvoiceImage() + { + // Returns the InvoiceImage view as PDF. + return new ViewAsImage(); + } +} +``` + +You can specify the View that should be transformed into a PDF or image: + +```csharp +return new ViewAsPdf("NameOfView"); +``` + +Pass ViewData as an optional property: + +```csharp +ViewData["Message"] = "Thank you for downloading this PDF."; +return new ViewAsPdf(viewData: ViewData); +``` + +We support partial views as well: + +```csharp +return new ViewAsImage("MyPartialView", isPartialView: true); +``` + +By default Rotativa injects a base url in the head section of the HTML. This can be disabled: + +```csharp +return new ViewAsImage(setBaseUrl: false); +``` + +The settings can be combined as well: + +```csharp +ViewData["Message"] = "Thank you for downloading this PDF."; +return new ViewAsImage("MyPartialView", isPartialView: true, viewData: ViewData, setBaseUrl: false); +``` + +To change the way the PDF or image is generated, you can pass the settings as parameters: + +```csharp +return new ViewAsImage() +{ + Format = ImageFormat.png, + Quality = 90 +}; +``` + +By default the PDF or image is shown to the user in the browser, like HTML. If you want to force the document to be downloaded use the Content-Disposition property: + +```csharp +return new ViewAsPdf() +{ + ContentDisposition = ContentDisposition.Attachment, + FileName = "MyDocument.pdf" +}; +``` + +Each property is documented in the object for easy reference. + +Rotativa uses wkhtmltopdf/wkhtmltoimage behind the scenes. If you want to specify custom switches that are unsupported by Rotativa, you can pass them as well: + +```csharp +return new ViewAsPdf() +{ + CustomSwitches = "--disable-smart-shrinking" +}; +``` + +If you need to write the PDF to the server, you can call `BuildFile` and use the resulting byte array to save the file: + +```csharp +var pdfFile = new ViewAsPdf().BuildFile(this.ControllerContext); +File.WriteAllBytes("wwwroot/output.pdf", pdfFile); +``` + +This is how you save the PDF file to the server before displaying it in the browser: + +```csharp +public IActionResult Invoice() +{ + // Generate the PDF. + var pdfFile = new ViewAsPdf(); + + // Save to the server. + File.WriteAllBytes("wwwroot/output.pdf", pdfFile.BuildFile(this.ControllerContext)); + + // Show in the browser. + return pdfFile; +} +``` + ## Issues and Pull Request Contribution is welcomed. If you would like to provide a PR please add some testing. ## rotativa.io -[rotativa.io](https://rotativa.io) is an API (SaaS) version of Rotativa, hosted on Azure. Works with just a HTTP call, no need to host the process on your server. You can register for free. +[rotativa.io](https://rotativa.io) is an API (SaaS) version of Rotativa, hosted on Azure. Works with just a HTTP call, no need to host the process on your server. You can register for free. \ No newline at end of file diff --git a/Rotativa.AspNetCore/Rotativa.AspNetCore.csproj b/Rotativa.AspNetCore/Rotativa.AspNetCore.csproj index f38b4f3..421b9c2 100644 --- a/Rotativa.AspNetCore/Rotativa.AspNetCore.csproj +++ b/Rotativa.AspNetCore/Rotativa.AspNetCore.csproj @@ -1,18 +1,18 @@  - netstandard2.0;netcoreapp3.1;net6;net5.0;net7 + netstandard2.0;netcoreapp3.1;net6;net5.0;net7;net8 true - 1.3.2 + 1.4.0 true https://github.com/webgio/Rotativa.AspNetCore https://github.com/webgio/Rotativa.AspNetCore Viva Software di Bozio Giorgio Giorgio Bozio - Rotativa: PDF easy creation for Asp.Net 6 and Asp.Net Core - Multi target enabling .net core 3.1, .net 5, .net 6, .net 7 + Rotativa: PDF easy creation for Asp.Net Core, Asp.Net 6 and Asp.Net 8 + Multi target enabling .net core 3.1, .net 5, .net 6, .net 7, .net 8 PDF AspNetCore - 2023 Viva Software di Bozio Giorgio + 2024 Viva Software di Bozio Giorgio MIT README.md @@ -30,6 +30,10 @@ + + + + @@ -49,7 +53,7 @@ - +