Skip to content

Commit

Permalink
Merge pull request #408 from steeltomato/enhance-textformatter-and-vi…
Browse files Browse the repository at this point in the history
…sual-test

feat(TextFormatter): Support vertical alignment, overflow, line height
  • Loading branch information
ststeiger authored Jan 16, 2024
2 parents df829ba + 8d53b80 commit cdf089b
Show file tree
Hide file tree
Showing 12 changed files with 339 additions and 50 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -258,4 +258,7 @@ paket-files/

# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
*.pyc

# OSX
.DS_Store
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
127 changes: 127 additions & 0 deletions PdfSharpCore.Test/Drawing/Layout/XTextFormatterTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System.IO;
using FluentAssertions;
using ImageMagick;
using PdfSharpCore.Drawing;
using PdfSharpCore.Drawing.Layout;
using PdfSharpCore.Drawing.Layout.enums;
using PdfSharpCore.Pdf;
using PdfSharpCore.Test.Helpers;
using Xunit;

namespace PdfSharpCore.Test.Drawing.Layout
{
public class XTextFormatterTest
{
private static readonly string _outDir = "TestResults/XTextFormatterTest";
private static readonly string _expectedImagesPath = Path.Combine("Drawing", "Layout");

private PdfDocument _document;
private XGraphics _renderer;
private XTextFormatter _textFormatter;

// Run before each test
public XTextFormatterTest()
{
_document = new PdfDocument();
var page = _document.AddPage();
page.Size = PageSize.A6; // 295 x 417 pts
_renderer = XGraphics.FromPdfPage(page);
_textFormatter = new XTextFormatter(_renderer);
}

[Fact]
public void DrawSingleLineString()
{
var layout = new XRect(12, 12, 200, 50);
_textFormatter.DrawString("This is a simple single line test", new XFont("Arial", 12), XBrushes.Black, layout);

var diffResult = DiffPage(_document, "DrawSingleLineString", 1);

diffResult.DiffValue.Should().Be(0);
}

[Fact]
public void DrawMultilineStringWithTruncate()
{
var layout = new XRect(12, 12, 200, 40);
_renderer.DrawRectangle(XBrushes.LightGray, layout);
_textFormatter.DrawString("This is text\nspanning 3 lines\nbut only space for 2", new XFont("Arial", 12), XBrushes.Black, layout);

var diffResult = DiffPage(_document, "DrawMultilineStringWithTruncate", 1);

diffResult.DiffValue.Should().Be(0);
}

[Fact]
public void DrawMultiLineStringWithOverflow()
{
var layout = new XRect(12, 12, 200, 40);
_renderer.DrawRectangle(XBrushes.LightGray, layout);
_textFormatter.AllowVerticalOverflow = true;
_textFormatter.DrawString("This is text\nspanning 3 lines\nand overflow shows all three", new XFont("Arial", 12), XBrushes.Black, layout);

var diffResult = DiffPage(_document, "DrawMultiLineStringWithOverflow", 1);

diffResult.DiffValue.Should().Be(0);
}

[Fact]
public void DrawMultiLineStringsWithAlignment()
{
var layout1 = new XRect(12, 12, 200, 80);
_renderer.DrawRectangle(XBrushes.LightGray, layout1);
_textFormatter.DrawString("This is text\naligned to the top-left", new XFont("Arial", 12), XBrushes.Black, layout1);

var layout2 = new XRect(12, 100, 200, 80);
_renderer.DrawRectangle(XBrushes.LightGray, layout2);
_textFormatter.SetAlignment(new TextFormatAlignment { Horizontal = XParagraphAlignment.Center, Vertical = XVerticalAlignment.Middle});
_textFormatter.DrawString("This is text\naligned to the middle-center", new XFont("Arial", 12), XBrushes.Black, layout2);

var layout3 = new XRect(12, 200, 200, 80);
_renderer.DrawRectangle(XBrushes.LightGray, layout3);
_textFormatter.SetAlignment(new TextFormatAlignment { Horizontal = XParagraphAlignment.Right, Vertical = XVerticalAlignment.Bottom});
_textFormatter.DrawString("This is text\naligned to the bottom-right", new XFont("Arial", 12), XBrushes.Black, layout3);

var diffResult = DiffPage(_document, "DrawMultiLineStringsWithAlignment", 1);

diffResult.DiffValue.Should().Be(0);
}

[Fact]
public void DrawMultiLineStringsWithLineHeight()
{
var font = new XFont("Arial", 12);

var layout1 = new XRect(10, 10, 200, 80);
_renderer.DrawRectangle(XBrushes.LightGray, layout1);
_textFormatter.DrawString("This is text\naligned to the top-left\nand a custom line height", font, XBrushes.Black, layout1, 16);

var layout2 = new XRect(10, 110, 200, 80);
_renderer.DrawRectangle(XBrushes.LightGray, layout2);
_textFormatter.SetAlignment(new TextFormatAlignment { Horizontal = XParagraphAlignment.Center, Vertical = XVerticalAlignment.Middle});
_textFormatter.DrawString("This is text\naligned to the middle-center\nand a custom line height", font, XBrushes.Black, layout2, 16);

var layout3 = new XRect(10, 210, 200, 80);
_renderer.DrawRectangle(XBrushes.LightGray, layout3);
_textFormatter.SetAlignment(new TextFormatAlignment { Horizontal = XParagraphAlignment.Right, Vertical = XVerticalAlignment.Bottom});
_textFormatter.DrawString("This is text\naligned to the bottom-right\nand a custom line height", font, XBrushes.Black, layout3, 16);

var layout4 = new XRect(10, 310, 200, 80);
_renderer.DrawRectangle(XBrushes.LightGray, layout4);
_textFormatter.SetAlignment(new TextFormatAlignment { Horizontal = XParagraphAlignment.Center, Vertical = XVerticalAlignment.Middle});
_textFormatter.DrawString("This is text\nwith a very small\nline height", font, XBrushes.Black, layout4, 6);

var diffResult = DiffPage(_document, "DrawMultiLineStringsWithLineHeight", 1);

diffResult.DiffValue.Should().Be(0);
}

private static DiffOutput DiffPage(PdfDocument document, string filePrefix, int pageNum)
{
var rasterized = PdfHelper.Rasterize(document);
var rasterizedFiles = PdfHelper.WriteImageCollection(rasterized.ImageCollection, _outDir, filePrefix);
var expectedImagePath = PathHelper.GetInstance().GetAssetPath(_expectedImagesPath, $"{filePrefix}_{pageNum}.png");
return PdfHelper.Diff(rasterizedFiles[pageNum-1], expectedImagePath, _outDir, filePrefix);
}
}
}
11 changes: 7 additions & 4 deletions PdfSharpCore.Test/Helpers/PathHelper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.IO;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

