font: freetype supports font variation settings

This commit is contained in:
Mitchell Hashimoto
2023-08-27 19:58:25 -07:00
parent 906852976b
commit 1ee5b7f91c
6 changed files with 125 additions and 5 deletions

View File

@ -57,6 +57,11 @@ pub fn initMemoryFace(self: Library, data: []const u8, index: i32) Error!Face {
return face;
}
/// Call when you're done with a loaded MM var.
pub fn doneMMVar(self: Library, mm: *c.FT_MM_Var) void {
_ = c.FT_Done_MM_Var(self.handle, mm);
}
pub const Version = struct {
major: i32,
minor: i32,

View File

@ -24,6 +24,12 @@ pub const Face = struct {
return c.FT_HAS_COLOR(self.handle);
}
/// A macro that returns true whenever a face object contains some
/// multiple masters.
pub fn hasMultipleMasters(self: Face) bool {
return c.FT_HAS_MULTIPLE_MASTERS(self.handle);
}
/// A macro that returns true whenever a face object contains a scalable
/// font face (true for TrueType, Type 1, Type 42, CID, OpenType/CFF,
/// and PFR font formats).
@ -95,6 +101,33 @@ pub const Face = struct {
const res = c.FT_Get_Sfnt_Name(self.handle, @intCast(i), &name);
return if (intToError(res)) |_| name else |err| err;
}
/// Retrieve the font variation descriptor for a font.
pub fn getMMVar(self: Face) Error!*c.FT_MM_Var {
var result: *c.FT_MM_Var = undefined;
const res = c.FT_Get_MM_Var(self.handle, @ptrCast(&result));
return if (intToError(res)) |_| result else |err| err;
}
/// Get the design coordinates of the currently selected interpolated font.
pub fn getVarDesignCoordinates(self: Face, coords: []c.FT_Fixed) Error!void {
const res = c.FT_Get_Var_Design_Coordinates(
self.handle,
@intCast(coords.len),
coords.ptr,
);
return intToError(res);
}
/// Choose an interpolated font design through design coordinates.
pub fn setVarDesignCoordinates(self: Face, coords: []c.FT_Fixed) Error!void {
const res = c.FT_Set_Var_Design_Coordinates(
self.handle,
@intCast(coords.len),
coords.ptr,
);
return intToError(res);
}
};
/// An enumeration to specify indices of SFNT tables loaded and parsed by

View File

@ -1,5 +1,6 @@
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_TRUETYPE_TABLES_H
#include <freetype/ftmm.h>
#include <freetype/ftsnames.h>
#include <freetype/ttnameid.h>

View File

