Merge pull request #179 from mitchellh/autoitalic

Auto-italicize regular font if no italic font is found (CoreText only)
This commit is contained in:
Mitchell Hashimoto
2023-07-03 16:08:18 -07:00
committed by GitHub
5 changed files with 121 additions and 10 deletions

View File

@ -18,13 +18,18 @@ pub const Font = opaque {
) orelse Allocator.Error.OutOfMemory;
}
pub fn copyWithAttributes(self: *Font, size: f32, attrs: ?*text.FontDescriptor) Allocator.Error!*Font {
pub fn copyWithAttributes(
self: *Font,
size: f32,
matrix: ?*const graphics.AffineTransform,
attrs: ?*text.FontDescriptor,
) Allocator.Error!*Font {
return @as(
?*Font,
@ptrFromInt(@intFromPtr(c.CTFontCreateCopyWithAttributes(
@ptrCast(self),
size,
null,
@ptrCast(matrix),
@ptrCast(attrs),
))),
) orelse Allocator.Error.OutOfMemory;
@ -217,6 +222,6 @@ test "copy" {
const font = try Font.createWithFontDescriptor(desc, 12);
defer font.release();
const f2 = try font.copyWithAttributes(14, null);
const f2 = try font.copyWithAttributes(14, null, null);
defer f2.release();
}

View File

@ -231,7 +231,7 @@ pub fn init(
if (try disco_it.next()) |face| {
log.info("font regular: {s}", .{try face.name()});
try group.addFace(alloc, .regular, face);
} else std.log.warn("font-family not found: {s}", .{family});
} else log.warn("font-family not found: {s}", .{family});
}
if (config.@"font-family-bold") |family| {
var disco_it = try disco.discover(.{
@ -243,7 +243,7 @@ pub fn init(
if (try disco_it.next()) |face| {
log.info("font bold: {s}", .{try face.name()});
try group.addFace(alloc, .bold, face);
} else std.log.warn("font-family-bold not found: {s}", .{family});
} else log.warn("font-family-bold not found: {s}", .{family});
}
if (config.@"font-family-italic") |family| {
var disco_it = try disco.discover(.{
@ -255,7 +255,7 @@ pub fn init(
if (try disco_it.next()) |face| {
log.info("font italic: {s}", .{try face.name()});
try group.addFace(alloc, .italic, face);
} else std.log.warn("font-family-italic not found: {s}", .{family});
} else log.warn("font-family-italic not found: {s}", .{family});
}
if (config.@"font-family-bold-italic") |family| {
var disco_it = try disco.discover(.{
@ -268,7 +268,7 @@ pub fn init(
if (try disco_it.next()) |face| {
log.info("font bold+italic: {s}", .{try face.name()});
try group.addFace(alloc, .bold_italic, face);
} else std.log.warn("font-family-bold-italic not found: {s}", .{family});
} else log.warn("font-family-bold-italic not found: {s}", .{family});
}
}
@ -284,6 +284,26 @@ pub fn init(
font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_bold_ttf, font_size)),
);
// If we support auto-italicization and we don't have an italic face,
// then we can try to auto-italicize our regular face.
if (comptime font.DeferredFace.canItalicize()) {
if (group.getFace(.italic) == null) {
if (group.getFace(.regular)) |regular| {
if (try regular.italicize()) |face| {
log.info("font auto-italicized: {s}", .{try face.name()});
try group.addFace(alloc, .italic, face);
}
}
}
} else {
// We don't support auto-italics. If we don't have an italic font
// face let the user know so they aren't surprised (if they look
// at logs).
if (group.getFace(.italic) == null) {
log.warn("no italic font face available, italics will not render", .{});
}
}
// Emoji fallback. We don't include this on Mac since Mac is expected
// to always have the Apple Emoji available.
if (builtin.os.tag != .macos or font.Discover == void) {

View File

@ -19,6 +19,17 @@ const Presentation = @import("main.zig").Presentation;
const log = std.log.scoped(.deferred_face);
/// The struct used for deferred face state.
///
/// TODO: Change the "fc", "ct", "wc" fields in @This to just use one field
/// with the state since there should be no world in which multiple are used.
const FaceState = switch (options.backend) {
.freetype => void,
.fontconfig_freetype => Fontconfig,
.coretext_freetype, .coretext => CoreText,
.web_canvas => WebCanvas,
};
/// The loaded face (once loaded).
face: ?Face = null,
@ -61,6 +72,13 @@ pub const CoreText = struct {
self.font.release();
self.* = undefined;
}
/// Auto-italicize the font by applying a skew.
pub fn italicize(self: *const CoreText) !CoreText {
const ct_font = try self.font.copyWithAttributes(0.0, &Face.italic_skew, null);
errdefer ct_font.release();
return .{ .font = ct_font };
}
};
/// WebCanvas specific data. This is only present when building with canvas.
@ -333,6 +351,40 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool {
unreachable;
}
/// Returns true if our deferred font implementation supports auto-itacilization.
pub fn canItalicize() bool {
return @hasDecl(FaceState, "italicize") and @hasDecl(Face, "italicize");
}
/// Returns a new deferred face with the italicized version of this face
/// by applying a skew. This is NOT TRUE italics. You should use the discovery
/// mechanism to try to find an italic font. This is a fallback for when
/// that fails.
pub fn italicize(self: *const DeferredFace) !?DeferredFace {
if (comptime !canItalicize()) return null;
var result: DeferredFace = .{};
if (self.face) |face| {
result.face = try face.italicize();
}
switch (options.backend) {
.freetype => {},
.fontconfig_freetype => if (self.fc) |*fc| {
result.fc = try fc.italicize();
},
.coretext, .coretext_freetype => if (self.ct) |*ct| {
result.ct = try ct.italicize();
},
.web_canvas => if (self.wc) |*wc| {
result.wc = try wc.italicize();
},
}
return result;
}
/// The wasm-compatible API.
pub const Wasm = struct {
const wasm = @import("../os/wasm.zig");

View File

@ -99,6 +99,15 @@ pub fn addFace(self: *Group, alloc: Allocator, style: Style, face: DeferredFace)
try list.append(alloc, face);
}
/// Get the face for the given style. This will always return the first
/// face (if it exists). The returned pointer is only valid as long as
/// the faces do not change.
pub fn getFace(self: *Group, style: Style) ?*DeferredFace {
const list = self.faces.getPtr(style);
if (list.items.len == 0) return null;
return &list.items[0];
}
/// Resize the fonts to the desired size.
pub fn setSize(self: *Group, size: font.face.DesiredSize) !void {
// Note: there are some issues here with partial failure. We don't
@ -189,8 +198,15 @@ pub fn indexForCodepoint(
// If we can find the exact value, then return that.
if (self.indexForCodepointExact(cp, style, p)) |value| return value;
// Try looking for another font that will satisfy this request.
if (font.Discover != void) {
// If we're not a regular font style, try looking for a regular font
// that will satisfy this request. Blindly looking for unmatched styled
// fonts to satisfy one codepoint results in some ugly rendering.
if (style != .regular) {
if (self.indexForCodepoint(cp, .regular, p)) |value| return value;
}
// If we are regular, try looking for a fallback using discovery.
if (style == .regular and font.Discover != void) {
if (self.discover) |*disco| discover: {
var disco_it = disco.discover(.{
.codepoint = cp,

View File

@ -20,6 +20,16 @@ pub const Face = struct {
/// Metrics for this font face. These are useful for renderers.
metrics: font.face.Metrics,
/// The matrix applied to a regular font to auto-italicize it.
pub const italic_skew = macos.graphics.AffineTransform{
.a = 1,
.b = 0,
.c = 0.267949, // approx. tan(15)
.d = 1,
.tx = 0,
.ty = 0,
};
/// Initialize a CoreText-based font from a TTF/TTC in memory.
pub fn init(lib: font.Library, source: [:0]const u8, size: font.face.DesiredSize) !Face {
_ = lib;
@ -47,7 +57,7 @@ pub const Face = struct {
// Create a copy. The copyWithAttributes docs say the size is in points,
// but we need to scale the points by the DPI and to do that we use our
// function called "pixels".
const ct_font = try base.copyWithAttributes(@floatFromInt(size.pixels()), null);
const ct_font = try base.copyWithAttributes(@floatFromInt(size.pixels()), null, null);
errdefer ct_font.release();
var hb_font = try harfbuzz.coretext.createFont(ct_font);
@ -69,6 +79,14 @@ pub const Face = struct {
self.* = undefined;
}
/// 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) !Face {
const ct_font = try self.font.copyWithAttributes(0.0, &italic_skew, null);
defer ct_font.release();
return try initFontCopy(ct_font, .{ .points = 0 });
}
/// Resize the font in-place. If this succeeds, the caller is responsible
/// for clearing any glyph caches, font atlas data, etc.
pub fn setSize(self: *Face, size: font.face.DesiredSize) !void {