Repository navigation
Description
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:
- the stroke width, 0.02 em (PdfGraphicsState.cs#L223);
- character spacing (
Tc) of 0.02 em on every character (PdfGraphicsState.cs#L333); - the same extra width in
MeasureString(FontHelper.cs#L134, #L178).
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.02A 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.
Activity
Let a font resolver choose how a face simulates bold (#294)