@ -54,6 +54,9 @@ pub const Fontconfig = struct {
charset: *const fontconfig.CharSet,
langset: *const fontconfig.LangSet,
/// Variations to apply to this font.
variations: []const font.face.Variation,
pub fn deinit(self: *Fontconfig) void {
self.pattern.destroy();
self.* = undefined;
@ -154,7 +157,10 @@ fn loadFontconfig(
const filename = (try fc.pattern.get(.file, 0)).string;
const face_index = (try fc.pattern.get(.index, 0)).integer;
return try Face.initFile(lib, filename, face_index, size);
var face = try Face.initFile(lib, filename, face_index, size);
errdefer face.deinit();
try face.setVariations(fc.variations);
return face;
}
fn loadCoreText(

View File

@ -205,6 +205,7 @@ pub const Fontconfig = struct {
.pattern = pat,
.set = res.fs,
.fonts = res.fs.fonts(),
.variations = desc.variations,
.i = 0,
};
}
@ -214,6 +215,7 @@ pub const Fontconfig = struct {
pattern: *fontconfig.Pattern,
set: *fontconfig.FontSet,
fonts: []*fontconfig.Pattern,
variations: []const Variation,
i: usize,
pub fn deinit(self: *DiscoverIterator) void {
@ -241,6 +243,7 @@ pub const Fontconfig = struct {
.pattern = font_pattern,
.charset = (try font_pattern.get(.charset, 0)).char_set,
.langset = (try font_pattern.get(.lang, 0)).lang_set,
.variations = self.variations,
},
};
}

View File

@ -23,6 +23,9 @@ const quirks = @import("../../quirks.zig");
const log = std.log.scoped(.font_face);
pub const Face = struct {
/// Our freetype library
lib: freetype.Library,
/// Our font face.
face: freetype.Face,
@ -43,30 +46,55 @@ pub const Face = struct {
pub fn initFile(lib: Library, path: [:0]const u8, index: i32, size: font.face.DesiredSize) !Face {
const face = try lib.lib.initFace(path, index);
errdefer face.deinit();
return try initFace(face, size);
return try initFace(lib, face, size);
}
/// Initialize a new font face with the given source in-memory.
pub fn init(lib: Library, source: [:0]const u8, size: font.face.DesiredSize) !Face {
const face = try lib.lib.initMemoryFace(source, 0);
errdefer face.deinit();
return try initFace(face, size);
return try initFace(lib, face, size);
}
fn initFace(face: freetype.Face, size: font.face.DesiredSize) !Face {
fn initFace(lib: Library, face: freetype.Face, size: font.face.DesiredSize) !Face {
try face.selectCharmap(.unicode);
try setSize_(face, size);
const hb_font = try harfbuzz.freetype.createFont(face.handle);
var hb_font = try harfbuzz.freetype.createFont(face.handle);
errdefer hb_font.destroy();
var result: Face = .{
.lib = lib.lib,
.face = face,
.hb_font = hb_font,
.presentation = if (face.hasColor()) .emoji else .text,
.metrics = calcMetrics(face),
};
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
// In debug mode, we output information about available variation axes,
// if they exist.
if (comptime builtin.mode == .Debug) mm: {
if (!face.hasMultipleMasters()) break :mm;
var buf: [1024]u8 = undefined;
log.debug("variation axes font={s}", .{try result.name(&buf)});
const mm = try face.getMMVar();
defer lib.lib.doneMMVar(mm);
for (0..mm.num_axis) |i| {
const axis = mm.axis[i];
const id_raw = std.math.cast(c_int, axis.tag) orelse continue;
const id: font.face.Variation.Id = @bitCast(id_raw);
log.debug("variation axis: name={s} id={s} min={} max={} def={}", .{
std.mem.sliceTo(axis.name, 0),
id.str(),
axis.minimum >> 16,
axis.maximum >> 16,
axis.def >> 16,
});
}
}
return result;
}
@ -132,6 +160,50 @@ pub const Face = struct {
try face.selectSize(best_i);
}
/// Set the variation axes for this font. This will modify this font
/// in-place.
pub fn setVariations(
self: *Face,
vs: []const font.face.Variation,
) !void {
// If this font doesn't support variations, we can't do anything.
if (!self.face.hasMultipleMasters() or vs.len == 0) return;
// Freetype requires that we send ALL coordinates in at once so the
// first thing we have to do is get all the vars and put them into
// an array.
const mm = try self.face.getMMVar();
defer self.lib.doneMMVar(mm);
// To avoid allocations, we cap the number of variation axes we can
// support. This is arbitrary but Firefox caps this at 16 so I
// feel like that's probably safe... and we do double cause its
// cheap.
var coords_buf: [32]freetype.c.FT_Fixed = undefined;
var coords = coords_buf[0..@min(coords_buf.len, mm.num_axis)];
try self.face.getVarDesignCoordinates(coords);
// Now we go through each axis and see if its set. This is slow
// but there usually aren't many axes and usually not many set
// variations, either.
for (0..mm.num_axis) |i| {
const axis = mm.axis[i];
const id = std.math.cast(u32, axis.tag) orelse continue;
for (vs) |v| {
if (id == @as(u32, @bitCast(v.id))) {
coords[i] = @intFromFloat(v.value * 65536);
break;
}
}
}
// Set them!
try self.face.setVarDesignCoordinates(coords);
// We need to recalculate font metrics which may have changed.
self.metrics = calcMetrics(self.face);
}
/// Returns the glyph index for the given Unicode code point. If this
/// face doesn't support this glyph, null is returned.
pub fn glyphIndex(self: Face, cp: u32) ?u32 {