From 40ac2834772f718ecbb6fe4e2d47466c6e2965bd Mon Sep 17 00:00:00 2001 From: Giorgio Bozio Date: Thu, 8 Oct 2026 16:24:17 +0200 Subject: [PATCH 1/4] Add .NET 10 target and patch netstandard2.0 dependencies - Target net10.0 alongside the existing frameworks. - Merge the per-framework FrameworkReference blocks into one that covers every target except netstandard2.0. - Bump Microsoft.AspNetCore.Mvc for netstandard2.0 from 2.2.0 to 2.3.13, which resolves patched Newtonsoft.Json, System.Text.Encodings.Web and System.Security.Cryptography.Xml (NuGet audit NU1902-NU1904). - Version 1.5.0 and updated package metadata. Co-Authored-By: Claude Opus 5.5 --- .../Rotativa.AspNetCore.csproj | 36 ++++--------------- 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/Rotativa.AspNetCore/Rotativa.AspNetCore.csproj b/Rotativa.AspNetCore/Rotativa.AspNetCore.csproj index 421b9c2..cf8d832 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;net8 + netstandard2.0;netcoreapp3.1;net6;net5.0;net7;net8;net10.0 true - 1.4.0 + 1.5.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 Core, Asp.Net 6 and Asp.Net 8 - Multi target enabling .net core 3.1, .net 5, .net 6, .net 7, .net 8 + Rotativa: PDF easy creation for Asp.Net Core, Asp.Net 6, Asp.Net 8 and Asp.Net 10 + Multi target enabling .net core 3.1, .net 5, .net 6, .net 7, .net 8, .net 10 PDF AspNetCore - 2024 Viva Software di Bozio Giorgio + 2026 Viva Software di Bozio Giorgio MIT README.md @@ -21,34 +21,12 @@ --> - - - - - - - - - - - - - - - - - - - - - + - + From bb70be65ade4ad184abe2d25ed69cad3e11b7c50 Mon Sep 17 00:00:00 2001 From: Giorgio Bozio Date: Thu, 8 Oct 2026 16:24:17 +0200 Subject: [PATCH 2/4] Move tests and demo app to .NET 10 and make tests cross-platform - DemoApp and test project target net10.0; test packages updated, and the redundant Microsoft.AspNetCore.Mvc.ViewFeatures 2.2.0 reference removed. - PdfContains compares on normalized whitespace (macOS wkhtmltopdf emits tabs between words). - Image tests check PNG/JPEG signatures instead of System.Drawing, which is Windows-only. - Integration tests read ROTATIVA_DEMO_URL, ROTATIVA_TESTS_HEADLESS (or CI) and ROTATIVA_CHROME_BINARY, and now quit Chrome after each test. Co-Authored-By: Claude Opus 5.5 --- .../Rotativa.AspNetCore.DemoApp.csproj | 2 +- Rotativa.AspNetCore.Tests/PdfTester.cs | 3 ++ .../Rotativa.AspNetCore.Tests.csproj | 17 ++++--- .../RotativaIntegrationTests.cs | 48 ++++++++++++++----- 4 files changed, 47 insertions(+), 23 deletions(-) diff --git a/Rotativa.AspNetCore.DemoApp/Rotativa.AspNetCore.DemoApp.csproj b/Rotativa.AspNetCore.DemoApp/Rotativa.AspNetCore.DemoApp.csproj index 5693452..c57bf1f 100644 --- a/Rotativa.AspNetCore.DemoApp/Rotativa.AspNetCore.DemoApp.csproj +++ b/Rotativa.AspNetCore.DemoApp/Rotativa.AspNetCore.DemoApp.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable diff --git a/Rotativa.AspNetCore.Tests/PdfTester.cs b/Rotativa.AspNetCore.Tests/PdfTester.cs index aea4530..280c99c 100644 --- a/Rotativa.AspNetCore.Tests/PdfTester.cs +++ b/Rotativa.AspNetCore.Tests/PdfTester.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; namespace Rotativa.AspNetCore.Tests @@ -45,6 +46,8 @@ public bool PdfContains(string text) string currentText = PdfTextExtractor.GetTextFromPage(pdfDocument.GetPage(page), strategy); currentText = Encoding.UTF8.GetString(ASCIIEncoding.Convert(Encoding.Default, Encoding.UTF8, Encoding.Default.GetBytes(currentText))); + // Some wkhtmltopdf builds (e.g. macOS) separate words with tabs, so compare on normalized whitespace. + currentText = Regex.Replace(currentText, @"\s+", " "); if (currentText.Contains(text)) return true; pdfReader.Close(); diff --git a/Rotativa.AspNetCore.Tests/Rotativa.AspNetCore.Tests.csproj b/Rotativa.AspNetCore.Tests/Rotativa.AspNetCore.Tests.csproj index 7a97c9f..46d892f 100644 --- a/Rotativa.AspNetCore.Tests/Rotativa.AspNetCore.Tests.csproj +++ b/Rotativa.AspNetCore.Tests/Rotativa.AspNetCore.Tests.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable @@ -10,17 +10,16 @@ - - - - - - - + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Rotativa.AspNetCore.Tests/RotativaIntegrationTests.cs b/Rotativa.AspNetCore.Tests/RotativaIntegrationTests.cs index 57f7f05..3c4f367 100644 --- a/Rotativa.AspNetCore.Tests/RotativaIntegrationTests.cs +++ b/Rotativa.AspNetCore.Tests/RotativaIntegrationTests.cs @@ -15,14 +15,36 @@ namespace Rotativa.AspNetCore.Tests { [Trait("Rotativa.AspNetCore", "accessing the demo site home page")] - public class RotativaIntegrationTests + public class RotativaIntegrationTests : IDisposable { ChromeDriver selenium; StringBuilder verificationErrors; + // The demo site to test against. Override with ROTATIVA_DEMO_URL (e.g. "http://localhost:5000" on Linux/CI). + public static IEnumerable DemoSites => new[] + { + new object[] { Environment.GetEnvironmentVariable("ROTATIVA_DEMO_URL") ?? "https://localhost:56246", "Asp.net 10" } + }; + + // Run Chrome headless when ROTATIVA_TESTS_HEADLESS is set, or on CI servers (which set CI=true). + static bool Headless => + IsTrue(Environment.GetEnvironmentVariable("ROTATIVA_TESTS_HEADLESS")) || IsTrue(Environment.GetEnvironmentVariable("CI")); + + static bool IsTrue(string? value) => value == "1" || string.Equals(value, "true", StringComparison.OrdinalIgnoreCase); + public RotativaIntegrationTests() { - selenium = new ChromeDriver(); + var options = new ChromeOptions(); + if (Headless) + { + // --no-sandbox and --disable-dev-shm-usage let Chrome start inside containers. + options.AddArguments("--headless=new", "--no-sandbox", "--disable-dev-shm-usage"); + } + // Optional path to a Chrome/Chromium binary, for machines where Selenium can't find or download one. + var chromeBinary = Environment.GetEnvironmentVariable("ROTATIVA_CHROME_BINARY"); + if (!string.IsNullOrEmpty(chromeBinary)) options.BinaryLocation = chromeBinary; + + selenium = new ChromeDriver(options); //selenium = new InternetExplorerDriver(); selenium.Manage().Timeouts().ImplicitWait = new TimeSpan(0, 0, 10); verificationErrors = new StringBuilder(); @@ -37,7 +59,7 @@ public void Dispose() //[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")] + [MemberData(nameof(DemoSites))] public void Is_the_site_reachable(string url, string site) { selenium.Navigate().GoToUrl(url); @@ -49,7 +71,7 @@ public void Is_the_site_reachable(string url, string site) //[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")] + [MemberData(nameof(DemoSites))] public async Task Contact_PDF_ViewData(string url, string site) { selenium.Navigate().GoToUrl(url); @@ -73,7 +95,7 @@ public async Task Contact_PDF_ViewData(string url, string site) //[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")] + [MemberData(nameof(DemoSites))] public async Task Contact_PDF_SpecialCharacters(string url, string site) { selenium.Navigate().GoToUrl(url); @@ -95,7 +117,7 @@ public async Task Contact_PDF_SpecialCharacters(string url, string site) //[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")] + [MemberData(nameof(DemoSites))] public async Task Can_create_png_image(string url, string site) { selenium.Navigate().GoToUrl(url); @@ -106,10 +128,10 @@ public async Task Can_create_png_image(string url, string site) using (var wc = new HttpClient()) { var imageResult = await wc.GetAsync(new Uri(pdfHref)); - var image = Image.FromStream(imageResult.Content.ReadAsStream()); + var image = await imageResult.Content.ReadAsByteArrayAsync(); - Assert.NotNull(image); - Assert.Equal(image.RawFormat, System.Drawing.Imaging.ImageFormat.Png); + // PNG file signature; checked directly because System.Drawing is Windows-only. + Assert.True(image.AsSpan().StartsWith(new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A })); } } @@ -117,7 +139,7 @@ public async Task Can_create_png_image(string url, string site) //[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")] + [MemberData(nameof(DemoSites))] public async Task Can_create_jpg_image(string url, string site) { selenium.Navigate().GoToUrl(url); @@ -128,10 +150,10 @@ public async Task Can_create_jpg_image(string url, string site) using (var wc = new HttpClient()) { var imageResult = await wc.GetAsync(new Uri(pdfHref)); - var image = Image.FromStream(imageResult.Content.ReadAsStream()); + var image = await imageResult.Content.ReadAsByteArrayAsync(); - Assert.NotNull(image); - Assert.Equal(image.RawFormat, System.Drawing.Imaging.ImageFormat.Jpeg); + // JPEG SOI marker; checked directly because System.Drawing is Windows-only. + Assert.True(image.AsSpan().StartsWith(new byte[] { 0xFF, 0xD8, 0xFF })); } } From a2997e35d07c3bbafce3da0b6528bb6d24775c13 Mon Sep 17 00:00:00 2001 From: Giorgio Bozio Date: Thu, 8 Oct 2026 16:24:17 +0200 Subject: [PATCH 3/4] Add Linux test container and GitHub Actions workflow - Dockerfile (Debian 12, .NET 10, Chromium, wkhtmltopdf 0.12.6.1) and run-tests.sh that starts the demo app and runs the full suite. - Workflow builds the library for all target frameworks and runs the tests in the container on pushes to master and pull requests. - Ignore local non-Windows wkhtmltopdf/wkhtmltoimage binaries. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/tests.yml | 36 +++++++++++++++++++ .gitignore | 4 +++ Rotativa.AspNetCore.Tests/Dockerfile | 36 +++++++++++++++++++ .../Dockerfile.dockerignore | 7 ++++ Rotativa.AspNetCore.Tests/run-tests.sh | 17 +++++++++ 5 files changed, 100 insertions(+) create mode 100644 .github/workflows/tests.yml create mode 100644 Rotativa.AspNetCore.Tests/Dockerfile create mode 100644 Rotativa.AspNetCore.Tests/Dockerfile.dockerignore create mode 100644 Rotativa.AspNetCore.Tests/run-tests.sh diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..82b8fd1 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,36 @@ +name: Build and test + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: Build library (all target frameworks) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-dotnet@v6 + with: + dotnet-version: 10.0.x + + - name: Build + run: dotnet build Rotativa.AspNetCore/Rotativa.AspNetCore.csproj -c Release + + test: + name: Unit and integration tests (Linux) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Build test image + run: docker build -f Rotativa.AspNetCore.Tests/Dockerfile -t rotativa-tests . + + - name: Run tests + run: docker run --rm rotativa-tests diff --git a/.gitignore b/.gitignore index 6217e6c..1e9d50b 100644 --- a/.gitignore +++ b/.gitignore @@ -313,3 +313,7 @@ OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ + +# Local macOS/Linux wkhtmltopdf binaries (only the Windows .exe files are tracked) +**/wwwroot/Rotativa/wkhtmltopdf +**/wwwroot/Rotativa/wkhtmltoimage diff --git a/Rotativa.AspNetCore.Tests/Dockerfile b/Rotativa.AspNetCore.Tests/Dockerfile new file mode 100644 index 0000000..6246045 --- /dev/null +++ b/Rotativa.AspNetCore.Tests/Dockerfile @@ -0,0 +1,36 @@ +# Runs the full test suite (unit + integration) on Linux. +# Build from the repository root: +# docker build -f Rotativa.AspNetCore.Tests/Dockerfile -t rotativa-tests . +# docker run --rm rotativa-tests +FROM debian:bookworm-slim + +ARG TARGETARCH +ARG WKHTMLTOX_VERSION=0.12.6.1-3 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates curl libicu72 chromium chromium-driver \ + && curl -fsSL -o /tmp/wkhtmltox.deb \ + "https://github.com/wkhtmltopdf/packaging/releases/download/${WKHTMLTOX_VERSION}/wkhtmltox_${WKHTMLTOX_VERSION}.bookworm_${TARGETARCH}.deb" \ + && apt-get install -y --no-install-recommends /tmp/wkhtmltox.deb \ + && rm -rf /tmp/wkhtmltox.deb /var/lib/apt/lists/* + +RUN curl -fsSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh \ + && bash /tmp/dotnet-install.sh --channel 10.0 --install-dir /usr/share/dotnet \ + && ln -s /usr/share/dotnet/dotnet /usr/local/bin/dotnet \ + && rm /tmp/dotnet-install.sh + +ENV DOTNET_NOLOGO=1 \ + DOTNET_CLI_TELEMETRY_OPTOUT=1 \ + CI=true \ + ROTATIVA_DEMO_URL=http://localhost:5000 \ + ROTATIVA_CHROME_BINARY=/usr/bin/chromium + +WORKDIR /src +COPY . . + +RUN dotnet build Rotativa.AspNetCore.DemoApp \ + && dotnet build Rotativa.AspNetCore.Tests \ + && ln -sf /usr/local/bin/wkhtmltopdf Rotativa.AspNetCore.DemoApp/wwwroot/Rotativa/wkhtmltopdf \ + && ln -sf /usr/local/bin/wkhtmltoimage Rotativa.AspNetCore.DemoApp/wwwroot/Rotativa/wkhtmltoimage + +CMD ["bash", "Rotativa.AspNetCore.Tests/run-tests.sh"] diff --git a/Rotativa.AspNetCore.Tests/Dockerfile.dockerignore b/Rotativa.AspNetCore.Tests/Dockerfile.dockerignore new file mode 100644 index 0000000..090b609 --- /dev/null +++ b/Rotativa.AspNetCore.Tests/Dockerfile.dockerignore @@ -0,0 +1,7 @@ +**/bin +**/obj +**/.vs +.vscode +.git +**/wwwroot/Rotativa/wkhtmltopdf +**/wwwroot/Rotativa/wkhtmltoimage diff --git a/Rotativa.AspNetCore.Tests/run-tests.sh b/Rotativa.AspNetCore.Tests/run-tests.sh new file mode 100644 index 0000000..1d8a690 --- /dev/null +++ b/Rotativa.AspNetCore.Tests/run-tests.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Starts the demo app, runs the test suite against it, then stops the app. +# Run from the repository root. Expects the projects to be built already. +set -euo pipefail + +url="${ROTATIVA_DEMO_URL:-http://localhost:5000}" + +(cd Rotativa.AspNetCore.DemoApp && exec dotnet run --no-build --urls "$url") & +app_pid=$! +trap 'kill $app_pid 2>/dev/null || true' EXIT + +for _ in $(seq 1 60); do + curl -sf -o /dev/null "$url" && break + sleep 1 +done + +dotnet test Rotativa.AspNetCore.Tests --no-build --logger "console;verbosity=normal" From 0249c9cfabf58d2be65022488c267bb6a9c4c3de Mon Sep 17 00:00:00 2001 From: Giorgio Bozio Date: Thu, 8 Oct 2026 16:24:17 +0200 Subject: [PATCH 4/4] Document .NET 10 support, non-Windows binaries and running the tests Co-Authored-By: Claude Opus 5.5 --- README.md | 21 ++++++++++++++++++--- Rotativa.AspNetCore/README.md | 6 +++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8377dc2..20bb63f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Create PDFs and images with .NET 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. +This package is compatible with .NET Core 3.1, .NET 5, .NET 6, .NET 7, .NET 8 and .NET 10. ## Install with nuget.org: @@ -11,7 +11,7 @@ https://www.nuget.org/packages/Rotativa.AspNetCore Please give feedback! ## Needs configuration -Basic configuration done in Program.cs (.NET 6 up to 8): +Basic configuration done in Program.cs (.NET 6 up to 10): ```csharp app.UseRotativa(); @@ -22,7 +22,7 @@ or, if using .NET Core 3.1 and .NET 5: 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")` +Make sure you have a folder with the wkhtmltopdf.exe file (and wkhtmltoimage.exe for images) accessible by the process running the web app. On Linux and macOS the files are named wkhtmltopdf and wkhtmltoimage, without the .exe extension: use the builds for your platform from https://wkhtmltopdf.org/downloads.html. 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 @@ -149,6 +149,21 @@ public IActionResult Invoice() ## Issues and Pull Request Contribution is welcomed. If you would like to provide a PR please add some testing. +### Running the tests +The integration tests use Selenium and Chrome against the demo app (`Rotativa.AspNetCore.DemoApp`). Start the demo app at `https://localhost:56246`, then run `dotnet test Rotativa.AspNetCore.Tests`. On Linux and macOS, copy the wkhtmltopdf and wkhtmltoimage binaries for your platform into `Rotativa.AspNetCore.DemoApp/wwwroot/Rotativa` first. + +The tests can be configured with environment variables: +- `ROTATIVA_DEMO_URL`: the demo app address (default `https://localhost:56246`). +- `ROTATIVA_TESTS_HEADLESS`: set to `true` to run Chrome headless. This is automatic when `CI=true`. +- `ROTATIVA_CHROME_BINARY`: path to a Chrome or Chromium executable, if Selenium can't find one. + +To run the whole suite on Linux with Docker, from the repository root: + +``` +docker build -f Rotativa.AspNetCore.Tests/Dockerfile -t rotativa-tests . +docker run --rm rotativa-tests +``` + ## rotativa.io diff --git a/Rotativa.AspNetCore/README.md b/Rotativa.AspNetCore/README.md index 994787c..51b17ce 100644 --- a/Rotativa.AspNetCore/README.md +++ b/Rotativa.AspNetCore/README.md @@ -1,7 +1,7 @@ # Create PDFs and images with .NET 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. +This package is compatible with .NET Core 3.1, .NET 5, .NET 6, .NET 7, .NET 8 and .NET 10. ## Install with nuget.org: @@ -11,7 +11,7 @@ https://www.nuget.org/packages/Rotativa.AspNetCore Please give feedback! ## Needs configuration -Basic configuration done in Program.cs (.NET 6 up to 8): +Basic configuration done in Program.cs (.NET 6 up to 10): ```csharp app.UseRotativa(); @@ -22,7 +22,7 @@ or, if using .NET Core 3.1 and .NET 5: 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")` +Make sure you have a folder with the wkhtmltopdf.exe file (and wkhtmltoimage.exe for images) accessible by the process running the web app. On Linux and macOS the files are named wkhtmltopdf and wkhtmltoimage, without the .exe extension: use the builds for your platform from https://wkhtmltopdf.org/downloads.html. 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