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 GetSamples() + { + EnsureSamplesLoaded(); + foreach (var sample in SamplesLoader.TestSamples) + { + yield return [sample.Name, sample.Html]; + } + } + + public static string GetSampleDisplayName(MethodInfo methodInfo, object[] data) + { + return data.Length > 0 && data[0] is string sampleName + ? $"{methodInfo.Name}_{sampleName}" + : methodInfo.Name; + } + + private static void EnsureSamplesLoaded() + { + if (_samplesLoaded) + { + return; + } + + lock (SamplesInitLock) + { + if (_samplesLoaded) + { + return; + } + + SamplesLoader.Init("Regression", typeof(HtmlRender).Assembly.GetName().Version.ToString()); + _samplesLoaded = true; + } + } + + private static Bitmap RenderSample(string html) + { + return (Bitmap)HtmlRender.RenderToImage( + html, + minSize: Size.Empty, + maxSize: new Size(MaxWidth, MaxHeight), + backgroundColor: Color.White, + stylesheetLoad: DemoUtils.OnStylesheetLoad, + imageLoad: OnImageLoad); + } + + private static void OnImageLoad(object? sender, HtmlImageLoadEventArgs e) + { + var image = TryLoadResourceImage(e.Src); + if (image == null && RequiresPlaceholderImage(e.Src)) + { + image = PlaceholderImage.Value; + } + + if (e.Attributes != null && e.Attributes.TryGetValue("byrect", out var byRectValue)) + { + var split = byRectValue.Split(','); + if (split.Length == 4 + && double.TryParse(split[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var x) + && double.TryParse(split[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var y) + && double.TryParse(split[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out var width) + && double.TryParse(split[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out var height)) + { + e.Callback(image ?? PlaceholderImage.Value, x, y, width, height); + return; + } + } + + if (e.Attributes != null && e.Attributes.ContainsKey("byevent")) + { + e.Callback(image ?? PlaceholderImage.Value); + return; + } + + if (image != null) + { + e.Callback(image); + } + } + + private static Image? TryLoadResourceImage(string? src) + { + if (string.IsNullOrWhiteSpace(src)) + { + return null; + } + + lock (ImageCacheLock) + { + if (ImageCache.TryGetValue(src, out var cached)) + { + return cached; + } + + using var stream = DemoUtils.GetImageStream(src); + if (stream == null) + { + return null; + } + + using var image = Image.FromStream(stream); + var cachedImage = new Bitmap(image); + ImageCache[src] = cachedImage; + return cachedImage; + } + } + + private static bool RequiresPlaceholderImage(string? src) + { + if (string.IsNullOrWhiteSpace(src)) + { + return true; + } + + if (src.StartsWith("http://", StringComparison.OrdinalIgnoreCase) + || src.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (src.StartsWith('#')) + { + return true; + } + + if (Uri.TryCreate(src, UriKind.Absolute, out var uri)) + { + return uri.Scheme != Uri.UriSchemeFile; + } + + return false; + } + + private static void CompareImages(Bitmap baseline, Bitmap actual, int tolerance, out int differentPixels, out int totalPixels) + { + var width = Math.Max(baseline.Width, actual.Width); + var height = Math.Max(baseline.Height, actual.Height); + + differentPixels = 0; + totalPixels = width * height; + + for (var y = 0; y < height; y++) + { + for (var x = 0; x < width; x++) + { + var expected = GetPixelOrDefault(baseline, x, y); + var rendered = GetPixelOrDefault(actual, x, y); + + var pixelDiffers = Math.Abs(expected.A - rendered.A) > tolerance + || Math.Abs(expected.R - rendered.R) > tolerance + || Math.Abs(expected.G - rendered.G) > tolerance + || Math.Abs(expected.B - rendered.B) > tolerance; + + if (pixelDiffers) + { + differentPixels++; + } + } + } + } + + private static Bitmap CreateDiffImage(Bitmap baseline, Bitmap actual, int tolerance) + { + var width = Math.Max(baseline.Width, actual.Width); + var height = Math.Max(baseline.Height, actual.Height); + var diff = new Bitmap(width, height); + + for (var y = 0; y < height; y++) + { + for (var x = 0; x < width; x++) + { + var expected = GetPixelOrDefault(baseline, x, y); + var rendered = GetPixelOrDefault(actual, x, y); + + var pixelDiffers = Math.Abs(expected.A - rendered.A) > tolerance + || Math.Abs(expected.R - rendered.R) > tolerance + || Math.Abs(expected.G - rendered.G) > tolerance + || Math.Abs(expected.B - rendered.B) > tolerance; + + if (pixelDiffers) + { + diff.SetPixel(x, y, Color.Red); + continue; + } + + diff.SetPixel( + x, + y, + Color.FromArgb( + 255, + expected.R / 3, + expected.G / 3, + expected.B / 3)); + } + } + + return diff; + } + + private static Color GetPixelOrDefault(Bitmap image, int x, int y) + { + if (x < image.Width && y < image.Height) + { + return image.GetPixel(x, y); + } + + return Color.White; + } + + private static string GetBaselineDirectory() + { + var configuredDirectory = Environment.GetEnvironmentVariable("HTML_RENDERER_BASELINE_DIRECTORY"); + if (!string.IsNullOrWhiteSpace(configuredDirectory)) + { + return configuredDirectory; + } + + var baseDirectory = new DirectoryInfo(AppContext.BaseDirectory); + while (baseDirectory != null) + { + var projectBaselineDirectory = Path.Combine(baseDirectory.FullName, "Baselines"); + if (Directory.Exists(projectBaselineDirectory)) + { + return projectBaselineDirectory; + } + + var sourceBaselineDirectory = Path.Combine(baseDirectory.FullName, "Test", "HtmlRenderer.IntegrationTest", "Baselines"); + if (Directory.Exists(sourceBaselineDirectory)) + { + return sourceBaselineDirectory; + } + + var repositoryBaselineDirectory = Path.Combine(baseDirectory.FullName, "Source", "Test", "HtmlRenderer.IntegrationTest", "Baselines"); + if (Directory.Exists(repositoryBaselineDirectory)) + { + return repositoryBaselineDirectory; + } + + baseDirectory = baseDirectory.Parent; + } + + throw new InvalidOperationException( + "Unable to locate baseline directory. Set HTML_RENDERER_BASELINE_DIRECTORY to an existing Baselines folder."); + } + + private static string GetOutputDirectory() + { + var configuredDirectory = Environment.GetEnvironmentVariable("HTML_RENDERER_REGRESSION_OUTPUT_DIRECTORY"); + if (!string.IsNullOrWhiteSpace(configuredDirectory)) + { + return configuredDirectory; + } + + return Path.Combine(Path.GetTempPath(), "html-renderer-image-regression"); + } + + private static bool GetBooleanEnvironmentVariable(string key) + { + var value = Environment.GetEnvironmentVariable(key); + return string.Equals(value, "1", StringComparison.OrdinalIgnoreCase) + || string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) + || string.Equals(value, "yes", StringComparison.OrdinalIgnoreCase); + } + + private static int GetIntEnvironmentVariable(string key, int fallback) + { + var value = Environment.GetEnvironmentVariable(key); + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)) + { + return parsed; + } + + return fallback; + } + + private static string SanitizeFileName(string name) + { + var invalidChars = Path.GetInvalidFileNameChars(); + var builder = new StringBuilder(name.Length); + foreach (var c in name) + { + if (invalidChars.Contains(c)) + { + builder.Append('_'); + } + else + { + builder.Append(c); + } + } + + return builder.ToString(); + } + + private static Image CreatePlaceholderImage() + { + var bitmap = new Bitmap(120, 80); + using var graphics = Graphics.FromImage(bitmap); + graphics.Clear(Color.WhiteSmoke); + using var borderPen = new Pen(Color.DimGray, 2); + graphics.DrawRectangle(borderPen, 1, 1, bitmap.Width - 3, bitmap.Height - 3); + using var crossPen = new Pen(Color.IndianRed, 2); + graphics.DrawLine(crossPen, 0, 0, bitmap.Width - 1, bitmap.Height - 1); + graphics.DrawLine(crossPen, bitmap.Width - 1, 0, 0, bitmap.Height - 1); + return bitmap; + } +} diff --git a/Source/Test/HtmlRenderer.IntegrationTest/MSTestSettings.cs b/Source/Test/HtmlRenderer.IntegrationTest/MSTestSettings.cs new file mode 100644 index 000000000..8b7de71cd --- /dev/null +++ b/Source/Test/HtmlRenderer.IntegrationTest/MSTestSettings.cs @@ -0,0 +1 @@ +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] \ No newline at end of file diff --git a/Source/Test/HtmlRenderer.PdfSharp.Test/HtmlRenderer.PdfSharp.Test.csproj b/Source/Test/HtmlRenderer.PdfSharp.Test/HtmlRenderer.PdfSharp.Test.csproj index e43631950..0ff91e24b 100644 --- a/Source/Test/HtmlRenderer.PdfSharp.Test/HtmlRenderer.PdfSharp.Test.csproj +++ b/Source/Test/HtmlRenderer.PdfSharp.Test/HtmlRenderer.PdfSharp.Test.csproj @@ -8,7 +8,7 @@ - +