Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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:

Expand All @@ -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();
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
Expand Down
36 changes: 36 additions & 0 deletions Rotativa.AspNetCore.Tests/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
7 changes: 7 additions & 0 deletions Rotativa.AspNetCore.Tests/Dockerfile.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
**/bin
**/obj
**/.vs
.vscode
.git
**/wwwroot/Rotativa/wkhtmltopdf
**/wwwroot/Rotativa/wkhtmltoimage
3 changes: 3 additions & 0 deletions Rotativa.AspNetCore.Tests/PdfTester.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down
17 changes: 8 additions & 9 deletions Rotativa.AspNetCore.Tests/Rotativa.AspNetCore.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

Expand All @@ -10,17 +10,16 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="itext7" Version="8.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" Version="2.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="Selenium.WebDriver" Version="4.23.0" />
<PackageReference Include="System.Drawing.Common" Version="8.0.7" />
<PackageReference Include="xunit" Version="2.9.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PackageReference Include="itext7" Version="9.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.10.1" />
<PackageReference Include="Selenium.WebDriver" Version="4.50.0" />
<PackageReference Include="System.Drawing.Common" Version="10.0.12" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="4.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PackageReference Include="coverlet.collector" Version="10.1.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
48 changes: 35 additions & 13 deletions Rotativa.AspNetCore.Tests/RotativaIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<object[]> 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();
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -106,18 +128,18 @@ 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 }));
}
}

[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")]
[MemberData(nameof(DemoSites))]
public async Task Can_create_jpg_image(string url, string site)
{
selenium.Navigate().GoToUrl(url);
Expand All @@ -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 }));
}
}

Expand Down
17 changes: 17 additions & 0 deletions Rotativa.AspNetCore.Tests/run-tests.sh
Original file line number Diff line number Diff line change
@@ -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"
6 changes: 3 additions & 3 deletions Rotativa.AspNetCore/README.md
Original file line number Diff line number Diff line change
@@ -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:

Expand All @@ -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();
Expand All @@ -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

Expand Down
Loading
Loading