代码
#include <cassert>
#include <algorithm>
#include "fontconfig/fontconfig.h"
#include <stdexcept>
#include <iostream>
#define HAS_FALLBACK_CONFIGURATION
enum FontStyle : uint16_t {
Regular = 0,
Italic = 0x01,
Bold = 0x02,
};
enum class FontConfigSearchFlags : uint16_t {
None = 0,
MatchPostScriptName =
0x01, ///< Match postscript font name. The default is match family name. This search may be more specific
};
struct FontConfigSearchParams {
uint16_t Style;
uint16_t Flags = 0;
};
FcConfig* m_FcConfig;
void createDefaultConfig()
{
#ifdef _WIN32
const char* fontconf =
R"(<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<dir>WINDOWSFONTDIR</dir>
<dir>WINDOWSUSERFONTDIR</dir>
<dir prefix="xdg">fonts</dir>
<cachedir>LOCAL_APPDATA_FONTCONFIG_CACHE</cachedir>
<cachedir prefix="xdg">fontconfig</cachedir>
</fontconfig>
)";
#elif __ANDROID__
// On android fonts are located in /system/fonts
const char* fontconf =
R"(<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<dir>/system/fonts</dir>
<dir prefix="xdg">fonts</dir>
<cachedir prefix="xdg">fontconfig</cachedir>
</fontconfig>
)";
#elif __LINUX__
const char* fontconf =
R"(<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<dir>/usr/share/fonts</dir>
<dir>/usr/local/share/fonts</dir>
<dir>/usr/X11R6/lib/X11/fonts</dir>
<dir>/usr/X11/lib/X11/fonts</dir>
<dir>/usr/lib/X11/fonts</dir>
<dir prefix="xdg">fonts</dir>
<cachedir prefix="xdg">fontconfig</cachedir>
</fontconfig>
)";
#elif __APPLE__
// Fonts location https://stackoverflow.com/a/2557291/213871
const char* fontconf =
R"(<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<dir>/System/Library/Fonts</dir>
<dir>/Library/Fonts</dir>
<dir>~/Library/Fonts</dir>
<dir>/System/Library/Assets/com_apple_MobileAsset_Font3</dir>
<dir>/System/Library/Assets/com_apple_MobileAsset_Font4"</dir>
<dir prefix="xdg">fonts</dir>
<cachedir prefix="xdg">fontconfig</cachedir>
</fontconfig>
)";
#endif
#ifdef HAS_FALLBACK_CONFIGURATION
// Implement the fallback as discussed in fontconfig mailing list
// https://lists.freedesktop.org/archives/fontconfig/2022-February/006883.html
auto config = FcConfigCreate();
if (config == nullptr)
throw std::runtime_error("Could not allocate font config");
// Manually try to load the config to determine
// if a system configuration exists. Tell FontConfig
// to not complain if it doesn't
(void)FcConfigParseAndLoad(config, nullptr, FcFalse);
auto configFiles = FcConfigGetConfigFiles(config);
if (FcStrListNext(configFiles) == nullptr) {
// No system config found, supply a fallback configuration
if (!FcConfigParseAndLoadFromMemory(config, (const FcChar8*)fontconf, true)) {
FcConfigDestroy(config);
throw std::runtime_error("Could not parse font config");
}
// Load fonts for the config
if (!FcConfigBuildFonts(config)) {
FcConfigDestroy(config);
throw std::runtime_error("Could not load fonts in fontconfig");
}
m_FcConfig = config;
}
else {
// Destroy the temporary config
FcStrListDone(configFiles);
FcConfigDestroy(config);
#endif
// Default initialize a local FontConfig configuration
// http://mces.blogspot.com/2015/05/how-to-use-custom-application-fonts.html
m_FcConfig = FcInitLoadConfigAndFonts();
assert(m_FcConfig != nullptr);
#ifdef HAS_FALLBACK_CONFIGURATION
}
#endif
}
bool getFontInfo(FcPattern* font, std::string& fontFamily, std::string& fontPath, uint16_t& style)
{
FcChar8* family = nullptr;
FcChar8* path = nullptr;
int slant;
int weight;
style = FontStyle::Regular;
if (FcPatternGetString(font, FC_FAMILY, 0, &family) == FcResultMatch) {
fontFamily = reinterpret_cast<char*>(family);
if (FcPatternGetString(font, FC_FILE, 0, &path) == FcResultMatch) {
fontPath = reinterpret_cast<char*>(path);
if (FcPatternGetInteger(font, FC_SLANT, 0, &slant) == FcResultMatch) {
if (slant == FC_SLANT_ITALIC || slant == FC_SLANT_OBLIQUE)
style |= FontStyle::Italic;
if (FcPatternGetInteger(font, FC_WEIGHT, 0, &weight) == FcResultMatch) {
if (weight >= FC_WEIGHT_BOLD)
style |= FontStyle::Bold;
return true;
}
}
// free( file );
}
// free( family );
}
return false;
}
std::string SearchFontPath(const std::string_view fontPattern, const FontConfigSearchParams& params, unsigned& faceIndex)
{
FcPattern* pattern;
FcPattern* matched;
FcResult result = FcResultMatch;
FcValue value;
pattern = FcPatternCreate();
if (pattern == nullptr)
return ("FcPatternCreate returned NULL");
// Build a pattern to search using postscript name, bold and italic
if (params.Flags != (uint16_t)FontConfigSearchFlags::MatchPostScriptName)
FcPatternAddString(pattern, FC_FAMILY, (const FcChar8*)fontPattern.data());
else
FcPatternAddString(pattern, FC_POSTSCRIPT_NAME, (const FcChar8*)fontPattern.data());
if (params.Style) {
bool isItalic = (params.Style & FontStyle::Italic) == FontStyle::Italic;
bool isBold = (params.Style & FontStyle::Bold) == FontStyle::Bold;
FcPatternAddInteger(pattern, FC_WEIGHT, (isBold ? FC_WEIGHT_BOLD : FC_WEIGHT_MEDIUM));
FcPatternAddInteger(pattern, FC_SLANT, (isItalic ? FC_SLANT_ITALIC : FC_SLANT_ROMAN));
}
// Follow fc-match procedure which proved to be more reliable
// https://github.com/freedesktop/fontconfig/blob/e291fda7d42e5d64379555097a066d9c2c4efce3/fc-match/fc-match.c#L188
if (!FcConfigSubstitute(m_FcConfig, pattern, FcMatchPattern)) {
FcPatternDestroy(pattern);
faceIndex = 0;
return {};
}
FcDefaultSubstitute(pattern);
std::string path;
matched = FcFontMatch(m_FcConfig, pattern, &result);
if (result != FcResultNoMatch) {
(void)FcPatternGet(matched, FC_FILE, 0, &value);
path = reinterpret_cast<const char*>(value.u.s);
(void)FcPatternGet(matched, FC_INDEX, 0, &value);
faceIndex = (unsigned)value.u.i;
}
FcPatternDestroy(pattern);
FcPatternDestroy(matched);
#if _WIN32
// Font config in Windows returns unix conventional path
// separator. Fix it
std::replace(path.begin(), path.end(), '/', '\\');
#endif
return path;
}
void testSingleFont(FcPattern* font)
{
std::string fontFamily;
std::string fontPath;
uint16_t style;
FontConfigSearchParams fcParams;
if (getFontInfo(font, fontFamily, fontPath, style)) {
unsigned faceIndex;
fcParams.Style = style;
fontPath = SearchFontPath(fontFamily, fcParams, faceIndex);
if (fontPath.length() != 0) {
std::cout << "fontPath:" << fontPath << ",family:" << fontFamily << ",style:" << style << std::endl;
}
}
// 简单打印
FcPatternPrint(font);
//自定义格式打印
const FcChar8* format = (const FcChar8*)"%{=fclist}\n";
//const FcChar8* format = (const FcChar8*)"%{=unparse}\n";
//const FcChar8* format = (const FcChar8*)"%{=fcmatch}\n";
auto s = FcPatternFormat(font, format);
std::cout << s << "\n";
FcStrFree(s);
}
// 测试字体安装目录
void listInstallFonts() {
// Get all installed fonts
auto pattern = FcPatternCreate();
auto objectSet = FcObjectSetBuild(FC_FAMILY, FC_STYLE, FC_FILE, FC_SLANT, FC_WEIGHT, nullptr);
auto fontSet = FcFontList(nullptr, pattern, objectSet);
FcObjectSetDestroy(objectSet);
FcPatternDestroy(pattern);
if (fontSet == nullptr) {
printf("Unable to search for fonts");
return;
}
std::cout << "font count " << fontSet->nfont << std::endl;
for (int i = 0; i < fontSet->nfont; i++)
testSingleFont(fontSet->fonts[i]);
FcFontSetDestroy(fontSet);
}
static FcStrSet* processed_dirs;
static int
scanDirs(FcStrList* list, FcConfig* config, FcBool force, FcBool really_force, FcBool verbose, FcBool error_on_no_fonts, int* changed)
{
int ret = 0;
const FcChar8* dir;
FcStrSet* subdirs;
FcStrList* sublist;
FcCache* cache;
struct stat statb;
FcBool was_valid, was_processed = FcFalse;
int i;
const FcChar8* sysroot = FcConfigGetSysRoot(config);
/*
* Now scan all of the directories into separate databases
* and write out the results
*/
while ((dir = FcStrListNext(list)))
{
if (verbose)
{
if (sysroot)
printf("[%s]", sysroot);
printf("%s: ", dir);
fflush(stdout);
}
if (FcStrSetMember(processed_dirs, dir))
{
if (verbose)
printf(("skipping, looped directory detected\n"));
continue;
}
FcChar8* rooted_dir = NULL;
if (sysroot)
{
rooted_dir = FcStrPlus(sysroot, dir);
}
else {
rooted_dir = FcStrCopy(dir);
}
if (stat((char*)rooted_dir, &statb) == -1)
{
switch (errno) {
case ENOENT:
case ENOTDIR:
if (verbose)
printf(("skipping, no such directory\n"));
break;
default:
fprintf(stderr, "\"%s\": ", dir);
perror("");
ret++;
break;
}
FcStrFree(rooted_dir);
rooted_dir = NULL;
continue;
}
FcStrFree(rooted_dir);
rooted_dir = NULL;
if ((statb.st_mode & 0xF000) != 0x4000)
{
fprintf(stderr, ("\"%s\": not a directory, skipping\n"), dir);
continue;
}
was_processed = FcTrue;
if (really_force)
{
FcDirCacheUnlink(dir, config);
}
cache = NULL;
was_valid = FcFalse;
if (!force) {
//if (FcFileIsDir(arg))
// cache = FcDirCacheLoad(arg, config, &rooted_dir);
//else
// cache = FcDirCacheLoadFile(arg, &statb);
cache = FcDirCacheLoad(dir, config, NULL);
if (cache)
was_valid = FcTrue;
}
if (!cache)
{
(*changed)++;
cache = FcDirCacheRead(dir, FcTrue, config);
if (!cache)
{
fprintf(stderr, ("\"%s\": scanning error\n"), dir);
ret++;
continue;
}
}
if (was_valid)
{
if (verbose)
printf(("skipping, existing cache is valid: %d fonts, %d dirs\n"),
FcCacheNumFont(cache), FcCacheNumSubdir(cache));
}
else
{
if (verbose)
printf(("caching, new cache contents: %d fonts, %d dirs\n"),
FcCacheNumFont(cache), FcCacheNumSubdir(cache));
if (!FcDirCacheValid(dir))
{
fprintf(stderr, ("%s: failed to write cache\n"), dir);
(void)FcDirCacheUnlink(dir, config);
ret++;
}
}
subdirs = FcStrSetCreate();
if (!subdirs)
{
fprintf(stderr, ("%s: Can't create subdir set\n"), dir);
ret++;
FcDirCacheUnload(cache);
continue;
}
for (i = 0; i < FcCacheNumSubdir(cache); i++)
FcStrSetAdd(subdirs, FcCacheSubdir(cache, i));
FcDirCacheUnload(cache);
sublist = FcStrListCreate(subdirs);
FcStrSetDestroy(subdirs);
if (!sublist)
{
fprintf(stderr, ("%s: Can't create subdir list\n"), dir);
ret++;
continue;
}
FcStrSetAdd(processed_dirs, dir);
ret += scanDirs(sublist, config, force, really_force, verbose, error_on_no_fonts, changed);
FcStrListDone(sublist);
}
if (error_on_no_fonts && !was_processed)
ret++;
return ret;
}
static FcBool
cleanCacheDirectories(FcConfig* config, FcBool verbose)
{
FcStrList* cache_dirs = FcConfigGetCacheDirs(config);
FcChar8* cache_dir;
FcBool ret = FcTrue;
if (!cache_dirs)
return FcFalse;
while ((cache_dir = FcStrListNext(cache_dirs)))
{
if (!FcDirCacheClean(cache_dir, verbose))
{
ret = FcFalse;
break;
}
}
FcStrListDone(cache_dirs);
return ret;
}
// 测试字体缓存目录
int testFontCache(const char* fontdirs) {
//if (!FcFileIsDir((const FCChar8*)fontdirs))
// return 0;
FcStrSet* dirs;
FcStrList* list;
FcBool verbose = FcFalse;
FcBool force = FcFalse;
FcBool really_force = FcFalse;
FcBool systemOnly = FcFalse;
FcBool error_on_no_fonts = FcFalse;
FcConfig* config;
FcChar8* sysroot = NULL;
int changed;
int ret;
if (systemOnly)
FcConfigEnableHome(FcFalse);
if (sysroot)
{
FcConfigSetSysRoot(NULL, sysroot);
FcStrFree(sysroot);
config = FcConfigGetCurrent();
}
else
{
config = FcInitLoadConfig();
}
if (!config)
{
fprintf(stderr, ("%s: Can't initialize font config library\n"), fontdirs);
return 1;
}
FcConfigSetCurrent(config);
if (fontdirs)
{
dirs = FcStrSetCreate();
if (!dirs)
{
fprintf(stderr, ("%s: Can't create list of directories\n"),
fontdirs);
return 1;
}
if (!FcStrSetAddFilename(dirs, (FcChar8*)fontdirs))
{
fprintf(stderr, ("%s: Can't add directory\n"), fontdirs);
return 1;
}
list = FcStrListCreate(dirs);
FcStrSetDestroy(dirs);
}
else
list = FcConfigGetFontDirs(config);
if ((processed_dirs = FcStrSetCreate()) == NULL) {
fprintf(stderr, ("Out of Memory\n"));
return 1;
}
if (verbose)
{
const FcChar8* dir;
printf("Font directories:\n");
while ((dir = FcStrListNext(list)))
{
printf("\t%s\n", dir);
}
FcStrListFirst(list);
}
changed = 0;
ret = scanDirs(list, config, force, really_force, verbose, error_on_no_fonts, &changed);
FcStrListDone(list);
/*
* Try to create CACHEDIR.TAG anyway.
* This expects the fontconfig cache directory already exists.
* If it doesn't, it won't be simply created.
*/
FcCacheCreateTagFile(config);
FcStrSetDestroy(processed_dirs);
cleanCacheDirectories(config, verbose);
FcConfigDestroy(config);
FcFini();
/*
* Now we need to sleep a second (or two, to be extra sure), to make
* sure that timestamps for changes after this run of fc-cache are later
* then any timestamps we wrote. We don't use gettimeofday() because
* sleep(3) can't be interrupted by a signal here -- this isn't in the
* library, and there aren't any signals flying around here.
*/
/* the resolution of mtime on FAT is 2 seconds */
//if (changed)
// sleep(2);
if (verbose)
printf("%s: %s\n", fontdirs, ret ? "failed" : "succeeded");
return ret;
}
// 配置列表
void testConfigList() {
FcConfig* config;
FcConfigFileInfoIter iter;
config = FcConfigGetCurrent();
FcConfigFileInfoIterInit(config, &iter);
do
{
FcChar8* name, * desc;
FcBool enabled;
if (FcConfigFileInfoIterGet(config, &iter, &name, &desc, &enabled))
{
printf("%c %s: %s\n", enabled ? '+' : '-', name, desc);
FcStrFree(name);
FcStrFree(desc);
}
} while (FcConfigFileInfoIterNext(config, &iter));
FcFini();
}
// 字体查找
void queryFont(const FcChar8 *fontdir) {
auto fs = FcFontSetCreate();
// FcFreeTypeQuery
if (!FcFreeTypeQueryAll(fontdir, 0, NULL, NULL, fs))
{
fprintf(stderr, "Can't query face %u of font file %s\n", -1, fontdir);
return;
}
for (int i = 0; i < fs->nfont; i++)
{
FcPattern* pat = fs->fonts[i];
FcPatternPrint(pat);
//FcPatternDel(pat, FC_CHARSET);
//FcPatternDel(pat, FC_LANG);
}
FcFontSetDestroy(fs);
FcFini();
}
// 字体目录解析
void scanFont(const FcChar8* file) {
auto fs = FcFontSetCreate();
if (!FcFileIsDir(file))
FcFileScan(fs, NULL, NULL, NULL, file, FcTrue);
else
{
FcStrSet* dirs = FcStrSetCreate();
FcStrList* strlist = FcStrListCreate(dirs);
do
{
FcDirScan(fs, dirs, NULL, NULL, file, FcTrue);
} while ((file = FcStrListNext(strlist)));
FcStrListDone(strlist);
FcStrSetDestroy(dirs);
}
for (int i = 0; i < fs->nfont; i++)
{
FcPattern* pat = fs->fonts[i];
FcPatternPrint(pat);
//FcPatternDel(pat, FC_CHARSET);
//FcPatternDel(pat, FC_LANG);
}
FcFontSetDestroy(fs);
FcFini();
}
void test()
{
//listInstallFonts();
//testFontCache("C:\\Fonts");
//testConfigList();
//queryFont((const FcChar8*)"C:\\Fonts\\LiberationSans.ttc");
//scanFont((const FcChar8*)"C:\\Fonts");
// 待续......
// FcConfigSetCurrent FcConfigUptoDate FcConfigGetCache FcConfigGetRescanInterval FcConfigGetFonts FcConfigAppFontAddFile FcConfigAppFontAddDir
// FcConfigAppFontClear FcDirSave FcDirCacheRescan FcFontSetList FcAtomicLock FcFontSetMatch FcFontRenderPrepare FcFontSetSort
// FcFontSetSortDestroy FcPatternHash
}
输出
fontPath:C:\Windows\fonts\GOTHIC.TTF,family:Century Gothic,style:0
Pattern has 5 elts (size 16)
family: "Century Gothic"(s)
style: "Regular"(s) "Normal"(s) "obyčejné"(s) "Standard"(s) "Κανονικά"(s) "Normaali"(s) "Normál"(s) "Normale"(s) "Standaard"(s) "Normalny"(s) "Обычный"(s) "Normálne"(s) "Navadno"(s) "Arrunta"(s)
slant: 0(i)(s)
weight: 80(f)(s)
file: "C:/Windows/fonts\GOTHIC.TTF"(s)
C:/Windows/fonts\GOTHIC.TTF: Century Gothic:style=Regular,Normal,obyčejné,Standard,Κανονικά,Normaali,Normál,Normale,Standaard,Normalny,Обычный,Normálne,Navadno,Arrunta:slant=0:weight=80
Pattern has 26 elts (size 32)
family: "Liberation Sans"(s)
familylang: "en"(s)
style: "Bold"(s)
stylelang: "en"(s)
fullname: "Liberation Sans Bold"(s)
fullnamelang: "en"(s)
slant: 0(i)(s)
weight: 200(f)(s)
width: 100(f)(s)
foundry: "1ASC"(s)
file: "C:\Fonts\LiberationSans-Bold.ttf"(s)
index: 0(i)(s)
outline: True(s)
scalable: True(s)
charset:
0000: 00000000 ffffffff ffffffff 7fffffff 00000000 ffffffff ffffffff ffffffff
0001: ffffffff ffffffff ffffffff ffffffff 00040000 00000000 00000000 fc000000
0002: 0f000000 00000000 00000000 00000000 00000000 00000000 3f0002c0 00000000
0003: 00000000 00000000 00000000 40000000 ffffd7f0 fffffffb 00007fff 00000000
0004: ffffffff ffffffff ffffffff 000c0000 00030000 00000000 00000000 00000000
001e: 00000000 00000000 00000000 00000000 0000003f 00000000 00000000 000c0000
0020: 7fbb0000 560d0047 00000010 80000000 00000000 00001098 00000000 00000000
0021: 00480020 00004044 78000000 00000000 003f0000 00000100 00000000 00000000
0022: c6268044 00000a00 00000100 00000033 00000000 00000000 00000000 00000000
0023: 00010004 00000003 00000000 00000000 00000000 00000000 00000000 00000000
0025: 11111005 10101010 ffff0000 00001fff 000f1111 14041c03 03008c10 00000040
0026: 00000000 1c000000 00000005 00001c69 00000000 00000000 00000000 00000000
00f0: 00000026 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00fb: 00000006 00000000 00000000 00000000 00000000 00000000 00000000 00000000
(s)
lang: aa|af|av|ay|be|bg|bi|br|bs|ca|ce|ch|co|cs|cy|da|de|el|en|eo|es|et|eu|fi|fj|fo|fr|fur|fy|gd|gl|gv|ho|hr|hu|ia|id|ie|ik|io|is|it|ki|kl|kum|la|lb|lez|lt|lv|mg|mh|mk|mo|mt|nb|nds|nl|nn|no|nr|nso|ny|oc|om|os|pl|pt|rm|ro|ru|se|sel|sk|sl|sma|smj|smn|so|sq|sr|ss|st|sv|sw|tk|tl|tn|tr|ts|uk|uz|vo|vot|wa|wen|wo|xh|yap|zu|an|crh|csb|fil|hsb|ht|jv|kj|ku-tr|kwm|lg|li|ms|na|ng|pap-an|pap-aw|rn|rw|sc|sg|sn|su|za(s)
fontversion: 70123(i)(s)
capability: "otlayout:DFLT otlayout:cyrl otlayout:grek otlayout:latn"(s)
fontformat: "TrueType"(s)
decorative: False(s)
postscriptname: "LiberationSans-Bold"(s)
color: False(s)
symbol: False(s)
variable: False(s)
fonthashint: True(s)
order: 0(i)(s)
参考
https://github.com/freedesktop/fontconfig
https://github.com/ShiftMediaProject/fontconfig
https://github.com/benoitkugler/textlayout
https://github.com/benoitkugler/textprocessing
https://github.com/go-text/typesetting