Skip to content

Bold simulation adds 0.02 em character spacing per character; please make it configurable #412

Open

Description

@JarKlo

Expected Behavior

When a font has no bold face and the resolver returns mustSimulateBold: true, I'd like to be able to draw simulated bold without extra character spacing, so bold text keeps the regular advance widths. Microsoft Word draws synthetic bold that way: fill plus stroke (text render mode 2), a stroke of about 1/35 em, and no extra spacing. Documents produced with PDFsharp then line up with documents produced by Word.

Actual Behavior

Simulated bold is controlled by one hard-coded constant, Const.BoldEmphasis = 0.02 (Configuration.cs#L44). It is used for three things:

At 12 pt every bold character is 0.24 pt wider than its regular advance, and the difference accumulates along the line. In our reports, which must match Word-generated PDFs, bold titles drifted 13–18 pt.

Scaling the bold font size down only fixes the average width. The spacing is constant per character, so narrow and wide glyphs stay wrong in opposite directions: all-caps, CJK and emoji text still drifted 2.5–5 pt.

The constant is internal and compile-time, so there's no way to change it from outside. 7.0.0-preview-1 has the same code.

Steps to Reproduce the Behavior

PDFsharp 6.2.4 (NuGet PDFsharp), .NET 8, any font without a bold face (Microsoft Sans Serif, micross.ttf, is one):

using PdfSharp.Drawing;
using PdfSharp.Fonts;
using PdfSharp.Pdf;

GlobalFontSettings.FontResolver = new SingleFaceResolver();
var regular = new XFont("Microsoft Sans Serif", 12);
var bold = new XFont("Microsoft Sans Serif", 12, XFontStyleEx.Bold);

using var doc = new PdfDocument();
doc.Options.CompressContentStreams = false;
using (var gfx = XGraphics.FromPdfPage(doc.AddPage()))
{
    Console.WriteLine($"regular: {gfx.MeasureString("Listing Schedule", regular).Width:F2}");
    Console.WriteLine($"bold:    {gfx.MeasureString("Listing Schedule", bold).Width:F2}");
    gfx.DrawString("Listing Schedule", regular, XBrushes.Black, 50, 100);
    gfx.DrawString("Listing Schedule", bold, XBrushes.Black, 50, 130);
}
doc.Save("bold-simulation.pdf");

class SingleFaceResolver : IFontResolver
{
    public FontResolverInfo ResolveTypeface(string familyName, bool bold, bool italic) =>
        new FontResolverInfo("micross", bold, italic);
    public byte[] GetFont(string faceName) =>
        File.ReadAllBytes(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), "micross.ttf"));
}

Output:

regular: 88.12
bold:    91.97

The bold string measures 3.85 pt wider: 16 characters × 0.24 pt = 3.84 pt, plus rounding.

The content stream sets the bold state just before the second string:

(Listing Schedule) Tj
0.24 w
0 J
0 j
[]0 d
/GS1 gs
2 Tr
0.24 Tc
0 -30 Td
(Listing Schedule) Tj

In the rendered page the bold "Schedule" starts 1.92 pt later than the regular one (x 87.99 → 89.91, i.e. 8 characters × 0.24 pt). The bold string's last glyph ends 3.60 pt further right (x 138.08 → 141.68, i.e. 15 × 0.24 pt).

Suggestion

Expose the two factors as public settings, with today's values as defaults so nothing changes for existing users. For example:

GlobalFontSettings.BoldSimulationStrokeWidth = 1.0 / 35;  // em fraction, default 0.02
GlobalFontSettings.BoldSimulationCharacterSpacing = 0;    // em fraction, default 0.02

A per-face option (on FontResolverInfo or XPdfFontOptions) would work too, if you prefer that.

We tested this locally by patching 3 files: two new constants used at the four places above (+18/−6 lines). With stroke = 1/35 em and spacing = 0, PDFsharp's simulated bold matched Word's output exactly in our tests: the same glyph positions on every page, all-caps, CJK and emoji included. Happy to open a PR if you're open to the change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions