font/freetype: disable SVG glyphs, simplify color check

We don't currently support rendering SVG glyphs so they should be
ignored when loading. Additionally, the check for whether a glyph is
colored has been simplified by just checking the pixel mode of the
rendered bitmap.

This commit also fixes a bug caused by calling the color check inside of
`renderGlyph`, which caused the bitmap to be freed creating a chance for
memory corruption and garbled glyphs.
This commit is contained in:
Qwerasd
2025-03-19 12:39:34 -06:00
parent bd315c8394
commit 6f84a5d682
2 changed files with 34 additions and 21 deletions

View File

@ -278,7 +278,9 @@ pub const LoadFlags = packed struct {
color: bool = false, color: bool = false,
compute_metrics: bool = false, compute_metrics: bool = false,
bitmap_metrics_only: bool = false, bitmap_metrics_only: bool = false,
_padding2: u9 = 0, _padding2: u1 = 0,
no_svg: bool = false,
_padding3: u7 = 0,
test { test {
// This must always be an i32 size so we can bitcast directly. // This must always be an i32 size so we can bitcast directly.

View File

@ -270,25 +270,21 @@ pub const Face = struct {
/// Returns true if the given glyph ID is colorized. /// Returns true if the given glyph ID is colorized.
pub fn isColorGlyph(self: *const Face, glyph_id: u32) bool { pub fn isColorGlyph(self: *const Face, glyph_id: u32) bool {
// sbix table is always true for now // Load the glyph and see what pixel mode it renders with.
if (self.face.hasSBIX()) return true; // All modes other than BGRA are non-color.
// If the glyph fails to load, just return false.
// CBDT/CBLC tables always imply colorized glyphs.
// These are used by Noto.
if (self.face.hasSfntTable(freetype.Tag.init("CBDT"))) return true;
if (self.face.hasSfntTable(freetype.Tag.init("CBLC"))) return true;
// Otherwise, load the glyph and see what format it is in.
self.face.loadGlyph(glyph_id, .{ self.face.loadGlyph(glyph_id, .{
.render = true, .render = true,
.color = self.face.hasColor(), .color = self.face.hasColor(),
// NO_SVG set to true because we don't currently support rendering
// SVG glyphs under FreeType, since that requires bundling another
// dependency to handle rendering the SVG.
.no_svg = true,
}) catch return false; }) catch return false;
// If the glyph is SVG we assume colorized
const glyph = self.face.handle.*.glyph; const glyph = self.face.handle.*.glyph;
if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_SVG) return true;
return false; return glyph.*.bitmap.pixel_mode == freetype.c.FT_PIXEL_MODE_BGRA;
} }
/// Render a glyph using the glyph index. The rendered glyph is stored in the /// Render a glyph using the glyph index. The rendered glyph is stored in the
@ -321,6 +317,11 @@ pub const Face = struct {
.force_autohint = !self.load_flags.@"force-autohint", .force_autohint = !self.load_flags.@"force-autohint",
.monochrome = !self.load_flags.monochrome, .monochrome = !self.load_flags.monochrome,
.no_autohint = !self.load_flags.autohint, .no_autohint = !self.load_flags.autohint,
// NO_SVG set to true because we don't currently support rendering
// SVG glyphs under FreeType, since that requires bundling another
// dependency to handle rendering the SVG.
.no_svg = true,
}); });
const glyph = self.face.handle.*.glyph; const glyph = self.face.handle.*.glyph;
@ -393,12 +394,13 @@ pub const Face = struct {
const original_width = bitmap_original.width; const original_width = bitmap_original.width;
const original_height = bitmap_original.rows; const original_height = bitmap_original.rows;
var result = bitmap_original; var result = bitmap_original;
// TODO: We are limiting this to only emoji. We can rework this after a future // TODO: We are limiting this to only color glyphs, so mainly emoji.
// improvement (promised by Qwerasd) which implements more flexible resizing rules. For // We can rework this after a future improvement (promised by Qwerasd)
// now, this will suffice // which implements more flexible resizing rules.
if (self.isColorGlyph(glyph_index) and opts.cell_width != null) { if (atlas.format != .grayscale and opts.cell_width != null) {
const cell_width = opts.cell_width orelse unreachable; const cell_width = opts.cell_width orelse unreachable;
// If we have a cell_width, we constrain the glyph to fit within the cell // If we have a cell_width, we constrain
// the glyph to fit within the cell(s).
result.width = metrics.cell_width * @as(u32, cell_width); result.width = metrics.cell_width * @as(u32, cell_width);
result.rows = (result.width * original_height) / original_width; result.rows = (result.width * original_height) / original_width;
} else { } else {
@ -743,7 +745,10 @@ pub const Face = struct {
var c: u8 = ' '; var c: u8 = ' ';
while (c < 127) : (c += 1) { while (c < 127) : (c += 1) {
if (face.getCharIndex(c)) |glyph_index| { if (face.getCharIndex(c)) |glyph_index| {
if (face.loadGlyph(glyph_index, .{ .render = true })) { if (face.loadGlyph(glyph_index, .{
.render = true,
.no_svg = true,
})) {
max = @max( max = @max(
f26dot6ToF64(face.handle.*.glyph.*.advance.x), f26dot6ToF64(face.handle.*.glyph.*.advance.x),
max, max,
@ -776,7 +781,10 @@ pub const Face = struct {
break :heights .{ break :heights .{
cap: { cap: {
if (face.getCharIndex('H')) |glyph_index| { if (face.getCharIndex('H')) |glyph_index| {
if (face.loadGlyph(glyph_index, .{ .render = true })) { if (face.loadGlyph(glyph_index, .{
.render = true,
.no_svg = true,
})) {
break :cap f26dot6ToF64(face.handle.*.glyph.*.metrics.height); break :cap f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
} else |_| {} } else |_| {}
} }
@ -784,7 +792,10 @@ pub const Face = struct {
}, },
ex: { ex: {
if (face.getCharIndex('x')) |glyph_index| { if (face.getCharIndex('x')) |glyph_index| {
if (face.loadGlyph(glyph_index, .{ .render = true })) { if (face.loadGlyph(glyph_index, .{
.render = true,
.no_svg = true,
})) {
break :ex f26dot6ToF64(face.handle.*.glyph.*.metrics.height); break :ex f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
} else |_| {} } else |_| {}
} }