diff --git a/src/SixLabors.Fonts/GlyphPositioningCollection.cs b/src/SixLabors.Fonts/GlyphPositioningCollection.cs index ed615869..fe395599 100644 --- a/src/SixLabors.Fonts/GlyphPositioningCollection.cs +++ b/src/SixLabors.Fonts/GlyphPositioningCollection.cs @@ -300,6 +300,8 @@ public void UpdatePosition(FontMetrics fontMetrics, int index) bool isDirtyWH = data.Bounds.IsDirtyWH; if (!isDirtyXY && !isDirtyWH) { + // No change required but the glyph has been processed. + data.IsPositioned = true; return; } @@ -311,12 +313,14 @@ public void UpdatePosition(FontMetrics fontMetrics, int index) if (isDirtyXY) { m.ApplyOffset((short)data.Bounds.X, (short)data.Bounds.Y); + data.IsPositioned = true; } if (isDirtyWH) { m.SetAdvanceWidth((ushort)data.Bounds.Width); m.SetAdvanceHeight((ushort)data.Bounds.Height); + data.IsPositioned = true; } } } @@ -362,7 +366,15 @@ public void Advance(FontMetrics fontMetrics, int index, ushort glyphId, short dx /// The zero-based index of the elements to position. /// if the element should be processed; otherwise, . public bool ShouldProcess(FontMetrics fontMetrics, int index) - => this.glyphs[index].Metrics.FontMetrics == fontMetrics; + { + GlyphPositioningData data = this.glyphs[index]; + if (data.Data.IsPositioned) + { + return false; + } + + return data.Metrics.FontMetrics == fontMetrics; + } [DebuggerDisplay("{DebuggerDisplay,nq}")] private class GlyphPositioningData diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index 4e0e3e93..0d306bbe 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -39,6 +39,9 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) this.CursiveAttachment = data.CursiveAttachment; this.IsSubstituted = data.IsSubstituted; this.IsDecomposed = data.IsDecomposed; + this.IsPositioned = data.IsPositioned; + this.IsKerned = data.IsKerned; + if (data.UniversalShapingEngineInfo != null) { this.UniversalShapingEngineInfo = new( @@ -144,6 +147,16 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) /// public bool IsDecomposed { get; set; } + /// + /// Gets or sets a value indicating whether this glyph has been positioned. + /// + public bool IsPositioned { get; set; } + + /// + /// Gets or sets a value indicating whether this glyph has been kerned. + /// + public bool IsKerned { get; set; } + /// /// Gets or sets the universal shaping information. /// diff --git a/src/SixLabors.Fonts/Tables/General/Kern/Format0SubTable.cs b/src/SixLabors.Fonts/Tables/General/Kern/Format0SubTable.cs index 7e89fe8b..83953b62 100644 --- a/src/SixLabors.Fonts/Tables/General/Kern/Format0SubTable.cs +++ b/src/SixLabors.Fonts/Tables/General/Kern/Format0SubTable.cs @@ -17,14 +17,15 @@ public static Format0SubTable Load(BigEndianBinaryReader reader, in KerningCover // -------|---------------|-------------------------------------------------------- // uint16 | nPairs | This gives the number of kerning pairs in the table. // uint16 | searchRange | The largest power of two less than or equal to the value of nPairs, multiplied by the size in bytes of an entry in the table. - // uint16 | entrySelector | This is calculated as log2 of the largest power of two less than or equal to the value of nPairs.This value indicates how many iterations of the search loop will have to be made. (For example, in a list of eight items, there would have to be three iterations of the loop). + // uint16 | entrySelector | This is calculated as log2 of the largest power of two less than or equal to the value of nPairs. + // | | This value indicates how many iterations of the search loop will have to be made. (For example, in a list of eight items, there would have to be three iterations of the loop). // uint16 | rangeShift | The value of nPairs minus the largest power of two less than or equal to nPairs, and then multiplied by the size in bytes of an entry in the table. ushort pairCount = reader.ReadUInt16(); ushort searchRange = reader.ReadUInt16(); ushort entrySelector = reader.ReadUInt16(); ushort rangeShift = reader.ReadUInt16(); - var pairs = new KerningPair[pairCount]; + KerningPair[] pairs = new KerningPair[pairCount]; for (int i = 0; i < pairCount; i++) { pairs[i] = KerningPair.Read(reader); diff --git a/src/SixLabors.Fonts/Tables/General/Kern/KerningSubTable.cs b/src/SixLabors.Fonts/Tables/General/Kern/KerningSubTable.cs index e7a7ce8c..d5076e84 100644 --- a/src/SixLabors.Fonts/Tables/General/Kern/KerningSubTable.cs +++ b/src/SixLabors.Fonts/Tables/General/Kern/KerningSubTable.cs @@ -27,7 +27,7 @@ public KerningSubTable(KerningCoverage coverage) // +--------+----------+----------------------------------------------------------+ ushort subVersion = reader.ReadUInt16(); ushort length = reader.ReadUInt16(); - var coverage = KerningCoverage.Read(reader); + KerningCoverage coverage = KerningCoverage.Read(reader); if (coverage.Format == 0) { return Format0SubTable.Load(reader, coverage); diff --git a/src/SixLabors.Fonts/Tables/General/Kern/KerningTable.cs b/src/SixLabors.Fonts/Tables/General/Kern/KerningTable.cs index 26654172..255d3d6d 100644 --- a/src/SixLabors.Fonts/Tables/General/Kern/KerningTable.cs +++ b/src/SixLabors.Fonts/Tables/General/Kern/KerningTable.cs @@ -20,7 +20,7 @@ public static KerningTable Load(FontReader fontReader) if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) { // this table is optional. - return new KerningTable(Array.Empty()); + return new KerningTable([]); } using (binaryReader) @@ -52,7 +52,7 @@ public static KerningTable Load(BigEndianBinaryReader reader) } } - return new KerningTable(tables.ToArray()); + return new KerningTable([.. tables]); } public void UpdatePositions(FontMetrics fontMetrics, GlyphPositioningCollection collection, int left, int right) @@ -62,12 +62,20 @@ public void UpdatePositions(FontMetrics fontMetrics, GlyphPositioningCollection return; } - ushort current = collection[left].GlyphId; - ushort next = collection[right].GlyphId; + GlyphShapingData current = collection[left]; + if (current.IsKerned) + { + // Already kerned via previous processing. + return; + } + + ushort currentId = current.GlyphId; + ushort nextId = collection[right].GlyphId; - if (this.TryGetKerningOffset(current, next, out Vector2 result)) + if (this.TryGetKerningOffset(currentId, nextId, out Vector2 result)) { - collection.Advance(fontMetrics, left, current, (short)result.X, (short)result.Y); + collection.Advance(fontMetrics, left, currentId, (short)result.X, (short)result.Y); + current.IsKerned = true; } } diff --git a/src/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.cs b/src/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.cs index 00ca5683..76a83a33 100644 --- a/src/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.cs +++ b/src/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.cs @@ -7,11 +7,32 @@ namespace SixLabors.Fonts.Tables.TrueType.Hinting; /// -/// Code adapted from -/// For further information -/// -/// -/// +/// Code adapted from +/// . +/// +/// Reference material: +/// – +/// the original TrueType instruction set and execution model. +/// – +/// details on how Microsoft's ClearType rasterizer interprets TrueType hints. +/// – +/// documentation of FreeType's subpixel hinting engines, including the v40 "minimal" interpreter. +/// +/// +/// This implementation matches the behavior of FreeType's v40 subpixel hinting interpreter, +/// with horizontal hinting disabled and full vertical TrueType instruction processing preserved. +/// It follows the v40 model in which outlines are adjusted primarily along the Y-axis and +/// instructions operate without backward compatibility constraints. This corresponds to +/// FreeType's configuration where TT_CONFIG_OPTION_SUBPIXEL_HINTING selects the +/// minimal (v40) engine and backward_compatibility is forced to zero. +/// +/// +/// +/// Modern ClearType-hinted fonts are designed for this style of processing and will render +/// consistently under this interpreter. Legacy CRT-era fonts such as Arial or Times New Roman +/// also render cleanly under v40 semantics, though without legacy bi-level horizontal snapping, +/// which v40 intentionally omits. +/// /// internal class TrueTypeInterpreter { @@ -567,7 +588,7 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF continue; } - this.points.Current[i].OnCurve ^= true; + this.points.Current[index].OnCurve ^= true; } this.state.Loop = 1; @@ -852,7 +873,7 @@ private void Execute(StackInstructionStream stream, bool inFunction, bool allowF } else { - this.iupYCalled = true; + this.iupXCalled = true; touchMask = TouchState.X; current = (byte*)¤tPtr->Point.X; original = (byte*)&originalPtr->Point.X; @@ -1565,7 +1586,7 @@ private void SetVectorToLine(int mode, bool dual) if (dual) { p1 = this.zp2.GetOriginal(index1); - p2 = this.zp2.GetOriginal(index2); + p2 = this.zp1.GetOriginal(index2); line = p2 - p1; if (line.LengthSquared() == 0) @@ -1803,7 +1824,7 @@ private void ShiftPoints(Vector2 displacement) bool postIUP = this.iupXCalled && this.iupYCalled; bool composite = this.isComposite; ControlPoint[] current = this.zp2.Current; - bool inTwilight = this.zp0.IsTwilight && this.zp1.IsTwilight && this.zp2.IsTwilight; + bool inTwilight = this.zp0.IsTwilight || this.zp1.IsTwilight || this.zp2.IsTwilight; for (int i = 0; i < this.state.Loop; i++) { @@ -1851,41 +1872,172 @@ private float Round(float value) { switch (this.state.RoundState) { + case RoundMode.Off: + // FreeType's Round_None with compensation = 0. + return value; + case RoundMode.ToGrid: - return value >= 0 ? (float)Math.Round(value) : -(float)Math.Round(-value); + { + // Round_To_Grid with compensation = 0. + if (value >= 0F) + { + float val = (float)Math.Floor(value + 0.5F); + if (val < 0F) + { + val = 0F; + } + + return val; + } + else + { + float val = -(float)Math.Floor(-value + 0.5F); + if (val > 0F) + { + val = 0F; + } + + return val; + } + } + case RoundMode.ToHalfGrid: - return value >= 0 ? (float)Math.Floor(value) + 0.5f : -((float)Math.Floor(-value) + 0.5f); - case RoundMode.ToDoubleGrid: - return value >= 0 ? (float)(Math.Round(value * 2, MidpointRounding.AwayFromZero) / 2) : -(float)(Math.Round(-value * 2, MidpointRounding.AwayFromZero) / 2); + { + // Round_To_Half_Grid with compensation = 0. + if (value >= 0F) + { + float val = (float)Math.Floor(value) + 0.5F; + if (val < 0F) + { + val = 0.5F; + } + + return val; + } + else + { + float val = -((float)Math.Floor(-value) + 0.5F); + if (val > 0F) + { + val = -0.5F; + } + + return val; + } + } + case RoundMode.DownToGrid: - return value >= 0 ? (float)Math.Floor(value) : -(float)Math.Floor(-value); + { + // Round_Down_To_Grid with compensation = 0. + if (value >= 0F) + { + float val = (float)Math.Floor(value); + if (val < 0F) + { + val = 0F; + } + + return val; + } + else + { + float val = -(float)Math.Floor(-value); + if (val > 0F) + { + val = 0F; + } + + return val; + } + } + case RoundMode.UpToGrid: - return value >= 0 ? (float)Math.Ceiling(value) : -(float)Math.Ceiling(-value); + { + // Round_Up_To_Grid with compensation = 0. + if (value >= 0F) + { + float val = (float)Math.Ceiling(value); + if (val < 0F) + { + val = 0F; + } + + return val; + } + else + { + float val = -(float)Math.Ceiling(-value); + if (val > 0F) + { + val = 0F; + } + + return val; + } + } + + case RoundMode.ToDoubleGrid: + { + // Round_To_Double_Grid: grid step is 0.5 pixels. + const float step = 0.5F; + + if (value >= 0F) + { + float val = step * (float)Math.Floor((value / step) + 0.5F); + if (val < 0F) + { + val = 0F; + } + + return val; + } + else + { + float val = -step * (float)Math.Floor((-value / step) + 0.5F); + if (val > 0F) + { + val = 0F; + } + + return val; + } + } + case RoundMode.Super: case RoundMode.Super45: - float result; - if (value >= 0) + { + // Round_Super / Round_Super_45 with compensation = 0. + float period = this.roundPeriod; + float phase = this.roundPhase; + float threshold = this.roundThreshold; + + if (value >= 0F) { - result = value - this.roundPhase + this.roundThreshold; - result = (float)Math.Truncate(result / this.roundPeriod) * this.roundPeriod; - result += this.roundPhase; - if (result < 0) + float val = value - phase + threshold; + val = (float)Math.Floor(val / period) * period; + val += phase; + + if (val < 0F) { - result = this.roundPhase; + val = phase; } + + return val; } else { - result = -value - this.roundPhase + this.roundThreshold; - result = -(float)Math.Truncate(result / this.roundPeriod) * this.roundPeriod; - result -= this.roundPhase; - if (result > 0) + float val = -value - phase + threshold; + val = (float)Math.Floor(val / period) * period; + val = -val - phase; + + if (val > 0F) { - result = -this.roundPhase; + val = -phase; } - } - return result; + return val; + } + } default: return value; diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index a192a2d9..a33d0efd 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -175,9 +175,18 @@ private static TextBox ProcessText(ReadOnlySpan text, TextOptions options) // Update the positions of the glyphs in the completed collection. // Each set of metrics is associated with single font and will only be updated // by that font so it's safe to use a single collection. - foreach (TextRun textRun in textRuns) + Font? lastFont = null; + for (int i = 0; i < textRuns.Count; i++) { + TextRun textRun = textRuns[i]; + + if (textRun.Font == lastFont) + { + continue; + } + textRun.Font!.FontMetrics.UpdatePositions(positionings); + lastFont = textRun.Font; } foreach (Font font in fallbackFonts) diff --git a/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-Arial-.png b/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-Arial-.png new file mode 100644 index 00000000..e163c78e --- /dev/null +++ b/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-Arial-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:427453b8e698998850955582fccf1362bf9501cbfdd81c1530094f62185c7396 +size 593953 diff --git a/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-OpenSansFile-.png b/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-OpenSansFile-.png new file mode 100644 index 00000000..0a78d7bd --- /dev/null +++ b/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-OpenSansFile-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:984d06c028a10b7576561fb8374cc07a7adbb0a6f4c3b90b6e731dc0154e458b +size 614364 diff --git a/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-Tahoma-.png b/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-Tahoma-.png new file mode 100644 index 00000000..6f2789c6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Test_Hinting_Robustness_-Tahoma-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:029ec203353022183b429a53af31eb2a62f03f751746390b4e4aaf4cd6374f95 +size 580804 diff --git a/tests/SixLabors.Fonts.Tests/Fonts/tahoma.ttf b/tests/SixLabors.Fonts.Tests/Fonts/tahoma.ttf new file mode 100644 index 00000000..62148d26 Binary files /dev/null and b/tests/SixLabors.Fonts.Tests/Fonts/tahoma.ttf differ diff --git a/tests/SixLabors.Fonts.Tests/HintingTests.cs b/tests/SixLabors.Fonts.Tests/HintingTests.cs new file mode 100644 index 00000000..a9af18ea --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/HintingTests.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Text; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Tests; + +public class HintingTests +{ + public static TheoryData HintingTestData { get; } = new() + { + // Arial and Tahoma are legacy TrueType fonts whose bytecode was written + // for pre-ClearType rasterizers. Under a v40-style interpreter (vertical + // hinting only, no horizontal grid-fitting, no backward-compatibility + // constraints), both fonts generally render cleanly, but small differences + // in horizontal features, joins and bar heights can occur at low ppem. + // This behaviour matches FreeType v40 expectations for older fonts that + // relied on full-axis grid fitting in legacy engines. + { TestFonts.Arial, nameof(TestFonts.Arial) }, + { TestFonts.Tahoma, nameof(TestFonts.Tahoma) }, + + // Modern ClearType-hinted OpenType fonts (for example Open Sans) are + // authored for the same vertical-dominant model used by v40 and therefore + // render consistently and predictably under these semantics. + { TestFonts.OpenSansFile, nameof(TestFonts.OpenSansFile) }, + }; + + [Theory] + [MemberData(nameof(HintingTestData))] + public void Test_Hinting_Robustness(string path, string name) + { + const string copy = "The quick brown fox jumps over the lazy dog."; + FontCollection collection = new(); + FontFamily family = collection.Add(path); + Font font = family.CreateFont(5); + + int fontSize = 5; + int start = 0; + int end = copy.GetGraphemeCount(); + int length = (end - start) + 1; // include the line ending. + List textRuns = []; + StringBuilder stringBuilder = new(); + while (fontSize < 64) + { + stringBuilder.AppendLine(copy); + TextRun run = new() + { + Start = start, + End = end, + Font = new Font(font, fontSize), + }; + + textRuns.Add(run); + fontSize += 1; + start += length; + end += length; + } + + string text = stringBuilder.ToString(); + + TextOptions options = new(font) + { + TextRuns = textRuns, + HintingMode = HintingMode.Standard, + }; + + TextLayoutTestUtilities.TestLayout( + text, + options, + properties: name); + } +} diff --git a/tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs b/tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs index 5fe7d3c9..828bf101 100644 --- a/tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs +++ b/tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs @@ -6,12 +6,19 @@ using System.Text; using SixLabors.Fonts.Tests.ImageComparison; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.Fonts.Tests.TestUtilities; public static class TestImageExtensions { + private static readonly PngEncoder Encoder = new() + { + CompressionLevel = PngCompressionLevel.BestCompression, + FilterMethod = PngFilterMethod.Adaptive + }; + public static string DebugSave( this Image image, string extension = null, @@ -24,8 +31,17 @@ public static string DebugSave( Directory.CreateDirectory(outputDirectory); } - string path = Path.Combine(outputDirectory, $"{test}{FormatTestDetails(properties)}.{extension ?? "png"}"); - image.Save(path); + string ext = extension ?? "png"; + string path = Path.Combine(outputDirectory, $"{test}{FormatTestDetails(properties)}.{ext}"); + + if (ext == "png") + { + image.Save(path, Encoder); + } + else + { + image.Save(path); + } return path; } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_462.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_462.cs index 8317477f..4a7a1de0 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_462.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_462.cs @@ -42,7 +42,8 @@ public void CanRenderEmojiFont_With_COLRv1() TextLayoutTestUtilities.TestLayout( text, options, - includeGeometry: true); + includeGeometry: true, + customDecorations: true); } [Fact] @@ -74,6 +75,7 @@ public void CanRenderEmojiFont_With_SVG() TextLayoutTestUtilities.TestLayout( text, options, - includeGeometry: true); + includeGeometry: true, + customDecorations: true); } } diff --git a/tests/SixLabors.Fonts.Tests/TestFonts.cs b/tests/SixLabors.Fonts.Tests/TestFonts.cs index 742cb28c..83539d86 100644 --- a/tests/SixLabors.Fonts.Tests/TestFonts.cs +++ b/tests/SixLabors.Fonts.Tests/TestFonts.cs @@ -267,6 +267,8 @@ public static class TestFonts public static string Arial => GetFullPath("arial.ttf"); + public static string Tahoma => GetFullPath("tahoma.ttf"); + public static string CousineRegular => GetFullPath("Cousine-Regular.ttf"); public static string HindRegular => GetFullPath("Hind-Regular.ttf"); diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs index e046a3c7..c5f317eb 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs @@ -22,6 +22,7 @@ public static void TestLayout( TextOptions options, float percentageTolerance = 0.05F, bool includeGeometry = false, + bool customDecorations = false, [CallerMemberName] string test = "", params object[] properties) { @@ -47,7 +48,7 @@ public static void TestLayout( // First render the text using the rich text renderer. using Image img = new(Configuration.Default, imageWidth, imageHeight, Color.White.ToPixel()); - img.Mutate(ctx => ctx.DrawText(FromTextOptions(options), text, Color.Black)); + img.Mutate(ctx => ctx.DrawText(FromTextOptions(options, customDecorations), text, Color.Black)); if (options.WrappingLength > 0) { @@ -61,8 +62,8 @@ public static void TestLayout( } } - img.DebugSave("png", test, properties: extended.ToArray()); - img.CompareToReference(percentageTolerance: percentageTolerance, test: test, properties: extended.ToArray()); + img.DebugSave("png", test, properties: [.. extended]); + img.CompareToReference(percentageTolerance: percentageTolerance, test: test, properties: [.. extended]); if (!includeGeometry) { @@ -89,13 +90,13 @@ public static void TestLayout( } } - img2.DebugSave("png", test, properties: extended.ToArray()); - img2.CompareToReference(percentageTolerance: percentageTolerance, test: test, properties: extended.ToArray()); + img2.DebugSave("png", test, properties: [.. extended]); + img2.CompareToReference(percentageTolerance: percentageTolerance, test: test, properties: [.. extended]); #endif } #if SUPPORTS_DRAWING - private static RichTextOptions FromTextOptions(TextOptions options) + private static RichTextOptions FromTextOptions(TextOptions options, bool customDecorations) { RichTextOptions result = new(options.Font) { @@ -124,17 +125,23 @@ private static RichTextOptions FromTextOptions(TextOptions options) List runs = new(options.TextRuns.Count); foreach (TextRun run in options.TextRuns) { - runs.Add(new RichTextRun() + RichTextRun richRun = new() { Font = run.Font, Start = run.Start, End = run.End, TextAttributes = run.TextAttributes, TextDecorations = run.TextDecorations, - StrikeoutPen = new SolidPen(Color.Green, 11.3334F), - UnderlinePen = new SolidPen(Color.Blue, 15.5555F), - OverlinePen = new SolidPen(Color.Purple, 13.7777F) - }); + }; + + if (customDecorations && run.TextDecorations != TextDecorations.None) + { + richRun.StrikeoutPen = new SolidPen(Color.Green, 11.3334F); + richRun.UnderlinePen = new SolidPen(Color.Blue, 15.5555F); + richRun.OverlinePen = new SolidPen(Color.Purple, 13.7777F); + } + + runs.Add(richRun); } result.TextRuns = runs;