namespace PdfSharpCore.Test.Helpers
Expand All @@ -10,9 +11,11 @@ public PathHelper()
RootDir = Path.GetDirectoryName(GetType().GetTypeInfo().Assembly.Location);
}

public string GetAssetPath(string name)
public string GetAssetPath(params string[] names)
{
return Path.Combine(RootDir, "Assets", name);
var segments = new List<string> { RootDir, "Assets" };
segments.AddRange(names);
return Path.Combine(segments.ToArray());
}

public string RootDir { get; }
Expand All @@ -24,4 +27,4 @@ public static PathHelper GetInstance()

private static PathHelper _instance;
}
}
}
116 changes: 116 additions & 0 deletions PdfSharpCore.Test/Helpers/PdfHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.IO;
using ImageMagick;
using PdfSharpCore.Pdf;

namespace PdfSharpCore.Test.Helpers
{
public class PdfHelper
{
private static readonly string _rootPath = PathHelper.GetInstance().RootDir;

/// <summary>
/// Rasterize all pages within a PDF to PNG images
/// </summary>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static RasterizeOutput Rasterize(PdfDocument document)
{
var readerSettings = new MagickReadSettings
{
Density = new Density(300, 300),
BackgroundColor = MagickColors.White
};
var images = new MagickImageCollection();

// Add all pages to the collection
using var ms = new MemoryStream();
document.Save(ms);

try
{
images.Read(ms, readerSettings);
}
catch (MagickDelegateErrorException ex)
{
throw new Exception("Ghostscript is not installed or is an incompatible version, unable to rasterize PDF", ex);
}

// Remove transparency to guarantee a standard white background
foreach (var img in images)
{
img.Alpha(AlphaOption.Deactivate);
img.BackgroundColor = MagickColors.White;
}

return new RasterizeOutput
{
ImageCollection = images,
};
}

public static List<string> WriteImageCollection(MagickImageCollection images, string outDir, string filePrefix)
{
var outPaths = new List<string>();
for (var pageNum = 0; pageNum < images.Count; pageNum++)
{
var outPath = GetOutFilePath(outDir, $"{filePrefix}_{pageNum+1}.png");
images[pageNum].Write(outPath);
outPaths.Add(outPath);
}

return outPaths;
}

public static string WriteImage(IMagickImage image, string outDir, string fileNameWithoutExtension)
{
var outPath = GetOutFilePath(outDir, $"{fileNameWithoutExtension}.png");
image.Write(outPath);
return outPath;
}

// Note: For diff to function properly, it requires the underlying image to be in the proper format
// For instance, actual and expected must both be sourced from .png files
public static DiffOutput Diff(string actualImagePath, string expectedImagePath, string outputPath = null, string filePrefix = null, int fuzzPct = 4)
{
var diffImg = new MagickImage();
var actual = new MagickImage(actualImagePath);
var expected = new MagickImage(expectedImagePath);

// Allow for subtle differences due to cross-platform rendering of the PDF fonts
actual.ColorFuzz = new Percentage(fuzzPct);
var diffVal = actual.Compare(expected, ErrorMetric.Absolute, diffImg);

if (diffVal > 0 && outputPath != null && filePrefix != null)
{
WriteImage(diffImg, outputPath, $"{filePrefix}_diff");
}

return new DiffOutput
{
DiffValue = diffVal,
DiffImage = diffImg
};
}

private static string GetOutFilePath(string outDir, string name)
{
var dir = Path.Combine(_rootPath, outDir);
Directory.CreateDirectory(dir);
return Path.Combine(dir, name);
}
}

public class RasterizeOutput
{
public List<string> OutputPaths;
public MagickImageCollection ImageCollection;
}

public class DiffOutput
{
public IMagickImage DiffImage;
public double DiffValue;
}
}
26 changes: 3 additions & 23 deletions PdfSharpCore.Test/PdfSharpCore.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.11.0" />
<PackageReference Include="Magick.NET-Q8-AnyCPU" Version="13.4.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
Expand All @@ -31,31 +32,10 @@
</ItemGroup>

<ItemGroup>
<None Update="Assets\AesEncrypted.pdf">
<None Update="Assets\**\*.pdf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\FamilyTree.pdf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\lenna.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\NotAValid.pdf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\protected-adobe.pdf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\protected-ilovepdf.pdf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\protected-pdfencrypt.pdf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\protected-sodapdf.pdf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\test.pdf">
<None Update="Assets\**\*.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
Expand Down
Loading

0 comments on commit cdf089b

Please sign in to comment.