From b2f4ca8839bb72b2d220260e3438950c8f0a5885 Mon Sep 17 00:00:00 2001
From: BobLd <38405645+BobLd@users.noreply.github.com>
Date: Sun, 21 Sep 2025 14:27:57 +0100
Subject: [PATCH] Add GetDescent() and GetAscent() methods to IFont, improve
font matrix for TrueTypeSimpleFont and TrueTypeStandard14FallbackSimpleFont
and add loose bounding box to Letter
---
.../TransformationMatrix.cs | 13 +++-
.../ContentOrderTextExtractor.cs | 1 +
.../Dla/UnsupervisedReadingOrderTests.cs | 1 +
.../Fonts/SystemFonts/Linux.cs | 6 +-
.../Integration/IntegrationDocumentTests.cs | 32 +++++++++
.../Integration/Type0FontTests.cs | 2 +
.../GenerateLetterBoundingBoxImages.cs | 26 +++++++
.../GenerateLetterGlyphImages.cs | 25 +++++++
src/UglyToad.PdfPig/Content/Letter.cs | 28 +++++---
.../Graphics/ContentStreamProcessor.cs | 24 +++++--
.../PdfFonts/CidFonts/ICidFont.cs | 4 ++
.../PdfFonts/CidFonts/ICidFontProgram.cs | 4 ++
.../CidFonts/PdfCidCompactFontFormatFont.cs | 12 ++++
.../PdfFonts/CidFonts/PdfCidTrueTypeFont.cs | 10 +++
.../PdfFonts/CidFonts/Type0CidFont.cs | 32 +++++++++
.../PdfFonts/CidFonts/Type2CidFont.cs | 32 +++++++++
.../PdfFonts/Composite/Type0Font.cs | 36 ++++++++++
src/UglyToad.PdfPig/PdfFonts/IFont.cs | 18 +++++
.../PdfFonts/Simple/TrueTypeSimpleFont.cs | 54 +++++++++++---
.../TrueTypeStandard14FallbackSimpleFont.cs | 52 ++++++++++++--
.../PdfFonts/Simple/Type1FontSimple.cs | 70 ++++++++++++++++++-
.../PdfFonts/Simple/Type1Standard14Font.cs | 34 +++++++++
.../PdfFonts/Simple/Type3Font.cs | 24 +++++++
src/UglyToad.PdfPig/Writer/PdfPageBuilder.cs | 1 +
24 files changed, 507 insertions(+), 34 deletions(-)
diff --git a/src/UglyToad.PdfPig.Core/TransformationMatrix.cs b/src/UglyToad.PdfPig.Core/TransformationMatrix.cs
index db6ab8e2..871c9e2f 100644
--- a/src/UglyToad.PdfPig.Core/TransformationMatrix.cs
+++ b/src/UglyToad.PdfPig.Core/TransformationMatrix.cs
@@ -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;
+
+ ///
+ /// Transform an Y coordinate using this transformation matrix.
+ ///
+ /// The Y coordinate.
+ /// The transformed Y coordinate.
+ public double TransformY(double y)
+ {
+ return D * y + F;
}
///
diff --git a/src/UglyToad.PdfPig.DocumentLayoutAnalysis/TextExtractor/ContentOrderTextExtractor.cs b/src/UglyToad.PdfPig.DocumentLayoutAnalysis/TextExtractor/ContentOrderTextExtractor.cs
index 1e23d92e..6b61c80e 100644
--- a/src/UglyToad.PdfPig.DocumentLayoutAnalysis/TextExtractor/ContentOrderTextExtractor.cs
+++ b/src/UglyToad.PdfPig.DocumentLayoutAnalysis/TextExtractor/ContentOrderTextExtractor.cs
@@ -60,6 +60,7 @@
letter = new Letter(
" ",
letter.GlyphRectangle,
+ letter.GlyphRectangleLoose,
letter.StartBaseLine,
letter.EndBaseLine,
letter.Width,
diff --git a/src/UglyToad.PdfPig.Tests/Dla/UnsupervisedReadingOrderTests.cs b/src/UglyToad.PdfPig.Tests/Dla/UnsupervisedReadingOrderTests.cs
index 5964be46..b1db12da 100644
--- a/src/UglyToad.PdfPig.Tests/Dla/UnsupervisedReadingOrderTests.cs
+++ b/src/UglyToad.PdfPig.Tests/Dla/UnsupervisedReadingOrderTests.cs
@@ -60,6 +60,7 @@
private static TextBlock CreateFakeTextBlock(PdfRectangle boundingBox)
{
var letter = new Letter("a",
+ boundingBox,
boundingBox,
boundingBox.BottomLeft,
boundingBox.BottomRight,
diff --git a/src/UglyToad.PdfPig.Tests/Fonts/SystemFonts/Linux.cs b/src/UglyToad.PdfPig.Tests/Fonts/SystemFonts/Linux.cs
index 0906c8d4..105c2f43 100644
--- a/src/UglyToad.PdfPig.Tests/Fonts/SystemFonts/Linux.cs
+++ b/src/UglyToad.PdfPig.Tests/Fonts/SystemFonts/Linux.cs
@@ -4,6 +4,7 @@ using UglyToad.PdfPig.Tests.Dla;
namespace UglyToad.PdfPig.Tests.Fonts.SystemFonts
{
using PdfPig.Core;
+ using PdfPig.Geometry;
public class Linux
{
@@ -68,7 +69,10 @@ namespace UglyToad.PdfPig.Tests.Fonts.SystemFonts
Assert.Equal(expectedData.TopLeft.Y, current.GlyphRectangle.TopLeft.Y, 6);
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.Equal(expectedData.Rotation, current.GlyphRectangle.Rotation, 3);
+
+ Assert.True(current.GlyphRectangle.IntersectsWith(current.GlyphRectangleLoose));
+ Assert.Equal(current.GlyphRectangle.Rotation, current.GlyphRectangleLoose.Rotation, 3);
}
}
}
diff --git a/src/UglyToad.PdfPig.Tests/Integration/IntegrationDocumentTests.cs b/src/UglyToad.PdfPig.Tests/Integration/IntegrationDocumentTests.cs
index 1d347915..45a6521d 100644
--- a/src/UglyToad.PdfPig.Tests/Integration/IntegrationDocumentTests.cs
+++ b/src/UglyToad.PdfPig.Tests/Integration/IntegrationDocumentTests.cs
@@ -1,5 +1,7 @@
namespace UglyToad.PdfPig.Tests.Integration
{
+ using PdfPig.Geometry;
+
public class IntegrationDocumentTests
{
private static readonly Lazy DocumentFolder = new Lazy(() => 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)
diff --git a/src/UglyToad.PdfPig.Tests/Integration/Type0FontTests.cs b/src/UglyToad.PdfPig.Tests/Integration/Type0FontTests.cs
index c3c924a8..f5bdd3b4 100644
--- a/src/UglyToad.PdfPig.Tests/Integration/Type0FontTests.cs
+++ b/src/UglyToad.PdfPig.Tests/Integration/Type0FontTests.cs
@@ -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);
}
}
}
diff --git a/src/UglyToad.PdfPig.Tests/Integration/VisualVerification/GenerateLetterBoundingBoxImages.cs b/src/UglyToad.PdfPig.Tests/Integration/VisualVerification/GenerateLetterBoundingBoxImages.cs
index 0a2fd9e7..0c06771f 100644
--- a/src/UglyToad.PdfPig.Tests/Integration/VisualVerification/GenerateLetterBoundingBoxImages.cs
+++ b/src/UglyToad.PdfPig.Tests/Integration/VisualVerification/GenerateLetterBoundingBoxImages.cs
@@ -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);
+ }
+ }
}
}
diff --git a/src/UglyToad.PdfPig.Tests/Integration/VisualVerification/GenerateLetterGlyphImages.cs b/src/UglyToad.PdfPig.Tests/Integration/VisualVerification/GenerateLetterGlyphImages.cs
index c5df64b4..763c097e 100644
--- a/src/UglyToad.PdfPig.Tests/Integration/VisualVerification/GenerateLetterGlyphImages.cs
+++ b/src/UglyToad.PdfPig.Tests/Integration/VisualVerification/GenerateLetterGlyphImages.cs
@@ -85,6 +85,31 @@
d.SaveTo(fs);
}
}
+
+ using (var picture = document.GetPage(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);
+ }
+ }
}
}
diff --git a/src/UglyToad.PdfPig/Content/Letter.cs b/src/UglyToad.PdfPig/Content/Letter.cs
index ec4ad313..27333390 100644
--- a/src/UglyToad.PdfPig/Content/Letter.cs
+++ b/src/UglyToad.PdfPig/Content/Letter.cs
@@ -46,6 +46,12 @@
///
public PdfRectangle GlyphRectangle { get; }
+ ///
+ /// The loose bounding box for the glyph. Contrary to the , the loose bounding box will be the same across all glyphes of the same font.
+ /// It takes in account the font Ascent and Descent.
+ ///
+ public PdfRectangle GlyphRectangleLoose { get; }
+
///
/// 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.
///
@@ -60,7 +66,7 @@
/// Details about the font for this letter.
///
public FontDetails FontDetails { get; }
-
+
///
/// Details about the font for this letter.
///
@@ -103,13 +109,14 @@
///
/// Sequence number of the ShowText operation that printed this letter.
///
- public int TextSequence { get; }
+ public int TextSequence { get; }
///
/// Create a new letter to represent some text drawn by the Tj operator.
///
public Letter(string value,
- PdfRectangle glyphRectangle,
+ 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,
@@ -131,7 +138,8 @@
/// Create a new letter to represent some text drawn by the Tj operator.
///
public Letter(string value,
- PdfRectangle glyphRectangle,
+ 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;
@@ -196,7 +207,8 @@
public Letter AsBold()
{
return new Letter(Value,
- GlyphRectangle,
+ GlyphRectangle,
+ GlyphRectangleLoose,
StartBaseLine,
EndBaseLine,
Width,
diff --git a/src/UglyToad.PdfPig/Graphics/ContentStreamProcessor.cs b/src/UglyToad.PdfPig/Graphics/ContentStreamProcessor.cs
index ac52c85e..b8bb6106 100644
--- a/src/UglyToad.PdfPig/Graphics/ContentStreamProcessor.cs
+++ b/src/UglyToad.PdfPig/Graphics/ContentStreamProcessor.cs
@@ -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);
}
diff --git a/src/UglyToad.PdfPig/PdfFonts/CidFonts/ICidFont.cs b/src/UglyToad.PdfPig/PdfFonts/CidFonts/ICidFont.cs
index 509c8b3d..a93f1d7f 100644
--- a/src/UglyToad.PdfPig/PdfFonts/CidFonts/ICidFont.cs
+++ b/src/UglyToad.PdfPig/PdfFonts/CidFonts/ICidFont.cs
@@ -57,6 +57,10 @@
TransformationMatrix GetFontMatrix(int characterIdentifier);
+ double GetDescent();
+
+ double GetAscent();
+
///
/// Returns the glyph path for the given character code.
///
diff --git a/src/UglyToad.PdfPig/PdfFonts/CidFonts/ICidFontProgram.cs b/src/UglyToad.PdfPig/PdfFonts/CidFonts/ICidFontProgram.cs
index 2638e92b..a076856a 100644
--- a/src/UglyToad.PdfPig/PdfFonts/CidFonts/ICidFontProgram.cs
+++ b/src/UglyToad.PdfPig/PdfFonts/CidFonts/ICidFontProgram.cs
@@ -20,6 +20,10 @@
bool TryGetBoundingAdvancedWidth(int characterIdentifier, out double width);
+ double? GetDescent();
+
+ double? GetAscent();
+
bool TryGetPath(int characterCode, [NotNullWhen(true)] out IReadOnlyList? path);
bool TryGetPath(int characterCode, Func characterCodeToGlyphId, [NotNullWhen(true)] out IReadOnlyList? path);
diff --git a/src/UglyToad.PdfPig/PdfFonts/CidFonts/PdfCidCompactFontFormatFont.cs b/src/UglyToad.PdfPig/PdfFonts/CidFonts/PdfCidCompactFontFormatFont.cs
index ac8f54ee..b699700a 100644
--- a/src/UglyToad.PdfPig/PdfFonts/CidFonts/PdfCidCompactFontFormatFont.cs
+++ b/src/UglyToad.PdfPig/PdfFonts/CidFonts/PdfCidCompactFontFormatFont.cs
@@ -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);
diff --git a/src/UglyToad.PdfPig/PdfFonts/CidFonts/PdfCidTrueTypeFont.cs b/src/UglyToad.PdfPig/PdfFonts/CidFonts/PdfCidTrueTypeFont.cs
index 12c59deb..af80c54f 100644
--- a/src/UglyToad.PdfPig/PdfFonts/CidFonts/PdfCidTrueTypeFont.cs
+++ b/src/UglyToad.PdfPig/PdfFonts/CidFonts/PdfCidTrueTypeFont.cs
@@ -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
diff --git a/src/UglyToad.PdfPig/PdfFonts/CidFonts/Type0CidFont.cs b/src/UglyToad.PdfPig/PdfFonts/CidFonts/Type0CidFont.cs
index ff66b031..69b86b62 100644
--- a/src/UglyToad.PdfPig/PdfFonts/CidFonts/Type0CidFont.cs
+++ b/src/UglyToad.PdfPig/PdfFonts/CidFonts/Type0CidFont.cs
@@ -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? path)
{
path = null;
diff --git a/src/UglyToad.PdfPig/PdfFonts/CidFonts/Type2CidFont.cs b/src/UglyToad.PdfPig/PdfFonts/CidFonts/Type2CidFont.cs
index 65487bfb..7e501a3a 100644
--- a/src/UglyToad.PdfPig/PdfFonts/CidFonts/Type2CidFont.cs
+++ b/src/UglyToad.PdfPig/PdfFonts/CidFonts/Type2CidFont.cs
@@ -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? path) => TryGetPath(characterCode, cidToGid.GetGlyphIndex, out path);
public bool TryGetPath(int characterCode, Func characterCodeToGlyphId, [NotNullWhen(true)] out IReadOnlyList? path)
diff --git a/src/UglyToad.PdfPig/PdfFonts/Composite/Type0Font.cs b/src/UglyToad.PdfPig/PdfFonts/Composite/Type0Font.cs
index 3184eb50..fe91cab0 100644
--- a/src/UglyToad.PdfPig/PdfFonts/Composite/Type0Font.cs
+++ b/src/UglyToad.PdfPig/PdfFonts/Composite/Type0Font.cs
@@ -22,6 +22,8 @@
= new Dictionary();
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);
diff --git a/src/UglyToad.PdfPig/PdfFonts/IFont.cs b/src/UglyToad.PdfPig/PdfFonts/IFont.cs
index 05fd8245..c44b7591 100644
--- a/src/UglyToad.PdfPig/PdfFonts/IFont.cs
+++ b/src/UglyToad.PdfPig/PdfFonts/IFont.cs
@@ -45,6 +45,24 @@
///
TransformationMatrix GetFontMatrix();
+ ///
+ /// Retrieves the descent value of the font, adjusted by the font matrix.
+ ///
+ ///
+ /// A representing the descent of the font,
+ /// which is the distance from the baseline to the lowest point of the font's glyphs.
+ ///
+ double GetDescent();
+
+ ///
+ /// Retrieves the ascent value of the font, adjusted byt the font matrix.
+ ///
+ ///
+ /// A representing the ascent of the font,
+ /// which is the distance from the baseline to the highest point of the font's glyphs.
+ ///
+ double GetAscent();
+
///
/// Returns the glyph path for the given character code.
///
diff --git a/src/UglyToad.PdfPig/PdfFonts/Simple/TrueTypeSimpleFont.cs b/src/UglyToad.PdfPig/PdfFonts/Simple/TrueTypeSimpleFont.cs
index e16f89ee..85421182 100644
--- a/src/UglyToad.PdfPig/PdfFonts/Simple/TrueTypeSimpleFont.cs
+++ b/src/UglyToad.PdfPig/PdfFonts/Simple/TrueTypeSimpleFont.cs
@@ -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;
+ }
+
///
public bool TryGetPath(int characterCode, [NotNullWhen(true)] out IReadOnlyList? path)
{
diff --git a/src/UglyToad.PdfPig/PdfFonts/Simple/TrueTypeStandard14FallbackSimpleFont.cs b/src/UglyToad.PdfPig/PdfFonts/Simple/TrueTypeStandard14FallbackSimpleFont.cs
index c4aa78a9..13bd00a9 100644
--- a/src/UglyToad.PdfPig/PdfFonts/Simple/TrueTypeStandard14FallbackSimpleFont.cs
+++ b/src/UglyToad.PdfPig/PdfFonts/Simple/TrueTypeStandard14FallbackSimpleFont.cs
@@ -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,8 +48,42 @@
// 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)
{
codeLength = 1;
@@ -127,14 +164,17 @@
public TransformationMatrix GetFontMatrix()
{
- if (font?.TableRegister.HeaderTable != null)
- {
- var scale = (double)font.GetUnitsPerEm();
+ return fontMatrix;
+ }
- return TransformationMatrix.FromValues(1 / scale, 0, 0, 1 / scale, 0, 0);
- }
+ public double GetDescent()
+ {
+ return descent;
+ }
- return DefaultTransformation;
+ public double GetAscent()
+ {
+ return ascent;
}
///
diff --git a/src/UglyToad.PdfPig/PdfFonts/Simple/Type1FontSimple.cs b/src/UglyToad.PdfPig/PdfFonts/Simple/Type1FontSimple.cs
index ca007695..fefb1ddb 100644
--- a/src/UglyToad.PdfPig/PdfFonts/Simple/Type1FontSimple.cs
+++ b/src/UglyToad.PdfPig/PdfFonts/Simple/Type1FontSimple.cs
@@ -36,7 +36,9 @@
private readonly ToUnicodeCMap toUnicodeCMap;
private readonly TransformationMatrix fontMatrix;
-
+ private readonly double ascent;
+ private readonly double descent;
+
private readonly bool isZapfDingbats;
public NameToken Name { get; }
@@ -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;
+ }
+
///
public bool TryGetPath(int characterCode, [NotNullWhen(true)] out IReadOnlyList? path)
{
diff --git a/src/UglyToad.PdfPig/PdfFonts/Simple/Type1Standard14Font.cs b/src/UglyToad.PdfPig/PdfFonts/Simple/Type1Standard14Font.cs
index 63ba9bdb..2e6a3a29 100644
--- a/src/UglyToad.PdfPig/PdfFonts/Simple/Type1Standard14Font.cs
+++ b/src/UglyToad.PdfPig/PdfFonts/Simple/Type1Standard14Font.cs
@@ -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;
+ }
+
///
///
/// Not implemented.
diff --git a/src/UglyToad.PdfPig/PdfFonts/Simple/Type3Font.cs b/src/UglyToad.PdfPig/PdfFonts/Simple/Type3Font.cs
index 5fc4f7a0..811ff951 100644
--- a/src/UglyToad.PdfPig/PdfFonts/Simple/Type3Font.cs
+++ b/src/UglyToad.PdfPig/PdfFonts/Simple/Type3Font.cs
@@ -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;
+ }
+
///
///
/// Type 3 fonts do not use vector paths. Always returns false.
diff --git a/src/UglyToad.PdfPig/Writer/PdfPageBuilder.cs b/src/UglyToad.PdfPig/Writer/PdfPageBuilder.cs
index 65c8f686..ff192d7c 100644
--- a/src/UglyToad.PdfPig/Writer/PdfPageBuilder.cs
+++ b/src/UglyToad.PdfPig/Writer/PdfPageBuilder.cs
@@ -1069,6 +1069,7 @@
var letter = new Letter(
c.ToString(),
documentSpace,
+ documentSpace,
advanceRect.BottomLeft,
advanceRect.BottomRight,
width,