Add GetDescent() and GetAscent() methods to IFont, improve font matrix for TrueTypeSimpleFont and TrueTypeStandard14FallbackSimpleFont and add loose bounding box to Letter
Some checks failed
Build, test and publish draft / build (push) Has been cancelled
Build and test [MacOS] / build (push) Has been cancelled
Run Common Crawl Tests / build (0000-0001) (push) Has been cancelled
Run Common Crawl Tests / build (0002-0003) (push) Has been cancelled
Run Common Crawl Tests / build (0004-0005) (push) Has been cancelled
Run Common Crawl Tests / build (0006-0007) (push) Has been cancelled
Run Integration Tests / build (push) Has been cancelled
Nightly Release / Check if this commit has already been published (push) Has been cancelled
Nightly Release / tests (push) Has been cancelled
Nightly Release / build_and_publish_nightly (push) Has been cancelled

This commit is contained in:
BobLd
2025-09-21 14:27:57 +01:00
parent 008959457a
commit b2f4ca8839
24 changed files with 507 additions and 34 deletions

View File

@@ -224,9 +224,18 @@
[Pure]
public double TransformX(double x)
{
var xt = A * x + C * 0 + E;
return A * x + E; // + C * 0
}
return xt;
/// <summary>
/// Transform an Y coordinate using this transformation matrix.
/// </summary>
/// <param name="y">The Y coordinate.</param>
/// <returns>The transformed Y coordinate.</returns>
public double TransformY(double y)
{
return D * y + F;
}
/// <summary>

View File

@@ -60,6 +60,7 @@
letter = new Letter(
" ",
letter.GlyphRectangle,
letter.GlyphRectangleLoose,
letter.StartBaseLine,
letter.EndBaseLine,
letter.Width,

View File

@@ -60,6 +60,7 @@
private static TextBlock CreateFakeTextBlock(PdfRectangle boundingBox)
{
var letter = new Letter("a",
boundingBox,
boundingBox,
boundingBox.BottomLeft,
boundingBox.BottomRight,

View File

@@ -4,6 +4,7 @@ using UglyToad.PdfPig.Tests.Dla;
namespace UglyToad.PdfPig.Tests.Fonts.SystemFonts
{
using PdfPig.Core;
using PdfPig.Geometry;
public class Linux
{
@@ -69,6 +70,9 @@ namespace UglyToad.PdfPig.Tests.Fonts.SystemFonts
Assert.Equal(expectedData.Width, current.GlyphRectangle.Width, 6);
Assert.Equal(expectedData.Height, current.GlyphRectangle.Height, 6);
Assert.Equal(expectedData.Rotation, current.GlyphRectangle.Rotation, 3);
Assert.True(current.GlyphRectangle.IntersectsWith(current.GlyphRectangleLoose));
Assert.Equal(current.GlyphRectangle.Rotation, current.GlyphRectangleLoose.Rotation, 3);
}
}
}

View File

@@ -1,5 +1,7 @@
namespace UglyToad.PdfPig.Tests.Integration
{
using PdfPig.Geometry;
public class IntegrationDocumentTests
{
private static readonly Lazy<string> DocumentFolder = new Lazy<string>(() => Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "Integration", "Documents")));
@@ -11,6 +13,36 @@
"cmap-parsing-exception.pdf"
];
[Theory]
[MemberData(nameof(GetAllDocuments))]
public void CheckGlyphLooseBoundingBoxes(string documentName)
{
// Add the full path back on, we removed it so we could see it in the test explorer.
documentName = Path.Combine(DocumentFolder.Value, documentName);
using (var document = PdfDocument.Open(documentName, new ParsingOptions { UseLenientParsing = true }))
{
for (var i = 0; i < document.NumberOfPages; i++)
{
var page = document.GetPage(i + 1);
foreach (var letter in page.Letters)
{
var bbox = letter.GlyphRectangle;
if (bbox.Height > 0)
{
if (letter.GlyphRectangleLoose.Height <= 0)
{
_ = letter.GetFont().GetAscent();
}
Assert.True(letter.GlyphRectangleLoose.Height > 0, $"Page {i + 1}");
}
}
}
}
}
[Theory]
[MemberData(nameof(GetAllDocuments))]
public void CanReadAllPages(string documentName)

View File

@@ -53,6 +53,8 @@ namespace UglyToad.PdfPig.Tests.Integration
Assert.Contains(page.Letters, x => x.GlyphRectangle.Width != 0);
Assert.Contains(page.Letters, x => x.GlyphRectangle.Height != 0);
Assert.Contains(page.Letters, x => x.GlyphRectangleLoose.Width != 0);
Assert.Contains(page.Letters, x => x.GlyphRectangleLoose.Height != 0);
}
}
}

