Skip to content

Commit 86f4c04

Browse files
committed
Support alignment and line height in TextFormatter, provide visual unit testing
1 parent df829ba commit 86f4c04

12 files changed

+338
-49
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,4 +258,7 @@ paket-files/
258258

259259
# Python Tools for Visual Studio (PTVS)
260260
__pycache__/
261-
*.pyc
261+
*.pyc
262+
263+
# OSX
264+
.DS_Store
11.7 KB
Loading
17.4 KB
Loading
28.7 KB
Loading
9.65 KB
Loading
2.69 KB
Loading
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
using System.IO;
2+
using FluentAssertions;
3+
using ImageMagick;
4+
using PdfSharpCore.Drawing;
5+
using PdfSharpCore.Drawing.Layout;
6+
using PdfSharpCore.Drawing.Layout.enums;
7+
using PdfSharpCore.Pdf;
8+
using PdfSharpCore.Test.Helpers;
9+
using Xunit;
10+
11+
namespace PdfSharpCore.Test.Drawing.Layout
12+
{
13+
public class XTextFormatterTest
14+
{
15+
private static readonly string _outDir = "TestResults/XTextFormatterTest";
16+
private static readonly string _expectedImagesPath = Path.Combine("Drawing", "Layout");
17+
18+
private PdfDocument _document;
19+
private XGraphics _renderer;
20+
private XTextFormatter _textFormatter;
21+
22+
// Run before each test
23+
public XTextFormatterTest()
24+
{
25+
_document = new PdfDocument();
26+
var page = _document.AddPage();
27+
page.Size = PageSize.A6; // 295 x 417 pts
28+
_renderer = XGraphics.FromPdfPage(page);
29+
_textFormatter = new XTextFormatter(_renderer);
30+
}
31+
32+
[Fact]
33+
public void DrawSingleLineString()
34+
{
35+
var layout = new XRect(12, 12, 200, 50);
36+
_textFormatter.DrawString("This is a simple single line test", new XFont("Arial", 12), XBrushes.Black, layout);
37+
38+
var diffResult = DiffPage(_document, "DrawSingleLineString", 1);
39+
40+
diffResult.DiffValue.Should().Be(0);
41+
}
42+
43+
[Fact]
44+
public void DrawMultilineStringWithTruncate()
45+
{
46+
var layout = new XRect(12, 12, 200, 40);
47+
_renderer.DrawRectangle(XBrushes.LightGray, layout);
48+
_textFormatter.DrawString("This is text\nspanning 3 lines\nbut only space for 2", new XFont("Arial", 12), XBrushes.Black, layout);
49+
50+
var diffResult = DiffPage(_document, "DrawMultilineStringWithTruncate", 1);
51+
52+
diffResult.DiffValue.Should().Be(0);
53+
}
54+
55+
[Fact]
56+
public void DrawMultiLineStringWithOverflow()
57+
{
58+
var layout = new XRect(12, 12, 200, 40);
59+
_renderer.DrawRectangle(XBrushes.LightGray, layout);
60+
_textFormatter.AllowVerticalOverflow = true;
61+
_textFormatter.DrawString("This is text\nspanning 3 lines\nand overflow shows all three", new XFont("Arial", 12), XBrushes.Black, layout);
62+
63+
var diffResult = DiffPage(_document, "DrawMultiLineStringWithOverflow", 1);
64+
65+
diffResult.DiffValue.Should().Be(0);
66+
}
67+
68+
[Fact]
69+
public void DrawMultiLineStringsWithAlignment()
70+
{
71+
var layout1 = new XRect(12, 12, 200, 80);
72+
_renderer.DrawRectangle(XBrushes.LightGray, layout1);
73+
_textFormatter.DrawString("This is text\naligned to the top-left", new XFont("Arial", 12), XBrushes.Black, layout1);
74+
75+
var layout2 = new XRect(12, 100, 200, 80);
76+
_renderer.DrawRectangle(XBrushes.LightGray, layout2);
77+
_textFormatter.SetAlignment(new TextFormatAlignment { Horizontal = XParagraphAlignment.Center, Vertical = XVerticalAlignment.Middle});
78+
_textFormatter.DrawString("This is text\naligned to the middle-center", new XFont("Arial", 12), XBrushes.Black, layout2);
79+
80+
var layout3 = new XRect(12, 200, 200, 80);
81+
_renderer.DrawRectangle(XBrushes.LightGray, layout3);
82+
_textFormatter.SetAlignment(new TextFormatAlignment { Horizontal = XParagraphAlignment.Right, Vertical = XVerticalAlignment.Bottom});
83+
_textFormatter.DrawString("This is text\naligned to the bottom-right", new XFont("Arial", 12), XBrushes.Black, layout3);
84+
85+
var diffResult = DiffPage(_document, "DrawMultiLineStringsWithAlignment", 1);
86+
87+
diffResult.DiffValue.Should().Be(0);
88+
}
89+
90+
[Fact]
91+
public void DrawMultiLineStringsWithLineHeight()
92+
{
93+
var font = new XFont("Arial", 12);
94+
95+
var layout1 = new XRect(10, 10, 200, 80);
96+
_renderer.DrawRectangle(XBrushes.LightGray, layout1);
97+
_textFormatter.DrawString("This is text\naligned to the top-left\nand a custom line height", font, XBrushes.Black, layout1, 16);
98+
99+
var layout2 = new XRect(10, 110, 200, 80);
100+
_renderer.DrawRectangle(XBrushes.LightGray, layout2);
101+
_textFormatter.SetAlignment(new TextFormatAlignment { Horizontal = XParagraphAlignment.Center, Vertical = XVerticalAlignment.Middle});
102+
_textFormatter.DrawString("This is text\naligned to the middle-center\nand a custom line height", font, XBrushes.Black, layout2, 16);
103+
104+
var layout3 = new XRect(10, 210, 200, 80);
105+
_renderer.DrawRectangle(XBrushes.LightGray, layout3);
106+
_textFormatter.SetAlignment(new TextFormatAlignment { Horizontal = XParagraphAlignment.Right, Vertical = XVerticalAlignment.Bottom});
107+
_textFormatter.DrawString("This is text\naligned to the bottom-right\nand a custom line height", font, XBrushes.Black, layout3, 16);
108+
109+
var layout4 = new XRect(10, 310, 200, 80);
110+
_renderer.DrawRectangle(XBrushes.LightGray, layout4);
111+
_textFormatter.SetAlignment(new TextFormatAlignment { Horizontal = XParagraphAlignment.Center, Vertical = XVerticalAlignment.Middle});
112+
_textFormatter.DrawString("This is text\nwith a very small\nline height", font, XBrushes.Black, layout4, 6);
113+
114+
var diffResult = DiffPage(_document, "DrawMultiLineStringsWithLineHeight", 1);
115+
116+
diffResult.DiffValue.Should().Be(0);
117+
}
118+
119+
private static DiffOutput DiffPage(PdfDocument document, string filePrefix, int pageNum)
120+
{
121+
var rasterized = PdfHelper.Rasterize(document);
122+
var rasterizedFiles = PdfHelper.WriteImageCollection(rasterized.ImageCollection, _outDir, filePrefix);
123+
var expectedImagePath = PathHelper.GetInstance().GetAssetPath(_expectedImagesPath, $"{filePrefix}_{pageNum}.png");
124+
return PdfHelper.Diff(rasterizedFiles[pageNum-1], expectedImagePath, _outDir, filePrefix);
125+
}
126+
}
127+
}

