/* * Copyright (C) 2003,2008 Red Hat, Inc. * Copyright © 2019, 2020 Christian Persch * * This library is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see . */ #include "config.h" #include "fonts-pangocairo.hh" #include "cairo-glue.hh" #include "debug.h" #include "vtedefines.hh" /* Have a space between letters to make sure ligatures aren't used when caching the glyphs: bug 793391. */ #define VTE_DRAW_SINGLE_WIDE_CHARACTERS \ " ! \" # $ % & ' ( ) * + , - . / " \ "0 1 2 3 4 5 6 7 8 9 " \ ": ; < = > ? @ " \ "A B C D E F G H I J K L M N O P Q R S T U V W X Y Z " \ "[ \\ ] ^ _ ` " \ "a b c d e f g h i j k l m n o p q r s t u v w x y z " \ "{ | } ~ " \ "" static inline bool _vte_double_equal(double a, double b) { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wfloat-equal" return a == b; #pragma GCC diagnostic pop } #define FONT_CACHE_TIMEOUT (30) /* seconds */ namespace vte { namespace view { static GHashTable* s_font_info_for_context{nullptr}; FontInfo::UnistrInfo* FontInfo::find_unistr_info(vteunistr c) { if (G_LIKELY (c < G_N_ELEMENTS(m_ascii_unistr_info))) return &m_ascii_unistr_info[c]; if (G_UNLIKELY (m_other_unistr_info == nullptr)) m_other_unistr_info = g_hash_table_new_full(nullptr, nullptr, nullptr, (GDestroyNotify)unistr_info_destroy); auto uinfo = reinterpret_cast(g_hash_table_lookup(m_other_unistr_info, GINT_TO_POINTER(c))); if (G_LIKELY (uinfo)) return uinfo; uinfo = new UnistrInfo{}; g_hash_table_insert(m_other_unistr_info, GINT_TO_POINTER (c), uinfo); return uinfo; } void FontInfo::cache_ascii() { PangoLayoutLine *line; PangoGlyphItemIter iter; PangoGlyphItem *glyph_item; PangoGlyphString *glyph_string; PangoFont *pango_font; cairo_scaled_font_t *scaled_font; const char *text; gboolean more; PangoLanguage *language; gboolean latin_uses_default_language; /* We have m_layout holding most ASCII characters. We want to * cache as much info as we can about the ASCII letters so we don't * have to look them up again later */ /* Don't cache if unknown glyphs found in layout */ if (pango_layout_get_unknown_glyphs_count(m_layout.get()) != 0) return; language = pango_context_get_language(pango_layout_get_context(m_layout.get())); if (language == nullptr) language = pango_language_get_default (); latin_uses_default_language = pango_language_includes_script (language, PANGO_SCRIPT_LATIN); text = pango_layout_get_text(m_layout.get()); line = pango_layout_get_line_readonly(m_layout.get(), 0); /* Don't cache if more than one font used for the line */ if (G_UNLIKELY (!line || !line->runs || line->runs->next)) return; glyph_item = (PangoGlyphItem *)line->runs->data; glyph_string = glyph_item->glyphs; pango_font = glyph_item->item->analysis.font; if (!pango_font) return; scaled_font = pango_cairo_font_get_scaled_font ((PangoCairoFont *) pango_font); if (!scaled_font) return; for (more = pango_glyph_item_iter_init_start (&iter, glyph_item, text); more; more = pango_glyph_item_iter_next_cluster (&iter)) { PangoGlyphGeometry *geometry; PangoGlyph glyph; vteunistr c; /* Only cache simple clusters */ if (iter.start_char +1 != iter.end_char || iter.start_index+1 != iter.end_index || iter.start_glyph+1 != iter.end_glyph) continue; c = text[iter.start_index]; glyph = glyph_string->glyphs[iter.start_glyph].glyph; geometry = &glyph_string->glyphs[iter.start_glyph].geometry; /* If not using the default locale language, only cache non-common * characters as common characters get their font from their neighbors * and we don't want to force Latin on them. */ if (!latin_uses_default_language && g_unichar_get_script (c) <= G_UNICODE_SCRIPT_INHERITED) continue; /* Only cache simple glyphs */ if (!(glyph <= 0xFFFF) || (geometry->x_offset | geometry->y_offset) != 0) continue; auto uinfo = find_unistr_info(c); if (G_UNLIKELY (uinfo->coverage() != UnistrInfo::Coverage::UNKNOWN)) continue; auto ufi = &uinfo->m_ufi; uinfo->width = PANGO_PIXELS_CEIL (geometry->width); uinfo->has_unknown_chars = false; uinfo->set_coverage(UnistrInfo::Coverage::USE_CAIRO_GLYPH); ufi->using_cairo_glyph.scaled_font = cairo_scaled_font_reference (scaled_font); ufi->using_cairo_glyph.glyph_index = glyph; #if VTE_DEBUG m_coverage_count[0]++; m_coverage_count[(unsigned)uinfo->coverage()]++; #endif } #if VTE_DEBUG _vte_debug_print (VTE_DEBUG_PANGOCAIRO, "vtepangocairo: %p cached %d ASCII letters\n", (void*)this, m_coverage_count[0]); #endif } void FontInfo::measure_font() { PangoRectangle logical; /* Measure U+0021..U+007E individually instead of all together and then * averaging. For monospace fonts, the results should be the same, but * if the user (by design, or trough mis-configuration) uses a proportional * font, the latter method will greatly underestimate the required width, * leading to unreadable, overlapping characters. * https://gitlab.gnome.org/GNOME/vte/issues/138 */ auto max_width = 1; auto max_height = 1; for (char c = 0x21; c < 0x7f; ++c) { pango_layout_set_text(m_layout.get(), &c, 1); pango_layout_get_extents(m_layout.get(), nullptr, &logical); max_width = std::max(max_width, PANGO_PIXELS_CEIL(logical.width)); max_height = std::max(max_height, PANGO_PIXELS_CEIL(logical.height)); } /* Use the sample text to get the baseline */ pango_layout_set_text(m_layout.get(), VTE_DRAW_SINGLE_WIDE_CHARACTERS, -1); pango_layout_get_extents(m_layout.get(), nullptr, &logical); /* We don't do CEIL for width since we are averaging; * rounding is more accurate */ m_ascent = PANGO_PIXELS_CEIL(pango_layout_get_baseline(m_layout.get())); m_height = max_height; m_width = max_width; /* Now that we shaped the entire ASCII character string, cache glyph * info for them */ cache_ascii(); } FontInfo::FontInfo(vte::glib::RefPtr context) { _vte_debug_print (VTE_DEBUG_PANGOCAIRO, "vtepangocairo: %p allocating FontInfo\n", (void*)this); m_layout = vte::glib::take_ref(pango_layout_new(context.get())); auto tabs = pango_tab_array_new_with_positions(1, FALSE, PANGO_TAB_LEFT, 1); pango_layout_set_tabs(m_layout.get(), tabs); pango_tab_array_free(tabs); // FIXME!!! m_string = g_string_sized_new(VTE_UTF8_BPC+1); measure_font(); #if PANGO_VERSION_CHECK(1, 44, 0) /* Try using the font's metrics; see issue#163. */ if (auto metrics = vte::take_freeable (pango_context_get_metrics(context.get(), nullptr /* use font from context */, nullptr /* use language from context */))) { /* Use provided metrics if possible */ auto const ascent = PANGO_PIXELS_CEIL(pango_font_metrics_get_ascent(metrics.get())); auto const height = PANGO_PIXELS_CEIL(pango_font_metrics_get_height(metrics.get())); #if 0 /* Note that we cannot use the font's width, since doing so * regresses issue#138 (non-monospaced font). * FIXME: Make sure the font is monospace before we get * here, and then use the font's width too. */ auto const width = PANGO_PIXELS_CEIL(pango_font_metrics_get_approximate_char_width(metrics.get())); #endif /* 0 */ /* Sometimes, the metrics return a lower height than the one we measured * in measure_font(), causing cut-off at the bottom of the last line, see * https://gitlab.gnome.org/GNOME/gnome-terminal/-/issues/340 . Therefore * we only use the metrics when its height is at least that which we measured. */ if (ascent > 0 && height >= m_height) { _vte_debug_print(VTE_DEBUG_PANGOCAIRO, "Using pango metrics\n"); m_ascent = ascent; m_height = height; #if 0 m_width = width; #endif } else if (ascent >= 0 && height > 0) { _vte_debug_print(VTE_DEBUG_PANGOCAIRO, "Disregarding pango metrics due to incorrect height (%d < %d)\n", height, m_height); } else { _vte_debug_print(VTE_DEBUG_PANGOCAIRO, "Not using pango metrics due to not providing height or ascent\n"); } } #endif /* pango >= 1.44 */ _vte_debug_print (VTE_DEBUG_PANGOCAIRO | VTE_DEBUG_MISC, "vtepangocairo: %p font metrics = %dx%d (%d)\n", (void*)this, m_width, m_height, m_ascent); g_hash_table_insert(s_font_info_for_context, pango_layout_get_context(m_layout.get()), this); } FontInfo::~FontInfo() { g_hash_table_remove(s_font_info_for_context, pango_layout_get_context(m_layout.get())); #if VTE_DEBUG _vte_debug_print (VTE_DEBUG_PANGOCAIRO, "vtepangocairo: %p freeing font_info. coverages %d = %d + %d + %d\n", (void*)this, m_coverage_count[0], m_coverage_count[1], m_coverage_count[2], m_coverage_count[3]); #endif g_string_free(m_string, true); if (m_other_unistr_info) { g_hash_table_destroy(m_other_unistr_info); } } static GQuark fontconfig_timestamp_quark (void) { static GQuark quark; if (G_UNLIKELY (!quark)) quark = g_quark_from_static_string ("vte-fontconfig-timestamp"); return quark; } static void vte_pango_context_set_fontconfig_timestamp (PangoContext *context, guint fontconfig_timestamp) { g_object_set_qdata ((GObject *) context, fontconfig_timestamp_quark (), GUINT_TO_POINTER (fontconfig_timestamp)); } static guint vte_pango_context_get_fontconfig_timestamp (PangoContext *context) { return GPOINTER_TO_UINT (g_object_get_qdata ((GObject *) context, fontconfig_timestamp_quark ())); } static guint context_hash (PangoContext *context) { return pango_units_from_double (pango_cairo_context_get_resolution (context)) ^ pango_font_description_hash (pango_context_get_font_description (context)) ^ cairo_font_options_hash (pango_cairo_context_get_font_options (context)) ^ GPOINTER_TO_UINT (pango_context_get_language (context)) ^ vte_pango_context_get_fontconfig_timestamp (context); } static gboolean context_equal (PangoContext *a, PangoContext *b) { return _vte_double_equal(pango_cairo_context_get_resolution(a), pango_cairo_context_get_resolution (b)) && pango_font_description_equal (pango_context_get_font_description (a), pango_context_get_font_description (b)) && cairo_font_options_equal (pango_cairo_context_get_font_options (a), pango_cairo_context_get_font_options (b)) && pango_context_get_language (a) == pango_context_get_language (b) && vte_pango_context_get_fontconfig_timestamp (a) == vte_pango_context_get_fontconfig_timestamp (b); } // FIXMEchpe return vte::base::RefPtr FontInfo* FontInfo::create_for_context(vte::glib::RefPtr context, PangoFontDescription const* desc, PangoLanguage* language, cairo_font_options_t const* font_options, guint fontconfig_timestamp) { if (!PANGO_IS_CAIRO_FONT_MAP(pango_context_get_font_map(context.get()))) { /* Ouch, Gtk+ switched over to some drawing system? * Lets just create one from the default font map. */ context = vte::glib::take_ref(pango_font_map_create_context(pango_cairo_font_map_get_default())); } vte_pango_context_set_fontconfig_timestamp(context.get(), fontconfig_timestamp); pango_context_set_base_dir(context.get(), PANGO_DIRECTION_LTR); if (desc) pango_context_set_font_description(context.get(), desc); if (language != nullptr && language != pango_context_get_language(context.get())) pango_context_set_language(context.get(), language); { // Make sure our contexts have a font_options set. We use // this invariant in our context hash and equal functions. auto builtin_font_options = vte::take_freeable(cairo_font_options_create()); #if VTE_GTK == 4 // On gtk4, we need to ensure Pango and cairo are configured to quantize // and hint font metrics. Terminal cells in vte have integer pixel sizes. // If Pango is configured to do sub-pixel glyph advances, a small fractional // part might get rounded up to a whole pixel; so the character spacing will // appear too wide. Setting the cairo hint metrics option ensures that there // are integer numbers of pixels both above and below the baseline. // See issue#2573. cairo_font_options_set_hint_metrics(builtin_font_options.get(), CAIRO_HINT_METRICS_ON); #endif /* VTE_GTK == 4 */ // Allow using the API to override the built-in hint metrics setting. if (!font_options) font_options = builtin_font_options.get(); if (auto const ctx_font_options = pango_cairo_context_get_font_options(context.get())) { auto const merged_font_options = vte::take_freeable(cairo_font_options_copy(ctx_font_options)); cairo_font_options_merge(merged_font_options.get(), font_options); pango_cairo_context_set_font_options(context.get(), merged_font_options.get()); } else { pango_cairo_context_set_font_options(context.get(), font_options); } #if VTE_GTK == 4 // If hinting font metrics, also make sure to round glyph positions // to integers. See issue#2573. if (auto const ctx_font_options = pango_cairo_context_get_font_options(context.get()); ctx_font_options && cairo_version() >= CAIRO_VERSION_ENCODE(1, 17, 4)) { auto const hint_metrics = cairo_font_options_get_hint_metrics(ctx_font_options); pango_context_set_round_glyph_positions(context.get(), hint_metrics == CAIRO_HINT_METRICS_ON); } else { pango_context_set_round_glyph_positions(context.get(), false); } #endif /* VTE_GTK == 4 */ } if (G_UNLIKELY(s_font_info_for_context == nullptr)) s_font_info_for_context = g_hash_table_new((GHashFunc) context_hash, (GEqualFunc) context_equal); auto info = reinterpret_cast(g_hash_table_lookup(s_font_info_for_context, context.get())); if (G_LIKELY(info)) { _vte_debug_print (VTE_DEBUG_PANGOCAIRO, "vtepangocairo: %p found FontInfo in cache\n", info); info = info->ref(); } else { info = new FontInfo{std::move(context)}; } return info; } #if VTE_GTK == 3 FontInfo* FontInfo::create_for_screen(GdkScreen* screen, PangoFontDescription const* desc, PangoLanguage* language, cairo_font_options_t const* font_options) { auto settings = gtk_settings_get_for_screen(screen); auto fontconfig_timestamp = guint{}; g_object_get (settings, "gtk-fontconfig-timestamp", &fontconfig_timestamp, nullptr); return create_for_context(vte::glib::take_ref(gdk_pango_context_get_for_screen(screen)), desc, language, font_options, fontconfig_timestamp); } #endif /* VTE_GTK */ FontInfo* FontInfo::create_for_widget(GtkWidget* widget, PangoFontDescription const* desc, cairo_font_options_t const* font_options) { #if VTE_GTK == 3 auto screen = gtk_widget_get_screen(widget); return create_for_screen(screen, desc, nullptr, font_options); #elif VTE_GTK == 4 auto display = gtk_widget_get_display(widget); auto settings = gtk_settings_get_for_display(display); auto fontconfig_timestamp = guint{}; g_object_get (settings, "gtk-fontconfig-timestamp", &fontconfig_timestamp, nullptr); return create_for_context(vte::glib::take_ref(gtk_widget_create_pango_context(widget)), desc, nullptr, font_options, fontconfig_timestamp); // FIXMEgtk4: this uses a per-widget context, while the gtk3 code uses a per-screen // one. That means there may be a lot less sharing and a lot more FontInfo's around? #endif } FontInfo::UnistrInfo* FontInfo::get_unistr_info(vteunistr c) { PangoRectangle logical; PangoLayoutLine *line; auto uinfo = find_unistr_info(c); if (G_LIKELY (uinfo->coverage() != UnistrInfo::Coverage::UNKNOWN)) return uinfo; auto ufi = &uinfo->m_ufi; g_string_set_size(m_string, 0); _vte_unistr_append_to_string(c, m_string); pango_layout_set_text(m_layout.get(), m_string->str, m_string->len); pango_layout_get_extents(m_layout.get(), NULL, &logical); uinfo->width = PANGO_PIXELS_CEIL (logical.width); line = pango_layout_get_line_readonly(m_layout.get(), 0); uinfo->has_unknown_chars = pango_layout_get_unknown_glyphs_count(m_layout.get()) != 0; /* we use PangoLayoutRun rendering unless there is exactly one run in the line. */ if (G_UNLIKELY (!line || !line->runs || line->runs->next)) { uinfo->set_coverage(UnistrInfo::Coverage::USE_PANGO_LAYOUT_LINE); ufi->using_pango_layout_line.line = pango_layout_line_ref (line); } else { PangoGlyphItem *glyph_item = (PangoGlyphItem *)line->runs->data; PangoFont *pango_font = glyph_item->item->analysis.font; PangoGlyphString *glyph_string = glyph_item->glyphs; /* we use fast cairo path if glyph string has only one real * glyph and at origin */ if (!uinfo->has_unknown_chars && glyph_string->num_glyphs == 1 && glyph_string->glyphs[0].glyph <= 0xFFFF && (glyph_string->glyphs[0].geometry.x_offset | glyph_string->glyphs[0].geometry.y_offset) == 0) { cairo_scaled_font_t *scaled_font = pango_cairo_font_get_scaled_font ((PangoCairoFont *) pango_font); if (scaled_font) { uinfo->set_coverage(UnistrInfo::Coverage::USE_CAIRO_GLYPH); ufi->using_cairo_glyph.scaled_font = cairo_scaled_font_reference (scaled_font); ufi->using_cairo_glyph.glyph_index = glyph_string->glyphs[0].glyph; } } /* use pango fast path otherwise */ if (G_UNLIKELY (uinfo->coverage() == UnistrInfo::Coverage::UNKNOWN)) { uinfo->set_coverage(UnistrInfo::Coverage::USE_PANGO_GLYPH_STRING); ufi->using_pango_glyph_string.font = pango_font ? (PangoFont *)g_object_ref (pango_font) : NULL; ufi->using_pango_glyph_string.glyph_string = pango_glyph_string_copy (glyph_string); } /* release internal layout resources */ pango_layout_set_text(m_layout.get(), "", -1); } #if VTE_DEBUG m_coverage_count[0]++; m_coverage_count[uinfo->m_coverage]++; #endif return uinfo; } } // namespace view } // namespace vte