View File

@@ -193,6 +193,32 @@
d.SaveTo(fs);
}
}
using (var bitmap = SKBitmap.FromImage(image))
using (var graphics = new SKCanvas(bitmap))
{
foreach (var letter in page.Letters)
{
DrawRectangle(letter.GlyphRectangleLoose, graphics, violetPen, imageHeight, scale);
}
graphics.Flush();
var imageName = $"{file}_loose.jpg";
if (!Directory.Exists(OutputPath))
{
Directory.CreateDirectory(OutputPath);
}
var savePath = Path.Combine(OutputPath, imageName);
using (var fs = new FileStream(savePath, FileMode.Create))
using (SKData d = bitmap.Encode(SKEncodedImageFormat.Jpeg, 100))
{
d.SaveTo(fs);
}
}
}
}

View File

@@ -85,6 +85,31 @@
d.SaveTo(fs);
}
}
using (var picture = document.GetPage<SKPicture>(pageNo))
using (var image = SKImage.FromPicture(picture, size, ScaleMatrix))
using (var bmp = SKBitmap.FromImage(image))
using (var canvas = new SKCanvas(bmp))
{
Assert.NotNull(picture);
if (RenderGlyphRectangle)
{
foreach (var letter in page.Letters)
{
DrawRectangle(letter.GlyphRectangleLoose, canvas, redPaint, size.Height, Scale);
}
}
var imageName = $"{file}_{pageNo}_loose.png";
var savePath = Path.Combine(OutputPath, imageName);
using (var fs = new FileStream(savePath, FileMode.Create))
using (var d = bmp.Encode(SKEncodedImageFormat.Png, 100))
{
d.SaveTo(fs);
}
}
}
}

View File