PdfSharpCore.Test/Helpers/PathHelper.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.IO;
1+
using System.Collections.Generic;
2+
using System.IO;
23
using System.Reflection;
34

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

13-
public string GetAssetPath(string name)
14+
public string GetAssetPath(params string[] names)
1415
{
15-
return Path.Combine(RootDir, "Assets", name);
16+
var segments = new List<string> { RootDir, "Assets" };
17+
segments.AddRange(names);
18+
return Path.Combine(segments.ToArray());
1619
}
1720

1821
public string RootDir { get; }
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using ImageMagick;
5+
using PdfSharpCore.Pdf;
6+
7+
namespace PdfSharpCore.Test.Helpers
8+
{
9+
public class PdfHelper
10+
{
11+
private static readonly string _rootPath = PathHelper.GetInstance().RootDir;
12+
13+
/// <summary>
14+
/// Rasterize all pages within a PDF to PNG images
15+
/// </summary>
16+
/// <returns></returns>
17+
/// <exception cref="Exception"></exception>
18+
public static RasterizeOutput Rasterize(PdfDocument document)
19+
{
20+
var readerSettings = new MagickReadSettings
21+
{
22+
Density = new Density(300, 300),
23+
BackgroundColor = MagickColors.White
24+
};
25+
var images = new MagickImageCollection();
26+
27+
// Add all pages to the collection
28+
using var ms = new MemoryStream();
29+
document.Save(ms);
30+
31+
try
32+
{
33+
images.Read(ms, readerSettings);
34+
}
35+
catch (MagickDelegateErrorException ex)
36+
{
37+
throw new Exception("Ghostscript is not installed or is an incompatible version, unable to rasterize PDF", ex);
38+
}
39+
40+
// Remove transparency to guarantee a standard white background
41+
foreach (var img in images)
42+
{
43+
img.Alpha(AlphaOption.Deactivate);
44+
img.BackgroundColor = MagickColors.White;
45+
}
46+
47+
return new RasterizeOutput
48+
{
49+
ImageCollection = images,
50+
};
51+
}
52+
53+
public static List<string> WriteImageCollection(MagickImageCollection images, string outDir, string filePrefix)
54+
{
55+
var outPaths = new List<string>();
56+
for (var pageNum = 0; pageNum < images.Count; pageNum++)
57+
{
58+
var outPath = GetOutFilePath(outDir, $"{filePrefix}_{pageNum+1}.png");
59+
images[pageNum].Write(outPath);
60+
outPaths.Add(outPath);
61+
}
62+
63+
return outPaths;
64+
}
65+
66+
public static string WriteImage(IMagickImage image, string outDir, string fileNameWithoutExtension)
67+
{
68+
var outPath = GetOutFilePath(outDir, $"{fileNameWithoutExtension}.png");
69+
image.Write(outPath);
70+
return outPath;
71+
}
72+
73+
// Note: For diff to function properly, it requires the underlying image to be in the proper format
74+
// For instance, actual and expected must both be sourced from .png files
75+
public static DiffOutput Diff(string actualImagePath, string expectedImagePath, string outputPath = null, string filePrefix = null, int fuzzPct = 4)
76+
{
77+
var diffImg = new MagickImage();
78+
var actual = new MagickImage(actualImagePath);
79+
var expected = new MagickImage(expectedImagePath);
80+
81+
// Allow for subtle differences due to cross-platform rendering of the PDF fonts
82+
actual.ColorFuzz = new Percentage(fuzzPct);
83+
var diffVal = actual.Compare(expected, ErrorMetric.Absolute, diffImg);
84+
85+
if (diffVal > 0 && outputPath != null && filePrefix != null)
86+
{
87+
WriteImage(diffImg, outputPath, $"{filePrefix}_diff.png");
88+
}
89+
90+
return new DiffOutput
91+
{
92+
DiffValue = diffVal,
93+
DiffImage = diffImg
94+
};
95+
}
96+
97+
private static string GetOutFilePath(string outDir, string name)
98+
{
99+
var dir = Path.Combine(_rootPath, outDir);
100+
Directory.CreateDirectory(dir);
101+
return Path.Combine(dir, name);
102+
}
103+
}
104+
105+
public class RasterizeOutput
106+
{
107+
public List<string> OutputPaths;
108+
public MagickImageCollection ImageCollection;
109+
}
110+
111+
public class DiffOutput
112+
{
113+
public IMagickImage DiffImage;
114+
public double DiffValue;
115+
}
116+
}

