fontfinder/src/fontconfig.zig
Emil Lerch 31fe824a8b
All checks were successful
Generic zig build / build (push) Successful in 1m6s
address most memory leaks
2024-05-05 09:16:19 -07:00

299 lines
12 KiB
Zig

const std = @import("std");
const unicode = @import("unicode.zig");
const c = @cImport({
@cInclude("fontconfig/fontconfig.h");
});
const log = std.log.scoped(.fontconfig);
extern fn allCharacters(p: ?*const c.FcPattern, chars: *[*]u32) c_int;
extern fn freeAllCharacters(chars: *[*]usize) void;
pub const RangeFont = struct {
starting_codepoint: u21,
ending_codepoint: u21,
font: Font,
};
pub const Font = struct {
full_name: []const u8,
family: []const u8,
style: []const u8,
supported_chars: []const u21,
const Self = @This();
pub fn deinit(self: *Self) void {
freeAllCharacters(@ptrCast(@alignCast(@constCast(self.supported_chars.ptr))));
}
};
pub const FontList = struct {
list: std.ArrayList(Font),
allocator: std.mem.Allocator,
pattern: *c.FcPattern,
fontset: *c.FcFontSet,
const Self = @This();
pub fn initCapacity(allocator: std.mem.Allocator, num: usize, pattern: *c.FcPattern, fontset: *c.FcFontSet) std.mem.Allocator.Error!Self {
const al = try std.ArrayList(Font).initCapacity(allocator, num);
return Self{
.allocator = allocator,
.list = al,
.pattern = pattern,
.fontset = fontset,
};
}
pub fn deinit(self: *Self) void {
c.FcPatternDestroy(self.pattern);
c.FcFontSetDestroy(self.fontset);
for (self.list.items) |*f| f.deinit();
self.list.deinit();
}
pub fn addFontAssumeCapacity(
self: *Self,
full_name: []const u8,
family: []const u8,
style: []const u8,
supported_chars: []const u21,
) !void {
self.list.appendAssumeCapacity(.{
.full_name = full_name,
.family = family,
.style = style,
.supported_chars = supported_chars,
});
}
};
var fc_config: ?*c.FcConfig = null;
var deinited = false;
// pub var test_should_deinit = true;
/// De-initializes the underlying c library. Should only be called
/// after all processing has completed
pub fn deinit() void {
// https://refspecs.linuxfoundation.org/fontconfig-2.6.0/r2370.html
// Says that "Note that calling this function with the return from FcConfigGetCurrent will place the library in an indeterminate state."
// However, it seems as though you can't do this either:
//
// 1. c.FcInitLoadConfigAndFonts();
// 2. c.FcConfigDestroy();
// 3. c.FcInitLoadConfigAndFonts();
// 4. c.FcConfigDestroy(); // Seg fault here
if (deinited) @panic("Cannot deinitialize this library more than once");
deinited = true;
if (fc_config) |conf| {
log.debug("destroying config ({*}): do not use library or call me again", .{conf});
// TODO: Well, our pointers line up, but we still end up with a seg fault,
// with valgrind reporting the following stack trace:
// Invalid read of size 4
// at 0x4868565: FcPatternDestroy (in /usr/lib/x86_64-linux-gnu/libfontconfig.so.1.12.0)
// by 0x4860A48: FcFontSetDestroy (in /usr/lib/x86_64-linux-gnu/libfontconfig.so.1.12.0)
// by 0x485386E: FcConfigDestroy (in /usr/lib/x86_64-linux-gnu/libfontconfig.so.1.12.0)
// by 0x147FEB8: fontconfig.deinit
//
// While that's here, all our deinits look good, so...?
// c.FcConfigDestroy(conf);
}
// c.FcFini();
fc_config = null;
}
pub const FontQuery = struct {
allocator: std.mem.Allocator,
// fc_config: ?*c.FcConfig = null,
const Self = @This();
pub fn init(allocator: std.mem.Allocator) Self {
return Self{
.allocator = allocator,
};
}
pub fn deinit(self: *Self) void {
_ = self;
// if (self.all_fonts) |a| a.deinit();
}
pub fn fontList(self: *Self, pattern: [:0]const u8) !FontList {
if (fc_config == null and deinited) @panic("fontconfig C library is in an inconsistent state - should not use");
if (fc_config == null) {
fc_config = c.FcInitLoadConfigAndFonts();
log.debug("config loaded ({*})", .{fc_config.?});
}
const config = if (fc_config) |conf| conf else return error.FontConfigInitLoadFailure;
// Pretty sure we want this...
const pat = c.FcNameParse(pattern);
// We cannot destroy the pattern until we're completely done
// This will be managed by FontList object
// defer if (pat != null) c.FcPatternDestroy(pat);
// const pat = c.FcPatternCreate(); // *FcPattern
// defer if (pat != null) c.FcPatternDestroy(pat);
//
// // FC_WEIGHT_NORMAL is 80
// // This is equivalent to "regular" style
// if (c.FcPatternAddInteger(pat, c.FC_WEIGHT, c.FC_WEIGHT_NORMAL) != c.FcTrue) return error.FontConfigCouldNotSetPattern;
//
// // This is "normal" vs Bold or Italic
// if (c.FcPatternAddInteger(pat, c.FC_WIDTH, c.FC_WIDTH_NORMAL) != c.FcTrue) return error.FontConfigCouldNotSetPattern;
//
// // Monospaced fonts
// if (c.FcPatternAddInteger(pat, c.FC_SPACING, c.FC_MONO) != c.FcTrue) return error.FontConfigCouldNotSetPattern;
//
// // FC_SLANT_ROMAN is 0 (italic 100, oblique 110)
// if (c.FcPatternAddInteger(pat, c.FC_SLANT, c.FC_SLANT_ROMAN) != c.FcTrue) return error.FontConfigCouldNotSetPattern;
//
const os = c.FcObjectSetBuild(c.FC_FAMILY, c.FC_STYLE, c.FC_LANG, c.FC_FULLNAME, c.FC_CHARSET, @as(?*u8, null)); // *FcObjectSet
defer if (os != null) c.FcObjectSetDestroy(os);
const fs = c.FcFontList(config, pat, os); // FcFontSet
// TODO: Move this defer into deinit
// defer if (fs != null) c.FcFontSetDestroy(fs);
// Use the following only when needed. NameUnparse allocates memory
// log.debug("Total matching fonts: {d}. Pattern: {s}\n", .{ fs.*.nfont, c.FcNameUnparse(pat) });
log.debug("Total matching fonts: {d}", .{fs.*.nfont});
var rc = try FontList.initCapacity(self.allocator, @as(usize, @intCast(fs.*.nfont)), pat.?, fs.?);
errdefer rc.deinit();
for (0..@as(usize, @intCast(fs.*.nfont))) |i| {
const font = fs.*.fonts[i].?; // *FcPattern
var fullname: [*:0]c.FcChar8 = undefined;
var style: [*:0]c.FcChar8 = undefined;
var family: [*:0]c.FcChar8 = undefined;
var charset: [*]u21 = undefined;
const len = allCharacters(font, @ptrCast(&charset));
if (len < 0) return error.FontConfigCouldNotGetCharSet;
// https://refspecs.linuxfoundation.org/fontconfig-2.6.0/r600.html
// Note that these (like FcPatternGet) do not make a copy of any data structure referenced by the return value
// https://refspecs.linuxfoundation.org/fontconfig-2.6.0/r570.html
// The value returned is not a copy, but rather refers to the data stored within the pattern directly. Applications must not free this value.
if (c.FcPatternGetString(font, c.FC_FULLNAME, 0, @as([*c][*c]c.FcChar8, @ptrCast(&fullname))) != c.FcResultMatch)
fullname = @constCast(@ptrCast("".ptr));
// return error.FontConfigCouldNotGetFontFullName;
if (c.FcPatternGetString(font, c.FC_FAMILY, 0, @as([*c][*c]c.FcChar8, @ptrCast(&family))) != c.FcResultMatch)
return error.FontConfigHasNoFamily;
if (c.FcPatternGetString(font, c.FC_STYLE, 0, @as([*c][*c]c.FcChar8, @ptrCast(&style))) != c.FcResultMatch)
return error.FontConfigHasNoStyle;
log.debug(
"Chars: {d:5.0} Family '{s}' Style '{s}' Full Name: {s}",
.{ @as(usize, @intCast(len)), family, style, fullname },
);
try rc.addFontAssumeCapacity(
fullname[0..std.mem.len(fullname)],
family[0..std.mem.len(family)],
style[0..std.mem.len(style)],
charset[0..@as(usize, @intCast(len))],
);
}
return rc;
}
pub fn fontsForRange(
self: *Self,
starting_codepoint: u21,
ending_codepoint: u21,
fonts: []const Font,
exclude_previous: bool,
) ![]RangeFont {
// const group_len = group.ending_codepoint - group.starting_codepoint;
var rc = std.ArrayList(RangeFont).init(self.allocator);
defer rc.deinit();
const previously_supported = blk: {
if (!exclude_previous) break :blk null;
var al = try std.ArrayList(bool).initCapacity(self.allocator, ending_codepoint - starting_codepoint);
defer al.deinit();
for (starting_codepoint..ending_codepoint) |_|
al.appendAssumeCapacity(false);
break :blk try al.toOwnedSlice();
};
defer if (previously_supported) |p| self.allocator.free(p);
for (fonts) |font| {
var current_start = @as(u21, 0);
var current_end = @as(u21, 0);
var inx = @as(usize, 0);
var range_count = @as(usize, 0);
// Advance to the start of the range
while (inx < font.supported_chars.len and
font.supported_chars[inx] < starting_codepoint)
inx += 1;
while (inx < font.supported_chars.len and
font.supported_chars[inx] < ending_codepoint)
{
if (previously_supported) |p| {
if (p[font.supported_chars[inx]]) {
inx += 1;
continue; // This was already supported - continue
}
}
// We found the beginning of a range
current_start = font.supported_chars[inx];
current_end = font.supported_chars[inx];
if (previously_supported) |p|
p[font.supported_chars[inx]] = true;
// Advance to the next supported character, then start checking for continuous ranges
inx += 1;
while (inx < font.supported_chars.len and
font.supported_chars[inx] == current_end + 1 and
font.supported_chars[inx] <= ending_codepoint and
(!exclude_previous or !previously_supported.?[font.supported_chars[inx]]))
{
if (previously_supported) |p|
p[font.supported_chars[inx]] = true;
inx += 1;
current_end += 1;
}
// We've found the end of the range (which could be the end of a group)
// If we have not hit the stops, inx at this point is at the beginning of
// a new range
range_count += 1;
try rc.append(.{
.font = font,
.starting_codepoint = current_start,
.ending_codepoint = current_end,
});
}
}
return rc.toOwnedSlice();
}
};
test {
std.testing.refAllDecls(@This()); // Only catches public decls
}
test "Get fonts" {
// std.testing.log_level = .debug;
log.debug("get fonts", .{});
var fq = FontQuery.init(std.testing.allocator);
defer fq.deinit();
var fl = try fq.fontList(":regular:normal:spacing=100:slant=0");
defer fl.deinit();
try std.testing.expect(fl.list.items.len > 0);
const matched = blk: {
for (fl.list.items) |item| {
log.debug("full_name: '{s}'", .{item.full_name});
if (std.mem.eql(u8, "DejaVu Sans Mono", item.full_name))
break :blk item;
}
break :blk null;
};
try std.testing.expect(matched != null);
try std.testing.expectEqual(@as(usize, 3322), matched.?.supported_chars.len);
}
test {
// if (test_should_deinit) deinit();
deinit();
}