Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
font-discovery.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
2/*
3 * Author:
4 * Michael Kowalski
5 *
6 * Copyright (C) 2022-2024 Michael Kowalski
7 *
8 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
9 */
10
11#include "font-discovery.h"
12#include "async/progress.h"
13#include <sigc++/scoped_connection.h>
15#include "io/resource.h"
16
17#include <algorithm>
18#include <filesystem>
19#include <cairo-ft.h>
20#include <cairomm/surface.h>
21#include <glibmm/ustring.h>
22#include <iostream>
25#include <glibmm/keyfile.h>
26#include <glibmm/miscutils.h>
27#include <memory>
28#include <pango/pango-fontmap.h>
29#include <pangomm/fontdescription.h>
30#include <pangomm/fontmap.h>
31#include <sigc++/connection.h>
32#include <unordered_map>
33#include <vector>
34
35namespace filesystem = std::filesystem;
36
37namespace Inkscape {
38
39// Attempt to estimate how heavy given typeface is by drawing some capital letters and counting
40// black pixels (alpha channel). This is imperfect, but reasonable proxy for font weight, as long
41// as Pango can instantiate correct font.
42double calculate_font_weight(Pango::FontDescription& desc, double caps_height) {
43 // pixmap with enough room for a few characters; the rest will be cropped
44 auto surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, 128, 64);
45 auto context = Cairo::Context::create(surface);
46 auto layout = Pango::Layout::create(context);
47 const char* txt = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
48 layout->set_text(txt);
49 auto size = 22 * PANGO_SCALE;
50 if (caps_height > 0) {
51 size /= caps_height;
52 }
53 desc.set_size(size);
54 layout->set_font_description(desc);
55 context->move_to(1, 1);
56 layout->show_in_cairo_context(context);
57 surface->flush();
58
59 auto pixels = surface->get_data();
60 auto width = surface->get_width();
61 auto stride = surface->get_stride() / width;
62 auto height = surface->get_height();
63 double sum = 0;
64 for (auto y = 0; y < height; ++y) {
65 for (auto x = 0; x < width; ++x) {
66 sum += pixels[3]; // read alpha
67 pixels += stride;
68 }
69 }
70 auto weight = sum / (width * height);
71 return weight;
72}
73
74// calculate width of a A-Z string to try to measure average character width
75double calculate_font_width(Pango::FontDescription& desc) {
76 auto surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, 1, 1);
77 auto context = Cairo::Context::create(surface);
78 auto layout = Pango::Layout::create(context);
79 const char* txt = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
80 layout->set_text(txt);
81 desc.set_size(72 * PANGO_SCALE);
82 layout->set_font_description(desc);
83 // layout->show_in_cairo_context(context);
84 Pango::Rectangle ink, rect;
85 layout->get_extents(ink, rect);
86 return static_cast<double>(ink.get_width()) / PANGO_SCALE / strlen(txt);
87}
88
89// construct font name from Pango face and family;
90// return font name as it is recorded in the font itself, as far as Pango allows it
91Glib::ustring get_full_font_name(Glib::RefPtr<Pango::FontFamily> ff, Glib::RefPtr<Pango::FontFace> face) {
92 if (!ff) return "";
93
94 auto family = ff->get_name();
95 auto face_name = face ? face->get_name() : Glib::ustring();
96 auto name = face_name.empty() ? family : family + ' ' + face_name;
97 return name;
98}
99
100
101// calculate value to order font's styles
102int get_font_style_order(const Pango::FontDescription& desc) {
103 return
104 static_cast<int>(desc.get_weight()) * 1'000'000 +
105 static_cast<int>(desc.get_style()) * 10'000 +
106 static_cast<int>(desc.get_stretch()) * 100 +
107 static_cast<int>(desc.get_variant());
108}
109
110// sort fonts in-place by name using lexicographical order; if 'sans_first' is true place "Sans" font first
111void sort_fonts_by_name(std::vector<FontInfo>& fonts, bool sans_first) {
112 std::sort(begin(fonts), end(fonts), [=](const FontInfo& a, const FontInfo& b) {
113 auto na = a.ff->get_name();
114 auto nb = b.ff->get_name();
115 if (sans_first) {
116 bool sans_a = a.synthetic && a.ff->get_name() == "Sans";
117 bool sans_b = b.synthetic && b.ff->get_name() == "Sans";
118 if (sans_a != sans_b) {
119 return sans_a;
120 }
121 }
122 // check family names first
123 if (na != nb) {
124 // lexicographical order:
125 return na < nb;
126 // alphabetical order:
127 //return na.raw() < nb.raw();
128 }
129 return get_font_style_order(a.face->describe()) < get_font_style_order(b.face->describe());
130 });
131}
132
133// sort fonts in requested order, in-place
134void sort_fonts(std::vector<FontInfo>& fonts, FontOrder order, bool sans_first) {
135 switch (order) {
137 sort_fonts_by_name(fonts, sans_first);
138 break;
139
141 // there are many repetitions for weight, due to font substitutions, so sort by name first
142 sort_fonts_by_name(fonts, sans_first);
143 std::stable_sort(begin(fonts), end(fonts), [](const FontInfo& a, const FontInfo& b) { return a.weight < b.weight; });
144 break;
145
147 sort_fonts_by_name(fonts, sans_first);
148 std::stable_sort(begin(fonts), end(fonts), [](const FontInfo& a, const FontInfo& b) { return a.width < b.width; });
149 break;
150
151 default:
152 g_warning("Missing case in sort_fonts");
153 break;
154 }
155}
156
157Glib::ustring get_fontspec(const Glib::ustring& family, const Glib::ustring& face, const Glib::ustring& variations) {
158 if (variations.empty()) {
159 return face.empty() ? family : family + ", " + face;
160 }
161 else {
162 auto desc = (face.empty() ? family : family + ", " + face) + " " + variations;
163 return desc;
164 }
165}
166
167Glib::ustring get_fontspec(const Glib::ustring& family, const Glib::ustring& face) {
168 return get_fontspec(family, face, Glib::ustring());
169}
170
171Glib::ustring get_face_style(const Pango::FontDescription& desc) {
172 Pango::FontDescription copy(desc);
173 copy.unset_fields(Pango::FontMask::FAMILY);
174 copy.unset_fields(Pango::FontMask::SIZE);
175 auto str = copy.to_string();
176 return str;
177}
178
179Glib::ustring get_inkscape_fontspec(const Glib::RefPtr<Pango::FontFamily>& ff, const Glib::RefPtr<Pango::FontFace>& face, const Glib::ustring& variations) {
180 if (!ff) return Glib::ustring();
181
182 return get_fontspec(ff->get_name(), face ? get_face_style(face->describe()) : Glib::ustring(), variations);
183}
184
185Pango::FontDescription get_font_description(const Glib::RefPtr<Pango::FontFamily>& ff, const Glib::RefPtr<Pango::FontFace>& face) {
186 if (!face) return Pango::FontDescription("sans serif");
187
188 auto desc = face->describe();
189 desc.unset_fields(Pango::FontMask::SIZE);
190 return desc;
191}
192
193// Font cache is a text file that stores under each font name some of its metadata, like average weight and height,
194// as well as flags (monospaced, variable, oblique, synthetic font). It is kept to speed up font metadata discovery.
195const char font_cache[] = "font-cache.ini";
196const char cache_header[] = "@font-cache@";
197constexpr auto cache_version = 1.0;
198enum FontCacheFlags : int {
200 Monospace = 0x01,
201 Oblique = 0x02,
202 Variable = 0x04,
203 Synthetic = 0x08,
204};
205
206void save_font_cache(const std::vector<FontInfo>& fonts) {
207 auto keyfile = Glib::KeyFile::create();
208
209 keyfile->set_double(cache_header, "version", cache_version);
210 Glib::ustring weight("weight");
211 Glib::ustring width("width");
212 Glib::ustring family("family");
213 Glib::ustring fontflags("flags");
214
215 for (auto&& font : fonts) {
216 auto desc = get_font_description(font.ff, font.face);
217 auto group = desc.to_string();
218 int flags = FontCacheFlags::Normal;
219 if (font.monospaced) {
221 }
222 if (font.oblique) {
224 }
225 if (font.variable_font) {
227 }
228 if (font.synthetic) {
230 }
231 keyfile->set_double(group, weight, font.weight);
232 keyfile->set_double(group, width, font.width);
233 keyfile->set_integer(group, family, font.family_kind);
234 keyfile->set_integer(group, fontflags, flags);
235 }
236
237 std::string filename = Glib::build_filename(Inkscape::IO::Resource::profile_path(), font_cache);
238 keyfile->save_to_file(filename);
239}
240
241std::unordered_map<std::string, FontInfo> load_cached_font_info() {
242 std::unordered_map<std::string, FontInfo> info;
243
244 try {
245 auto keyfile = Glib::KeyFile::create();
246 std::string filename = Glib::build_filename(Inkscape::IO::Resource::profile_path(), font_cache);
247
248#ifdef G_OS_WIN32
249 bool exists = filesystem::exists(filesystem::u8path(filename));
250#else
251 bool exists = filesystem::exists(filesystem::path(filename));
252#endif
253
254 if (exists && keyfile->load_from_file(filename)) {
255
256 auto ver = keyfile->get_double(cache_header, "version");
257 if (std::abs(ver - cache_version) > 0.0001) return info;
258
259 Glib::ustring weight("weight");
260 Glib::ustring width("width");
261 Glib::ustring family("family");
262 Glib::ustring fontflags("flags");
263
264 for (auto&& group : keyfile->get_groups()) {
265 if (group == cache_header) continue;
266
267 FontInfo font;
268 auto flags = keyfile->get_integer(group, fontflags);
269 if (flags & FontCacheFlags::Monospace) {
270 font.monospaced = true;
271 }
272 if (flags & FontCacheFlags::Oblique) {
273 font.oblique = true;
274 }
275 if (flags & FontCacheFlags::Variable) {
276 font.variable_font = true;
277 }
278 if (flags & FontCacheFlags::Synthetic) {
279 font.synthetic = true;
280 }
281 font.weight = keyfile->get_double(group, weight);
282 font.width = keyfile->get_double(group, width);
283 font.family_kind = keyfile->get_integer(group, family);
284
285 info[group.raw()] = font;
286 }
287 }
288 }
289 catch (Glib::Error &error) {
290 std::cerr << G_STRFUNC << ": font cache not loaded - " << error.what() << std::endl;
291 }
292
293 return info;
294}
295
296std::vector<FontInfo> get_all_fonts() {
297 std::vector<FontInfo> fonts;
298 return fonts;
299}
300
301std::shared_ptr<const std::vector<FontInfo>> get_all_fonts(Async::Progress<double, Glib::ustring, std::vector<FontInfo>>& progress) {
302 auto result = std::make_shared<std::vector<FontInfo>>();
303 auto& fonts = *result;
305
306 std::vector<FontInfo> empty;
307 progress.report_or_throw(0, "", empty);
308
309 auto families = FontFactory::get().get_font_families();
310
311 progress.throw_if_cancelled();
312 bool update_cache = false;
313
314 double counter = 0.0;
315 for (auto ff : families) {
316 bool synthetic_font = false;
317#if PANGO_VERSION_CHECK(1,46,0)
318 auto default_face = ff->get_face();
319 if (default_face && default_face->is_synthesized()) {
320 synthetic_font = true;
321 }
322#endif
323 progress.report_or_throw(counter / families.size(), ff->get_name(), empty);
324 std::vector<FontInfo> family;
325 auto faces = ff->list_faces();
326 std::set<std::string> styles;
327 for (auto face : faces) {
328 // skip synthetic faces of normal fonts, they pollute listing with fake entries,
329 // but let entirely synthetic fonts in ("Sans", "Monospace", etc)
330 if (!synthetic_font && face->is_synthesized()) continue;
331
332 auto desc = face->describe();
333 desc.unset_fields(Pango::FontMask::SIZE);
334 std::string key = desc.to_string();
335 if (styles.count(key)) continue;
336
337 styles.insert(key);
338
339 FontInfo info = { ff, face };
340 info.synthetic = synthetic_font;
341 bool valid = false;
342
343 desc = get_font_description(ff, face);
344 auto it = cache.find(desc.to_string().raw());
345 if (it == cache.end()) {
346 // font not found in a cache; calculate metrics
347
348 update_cache = true;
349
350 double caps_height = 0.0;
351
352 try {
353 auto font = FontFactory::get().create_face(desc.gobj());
354 if (!font) {
355 g_warning("Cannot load font %s", key.c_str());
356 }
357 else {
358 valid = true;
359 info.monospaced = font->is_fixed_width();
360 info.oblique = font->is_oblique();
361 info.family_kind = font->family_class();
362 info.variable_font = !font->get_opentype_varaxes().empty();
363 auto glyph = font->LoadGlyph(font->MapUnicodeChar('E'));
364 if (glyph) {
365 // caps height normalized to 0..1
366 caps_height = glyph->bbox_exact.height();
367 }
368 }
369 }
370 catch (...) {
371 g_warning("Error loading font %s", key.c_str());
372 }
373 desc = get_font_description(ff, face);
374 info.weight = calculate_font_weight(desc, caps_height);
375
376 desc = get_font_description(ff, face);
377 info.width = calculate_font_width(desc);
378 }
379 else {
380 // font in a cache already
381 info = it->second;
382 valid = true;
383 }
384
385 if (valid) {
386 info.ff = ff;
387 info.face = face;
388 fonts.emplace_back(info);
389 family.emplace_back(info);
390 }
391 }
392 progress.report_or_throw(++counter / families.size(), "", family);
393 }
394
395 if (update_cache) {
396 save_font_cache(fonts);
397 }
398
399 progress.report_or_throw(1, "", empty);
400
401 return result;
402}
403
404Glib::ustring get_fontspec_without_variants(const Glib::ustring& fontspec) {
405 auto at = fontspec.rfind('@');
406 if (at != Glib::ustring::npos && at > 0) {
407 // remove variations
408 while (at > 0 && fontspec[at - 1] == ' ') at--; // trim spaces
409
410 return fontspec.substr(0, at);
411 }
412 return fontspec;
413}
414
416 if (auto i = InkscapeApplication::instance()) {
417 i->gio_app()->signal_shutdown().connect([this](){
418 _loading.cancel();
419 });
420 }
421
422 _connection = _loading.subscribe([this](const MessageType& msg) {
423 if (auto result = Async::Msg::get_result(msg)) {
424 // cache results
425 _fonts = *result;
426 }
427 // propagate events
428 _events.emit(msg);
429 });
430}
431
432sigc::scoped_connection FontDiscovery::connect_to_fonts(std::function<void (const MessageType&)> fn) {
433
434 sigc::scoped_connection con = static_cast<sigc::connection>(_events.connect(fn));
435
436 if (!_fonts && !_loading.is_running()) {
437 // load fonts async
438 _loading.start(
439 [=](Async::Progress<double, Glib::ustring, std::vector<FontInfo>>& p) { return get_all_fonts(p); }
440 );
441 }
442 else if (_fonts) {
443 // fonts already loaded
446 }
447
448 return con;
449}
450
451} // namespace
452
453/*
454 Local Variables:
455 mode:c++
456 c-file-style:"stroustrup"
457 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
458 indent-tabs-mode:nil
459 fill-column:99
460 End:
461*/
462// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
Cairo::RefPtr< Cairo::ImageSurface > surface
Definition canvas.cpp:137
std::unique_ptr< FontInstance > create_face(PangoFontDescription *descr)
std::vector< Glib::RefPtr< Pango::FontFamily > > get_font_families()
static InkscapeApplication * instance()
Singleton instance.
An interface for tasks to report progress and check for cancellation.
Definition progress.h:23
Async::Msg::Message< FontsPayload, double, Glib::ustring, std::vector< FontInfo > > MessageType
sigc::signal< void(const MessageType &)> _events
Inkscape::Async::OperationStream< FontsPayload, double, Glib::ustring, std::vector< FontInfo > > _loading
sigc::scoped_connection connect_to_fonts(std::function< void(const MessageType &)> fn)
sigc::scoped_connection _connection
static FontFactory & get(Args &&... args)
Definition statics.h:153
Css & result
Glib::ustring msg
const unsigned order
TODO: insert short description here.
The data describing a single loaded font.
std::string cache
Geom::Point end
const R * get_result(const Msg::Message< R, T... > &msg)
std::string profile_path()
Definition resource.cpp:415
Helper class to stream background task notifications as a series of messages.
const char cache_header[]
void save_font_cache(const std::vector< FontInfo > &fonts)
Glib::ustring get_inkscape_fontspec(const Glib::RefPtr< Pango::FontFamily > &ff, const Glib::RefPtr< Pango::FontFace > &face, const Glib::ustring &variations)
double calculate_font_weight(Pango::FontDescription &desc, double caps_height)
Glib::ustring get_fontspec_without_variants(const Glib::ustring &fontspec)
void sort_fonts_by_name(std::vector< FontInfo > &fonts, bool sans_first)
int get_font_style_order(const Pango::FontDescription &desc)
void sort_fonts(std::vector< FontInfo > &fonts, FontOrder order, bool sans_first)
Pango::FontDescription get_font_description(const Glib::RefPtr< Pango::FontFamily > &ff, const Glib::RefPtr< Pango::FontFace > &face)
Glib::ustring get_fontspec(const Glib::ustring &family, const Glib::ustring &face, const Glib::ustring &variations)
constexpr auto cache_version
Glib::ustring get_face_style(const Pango::FontDescription &desc)
std::vector< FontInfo > get_all_fonts()
Glib::ustring get_full_font_name(Glib::RefPtr< Pango::FontFamily > ff, Glib::RefPtr< Pango::FontFace > face)
double calculate_font_width(Pango::FontDescription &desc)
const char font_cache[]
std::unordered_map< std::string, FontInfo > load_cached_font_info()
static cairo_user_data_key_t key
static gint counter
Definition box3d.cpp:39
int stride
unsigned long weight
Definition quantize.cpp:37
Inkscape::IO::Resource - simple resource API.
double sum(const double alpha[16], const double &x, const double &y)
unsigned short family_kind
Glib::RefPtr< Pango::FontFace > face
Glib::RefPtr< Pango::FontFamily > ff
double height
double width
Glib::ustring name
Definition toolbars.cpp:55