@@ -46,6 +46,12 @@
/// </summary>
public PdfRectangle GlyphRectangle { get; }
/// <summary>
/// The loose bounding box for the glyph. Contrary to the <see cref="GlyphRectangle"/>, the loose bounding box will be the same across all glyphes of the same font.
/// It takes in account the font Ascent and Descent.
/// </summary>
public PdfRectangle GlyphRectangleLoose { get; }
/// <summary>
/// Size as defined in the PDF file. This is not equivalent to font size in points but is relative to other font sizes on the page.
/// </summary>
@@ -110,6 +116,7 @@
/// </summary>
public Letter(string value,
PdfRectangle glyphRectangle,
PdfRectangle glyphRectangleLoose,
PdfPoint startBaseLine,
PdfPoint endBaseLine,
double width,
@@ -120,7 +127,7 @@
IColor fillColor,
double pointSize,
int textSequence) :
this(value, glyphRectangle,
this(value, glyphRectangle, glyphRectangleLoose,
startBaseLine, endBaseLine,
width, fontSize, font.Details, font,
renderingMode, strokeColor, fillColor,
@@ -132,6 +139,7 @@
/// </summary>
public Letter(string value,
PdfRectangle glyphRectangle,
PdfRectangle glyphRectangleLoose,
PdfPoint startBaseLine,
PdfPoint endBaseLine,
double width,
@@ -142,14 +150,16 @@
IColor fillColor,
double pointSize,
int textSequence):
this(value, glyphRectangle,
this(value, glyphRectangle, glyphRectangleLoose,
startBaseLine, endBaseLine,
width, fontSize, fontDetails, null,
renderingMode, strokeColor, fillColor,
pointSize, textSequence)
{ }
private Letter(string value, PdfRectangle glyphRectangle,
private Letter(string value,
PdfRectangle glyphRectangle,
PdfRectangle glyphRectangleLoose,
PdfPoint startBaseLine,
PdfPoint endBaseLine,
double width,
@@ -164,6 +174,7 @@
{
Value = value;
GlyphRectangle = glyphRectangle;
GlyphRectangleLoose = glyphRectangleLoose;
StartBaseLine = startBaseLine;
EndBaseLine = endBaseLine;
Width = width;
@@ -197,6 +208,7 @@
{
return new Letter(Value,
GlyphRectangle,
GlyphRectangleLoose,
StartBaseLine,
EndBaseLine,
Width,

View File

@@ -100,12 +100,6 @@ namespace UglyToad.PdfPig.Graphics
var transformedGlyphBounds = PerformantRectangleTransformer
.Transform(renderingMatrix, textMatrix, transformationMatrix, characterBoundingBox.GlyphBounds);
var transformedPdfBounds = PerformantRectangleTransformer
.Transform(renderingMatrix,
textMatrix,
transformationMatrix,
new PdfRectangle(0, 0, characterBoundingBox.Width, UserSpaceUnit.PointMultiples));
if (ParsingOptions.ClipPaths)
{
var currentClipping = currentState.CurrentClippingPath;
@@ -129,6 +123,7 @@ namespace UglyToad.PdfPig.Graphics
letter = new Letter(
newLetter,
attachTo.GlyphRectangle,
attachTo.GlyphRectangleLoose,
attachTo.StartBaseLine,
attachTo.EndBaseLine,
attachTo.Width,
@@ -151,9 +146,25 @@ namespace UglyToad.PdfPig.Graphics
// If we did not create a letter for a combined diacritic, create one here.
if (letter is null)
{
var transformedPdfBounds = PerformantRectangleTransformer
.Transform(renderingMatrix,
textMatrix,
transformationMatrix,
new PdfRectangle(0, 0, characterBoundingBox.Width, UserSpaceUnit.PointMultiples));
var looseBox = PerformantRectangleTransformer
.Transform(renderingMatrix,
textMatrix,
transformationMatrix,
new PdfRectangle(0,
font.GetDescent(),
characterBoundingBox.Width,
font.GetAscent()));
letter = new Letter(
unicode,
isBboxValid ? transformedGlyphBounds : transformedPdfBounds,
looseBox,
transformedPdfBounds.BottomLeft,
transformedPdfBounds.BottomRight,
transformedPdfBounds.Width,
@@ -167,7 +178,6 @@ namespace UglyToad.PdfPig.Graphics
}
letters.Add(letter);
markedContentStack.AddLetter(letter);
}

View File

@@ -57,6 +57,10 @@
TransformationMatrix GetFontMatrix(int characterIdentifier);
double GetDescent();
double GetAscent();
/// <summary>
/// Returns the glyph path for the given character code.
/// </summary>

View File

@@ -20,6 +20,10 @@
bool TryGetBoundingAdvancedWidth(int characterIdentifier, out double width);
double? GetDescent();
double? GetAscent();
bool TryGetPath(int characterCode, [NotNullWhen(true)] out IReadOnlyList<PdfSubpath>? path);
bool TryGetPath(int characterCode, Func<int, int?> characterCodeToGlyphId, [NotNullWhen(true)] out IReadOnlyList<PdfSubpath>? path);

View File

@@ -42,6 +42,18 @@
public PdfRectangle? GetCharacterBoundingBox(string characterName) => fontCollection.GetCharacterBoundingBox(characterName);
public double? GetDescent()
{
// BobLd: we don't support ascent / descent for cff for the moment
return null;
}
public double? GetAscent()
{
// BobLd: we don't support ascent / descent for cff for the moment
return null;
}
public bool TryGetBoundingBox(int characterIdentifier, out PdfRectangle boundingBox)
{
boundingBox = new PdfRectangle(0, 0, 500, 0);

View File

@@ -36,6 +36,16 @@
public int GetFontMatrixMultiplier() => font.GetUnitsPerEm();
public double? GetDescent()
{
return font.TableRegister.HorizontalHeaderTable.Descent;
}
public double? GetAscent()
{
return font.TableRegister.HorizontalHeaderTable.Ascent;
}
public bool TryGetFontMatrix(int characterCode, [NotNullWhen(true)] out TransformationMatrix? matrix)
{
// We don't have a matrix here

View File

@@ -148,6 +148,38 @@
return fontProgram.TryGetFontMatrix(characterIdentifier, out var m) ? m.Value : FontMatrix;
}
public double GetDescent()
{
if (fontProgram is null)
{
return Descriptor.Descent;
}
double? descent = fontProgram.GetDescent();
if (descent.HasValue)
{
return descent.Value;
}
return Descriptor.Descent;
}
public double GetAscent()
{
if (fontProgram is null)
{
return Descriptor.Ascent;
}
double? ascent = fontProgram.GetAscent();
if (ascent.HasValue)
{
return ascent.Value;
}
return Descriptor.Ascent;
}
public bool TryGetPath(int characterCode, [NotNullWhen(true)] out IReadOnlyList<PdfSubpath>? path)
{
path = null;

View File

@@ -132,6 +132,38 @@
return FontMatrix;
}
public double GetDescent()
{
if (fontProgram is null)
{
return Descriptor.Descent;
}
double? descent = fontProgram.GetDescent();
if (descent.HasValue)
{
return descent.Value;
}
return Descriptor.Descent;
}
public double GetAscent()
{
if (fontProgram is null)
{
return Descriptor.Ascent;
}
double? ascent = fontProgram.GetAscent();
if (ascent.HasValue)
{
return ascent.Value;
}
return Descriptor.Ascent;
}
public bool TryGetPath(int characterCode, [NotNullWhen(true)] out IReadOnlyList<PdfSubpath>? path) => TryGetPath(characterCode, cidToGid.GetGlyphIndex, out path);
public bool TryGetPath(int characterCode, Func<int, int?> characterCodeToGlyphId, [NotNullWhen(true)] out IReadOnlyList<PdfSubpath>? path)

View File

@@ -22,6 +22,8 @@
= new Dictionary<int, CharacterBoundingBox>();
private readonly bool useLenientParsing;
private readonly double ascent;
private readonly double descent;
public NameToken Name => BaseFont;
@@ -57,6 +59,30 @@
?? FontDetails.GetDefault(Name.Data);
useLenientParsing = parsingOptions.UseLenientParsing;
ascent = ComputeAscent();
descent = ComputeDescent();
}
private double ComputeDescent()
{
double d = CidFont.GetDescent();
if (Math.Abs(d) > double.Epsilon)
{
return GetFontMatrix().TransformY(d);
}
return -0.25;
}
private double ComputeAscent()
{
double a = CidFont.GetAscent();
if (Math.Abs(a) > double.Epsilon)
{
return GetFontMatrix().TransformY(a);
}
return 0.75;
}
public int ReadCharacterCode(IInputBytes bytes, out int codeLength)
@@ -144,6 +170,16 @@
return CidFont.FontMatrix;
}
public double GetDescent()
{
return descent;
}
public double GetAscent()
{
return ascent;
}
public PdfVector GetPositionVector(int characterCode)
{
var characterIdentifier = CMap.ConvertToCid(characterCode);

View File

@@ -45,6 +45,24 @@
/// </summary>
TransformationMatrix GetFontMatrix();
/// <summary>
/// Retrieves the descent value of the font, adjusted by the font matrix.
/// </summary>
/// <returns>
/// A <see cref="double"/> representing the descent of the font,
/// which is the distance from the baseline to the lowest point of the font's glyphs.
/// </returns>
double GetDescent();
/// <summary>
/// Retrieves the ascent value of the font, adjusted byt the font matrix.
/// </summary>
/// <returns>
/// A <see cref="double"/> representing the ascent of the font,
/// which is the distance from the baseline to the highest point of the font's glyphs.
/// </returns>
double GetAscent();
/// <summary>
/// Returns the glyph path for the given character code.
/// </summary>

View File

@@ -33,6 +33,10 @@
private readonly bool isZapfDingbats;
private readonly TransformationMatrix fontMatrix;
private readonly double descent;
private readonly double ascent;
#nullable disable
public NameToken Name { get; }
#nullable enable
@@ -66,6 +70,37 @@
?? FontDetails.GetDefault(Name?.Data);
isZapfDingbats = encoding is ZapfDingbatsEncoding || Details.Name.Contains("ZapfDingbats");
// Set font matrix
double scale = 1000.0;
if (this.font?.TableRegister.HeaderTable is not null)
{
scale = this.font.GetUnitsPerEm();
}
fontMatrix = TransformationMatrix.FromValues(1.0 / scale, 0, 0, 1.0 / scale, 0, 0);
descent = ComputeDescent();
ascent = ComputeAscent();
}
private double ComputeDescent()
{
if (font is null)
{
return DefaultTransformation.TransformY(descriptor!.Descent);
}
return GetFontMatrix().TransformY(font.TableRegister.HorizontalHeaderTable.Descent);
}
private double ComputeAscent()
{
if (font is null)
{
return DefaultTransformation.TransformY(descriptor!.Ascent);
}
return GetFontMatrix().TransformY(font.TableRegister.HorizontalHeaderTable.Ascent);
}
public int ReadCharacterCode(IInputBytes bytes, out int codeLength)
@@ -195,14 +230,7 @@
public TransformationMatrix GetFontMatrix()
{
var scale = 1000.0;
if (font?.TableRegister.HeaderTable != null)
{
scale = font.GetUnitsPerEm();
}
return TransformationMatrix.FromValues(1 / scale, 0, 0, 1 / scale, 0, 0);
return fontMatrix;
}
private PdfRectangle GetBoundingBoxInGlyphSpace(int characterCode, out bool fromFont)
@@ -338,6 +366,16 @@
return widths[index];
}
public double GetDescent()
{
return descent;
}
public double GetAscent()
{
return ascent;
}
/// <inheritdoc/>
public bool TryGetPath(int characterCode, [NotNullWhen(true)] out IReadOnlyList<PdfSubpath>? path)
{

View File

@@ -19,6 +19,9 @@
private static readonly TransformationMatrix DefaultTransformation =
TransformationMatrix.FromValues(1 / 1000.0, 0, 0, 1 / 1000.0, 0, 0);
private readonly TransformationMatrix fontMatrix;
private readonly double ascent;
private readonly double descent;
private readonly AdobeFontMetrics fontMetrics;
private readonly Encoding encoding;
private readonly TrueTypeFont font;
@@ -45,6 +48,40 @@
// Assumption is ZapfDingbats is not possible here. We need to change the behaviour if not the case
System.Diagnostics.Debug.Assert(!(encoding is ZapfDingbatsEncoding || Details.Name.Contains("ZapfDingbats")));
// Set font matrix
if (this.font?.TableRegister.HeaderTable is not null)
{
var scale = (double)this.font.GetUnitsPerEm();
fontMatrix = TransformationMatrix.FromValues(1.0 / scale, 0, 0, 1.0 / scale, 0, 0);
}
else
{
fontMatrix = DefaultTransformation;
}
descent = ComputeDescent();
ascent = ComputeAscent();
}
private double ComputeDescent()
{
if (fontMetrics is not null)
{
return GetFontMatrix().TransformY(fontMetrics.Descender);
}
return GetFontMatrix().TransformY(font.TableRegister.HorizontalHeaderTable.Descent);
}
private double ComputeAscent()
{
if (fontMetrics is not null)
{
return GetFontMatrix().TransformY(fontMetrics.Ascender);
}
return GetFontMatrix().TransformY(font.TableRegister.HorizontalHeaderTable.Ascent);
}
public int ReadCharacterCode(IInputBytes bytes, out int codeLength)
@@ -127,14 +164,17 @@
public TransformationMatrix GetFontMatrix()
{
if (font?.TableRegister.HeaderTable != null)
{
var scale = (double)font.GetUnitsPerEm();
return TransformationMatrix.FromValues(1 / scale, 0, 0, 1 / scale, 0, 0);
return fontMatrix;
}
return DefaultTransformation;
public double GetDescent()
{
return descent;
}
public double GetAscent()
{
return ascent;
}
/// <inheritdoc/>

View File

@@ -36,6 +36,8 @@
private readonly ToUnicodeCMap toUnicodeCMap;
private readonly TransformationMatrix fontMatrix;
private readonly double ascent;
private readonly double descent;
private readonly bool isZapfDingbats;
@@ -83,6 +85,60 @@
Details = fontDescriptor?.ToDetails(name?.Data)
?? FontDetails.GetDefault(name?.Data);
isZapfDingbats = encoding is ZapfDingbatsEncoding || Details.Name.Contains("ZapfDingbats");
descent = ComputeDescent();
ascent = ComputeAscent();
}
private double ComputeDescent()
{
if (Math.Abs(fontDescriptor.Descent) > double.Epsilon)
{
return fontMatrix.TransformY(fontDescriptor.Descent);
}
/*
// BobLd: Should 'fontProgram' be used
if (fontProgram is not null)
{
if (fontProgram.TryGetFirst(out var t1))
{
}
if (fontProgram.TryGetSecond(out var cffCol))
{
}
}
*/
return -0.25;
}
private double ComputeAscent()
{
if (Math.Abs(fontDescriptor.Ascent) > double.Epsilon)
{
return fontMatrix.TransformY(fontDescriptor.Ascent);
}
/*
// BobLd: Should 'fontProgram' be used
if (fontProgram is not null)
{
if (fontProgram.TryGetFirst(out var t1))
{
}
if (fontProgram.TryGetSecond(out var cffCol))
{
}
}
*/
return 0.75;
}
public int ReadCharacterCode(IInputBytes bytes, out int codeLength)
@@ -206,7 +262,7 @@
{
var first = cffFont.FirstFont;
string characterName;
if (encoding != null)
if (encoding is not null)
{
characterName = encoding.GetName(characterCode);
}
@@ -232,6 +288,16 @@
return fontMatrix;
}
public double GetDescent()
{
return descent;
}
public double GetAscent()
{
return ascent;
}
/// <inheritdoc/>
public bool TryGetPath(int characterCode, [NotNullWhen(true)] out IReadOnlyList<PdfSubpath>? path)
{

View File

@@ -26,6 +26,8 @@ namespace UglyToad.PdfPig.PdfFonts.Simple
public FontDetails Details { get; }
private readonly TransformationMatrix fontMatrix = TransformationMatrix.FromValues(0.001, 0, 0, 0.001, 0, 0);
private readonly double ascent;
private readonly double descent;
public Type1Standard14Font(AdobeFontMetrics standardFontMetrics, Encoding? overrideEncoding = null)
{
@@ -40,6 +42,28 @@ namespace UglyToad.PdfPig.PdfFonts.Simple
standardFontMetrics.Weight == "Bold" ? 700 : FontDetails.DefaultWeight,
standardFontMetrics.ItalicAngle != 0);
isZapfDingbats = encoding is ZapfDingbatsEncoding || Details.Name.Contains("ZapfDingbats");
descent = ComputeDescent();
ascent = ComputeAscent();
}
private double ComputeDescent()
{
if (Math.Abs(standardFontMetrics.Descender) < double.Epsilon)
{
return -0.25;
}
return fontMatrix.TransformY(standardFontMetrics.Descender);
}
private double ComputeAscent()
{
if (Math.Abs(standardFontMetrics.Ascender) < double.Epsilon)
{
return 0.75;
}
return fontMatrix.TransformY(standardFontMetrics.Ascender);
}
public int ReadCharacterCode(IInputBytes bytes, out int codeLength)
@@ -115,6 +139,16 @@ namespace UglyToad.PdfPig.PdfFonts.Simple
return fontMatrix;
}
public double GetDescent()
{
return descent;
}
public double GetAscent()
{
return ascent;
}
/// <summary>
/// <inheritdoc/>
/// <para>Not implemented.</para>

View File

@@ -13,6 +13,8 @@
{
private readonly PdfRectangle boundingBox;
private readonly TransformationMatrix fontMatrix;
private readonly double ascent;
private readonly double descent;
private readonly Encoding encoding;
private readonly int firstChar;
private readonly int lastChar;
@@ -45,6 +47,18 @@
// Assumption is ZapfDingbats is not possible here. We need to change the behaviour if not the case
System.Diagnostics.Debug.Assert(!(encoding is ZapfDingbatsEncoding || Details.Name.Contains("ZapfDingbats")));
descent = ComputeDescent();
ascent = ComputeAscent();
}
private double ComputeDescent()
{
return 0;
}
private double ComputeAscent()
{
return fontMatrix.TransformY(boundingBox.Height);
}
public int ReadCharacterCode(IInputBytes bytes, out int codeLength)
@@ -106,6 +120,16 @@
return fontMatrix;
}
public double GetDescent()
{
return descent;
}
public double GetAscent()
{
return ascent;
}
/// <summary>
/// <inheritdoc/>
/// <para>Type 3 fonts do not use vector paths. Always returns <c>false</c>.</para>

View File

@@ -1069,6 +1069,7 @@
var letter = new Letter(
c.ToString(),
documentSpace,
documentSpace,
advanceRect.BottomLeft,
advanceRect.BottomRight,
width,