All checks were successful
Generic zig build / build (push) Successful in 1m6s
299 lines
12 KiB
Zig
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();
|
|
}
|