diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index b26bbb4bc..53e17ed0c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -52,9 +52,17 @@ jobs:
run: dotnet build Source/HtmlRenderer.sln --configuration Release --no-restore
- name: Run Tests
+ if: runner.os == 'Windows'
run: dotnet test Source/HtmlRenderer.sln --configuration Release --no-build --logger trx --results-directory ${{ github.workspace }}/TestResults
env:
PDF_OUTPUT_DIRECTORY: ${{ github.workspace }}/pdf-artifacts/${{ matrix.platform.name }}-${{ matrix.dotnet.name }}
+ HTML_RENDERER_REGRESSION_OUTPUT_DIRECTORY: ${{ github.workspace }}/image-regression/${{ matrix.platform.name }}-${{ matrix.dotnet.name }}
+
+ - name: Run Tests
+ if: runner.os != 'Windows'
+ run: dotnet test Source/Test/**/*.Test.csproj --configuration Release --no-build --logger trx --results-directory ${{ github.workspace }}/TestResults
+ env:
+ PDF_OUTPUT_DIRECTORY: ${{ github.workspace }}/pdf-artifacts/${{ matrix.platform.name }}-${{ matrix.dotnet.name }}
- name: Create HtmlRenderer.Core NuGet package
run: dotnet pack Source/HtmlRenderer/HtmlRenderer.csproj --configuration Release --include-symbols -p:SymbolPackageFormat=snupkg --no-build --verbosity normal --output ${{ env.NuGetDirectory }}
@@ -82,6 +90,14 @@ jobs:
path: ${{ github.workspace }}/pdf-artifacts/**/*.pdf
if-no-files-found: ignore
+ - name: Upload image regression artifacts
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: "Image regression artifacts (${{ matrix.platform.name }} ${{ matrix.dotnet.name }})"
+ path: ${{ github.workspace }}/image-regression/**/*.png
+ if-no-files-found: ignore
+
- name: NuGet Login
if: startsWith(github.ref, 'refs/tags/') && matrix.dotnet.name == '.NET 8' && runner.os == 'Windows'
uses: NuGet/login@v1
diff --git a/Source/HtmlRenderer.sln b/Source/HtmlRenderer.sln
index 56fea4960..5ebedf2c0 100644
--- a/Source/HtmlRenderer.sln
+++ b/Source/HtmlRenderer.sln
@@ -23,6 +23,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{E508FA78-D
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HtmlRenderer.PdfSharp.Test", "Test\HtmlRenderer.PdfSharp.Test\HtmlRenderer.PdfSharp.Test.csproj", "{AF39851E-37B9-409E-A34D-CE091A098C86}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HtmlRenderer.Test", "Test\HtmlRenderer.IntegrationTest\HtmlRenderer.IntegrationTest.csproj", "{A12F4432-337B-4030-B834-5D312A9FB16C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -115,6 +117,18 @@ Global
{AF39851E-37B9-409E-A34D-CE091A098C86}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{AF39851E-37B9-409E-A34D-CE091A098C86}.Release|x86.ActiveCfg = Release|Any CPU
{AF39851E-37B9-409E-A34D-CE091A098C86}.Release|x86.Build.0 = Release|Any CPU
+ {A12F4432-337B-4030-B834-5D312A9FB16C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A12F4432-337B-4030-B834-5D312A9FB16C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A12F4432-337B-4030-B834-5D312A9FB16C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {A12F4432-337B-4030-B834-5D312A9FB16C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {A12F4432-337B-4030-B834-5D312A9FB16C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A12F4432-337B-4030-B834-5D312A9FB16C}.Debug|x86.Build.0 = Debug|Any CPU
+ {A12F4432-337B-4030-B834-5D312A9FB16C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A12F4432-337B-4030-B834-5D312A9FB16C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A12F4432-337B-4030-B834-5D312A9FB16C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {A12F4432-337B-4030-B834-5D312A9FB16C}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {A12F4432-337B-4030-B834-5D312A9FB16C}.Release|x86.ActiveCfg = Release|Any CPU
+ {A12F4432-337B-4030-B834-5D312A9FB16C}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -124,6 +138,7 @@ Global
{8AD34FE8-8382-4A8A-B3AA-A0392ED42423} = {E263EA16-2E6A-4269-A319-AA2F97ADA8E1}
{F02E0216-4AE3-474F-9381-FCB93411CDB0} = {E263EA16-2E6A-4269-A319-AA2F97ADA8E1}
{AF39851E-37B9-409E-A34D-CE091A098C86} = {E508FA78-D57E-492F-9BF7-23640001848A}
+ {A12F4432-337B-4030-B834-5D312A9FB16C} = {E508FA78-D57E-492F-9BF7-23640001848A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {902788D6-3165-491D-B860-DF2F74E66C1D}
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/ACID 1.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/ACID 1.png
new file mode 100644
index 000000000..c5686cf06
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/ACID 1.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Anchors.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Anchors.png
new file mode 100644
index 000000000..ac8c207c4
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Anchors.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Background Image.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Background Image.png
new file mode 100644
index 000000000..38f7feb91
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Background Image.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/BlockInInline.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/BlockInInline.png
new file mode 100644
index 000000000..f5e1c51db
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/BlockInInline.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Blockquotes.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Blockquotes.png
new file mode 100644
index 000000000..43db1710c
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Blockquotes.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Borders.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Borders.png
new file mode 100644
index 000000000..d08af6bc1
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Borders.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Breaking pages 1 - Paragraphs.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Breaking pages 1 - Paragraphs.png
new file mode 100644
index 000000000..264b7413f
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Breaking pages 1 - Paragraphs.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Breaking pages 2 - Tables.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Breaking pages 2 - Tables.png
new file mode 100644
index 000000000..ec8ef692a
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Breaking pages 2 - Tables.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Bullets.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Bullets.png
new file mode 100644
index 000000000..8707f623e
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Bullets.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/External Image.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/External Image.png
new file mode 100644
index 000000000..888eb3a2a
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/External Image.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Fixed position.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Fixed position.png
new file mode 100644
index 000000000..11a9e2dec
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Fixed position.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Fonts decorations.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Fonts decorations.png
new file mode 100644
index 000000000..adc6aff76
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Fonts decorations.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Header.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Header.png
new file mode 100644
index 000000000..3f1fcbd38
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Header.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Iframes.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Iframes.png
new file mode 100644
index 000000000..0a1e0fd89
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Iframes.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Image in css content.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Image in css content.png
new file mode 100644
index 000000000..6739c2edd
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Image in css content.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Images.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Images.png
new file mode 100644
index 000000000..48bdccf4a
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Images.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Inline.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Inline.png
new file mode 100644
index 000000000..8ed9d3f91
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Inline.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Languages.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Languages.png
new file mode 100644
index 000000000..569e1547c
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Languages.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Line break.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Line break.png
new file mode 100644
index 000000000..d5b7568e8
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Line break.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/LineHeight.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/LineHeight.png
new file mode 100644
index 000000000..e63b3ca00
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/LineHeight.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Many images.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Many images.png
new file mode 100644
index 000000000..f0dd3072b
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Many images.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/MaxWidth.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/MaxWidth.png
new file mode 100644
index 000000000..85ba90cef
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/MaxWidth.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Misc.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Misc.png
new file mode 100644
index 000000000..70378caa9
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Misc.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Paragraphs.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Paragraphs.png
new file mode 100644
index 000000000..d156db230
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Paragraphs.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/RTL.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/RTL.png
new file mode 100644
index 000000000..606696f49
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/RTL.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Tables.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Tables.png
new file mode 100644
index 000000000..1350dc8fe
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Tables.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Text.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Text.png
new file mode 100644
index 000000000..47d955000
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/Text.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/Baselines/White-space.png b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/White-space.png
new file mode 100644
index 000000000..901825887
Binary files /dev/null and b/Source/Test/HtmlRenderer.IntegrationTest/Baselines/White-space.png differ
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderer.IntegrationTest.csproj b/Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderer.IntegrationTest.csproj
new file mode 100644
index 000000000..f9ee2bcde
--- /dev/null
+++ b/Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderer.IntegrationTest.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net8.0-windows
+ true
+ latest
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs b/Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs
new file mode 100644
index 000000000..1b1f68f80
--- /dev/null
+++ b/Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs
@@ -0,0 +1,397 @@
+using System.Drawing;
+using System.Globalization;
+using System.Reflection;
+using System.Text;
+using TheArtOfDev.HtmlRenderer.Core.Entities;
+using TheArtOfDev.HtmlRenderer.Demo.Common;
+using TheArtOfDev.HtmlRenderer.WinForms;
+
+namespace HtmlRenderer.IntegrationTest;
+
+[TestClass]
+public sealed class HtmlRenderingRegressionTests
+{
+ private const int MaxWidth = 1200;
+ private const int MaxHeight = 6400;
+
+ private static readonly object SamplesInitLock = new();
+ private static readonly object ImageCacheLock = new();
+ private static readonly Dictionary ImageCache = new(StringComparer.OrdinalIgnoreCase);
+ private static readonly Lazy PlaceholderImage = new(CreatePlaceholderImage);
+
+ private static bool _samplesLoaded;
+
+ [DoNotParallelize]
+ [TestMethod]
+ [DynamicData(nameof(GetSamples), DynamicDataDisplayName = nameof(GetSampleDisplayName))]
+ public void Render_DemoTestSample_MatchesBaselineImage(string sampleName, string sampleHtml)
+ {
+ EnsureSamplesLoaded();
+
+ var baselineDirectory = GetBaselineDirectory();
+ var outputDirectory = GetOutputDirectory();
+ var approveBaselines = GetBooleanEnvironmentVariable("HTML_RENDERER_APPROVE_BASELINES");
+ var tolerance = GetIntEnvironmentVariable("HTML_RENDERER_PIXEL_TOLERANCE", 2);
+ var saveAllRendered = GetBooleanEnvironmentVariable("HTML_RENDERER_SAVE_ALL_RENDERED");
+
+ Directory.CreateDirectory(baselineDirectory);
+ Directory.CreateDirectory(outputDirectory);
+
+ var sampleFileName = SanitizeFileName(sampleName);
+ var baselinePath = Path.Combine(baselineDirectory, sampleFileName + ".png");
+ var actualPath = Path.Combine(outputDirectory, sampleFileName + ".actual.png");
+ var diffPath = Path.Combine(outputDirectory, sampleFileName + ".diff.png");
+
+ using var rendered = RenderSample(sampleHtml);
+ if (!File.Exists(baselinePath))
+ {
+ if (approveBaselines)
+ {
+ rendered.Save(baselinePath);
+ return;
+ }
+
+ rendered.Save(actualPath);
+ Assert.Fail($"[{sampleName}] Missing baseline image: {baselinePath}");
+ }
+
+ using var baseline = new Bitmap(new MemoryStream(File.ReadAllBytes(baselinePath)));
+ CompareImages(baseline, rendered, tolerance, out var differentPixels, out var totalPixels);
+
+ var differencePercentage = totalPixels == 0
+ ? 0
+ : (double)differentPixels * 100.0 / totalPixels;
+
+ if (differentPixels == 0)
+ {
+ if (saveAllRendered)
+ {
+ rendered.Save(actualPath);
+ }
+ return;
+ }
+
+ if (approveBaselines)
+ {
+ rendered.Save(baselinePath);
+ return;
+ }
+
+ rendered.Save(actualPath);
+ using var diff = CreateDiffImage(baseline, rendered, tolerance);
+ diff.Save(diffPath);
+
+ Assert.Fail(
+ $"[{sampleName}] Difference: {differencePercentage.ToString("F3", CultureInfo.InvariantCulture)}% ({differentPixels}/{totalPixels} pixels){Environment.NewLine}" +
+ $" Baseline: {baselinePath}{Environment.NewLine}" +
+ $" Actual: {actualPath}{Environment.NewLine}" +
+ $" Diff: {diffPath}");
+ }
+
+ public static IEnumerable