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,