Merge pull request #2144 from ghostty-org/synth-bold

CoreText: Synthetic Bold
This commit is contained in:
Mitchell Hashimoto
2024-08-24 19:49:37 -07:00
committed by GitHub
3 changed files with 126 additions and 38 deletions

View File

@ -62,7 +62,9 @@ const c = @cImport({
/// Finally, some styles may be synthesized if they are not supported.
/// For example, if a font does not have an italic style and no alternative
/// italic font is specified, Ghostty will synthesize an italic style by
/// applying a slant to the regular style.
/// applying a slant to the regular style. If you want to disable these
/// synthesized styles then you can use the `font-style` configurations
/// as documented below.
///
/// You can disable styles completely by using the `font-style` set of
/// configurations. See the documentation for `font-style` for more information.

View File

@ -260,7 +260,8 @@ pub fn completeStyles(self: *Collection, alloc: Allocator) CompleteError!void {
// If we can't create a synthetic italic face, we'll just use the regular
// face for italic.
const italic_list = self.faces.getPtr(.italic);
if (italic_list.count() == 0) italic: {
const have_italic = italic_list.count() > 0;
if (!have_italic) italic: {
const synthetic = self.syntheticItalic(regular_entry) catch |err| {
log.warn("failed to create synthetic italic, italic style will not be available err={}", .{err});
try italic_list.append(alloc, .{ .alias = regular_entry });
@ -273,52 +274,95 @@ pub fn completeStyles(self: *Collection, alloc: Allocator) CompleteError!void {
// If we don't have bold, use the regular font.
const bold_list = self.faces.getPtr(.bold);
if (bold_list.count() == 0) {
log.warn("bold style not available, using regular font", .{});
try bold_list.append(alloc, .{ .alias = regular_entry });
const have_bold = bold_list.count() > 0;
if (!have_bold) bold: {
const synthetic = self.syntheticBold(regular_entry) catch |err| {
log.warn("failed to create synthetic bold, bold style will not be available err={}", .{err});
try bold_list.append(alloc, .{ .alias = regular_entry });
break :bold;
};
log.info("synthetic bold face created", .{});
try bold_list.append(alloc, .{ .loaded = synthetic });
}
// If we don't have bold italic, use the regular italic font.
// If we don't have bold italic, we attempt to synthesize a bold variant
// of the italic font. If we can't do that, we'll use the italic font.
const bold_italic_list = self.faces.getPtr(.bold_italic);
if (bold_italic_list.count() == 0) {
log.warn("bold italic style not available, using italic font", .{});
if (bold_italic_list.count() == 0) bold_italic: {
// Prefer to synthesize on top of the face we already had. If we
// have bold then we try to synthesize italic on top of bold.
if (have_bold) {
if (self.syntheticItalic(bold_list.at(0))) |synthetic| {
log.info("synthetic bold italic face created from bold", .{});
try bold_italic_list.append(alloc, .{ .loaded = synthetic });
break :bold_italic;
} else |_| {}
// Nested alias isn't allowed so if the italic entry is an
// alias then we use the aliased entry.
const italic_entry = italic_list.at(0);
switch (italic_entry.*) {
.alias => |v| try bold_italic_list.append(
alloc,
.{ .alias = v },
),
.loaded,
.fallback_loaded,
.deferred,
.fallback_deferred,
=> try bold_italic_list.append(
alloc,
.{ .alias = italic_entry },
),
// If synthesizing italic failed, then we try to synthesize
// bold on whatever italic font we have.
}
// Nested alias isn't allowed so we need to unwrap the italic entry.
const base_entry = base: {
const italic_entry = italic_list.at(0);
break :base switch (italic_entry.*) {
.alias => |v| v,
.loaded,
.fallback_loaded,
.deferred,
.fallback_deferred,
=> italic_entry,
};
};
if (self.syntheticBold(base_entry)) |synthetic| {
log.info("synthetic bold italic face created from italic", .{});
try bold_italic_list.append(alloc, .{ .loaded = synthetic });
break :bold_italic;
} else |_| {}
log.warn("bold italic style not available, using italic font", .{});
try bold_italic_list.append(alloc, .{ .alias = base_entry });
}
}
// Create an synthetic italic font face from the given entry and return it.
fn syntheticItalic(self: *Collection, entry: *Entry) !Face {
// Not all font backends support auto-italicization.
if (comptime !@hasDecl(Face, "italicize")) return error.SyntheticItalicUnavailable;
// Create a synthetic bold font face from the given entry and return it.
fn syntheticBold(self: *Collection, entry: *Entry) !Face {
// Not all font backends support synthetic bold.
if (comptime !@hasDecl(Face, "syntheticBold")) return error.SyntheticBoldUnavailable;
// We require loading options to auto-italicize.
// We require loading options to create a synthetic bold face.
const opts = self.load_options orelse return error.DeferredLoadingUnavailable;
// Try to bold it.
const regular = try self.getFaceFromEntry(entry);
const face = try regular.syntheticBold(opts.faceOptions());
var buf: [256]u8 = undefined;
if (face.name(&buf)) |name| {
log.info("font synthetic bold created family={s}", .{name});
} else |_| {}
return face;
}
// Create a synthetic italic font face from the given entry and return it.
fn syntheticItalic(self: *Collection, entry: *Entry) !Face {
// Not all font backends support synthetic italicization.
if (comptime !@hasDecl(Face, "syntheticItalic")) return error.SyntheticItalicUnavailable;
// We require loading options to create a synthetic italic face.
const opts = self.load_options orelse return error.DeferredLoadingUnavailable;
// Try to italicize it.
const regular = try self.getFaceFromEntry(entry);
const face = try regular.italicize(opts.faceOptions());
const face = try regular.syntheticItalic(opts.faceOptions());
var buf: [256]u8 = undefined;
if (face.name(&buf)) |name| {
log.info("font auto-italicized: {s}", .{name});
log.info("font synthetic italic created family={s}", .{name});
} else |_| {}
return face;

View File

@ -24,6 +24,10 @@ pub const Face = struct {
/// Set quirks.disableDefaultFontFeatures
quirks_disable_default_font_features: bool = false,
/// True if this font face should be rasterized with a synthetic bold
/// effect. This is used for fonts that don't have a bold variant.
synthetic_bold: ?f64 = null,
/// If the face can possibly be colored, then this is the state
/// used to check for color information. This is null if the font
/// can't possibly be colored (i.e. doesn't have SVG, sbix, etc
@ -169,12 +173,31 @@ pub const Face = struct {
/// Return a new face that is the same as this but has a transformation
/// matrix applied to italicize it.
pub fn italicize(self: *const Face, opts: font.face.Options) !Face {
pub fn syntheticItalic(self: *const Face, opts: font.face.Options) !Face {
const ct_font = try self.font.copyWithAttributes(0.0, &italic_skew, null);
errdefer ct_font.release();
return try initFont(ct_font, opts);
}
/// Return a new face that is the same as this but applies a synthetic
/// bold effect to it. This is useful for fonts that don't have a bold
/// variant.
pub fn syntheticBold(self: *const Face, opts: font.face.Options) !Face {
const ct_font = try self.font.copyWithAttributes(0.0, null, null);
errdefer ct_font.release();
var face = try initFont(ct_font, opts);
// To determine our synthetic bold line width we get a multiplier
// from the font size in points. This is a heuristic that is based
// on the fact that a line width of 1 looks good to me at 12 points
// and we want to scale that up roughly linearly with the font size.
const points_f64: f64 = @floatCast(opts.size.points);
const line_width = @max(points_f64 / 12.0, 1);
face.synthetic_bold = line_width;
return face;
}
/// Returns the font name. If allocation is required, buf will be used,
/// but sometimes allocation isn't required and a static string is
/// returned.
@ -300,11 +323,23 @@ pub const Face = struct {
.advance_x = 0,
};
// If we're doing thicken, then getBoundsForGlyphs does not take
// into account the anti-aliasing that will be added to the glyph.
// We need to add some padding to allow that to happen. A padding of
// 2 is usually enough for anti-aliasing.
const padding_ctx: u32 = if (opts.thicken) 2 else 0;
// Additional padding we need to add to the bitmap context itself
// due to the glyph being larger than standard.
const padding_ctx: u32 = padding_ctx: {
// If we're doing thicken, then getBoundsForGlyphs does not take
// into account the anti-aliasing that will be added to the glyph.
// We need to add some padding to allow that to happen. A padding of
// 2 is usually enough for anti-aliasing.
var result: u32 = if (opts.thicken) 2 else 0;
// If we have a synthetic bold, add padding for the stroke width
if (self.synthetic_bold) |line_width| {
// x2 for top and bottom padding
result += @intFromFloat(@ceil(line_width) * 2);
}
break :padding_ctx result;
};
const padded_width: u32 = width + (padding_ctx * 2);
const padded_height: u32 = height + (padding_ctx * 2);
@ -390,6 +425,13 @@ pub const Face = struct {
context.setGrayStrokeColor(ctx, 1, 1);
}
// If we are drawing with synthetic bold then use a fill stroke
// which strokes the outlines of the glyph making a more bold look.
if (self.synthetic_bold) |line_width| {
context.setTextDrawingMode(ctx, .fill_stroke);
context.setLineWidth(ctx, line_width);
}
// We want to render the glyphs at (0,0), but the glyphs themselves
// are offset by bearings, so we have to undo those bearings in order
// to get them to 0,0. We also add the padding so that they render