PdfSharpCore.Test/PdfSharpCore.Test.csproj

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1717
</PackageReference>
1818
<PackageReference Include="FluentAssertions" Version="6.11.0" />
19+
<PackageReference Include="Magick.NET-Q8-AnyCPU" Version="13.4.0" />
1920
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
2021
<PackageReference Include="xunit" Version="2.4.2" />
2122
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
@@ -31,31 +32,10 @@
3132
</ItemGroup>
3233

3334
<ItemGroup>
34-
<None Update="Assets\AesEncrypted.pdf">
35+
<None Update="Assets\**\*.pdf">
3536
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
3637
</None>
37-
<None Update="Assets\FamilyTree.pdf">
38-
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
39-
</None>
40-
<None Update="Assets\lenna.png">
41-
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
42-
</None>
43-
<None Update="Assets\NotAValid.pdf">
44-
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
45-
</None>
46-
<None Update="Assets\protected-adobe.pdf">
47-
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
48-
</None>
49-
<None Update="Assets\protected-ilovepdf.pdf">
50-
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
51-
</None>
52-
<None Update="Assets\protected-pdfencrypt.pdf">
53-
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
54-
</None>
55-
<None Update="Assets\protected-sodapdf.pdf">
56-
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
57-
</None>
58-
<None Update="Assets\test.pdf">
38+
<None Update="Assets\**\*.png">
5939
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
6040
</None>
6141
</ItemGroup>

0 commit comments

Comments
 (0)