diff options
Diffstat (limited to 'pango2')
156 files changed, 58087 insertions, 0 deletions
diff --git a/pango2/break-arabic.c b/pango2/break-arabic.c new file mode 100644 index 00000000..2368586b --- /dev/null +++ b/pango2/break-arabic.c @@ -0,0 +1,91 @@ +/* Pango2 + * break-arabic.c: + * + * Copyright (C) 2006 Red Hat Software + * Copyright (C) 2006 Sharif FarsiWeb, Inc. + * Authors: Behdad Esfahbod <besfahbo@redhat.com> + * Roozbeh Pournader <roozbeh@farsiweb.info> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include "pango-break.h" + +#define ALEF_WITH_MADDA_ABOVE 0x0622 +#define YEH_WITH_HAMZA_ABOVE 0x0626 +#define ALEF 0x0627 +#define WAW 0x0648 +#define YEH 0x064A + +#define MADDAH_ABOVE 0x0653 +#define HAMZA_ABOVE 0x0654 +#define HAMZA_BELOW 0x0655 + +/* + * Arabic characters with canonical decompositions that are not just + * ligatures. The characters U+06C0, U+06C2, and U+06D3 are intentionally + * excluded as they are marked as "not an independent letter" in Unicode + * Character Database's NamesList.txt + */ +#define IS_COMPOSITE(c) (ALEF_WITH_MADDA_ABOVE <= (c) && (c) <= YEH_WITH_HAMZA_ABOVE) + +/* If a character is the second part of a composite Arabic character with an Alef */ +#define IS_COMPOSITE_WITH_ALEF(c) (MADDAH_ABOVE <= (c) && (c) <= HAMZA_BELOW) + +static void +break_arabic (const char *text, + int length, + const Pango2Analysis *analysis G_GNUC_UNUSED, + Pango2LogAttr *attrs, + int attrs_len G_GNUC_UNUSED) +{ + int i; + const char *p; + gunichar prev_wc, this_wc; + + /* See http://bugzilla.gnome.org/show_bug.cgi?id=350132 for issues this + * module tries to solve. + */ + + for (p = text, i = 0, prev_wc = 0; + p < text + length; + p = g_utf8_next_char (p), i++, prev_wc = this_wc) + { + this_wc = g_utf8_get_char (p); + + /* + * Unset backspace_deletes_character for various combinations. + * + * A few more combinations may need to be handled here, but are not + * handled yet, as expectations of users is not known or may differ + * among different languages or users: + * some letters combined with U+0658 ARABIC MARK NOON GHUNNA; + * combinations considered one letter in Azerbaijani (WAW+SUKUN and + * FARSI_YEH+HAMZA_ABOVE); combinations of YEH and ALEF_MAKSURA with + * HAMZA_BELOW (Qur'anic); TATWEEL+HAMZA_ABOVE (Qur'anic). + * + * FIXME: Ordering these in some other way may lower the time spent here, or not. + */ + if (G_UNLIKELY ( + IS_COMPOSITE (this_wc) || + (prev_wc == ALEF && IS_COMPOSITE_WITH_ALEF (this_wc)) || + (this_wc == HAMZA_ABOVE && (prev_wc == WAW || prev_wc == YEH)) + )) + attrs[i+1].backspace_deletes_character = FALSE; + } +} diff --git a/pango2/break-indic.c b/pango2/break-indic.c new file mode 100644 index 00000000..62ecdaab --- /dev/null +++ b/pango2/break-indic.c @@ -0,0 +1,212 @@ +/* Pango2 + * break-indic.c: + * + * Copyright (C) 2006 Red Hat Software + * Author: Akira TAGOH <tagoh@redhat.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include "pango-break.h" +#include "pango-item-private.h" + +#define DEV_RRA 0x0931 /* 0930 + 093c */ +#define DEV_QA 0x0958 /* 0915 + 093c */ +#define DEV_YA 0x095F /* 092f + 003c */ +#define DEV_KHHA 0x0959 +#define DEV_GHHA 0x095A +#define DEV_ZA 0x095B +#define DEV_DDDHA 0x095C +#define DEV_RHA 0x095D +#define DEV_FA 0x095E +#define DEV_YYA 0x095F + +/* Bengali */ +/* for split matras in all brahmi based script */ +#define BENGALI_SIGN_O 0x09CB /* 09c7 + 09be */ +#define BENGALI_SIGN_AU 0x09CC /* 09c7 + 09d7 */ +#define BENGALI_RRA 0x09DC +#define BENGALI_RHA 0x09DD +#define BENGALI_YYA 0x09DF + +/* Gurumukhi */ +#define GURUMUKHI_LLA 0x0A33 +#define GURUMUKHI_SHA 0x0A36 +#define GURUMUKHI_KHHA 0x0A59 +#define GURUMUKHI_GHHA 0x0A5A +#define GURUMUKHI_ZA 0x0A5B +#define GURUMUKHI_RRA 0x0A5C +#define GURUMUKHI_FA 0x0A5E + +/* Oriya */ +#define ORIYA_AI 0x0B48 +#define ORIYA_O 0x0B4B +#define ORIYA_AU 0x0B4C + +/* Telugu */ +#define TELUGU_EE 0x0C47 +#define TELUGU_AI 0x0C48 + +/* Tamil */ +#define TAMIL_O 0x0BCA +#define TAMIL_OO 0x0BCB +#define TAMIL_AU 0x0BCC + +/* Kannada */ +#define KNDA_EE 0x0CC7 +#define KNDA_AI 0x0CC8 +#define KNDA_O 0x0CCA +#define KNDA_OO 0x0CCB + +/* Malayalam */ +#define MLYM_O 0x0D4A +#define MLYM_OO 0x0D4B +#define MLYM_AU 0x0D4C + +#define IS_COMPOSITE_WITH_BRAHMI_NUKTA(c) ( \ + (c >= BENGALI_RRA && c <= BENGALI_YYA) || \ + (c >= DEV_QA && c <= DEV_YA) || (c == DEV_RRA) || (c >= DEV_KHHA && c <= DEV_YYA) || \ + (c >= KNDA_EE && c <= KNDA_AI) ||(c >= KNDA_O && c <= KNDA_OO) || \ + (c == TAMIL_O) || (c == TAMIL_OO) || (c == TAMIL_AU) || \ + (c == TELUGU_EE) || (c == TELUGU_AI) || \ + (c == ORIYA_AI) || (c == ORIYA_O) || (c == ORIYA_AU) || \ + (c >= GURUMUKHI_KHHA && c <= GURUMUKHI_RRA) || (c == GURUMUKHI_FA)|| (c == GURUMUKHI_LLA)|| (c == GURUMUKHI_SHA) || \ + FALSE) +#define IS_SPLIT_MATRA_BRAHMI(c) ( \ + (c == BENGALI_SIGN_O) || (c == BENGALI_SIGN_AU) || \ + (c >= MLYM_O && c <= MLYM_AU) || \ + FALSE) + +static void +not_cursor_position (Pango2LogAttr *attr) +{ + if (!attr->is_mandatory_break) + { + attr->is_cursor_position = FALSE; + attr->is_char_break = FALSE; + attr->is_line_break = FALSE; + attr->is_mandatory_break = FALSE; + } +} + +static void +break_indic (const char *text, + int length, + const Pango2Analysis *analysis, + Pango2LogAttr *attrs, + int attrs_len G_GNUC_UNUSED) +{ + const char *p, *next = NULL, *next_next; + gunichar prev_wc, this_wc, next_wc, next_next_wc; + gboolean is_conjunct = FALSE; + int i; + + for (p = text, prev_wc = 0, i = 0; + p != NULL && p < (text + length); + p = next, prev_wc = this_wc, i++) + { + this_wc = g_utf8_get_char (p); + next = g_utf8_next_char (p); + + if (G_UNLIKELY ( + IS_COMPOSITE_WITH_BRAHMI_NUKTA(this_wc) || IS_SPLIT_MATRA_BRAHMI(this_wc))) { + attrs[i+1].backspace_deletes_character = FALSE; + } + + if (next != NULL && next < (text + length)) + { + next_wc = g_utf8_get_char (next); + next_next = g_utf8_next_char (next); + } + else + { + next_wc = 0; + next_next = NULL; + } + if (next_next != NULL && next_next < (text + length)) + next_next_wc = g_utf8_get_char (next_next); + else + next_next_wc = 0; + + switch (analysis->script) + { + case G_UNICODE_SCRIPT_SINHALA: + /* + * TODO: The cursor position should be based on the state table. + * This is the wrong place to be doing this. + */ + + /* + * The cursor should treat as a single glyph: + * SINHALA CONS + 0x0DCA + 0x200D + SINHALA CONS + * SINHALA CONS + 0x200D + 0x0DCA + SINHALA CONS + */ + if ((this_wc == 0x0DCA && next_wc == 0x200D) + || (this_wc == 0x200D && next_wc == 0x0DCA)) + { + not_cursor_position(&attrs[i]); + not_cursor_position(&attrs[i + 1]); + is_conjunct = TRUE; + } + else if (is_conjunct + && (prev_wc == 0x200D || prev_wc == 0x0DCA) + && this_wc >= 0x0D9A + && this_wc <= 0x0DC6) + { + not_cursor_position(&attrs[i]); + is_conjunct = FALSE; + } + /* + * Consonant clusters do NOT result in implicit conjuncts + * in SINHALA orthography. + */ + else if (!is_conjunct && prev_wc == 0x0DCA && this_wc != 0x200D) + { + attrs[i].is_cursor_position = TRUE; + } + + break; + + default: + + if (prev_wc != 0 && (this_wc == 0x200D || this_wc == 0x200C)) + { + not_cursor_position(&attrs[i]); + if (next_wc != 0) + { + not_cursor_position(&attrs[i+1]); + if ((next_next_wc != 0) && + (next_wc == 0x09CD || /* Bengali */ + next_wc == 0x0ACD || /* Gujarati */ + next_wc == 0x094D || /* Hindi */ + next_wc == 0x0CCD || /* Kannada */ + next_wc == 0x0D4D || /* Malayalam */ + next_wc == 0x0B4D || /* Oriya */ + next_wc == 0x0A4D || /* Punjabi */ + next_wc == 0x0BCD || /* Tamil */ + next_wc == 0x0C4D)) /* Telugu */ + { + not_cursor_position(&attrs[i+2]); + } + } + } + + break; + } + } +} diff --git a/pango2/break-latin.c b/pango2/break-latin.c new file mode 100644 index 00000000..ffbc8f9e --- /dev/null +++ b/pango2/break-latin.c @@ -0,0 +1,60 @@ +/* Pango2 + * break-latin.c: + * + * Copyright (C) 2021 Jordi Mas i Hernàndez <jmas@softcatala.org> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "config.h" + +#include "pango-break.h" +#include "pango-item-private.h" +#include "pango-impl-utils.h" + +static void +break_latin (const char *text, + int length, + const Pango2Analysis *analysis, + Pango2LogAttr *attrs, + int attrs_len G_GNUC_UNUSED) +{ + int i; + const char *p, *next; + gunichar wc, prev_wc; + + if (!analysis || !analysis->language || + g_ascii_strncasecmp (pango2_language_to_string (analysis->language), "ca-", 3) != 0) + return; + + for (p = text, i = 0, prev_wc = 0; + p < text + length; + p = next, i++, prev_wc = wc) + { + wc = g_utf8_get_char (p); + next = g_utf8_next_char (p); + + /* Catalan middle dot does not break words */ + if (wc == 0x00b7) + { + gunichar middle_next = g_utf8_get_char (next); + if (g_unichar_tolower (middle_next) == 'l' && g_unichar_tolower (prev_wc) == 'l') + { + attrs[i].is_word_end = FALSE; + attrs[i+1].is_word_start = FALSE; + } + } + } +} diff --git a/pango2/break-thai.c b/pango2/break-thai.c new file mode 100644 index 00000000..ce38cfc1 --- /dev/null +++ b/pango2/break-thai.c @@ -0,0 +1,124 @@ +/* Pango2 + * break-thai.c: + * + * Copyright (C) 2003 Theppitak Karoonboonyanan <thep@linux.thai.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "config.h" + +#include "pango-break.h" +#include "pango-impl-utils.h" + +#ifdef HAVE_LIBTHAI +#include <thai/thwchar.h> +#include <thai/thbrk.h> + +G_LOCK_DEFINE_STATIC (thai_brk); + +#ifdef HAVE_TH_BRK_FIND_BREAKS +static ThBrk *thai_brk = NULL; +#endif + +/* + * tis_text is assumed to be large enough to hold the converted string, + * i.e. it must be at least pango2_utf8_strlen(text, len)+1 bytes. + */ +static thchar_t * +utf8_to_tis (const char *text, int len, thchar_t *tis_text, int *tis_cnt) +{ + thchar_t *tis_p; + const char *text_p; + + tis_p = tis_text; + for (text_p = text; text_p < text + len; text_p = g_utf8_next_char (text_p)) + *tis_p++ = th_uni2tis (g_utf8_get_char (text_p)); + *tis_p++ = '\0'; + + *tis_cnt = tis_p - tis_text; + return tis_text; +} + +#endif +static void +break_thai (const char *text, + int len, + const Pango2Analysis *analysis G_GNUC_UNUSED, + Pango2LogAttr *attrs, + int attrs_len G_GNUC_UNUSED) +{ +#ifdef HAVE_LIBTHAI + thchar_t tis_stack[512]; + int brk_stack[512]; + thchar_t *tis_text; + int *brk_pnts; + int cnt; + + cnt = pango2_utf8_strlen (text, len) + 1; + + tis_text = tis_stack; + if (cnt > (int) G_N_ELEMENTS (tis_stack)) + tis_text = g_new (thchar_t, cnt); + + utf8_to_tis (text, len, tis_text, &cnt); + + brk_pnts = brk_stack; + if (cnt > (int) G_N_ELEMENTS (brk_stack)) + brk_pnts = g_new (int, cnt); + + /* find line break positions */ + + G_LOCK (thai_brk); +#ifdef HAVE_TH_BRK_FIND_BREAKS + if (thai_brk == NULL) + thai_brk = th_brk_new(NULL); + len = th_brk_find_breaks(thai_brk, tis_text, brk_pnts, cnt); +#else + len = th_brk (tis_text, brk_pnts, cnt); +#endif + G_UNLOCK (thai_brk); + + for (cnt = 0; cnt < len; cnt++) + { + if (!attrs[brk_pnts[cnt]].is_line_break) + { + /* Insert line breaks where there wasn't one. + * Satisfy invariants by marking it as char break too. + */ + attrs[brk_pnts[cnt]].is_char_break = TRUE; + attrs[brk_pnts[cnt]].is_line_break = TRUE; + } + if (!(attrs[brk_pnts[cnt]].is_word_start || + attrs[brk_pnts[cnt]].is_word_end)) + { + /* If we find a break in the middle of a sequence + * of characters, end and start a word. We must + * be careful only to do that if default_break + * did not already find a word start or end, + * otherwise we mess up the sequence. + */ + attrs[brk_pnts[cnt]].is_word_start = TRUE; + attrs[brk_pnts[cnt]].is_word_end = TRUE; + } + } + + if (brk_pnts != brk_stack) + g_free (brk_pnts); + + if (tis_text != tis_stack) + g_free (tis_text); +#endif +} diff --git a/pango2/break.c b/pango2/break.c new file mode 100644 index 00000000..404f3058 --- /dev/null +++ b/pango2/break.c @@ -0,0 +1,2359 @@ +/* Pango2 + * break.c: + * + * Copyright (C) 1999 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include "pango-break.h" +#include "pango-script-private.h" +#include "pango-emoji-private.h" +#include "pango-attributes.h" +#include "pango-attr-private.h" +#include "pango-attr-list-private.h" +#include "pango-attr-iterator-private.h" +#include "pango-break-table.h" +#include "pango-item-private.h" +#include "pango-impl-utils.h" +#include <string.h> + +/* {{{ Unicode line breaking and segmentation */ + +#define PARAGRAPH_SEPARATOR 0x2029 + +/* See http://www.unicode.org/unicode/reports/tr14/ if you hope + * to understand the line breaking code. + */ + +typedef enum +{ + BREAK_ALREADY_HANDLED, /* didn't use the table */ + BREAK_PROHIBITED, /* no break, even if spaces intervene */ + BREAK_IF_SPACES, /* "indirect break" (only if there are spaces) */ + BREAK_ALLOWED /* "direct break" (can always break here) */ + /* TR 14 has two more break-opportunity classes, + * "indirect break opportunity for combining marks following a space" + * and "prohibited break for combining marks" + * but we handle that inline in the code. + */ +} BreakOpportunity; + +/* need to sync the break range to glib/gunicode.h . */ +#define BREAK_TYPE_SAFE(btype) \ + ((btype) <= G_UNICODE_BREAK_ZERO_WIDTH_JOINER ? (btype) : G_UNICODE_BREAK_UNKNOWN) + + +/* + * Hangul Conjoining Jamo handling. + * + * The way we implement it is just a bit different from TR14, + * but produces the same results. + * The same algorithm is also used in TR29 for cluster boundaries. + * + */ + + +/* An enum that works as the states of the Hangul syllables system. + **/ +typedef enum +{ + JAMO_L, /* G_UNICODE_BREAK_HANGUL_L_JAMO */ + JAMO_V, /* G_UNICODE_BREAK_HANGUL_V_JAMO */ + JAMO_T, /* G_UNICODE_BREAK_HANGUL_T_JAMO */ + JAMO_LV, /* G_UNICODE_BREAK_HANGUL_LV_SYLLABLE */ + JAMO_LVT, /* G_UNICODE_BREAK_HANGUL_LVT_SYLLABLE */ + NO_JAMO /* Other */ +} JamoType; + +/* There are Hangul syllables encoded as characters, that act like a + * sequence of Jamos. For each character we define a JamoType + * that the character starts with, and one that it ends with. This + * decomposes JAMO_LV and JAMO_LVT to simple other JAMOs. So for + * example, a character with LineBreak type + * G_UNICODE_BREAK_HANGUL_LV_SYLLABLE has start=JAMO_L and end=JAMO_V. + */ +typedef struct _CharJamoProps +{ + JamoType start, end; +} CharJamoProps; + +/* Map from JamoType to CharJamoProps that hold only simple + * JamoTypes (no LV or LVT) or none. + */ +static const CharJamoProps HangulJamoProps[] = { + {JAMO_L, JAMO_L}, /* JAMO_L */ + {JAMO_V, JAMO_V}, /* JAMO_V */ + {JAMO_T, JAMO_T}, /* JAMO_T */ + {JAMO_L, JAMO_V}, /* JAMO_LV */ + {JAMO_L, JAMO_T}, /* JAMO_LVT */ + {NO_JAMO, NO_JAMO} /* NO_JAMO */ +}; + +/* A character forms a syllable with the previous character if and only if: + * JamoType(this) is not NO_JAMO and: + * + * HangulJamoProps[JamoType(prev)].end and + * HangulJamoProps[JamoType(this)].start are equal, + * or the former is one less than the latter. + */ + +#define IS_JAMO(btype) \ + ((btype >= G_UNICODE_BREAK_HANGUL_L_JAMO) && \ + (btype <= G_UNICODE_BREAK_HANGUL_LVT_SYLLABLE)) +#define JAMO_TYPE(btype) \ + (IS_JAMO(btype) ? (btype - G_UNICODE_BREAK_HANGUL_L_JAMO) : NO_JAMO) + +/* Types of Japanese characters */ +#define JAPANESE(wc) ((wc) >= 0x2F00 && (wc) <= 0x30FF) +#define KANJI(wc) ((wc) >= 0x2F00 && (wc) <= 0x2FDF) +#define HIRAGANA(wc) ((wc) >= 0x3040 && (wc) <= 0x309F) +#define KATAKANA(wc) ((wc) >= 0x30A0 && (wc) <= 0x30FF) + +#define LATIN(wc) (((wc) >= 0x0020 && (wc) <= 0x02AF) || ((wc) >= 0x1E00 && (wc) <= 0x1EFF)) +#define CYRILLIC(wc) (((wc) >= 0x0400 && (wc) <= 0x052F)) +#define GREEK(wc) (((wc) >= 0x0370 && (wc) <= 0x3FF) || ((wc) >= 0x1F00 && (wc) <= 0x1FFF)) +#define KANA(wc) ((wc) >= 0x3040 && (wc) <= 0x30FF) +#define HANGUL(wc) ((wc) >= 0xAC00 && (wc) <= 0xD7A3) +#define EMOJI(wc) (_pango2_Is_Emoji_Base_Character (wc)) +#define BACKSPACE_DELETES_CHARACTER(wc) (!LATIN (wc) && !CYRILLIC (wc) && !GREEK (wc) && !KANA (wc) && !HANGUL (wc) && !EMOJI (wc)) + +/* Previously "123foo" was two words. But in UAX 29 of Unicode, + * we know don't break words between consecutive letters and numbers + */ +typedef enum +{ + WordNone, + WordLetters, + WordNumbers +} WordType; + +static void +default_break (const char *text, + int length, + Pango2LogAttr *attrs, + int attrs_len G_GNUC_UNUSED) +{ + /* The rationale for all this is in section 5.15 of the Unicode 3.0 book, + * the line breaking stuff is also in TR14 on unicode.org + */ + + /* This is a default break implementation that should work for nearly all + * languages. Language engines can override it optionally. + */ + + /* FIXME one cheesy optimization here would be to memset attrs to 0 + * before we start, and then never assign %FALSE to anything + */ + + const char *next; + int i; + + gunichar prev_wc; + gunichar next_wc; + + JamoType prev_jamo; + + GUnicodeBreakType next_break_type; + GUnicodeBreakType prev_break_type; + GUnicodeBreakType prev_prev_break_type; + + GUnicodeScript prev_script; + + /* See Grapheme_Cluster_Break Property Values table of UAX#29 */ + typedef enum + { + GB_Other, + GB_ControlCRLF, + GB_Extend, + GB_ZWJ, + GB_Prepend, + GB_SpacingMark, + GB_InHangulSyllable, /* Handles all of L, V, T, LV, LVT rules */ + /* Use state machine to handle emoji sequence */ + /* Rule GB12 and GB13 */ + GB_RI_Odd, /* Meets odd number of RI */ + GB_RI_Even, /* Meets even number of RI */ + } GraphemeBreakType; + GraphemeBreakType prev_GB_type = GB_Other; + gboolean met_Extended_Pictographic = FALSE; + + /* See Word_Break Property Values table of UAX#29 */ + typedef enum + { + WB_Other, + WB_NewlineCRLF, + WB_ExtendFormat, + WB_Katakana, + WB_Hebrew_Letter, + WB_ALetter, + WB_MidNumLet, + WB_MidLetter, + WB_MidNum, + WB_Numeric, + WB_ExtendNumLet, + WB_RI_Odd, + WB_RI_Even, + WB_WSegSpace, + } WordBreakType; + WordBreakType prev_prev_WB_type = WB_Other, prev_WB_type = WB_Other; + int prev_WB_i = -1; + + /* See Sentence_Break Property Values table of UAX#29 */ + typedef enum + { + SB_Other, + SB_ExtendFormat, + SB_ParaSep, + SB_Sp, + SB_Lower, + SB_Upper, + SB_OLetter, + SB_Numeric, + SB_ATerm, + SB_SContinue, + SB_STerm, + SB_Close, + /* Rules SB8 and SB8a */ + SB_ATerm_Close_Sp, + SB_STerm_Close_Sp, + } SentenceBreakType; + SentenceBreakType prev_prev_SB_type = SB_Other, prev_SB_type = SB_Other; + int prev_SB_i = -1; + + /* Rule LB25 with Example 7 of Customization */ + typedef enum + { + LB_Other, + LB_Numeric, + LB_Numeric_Close, + LB_RI_Odd, + LB_RI_Even, + } LineBreakType; + LineBreakType prev_LB_type = LB_Other; + + WordType current_word_type = WordNone; + gunichar last_word_letter = 0; + gunichar base_character = 0; + + int last_sentence_start = -1; + int last_non_space = -1; + + gboolean prev_space_or_hyphen; + + gboolean almost_done = FALSE; + gboolean done = FALSE; + + g_return_if_fail (length == 0 || text != NULL); + g_return_if_fail (attrs != NULL); + + next = text; + + prev_break_type = G_UNICODE_BREAK_UNKNOWN; + prev_prev_break_type = G_UNICODE_BREAK_UNKNOWN; + prev_wc = 0; + prev_script = G_UNICODE_SCRIPT_COMMON; + prev_jamo = NO_JAMO; + prev_space_or_hyphen = FALSE; + + if (length == 0 || *text == '\0') + { + next_wc = PARAGRAPH_SEPARATOR; + almost_done = TRUE; + } + else + next_wc = g_utf8_get_char (next); + + next_break_type = g_unichar_break_type (next_wc); + next_break_type = BREAK_TYPE_SAFE (next_break_type); + + for (i = 0; !done ; i++) + { + GUnicodeType type; + gunichar wc; + GUnicodeBreakType break_type; + GUnicodeBreakType row_break_type; + BreakOpportunity break_op; + JamoType jamo; + gboolean makes_hangul_syllable; + + /* UAX#29 boundaries */ + gboolean is_grapheme_boundary; + gboolean is_word_boundary; + gboolean is_sentence_boundary; + + /* Emoji extended pictographics */ + gboolean is_Extended_Pictographic; + + GUnicodeScript script; + + wc = next_wc; + break_type = next_break_type; + + if (almost_done) + { + /* + * If we have already reached the end of @text g_utf8_next_char() + * may not increment next + */ + next_wc = 0; + next_break_type = G_UNICODE_BREAK_UNKNOWN; + done = TRUE; + } + else + { + next = g_utf8_next_char (next); + + if ((length >= 0 && next >= text + length) || *next == '\0') + { + /* This is how we fill in the last element (end position) of the + * attr array - assume there's a paragraph separators off the end + * of @text. + */ + next_wc = PARAGRAPH_SEPARATOR; + almost_done = TRUE; + } + else + next_wc = g_utf8_get_char (next); + + next_break_type = g_unichar_break_type (next_wc); + next_break_type = BREAK_TYPE_SAFE (next_break_type); + } + + type = g_unichar_type (wc); + jamo = JAMO_TYPE (break_type); + + /* Determine wheter this forms a Hangul syllable with prev. */ + if (jamo == NO_JAMO) + makes_hangul_syllable = FALSE; + else + { + JamoType prev_end = HangulJamoProps[prev_jamo].end ; + JamoType this_start = HangulJamoProps[ jamo].start; + + /* See comments before IS_JAMO */ + makes_hangul_syllable = (prev_end == this_start) || (prev_end + 1 == this_start); + } + + switch ((int)type) + { + case G_UNICODE_SPACE_SEPARATOR: + case G_UNICODE_LINE_SEPARATOR: + case G_UNICODE_PARAGRAPH_SEPARATOR: + attrs[i].is_white = TRUE; + break; + case G_UNICODE_CONTROL: + if (wc == '\t' || wc == '\n' || wc == '\r' || wc == '\f') + attrs[i].is_white = TRUE; + else + attrs[i].is_white = FALSE; + break; + default: + attrs[i].is_white = FALSE; + break; + } + + /* Just few spaces have variable width. So explicitly mark them. + */ + attrs[i].is_expandable_space = (0x0020 == wc || 0x00A0 == wc); + is_Extended_Pictographic = + _pango2_Is_Emoji_Extended_Pictographic (wc); + + + /* ---- UAX#29 Grapheme Boundaries ---- */ + { + GraphemeBreakType GB_type; + + /* Find the GraphemeBreakType of wc */ + GB_type = GB_Other; + switch ((int)type) + { + case G_UNICODE_FORMAT: + if (G_UNLIKELY (wc == 0x200C)) + { + GB_type = GB_Extend; + break; + } + if (G_UNLIKELY (wc == 0x200D)) + { + GB_type = GB_ZWJ; + break; + } + if (G_UNLIKELY((wc >= 0x600 && wc <= 0x605) || + wc == 0x6DD || + wc == 0x70F || + wc == 0x8E2 || + wc == 0x110BD || + wc == 0x110CD)) + { + GB_type = GB_Prepend; + break; + } + /* Tag chars */ + if (wc >= 0xE0020 && wc <= 0xE00FF) + { + GB_type = GB_Extend; + break; + } + G_GNUC_FALLTHROUGH; + case G_UNICODE_CONTROL: + case G_UNICODE_LINE_SEPARATOR: + case G_UNICODE_PARAGRAPH_SEPARATOR: + case G_UNICODE_SURROGATE: + GB_type = GB_ControlCRLF; + break; + + case G_UNICODE_UNASSIGNED: + /* Unassigned default ignorables */ + if ((wc >= 0xFFF0 && wc <= 0xFFF8) || + (wc >= 0xE0000 && wc <= 0xE0FFF)) + { + GB_type = GB_ControlCRLF; + break; + } + G_GNUC_FALLTHROUGH; + + case G_UNICODE_OTHER_LETTER: + if (makes_hangul_syllable) + GB_type = GB_InHangulSyllable; + + if (_pango2_is_Consonant_Preceding_Repha (wc) || + _pango2_is_Consonant_Prefixed (wc)) + GB_type = GB_Prepend; + break; + + case G_UNICODE_MODIFIER_LETTER: + if (wc >= 0xFF9E && wc <= 0xFF9F) + GB_type = GB_Extend; /* Other_Grapheme_Extend */ + break; + + case G_UNICODE_SPACING_MARK: + GB_type = GB_SpacingMark; /* SpacingMark */ + if (wc >= 0x0900) + { + if (wc == 0x09BE || wc == 0x09D7 || + wc == 0x0B3E || wc == 0x0B57 || wc == 0x0BBE || wc == 0x0BD7 || + wc == 0x0CC2 || wc == 0x0CD5 || wc == 0x0CD6 || + wc == 0x0D3E || wc == 0x0D57 || wc == 0x0DCF || wc == 0x0DDF || + wc == 0x1D165 || (wc >= 0x1D16E && wc <= 0x1D172)) + GB_type = GB_Extend; /* Other_Grapheme_Extend */ + } + break; + + case G_UNICODE_ENCLOSING_MARK: + case G_UNICODE_NON_SPACING_MARK: + GB_type = GB_Extend; /* Grapheme_Extend */ + break; + + case G_UNICODE_OTHER_SYMBOL: + if (G_UNLIKELY(wc >=0x1F1E6 && wc <=0x1F1FF)) + { + if (prev_GB_type == GB_RI_Odd) + GB_type = GB_RI_Even; + else + GB_type = GB_RI_Odd; + break; + } + break; + + case G_UNICODE_MODIFIER_SYMBOL: + /* Fitzpatrick modifiers */ + if (wc >= 0x1F3FB && wc <= 0x1F3FF) + GB_type = GB_Extend; + break; + + default: + break; + } + + /* Rule GB11 */ + if (met_Extended_Pictographic) + { + if (GB_type == GB_Extend) + met_Extended_Pictographic = TRUE; + else if (_pango2_Is_Emoji_Extended_Pictographic (prev_wc) && + GB_type == GB_ZWJ) + met_Extended_Pictographic = TRUE; + else if (prev_GB_type == GB_Extend && GB_type == GB_ZWJ) + met_Extended_Pictographic = TRUE; + else if (prev_GB_type == GB_ZWJ && is_Extended_Pictographic) + met_Extended_Pictographic = TRUE; + else + met_Extended_Pictographic = FALSE; + } + + /* Grapheme Cluster Boundary Rules */ + is_grapheme_boundary = TRUE; /* Rule GB999 */ + + /* We apply Rules GB1 and GB2 at the end of the function */ + if (wc == '\n' && prev_wc == '\r') + is_grapheme_boundary = FALSE; /* Rule GB3 */ + else if (prev_GB_type == GB_ControlCRLF || GB_type == GB_ControlCRLF) + is_grapheme_boundary = TRUE; /* Rules GB4 and GB5 */ + else if (GB_type == GB_InHangulSyllable) + is_grapheme_boundary = FALSE; /* Rules GB6, GB7, GB8 */ + else if (GB_type == GB_Extend) + is_grapheme_boundary = FALSE; /* Rule GB9 */ + else if (GB_type == GB_ZWJ) + is_grapheme_boundary = FALSE; /* Rule GB9 */ + else if (GB_type == GB_SpacingMark) + is_grapheme_boundary = FALSE; /* Rule GB9a */ + else if (prev_GB_type == GB_Prepend) + is_grapheme_boundary = FALSE; /* Rule GB9b */ + else if (is_Extended_Pictographic) + { /* Rule GB11 */ + if (prev_GB_type == GB_ZWJ && met_Extended_Pictographic) + is_grapheme_boundary = FALSE; + } + else if (prev_GB_type == GB_RI_Odd && GB_type == GB_RI_Even) + is_grapheme_boundary = FALSE; /* Rule GB12 and GB13 */ + + if (is_Extended_Pictographic) + met_Extended_Pictographic = TRUE; + + attrs[i].is_cursor_position = is_grapheme_boundary; + /* If this is a grapheme boundary, we have to decide if backspace + * deletes a character or the whole grapheme cluster */ + if (is_grapheme_boundary) + { + attrs[i].backspace_deletes_character = BACKSPACE_DELETES_CHARACTER (base_character); + + /* Dependent Vowels for Indic language */ + if (_pango2_is_Virama (prev_wc) || + _pango2_is_Vowel_Dependent (prev_wc)) + attrs[i].backspace_deletes_character = TRUE; + } + else + attrs[i].backspace_deletes_character = FALSE; + + prev_GB_type = GB_type; + } + + script = g_unichar_get_script (wc); + /* ---- UAX#29 Word Boundaries ---- */ + { + is_word_boundary = FALSE; + if (is_grapheme_boundary || + G_UNLIKELY(wc >=0x1F1E6 && wc <=0x1F1FF)) /* Rules WB3 and WB4 */ + { + WordBreakType WB_type; + + /* Find the WordBreakType of wc */ + WB_type = WB_Other; + + if (script == G_UNICODE_SCRIPT_KATAKANA) + WB_type = WB_Katakana; + + if (script == G_UNICODE_SCRIPT_HEBREW && type == G_UNICODE_OTHER_LETTER) + WB_type = WB_Hebrew_Letter; + + if (WB_type == WB_Other) + switch (wc >> 8) + { + case 0x30: + if (wc == 0x3031 || wc == 0x3032 || wc == 0x3033 || wc == 0x3034 || wc == 0x3035 || + wc == 0x309b || wc == 0x309c || wc == 0x30a0 || wc == 0x30fc) + WB_type = WB_Katakana; /* Katakana exceptions */ + break; + case 0xFF: + if (wc == 0xFF70) + WB_type = WB_Katakana; /* Katakana exceptions */ + else if (wc >= 0xFF9E && wc <= 0xFF9F) + WB_type = WB_ExtendFormat; /* Other_Grapheme_Extend */ + break; + case 0x05: + if (wc == 0x058A) + WB_type = WB_ALetter; /* ALetter exceptions */ + break; + default: + break; + } + + if (WB_type == WB_Other) + switch ((int) break_type) + { + case G_UNICODE_BREAK_NUMERIC: + if (wc != 0x066C) + WB_type = WB_Numeric; /* Numeric */ + break; + case G_UNICODE_BREAK_INFIX_SEPARATOR: + if (wc != 0x003A && wc != 0xFE13 && wc != 0x002E) + WB_type = WB_MidNum; /* MidNum */ + break; + default: + break; + } + + if (WB_type == WB_Other) + switch ((int) type) + { + case G_UNICODE_CONTROL: + if (wc != 0x000D && wc != 0x000A && wc != 0x000B && wc != 0x000C && wc != 0x0085) + break; + G_GNUC_FALLTHROUGH; + case G_UNICODE_LINE_SEPARATOR: + case G_UNICODE_PARAGRAPH_SEPARATOR: + WB_type = WB_NewlineCRLF; /* CR, LF, Newline */ + break; + + case G_UNICODE_FORMAT: + case G_UNICODE_SPACING_MARK: + case G_UNICODE_ENCLOSING_MARK: + case G_UNICODE_NON_SPACING_MARK: + WB_type = WB_ExtendFormat; /* Extend, Format */ + break; + + case G_UNICODE_CONNECT_PUNCTUATION: + WB_type = WB_ExtendNumLet; /* ExtendNumLet */ + break; + + case G_UNICODE_INITIAL_PUNCTUATION: + case G_UNICODE_FINAL_PUNCTUATION: + if (wc == 0x2018 || wc == 0x2019) + WB_type = WB_MidNumLet; /* MidNumLet */ + break; + case G_UNICODE_OTHER_PUNCTUATION: + if ((wc >= 0x055a && wc <= 0x055c) || + wc == 0x055e || wc == 0x05f3) + WB_type = WB_ALetter; /* ALetter */ + else if (wc == 0x0027 || wc == 0x002e || wc == 0x2024 || + wc == 0xfe52 || wc == 0xff07 || wc == 0xff0e) + WB_type = WB_MidNumLet; /* MidNumLet */ + else if (wc == 0x00b7 || wc == 0x05f4 || wc == 0x2027 || + wc == 0x003a || wc == 0x0387 || wc == 0x055f || + wc == 0xfe13 || wc == 0xfe55 || wc == 0xff1a) + WB_type = WB_MidLetter; /* MidLetter */ + else if (wc == 0x066c || + wc == 0xfe50 || wc == 0xfe54 || wc == 0xff0c || wc == 0xff1b) + WB_type = WB_MidNum; /* MidNum */ + break; + + case G_UNICODE_OTHER_SYMBOL: + if (wc >= 0x24B6 && wc <= 0x24E9) /* Other_Alphabetic */ + goto Alphabetic; + + if (G_UNLIKELY(wc >= 0x1F1E6 && wc <= 0x1F1FF)) + { + if (prev_WB_type == WB_RI_Odd) + WB_type = WB_RI_Even; + else + WB_type = WB_RI_Odd; + } + + break; + + case G_UNICODE_OTHER_LETTER: + case G_UNICODE_LETTER_NUMBER: + if (wc == 0x3006 || wc == 0x3007 || + (wc >= 0x3021 && wc <= 0x3029) || + (wc >= 0x3038 && wc <= 0x303A) || + (wc >= 0x3400 && wc <= 0x4DB5) || + (wc >= 0x4E00 && wc <= 0x9FC3) || + (wc >= 0xF900 && wc <= 0xFA2D) || + (wc >= 0xFA30 && wc <= 0xFA6A) || + (wc >= 0xFA70 && wc <= 0xFAD9) || + (wc >= 0x20000 && wc <= 0x2A6D6) || + (wc >= 0x2F800 && wc <= 0x2FA1D)) + break; /* ALetter exceptions: Ideographic */ + goto Alphabetic; + + case G_UNICODE_LOWERCASE_LETTER: + case G_UNICODE_MODIFIER_LETTER: + case G_UNICODE_TITLECASE_LETTER: + case G_UNICODE_UPPERCASE_LETTER: + Alphabetic: + if (break_type != G_UNICODE_BREAK_COMPLEX_CONTEXT && script != G_UNICODE_SCRIPT_HIRAGANA) + WB_type = WB_ALetter; /* ALetter */ + break; + default: + break; + } + + if (WB_type == WB_Other) + { + if (type == G_UNICODE_SPACE_SEPARATOR && + break_type != G_UNICODE_BREAK_NON_BREAKING_GLUE) + WB_type = WB_WSegSpace; + } + + /* Word Cluster Boundary Rules */ + + /* We apply Rules WB1 and WB2 at the end of the function */ + + if (prev_wc == 0x3031 && wc == 0x41) + g_debug ("Y %d %d", prev_WB_type, WB_type); + if (prev_WB_type == WB_NewlineCRLF && prev_WB_i + 1 == i) + { + /* The extra check for prev_WB_i is to correctly handle sequences like + * Newline ÷ Extend × Extend + * since we have not skipped ExtendFormat yet. + */ + is_word_boundary = TRUE; /* Rule WB3a */ + } + else if (WB_type == WB_NewlineCRLF) + is_word_boundary = TRUE; /* Rule WB3b */ + else if (prev_wc == 0x200D && is_Extended_Pictographic) + is_word_boundary = FALSE; /* Rule WB3c */ + else if (prev_WB_type == WB_WSegSpace && + WB_type == WB_WSegSpace && prev_WB_i + 1 == i) + is_word_boundary = FALSE; /* Rule WB3d */ + else if (WB_type == WB_ExtendFormat) + is_word_boundary = FALSE; /* Rules WB4? */ + else if ((prev_WB_type == WB_ALetter || + prev_WB_type == WB_Hebrew_Letter || + prev_WB_type == WB_Numeric) && + (WB_type == WB_ALetter || + WB_type == WB_Hebrew_Letter || + WB_type == WB_Numeric)) + is_word_boundary = FALSE; /* Rules WB5, WB8, WB9, WB10 */ + else if (prev_WB_type == WB_Katakana && WB_type == WB_Katakana) + is_word_boundary = FALSE; /* Rule WB13 */ + else if ((prev_WB_type == WB_ALetter || + prev_WB_type == WB_Hebrew_Letter || + prev_WB_type == WB_Numeric || + prev_WB_type == WB_Katakana || + prev_WB_type == WB_ExtendNumLet) && + WB_type == WB_ExtendNumLet) + is_word_boundary = FALSE; /* Rule WB13a */ + else if (prev_WB_type == WB_ExtendNumLet && + (WB_type == WB_ALetter || + WB_type == WB_Hebrew_Letter || + WB_type == WB_Numeric || + WB_type == WB_Katakana)) + is_word_boundary = FALSE; /* Rule WB13b */ + else if (((prev_prev_WB_type == WB_ALetter || + prev_prev_WB_type == WB_Hebrew_Letter) && + (WB_type == WB_ALetter || + WB_type == WB_Hebrew_Letter)) && + (prev_WB_type == WB_MidLetter || + prev_WB_type == WB_MidNumLet || + prev_wc == 0x0027)) + { + attrs[prev_WB_i].is_word_boundary = FALSE; /* Rule WB6 */ + is_word_boundary = FALSE; /* Rule WB7 */ + } + else if (prev_WB_type == WB_Hebrew_Letter && wc == 0x0027) + is_word_boundary = FALSE; /* Rule WB7a */ + else if (prev_prev_WB_type == WB_Hebrew_Letter && prev_wc == 0x0022 && + WB_type == WB_Hebrew_Letter) { + attrs[prev_WB_i].is_word_boundary = FALSE; /* Rule WB7b */ + is_word_boundary = FALSE; /* Rule WB7c */ + } + else if ((prev_prev_WB_type == WB_Numeric && WB_type == WB_Numeric) && + (prev_WB_type == WB_MidNum || prev_WB_type == WB_MidNumLet || + prev_wc == 0x0027)) + { + is_word_boundary = FALSE; /* Rule WB11 */ + attrs[prev_WB_i].is_word_boundary = FALSE; /* Rule WB12 */ + } + else if (prev_WB_type == WB_RI_Odd && WB_type == WB_RI_Even) + is_word_boundary = FALSE; /* Rule WB15 and WB16 */ + else + is_word_boundary = TRUE; /* Rule WB999 */ + + if (WB_type != WB_ExtendFormat) + { + prev_prev_WB_type = prev_WB_type; + prev_WB_type = WB_type; + prev_WB_i = i; + } + } + + attrs[i].is_word_boundary = is_word_boundary; + } + + /* ---- UAX#29 Sentence Boundaries ---- */ + { + is_sentence_boundary = FALSE; + if (is_word_boundary || + wc == '\r' || wc == '\n') /* Rules SB3 and SB5 */ + { + SentenceBreakType SB_type; + + /* Find the SentenceBreakType of wc */ + SB_type = SB_Other; + + if (break_type == G_UNICODE_BREAK_NUMERIC) + SB_type = SB_Numeric; /* Numeric */ + + if (SB_type == SB_Other) + switch ((int) type) + { + case G_UNICODE_CONTROL: + if (wc == '\r' || wc == '\n') + SB_type = SB_ParaSep; + else if (wc == 0x0009 || wc == 0x000B || wc == 0x000C) + SB_type = SB_Sp; + else if (wc == 0x0085) + SB_type = SB_ParaSep; + break; + + case G_UNICODE_SPACE_SEPARATOR: + if (wc == 0x0020 || wc == 0x00A0 || wc == 0x1680 || + (wc >= 0x2000 && wc <= 0x200A) || + wc == 0x202F || wc == 0x205F || wc == 0x3000) + SB_type = SB_Sp; + break; + + case G_UNICODE_LINE_SEPARATOR: + case G_UNICODE_PARAGRAPH_SEPARATOR: + SB_type = SB_ParaSep; + break; + + case G_UNICODE_FORMAT: + case G_UNICODE_SPACING_MARK: + case G_UNICODE_ENCLOSING_MARK: + case G_UNICODE_NON_SPACING_MARK: + SB_type = SB_ExtendFormat; /* Extend, Format */ + break; + + case G_UNICODE_MODIFIER_LETTER: + if (wc >= 0xFF9E && wc <= 0xFF9F) + SB_type = SB_ExtendFormat; /* Other_Grapheme_Extend */ + break; + + case G_UNICODE_TITLECASE_LETTER: + SB_type = SB_Upper; + break; + + case G_UNICODE_DASH_PUNCTUATION: + if (wc == 0x002D || + (wc >= 0x2013 && wc <= 0x2014) || + (wc >= 0xFE31 && wc <= 0xFE32) || + wc == 0xFE58 || + wc == 0xFE63 || + wc == 0xFF0D) + SB_type = SB_SContinue; + break; + + case G_UNICODE_OTHER_PUNCTUATION: + if (wc == 0x05F3) + SB_type = SB_OLetter; + else if (wc == 0x002E || wc == 0x2024 || + wc == 0xFE52 || wc == 0xFF0E) + SB_type = SB_ATerm; + + if (wc == 0x002C || + wc == 0x003A || + wc == 0x055D || + (wc >= 0x060C && wc <= 0x060D) || + wc == 0x07F8 || + wc == 0x1802 || + wc == 0x1808 || + wc == 0x3001 || + (wc >= 0xFE10 && wc <= 0xFE11) || + wc == 0xFE13 || + (wc >= 0xFE50 && wc <= 0xFE51) || + wc == 0xFE55 || + wc == 0xFF0C || + wc == 0xFF1A || + wc == 0xFF64) + SB_type = SB_SContinue; + + if (_pango2_is_STerm(wc)) + SB_type = SB_STerm; + + break; + + default: + break; + } + + if (SB_type == SB_Other) + { + if (type == G_UNICODE_LOWERCASE_LETTER) + SB_type = SB_Lower; + else if (type == G_UNICODE_UPPERCASE_LETTER) + SB_type = SB_Upper; + else if (type == G_UNICODE_TITLECASE_LETTER || + type == G_UNICODE_MODIFIER_LETTER || + type == G_UNICODE_OTHER_LETTER) + SB_type = SB_OLetter; + + if (type == G_UNICODE_OPEN_PUNCTUATION || + type == G_UNICODE_CLOSE_PUNCTUATION || + break_type == G_UNICODE_BREAK_QUOTATION) + SB_type = SB_Close; + } + + /* Sentence Boundary Rules */ + + /* We apply Rules SB1 and SB2 at the end of the function */ + +#define IS_OTHER_TERM(SB_type) \ + /* not in (OLetter | Upper | Lower | ParaSep | SATerm) */ \ + !(SB_type == SB_OLetter || \ + SB_type == SB_Upper || SB_type == SB_Lower || \ + SB_type == SB_ParaSep || \ + SB_type == SB_ATerm || SB_type == SB_STerm || \ + SB_type == SB_ATerm_Close_Sp || \ + SB_type == SB_STerm_Close_Sp) + + + if (wc == '\n' && prev_wc == '\r') + is_sentence_boundary = FALSE; /* Rule SB3 */ + else if (prev_SB_type == SB_ParaSep && prev_SB_i + 1 == i) + { + /* The extra check for prev_SB_i is to correctly handle sequences like + * ParaSep ÷ Extend × Extend + * since we have not skipped ExtendFormat yet. + */ + + is_sentence_boundary = TRUE; /* Rule SB4 */ + } + else if (SB_type == SB_ExtendFormat) + is_sentence_boundary = FALSE; /* Rule SB5? */ + else if (prev_SB_type == SB_ATerm && SB_type == SB_Numeric) + is_sentence_boundary = FALSE; /* Rule SB6 */ + else if ((prev_prev_SB_type == SB_Upper || + prev_prev_SB_type == SB_Lower) && + prev_SB_type == SB_ATerm && + SB_type == SB_Upper) + is_sentence_boundary = FALSE; /* Rule SB7 */ + else if (prev_SB_type == SB_ATerm && SB_type == SB_Close) + SB_type = SB_ATerm; + else if (prev_SB_type == SB_STerm && SB_type == SB_Close) + SB_type = SB_STerm; + else if (prev_SB_type == SB_ATerm && SB_type == SB_Sp) + SB_type = SB_ATerm_Close_Sp; + else if (prev_SB_type == SB_STerm && SB_type == SB_Sp) + SB_type = SB_STerm_Close_Sp; + /* Rule SB8 */ + else if ((prev_SB_type == SB_ATerm || + prev_SB_type == SB_ATerm_Close_Sp) && + SB_type == SB_Lower) + is_sentence_boundary = FALSE; + else if ((prev_prev_SB_type == SB_ATerm || + prev_prev_SB_type == SB_ATerm_Close_Sp) && + IS_OTHER_TERM(prev_SB_type) && + SB_type == SB_Lower) + { + attrs[prev_SB_i].is_sentence_boundary = FALSE; + attrs[prev_SB_i].is_sentence_end = FALSE; + last_sentence_start = -1; + for (int j = prev_SB_i - 1; j >= 0; j--) + { + attrs[j].is_sentence_end = FALSE; + if (attrs[j].is_sentence_boundary) + { + last_sentence_start = j; + break; + } + } + } + else if ((prev_SB_type == SB_ATerm || + prev_SB_type == SB_ATerm_Close_Sp || + prev_SB_type == SB_STerm || + prev_SB_type == SB_STerm_Close_Sp) && + (SB_type == SB_SContinue || + SB_type == SB_ATerm || SB_type == SB_STerm)) + is_sentence_boundary = FALSE; /* Rule SB8a */ + else if ((prev_SB_type == SB_ATerm || + prev_SB_type == SB_STerm) && + (SB_type == SB_Close || SB_type == SB_Sp || + SB_type == SB_ParaSep)) + is_sentence_boundary = FALSE; /* Rule SB9 */ + else if ((prev_SB_type == SB_ATerm || + prev_SB_type == SB_ATerm_Close_Sp || + prev_SB_type == SB_STerm || + prev_SB_type == SB_STerm_Close_Sp) && + (SB_type == SB_Sp || SB_type == SB_ParaSep)) + is_sentence_boundary = FALSE; /* Rule SB10 */ + else if ((prev_SB_type == SB_ATerm || + prev_SB_type == SB_ATerm_Close_Sp || + prev_SB_type == SB_STerm || + prev_SB_type == SB_STerm_Close_Sp) && + SB_type != SB_ParaSep) + is_sentence_boundary = TRUE; /* Rule SB11 */ + else + is_sentence_boundary = FALSE; /* Rule SB998 */ + + if (SB_type != SB_ExtendFormat && + !((prev_prev_SB_type == SB_ATerm || + prev_prev_SB_type == SB_ATerm_Close_Sp) && + IS_OTHER_TERM(prev_SB_type) && + IS_OTHER_TERM(SB_type))) + { + prev_prev_SB_type = prev_SB_type; + prev_SB_type = SB_type; + prev_SB_i = i; + } + +#undef IS_OTHER_TERM + + } + + if (i == 0 || done) + is_sentence_boundary = TRUE; /* Rules SB1 and SB2 */ + + attrs[i].is_sentence_boundary = is_sentence_boundary; + } + + /* ---- Line breaking ---- */ + + break_op = BREAK_ALREADY_HANDLED; + + row_break_type = prev_break_type == G_UNICODE_BREAK_SPACE ? + prev_prev_break_type : prev_break_type; + g_assert (row_break_type != G_UNICODE_BREAK_SPACE); + + attrs[i].is_char_break = FALSE; + attrs[i].is_line_break = FALSE; + attrs[i].is_mandatory_break = FALSE; + + /* Rule LB1: + assign a line breaking class to each code point of the input. */ + switch ((int)break_type) + { + case G_UNICODE_BREAK_AMBIGUOUS: + case G_UNICODE_BREAK_SURROGATE: + case G_UNICODE_BREAK_UNKNOWN: + break_type = G_UNICODE_BREAK_ALPHABETIC; + break; + + case G_UNICODE_BREAK_COMPLEX_CONTEXT: + if (type == G_UNICODE_NON_SPACING_MARK || + type == G_UNICODE_SPACING_MARK) + break_type = G_UNICODE_BREAK_COMBINING_MARK; + else + break_type = G_UNICODE_BREAK_ALPHABETIC; + break; + + case G_UNICODE_BREAK_CONDITIONAL_JAPANESE_STARTER: + break_type = G_UNICODE_BREAK_NON_STARTER; + break; + + default: + break; + } + + /* If it's not a grapheme boundary, it's not a line break either */ + if (attrs[i].is_cursor_position || + break_type == G_UNICODE_BREAK_COMBINING_MARK || + break_type == G_UNICODE_BREAK_ZERO_WIDTH_JOINER || + break_type == G_UNICODE_BREAK_HANGUL_L_JAMO || + break_type == G_UNICODE_BREAK_HANGUL_V_JAMO || + break_type == G_UNICODE_BREAK_HANGUL_T_JAMO || + break_type == G_UNICODE_BREAK_HANGUL_LV_SYLLABLE || + break_type == G_UNICODE_BREAK_HANGUL_LVT_SYLLABLE || + break_type == G_UNICODE_BREAK_EMOJI_MODIFIER || + break_type == G_UNICODE_BREAK_REGIONAL_INDICATOR) + { + LineBreakType LB_type; + + /* Find the LineBreakType of wc */ + LB_type = LB_Other; + + if (break_type == G_UNICODE_BREAK_NUMERIC) + LB_type = LB_Numeric; + + if (break_type == G_UNICODE_BREAK_SYMBOL || + break_type == G_UNICODE_BREAK_INFIX_SEPARATOR) + { + if (!(prev_LB_type == LB_Numeric)) + LB_type = LB_Other; + } + + if (break_type == G_UNICODE_BREAK_CLOSE_PUNCTUATION || + break_type == G_UNICODE_BREAK_CLOSE_PARANTHESIS) + { + if (prev_LB_type == LB_Numeric) + LB_type = LB_Numeric_Close; + else + LB_type = LB_Other; + } + + if (break_type == G_UNICODE_BREAK_REGIONAL_INDICATOR) + { + if (prev_LB_type == LB_RI_Odd) + LB_type = LB_RI_Even; + else + LB_type = LB_RI_Odd; + } + + attrs[i].is_line_break = TRUE; /* Rule LB31 */ + /* Unicode doesn't specify char wrap; + we wrap around all chars currently. */ + if (attrs[i].is_cursor_position) + attrs[i].is_char_break = TRUE; + + /* Make any necessary replacements first */ + if (row_break_type == G_UNICODE_BREAK_UNKNOWN) + row_break_type = G_UNICODE_BREAK_ALPHABETIC; + + /* add the line break rules in reverse order to override + the lower priority rules. */ + + /* Rule LB30 */ + if ((prev_break_type == G_UNICODE_BREAK_ALPHABETIC || + prev_break_type == G_UNICODE_BREAK_HEBREW_LETTER || + prev_break_type == G_UNICODE_BREAK_NUMERIC) && + break_type == G_UNICODE_BREAK_OPEN_PUNCTUATION && + !_pango2_is_EastAsianWide (wc)) + break_op = BREAK_PROHIBITED; + + if (prev_break_type == G_UNICODE_BREAK_CLOSE_PARANTHESIS && + !_pango2_is_EastAsianWide (prev_wc)&& + (break_type == G_UNICODE_BREAK_ALPHABETIC || + break_type == G_UNICODE_BREAK_HEBREW_LETTER || + break_type == G_UNICODE_BREAK_NUMERIC)) + break_op = BREAK_PROHIBITED; + + /* Rule LB30a */ + if (prev_LB_type == LB_RI_Odd && LB_type == LB_RI_Even) + break_op = BREAK_PROHIBITED; + + /* Rule LB30b */ + if (prev_break_type == G_UNICODE_BREAK_EMOJI_BASE && + break_type == G_UNICODE_BREAK_EMOJI_MODIFIER) + break_op = BREAK_PROHIBITED; + + if ((_pango2_Is_Emoji_Extended_Pictographic (prev_wc) && + g_unichar_type (prev_wc) == G_UNICODE_UNASSIGNED) && + break_type == G_UNICODE_BREAK_EMOJI_MODIFIER) + break_op = BREAK_PROHIBITED; + + /* Rule LB29 */ + if (prev_break_type == G_UNICODE_BREAK_INFIX_SEPARATOR && + (break_type == G_UNICODE_BREAK_ALPHABETIC || + break_type == G_UNICODE_BREAK_HEBREW_LETTER)) + break_op = BREAK_PROHIBITED; + + /* Rule LB28 */ + if ((prev_break_type == G_UNICODE_BREAK_ALPHABETIC || + prev_break_type == G_UNICODE_BREAK_HEBREW_LETTER) && + (break_type == G_UNICODE_BREAK_ALPHABETIC || + break_type == G_UNICODE_BREAK_HEBREW_LETTER)) + break_op = BREAK_PROHIBITED; + + /* Rule LB27 */ + if ((prev_break_type == G_UNICODE_BREAK_HANGUL_L_JAMO || + prev_break_type == G_UNICODE_BREAK_HANGUL_V_JAMO || + prev_break_type == G_UNICODE_BREAK_HANGUL_T_JAMO || + prev_break_type == G_UNICODE_BREAK_HANGUL_LV_SYLLABLE || + prev_break_type == G_UNICODE_BREAK_HANGUL_LVT_SYLLABLE) && + break_type == G_UNICODE_BREAK_POSTFIX) + break_op = BREAK_PROHIBITED; + + if (prev_break_type == G_UNICODE_BREAK_PREFIX && + (break_type == G_UNICODE_BREAK_HANGUL_L_JAMO || + break_type == G_UNICODE_BREAK_HANGUL_V_JAMO || + break_type == G_UNICODE_BREAK_HANGUL_T_JAMO || + break_type == G_UNICODE_BREAK_HANGUL_LV_SYLLABLE || + break_type == G_UNICODE_BREAK_HANGUL_LVT_SYLLABLE)) + break_op = BREAK_PROHIBITED; + + /* Rule LB26 */ + if (prev_break_type == G_UNICODE_BREAK_HANGUL_L_JAMO && + (break_type == G_UNICODE_BREAK_HANGUL_L_JAMO || + break_type == G_UNICODE_BREAK_HANGUL_V_JAMO || + break_type == G_UNICODE_BREAK_HANGUL_LV_SYLLABLE || + break_type == G_UNICODE_BREAK_HANGUL_LVT_SYLLABLE)) + break_op = BREAK_PROHIBITED; + + if ((prev_break_type == G_UNICODE_BREAK_HANGUL_V_JAMO || + prev_break_type == G_UNICODE_BREAK_HANGUL_LV_SYLLABLE) && + (break_type == G_UNICODE_BREAK_HANGUL_V_JAMO || + break_type == G_UNICODE_BREAK_HANGUL_T_JAMO)) + break_op = BREAK_PROHIBITED; + + if ((prev_break_type == G_UNICODE_BREAK_HANGUL_T_JAMO || + prev_break_type == G_UNICODE_BREAK_HANGUL_LVT_SYLLABLE) && + break_type == G_UNICODE_BREAK_HANGUL_T_JAMO) + break_op = BREAK_PROHIBITED; + + /* Rule LB25 with Example 7 of Customization */ + if ((prev_break_type == G_UNICODE_BREAK_PREFIX || + prev_break_type == G_UNICODE_BREAK_POSTFIX) && + break_type == G_UNICODE_BREAK_NUMERIC) + break_op = BREAK_PROHIBITED; + + if ((prev_break_type == G_UNICODE_BREAK_PREFIX || + prev_break_type == G_UNICODE_BREAK_POSTFIX) && + (break_type == G_UNICODE_BREAK_OPEN_PUNCTUATION || + break_type == G_UNICODE_BREAK_HYPHEN) && + next_break_type == G_UNICODE_BREAK_NUMERIC) + break_op = BREAK_PROHIBITED; + + if ((prev_break_type == G_UNICODE_BREAK_OPEN_PUNCTUATION || + prev_break_type == G_UNICODE_BREAK_HYPHEN) && + break_type == G_UNICODE_BREAK_NUMERIC) + break_op = BREAK_PROHIBITED; + + if (prev_break_type == G_UNICODE_BREAK_NUMERIC && + (break_type == G_UNICODE_BREAK_NUMERIC || + break_type == G_UNICODE_BREAK_SYMBOL || + break_type == G_UNICODE_BREAK_INFIX_SEPARATOR)) + break_op = BREAK_PROHIBITED; + + if (prev_LB_type == LB_Numeric && + (break_type == G_UNICODE_BREAK_NUMERIC || + break_type == G_UNICODE_BREAK_SYMBOL || + break_type == G_UNICODE_BREAK_INFIX_SEPARATOR || + break_type == G_UNICODE_BREAK_CLOSE_PUNCTUATION || + break_type == G_UNICODE_BREAK_CLOSE_PARANTHESIS)) + break_op = BREAK_PROHIBITED; + + if ((prev_LB_type == LB_Numeric || + prev_LB_type == LB_Numeric_Close) && + (break_type == G_UNICODE_BREAK_POSTFIX || + break_type == G_UNICODE_BREAK_PREFIX)) + break_op = BREAK_PROHIBITED; + + /* Rule LB24 */ + if ((prev_break_type == G_UNICODE_BREAK_PREFIX || + prev_break_type == G_UNICODE_BREAK_POSTFIX) && + (break_type == G_UNICODE_BREAK_ALPHABETIC || + break_type == G_UNICODE_BREAK_HEBREW_LETTER)) + break_op = BREAK_PROHIBITED; + + if ((prev_break_type == G_UNICODE_BREAK_ALPHABETIC || + prev_break_type == G_UNICODE_BREAK_HEBREW_LETTER) && + (break_type == G_UNICODE_BREAK_PREFIX || + break_type == G_UNICODE_BREAK_POSTFIX)) + break_op = BREAK_PROHIBITED; + + /* Rule LB23 */ + if ((prev_break_type == G_UNICODE_BREAK_ALPHABETIC || + prev_break_type == G_UNICODE_BREAK_HEBREW_LETTER) && + break_type == G_UNICODE_BREAK_NUMERIC) + break_op = BREAK_PROHIBITED; + + if (prev_break_type == G_UNICODE_BREAK_NUMERIC && + (break_type == G_UNICODE_BREAK_ALPHABETIC || + break_type == G_UNICODE_BREAK_HEBREW_LETTER)) + break_op = BREAK_PROHIBITED; + + /* Rule LB23a */ + if (prev_break_type == G_UNICODE_BREAK_PREFIX && + (break_type == G_UNICODE_BREAK_IDEOGRAPHIC || + break_type == G_UNICODE_BREAK_EMOJI_BASE || + break_type == G_UNICODE_BREAK_EMOJI_MODIFIER)) + break_op = BREAK_PROHIBITED; + + if ((prev_break_type == G_UNICODE_BREAK_IDEOGRAPHIC || + prev_break_type == G_UNICODE_BREAK_EMOJI_BASE || + prev_break_type == G_UNICODE_BREAK_EMOJI_MODIFIER) && + break_type == G_UNICODE_BREAK_POSTFIX) + break_op = BREAK_PROHIBITED; + + /* Rule LB22 */ + if (break_type == G_UNICODE_BREAK_INSEPARABLE) + break_op = BREAK_PROHIBITED; + + if (break_type == G_UNICODE_BREAK_AFTER || + break_type == G_UNICODE_BREAK_HYPHEN || + break_type == G_UNICODE_BREAK_NON_STARTER || + prev_break_type == G_UNICODE_BREAK_BEFORE) + break_op = BREAK_PROHIBITED; /* Rule LB21 */ + + if (prev_prev_break_type == G_UNICODE_BREAK_HEBREW_LETTER && + (prev_break_type == G_UNICODE_BREAK_HYPHEN || + prev_break_type == G_UNICODE_BREAK_AFTER)) + break_op = BREAK_PROHIBITED; /* Rule LB21a */ + + if (prev_break_type == G_UNICODE_BREAK_SYMBOL && + break_type == G_UNICODE_BREAK_HEBREW_LETTER) + break_op = BREAK_PROHIBITED; /* Rule LB21b */ + + if (prev_break_type == G_UNICODE_BREAK_CONTINGENT || + break_type == G_UNICODE_BREAK_CONTINGENT) + break_op = BREAK_ALLOWED; /* Rule LB20 */ + + if (prev_break_type == G_UNICODE_BREAK_QUOTATION || + break_type == G_UNICODE_BREAK_QUOTATION) + break_op = BREAK_PROHIBITED; /* Rule LB19 */ + + /* handle related rules for Space as state machine here, + and override the pair table result. */ + if (prev_break_type == G_UNICODE_BREAK_SPACE) /* Rule LB18 */ + break_op = BREAK_ALLOWED; + + if (row_break_type == G_UNICODE_BREAK_BEFORE_AND_AFTER && + break_type == G_UNICODE_BREAK_BEFORE_AND_AFTER) + break_op = BREAK_PROHIBITED; /* Rule LB17 */ + + if ((row_break_type == G_UNICODE_BREAK_CLOSE_PUNCTUATION || + row_break_type == G_UNICODE_BREAK_CLOSE_PARANTHESIS) && + break_type == G_UNICODE_BREAK_NON_STARTER) + break_op = BREAK_PROHIBITED; /* Rule LB16 */ + + if (row_break_type == G_UNICODE_BREAK_QUOTATION && + break_type == G_UNICODE_BREAK_OPEN_PUNCTUATION) + break_op = BREAK_PROHIBITED; /* Rule LB15 */ + + if (row_break_type == G_UNICODE_BREAK_OPEN_PUNCTUATION) + break_op = BREAK_PROHIBITED; /* Rule LB14 */ + + /* Rule LB13 with Example 7 of Customization */ + if (break_type == G_UNICODE_BREAK_EXCLAMATION) + break_op = BREAK_PROHIBITED; + + if (prev_break_type != G_UNICODE_BREAK_NUMERIC && + (break_type == G_UNICODE_BREAK_CLOSE_PUNCTUATION || + break_type == G_UNICODE_BREAK_CLOSE_PARANTHESIS || + break_type == G_UNICODE_BREAK_INFIX_SEPARATOR || + break_type == G_UNICODE_BREAK_SYMBOL)) + break_op = BREAK_PROHIBITED; + + if (prev_break_type == G_UNICODE_BREAK_NON_BREAKING_GLUE) + break_op = BREAK_PROHIBITED; /* Rule LB12 */ + + if (break_type == G_UNICODE_BREAK_NON_BREAKING_GLUE && + (prev_break_type != G_UNICODE_BREAK_SPACE && + prev_break_type != G_UNICODE_BREAK_AFTER && + prev_break_type != G_UNICODE_BREAK_HYPHEN)) + break_op = BREAK_PROHIBITED; /* Rule LB12a */ + + if (prev_break_type == G_UNICODE_BREAK_WORD_JOINER || + break_type == G_UNICODE_BREAK_WORD_JOINER) + break_op = BREAK_PROHIBITED; /* Rule LB11 */ + + + /* Rule LB9 */ + if (break_type == G_UNICODE_BREAK_COMBINING_MARK || + break_type == G_UNICODE_BREAK_ZERO_WIDTH_JOINER) + { + if (!(prev_break_type == G_UNICODE_BREAK_MANDATORY || + prev_break_type == G_UNICODE_BREAK_CARRIAGE_RETURN || + prev_break_type == G_UNICODE_BREAK_LINE_FEED || + prev_break_type == G_UNICODE_BREAK_NEXT_LINE || + prev_break_type == G_UNICODE_BREAK_SPACE || + prev_break_type == G_UNICODE_BREAK_ZERO_WIDTH_SPACE)) + break_op = BREAK_PROHIBITED; + } + + if (row_break_type == G_UNICODE_BREAK_ZERO_WIDTH_SPACE) + break_op = BREAK_ALLOWED; /* Rule LB8 */ + + if (prev_wc == 0x200D) + break_op = BREAK_PROHIBITED; /* Rule LB8a */ + + if (break_type == G_UNICODE_BREAK_SPACE || + break_type == G_UNICODE_BREAK_ZERO_WIDTH_SPACE) + break_op = BREAK_PROHIBITED; /* Rule LB7 */ + + /* Rule LB6 */ + if (break_type == G_UNICODE_BREAK_MANDATORY || + break_type == G_UNICODE_BREAK_CARRIAGE_RETURN || + break_type == G_UNICODE_BREAK_LINE_FEED || + break_type == G_UNICODE_BREAK_NEXT_LINE) + break_op = BREAK_PROHIBITED; + + /* Rules LB4 and LB5 */ + if (prev_break_type == G_UNICODE_BREAK_MANDATORY || + (prev_break_type == G_UNICODE_BREAK_CARRIAGE_RETURN && + wc != '\n') || + prev_break_type == G_UNICODE_BREAK_LINE_FEED || + prev_break_type == G_UNICODE_BREAK_NEXT_LINE) + { + attrs[i].is_mandatory_break = TRUE; + break_op = BREAK_ALLOWED; + } + + switch (break_op) + { + case BREAK_PROHIBITED: + /* can't break here */ + attrs[i].is_line_break = FALSE; + break; + + case BREAK_IF_SPACES: + /* break if prev char was space */ + if (prev_break_type != G_UNICODE_BREAK_SPACE) + attrs[i].is_line_break = FALSE; + break; + + case BREAK_ALLOWED: + attrs[i].is_line_break = TRUE; + break; + + case BREAK_ALREADY_HANDLED: + break; + + default: + g_assert_not_reached (); + break; + } + + /* Rule LB9 */ + if (!(break_type == G_UNICODE_BREAK_COMBINING_MARK || + break_type == G_UNICODE_BREAK_ZERO_WIDTH_JOINER)) + { + /* Rule LB25 with Example 7 of Customization */ + if (break_type == G_UNICODE_BREAK_NUMERIC || + break_type == G_UNICODE_BREAK_SYMBOL || + break_type == G_UNICODE_BREAK_INFIX_SEPARATOR) + { + if (prev_LB_type != LB_Numeric) + prev_LB_type = LB_type; + /* else don't change the prev_LB_type */ + } + else + { + prev_LB_type = LB_type; + } + } + /* else don't change the prev_LB_type for Rule LB9 */ + } + + if (break_type != G_UNICODE_BREAK_SPACE) + { + /* Rule LB9 */ + if (break_type == G_UNICODE_BREAK_COMBINING_MARK || + break_type == G_UNICODE_BREAK_ZERO_WIDTH_JOINER) + { + if (i == 0 /* start of text */ || + prev_break_type == G_UNICODE_BREAK_MANDATORY || + prev_break_type == G_UNICODE_BREAK_CARRIAGE_RETURN || + prev_break_type == G_UNICODE_BREAK_LINE_FEED || + prev_break_type == G_UNICODE_BREAK_NEXT_LINE || + prev_break_type == G_UNICODE_BREAK_SPACE || + prev_break_type == G_UNICODE_BREAK_ZERO_WIDTH_SPACE) + prev_break_type = G_UNICODE_BREAK_ALPHABETIC; /* Rule LB10 */ + /* else don't change the prev_break_type for Rule LB9 */ + } + else + { + prev_prev_break_type = prev_break_type; + prev_break_type = break_type; + } + + prev_jamo = jamo; + } + else + { + if (prev_break_type != G_UNICODE_BREAK_SPACE) + { + prev_prev_break_type = prev_break_type; + prev_break_type = break_type; + } + /* else don't change the prev_break_type */ + } + + /* ---- Word breaks ---- */ + + /* default to not a word start/end */ + attrs[i].is_word_start = FALSE; + attrs[i].is_word_end = FALSE; + + if (current_word_type != WordNone) + { + /* Check for a word end */ + switch ((int) type) + { + case G_UNICODE_SPACING_MARK: + case G_UNICODE_ENCLOSING_MARK: + case G_UNICODE_NON_SPACING_MARK: + case G_UNICODE_FORMAT: + /* nothing, we just eat these up as part of the word */ + break; + + case G_UNICODE_LOWERCASE_LETTER: + case G_UNICODE_MODIFIER_LETTER: + case G_UNICODE_OTHER_LETTER: + case G_UNICODE_TITLECASE_LETTER: + case G_UNICODE_UPPERCASE_LETTER: + if (current_word_type == WordLetters) + { + /* Japanese special cases for ending the word */ + if (JAPANESE (last_word_letter) || + JAPANESE (wc)) + { + if ((HIRAGANA (last_word_letter) && + !HIRAGANA (wc)) || + (KATAKANA (last_word_letter) && + !(KATAKANA (wc) || HIRAGANA (wc))) || + (KANJI (last_word_letter) && + !(HIRAGANA (wc) || KANJI (wc))) || + (JAPANESE (last_word_letter) && + !JAPANESE (wc)) || + (!JAPANESE (last_word_letter) && + JAPANESE (wc))) + attrs[i].is_word_end = TRUE; + } + } + last_word_letter = wc; + break; + + case G_UNICODE_DECIMAL_NUMBER: + case G_UNICODE_LETTER_NUMBER: + case G_UNICODE_OTHER_NUMBER: + last_word_letter = wc; + break; + + default: + /* Punctuation, control/format chars, etc. all end a word. */ + attrs[i].is_word_end = TRUE; + current_word_type = WordNone; + break; + } + } + else + { + /* Check for a word start */ + switch ((int) type) + { + case G_UNICODE_LOWERCASE_LETTER: + case G_UNICODE_MODIFIER_LETTER: + case G_UNICODE_OTHER_LETTER: + case G_UNICODE_TITLECASE_LETTER: + case G_UNICODE_UPPERCASE_LETTER: + current_word_type = WordLetters; + last_word_letter = wc; + attrs[i].is_word_start = TRUE; + break; + + case G_UNICODE_DECIMAL_NUMBER: + case G_UNICODE_LETTER_NUMBER: + case G_UNICODE_OTHER_NUMBER: + current_word_type = WordNumbers; + last_word_letter = wc; + attrs[i].is_word_start = TRUE; + break; + + default: + /* No word here */ + break; + } + } + + /* ---- Sentence breaks ---- */ + { + + /* default to not a sentence start/end */ + attrs[i].is_sentence_start = FALSE; + attrs[i].is_sentence_end = FALSE; + + /* maybe start sentence */ + if (last_sentence_start == -1 && !is_sentence_boundary) + last_sentence_start = i - 1; + + /* remember last non space character position */ + if (i > 0 && !attrs[i - 1].is_white) + last_non_space = i; + + /* meets sentence end, mark both sentence start and end */ + if (last_sentence_start != -1 && is_sentence_boundary) { + if (last_non_space >= last_sentence_start) { + attrs[last_sentence_start].is_sentence_start = TRUE; + attrs[last_non_space].is_sentence_end = TRUE; + } + + last_sentence_start = -1; + last_non_space = -1; + } + + /* meets space character, move sentence start */ + if (last_sentence_start != -1 && + last_sentence_start == i - 1 && + attrs[i - 1].is_white) { + last_sentence_start++; + } + } + + /* --- Hyphens --- */ + + { + gboolean insert_hyphens; + gboolean space_or_hyphen = FALSE; + + attrs[i].break_inserts_hyphen = FALSE; + attrs[i].break_removes_preceding = FALSE; + + switch ((int)prev_script) + { + case G_UNICODE_SCRIPT_COMMON: + insert_hyphens = prev_wc == 0x00ad; + break; + case G_UNICODE_SCRIPT_HAN: + case G_UNICODE_SCRIPT_HANGUL: + case G_UNICODE_SCRIPT_HIRAGANA: + case G_UNICODE_SCRIPT_KATAKANA: + insert_hyphens = FALSE; + break; + default: + insert_hyphens = TRUE; + break; + } + + switch ((int)type) + { + case G_UNICODE_SPACE_SEPARATOR: + case G_UNICODE_LINE_SEPARATOR: + case G_UNICODE_PARAGRAPH_SEPARATOR: + space_or_hyphen = TRUE; + break; + case G_UNICODE_CONTROL: + if (wc == '\t' || wc == '\n' || wc == '\r' || wc == '\f') + space_or_hyphen = TRUE; + break; + default: + if (wc == '-' || /* Hyphen-minus */ + wc == 0x058a || /* Armenian hyphen */ + wc == 0x1400 || /* Canadian syllabics hyphen */ + wc == 0x1806 || /* Mongolian todo hyphen */ + wc == 0x2010 || /* Hyphen */ + wc == 0x2e17 || /* Double oblique hyphen */ + wc == 0x2e40 || /* Double hyphen */ + wc == 0x30a0 || /* Katakana-Hiragana double hyphen */ + wc == 0xfe63 || /* Small hyphen-minus */ + wc == 0xff0d) /* Fullwidth hyphen-minus */ + space_or_hyphen = TRUE; + break; + } + + if (prev_wc == 0x2027) /* Hyphenation point */ + { + attrs[i].break_inserts_hyphen = TRUE; + attrs[i].break_removes_preceding = TRUE; + } + else if (attrs[i].is_word_boundary) + attrs[i].break_inserts_hyphen = FALSE; + else if (prev_space_or_hyphen) + attrs[i].break_inserts_hyphen = FALSE; + else if (space_or_hyphen) + attrs[i].break_inserts_hyphen = FALSE; + else + attrs[i].break_inserts_hyphen = insert_hyphens; + + prev_space_or_hyphen = space_or_hyphen; + } + + prev_wc = wc; + prev_script = script; + + /* wc might not be a valid Unicode base character, but really all we + * need to know is the last non-combining character */ + if (type != G_UNICODE_SPACING_MARK && + type != G_UNICODE_ENCLOSING_MARK && + type != G_UNICODE_NON_SPACING_MARK) + base_character = wc; + } + + i--; + + attrs[0].is_cursor_position = TRUE; /* Rule GB1 */ + attrs[i].is_cursor_position = TRUE; /* Rule GB2 */ + + attrs[0].is_word_boundary = TRUE; /* Rule WB1 */ + attrs[i].is_word_boundary = TRUE; /* Rule WB2 */ + + attrs[0].is_line_break = FALSE; /* Rule LB2 */ + attrs[i].is_line_break = TRUE; /* Rule LB3 */ + attrs[i].is_mandatory_break = TRUE; /* Rule LB3 */ +} + +/* }}} */ +/* {{{ Tailoring */ +/* {{{ Script-specific tailoring */ + +#include "break-arabic.c" +#include "break-indic.c" +#include "break-thai.c" +#include "break-latin.c" + +static gboolean +break_script (const char *item_text, + unsigned int item_length, + const Pango2Analysis *analysis, + Pango2LogAttr *attrs, + int attrs_len) +{ + switch (analysis->script) + { + case G_UNICODE_SCRIPT_ARABIC: + break_arabic (item_text, item_length, analysis, attrs, attrs_len); + break; + + case G_UNICODE_SCRIPT_DEVANAGARI: + case G_UNICODE_SCRIPT_BENGALI: + case G_UNICODE_SCRIPT_GURMUKHI: + case G_UNICODE_SCRIPT_GUJARATI: + case G_UNICODE_SCRIPT_ORIYA: + case G_UNICODE_SCRIPT_TAMIL: + case G_UNICODE_SCRIPT_TELUGU: + case G_UNICODE_SCRIPT_KANNADA: + case G_UNICODE_SCRIPT_MALAYALAM: + case G_UNICODE_SCRIPT_SINHALA: + break_indic (item_text, item_length, analysis, attrs, attrs_len); + break; + + case G_UNICODE_SCRIPT_THAI: + break_thai (item_text, item_length, analysis, attrs, attrs_len); + break; + + case G_UNICODE_SCRIPT_LATIN: + break_latin (item_text, item_length, analysis, attrs, attrs_len); + break; + + default: + return FALSE; + } + + return TRUE; +} + +/* }}} */ +/* {{{ Attribute-based customization */ + +/* We allow customizing log attrs in two ways: + * + * - You can directly remove breaks from a range, using allow_breaks=false. + * We preserve the non-tailorable rules from UAX #14, so mandatory breaks + * and breaks after ZWS remain. We also preserve break opportunities after + * hyphens and visible word dividers. + * + * - You can tweak the segmentation by marking ranges as word or sentence. + * When doing so, we split adjacent segments to preserve alternating + * starts and ends. We add a line break opportunity before each word that + * is created in this way, and we remove line break opportunities inside + * the word in the same way as for a range marked as allow_breaks=false, + * except that we don't remove char break opportunities. + * + * Note that UAX #14 does not guarantee that words fall neatly into + * sentences, so we don't do extra work to enforce that. + */ + +static void +remove_breaks_from_range (const char *text, + int start, + Pango2LogAttr *log_attrs, + int start_pos, + int end_pos) +{ + int pos; + const char *p; + gunichar ch; + int bt; + gboolean after_zws; + gboolean after_hyphen; + + /* Assume our range doesn't start after a hyphen or in a zws sequence */ + after_zws = FALSE; + after_hyphen = FALSE; + for (pos = start_pos + 1, p = g_utf8_next_char (text + start); + pos < end_pos; + pos++, p = g_utf8_next_char (p)) + { + /* Mandatory breaks aren't tailorable */ + if (!log_attrs[pos].is_mandatory_break) + log_attrs[pos].is_line_break = FALSE; + + ch = g_utf8_get_char (p); + bt = g_unichar_break_type (ch); + + /* Hyphens and visible word dividers */ + if (after_hyphen) + log_attrs[pos].is_line_break = TRUE; + + after_hyphen = ch == 0x00ad || /* Soft Hyphen */ + ch == 0x05A0 || ch == 0x2010 || /* Breaking Hyphens */ + ch == 0x2012 || ch == 0x2013 || + ch == 0x05BE || ch == 0x0F0B || /* Visible word dividers */ + ch == 0x1361 || ch == 0x17D8 || + ch == 0x17DA || ch == 0x2027 || + ch == 0x007C; + + /* ZWS sequence */ + if (after_zws && bt != G_UNICODE_BREAK_SPACE) + log_attrs[pos].is_line_break = TRUE; + + after_zws = bt == G_UNICODE_BREAK_ZERO_WIDTH_SPACE || + (bt == G_UNICODE_BREAK_SPACE && after_zws); + } +} + +static gboolean +handle_allow_breaks (const char *text, + int length, + Pango2AttrList *attrs, + int offset, + Pango2LogAttr *log_attrs, + int log_attrs_len) +{ + Pango2AttrIterator iter; + gboolean tailored = FALSE; + + pango2_attr_list_init_iterator (attrs, &iter); + + do + { + const Pango2Attribute *attr = pango2_attr_iterator_get (&iter, PANGO2_ATTR_ALLOW_BREAKS); + + if (!attr) + continue; + + if (!attr->int_value) + { + int start, end; + int start_pos, end_pos; + int pos; + + start = attr->start_index; + end = attr->end_index; + if (start < offset) + start_pos = 0; + else + start_pos = g_utf8_pointer_to_offset (text, text + start - offset); + if (end >= offset + length) + end_pos = log_attrs_len; + else + end_pos = g_utf8_pointer_to_offset (text, text + end - offset); + + for (pos = start_pos + 1; pos < end_pos; pos++) + log_attrs[pos].is_char_break = FALSE; + + remove_breaks_from_range (text, MAX (start - offset, 0), log_attrs, start_pos, end_pos); + + tailored = TRUE; + } + } + while (pango2_attr_iterator_next (&iter)); + + pango2_attr_iterator_clear (&iter); + + return tailored; +} + + +static gboolean +handle_words (const char *text, + int length, + Pango2AttrList *attrs, + int offset, + Pango2LogAttr *log_attrs, + int log_attrs_len) +{ + Pango2AttrIterator iter; + gboolean tailored = FALSE; + + pango2_attr_list_init_iterator (attrs, &iter); + + do + { + const Pango2Attribute *attr = pango2_attr_iterator_get (&iter, PANGO2_ATTR_WORD); + int start, end; + int start_pos, end_pos; + int pos; + + if (!attr) + continue; + + start = attr->start_index; + end = attr->end_index; + if (start < offset) + start_pos = 0; + else + start_pos = g_utf8_pointer_to_offset (text, text + start - offset); + if (end >= offset + length) + end_pos = log_attrs_len; + else + end_pos = g_utf8_pointer_to_offset (text, text + end - offset); + + for (pos = start_pos + 1; pos < end_pos; pos++) + { + log_attrs[pos].is_word_start = FALSE; + log_attrs[pos].is_word_end = FALSE; + log_attrs[pos].is_word_boundary = FALSE; + } + + remove_breaks_from_range (text, MAX (start - offset, 0), log_attrs, + start_pos, end_pos); + + if (start >= offset) + { + gboolean in_word = FALSE; + for (pos = start_pos; pos >= 0; pos--) + { + if (log_attrs[pos].is_word_end) + { + in_word = pos == start_pos; + break; + } + if (pos < start_pos && log_attrs[pos].is_word_start) + { + in_word = TRUE; + break; + } + } + log_attrs[start_pos].is_word_start = TRUE; + log_attrs[start_pos].is_word_end = in_word; + log_attrs[start_pos].is_word_boundary = TRUE; + + /* Allow line breaks before words */ + if (start_pos > 0) + log_attrs[start_pos].is_line_break = TRUE; + + tailored = TRUE; + } + + if (end < offset + length) + { + gboolean in_word = FALSE; + for (pos = end_pos; pos < log_attrs_len; pos++) + { + if (log_attrs[pos].is_word_start) + { + in_word = pos == end_pos; + break; + } + if (pos > end_pos && log_attrs[pos].is_word_end) + { + in_word = TRUE; + break; + } + } + log_attrs[end_pos].is_word_start = in_word; + log_attrs[end_pos].is_word_end = TRUE; + log_attrs[end_pos].is_word_boundary = TRUE; + + /* Allow line breaks before words */ + if (in_word) + log_attrs[end_pos].is_line_break = TRUE; + + tailored = TRUE; + } + } + while (pango2_attr_iterator_next (&iter)); + + pango2_attr_iterator_clear (&iter); + + return tailored; +} + +static gboolean +handle_sentences (const char *text, + int length, + Pango2AttrList *attrs, + int offset, + Pango2LogAttr *log_attrs, + int log_attrs_len) +{ + Pango2AttrIterator iter; + gboolean tailored = FALSE; + + pango2_attr_list_init_iterator (attrs, &iter); + + do + { + const Pango2Attribute *attr = pango2_attr_iterator_get (&iter, PANGO2_ATTR_SENTENCE); + int start, end; + int start_pos, end_pos; + int pos; + + if (!attr) + continue; + + start = attr->start_index; + end = attr->end_index; + if (start < offset) + start_pos = 0; + else + start_pos = g_utf8_pointer_to_offset (text, text + start - offset); + if (end >= offset + length) + end_pos = log_attrs_len; + else + end_pos = g_utf8_pointer_to_offset (text, text + end - offset); + + for (pos = start_pos + 1; pos < end_pos; pos++) + { + log_attrs[pos].is_sentence_start = FALSE; + log_attrs[pos].is_sentence_end = FALSE; + log_attrs[pos].is_sentence_boundary = FALSE; + + tailored = TRUE; + } + if (start >= offset) + { + gboolean in_sentence = FALSE; + for (pos = start_pos - 1; pos >= 0; pos--) + { + if (log_attrs[pos].is_sentence_end) + break; + if (log_attrs[pos].is_sentence_start) + { + in_sentence = TRUE; + break; + } + } + log_attrs[start_pos].is_sentence_start = TRUE; + log_attrs[start_pos].is_sentence_end = in_sentence; + log_attrs[start_pos].is_sentence_boundary = TRUE; + + tailored = TRUE; + } + if (end < offset + length) + { + gboolean in_sentence = FALSE; + for (pos = end_pos + 1; end_pos < log_attrs_len; pos++) + { + if (log_attrs[pos].is_sentence_start) + break; + if (log_attrs[pos].is_sentence_end) + { + in_sentence = TRUE; + break; + } + } + log_attrs[end_pos].is_sentence_start = in_sentence; + log_attrs[end_pos].is_sentence_end = TRUE; + log_attrs[end_pos].is_sentence_boundary = TRUE; + + tailored = TRUE; + } + } + while (pango2_attr_iterator_next (&iter)); + + pango2_attr_iterator_clear (&iter); + + return tailored; +} + +static gboolean +handle_hyphens (const char *text, + int length, + Pango2AttrList *attrs, + int offset, + Pango2LogAttr *log_attrs, + int log_attrs_len) +{ + Pango2AttrIterator iter; + gboolean tailored = FALSE; + + pango2_attr_list_init_iterator (attrs, &iter); + + do { + const Pango2Attribute *attr = pango2_attr_iterator_get (&iter, PANGO2_ATTR_INSERT_HYPHENS); + + if (attr && attr->int_value == 0) + { + int start, end; + int start_pos, end_pos; + int pos; + + pango2_attr_iterator_range (&iter, &start, &end); + if (start < offset) + start_pos = 0; + else + start_pos = g_utf8_pointer_to_offset (text, text + start - offset); + if (end >= offset + length) + end_pos = log_attrs_len; + else + end_pos = g_utf8_pointer_to_offset (text, text + end - offset); + + for (pos = start_pos + 1; pos < end_pos; pos++) + { + if (!log_attrs[pos].break_removes_preceding) + { + log_attrs[pos].break_inserts_hyphen = FALSE; + + tailored = TRUE; + } + } + } + } while (pango2_attr_iterator_next (&iter)); + + pango2_attr_iterator_clear (&iter); + + return tailored; +} + +static gboolean +break_attrs (const char *text, + int length, + GSList *attributes, + int offset, + Pango2LogAttr *log_attrs, + int log_attrs_len) +{ + Pango2AttrList allow_breaks; + Pango2AttrList words; + Pango2AttrList sentences; + Pango2AttrList hyphens; + GSList *l; + gboolean tailored = FALSE; + + pango2_attr_list_init (&allow_breaks); + pango2_attr_list_init (&words); + pango2_attr_list_init (&sentences); + pango2_attr_list_init (&hyphens); + + for (l = attributes; l; l = l->next) + { + Pango2Attribute *attr = l->data; + + if (attr->type == PANGO2_ATTR_ALLOW_BREAKS) + pango2_attr_list_insert (&allow_breaks, pango2_attribute_copy (attr)); + else if (attr->type == PANGO2_ATTR_WORD) + pango2_attr_list_insert (&words, pango2_attribute_copy (attr)); + else if (attr->type == PANGO2_ATTR_SENTENCE) + pango2_attr_list_insert (&sentences, pango2_attribute_copy (attr)); + else if (attr->type == PANGO2_ATTR_INSERT_HYPHENS) + pango2_attr_list_insert (&hyphens, pango2_attribute_copy (attr)); + } + + tailored |= handle_words (text, length, &words, offset, + log_attrs, log_attrs_len); + + tailored |= handle_sentences (text, length, &words, offset, + log_attrs, log_attrs_len); + + tailored |= handle_hyphens (text, length, &hyphens, offset, + log_attrs, log_attrs_len); + + tailored |= handle_allow_breaks (text, length, &allow_breaks, offset, + log_attrs, log_attrs_len); + + pango2_attr_list_destroy (&allow_breaks); + pango2_attr_list_destroy (&words); + pango2_attr_list_destroy (&sentences); + pango2_attr_list_destroy (&hyphens); + + return tailored; +} + +/* }}} */ + +static gboolean +tailor_break (const char *text, + int length, + Pango2Analysis *analysis, + int item_offset, + Pango2LogAttr *attrs, + int attrs_len) +{ + gboolean res; + + if (length < 0) + length = strlen (text); + else if (text == NULL) + text = ""; + + res = break_script (text, length, analysis, attrs, attrs_len); + + if (item_offset >= 0 && analysis->extra_attrs) + res |= break_attrs (text, length, analysis->extra_attrs, item_offset, attrs, attrs_len); + + return res; +} + +/* }}} */ +/* {{{ Public API */ + +/** + * pango2_default_break: + * @text: text to break. Must be valid UTF-8 + * @length: length of text in bytes (may be -1 if @text is nul-terminated) + * @attrs: logical attributes to fill in + * @attrs_len: size of the array passed as @attrs + * + * This is the default break algorithm. + * + * It applies rules from the [Unicode Line Breaking Algorithm](http://www.unicode.org/unicode/reports/tr14/) + * without language-specific tailoring. + * + * See [func@Pango2.tailor_break] for language-specific breaks. + * + * See [func@Pango2.attr_break] for attribute-based customization. + */ +void +pango2_default_break (const char *text, + int length, + Pango2LogAttr *attrs, + int attrs_len G_GNUC_UNUSED) +{ + Pango2LogAttr before = *attrs; + + default_break (text, length, attrs, attrs_len); + + attrs->is_line_break |= before.is_line_break; + attrs->is_mandatory_break |= before.is_mandatory_break; + attrs->is_cursor_position |= before.is_cursor_position; +} + +/** + * pango2_tailor_break: + * @text: text to process. Must be valid UTF-8 + * @length: length in bytes of @text + * @analysis: `Pango2Analysis` for @text + * @offset: Byte offset of @text from the beginning of the + * paragraph, or -1 to ignore attributes from @analysis + * @attrs: (array length=attrs_len): array with one `Pango2LogAttr` + * per character in @text, plus one extra, to be filled in + * @attrs_len: length of @attrs array + * + * Apply language-specific tailoring to the breaks in @attrs. + * + * The line breaks are assumed to have been produced by [func@Pango2.default_break]. + * + * If @offset is not -1, it is used to apply attributes from @analysis that are + * relevant to line breaking. + * + * Note that it is better to pass -1 for @offset and use [func@Pango2.attr_break] + * to apply attributes to the whole paragraph. + */ +void +pango2_tailor_break (const char *text, + int length, + Pango2Analysis *analysis, + int offset, + Pango2LogAttr *attrs, + int attrs_len) +{ + Pango2LogAttr *start = attrs; + Pango2LogAttr attr_before = *start; + + if (tailor_break (text, length, analysis, offset, attrs, attrs_len)) + { + /* if tailored, we enforce some of the attrs from before + * tailoring at the boundary + */ + + start->backspace_deletes_character = attr_before.backspace_deletes_character; + + start->is_line_break |= attr_before.is_line_break; + start->is_mandatory_break |= attr_before.is_mandatory_break; + start->is_cursor_position |= attr_before.is_cursor_position; + } +} + +/** + * pango2_attr_break: + * @text: text to break. Must be valid UTF-8 + * @length: length of text in bytes (may be -1 if @text is nul-terminated) + * @attr_list: `Pango2AttrList` to apply + * @offset: Byte offset of @text from the beginning of the paragraph + * @attrs: (array length=attrs_len): array with one `Pango2LogAttr` + * per character in @text, plus one extra, to be filled in + * @attrs_len: length of @attrs array + * + * Apply customization from attributes to the breaks in @attrs. + * + * The line breaks are assumed to have been produced + * by [func@Pango2.default_break] and [func@Pango2.tailor_break]. + */ +void +pango2_attr_break (const char *text, + int length, + Pango2AttrList *attr_list, + int offset, + Pango2LogAttr *attrs, + int attrs_len) +{ + Pango2LogAttr *start = attrs; + Pango2LogAttr attr_before = *start; + GSList *attributes; + + attributes = pango2_attr_list_get_attributes (attr_list); + if (break_attrs (text, length, attributes, offset, attrs, attrs_len)) + { + /* if tailored, we enforce some of the attrs from before + * tailoring at the boundary + */ + + start->backspace_deletes_character = attr_before.backspace_deletes_character; + + start->is_line_break |= attr_before.is_line_break; + start->is_mandatory_break |= attr_before.is_mandatory_break; + start->is_cursor_position |= attr_before.is_cursor_position; + } + + g_slist_free_full (attributes, (GDestroyNotify)pango2_attribute_destroy); +} + +/** + * pango2_get_log_attrs: + * @text: text to process. Must be valid UTF-8 + * @length: length in bytes of @text + * @attr_list: (nullable): `Pango2AttrList` to apply + * @level: embedding level, or -1 if unknown + * @language: language tag + * @attrs: (array length=attrs_len): array with one `Pango2LogAttr` + * per character in @text, plus one extra, to be filled in + * @attrs_len: length of @attrs array + * + * Computes a `Pango2LogAttr` for each character in @text. + * + * The @attrs array must have one `Pango2LogAttr` for + * each position in @text; if @text contains N characters, + * it has N+1 positions, including the last position at the + * end of the text. @text should be an entire paragraph; + * logical attributes can't be computed without context + * (for example you need to see spaces on either side of + * a word to know the word is a word). + */ +void +pango2_get_log_attrs (const char *text, + int length, + Pango2AttrList *attr_list, + int level, + Pango2Language *language, + Pango2LogAttr *attrs, + int attrs_len) +{ + int chars_broken; + Pango2Analysis analysis = { NULL }; + Pango2ScriptIter iter; + + g_return_if_fail (length == 0 || text != NULL); + g_return_if_fail (attrs != NULL); + + analysis.level = level; + analysis.language = language; + + pango2_default_break (text, length, attrs, attrs_len); + + chars_broken = 0; + + _pango2_script_iter_init (&iter, text, length); + do + { + const char *run_start, *run_end; + GUnicodeScript script; + int chars_in_range; + + pango2_script_iter_get_range (&iter, &run_start, &run_end, &script); + analysis.script = script; + + chars_in_range = pango2_utf8_strlen (run_start, run_end - run_start); + + pango2_tailor_break (run_start, + run_end - run_start, + &analysis, + -1, + attrs + chars_broken, + chars_in_range + 1); + + chars_broken += chars_in_range; + } + while (pango2_script_iter_next (&iter)); + _pango2_script_iter_fini (&iter); + + if (attr_list) + pango2_attr_break (text, length, attr_list, 0, attrs, attrs_len); + + if (chars_broken + 1 > attrs_len) + g_warning ("pango2_get_log_attrs: attrs_len should have been at least %d, but was %d. Expect corrupted memory.", + chars_broken + 1, + attrs_len); +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/ellipsize.c b/pango2/ellipsize.c new file mode 100644 index 00000000..a5f74131 --- /dev/null +++ b/pango2/ellipsize.c @@ -0,0 +1,797 @@ +/* Pango2 + * ellipsize.c: Routine to ellipsize layout lines + * + * Copyright (C) 2004 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include <string.h> + +#include "pango-item-private.h" +#include "pango-glyph-item-private.h" +#include "pango-glyph-iter-private.h" +#include "pango-font-private.h" +#include "pango-attributes-private.h" +#include "pango-attr-private.h" +#include "pango-attr-list-private.h" +#include "pango-attr-iterator-private.h" +#include "pango-impl-utils.h" +#include "pango-line-private.h" + +typedef struct _EllipsizeState EllipsizeState; +typedef struct _RunInfo RunInfo; +typedef struct _LineIter LineIter; + + +/* Overall, the way we ellipsize is we grow a "gap" out from an original + * gap center position until: + * + * line_width - gap_width + ellipsize_width <= goal_width + * + * Line: [-------------------------------------------] + * Runs: [------)[---------------)[------------------] + * Gap center: * + * Gap: [----------------------] + * + * The gap center may be at the start or end in which case the gap grows + * in only one direction. + * + * Note the line and last run are logically closed at the end; this allows + * us to use a gap position at x=line_width and still have it be part of + * of a run. + * + * We grow the gap out one "span" at a time, where a span is simply a + * consecutive run of clusters that we can't interrupt with an ellipsis. + * + * When choosing whether to grow the gap at the start or the end, we + * calculate the next span to remove in both directions and see which + * causes the smaller increase in: + * + * MAX (gap_end - gap_center, gap_start - gap_center) + * + * All computations are done using logical order; the ellipsization + * process occurs before the runs are ordered into visual order. + */ + +/* Keeps information about a single run */ +struct _RunInfo +{ + Pango2GlyphItem *run; + int start_offset; /* Character offset of run start */ + int width; /* Width of run in Pango2 units */ +}; + +/* Iterator to a position within the ellipsized line */ +struct _LineIter +{ + Pango2GlyphItemIter run_iter; + int run_index; +}; + +/* State of ellipsization process */ +struct _EllipsizeState +{ + Pango2Context *context; + const char *text; + Pango2LogAttr *log_attrs; + Pango2EllipsizeMode ellipsize; + Pango2AttrList *attrs; /* Attributes used for itemization/shaping */ + + RunInfo *run_info; /* Array of information about each run */ + int n_runs; + + int total_width; /* Original width of line in Pango2 units */ + int gap_center; /* Goal for center of gap */ + + Pango2GlyphItem *ellipsis_run; /* Run created to hold ellipsis */ + int ellipsis_width; /* Width of ellipsis, in Pango2 units */ + int ellipsis_is_cjk; /* Whether the first character in the ellipsized + * is wide; this triggers us to try to use a + * mid-line ellipsis instead of a baseline + */ + + Pango2AttrIterator *line_start_attr; /* Cached Pango2AttrIterator for the start of the run */ + + LineIter gap_start_iter; /* Iteratator pointig to the first cluster in gap */ + int gap_start_x; /* x position of start of gap, in Pango2 units */ + Pango2AttrIterator *gap_start_attr; /* Attribute iterator pointing to a range containing + * the first character in gap */ + + LineIter gap_end_iter; /* Iterator pointing to last cluster in gap */ + int gap_end_x; /* x position of end of gap, in Pango2 units */ + + Pango2ShapeFlags shape_flags; +}; + +static void +init_state (EllipsizeState *state, + Pango2Context *context, + const char *text, + int start_index, + Pango2LogAttr *log_attrs, + Pango2EllipsizeMode ellipsize, + GSList *runs, + Pango2AttrList *attrs, + Pango2ShapeFlags shape_flags) +{ + GSList *l; + int i; + int start_offset; + + state->context = context; + state->text = text; + state->log_attrs = log_attrs; + state->ellipsize = ellipsize; + + if (attrs) + state->attrs = pango2_attr_list_ref (attrs); + else + state->attrs = pango2_attr_list_new (); + + state->shape_flags = shape_flags; + + state->n_runs = g_slist_length (runs); + state->run_info = g_new (RunInfo, state->n_runs); + + start_offset = pango2_utf8_strlen (state->text, start_index); + + state->total_width = 0; + for (l = runs, i = 0; l; l = l->next, i++) + { + Pango2GlyphItem *run = l->data; + int width = pango2_glyph_string_get_width (run->glyphs); + state->run_info[i].run = run; + state->run_info[i].width = width; + state->run_info[i].start_offset = start_offset; + state->total_width += width; + + start_offset += run->item->num_chars; + } + + state->ellipsis_run = NULL; + state->ellipsis_is_cjk = FALSE; + state->line_start_attr = NULL; + state->gap_start_attr = NULL; +} + +/* Cleanup memory allocation + */ +static void +free_state (EllipsizeState *state) +{ + pango2_attr_list_unref (state->attrs); + if (state->line_start_attr) + pango2_attr_iterator_destroy (state->line_start_attr); + if (state->gap_start_attr) + pango2_attr_iterator_destroy (state->gap_start_attr); + g_free (state->run_info); +} + +/* Computes the width of a single cluster + */ +static int +get_cluster_width (LineIter *iter) +{ + Pango2GlyphItemIter *run_iter = &iter->run_iter; + Pango2GlyphString *glyphs = run_iter->glyph_item->glyphs; + int width = 0; + int i; + + if (run_iter->start_glyph < run_iter->end_glyph) /* LTR */ + { + for (i = run_iter->start_glyph; i < run_iter->end_glyph; i++) + width += glyphs->glyphs[i].geometry.width; + } + else /* RTL */ + { + for (i = run_iter->start_glyph; i > run_iter->end_glyph; i--) + width += glyphs->glyphs[i].geometry.width; + } + + return width; +} + +/* Move forward one cluster. Returns %FALSE if we were already at the end + */ +static gboolean +line_iter_next_cluster (EllipsizeState *state, + LineIter *iter) +{ + if (!pango2_glyph_item_iter_next_cluster (&iter->run_iter)) + { + if (iter->run_index == state->n_runs - 1) + return FALSE; + else + { + iter->run_index++; + pango2_glyph_item_iter_init_start (&iter->run_iter, + state->run_info[iter->run_index].run, + state->text); + } + } + + return TRUE; +} + +/* Move backward one cluster. Returns %FALSE if we were already at the end + */ +static gboolean +line_iter_prev_cluster (EllipsizeState *state, + LineIter *iter) +{ + if (!pango2_glyph_item_iter_prev_cluster (&iter->run_iter)) + { + if (iter->run_index == 0) + return FALSE; + else + { + iter->run_index--; + pango2_glyph_item_iter_init_end (&iter->run_iter, + state->run_info[iter->run_index].run, + state->text); + } + } + + return TRUE; +} + +/* + * An ellipsization boundary is defined by two things + * + * - Starts a cluster - forced by structure of code + * - Starts a grapheme - checked here + * + * In the future we'd also like to add a check for cursive connectivity here. + * This should be an addition to `Pango2GlyphVisAttr` + * + */ + +/* Checks if there is a ellipsization boundary before the cluster @iter points to + */ +static gboolean +starts_at_ellipsization_boundary (EllipsizeState *state, + LineIter *iter) +{ + RunInfo *run_info = &state->run_info[iter->run_index]; + + if (iter->run_iter.start_char == 0 && iter->run_index == 0) + return TRUE; + + return state->log_attrs[run_info->start_offset + iter->run_iter.start_char].is_cursor_position; +} + +/* Checks if there is a ellipsization boundary after the cluster @iter points to + */ +static gboolean +ends_at_ellipsization_boundary (EllipsizeState *state, + LineIter *iter) +{ + RunInfo *run_info = &state->run_info[iter->run_index]; + + if (iter->run_iter.end_char == run_info->run->item->num_chars && iter->run_index == state->n_runs - 1) + return TRUE; + + return state->log_attrs[run_info->start_offset + iter->run_iter.end_char + 1].is_cursor_position; +} + +/* Helper function to re-itemize a string of text + */ +static Pango2Item * +itemize_text (EllipsizeState *state, + const char *text, + Pango2AttrList *attrs) +{ + GList *items; + Pango2Item *item; + Pango2Direction dir; + + dir = pango2_context_get_base_dir (state->context); + items = pango2_itemize (state->context, dir, text, 0, strlen (text), attrs); + g_assert (g_list_length (items) == 1); + + item = items->data; + g_list_free (items); + + return item; +} + +/* Shapes the ellipsis using the font and is_cjk information computed by + * update_ellipsis_shape() from the first character in the gap. + */ +static void +shape_ellipsis (EllipsizeState *state) +{ + Pango2AttrList attrs; + GSList *run_attrs; + Pango2Item *item; + Pango2GlyphString *glyphs; + GSList *l; + Pango2Attribute *fallback; + const char *ellipsis_text; + int len; + int i; + + pango2_attr_list_init (&attrs); + + /* Create/reset state->ellipsis_run + */ + if (!state->ellipsis_run) + { + state->ellipsis_run = g_slice_new0 (Pango2GlyphItem); + state->ellipsis_run->glyphs = pango2_glyph_string_new (); + } + + if (state->ellipsis_run->item) + { + pango2_item_free (state->ellipsis_run->item); + state->ellipsis_run->item = NULL; + } + + /* Create an attribute list + */ + run_attrs = pango2_attr_iterator_get_attrs (state->gap_start_attr); + int s, e; + pango2_attr_iterator_range (state->gap_start_attr, &s, &e); + for (l = run_attrs; l; l = l->next) + { + Pango2Attribute *attr = l->data; + attr->start_index = PANGO2_ATTR_INDEX_FROM_TEXT_BEGINNING; + attr->end_index = PANGO2_ATTR_INDEX_TO_TEXT_END; + + if (pango2_attribute_affects_itemization (attr, NULL) || + pango2_attribute_affects_break_or_shape (attr, NULL)) + pango2_attr_list_insert (&attrs, attr); + else + pango2_attribute_destroy (attr); + } + + g_slist_free (run_attrs); + + fallback = pango2_attr_fallback_new (FALSE); + fallback->start_index = 0; + fallback->end_index = G_MAXINT; + pango2_attr_list_insert (&attrs, fallback); + + /* First try using a specific ellipsis character in the best matching font + */ + if (state->ellipsis_is_cjk) + ellipsis_text = "\342\213\257"; /* U+22EF: MIDLINE HORIZONTAL ELLIPSIS, used for CJK */ + else + ellipsis_text = "\342\200\246"; /* U+2026: HORIZONTAL ELLIPSIS */ + + item = itemize_text (state, ellipsis_text, &attrs); + + /* If that fails we use "..." in the first matching font + */ + if (!item->analysis.font || + !pango2_font_face_has_char (item->analysis.font->face, + g_utf8_get_char (ellipsis_text))) + { + pango2_item_free (item); + + /* Modify the fallback iter while it is inside the Pango2AttrList; Don't try this at home + */ + fallback->int_value = TRUE; + + ellipsis_text = "..."; + item = itemize_text (state, ellipsis_text, &attrs); + } + + pango2_attr_list_destroy (&attrs); + + state->ellipsis_run->item = item; + + /* Now shape + */ + glyphs = state->ellipsis_run->glyphs; + + len = strlen (ellipsis_text); + pango2_shape (ellipsis_text, len, + ellipsis_text, len, + &item->analysis, glyphs, + state->shape_flags); + + state->ellipsis_width = 0; + for (i = 0; i < glyphs->num_glyphs; i++) + state->ellipsis_width += glyphs->glyphs[i].geometry.width; +} + +/* Helper function to advance a Pango2AttrIterator to a particular + * byte index. + */ +static void +advance_iterator_to (Pango2AttrIterator *iter, + int new_index) +{ + int start, end; + + do + { + pango2_attr_iterator_range (iter, &start, &end); + if (end > new_index) + break; + } + while (pango2_attr_iterator_next (iter)); +} + +/* Updates the shaping of the ellipsis if necessary when we move the + * position of the start of the gap. + * + * The shaping of the ellipsis is determined by two things: + * + * - The font attributes applied to the first character in the gap + * - Whether the first character in the gap is wide or not. If the + * first character is wide, then we assume that we are ellipsizing + * East-Asian text, so prefer a mid-line ellipsizes to a baseline + * ellipsis, since that's typical practice for Chinese/Japanese/Korean. + */ +static void +update_ellipsis_shape (EllipsizeState *state) +{ + gboolean recompute = FALSE; + gunichar start_wc; + gboolean is_cjk; + + /* Unfortunately, we can only advance Pango2AttrIterator forward; so each + * time we back up we need to go forward to find the new position. To make + * this not utterly slow, we cache an iterator at the start of the line + */ + if (!state->line_start_attr) + { + state->line_start_attr = pango2_attr_list_get_iterator (state->attrs); + advance_iterator_to (state->line_start_attr, state->run_info[0].run->item->offset); + } + + if (state->gap_start_attr) + { + /* See if the current attribute range contains the new start position + */ + int start, end; + + pango2_attr_iterator_range (state->gap_start_attr, &start, &end); + + if (state->gap_start_iter.run_iter.start_index < start) + { + pango2_attr_iterator_destroy (state->gap_start_attr); + state->gap_start_attr = NULL; + } + } + + /* Check whether we need to recompute the ellipsis because of new font attributes + */ + if (!state->gap_start_attr) + { + state->gap_start_attr = pango2_attr_iterator_copy (state->line_start_attr); + advance_iterator_to (state->gap_start_attr, + state->run_info[state->gap_start_iter.run_index].run->item->offset); + + recompute = TRUE; + } + + /* Check whether we need to recompute the ellipsis because we switch from CJK to not + * or vice-versa + */ + start_wc = g_utf8_get_char (state->text + state->gap_start_iter.run_iter.start_index); + is_cjk = g_unichar_iswide (start_wc); + + if (is_cjk != state->ellipsis_is_cjk) + { + state->ellipsis_is_cjk = is_cjk; + recompute = TRUE; + } + + if (recompute) + shape_ellipsis (state); +} + +/* Computes the position of the gap center and finds the smallest span containing it + */ +static void +find_initial_span (EllipsizeState *state) +{ + Pango2GlyphItem *glyph_item; + Pango2GlyphItemIter *run_iter; + gboolean have_cluster; + int i; + int x; + int cluster_width; + + switch (state->ellipsize) + { + case PANGO2_ELLIPSIZE_NONE: + default: + g_assert_not_reached (); + case PANGO2_ELLIPSIZE_START: + state->gap_center = 0; + break; + case PANGO2_ELLIPSIZE_MIDDLE: + state->gap_center = state->total_width / 2; + break; + case PANGO2_ELLIPSIZE_END: + state->gap_center = state->total_width; + break; + } + + /* Find the run containing the gap center + */ + x = 0; + for (i = 0; i < state->n_runs; i++) + { + if (x + state->run_info[i].width > state->gap_center) + break; + + x += state->run_info[i].width; + } + + if (i == state->n_runs) /* Last run is a closed interval, so back off one run */ + { + i--; + x -= state->run_info[i].width; + } + + /* Find the cluster containing the gap center + */ + state->gap_start_iter.run_index = i; + run_iter = &state->gap_start_iter.run_iter; + glyph_item = state->run_info[i].run; + + cluster_width = 0; /* Quiet GCC, the line must have at least one cluster */ + for (have_cluster = pango2_glyph_item_iter_init_start (run_iter, glyph_item, state->text); + have_cluster; + have_cluster = pango2_glyph_item_iter_next_cluster (run_iter)) + { + cluster_width = get_cluster_width (&state->gap_start_iter); + + if (x + cluster_width > state->gap_center) + break; + + x += cluster_width; + } + + if (!have_cluster) /* Last cluster is a closed interval, so back off one cluster */ + x -= cluster_width; + + state->gap_end_iter = state->gap_start_iter; + + state->gap_start_x = x; + state->gap_end_x = x + cluster_width; + + /* Expand the gap to a full span + */ + while (!starts_at_ellipsization_boundary (state, &state->gap_start_iter)) + { + line_iter_prev_cluster (state, &state->gap_start_iter); + state->gap_start_x -= get_cluster_width (&state->gap_start_iter); + } + + while (!ends_at_ellipsization_boundary (state, &state->gap_end_iter)) + { + line_iter_next_cluster (state, &state->gap_end_iter); + state->gap_end_x += get_cluster_width (&state->gap_end_iter); + } + + update_ellipsis_shape (state); +} + +/* Removes one run from the start or end of the gap. Returns FALSE + * if there's nothing left to remove in either direction. + */ +static gboolean +remove_one_span (EllipsizeState *state) +{ + LineIter new_gap_start_iter; + LineIter new_gap_end_iter; + int new_gap_start_x; + int new_gap_end_x; + int width; + + /* Find one span backwards and forward from the gap + */ + new_gap_start_iter = state->gap_start_iter; + new_gap_start_x = state->gap_start_x; + do + { + if (!line_iter_prev_cluster (state, &new_gap_start_iter)) + break; + width = get_cluster_width (&new_gap_start_iter); + new_gap_start_x -= width; + } + while (!starts_at_ellipsization_boundary (state, &new_gap_start_iter) || + width == 0); + + new_gap_end_iter = state->gap_end_iter; + new_gap_end_x = state->gap_end_x; + do + { + if (!line_iter_next_cluster (state, &new_gap_end_iter)) + break; + width = get_cluster_width (&new_gap_end_iter); + new_gap_end_x += width; + } + while (!ends_at_ellipsization_boundary (state, &new_gap_end_iter) || + width == 0); + + if (state->gap_end_x == new_gap_end_x && state->gap_start_x == new_gap_start_x) + return FALSE; + + /* In the case where we could remove a span from either end of the + * gap, we look at which causes the smaller increase in the + * MAX (gap_end - gap_center, gap_start - gap_center) + */ + if (state->gap_end_x == new_gap_end_x || + (state->gap_start_x != new_gap_start_x && + state->gap_center - new_gap_start_x < new_gap_end_x - state->gap_center)) + { + state->gap_start_iter = new_gap_start_iter; + state->gap_start_x = new_gap_start_x; + + update_ellipsis_shape (state); + } + else + { + state->gap_end_iter = new_gap_end_iter; + state->gap_end_x = new_gap_end_x; + } + + return TRUE; +} + +/* Fixes up the properties of the ellipsis run once we've determined the final extents + * of the gap + */ +static void +fixup_ellipsis_run (EllipsizeState *state, + int extra_width) +{ + Pango2GlyphString *glyphs = state->ellipsis_run->glyphs; + Pango2Item *item = state->ellipsis_run->item; + int level; + int i; + + /* Make the entire glyphstring into a single logical cluster */ + for (i = 0; i < glyphs->num_glyphs; i++) + { + glyphs->log_clusters[i] = 0; + glyphs->glyphs[i].attr.is_cluster_start = FALSE; + } + + glyphs->glyphs[0].attr.is_cluster_start = TRUE; + + glyphs->glyphs[glyphs->num_glyphs - 1].geometry.width += extra_width; + + /* Fix up the item to point to the entire elided text */ + item->offset = state->gap_start_iter.run_iter.start_index; + item->length = state->gap_end_iter.run_iter.end_index - item->offset; + item->num_chars = pango2_utf8_strlen (state->text + item->offset, item->length); + + /* The level for the item is the minimum level of the elided text */ + level = G_MAXINT; + for (i = state->gap_start_iter.run_index; i <= state->gap_end_iter.run_index; i++) + level = MIN (level, state->run_info[i].run->item->analysis.level); + + item->analysis.level = level; + + item->analysis.flags |= PANGO2_ANALYSIS_FLAG_IS_ELLIPSIS; +} + +/* Computes the new list of runs for the line + */ +static GSList * +get_run_list (EllipsizeState *state) +{ + Pango2GlyphItem *partial_start_run = NULL; + Pango2GlyphItem *partial_end_run = NULL; + GSList *result = NULL; + RunInfo *run_info; + Pango2GlyphItemIter *run_iter; + int i; + + /* We first cut out the pieces of the starting and ending runs we want to + * preserve; we do the end first in case the end and the start are + * the same. Doing the start first would disturb the indices for the end. + */ + run_info = &state->run_info[state->gap_end_iter.run_index]; + run_iter = &state->gap_end_iter.run_iter; + if (run_iter->end_char != run_info->run->item->num_chars) + { + partial_end_run = run_info->run; + run_info->run = pango2_glyph_item_split (run_info->run, state->text, + run_iter->end_index - run_info->run->item->offset); + } + + run_info = &state->run_info[state->gap_start_iter.run_index]; + run_iter = &state->gap_start_iter.run_iter; + if (run_iter->start_char != 0) + { + partial_start_run = pango2_glyph_item_split (run_info->run, state->text, + run_iter->start_index - run_info->run->item->offset); + } + + /* Now assemble the new list of runs + */ + for (i = 0; i < state->gap_start_iter.run_index; i++) + result = g_slist_prepend (result, state->run_info[i].run); + + if (partial_start_run) + result = g_slist_prepend (result, partial_start_run); + + result = g_slist_prepend (result, state->ellipsis_run); + + if (partial_end_run) + result = g_slist_prepend (result, partial_end_run); + + for (i = state->gap_end_iter.run_index + 1; i < state->n_runs; i++) + result = g_slist_prepend (result, state->run_info[i].run); + + /* And free the ones we didn't use + */ + for (i = state->gap_start_iter.run_index; i <= state->gap_end_iter.run_index; i++) + pango2_glyph_item_free (state->run_info[i].run); + + return g_slist_reverse (result); +} + +/* Computes the width of the line as currently ellipsized + */ +static int +current_width (EllipsizeState *state) +{ + return state->total_width - (state->gap_end_x - state->gap_start_x) + state->ellipsis_width; +} + +void +pango2_line_ellipsize (Pango2Line *line, + Pango2Context *context, + Pango2EllipsizeMode ellipsize, + int goal_width) +{ + EllipsizeState state; + const char *text = line->data->text; + Pango2AttrList *attrs = line->data->attrs; + Pango2LogAttr *log_attrs = line->data->log_attrs; + Pango2ShapeFlags shape_flags; + + g_return_if_fail (ellipsize != PANGO2_ELLIPSIZE_NONE && goal_width >= 0); + + if (pango2_context_get_round_glyph_positions (context)) + shape_flags = PANGO2_SHAPE_ROUND_POSITIONS; + else + shape_flags = PANGO2_SHAPE_NONE; + + init_state (&state, context, text, line->start_index, log_attrs, ellipsize, line->runs, attrs, shape_flags); + + if (state.total_width <= goal_width) + goto out; + + find_initial_span (&state); + + while (current_width (&state) > goal_width) + { + if (!remove_one_span (&state)) + break; + } + + fixup_ellipsis_run (&state, MAX (goal_width - current_width (&state), 0)); + + g_slist_free (line->runs); + line->runs = get_run_list (&state); + line->ellipsized = TRUE; + + out: + free_state (&state); +} diff --git a/pango2/emoji_presentation_scanner.c b/pango2/emoji_presentation_scanner.c new file mode 100644 index 00000000..e40b9d45 --- /dev/null +++ b/pango2/emoji_presentation_scanner.c @@ -0,0 +1,280 @@ + +#line 1 "emoji_presentation_scanner.rl" +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + + +#line 9 "emoji_presentation_scanner.c" +static const char _emoji_presentation_actions[] = { + 0, 1, 0, 1, 1, 1, 5, 1, + 6, 1, 7, 1, 8, 1, 9, 1, + 10, 1, 11, 2, 2, 3, 2, 2, + 4 +}; + +static const char _emoji_presentation_key_offsets[] = { + 0, 5, 7, 14, 18, 20, 21, 24, + 29, 30, 34, 36 +}; + +static const unsigned char _emoji_presentation_trans_keys[] = { + 3u, 7u, 13u, 0u, 2u, 14u, 15u, 2u, + 3u, 6u, 7u, 13u, 0u, 1u, 9u, 10u, + 11u, 12u, 10u, 12u, 10u, 4u, 10u, 12u, + 4u, 9u, 10u, 11u, 12u, 6u, 9u, 10u, + 11u, 12u, 8u, 10u, 9u, 10u, 11u, 12u, + 14u, 0 +}; + +static const char _emoji_presentation_single_lengths[] = { + 3, 2, 5, 4, 2, 1, 3, 5, + 1, 4, 2, 5 +}; + +static const char _emoji_presentation_range_lengths[] = { + 1, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0 +}; + +static const char _emoji_presentation_index_offsets[] = { + 0, 5, 8, 15, 20, 23, 25, 29, + 35, 37, 42, 45 +}; + +static const char _emoji_presentation_indicies[] = { + 2, 1, 1, 1, 0, 4, 5, 3, + 7, 8, 10, 11, 12, 6, 9, 5, + 13, 14, 15, 0, 13, 15, 16, 13, + 16, 15, 13, 15, 16, 15, 5, 13, + 14, 15, 16, 5, 17, 5, 13, 14, + 18, 17, 5, 13, 16, 5, 13, 14, + 15, 4, 16, 0 +}; + +static const char _emoji_presentation_trans_targs[] = { + 2, 4, 6, 2, 1, 2, 3, 3, + 7, 2, 8, 9, 11, 0, 2, 5, + 2, 2, 10 +}; + +static const char _emoji_presentation_trans_actions[] = { + 17, 19, 19, 15, 0, 7, 22, 19, + 19, 9, 0, 22, 19, 0, 5, 19, + 11, 13, 19 +}; + +static const char _emoji_presentation_to_state_actions[] = { + 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0 +}; + +static const char _emoji_presentation_from_state_actions[] = { + 0, 0, 3, 0, 0, 0, 0, 0, + 0, 0, 0, 0 +}; + +static const char _emoji_presentation_eof_trans[] = { + 1, 4, 0, 1, 17, 17, 17, 17, + 18, 18, 17, 17 +}; + +static const int emoji_presentation_start = 2; + +static const int emoji_presentation_en_text_and_emoji_run = 2; + + +#line 9 "emoji_presentation_scanner.rl" + + + +#line 78 "emoji_presentation_scanner.rl" + + +static emoji_text_iter_t +scan_emoji_presentation (emoji_text_iter_t p, + const emoji_text_iter_t pe, + bool* is_emoji) +{ + emoji_text_iter_t te; + const emoji_text_iter_t eof = pe; + + unsigned act; + int cs; + + +#line 107 "emoji_presentation_scanner.c" + { + cs = emoji_presentation_start; + te = 0; + act = 0; + } + +#line 115 "emoji_presentation_scanner.c" + { + int _klen; + unsigned int _trans; + const char *_acts; + unsigned int _nacts; + const unsigned char *_keys; + + if ( p == pe ) + goto _test_eof; +_resume: + _acts = _emoji_presentation_actions + _emoji_presentation_from_state_actions[cs]; + _nacts = (unsigned int) *_acts++; + while ( _nacts-- > 0 ) { + switch ( *_acts++ ) { + case 1: +#line 1 "NONE" + break; +#line 134 "emoji_presentation_scanner.c" + } + } + + _keys = _emoji_presentation_trans_keys + _emoji_presentation_key_offsets[cs]; + _trans = _emoji_presentation_index_offsets[cs]; + + _klen = _emoji_presentation_single_lengths[cs]; + if ( _klen > 0 ) { + const unsigned char *_lower = _keys; + const unsigned char *_mid; + const unsigned char *_upper = _keys + _klen - 1; + while (1) { + if ( _upper < _lower ) + break; + + _mid = _lower + ((_upper-_lower) >> 1); + if ( (*p) < *_mid ) + _upper = _mid - 1; + else if ( (*p) > *_mid ) + _lower = _mid + 1; + else { + _trans += (unsigned int)(_mid - _keys); + goto _match; + } + } + _keys += _klen; + _trans += _klen; + } + + _klen = _emoji_presentation_range_lengths[cs]; + if ( _klen > 0 ) { + const unsigned char *_lower = _keys; + const unsigned char *_mid; + const unsigned char *_upper = _keys + (_klen<<1) - 2; + while (1) { + if ( _upper < _lower ) + break; + + _mid = _lower + (((_upper-_lower) >> 1) & ~1); + if ( (*p) < _mid[0] ) + _upper = _mid - 2; + else if ( (*p) > _mid[1] ) + _lower = _mid + 2; + else { + _trans += (unsigned int)((_mid - _keys)>>1); + goto _match; + } + } + _trans += _klen; + } + +_match: + _trans = _emoji_presentation_indicies[_trans]; +_eof_trans: + cs = _emoji_presentation_trans_targs[_trans]; + + if ( _emoji_presentation_trans_actions[_trans] == 0 ) + goto _again; + + _acts = _emoji_presentation_actions + _emoji_presentation_trans_actions[_trans]; + _nacts = (unsigned int) *_acts++; + while ( _nacts-- > 0 ) + { + switch ( *_acts++ ) + { + case 2: +#line 1 "NONE" + {te = p+1;} + break; + case 3: +#line 74 "emoji_presentation_scanner.rl" + {act = 2;} + break; + case 4: +#line 75 "emoji_presentation_scanner.rl" + {act = 3;} + break; + case 5: +#line 73 "emoji_presentation_scanner.rl" + {te = p+1;{ *is_emoji = false; return te; }} + break; + case 6: +#line 74 "emoji_presentation_scanner.rl" + {te = p+1;{ *is_emoji = true; return te; }} + break; + case 7: +#line 75 "emoji_presentation_scanner.rl" + {te = p+1;{ *is_emoji = false; return te; }} + break; + case 8: +#line 74 "emoji_presentation_scanner.rl" + {te = p;p--;{ *is_emoji = true; return te; }} + break; + case 9: +#line 75 "emoji_presentation_scanner.rl" + {te = p;p--;{ *is_emoji = false; return te; }} + break; + case 10: +#line 74 "emoji_presentation_scanner.rl" + {{p = ((te))-1;}{ *is_emoji = true; return te; }} + break; + case 11: +#line 1 "NONE" + { switch( act ) { + case 2: + {{p = ((te))-1;} *is_emoji = true; return te; } + break; + case 3: + {{p = ((te))-1;} *is_emoji = false; return te; } + break; + } + } + break; +#line 248 "emoji_presentation_scanner.c" + } + } + +_again: + _acts = _emoji_presentation_actions + _emoji_presentation_to_state_actions[cs]; + _nacts = (unsigned int) *_acts++; + while ( _nacts-- > 0 ) { + switch ( *_acts++ ) { + case 0: +#line 1 "NONE" + break; +#line 261 "emoji_presentation_scanner.c" + } + } + + if ( ++p != pe ) + goto _resume; + _test_eof: {} + if ( p == eof ) + { + if ( _emoji_presentation_eof_trans[cs] > 0 ) { + _trans = _emoji_presentation_eof_trans[cs] - 1; + goto _eof_trans; + } + } + + } + +#line 94 "emoji_presentation_scanner.rl" + + + /* Should not be reached. */ + *is_emoji = false; + return pe; +} diff --git a/pango2/emoji_presentation_scanner.rl b/pango2/emoji_presentation_scanner.rl new file mode 100644 index 00000000..d9c26919 --- /dev/null +++ b/pango2/emoji_presentation_scanner.rl @@ -0,0 +1,99 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +%%{ + machine emoji_presentation; + alphtype unsigned char; + write data noerror nofinal noentry; +}%% + +%%{ + +EMOJI = 0; +EMOJI_TEXT_PRESENTATION = 1; +EMOJI_EMOJI_PRESENTATION = 2; +EMOJI_MODIFIER_BASE = 3; +EMOJI_MODIFIER = 4; +EMOJI_VS_BASE = 5; +REGIONAL_INDICATOR = 6; +KEYCAP_BASE = 7; +COMBINING_ENCLOSING_KEYCAP = 8; +COMBINING_ENCLOSING_CIRCLE_BACKSLASH = 9; +ZWJ = 10; +VS15 = 11; +VS16 = 12; +TAG_BASE = 13; +TAG_SEQUENCE = 14; +TAG_TERM = 15; + +any_emoji = EMOJI_TEXT_PRESENTATION | EMOJI_EMOJI_PRESENTATION | KEYCAP_BASE | + EMOJI_MODIFIER_BASE | TAG_BASE | EMOJI; + +emoji_combining_enclosing_circle_backslash_sequence = any_emoji + COMBINING_ENCLOSING_CIRCLE_BACKSLASH; + +# This could be sharper than any_emoji by restricting this only to valid +# variation sequences: +# https://www.unicode.org/Public/emoji/11.0/emoji-variation-sequences.txt +# However, implementing +# https://www.unicode.org/reports/tr51/#def_emoji_presentation_sequence is +# sufficient for our purposes here. +emoji_presentation_sequence = any_emoji VS16; + +emoji_modifier_sequence = EMOJI_MODIFIER_BASE EMOJI_MODIFIER; + +emoji_flag_sequence = REGIONAL_INDICATOR REGIONAL_INDICATOR; + +# Here we only allow the valid tag sequences +# https://www.unicode.org/reports/tr51/#valid-emoji-tag-sequences, instead of +# all well-formed ones defined in +# https://www.unicode.org/reports/tr51/#def_emoji_tag_sequence +emoji_tag_sequence = TAG_BASE TAG_SEQUENCE+ TAG_TERM; + +emoji_keycap_sequence = KEYCAP_BASE VS16 COMBINING_ENCLOSING_KEYCAP; + +emoji_zwj_element = emoji_presentation_sequence | emoji_modifier_sequence | any_emoji; + +emoji_zwj_sequence = emoji_zwj_element ( ZWJ emoji_zwj_element )+; + +emoji_presentation = EMOJI_EMOJI_PRESENTATION | TAG_BASE | EMOJI_MODIFIER_BASE | + emoji_presentation_sequence | emoji_modifier_sequence | emoji_flag_sequence | + emoji_tag_sequence | emoji_keycap_sequence | emoji_zwj_sequence | + emoji_combining_enclosing_circle_backslash_sequence; + +emoji_run = emoji_presentation; + +text_presentation_emoji = any_emoji VS15; +text_run = any; + +text_and_emoji_run := |* +# In order to give the the VS15 sequences higher priority than detecting +# emoji sequences they are listed first as scanner token here. +text_presentation_emoji => { *is_emoji = false; return te; }; +emoji_run => { *is_emoji = true; return te; }; +text_run => { *is_emoji = false; return te; }; +*|; + +}%% + +static emoji_text_iter_t +scan_emoji_presentation (emoji_text_iter_t p, + const emoji_text_iter_t pe, + bool* is_emoji) +{ + emoji_text_iter_t ts, te; + const emoji_text_iter_t eof = pe; + + unsigned act; + int cs; + + %%{ + write init; + write exec; + }%% + + /* Should not be reached. */ + *is_emoji = false; + return pe; +} diff --git a/pango2/glyphstring.c b/pango2/glyphstring.c new file mode 100644 index 00000000..9ae86b93 --- /dev/null +++ b/pango2/glyphstring.c @@ -0,0 +1,793 @@ +/* Pango2 + * glyphstring.c: + * + * Copyright (C) 1999 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include <glib.h> +#include "pango-glyph.h" +#include "pango-font.h" +#include "pango-item-private.h" +#include "pango-glyph-item-private.h" +#include "pango-impl-utils.h" + +#include <hb-ot.h> + +/** + * pango2_glyph_string_new: + * + * Create a new `Pango2GlyphString`. + * + * Return value: the newly allocated `Pango2GlyphString`, which + * should be freed with [method@Pango2.GlyphString.free]. + */ +Pango2GlyphString * +pango2_glyph_string_new (void) +{ + Pango2GlyphString *string = g_slice_new (Pango2GlyphString); + + string->num_glyphs = 0; + string->space = 0; + string->glyphs = NULL; + string->log_clusters = NULL; + + return string; +} + +/** + * pango2_glyph_string_set_size: + * @string: a `Pango2GlyphString`. + * @new_len: the new length of the string + * + * Resize a glyph string to the given length. + */ +void +pango2_glyph_string_set_size (Pango2GlyphString *string, + int new_len) +{ + g_return_if_fail (new_len >= 0); + + while (new_len > string->space) + { + if (string->space == 0) + { + string->space = 4; + } + else + { + const guint max_space = + MIN (G_MAXINT, G_MAXSIZE / MAX (sizeof(Pango2GlyphInfo), sizeof(int))); + + guint more_space = (guint)string->space * 2; + + if (more_space > max_space) + { + more_space = max_space; + + if ((guint)new_len > max_space) + { + g_error ("%s: failed to allocate glyph string of length %i\n", + G_STRLOC, new_len); + } + } + + string->space = more_space; + } + } + + string->glyphs = g_realloc (string->glyphs, string->space * sizeof (Pango2GlyphInfo)); + string->log_clusters = g_realloc (string->log_clusters, string->space * sizeof (int)); + string->num_glyphs = new_len; +} + +G_DEFINE_BOXED_TYPE (Pango2GlyphString, pango2_glyph_string, + pango2_glyph_string_copy, + pango2_glyph_string_free); + +/** + * pango2_glyph_string_copy: + * @string: (nullable): a `Pango2GlyphString` + * + * Copy a glyph string and associated storage. + * + * Return value: (nullable): the newly allocated `Pango2GlyphString` + */ +Pango2GlyphString * +pango2_glyph_string_copy (Pango2GlyphString *string) +{ + Pango2GlyphString *new_string; + + if (string == NULL) + return NULL; + + new_string = g_slice_new (Pango2GlyphString); + + *new_string = *string; + + new_string->glyphs = g_memdup2 (string->glyphs, + string->space * sizeof (Pango2GlyphInfo)); + new_string->log_clusters = g_memdup2 (string->log_clusters, + string->space * sizeof (int)); + + return new_string; +} + +/** + * pango2_glyph_string_free: + * @string: (nullable): a `Pango2GlyphString`, may be %NULL + * + * Free a glyph string and associated storage. + */ +void +pango2_glyph_string_free (Pango2GlyphString *string) +{ + if (string == NULL) + return; + + g_free (string->glyphs); + g_free (string->log_clusters); + g_slice_free (Pango2GlyphString, string); +} + +/** + * pango2_glyph_string_extents_range: + * @glyphs: a `Pango2GlyphString` + * @start: start index + * @end: end index (the range is the set of bytes with + * indices such that start <= index < end) + * @font: a `Pango2Font` + * @ink_rect: (out caller-allocates) (optional): rectangle used to + * store the extents of the glyph string range as drawn + * @logical_rect: (out caller-allocates) (optional): rectangle used to + * store the logical extents of the glyph string range + * + * Computes the extents of a sub-portion of a glyph string. + * + * The extents are relative to the start of the glyph string range + * (the origin of their coordinate system is at the start of the range, + * not at the start of the entire glyph string). + */ +void +pango2_glyph_string_extents_range (Pango2GlyphString *glyphs, + int start, + int end, + Pango2Font *font, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect) +{ + int x_pos = 0; + int i; + + /* Note that the handling of empty rectangles for ink + * and logical rectangles is different. A zero-height ink + * rectangle makes no contribution to the overall ink rect, + * while a zero-height logical rect still reserves horizontal + * width. Also, we may return zero-width, positive height + * logical rectangles, while we'll never do that for the + * ink rect. + */ + g_return_if_fail (start <= end); + + if (G_UNLIKELY (!ink_rect && !logical_rect)) + return; + + if (ink_rect) + { + ink_rect->x = 0; + ink_rect->y = 0; + ink_rect->width = 0; + ink_rect->height = 0; + } + + if (logical_rect) + { + logical_rect->x = 0; + logical_rect->y = 0; + logical_rect->width = 0; + logical_rect->height = 0; + } + + for (i = start; i < end; i++) + { + Pango2Rectangle glyph_ink; + Pango2Rectangle glyph_logical; + + Pango2GlyphGeometry *geometry = &glyphs->glyphs[i].geometry; + + pango2_font_get_glyph_extents (font, glyphs->glyphs[i].glyph, + ink_rect ? &glyph_ink : NULL, + logical_rect ? &glyph_logical : NULL); + + if (ink_rect && glyph_ink.width != 0 && glyph_ink.height != 0) + { + if (ink_rect->width == 0 || ink_rect->height == 0) + { + ink_rect->x = x_pos + glyph_ink.x + geometry->x_offset; + ink_rect->width = glyph_ink.width; + ink_rect->y = glyph_ink.y + geometry->y_offset; + ink_rect->height = glyph_ink.height; + } + else + { + int new_x, new_y; + + new_x = MIN (ink_rect->x, x_pos + glyph_ink.x + geometry->x_offset); + ink_rect->width = MAX (ink_rect->x + ink_rect->width, + x_pos + glyph_ink.x + glyph_ink.width + geometry->x_offset) - new_x; + ink_rect->x = new_x; + + new_y = MIN (ink_rect->y, glyph_ink.y + geometry->y_offset); + ink_rect->height = MAX (ink_rect->y + ink_rect->height, + glyph_ink.y + glyph_ink.height + geometry->y_offset) - new_y; + ink_rect->y = new_y; + } + } + + if (logical_rect) + { + logical_rect->width += geometry->width; + + if (i == start) + { + logical_rect->y = glyph_logical.y; + logical_rect->height = glyph_logical.height; + } + else + { + int new_y = MIN (logical_rect->y, glyph_logical.y); + logical_rect->height = MAX (logical_rect->y + logical_rect->height, + glyph_logical.y + glyph_logical.height) - new_y; + logical_rect->y = new_y; + } + } + + x_pos += geometry->width; + } +} + +/** + * pango2_glyph_string_extents: + * @glyphs: a `Pango2GlyphString` + * @font: a `Pango2Font` + * @ink_rect: (out) (optional): rectangle used to store the extents of the glyph string as drawn + * @logical_rect: (out) (optional): rectangle used to store the logical extents of the glyph string + * + * Compute the logical and ink extents of a glyph string. + * + * See the documentation for [method@Pango2.Font.get_glyph_extents] for details + * about the interpretation of the rectangles. + * + * Examples of logical (red) and ink (green) rects: + * + * ![](rects1.png) ![](rects2.png) + */ +void +pango2_glyph_string_extents (Pango2GlyphString *glyphs, + Pango2Font *font, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect) +{ + pango2_glyph_string_extents_range (glyphs, 0, glyphs->num_glyphs, + font, ink_rect, logical_rect); +} + +/** + * pango2_glyph_string_get_width: + * @glyphs: a `Pango2GlyphString` + * + * Computes the logical width of the glyph string. + * + * This can also be computed using [method@Pango2.GlyphString.extents]. + * However, since this only computes the width, it's much faster. This + * is in fact only a convenience function that computes the sum of + * @geometry.width for each glyph in the @glyphs. + * + * Return value: the logical width of the glyph string. + */ +int +pango2_glyph_string_get_width (Pango2GlyphString *glyphs) +{ + int i; + int width = 0; + + for (i = 0; i < glyphs->num_glyphs; i++) + width += glyphs->glyphs[i].geometry.width; + + return width; +} + +/** + * pango2_glyph_string_get_logical_widths: + * @glyphs: a `Pango2GlyphString` + * @text: the text corresponding to the glyphs + * @length: the length of @text, in bytes + * @embedding_level: the embedding level of the string + * @logical_widths: (array): an array whose length is the number of + * characters in text (equal to `g_utf8_strlen (text, length)` unless + * text has `NUL` bytes) to be filled in with the resulting character widths. + * + * Given a `Pango2GlyphString` and corresponding text, determine the width + * corresponding to each character. + * + * When multiple characters compose a single cluster, the width of the + * entire cluster is divided equally among the characters. + */ +void +pango2_glyph_string_get_logical_widths (Pango2GlyphString *glyphs, + const char *text, + int length, + int embedding_level, + int *logical_widths) +{ + /* Build a Pango2GlyphItem and call the other API */ + Pango2Item item = {0, length, pango2_utf8_strlen (text, length), 0, + {NULL, NULL, + embedding_level, PANGO2_GRAVITY_AUTO, 0, + G_UNICODE_SCRIPT_UNKNOWN, NULL, + NULL}}; + Pango2GlyphItem glyph_item = {&item, glyphs}; + + pango2_glyph_item_get_logical_widths (&glyph_item, text, logical_widths); +} + +/* The initial implementation here is script independent, + * but it might actually need to be virtualized into the + * rendering modules. Otherwise, we probably will end up + * enforcing unnatural cursor behavior for some languages. + * + * The only distinction that Uniscript makes is whether + * cursor positioning is allowed within clusters or not. + */ + +/** + * pango2_glyph_string_index_to_x: + * @glyphs: the glyphs return from [func@shape] + * @text: the text for the run + * @length: the number of bytes (not characters) in @text. + * @analysis: the analysis information return from [func@itemize] + * @index_: the byte index within @text + * @trailing: whether we should compute the result for the beginning (%FALSE) + * or end (%TRUE) of the character. + * @x_pos: (out): location to store result + * + * Converts from character position to x position. + * + * The X position is measured from the left edge of the run. + * Character positions are obtained using font metrics for ligatures + * where available, and computed by dividing up each cluster + * into equal portions, otherwise. + * + * <picture> + * <source srcset="glyphstring-positions-dark.png" media="(prefers-color-scheme: dark)"> + * <img alt="Glyph positions" src="glyphstring-positions-light.png"> + * </picture> + */ +void +pango2_glyph_string_index_to_x (Pango2GlyphString *glyphs, + const char *text, + int length, + const Pango2Analysis *analysis, + int index, + gboolean trailing, + int *x_pos) +{ + pango2_glyph_string_index_to_x_full (glyphs, + text, length, + analysis, + NULL, + index, trailing, + x_pos); +} + +/** + * pango2_glyph_string_index_to_x_full: + * @glyphs: the glyphs return from [func@shape] + * @text: the text for the run + * @length: the number of bytes (not characters) in @text. + * @analysis: the analysis information return from [func@itemize] + * @attrs: (nullable): `Pango2LogAttr` array for @text + * @index_: the byte index within @text + * @trailing: whether we should compute the result for the beginning (%FALSE) + * or end (%TRUE) of the character. + * @x_pos: (out): location to store result + * + * Converts from character position to x position. + * + * This variant of [method@Pango2.GlyphString.index_to_x] additionally + * accepts a `Pango2LogAttr` array. The grapheme boundary information + * in it can be used to disambiguate positioning inside some complex + * clusters. + */ +void +pango2_glyph_string_index_to_x_full (Pango2GlyphString *glyphs, + const char *text, + int length, + const Pango2Analysis *analysis, + Pango2LogAttr *attrs, + int index, + gboolean trailing, + int *x_pos) +{ + int i; + int start_xpos = 0; + int end_xpos = 0; + int width = 0; + + int start_index = -1; + int end_index = -1; + + int cluster_chars = 0; + int cluster_offset = 0; + int start_glyph_pos = -1; + int end_glyph_pos = -1; + + const char *p; + + g_return_if_fail (glyphs != NULL); + g_return_if_fail (length >= 0); + g_return_if_fail (length == 0 || text != NULL); + + if (!x_pos) /* Allow the user to do the useless */ + return; + + if (glyphs->num_glyphs == 0) + { + *x_pos = 0; + return; + } + + start_glyph_pos = -1; + end_glyph_pos = -1; + + /* Calculate the starting and ending character positions + * and x positions for the cluster + */ + if (analysis->level % 2) /* Right to left */ + { + for (i = glyphs->num_glyphs - 1; i >= 0; i--) + width += glyphs->glyphs[i].geometry.width; + + for (i = glyphs->num_glyphs - 1; i >= 0; i--) + { + if (glyphs->log_clusters[i] > index) + { + end_index = glyphs->log_clusters[i]; + end_xpos = width; + break; + } + + if (glyphs->log_clusters[i] != start_index) + { + start_index = glyphs->log_clusters[i]; + start_xpos = width; + } + + width -= glyphs->glyphs[i].geometry.width; + } + + for (i = glyphs->num_glyphs - 1; i >= 0; i--) + { + if (glyphs->log_clusters[i] == start_index) + { + if (end_glyph_pos < 0) + end_glyph_pos = i; + start_glyph_pos = i; + } + } + } + else /* Left to right */ + { + for (i = 0; i < glyphs->num_glyphs; i++) + { + if (glyphs->log_clusters[i] > index) + { + end_index = glyphs->log_clusters[i]; + end_xpos = width; + break; + } + + if (glyphs->log_clusters[i] != start_index) + { + start_index = glyphs->log_clusters[i]; + start_xpos = width; + } + + width += glyphs->glyphs[i].geometry.width; + } + + for (i = 0; i < glyphs->num_glyphs; i++) + { + if (glyphs->log_clusters[i] == start_index) + { + if (start_glyph_pos < 0) + start_glyph_pos = i; + end_glyph_pos = i; + } + } + } + + if (end_index == -1) + { + end_index = length; + end_xpos = (analysis->level % 2) ? 0 : width; + } + + /* Calculate offset of character within cluster. + * To come up with accurate answers here, we need to know grapheme + * boundaries. + */ + for (p = text + start_index, i = attrs ? g_utf8_pointer_to_offset (text, text + start_index) : 0; + p < text + end_index; + p = g_utf8_next_char (p), i++) + { + if (attrs && !attrs[i].is_cursor_position) + continue; + + if (p < text + index) + cluster_offset++; + cluster_chars++; + } + + if (trailing) + cluster_offset += 1; + + if (G_UNLIKELY (!cluster_chars)) /* pedantic */ + { + *x_pos = start_xpos; + return; + } + + /* Try to get a ligature caret position for the glyph from the font. + * This only makes sense if the cluster contains a single spacing + * glyph, so we need to check that all but one of them are marks. + */ + if (cluster_offset > 0 && cluster_offset < cluster_chars) + { + hb_font_t *hb_font; + hb_position_t caret; + unsigned int caret_count = 1; + int glyph_pos; + int num_carets; + + hb_font = pango2_font_get_hb_font (analysis->font); + + if (start_glyph_pos == end_glyph_pos) + glyph_pos = start_glyph_pos; + else + { + hb_face_t *hb_face; + + hb_face = hb_font_get_face (hb_font); + + glyph_pos = -1; + for (i = start_glyph_pos; i <= end_glyph_pos; i++) + { + if (hb_ot_layout_get_glyph_class (hb_face, glyphs->glyphs[i].glyph) != HB_OT_LAYOUT_GLYPH_CLASS_MARK) + { + if (glyph_pos != -1) + { + /* multiple non-mark glyphs in cluster, giving up */ + goto fallback; + } + glyph_pos = i; + } + } + if (glyph_pos == -1) + { + /* no non-mark glyph in a multi-glyph cluster, giving up */ + goto fallback; + } + } + + num_carets = hb_ot_layout_get_ligature_carets (hb_font, + (analysis->level % 2) ? HB_DIRECTION_RTL : HB_DIRECTION_LTR, + glyphs->glyphs[glyph_pos].glyph, + cluster_offset - 1, &caret_count, &caret); + if (caret_count == 0 || num_carets == 0) + { + /* no ligature caret information found for this glyph */ + goto fallback; + } + + if (analysis->level % 2) /* Right to left */ + *x_pos = end_xpos + caret; + else + *x_pos = start_xpos + caret; + *x_pos += glyphs->glyphs[glyph_pos].geometry.x_offset; + return; + } + +fallback: + + *x_pos = ((cluster_chars - cluster_offset) * start_xpos + + cluster_offset * end_xpos) / cluster_chars; +} + +/** + * pango2_glyph_string_x_to_index: + * @glyphs: the glyphs returned from [func@shape] + * @text: the text for the run + * @length: the number of bytes (not characters) in text. + * @analysis: the analysis information return from [func@itemize] + * @x_pos: the x offset (in Pango2 units) + * @index_: (out): location to store calculated byte index within @text + * @trailing: (out): location to store a boolean indicating whether the + * user clicked on the leading or trailing edge of the character + * + * Convert from x offset to character position. + * + * Character positions are computed by dividing up each cluster into + * equal portions. In scripts where positioning within a cluster is + * not allowed (such as Thai), the returned value may not be a valid + * cursor position; the caller must combine the result with the logical + * attributes for the text to compute the valid cursor position. + */ +void +pango2_glyph_string_x_to_index (Pango2GlyphString *glyphs, + const char *text, + int length, + const Pango2Analysis *analysis, + int x_pos, + int *index, + gboolean *trailing) +{ + int i; + int start_xpos = 0; + int end_xpos = 0; + int width = 0; + + int start_index = -1; + int end_index = -1; + + int cluster_chars = 0; + const char *p; + + gboolean found = FALSE; + + /* Find the cluster containing the position */ + + width = 0; + + if (analysis->level % 2) /* Right to left */ + { + for (i = glyphs->num_glyphs - 1; i >= 0; i--) + width += glyphs->glyphs[i].geometry.width; + + for (i = glyphs->num_glyphs - 1; i >= 0; i--) + { + if (glyphs->log_clusters[i] != start_index) + { + if (found) + { + end_index = glyphs->log_clusters[i]; + end_xpos = width; + break; + } + else + { + start_index = glyphs->log_clusters[i]; + start_xpos = width; + } + } + + width -= glyphs->glyphs[i].geometry.width; + + if (width <= x_pos && x_pos < width + glyphs->glyphs[i].geometry.width) + found = TRUE; + } + } + else /* Left to right */ + { + for (i = 0; i < glyphs->num_glyphs; i++) + { + if (glyphs->log_clusters[i] != start_index) + { + if (found) + { + end_index = glyphs->log_clusters[i]; + end_xpos = width; + break; + } + else + { + start_index = glyphs->log_clusters[i]; + start_xpos = width; + } + } + + if (width <= x_pos && x_pos < width + glyphs->glyphs[i].geometry.width) + found = TRUE; + + width += glyphs->glyphs[i].geometry.width; + } + } + + if (end_index == -1) + { + end_index = length; + end_xpos = (analysis->level % 2) ? 0 : width; + } + + /* Calculate number of chars within cluster */ + p = text + start_index; + while (p < text + end_index) + { + p = g_utf8_next_char (p); + cluster_chars++; + } + + if (start_xpos == end_xpos) + { + if (index) + *index = start_index; + if (trailing) + *trailing = FALSE; + } + else + { + double cp = ((double)(x_pos - start_xpos) * cluster_chars) / (end_xpos - start_xpos); + + /* LTR and right-to-left have to be handled separately + * here because of the edge condition when we are exactly + * at a pixel boundary; end_xpos goes with the next + * character for LTR, with the previous character for RTL. + */ + if (start_xpos < end_xpos) /* Left-to-right */ + { + if (index) + { + const char *p = text + start_index; + int i = 0; + + while (i + 1 <= cp) + { + p = g_utf8_next_char (p); + i++; + } + + *index = (p - text); + } + + if (trailing) + *trailing = (cp - (int)cp >= 0.5) ? TRUE : FALSE; + } + else /* Right-to-left */ + { + if (index) + { + const char *p = text + start_index; + int i = 0; + + while (i + 1 < cp) + { + p = g_utf8_next_char (p); + i++; + } + + *index = (p - text); + } + + if (trailing) + { + double cp_flip = cluster_chars - cp; + *trailing = (cp_flip - (int)cp_flip >= 0.5) ? FALSE : TRUE; + } + } + } +} diff --git a/pango2/itemize.c b/pango2/itemize.c new file mode 100644 index 00000000..1cf4e6ea --- /dev/null +++ b/pango2/itemize.c @@ -0,0 +1,1634 @@ +/* Pango2 + * itemize.c: Turning text into items + * + * Copyright (C) 2000, 2006 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include <string.h> +#include <stdlib.h> + +#include "pango-context-private.h" +#include "pango-impl-utils.h" + +#include "pango-font-private.h" +#include "pango-fontset.h" +#include "pango-fontmap-private.h" +#include "pango-script-private.h" +#include "pango-emoji-private.h" +#include "pango-attr-iterator-private.h" +#include "pango-attr-private.h" +#include "pango-item-private.h" +#include "pango-bidi-private.h" + +#include <hb-ot.h> + +/* {{{ Font cache */ + +/* + * We cache the results of character,fontset => font in a hash table + */ + +typedef struct { + GHashTable *hash; +} FontCache; + +typedef struct { + Pango2Font *font; + int position; /* position of the font in the fontset */ +} FontElement; + +static void +font_cache_destroy (FontCache *cache) +{ + g_hash_table_destroy (cache->hash); + g_slice_free (FontCache, cache); +} + +static void +font_element_destroy (FontElement *element) +{ + if (element->font) + g_object_unref (element->font); + g_slice_free (FontElement, element); +} + +static FontCache * +get_font_cache (Pango2Fontset *fontset) +{ + FontCache *cache; + + static GQuark cache_quark = 0; /* MT-safe */ + if (G_UNLIKELY (!cache_quark)) + cache_quark = g_quark_from_static_string ("pango-font-cache"); + +retry: + cache = g_object_get_qdata (G_OBJECT (fontset), cache_quark); + if (G_UNLIKELY (!cache)) + { + cache = g_slice_new (FontCache); + cache->hash = g_hash_table_new_full (g_direct_hash, NULL, + NULL, (GDestroyNotify)font_element_destroy); + if (!g_object_replace_qdata (G_OBJECT (fontset), cache_quark, NULL, + cache, (GDestroyNotify)font_cache_destroy, + NULL)) + { + font_cache_destroy (cache); + goto retry; + } + } + + return cache; +} + +static gboolean +font_cache_get (FontCache *cache, + gunichar wc, + Pango2Font **font, + int *position) +{ + FontElement *element; + + element = g_hash_table_lookup (cache->hash, GUINT_TO_POINTER (wc)); + if (element) + { + *font = element->font; + *position = element->position; + return TRUE; + } + else + return FALSE; +} + +static void +font_cache_insert (FontCache *cache, + gunichar wc, + Pango2Font *font, + int position) +{ + FontElement *element = g_slice_new (FontElement); + element->font = font ? g_object_ref (font) : NULL; + element->position = position; + + g_hash_table_insert (cache->hash, GUINT_TO_POINTER (wc), element); +} + +/* }}} */ +/* {{{ Width Iter */ + +typedef struct _Pango2WidthIter Pango2WidthIter; + +struct _Pango2WidthIter +{ + const char *text_start; + const char *text_end; + const char *start; + const char *end; + gboolean upright; +}; + +static gboolean +width_iter_is_upright (gunichar ch) +{ + /* https://www.unicode.org/Public/11.0.0/ucd/VerticalOrientation.txt + * VO=U or Tu table generated by tools/gen-vertical-orientation-U-table.py. + * + * FIXME: In the future, If GLib supports VerticalOrientation, please use it. + */ + static const gunichar upright[][2] = { + {0x00A7, 0x00A7}, {0x00A9, 0x00A9}, {0x00AE, 0x00AE}, {0x00B1, 0x00B1}, + {0x00BC, 0x00BE}, {0x00D7, 0x00D7}, {0x00F7, 0x00F7}, {0x02EA, 0x02EB}, + {0x1100, 0x11FF}, {0x1401, 0x167F}, {0x18B0, 0x18FF}, {0x2016, 0x2016}, + {0x2020, 0x2021}, {0x2030, 0x2031}, {0x203B, 0x203C}, {0x2042, 0x2042}, + {0x2047, 0x2049}, {0x2051, 0x2051}, {0x2065, 0x2065}, {0x20DD, 0x20E0}, + {0x20E2, 0x20E4}, {0x2100, 0x2101}, {0x2103, 0x2109}, {0x210F, 0x210F}, + {0x2113, 0x2114}, {0x2116, 0x2117}, {0x211E, 0x2123}, {0x2125, 0x2125}, + {0x2127, 0x2127}, {0x2129, 0x2129}, {0x212E, 0x212E}, {0x2135, 0x213F}, + {0x2145, 0x214A}, {0x214C, 0x214D}, {0x214F, 0x2189}, {0x218C, 0x218F}, + {0x221E, 0x221E}, {0x2234, 0x2235}, {0x2300, 0x2307}, {0x230C, 0x231F}, + {0x2324, 0x2328}, {0x232B, 0x232B}, {0x237D, 0x239A}, {0x23BE, 0x23CD}, + {0x23CF, 0x23CF}, {0x23D1, 0x23DB}, {0x23E2, 0x2422}, {0x2424, 0x24FF}, + {0x25A0, 0x2619}, {0x2620, 0x2767}, {0x2776, 0x2793}, {0x2B12, 0x2B2F}, + {0x2B50, 0x2B59}, {0x2BB8, 0x2BD1}, {0x2BD3, 0x2BEB}, {0x2BF0, 0x2BFF}, + {0x2E80, 0x3007}, {0x3012, 0x3013}, {0x3020, 0x302F}, {0x3031, 0x309F}, + {0x30A1, 0x30FB}, {0x30FD, 0xA4CF}, {0xA960, 0xA97F}, {0xAC00, 0xD7FF}, + {0xE000, 0xFAFF}, {0xFE10, 0xFE1F}, {0xFE30, 0xFE48}, {0xFE50, 0xFE57}, + {0xFE5F, 0xFE62}, {0xFE67, 0xFE6F}, {0xFF01, 0xFF07}, {0xFF0A, 0xFF0C}, + {0xFF0E, 0xFF19}, {0xFF1F, 0xFF3A}, {0xFF3C, 0xFF3C}, {0xFF3E, 0xFF3E}, + {0xFF40, 0xFF5A}, {0xFFE0, 0xFFE2}, {0xFFE4, 0xFFE7}, {0xFFF0, 0xFFF8}, + {0xFFFC, 0xFFFD}, {0x10980, 0x1099F}, {0x11580, 0x115FF}, {0x11A00, 0x11AAF}, + {0x13000, 0x1342F}, {0x14400, 0x1467F}, {0x16FE0, 0x18AFF}, {0x1B000, 0x1B12F}, + {0x1B170, 0x1B2FF}, {0x1D000, 0x1D1FF}, {0x1D2E0, 0x1D37F}, {0x1D800, 0x1DAAF}, + {0x1F000, 0x1F7FF}, {0x1F900, 0x1FA6F}, {0x20000, 0x2FFFD}, {0x30000, 0x3FFFD}, + {0xF0000, 0xFFFFD}, {0x100000, 0x10FFFD} + }; + static const int max = sizeof(upright) / sizeof(upright[0]); + int st = 0; + int ed = max; + + if (ch < upright[0][0]) + return FALSE; + + while (st <= ed) + { + int mid = (st + ed) / 2; + if (upright[mid][0] <= ch && ch <= upright[mid][1]) + return TRUE; + else + if (upright[mid][0] <= ch) + st = mid + 1; + else + ed = mid - 1; + } + + return FALSE; +} + +static void +width_iter_next (Pango2WidthIter *iter) +{ + gboolean met_joiner = FALSE; + iter->start = iter->end; + + if (iter->end < iter->text_end) + { + gunichar ch = g_utf8_get_char (iter->end); + iter->upright = width_iter_is_upright (ch); + } + + while (iter->end < iter->text_end) + { + gunichar ch = g_utf8_get_char (iter->end); + + /* for zero width joiner */ + if (ch == 0x200D) + { + iter->end = g_utf8_next_char (iter->end); + met_joiner = TRUE; + continue; + } + + /* ignore the upright check if met joiner */ + if (met_joiner) + { + iter->end = g_utf8_next_char (iter->end); + met_joiner = FALSE; + continue; + } + + /* for variation selector, tag and emoji modifier. */ + if (G_UNLIKELY (ch == 0xFE0EU || ch == 0xFE0FU || + (ch >= 0xE0020 && ch <= 0xE007F) || + (ch >= 0x1F3FB && ch <= 0x1F3FF))) + { + iter->end = g_utf8_next_char (iter->end); + continue; + } + + if (width_iter_is_upright (ch) != iter->upright) + break; + + iter->end = g_utf8_next_char (iter->end); + } +} + +static void +width_iter_init (Pango2WidthIter *iter, + const char *text, + int length) +{ + iter->text_start = text; + iter->text_end = text + length; + iter->start = iter->end = text; + + width_iter_next (iter); +} + +static void +width_iter_fini (Pango2WidthIter *iter) +{ +} + +/* }}} */ + /* {{{ Itemization */ + +typedef struct _ItemizeState ItemizeState; + + +typedef enum { + EMBEDDING_CHANGED = 1 << 0, + SCRIPT_CHANGED = 1 << 1, + LANG_CHANGED = 1 << 2, + FONT_CHANGED = 1 << 3, + DERIVED_LANG_CHANGED = 1 << 4, + WIDTH_CHANGED = 1 << 5, + EMOJI_CHANGED = 1 << 6, +} ChangedFlags; + + +struct _ItemizeState +{ + Pango2Context *context; + const char *text; + const char *end; + + const char *run_start; + const char *run_end; + + GList *result; + Pango2Item *item; + + guint8 *embedding_levels; + int embedding_end_offset; + const char *embedding_end; + guint8 embedding; + + Pango2Gravity gravity; + Pango2GravityHint gravity_hint; + Pango2Gravity resolved_gravity; + Pango2Gravity font_desc_gravity; + gboolean centered_baseline; + + Pango2AttrIterator *attr_iter; + gboolean free_attr_iter; + const char *attr_end; + Pango2FontDescription *font_desc; + Pango2FontDescription *emoji_font_desc; + Pango2Language *lang; + GSList *extra_attrs; + gboolean copy_extra_attrs; + + ChangedFlags changed; + + Pango2ScriptIter script_iter; + const char *script_end; + GUnicodeScript script; + + Pango2WidthIter width_iter; + Pango2EmojiIter emoji_iter; + + Pango2Language *derived_lang; + + Pango2Fontset *current_fonts; + FontCache *cache; + Pango2Font *base_font; + gboolean enable_fallback; + + const char *first_space; /* first of a sequence of spaces we've seen */ + int font_position; /* position of the current font in the fontset */ +}; + +static void +update_embedding_end (ItemizeState *state) +{ + state->embedding = state->embedding_levels[state->embedding_end_offset]; + while (state->embedding_end < state->end && + state->embedding_levels[state->embedding_end_offset] == state->embedding) + { + state->embedding_end_offset++; + state->embedding_end = g_utf8_next_char (state->embedding_end); + } + + state->changed |= EMBEDDING_CHANGED; +} + +static Pango2Attribute * +find_attribute (GSList *attr_list, + Pango2AttrType type) +{ + GSList *node; + + for (node = attr_list; node; node = node->next) + if (((Pango2Attribute *) node->data)->type == type) + return (Pango2Attribute *) node->data; + + return NULL; +} + +static void +update_attr_iterator (ItemizeState *state) +{ + Pango2Language *old_lang; + Pango2Attribute *attr; + int end_index; + + pango2_attr_iterator_range (state->attr_iter, NULL, &end_index); + if (end_index < state->end - state->text) + state->attr_end = state->text + end_index; + else + state->attr_end = state->end; + + if (state->emoji_font_desc) + { + pango2_font_description_free (state->emoji_font_desc); + state->emoji_font_desc = NULL; + } + + old_lang = state->lang; + if (state->font_desc) + pango2_font_description_free (state->font_desc); + state->font_desc = pango2_font_description_copy_static (state->context->font_desc); + pango2_attr_iterator_get_font (state->attr_iter, state->font_desc, + &state->lang, &state->extra_attrs); + if (pango2_font_description_get_set_fields (state->font_desc) & PANGO2_FONT_MASK_GRAVITY) + state->font_desc_gravity = pango2_font_description_get_gravity (state->font_desc); + else + state->font_desc_gravity = PANGO2_GRAVITY_AUTO; + + state->copy_extra_attrs = FALSE; + + if (!state->lang) + state->lang = state->context->language; + + attr = find_attribute (state->extra_attrs, PANGO2_ATTR_FALLBACK); + state->enable_fallback = (attr == NULL || attr->int_value); + + attr = find_attribute (state->extra_attrs, PANGO2_ATTR_GRAVITY); + state->gravity = attr == NULL ? PANGO2_GRAVITY_AUTO : attr->int_value; + + attr = find_attribute (state->extra_attrs, PANGO2_ATTR_GRAVITY_HINT); + state->gravity_hint = attr == NULL ? state->context->gravity_hint : (Pango2GravityHint)attr->int_value; + + state->changed |= FONT_CHANGED; + if (state->lang != old_lang) + state->changed |= LANG_CHANGED; +} + +static void +update_end (ItemizeState *state) +{ + state->run_end = state->embedding_end; + if (state->attr_end < state->run_end) + state->run_end = state->attr_end; + if (state->script_end < state->run_end) + state->run_end = state->script_end; + if (state->width_iter.end < state->run_end) + state->run_end = state->width_iter.end; + if (state->emoji_iter.end < state->run_end) + state->run_end = state->emoji_iter.end; +} + + +static void +itemize_state_init (ItemizeState *state, + Pango2Context *context, + const char *text, + Pango2Direction base_dir, + int start_index, + int length, + Pango2AttrList *attrs, + Pango2AttrIterator *cached_iter, + const Pango2FontDescription *desc) +{ + state->context = context; + state->text = text; + state->end = text + start_index + length; + + state->result = NULL; + state->item = NULL; + + state->run_start = text + start_index; + state->changed = EMBEDDING_CHANGED | SCRIPT_CHANGED | LANG_CHANGED | + FONT_CHANGED | WIDTH_CHANGED | EMOJI_CHANGED; + + /* First, apply the bidirectional algorithm to break + * the text into directional runs. + */ + state->embedding_levels = pango2_log2vis_get_embedding_levels (text + start_index, length, &base_dir); + + state->embedding_end_offset = 0; + state->embedding_end = text + start_index; + update_embedding_end (state); + + state->gravity = PANGO2_GRAVITY_AUTO; + state->centered_baseline = PANGO2_GRAVITY_IS_VERTICAL (state->context->resolved_gravity); + state->gravity_hint = state->context->gravity_hint; + state->resolved_gravity = PANGO2_GRAVITY_AUTO; + + /* Initialize the attribute iterator + */ + if (cached_iter) + { + state->attr_iter = cached_iter; + state->free_attr_iter = FALSE; + } + else if (attrs) + { + state->attr_iter = pango2_attr_list_get_iterator (attrs); + state->free_attr_iter = TRUE; + } + else + { + state->attr_iter = NULL; + state->free_attr_iter = FALSE; + } + + state->emoji_font_desc = NULL; + if (state->attr_iter) + { + state->font_desc = NULL; + state->lang = NULL; + + pango2_attr_iterator_advance (state->attr_iter, start_index); + update_attr_iterator (state); + } + else + { + state->font_desc = pango2_font_description_copy_static (desc ? desc : state->context->font_desc); + state->lang = state->context->language; + state->extra_attrs = NULL; + state->copy_extra_attrs = FALSE; + + state->attr_end = state->end; + state->enable_fallback = TRUE; + } + + /* Initialize the script iterator + */ + _pango2_script_iter_init (&state->script_iter, text + start_index, length); + pango2_script_iter_get_range (&state->script_iter, NULL, + &state->script_end, &state->script); + + width_iter_init (&state->width_iter, text + start_index, length); + _pango2_emoji_iter_init (&state->emoji_iter, text + start_index, length); + + if (!PANGO2_GRAVITY_IS_VERTICAL (state->context->resolved_gravity)) + state->width_iter.end = state->end; + else if (state->emoji_iter.is_emoji) + state->width_iter.end = MAX (state->width_iter.end, state->emoji_iter.end); + + update_end (state); + + if (pango2_font_description_get_set_fields (state->font_desc) & PANGO2_FONT_MASK_GRAVITY) + state->font_desc_gravity = pango2_font_description_get_gravity (state->font_desc); + else + state->font_desc_gravity = PANGO2_GRAVITY_AUTO; + + state->derived_lang = NULL; + state->current_fonts = NULL; + state->cache = NULL; + state->base_font = NULL; + state->first_space = NULL; + state->font_position = 0xffff; +} + +static gboolean +itemize_state_next (ItemizeState *state) +{ + if (state->run_end == state->end) + return FALSE; + + state->changed = 0; + + state->run_start = state->run_end; + + if (state->run_end == state->embedding_end) + { + update_embedding_end (state); + } + + if (state->run_end == state->attr_end) + { + pango2_attr_iterator_next (state->attr_iter); + update_attr_iterator (state); + } + + if (state->run_end == state->script_end) + { + pango2_script_iter_next (&state->script_iter); + pango2_script_iter_get_range (&state->script_iter, NULL, + &state->script_end, &state->script); + state->changed |= SCRIPT_CHANGED; + } + if (state->run_end == state->emoji_iter.end) + { + _pango2_emoji_iter_next (&state->emoji_iter); + state->changed |= EMOJI_CHANGED; + + if (state->emoji_iter.is_emoji) + state->width_iter.end = MAX (state->width_iter.end, state->emoji_iter.end); + } + if (state->run_end == state->width_iter.end) + { + width_iter_next (&state->width_iter); + state->changed |= WIDTH_CHANGED; + } + + update_end (state); + + return TRUE; +} + +static GSList * +copy_attr_slist (GSList *attr_slist) +{ + GSList *new_list = NULL; + GSList *l; + + for (l = attr_slist; l; l = l->next) + new_list = g_slist_prepend (new_list, pango2_attribute_copy (l->data)); + + return g_slist_reverse (new_list); +} + +static void +itemize_state_fill_font (ItemizeState *state, + Pango2Font *font) +{ + GList *l; + + for (l = state->result; l; l = l->next) + { + Pango2Item *item = l->data; + if (item->analysis.font) + break; + if (font) + item->analysis.font = g_object_ref (font); + } +} + +static void +itemize_state_add_character (ItemizeState *state, + Pango2Font *font, + int font_position, + gboolean force_break, + const char *pos, + gboolean is_space) +{ + const char *first_space = state->first_space; + int n_spaces = 0; + + if (is_space) + { + if (state->first_space == NULL) + state->first_space = pos; + } + else + state->first_space = NULL; + + if (state->item) + { + if (!state->item->analysis.font && font) + { + itemize_state_fill_font (state, font); + state->font_position = font_position; + } + else if (state->item->analysis.font && !font) + { + font = state->item->analysis.font; + font_position = state->font_position; + } + + if (!force_break && + state->item->analysis.font == font) + { + state->item->num_chars++; + return; + } + + /* Font is changing, we are about to end the current item. + * If it ended in a sequence of spaces (but wasn't only spaces), + * check if we should move those spaces to the new item (since + * the font is less "fallback". + * + * See https://gitlab.gnome.org/GNOME/pango/-/issues/249 + */ + if (state->text + state->item->offset < first_space && + font_position < state->font_position) + { + n_spaces = g_utf8_strlen (first_space, pos - first_space); + state->item->num_chars -= n_spaces; + pos = first_space; + } + + state->item->length = (pos - state->text) - state->item->offset; + } + + state->item = pango2_item_new (); + state->item->offset = pos - state->text; + state->item->length = 0; + state->item->num_chars = n_spaces + 1; + + if (font) + g_object_ref (font); + state->item->analysis.font = font; + state->font_position = font_position; + + state->item->analysis.level = state->embedding; + state->item->analysis.gravity = state->resolved_gravity; + + /* The level vs. gravity dance: + * - If gravity is SOUTH, leave level untouched. + * - If gravity is NORTH, step level one up, to + * not get mirrored upside-down text. + * - If gravity is EAST, step up to an even level, as + * it's a clockwise-rotated layout, so the rotated + * top is unrotated left. + * - If gravity is WEST, step up to an odd level, as + * it's a counter-clockwise-rotated layout, so the rotated + * top is unrotated right. + * + * A similar dance is performed in pango-layout.c: + * line_set_resolved_dir(). Keep in synch. + */ + switch (state->item->analysis.gravity) + { + case PANGO2_GRAVITY_SOUTH: + default: + break; + case PANGO2_GRAVITY_NORTH: + state->item->analysis.level++; + break; + case PANGO2_GRAVITY_EAST: + state->item->analysis.level += 1; + state->item->analysis.level &= ~1; + break; + case PANGO2_GRAVITY_WEST: + state->item->analysis.level |= 1; + break; + } + + state->item->analysis.flags |= state->centered_baseline ? PANGO2_ANALYSIS_FLAG_CENTERED_BASELINE : 0; + + state->item->analysis.script = state->script; + state->item->analysis.language = state->derived_lang; + + if (state->copy_extra_attrs) + { + state->item->analysis.extra_attrs = copy_attr_slist (state->extra_attrs); + } + else + { + state->item->analysis.extra_attrs = state->extra_attrs; + state->copy_extra_attrs = TRUE; + } + + state->result = g_list_prepend (state->result, state->item); +} + +typedef struct { + Pango2Font *font; + int position; +} GetFontInfo; + +static gboolean +get_font_foreach (Pango2Fontset *fontset, + Pango2Font *font, + gpointer data) +{ + GetFontInfo *info = data; + + if (font == info->font) + return TRUE; + + info->position++; + + return FALSE; +} + +static Pango2Font * +get_base_font (ItemizeState *state) +{ + if (!state->base_font) + state->base_font = pango2_font_map_load_font (state->context->font_map, + state->context, + state->font_desc); + return state->base_font; +} + +static gboolean +get_font (ItemizeState *state, + gunichar wc, + Pango2Font **font, + int *position) +{ + GetFontInfo info; + + /* We'd need a separate cache when fallback is disabled, but since lookup + * with fallback disabled is faster anyways, we just skip caching + */ + if (state->enable_fallback && font_cache_get (state->cache, wc, font, position)) + return TRUE; + + info.font = NULL; + info.position = 0; + + if (state->enable_fallback) + { + info.font = pango2_fontset_get_font (state->current_fonts, wc); + if (info.font) + g_object_unref (info.font); + pango2_fontset_foreach (state->current_fonts, get_font_foreach, &info); + } + + if (!info.font) + info.font = get_base_font (state); + + *font = info.font; + *position = info.position; + + /* skip caching if fallback disabled (see above) */ + if (state->enable_fallback) + font_cache_insert (state->cache, wc, *font, *position); + + return TRUE; +} + +static Pango2Language * +compute_derived_language (Pango2Language *lang, + GUnicodeScript script) +{ + Pango2Language *derived_lang; + + /* Make sure the language tag is consistent with the derived + * script. There is no point in marking up a section of + * Arabic text with the "en" language tag. + */ + if (lang && pango2_language_includes_script (lang, script)) + derived_lang = lang; + else + { + derived_lang = pango2_script_get_sample_language (script); + /* If we don't find a sample language for the script, we + * use a language tag that shouldn't actually be used + * anywhere. This keeps fontconfig (for the Pango2Fc* + * backend) from using the language tag to affect the + * sort order. I don't have a reference for 'xx' being + * safe here, though Keith Packard claims it is. + */ + if (!derived_lang) + derived_lang = pango2_language_from_string ("xx"); + } + + return derived_lang; +} + +static void +itemize_state_update_for_new_run (ItemizeState *state) +{ + /* This block should be moved to update_attr_iterator, but I'm too lazy to + * do it right now */ + if (state->changed & (FONT_CHANGED | SCRIPT_CHANGED | WIDTH_CHANGED)) + { + /* Font-desc gravity overrides everything */ + if (state->font_desc_gravity != PANGO2_GRAVITY_AUTO) + { + state->resolved_gravity = state->font_desc_gravity; + } + else + { + Pango2Gravity gravity = state->gravity; + Pango2GravityHint gravity_hint = state->gravity_hint; + + if (G_LIKELY (gravity == PANGO2_GRAVITY_AUTO)) + gravity = state->context->resolved_gravity; + + state->resolved_gravity = pango2_gravity_get_for_script_and_width (state->script, + state->width_iter.upright, + gravity, + gravity_hint); + } + + if (state->font_desc_gravity != state->resolved_gravity) + { + pango2_font_description_set_gravity (state->font_desc, state->resolved_gravity); + state->changed |= FONT_CHANGED; + } + } + + if (state->changed & (SCRIPT_CHANGED | LANG_CHANGED)) + { + Pango2Language *old_derived_lang = state->derived_lang; + state->derived_lang = compute_derived_language (state->lang, state->script); + if (old_derived_lang != state->derived_lang) + state->changed |= DERIVED_LANG_CHANGED; + } + + if (state->changed & (EMOJI_CHANGED)) + { + state->changed |= FONT_CHANGED; + } + + if (state->changed & (FONT_CHANGED | DERIVED_LANG_CHANGED) && + state->current_fonts) + { + g_object_unref (state->current_fonts); + state->current_fonts = NULL; + state->cache = NULL; + } + + if (!state->current_fonts) + { + gboolean is_emoji = state->emoji_iter.is_emoji; + if (is_emoji && !state->emoji_font_desc) + { + state->emoji_font_desc = pango2_font_description_copy_static (state->font_desc); + pango2_font_description_set_family_static (state->emoji_font_desc, "emoji"); + } + state->current_fonts = pango2_font_map_load_fontset (state->context->font_map, + state->context, + is_emoji ? state->emoji_font_desc : state->font_desc, + state->derived_lang); + state->cache = get_font_cache (state->current_fonts); + } + + if ((state->changed & FONT_CHANGED) && state->base_font) + { + g_object_unref (state->base_font); + state->base_font = NULL; + } +} + +/* We don't want space characters to affect font selection; in general, + * it's always wrong to select a font just to render a space. + * + * We assume that all fonts have the ASCII space, and for other space + * characters if they don't, HarfBuzz will compatibility-decompose them + * to ASCII space... + * See bugs #355987 and #701652. + * + * We don't want to change fonts just for variation selectors. + * See bug #781123. + * + * We don't want to change fonts for default ignorables such as Cf chars. + * Note that Cf chars in the Arabic block are visible and need to have + * a font, so we exclude. + * + * Finally, don't change fonts for line or paragraph separators. + * + * Note that we want spaces to use the 'better' font, comparing + * the font that is used before and after the space. This is handled + * in itemize_state_add_character(). + */ +static gboolean +consider_as_space (gunichar wc) +{ + GUnicodeType type = g_unichar_type (wc); + return type == G_UNICODE_CONTROL || + (type == G_UNICODE_FORMAT && !((wc >= 0x600 && wc <= 0x06ff) || wc == 0x70f || wc == 0x8e2)) || + type == G_UNICODE_SURROGATE || + type == G_UNICODE_LINE_SEPARATOR || + type == G_UNICODE_PARAGRAPH_SEPARATOR || + (type == G_UNICODE_SPACE_SEPARATOR && wc != 0x1680u /* OGHAM SPACE MARK */) || + (wc >= 0xfe00u && wc <= 0xfe0fu) || + (wc >= 0xe0100u && wc <= 0xe01efu); +} + +static void +itemize_state_process_run (ItemizeState *state) +{ + const char *p; + gboolean last_was_forced_break = FALSE; + gboolean is_space; + gunichar prev_wc = 0; + + /* Only one character has type G_UNICODE_LINE_SEPARATOR in Unicode 4.0; + * update this if that changes. */ +#define LINE_SEPARATOR 0x2028 + + itemize_state_update_for_new_run (state); + + /* We should never get an empty run */ + g_assert (state->run_end != state->run_start); + + for (p = state->run_start; + p < state->run_end; + p = g_utf8_next_char (p)) + { + gunichar wc = g_utf8_get_char (p); + gboolean is_forced_break = wc == '\t' || wc == '\r' || wc == '\n' || + wc == 0x2028 || wc == 0x2029; + Pango2Font *font; + int font_position; + + if (consider_as_space (wc)) + { + font = NULL; + font_position = 0xffff; + is_space = TRUE; + } + else + { + get_font (state, wc, &font, &font_position); + is_space = FALSE; + } + + /* Don't break between \r and \n */ + if (prev_wc == '\r' && wc == '\n') + state->item->num_chars++; + else + itemize_state_add_character (state, font, font_position, + is_forced_break || last_was_forced_break, + p, + is_space); + + last_was_forced_break = is_forced_break; + prev_wc = wc; + } + + /* Finish the final item from the current segment */ + state->item->length = (p - state->text) - state->item->offset; + if (!state->item->analysis.font) + { + Pango2Font *font; + int position; + + if (G_UNLIKELY (!get_font (state, ' ', &font, &position))) + { + /* If no font was found, warn once per fontmap/script pair */ + Pango2FontMap *fontmap = state->context->font_map; + char *script_tag = g_strdup_printf ("g-unicode-script-%d", state->script); + + if (!g_object_get_data (G_OBJECT (fontmap), script_tag)) + { + g_warning ("failed to choose a font, expect ugly output. script='%d'", + state->script); + + g_object_set_data_full (G_OBJECT (fontmap), script_tag, + GINT_TO_POINTER (1), NULL); + } + + g_free (script_tag); + + font = NULL; + } + itemize_state_fill_font (state, font); + } + state->item = NULL; +} + +static void +itemize_state_finish (ItemizeState *state) +{ + g_free (state->embedding_levels); + if (state->free_attr_iter) + pango2_attr_iterator_destroy (state->attr_iter); + _pango2_script_iter_fini (&state->script_iter); + pango2_font_description_free (state->font_desc); + pango2_font_description_free (state->emoji_font_desc); + width_iter_fini (&state->width_iter); + _pango2_emoji_iter_fini (&state->emoji_iter); + + if (state->current_fonts) + g_object_unref (state->current_fonts); + if (state->base_font) + g_object_unref (state->base_font); +} + +/* }}} */ + /* {{{ Post-processing */ + + /* {{{ Handling font scale */ + +typedef struct { + Pango2Attribute *attr; + double scale; +} ScaleItem; + +static gboolean +collect_font_scale (Pango2Context *context, + GList **stack, + Pango2Item *item, + Pango2Item *prev, + double *scale, + gboolean *is_small_caps) +{ + gboolean retval = FALSE; + GList *l; + + for (GSList *l = item->analysis.extra_attrs; l; l = l->next) + { + Pango2Attribute *attr = l->data; + + if (attr->type == PANGO2_ATTR_FONT_SCALE) + { + if (attr->start_index == item->offset) + { + ScaleItem *entry; + int y_scale; + hb_position_t y_size; + hb_position_t cap_height; + hb_position_t x_height; + + entry = g_new (ScaleItem, 1); + entry->attr = attr; + *stack = g_list_prepend (*stack, entry); + + switch (attr->int_value) + { + case PANGO2_FONT_SCALE_NONE: + break; + case PANGO2_FONT_SCALE_SUPERSCRIPT: + if (prev && + hb_ot_metrics_get_position (pango2_font_get_hb_font (prev->analysis.font), + HB_OT_METRICS_TAG_SUPERSCRIPT_EM_Y_SIZE, + &y_size)) + { + hb_font_get_scale (pango2_font_get_hb_font (prev->analysis.font), NULL, &y_scale); + entry->scale = y_size / (double) y_scale; + } + else + { + entry->scale = 1 / 1.2; + } + break; + case PANGO2_FONT_SCALE_SUBSCRIPT: + if (prev && + hb_ot_metrics_get_position (pango2_font_get_hb_font (prev->analysis.font), + HB_OT_METRICS_TAG_SUBSCRIPT_EM_Y_SIZE, + &y_size)) + { + hb_font_get_scale (pango2_font_get_hb_font (prev->analysis.font), NULL, &y_scale); + entry->scale = y_size / (double) y_scale; + } + else + { + entry->scale = 1 / 1.2; + } + break; + case PANGO2_FONT_SCALE_SMALL_CAPS: + if (hb_ot_metrics_get_position (pango2_font_get_hb_font (item->analysis.font), + HB_OT_METRICS_TAG_CAP_HEIGHT, + &cap_height) && + hb_ot_metrics_get_position (pango2_font_get_hb_font (item->analysis.font), + HB_OT_METRICS_TAG_X_HEIGHT, + &x_height)) + { + entry->scale = x_height / (double) cap_height; + } + else + { + entry->scale = 0.8; + } + break; + default: + g_assert_not_reached (); + } + } + } + } + + *scale = 1.0; + *is_small_caps = TRUE; + + for (l = *stack; l; l = l->next) + { + ScaleItem *entry = l->data; + *scale *= entry->scale; + if (entry->attr->int_value != PANGO2_FONT_SCALE_SMALL_CAPS) + *is_small_caps = FALSE; + retval = TRUE; + } + + l = *stack; + while (l) + { + ScaleItem *entry = l->data; + GList *next = l->next; + + if (entry->attr->end_index == item->offset + item->length) + { + *stack = g_list_delete_link (*stack, l); + g_free (entry); + } + + l = next; + } + + return retval; +} + +static void +apply_scale_to_item (Pango2Context *context, + Pango2Item *item, + double scale, + gboolean is_small_caps) +{ + Pango2FontDescription *desc; + double size; + + if (!item->analysis.font) + return; + + if (is_small_caps) + pango2_analysis_set_size_font (&item->analysis, item->analysis.font); + + desc = pango2_font_describe (item->analysis.font); + size = scale * pango2_font_description_get_size (desc); + + if (pango2_font_description_get_size_is_absolute (desc)) + pango2_font_description_set_absolute_size (desc, size); + else + pango2_font_description_set_size (desc, size); + + g_object_unref (item->analysis.font); + item->analysis.font = pango2_font_map_load_font (context->font_map, context, desc); + + pango2_font_description_free (desc); +} + +static void +apply_font_scale (Pango2Context *context, + GList *items) +{ + Pango2Item *prev = NULL; + GList *stack = NULL; + + for (GList *l = items; l; l = l->next) + { + Pango2Item *item = l->data; + double scale; + gboolean is_small_caps; + + if (collect_font_scale (context, &stack, item, prev, &scale, &is_small_caps)) + apply_scale_to_item (context, item, scale, is_small_caps); + + prev = item; + } + + if (stack != NULL) + { + g_warning ("Leftover font scales"); + g_list_free_full (stack, g_free); + } +} + +/* }}} */ +/* { {{ Handling Casing variants */ + +static gboolean +all_features_supported (Pango2Item *item, + hb_tag_t *features, + guint n_features) +{ + hb_font_t *font = pango2_font_get_hb_font (item->analysis.font); + hb_face_t *face = hb_font_get_face (font); + hb_script_t script; + hb_language_t language; + guint script_count = HB_OT_MAX_TAGS_PER_SCRIPT; + hb_tag_t script_tags[HB_OT_MAX_TAGS_PER_SCRIPT]; + hb_tag_t chosen_script; + guint language_count = HB_OT_MAX_TAGS_PER_LANGUAGE; + hb_tag_t language_tags[HB_OT_MAX_TAGS_PER_LANGUAGE]; + guint script_index, language_index; + guint index; + + script = g_unicode_script_to_iso15924 (item->analysis.script); + language = hb_language_from_string (pango2_language_to_string (item->analysis.language), -1); + + hb_ot_tags_from_script_and_language (script, language, + &script_count, script_tags, + &language_count, language_tags); + hb_ot_layout_table_select_script (face, HB_OT_TAG_GSUB, + script_count, script_tags, + &script_index, + &chosen_script); + hb_ot_layout_script_select_language (face, HB_OT_TAG_GSUB, + script_index, + language_count, language_tags, + &language_index); + + for (int i = 0; i < n_features; i++) + { + if (!hb_ot_layout_language_find_feature (face, HB_OT_TAG_GSUB, + script_index, language_index, + features[i], + &index)) + return FALSE; + } + + return TRUE; +} + +static gboolean +variant_supported (Pango2Item *item, + Pango2Variant variant) +{ + hb_tag_t features[2]; + guint num_features = 0; + + switch (variant) + { + case PANGO2_VARIANT_NORMAL: + case PANGO2_VARIANT_TITLE_CAPS: + return TRUE; + case PANGO2_VARIANT_SMALL_CAPS: + features[num_features++] = HB_TAG ('s', 'm', 'c', 'p'); + break; + case PANGO2_VARIANT_ALL_SMALL_CAPS: + features[num_features++] = HB_TAG ('s', 'm', 'c', 'p'); + features[num_features++] = HB_TAG ('c', '2', 's', 'c'); + break; + case PANGO2_VARIANT_PETITE_CAPS: + features[num_features++] = HB_TAG ('p', 'c', 'a', 'p'); + break; + case PANGO2_VARIANT_ALL_PETITE_CAPS: + features[num_features++] = HB_TAG ('p', 'c', 'a', 'p'); + features[num_features++] = HB_TAG ('c', '2', 'p', 'c'); + break; + case PANGO2_VARIANT_UNICASE: + features[num_features++] = HB_TAG ('u', 'n', 'i', 'c'); + break; + default: + g_assert_not_reached (); + } + + return all_features_supported (item, features, num_features); +} + +static Pango2Variant +get_font_variant (Pango2Item *item) +{ + Pango2FontDescription *desc; + Pango2Variant variant = PANGO2_VARIANT_NORMAL; + + if (item->analysis.font) + { + desc = pango2_font_describe (item->analysis.font); + variant = pango2_font_description_get_variant (desc); + pango2_font_description_free (desc); + } + + return variant; +} + +static Pango2TextTransform +find_text_transform (const Pango2Analysis *analysis) +{ + GSList *l; + Pango2TextTransform transform = PANGO2_TEXT_TRANSFORM_NONE; + + for (l = analysis->extra_attrs; l; l = l->next) + { + Pango2Attribute *attr = l->data; + + if (attr->type == PANGO2_ATTR_TEXT_TRANSFORM) + transform = (Pango2TextTransform) attr->int_value; + } + + return transform; +} + +/* Split list_item into upper- and lowercase runs, and + * add font scale and text transform attributes to make + * them be appear according to variant. The log_attrs are + * needed for taking text transforms into account when + * determining the case of characters int he run. + */ +static void +split_item_for_variant (const char *text, + Pango2LogAttr *log_attrs, + Pango2Variant variant, + GList *list_item) +{ + Pango2Item *item = list_item->data; + const char *start, *end; + const char *p, *p0; + gunichar wc; + Pango2TextTransform transform = PANGO2_TEXT_TRANSFORM_NONE; + Pango2FontScale lowercase_scale = PANGO2_FONT_SCALE_NONE; + Pango2FontScale uppercase_scale = PANGO2_FONT_SCALE_NONE; + Pango2TextTransform item_transform; + gboolean is_word_start; + int offset; + + switch (variant) + { + case PANGO2_VARIANT_ALL_SMALL_CAPS: + case PANGO2_VARIANT_ALL_PETITE_CAPS: + uppercase_scale = PANGO2_FONT_SCALE_SMALL_CAPS; + G_GNUC_FALLTHROUGH; + case PANGO2_VARIANT_SMALL_CAPS: + case PANGO2_VARIANT_PETITE_CAPS: + transform = PANGO2_TEXT_TRANSFORM_UPPERCASE; + lowercase_scale = PANGO2_FONT_SCALE_SMALL_CAPS; + break; + case PANGO2_VARIANT_UNICASE: + uppercase_scale = PANGO2_FONT_SCALE_SMALL_CAPS; + break; + case PANGO2_VARIANT_NORMAL: + case PANGO2_VARIANT_TITLE_CAPS: + default: + g_assert_not_reached (); + } + + item_transform = find_text_transform (&item->analysis); + + start = text + item->offset; + end = start + item->length; + offset = item->char_offset; + + p = start; + while (p < end) + { + p0 = p; + wc = g_utf8_get_char (p); + is_word_start = log_attrs && log_attrs[offset].is_word_start; + while (p < end && (item_transform == PANGO2_TEXT_TRANSFORM_LOWERCASE || + consider_as_space (wc) || + (g_unichar_islower (wc) && + !(item_transform == PANGO2_TEXT_TRANSFORM_UPPERCASE || + (item_transform == PANGO2_TEXT_TRANSFORM_CAPITALIZE && is_word_start))))) + { + p = g_utf8_next_char (p); + wc = g_utf8_get_char (p); + offset++; + is_word_start = log_attrs && log_attrs[offset].is_word_start; + } + + if (p0 < p) + { + Pango2Item *new_item; + Pango2Attribute *attr; + + /* p0 .. p is a lowercase segment */ + if (p < end) + { + new_item = pango2_item_split (item, p - p0, g_utf8_strlen (p0, p - p0)); + list_item->data = new_item; + list_item = g_list_insert_before (list_item, list_item->next, item); + list_item = list_item->next; + } + else + { + new_item = item; + } + + if (transform != PANGO2_TEXT_TRANSFORM_NONE) + { + attr = pango2_attr_text_transform_new (transform); + attr->start_index = new_item->offset; + attr->end_index = new_item->offset + new_item->length; + new_item->analysis.extra_attrs = g_slist_append (new_item->analysis.extra_attrs, attr); + } + + if (lowercase_scale != PANGO2_FONT_SCALE_NONE) + { + attr = pango2_attr_font_scale_new (lowercase_scale); + attr->start_index = new_item->offset; + attr->end_index = new_item->offset + new_item->length; + new_item->analysis.extra_attrs = g_slist_append (new_item->analysis.extra_attrs, attr); + } + } + + p0 = p; + wc = g_utf8_get_char (p); + is_word_start = log_attrs && log_attrs[offset].is_word_start; + while (p < end && (item_transform == PANGO2_TEXT_TRANSFORM_UPPERCASE || + consider_as_space (wc) || + !(item_transform == PANGO2_TEXT_TRANSFORM_LOWERCASE || g_unichar_islower (wc)) || + (item_transform == PANGO2_TEXT_TRANSFORM_CAPITALIZE && is_word_start))) + { + p = g_utf8_next_char (p); + wc = g_utf8_get_char (p); + offset++; + is_word_start = log_attrs && log_attrs[offset].is_word_start; + } + + if (p0 < p) + { + Pango2Item *new_item; + Pango2Attribute *attr; + + /* p0 .. p is a uppercase segment */ + if (p < end) + { + new_item = pango2_item_split (item, p - p0, g_utf8_strlen (p0, p - p0)); + list_item->data = new_item; + list_item = g_list_insert_before (list_item, list_item->next, item); + list_item = list_item->next; + } + else + { + new_item = item; + } + + if (uppercase_scale != PANGO2_FONT_SCALE_NONE) + { + attr = pango2_attr_font_scale_new (uppercase_scale); + attr->start_index = new_item->offset; + attr->end_index = new_item->offset + new_item->length; + new_item->analysis.extra_attrs = g_slist_append (new_item->analysis.extra_attrs, attr); + } + } + } +} + +static void +handle_variants_for_item (const char *text, + Pango2LogAttr *log_attrs, + GList *l) +{ + Pango2Item *item = l->data; + Pango2Variant variant; + + variant = get_font_variant (item); + if (!variant_supported (item, variant)) + split_item_for_variant (text, log_attrs, variant, l); +} + +static void +handle_variants (const char *text, + Pango2LogAttr *log_attrs, + GList *items) +{ + GList *next; + + for (GList *l = items; l; l = next) + { + next = l->next; + handle_variants_for_item (text, log_attrs, l); + } +} + +/* }}} */ + +static GList * +reorder_items (Pango2Context *context, + GList *items) +{ + int char_offset = 0; + + items = g_list_reverse (items); + + /* Also cmpute the char offset for each item here */ + for (GList *l = items; l; l = l->next) + { + Pango2Item *item = l->data; + item->char_offset = char_offset; + char_offset += item->num_chars; + } + + return items; +} + +static GList * +post_process_items (Pango2Context *context, + const char *text, + Pango2LogAttr *log_attrs, + GList *items) +{ + handle_variants (text, log_attrs, items); + apply_font_scale (context, items); + + return items; +} + +/* }}} */ +/* {{{ Private API */ + +/* Like pango2_itemize, but takes a font description. + * In contrast to pango2_itemize, this function does + * not call pango2_itemize_post_process_items, so you need to do that + * separately, after applying attributes that affect segmentation and + * computing the log attrs. + */ +GList * +pango2_itemize_with_font (Pango2Context *context, + Pango2Direction base_dir, + const char *text, + int start_index, + int length, + Pango2AttrList *attrs, + Pango2AttrIterator *cached_iter, + const Pango2FontDescription *desc) +{ + ItemizeState state; + + g_return_val_if_fail (context->font_map != NULL, NULL); + + if (length == 0 || g_utf8_get_char (text + start_index) == '\0') + return NULL; + + itemize_state_init (&state, context, text, base_dir, start_index, length, + attrs, cached_iter, desc); + + do + itemize_state_process_run (&state); + while (itemize_state_next (&state)); + + itemize_state_finish (&state); + + return reorder_items (context, state.result); +} + +/* Apply post-processing steps that may require log attrs. + */ +GList * +pango2_itemize_post_process_items (Pango2Context *context, + const char *text, + Pango2LogAttr *log_attrs, + GList *items) +{ + return post_process_items (context, text, log_attrs, items); +} + +/* }}} */ +/* {{{ Public API */ + +/** + * pango2_itemize: + * @context: a structure holding information that affects + * the itemization process. + * @base_dir: base direction to use for bidirectional processing + * @text: the text to itemize. + * @start_index: first byte in @text to process + * @length: the number of bytes (not characters) to process + * after @start_index. This must be >= 0. + * @attrs: the set of attributes that apply to @text. + * + * Breaks a piece of text into segments with consistent directional + * level and font. + * + * Each byte of @text will be contained in exactly one of the items in the + * returned list; the generated list of items will be in logical order (the + * start offsets of the items are ascending). + * + * The base direction is used when computing bidirectional levels. + * [func@itemize] gets the base direction from the `Pango2Context` + * (see [method@Pango2.Context.set_base_dir]). + * + * Return value: (transfer full) (element-type Pango2.Item): a `GList` of + * [struct@Pango2.Item] structures. The items should be freed using + * [method@Pango2.Item.free] probably in combination with [func@GLib.List.free_full]. + */ +GList * +pango2_itemize (Pango2Context *context, + Pango2Direction base_dir, + const char *text, + int start_index, + int length, + Pango2AttrList *attrs) +{ + GList *items; + + g_return_val_if_fail (context != NULL, NULL); + g_return_val_if_fail (start_index >= 0, NULL); + g_return_val_if_fail (length >= 0, NULL); + g_return_val_if_fail (length == 0 || text != NULL, NULL); + + items = pango2_itemize_with_font (context, base_dir, + text, start_index, length, + attrs, NULL, NULL); + + return pango2_itemize_post_process_items (context, text, NULL, items); +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/json/gtkjsonparser.c b/pango2/json/gtkjsonparser.c new file mode 100644 index 00000000..0599d8f1 --- /dev/null +++ b/pango2/json/gtkjsonparser.c @@ -0,0 +1,1737 @@ +/* + * Copyright © 2021 Benjamin Otte + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + * + * Authors: Benjamin Otte <otte@gnome.org> + */ + + +#include "config.h" + +#include "gtkjsonparserprivate.h" +#include <stdlib.h> +#include <errno.h> + +typedef struct _GtkJsonBlock GtkJsonBlock; + +typedef enum { + GTK_JSON_BLOCK_TOPLEVEL, + GTK_JSON_BLOCK_OBJECT, + GTK_JSON_BLOCK_ARRAY, +} GtkJsonBlockType; + +struct _GtkJsonBlock +{ + GtkJsonBlockType type; + const guchar *value; /* start of current value to be consumed by external code */ + const guchar *member_name; /* name of current value, only used for object types */ + gsize index; /* index of the current element */ +}; + +struct _GtkJsonParser +{ + GBytes *bytes; + const guchar *reader; /* current read head, pointing as far as we've read */ + const guchar *start; /* pointer at start of data, after optional BOM */ + const guchar *end; /* pointer after end of data we're reading */ + + GError *error; /* if an error has happened, it's stored here. Errors aren't recoverable. */ + const guchar *error_start; /* start of error location */ + const guchar *error_end; /* end of error location */ + + GtkJsonBlock *block; /* current block */ + GtkJsonBlock *blocks; /* blocks array */ + GtkJsonBlock *blocks_end; /* blocks array */ + GtkJsonBlock blocks_preallocated[128]; /* preallocated */ +}; + +typedef enum { + WHITESPACE = (1 << 4), + NEWLINE = (1 << 5), + STRING_ELEMENT = (1 << 6), + STRING_MARKER = (1 << 7), +} JsonCharacterType; + +#define JSON_CHARACTER_NODE_MASK ((1 << 4) - 1) + +static const guchar json_character_table[256] = { + ['\t'] = WHITESPACE, + ['\r'] = WHITESPACE | NEWLINE, + ['\n'] = WHITESPACE | NEWLINE, + [' '] = WHITESPACE | STRING_ELEMENT, + ['!'] = STRING_ELEMENT, + ['"'] = GTK_JSON_STRING | STRING_MARKER, + ['#'] = STRING_ELEMENT, + ['$'] = STRING_ELEMENT, + ['%'] = STRING_ELEMENT, + ['&'] = STRING_ELEMENT, + ['\''] = STRING_ELEMENT, + ['('] = STRING_ELEMENT, + [')'] = STRING_ELEMENT, + ['*'] = STRING_ELEMENT, + ['+'] = STRING_ELEMENT, + [','] = STRING_ELEMENT, + ['-'] = GTK_JSON_NUMBER | STRING_ELEMENT, + ['.'] = STRING_ELEMENT, + ['/'] = STRING_ELEMENT, + ['0'] = GTK_JSON_NUMBER | STRING_ELEMENT, + ['1'] = GTK_JSON_NUMBER | STRING_ELEMENT, + ['2'] = GTK_JSON_NUMBER | STRING_ELEMENT, + ['3'] = GTK_JSON_NUMBER | STRING_ELEMENT, + ['4'] = GTK_JSON_NUMBER | STRING_ELEMENT, + ['5'] = GTK_JSON_NUMBER | STRING_ELEMENT, + ['6'] = GTK_JSON_NUMBER | STRING_ELEMENT, + ['7'] = GTK_JSON_NUMBER | STRING_ELEMENT, + ['8'] = GTK_JSON_NUMBER | STRING_ELEMENT, + ['9'] = GTK_JSON_NUMBER | STRING_ELEMENT, + [':'] = STRING_ELEMENT, + [';'] = STRING_ELEMENT, + ['<'] = STRING_ELEMENT, + ['='] = STRING_ELEMENT, + ['>'] = STRING_ELEMENT, + ['?'] = STRING_ELEMENT, + ['@'] = STRING_ELEMENT, + ['A'] = STRING_ELEMENT, + ['B'] = STRING_ELEMENT, + ['C'] = STRING_ELEMENT, + ['D'] = STRING_ELEMENT, + ['E'] = STRING_ELEMENT, + ['F'] = STRING_ELEMENT, + ['G'] = STRING_ELEMENT, + ['H'] = STRING_ELEMENT, + ['I'] = STRING_ELEMENT, + ['J'] = STRING_ELEMENT, + ['K'] = STRING_ELEMENT, + ['L'] = STRING_ELEMENT, + ['M'] = STRING_ELEMENT, + ['N'] = STRING_ELEMENT, + ['O'] = STRING_ELEMENT, + ['P'] = STRING_ELEMENT, + ['Q'] = STRING_ELEMENT, + ['R'] = STRING_ELEMENT, + ['S'] = STRING_ELEMENT, + ['T'] = STRING_ELEMENT, + ['U'] = STRING_ELEMENT, + ['V'] = STRING_ELEMENT, + ['W'] = STRING_ELEMENT, + ['X'] = STRING_ELEMENT, + ['Y'] = STRING_ELEMENT, + ['Z'] = STRING_ELEMENT, + ['['] = GTK_JSON_ARRAY | STRING_ELEMENT, + ['\\'] = STRING_MARKER, + [']'] = STRING_ELEMENT, + ['^'] = STRING_ELEMENT, + ['_'] = STRING_ELEMENT, + ['`'] = STRING_ELEMENT, + ['a'] = STRING_ELEMENT, + ['b'] = STRING_ELEMENT, + ['c'] = STRING_ELEMENT, + ['d'] = STRING_ELEMENT, + ['e'] = STRING_ELEMENT, + ['f'] = GTK_JSON_BOOLEAN | STRING_ELEMENT, + ['g'] = STRING_ELEMENT, + ['h'] = STRING_ELEMENT, + ['i'] = STRING_ELEMENT, + ['j'] = STRING_ELEMENT, + ['k'] = STRING_ELEMENT, + ['l'] = STRING_ELEMENT, + ['m'] = STRING_ELEMENT, + ['n'] = GTK_JSON_NULL | STRING_ELEMENT, + ['o'] = STRING_ELEMENT, + ['p'] = STRING_ELEMENT, + ['q'] = STRING_ELEMENT, + ['r'] = STRING_ELEMENT, + ['s'] = STRING_ELEMENT, + ['t'] = GTK_JSON_BOOLEAN | STRING_ELEMENT, + ['u'] = STRING_ELEMENT, + ['v'] = STRING_ELEMENT, + ['w'] = STRING_ELEMENT, + ['x'] = STRING_ELEMENT, + ['y'] = STRING_ELEMENT, + ['z'] = STRING_ELEMENT, + ['{'] = GTK_JSON_OBJECT | STRING_ELEMENT, + ['|'] = STRING_ELEMENT, + ['}'] = STRING_ELEMENT, + ['~'] = STRING_ELEMENT, + [127] = STRING_ELEMENT, +}; + +static const guchar * +json_skip_characters (const guchar *start, + const guchar *end, + JsonCharacterType type) +{ + const guchar *s; + + for (s = start; s < end; s++) + { + if (!(json_character_table[*s] & type)) + break; + } + return s; +} + +static const guchar * +json_skip_characters_until (const guchar *start, + const guchar *end, + JsonCharacterType type) +{ + const guchar *s; + + for (s = start; s < end; s++) + { + if (json_character_table[*s] & type) + break; + } + return s; +} + +static const guchar * +json_find_character (const guchar *start, + JsonCharacterType type) +{ + const guchar *s; + + for (s = start; ; s++) + { + if ((json_character_table[*s] & type)) + break; + } + return s; +} + +GQuark +gtk_json_error_quark (void) +{ + return g_quark_from_static_string ("gtk-json-error-quark"); +} + +static void +gtk_json_parser_take_error (GtkJsonParser *self, + const guchar *start_location, + const guchar *end_location, + GError *error) +{ + g_assert (start_location <= end_location); + g_assert (self->start <= start_location); + g_assert (end_location <= self->end); + + if (self->error) + { + g_error_free (error); + return; + } + + self->error = error; + self->error_start = start_location; + self->error_end = end_location; +} + +static void +gtk_json_parser_syntax_error_at (GtkJsonParser *self, + const guchar *error_start, + const guchar *error_end, + const char *format, + ...) G_GNUC_PRINTF(4, 5); +static void +gtk_json_parser_syntax_error_at (GtkJsonParser *self, + const guchar *error_start, + const guchar *error_end, + const char *format, + ...) +{ + va_list args; + + if (self->error) + return; + + va_start (args, format); + gtk_json_parser_take_error (self, + error_start, + error_end, + g_error_new_valist (GTK_JSON_ERROR, + GTK_JSON_ERROR_SYNTAX, + format, args)); + va_end (args); +} + +static void +gtk_json_parser_syntax_error (GtkJsonParser *self, + const char *format, + ...) G_GNUC_PRINTF(2, 3); +static void +gtk_json_parser_syntax_error (GtkJsonParser *self, + const char *format, + ...) +{ + va_list args; + const guchar *error_end; + + if (self->error) + return; + + va_start (args, format); + for (error_end = self->reader; + error_end < self->end && g_ascii_isalnum (*error_end); + error_end++) + ; + if (error_end == self->reader && + g_utf8_get_char_validated ((const char *) error_end, self->end - error_end) < (gunichar) -2) + { + error_end = (const guchar *) g_utf8_next_char (error_end); + } + + gtk_json_parser_take_error (self, + self->reader, + error_end, + g_error_new_valist (GTK_JSON_ERROR, + GTK_JSON_ERROR_SYNTAX, + format, args)); + va_end (args); +} + +static void +gtk_json_parser_type_error (GtkJsonParser *self, + const char *format, + ...) G_GNUC_PRINTF(2, 3); +static void +gtk_json_parser_type_error (GtkJsonParser *self, + const char *format, + ...) +{ + const guchar *start_location; + va_list args; + + if (self->error) + return; + + if (self->block->value) + start_location = self->block->value; + else if (self->block != self->blocks) + start_location = self->block[-1].value; + else + start_location = self->start; + + va_start (args, format); + gtk_json_parser_take_error (self, + start_location, + self->reader, + g_error_new_valist (GTK_JSON_ERROR, + GTK_JSON_ERROR_TYPE, + format, args)); + va_end (args); +} + +void +gtk_json_parser_value_error (GtkJsonParser *self, + const char *format, + ...) +{ + const guchar *start_location; + va_list args; + + if (self->error) + return; + + if (self->block->value) + start_location = self->block->value; + else if (self->block != self->blocks) + start_location = self->block[-1].value; + else + start_location = self->start; + + va_start (args, format); + gtk_json_parser_take_error (self, + start_location, + self->reader, + g_error_new_valist (GTK_JSON_ERROR, + GTK_JSON_ERROR_VALUE, + format, args)); + va_end (args); +} + +void +gtk_json_parser_schema_error (GtkJsonParser *self, + const char *format, + ...) +{ + const guchar *start_location; + va_list args; + + if (self->error) + return; + + if (self->block->member_name) + start_location = self->block->member_name; + if (self->block->value) + start_location = self->block->value; + else if (self->block != self->blocks) + start_location = self->block[-1].value; + else + start_location = self->start; + + va_start (args, format); + gtk_json_parser_take_error (self, + start_location, + self->reader, + g_error_new_valist (GTK_JSON_ERROR, + GTK_JSON_ERROR_SCHEMA, + format, args)); + va_end (args); +} + +static gboolean +gtk_json_parser_is_eof (GtkJsonParser *self) +{ + return self->reader >= self->end; +} + +static gsize +gtk_json_parser_remaining (GtkJsonParser *self) +{ + g_return_val_if_fail (self->reader <= self->end, 0); + + return self->end - self->reader; +} + +static void +gtk_json_parser_skip_bom (GtkJsonParser *self) +{ + if (gtk_json_parser_remaining (self) < 3) + return; + + if (self->reader[0] == 0xEF && + self->reader[1] == 0xBB && + self->reader[2] == 0xBF) + self->reader += 3; +} + +static void +gtk_json_parser_skip_whitespace (GtkJsonParser *self) +{ + self->reader = json_skip_characters (self->reader, self->end, WHITESPACE); +} + +static gboolean +gtk_json_parser_has_char (GtkJsonParser *self, + char c) +{ + return gtk_json_parser_remaining (self) && *self->reader == c; +} + +static gboolean +gtk_json_parser_try_char (GtkJsonParser *self, + char c) +{ + if (!gtk_json_parser_has_char (self, c)) + return FALSE; + + self->reader++; + return TRUE; +} + +static gboolean +gtk_json_parser_try_identifier_len (GtkJsonParser *self, + const char *ident, + gsize len) +{ + if (gtk_json_parser_remaining (self) < len) + return FALSE; + + if (memcmp (self->reader, ident, len) != 0) + return FALSE; + + self->reader += len; + return TRUE; +} + +#define gtk_json_parser_try_identifier(parser, ident) gtk_json_parser_try_identifier_len(parser, ident, strlen(ident)) + +/* + * decode_utf16_surrogate_pair: + * @first: the first UTF-16 code point + * @second: the second UTF-16 code point + * + * Decodes a surrogate pair of UTF-16 code points into the equivalent + * Unicode code point. + * + * If the code points are not valid, 0 is returned. + * + * Returns: the Unicode code point equivalent to the surrogate pair + */ +static inline gunichar +decode_utf16_surrogate_pair (gunichar first, + gunichar second) +{ + if (0xd800 > first || first > 0xdbff || + 0xdc00 > second || second > 0xdfff) + return 0; + + return 0x10000 + | (first & 0x3ff) << 10 + | (second & 0x3ff); +} + +static gsize +gtk_json_unescape_char (const guchar *json_escape, + char out_data[6], + gsize *out_len) +{ + switch (json_escape[1]) + { + case '"': + case '\\': + case '/': + out_data[0] = json_escape[1]; + *out_len = 1; + return 2; + case 'b': + out_data[0] = '\b'; + *out_len = 1; + return 2; + case 'f': + out_data[0] = '\f'; + *out_len = 1; + return 2; + case 'n': + out_data[0] = '\n'; + *out_len = 1; + return 2; + case 'r': + out_data[0] = '\r'; + *out_len = 1; + return 2; + case 't': + out_data[0] = '\t'; + *out_len = 1; + return 2; + case 'u': + { + gunichar unichar = (g_ascii_xdigit_value (json_escape[2]) << 12) | + (g_ascii_xdigit_value (json_escape[3]) << 8) | + (g_ascii_xdigit_value (json_escape[4]) << 4) | + (g_ascii_xdigit_value (json_escape[5])); + gsize result = 6; + + /* resolve UTF-16 surrogates for Unicode characters not in the BMP, + * as per ECMA 404, § 9, "String" + */ + if (g_unichar_type (unichar) == G_UNICODE_SURROGATE) + { + unichar = decode_utf16_surrogate_pair (unichar, + (g_ascii_xdigit_value (json_escape[8]) << 12) | + (g_ascii_xdigit_value (json_escape[9]) << 8) | + (g_ascii_xdigit_value (json_escape[10]) << 4) | + (g_ascii_xdigit_value (json_escape[11]))); + result += 6; + } + *out_len = g_unichar_to_utf8 (unichar, out_data); + return result; + } + default: + g_assert_not_reached (); + return 0; + } +} + +typedef struct _JsonStringIter JsonStringIter; +struct _JsonStringIter +{ + char buf[6]; + const guchar *s; + const guchar *next; +}; + +static gsize +json_string_iter_next (JsonStringIter *iter) +{ + gsize len; + + iter->s = iter->next; + iter->next = json_find_character (iter->s, STRING_MARKER); + if (iter->next != iter->s) + return iter->next - iter->s; + if (*iter->next == '"') + return 0; + iter->next += gtk_json_unescape_char (iter->next, iter->buf, &len); + iter->s = (const guchar *) iter->buf; + return len; +} + +/* The escaped string MUST be valid json, so it must begin + * with " and end with " and must not contain any invalid + * escape codes. + * This function is meant to be fast + */ +static gsize +json_string_iter_init (JsonStringIter *iter, + const guchar *string) +{ + g_assert (*string == '"'); + + iter->next = string + 1; + + return json_string_iter_next (iter); +} + +static gboolean +json_string_iter_has_next (JsonStringIter *iter) +{ + return *iter->next != '"'; +} + +static const char * +json_string_iter_get (JsonStringIter *iter) +{ + return (const char *) iter->s; +} + +/* The escaped string MUST be valid json, so it must begin + * with " and end with " and must not contain any invalid + * escape codes. + * This function is meant to be fast + */ +static char * +gtk_json_unescape_string (const guchar *escaped) +{ + JsonStringIter iter; + GString *string; + gsize len; + + len = json_string_iter_init (&iter, escaped); + string = NULL; + + if (!json_string_iter_has_next (&iter)) + return g_strndup (json_string_iter_get (&iter), len); + + string = g_string_new (NULL); + + do + { + g_string_append_len (string, json_string_iter_get (&iter), len); + } + while ((len = json_string_iter_next (&iter))); + + return g_string_free (string, FALSE); +} + +static gboolean +gtk_json_parser_parse_string (GtkJsonParser *self) +{ + const guchar *start; + + start = self->reader; + + if (!gtk_json_parser_try_char (self, '"')) + { + gtk_json_parser_type_error (self, "Not a string"); + return FALSE; + } + + self->reader = json_skip_characters (self->reader, self->end, STRING_ELEMENT); + + while (gtk_json_parser_remaining (self)) + { + if (*self->reader < 0x20) + { + if (*self->reader == '\r' || *self->reader == '\n') + gtk_json_parser_syntax_error (self, "Newlines in strings are not allowed"); + else if (*self->reader == '\t') + gtk_json_parser_syntax_error (self, "Tabs not allowed in strings"); + else + gtk_json_parser_syntax_error (self, "Disallowed control character in string literal"); + return FALSE; + } + else if (*self->reader > 127) + { + gunichar c = g_utf8_get_char_validated ((const char *) self->reader, gtk_json_parser_remaining (self)); + if (c == (gunichar) -2 || c == (gunichar) -1) + { + gtk_json_parser_syntax_error (self, "Invalid UTF-8"); + return FALSE; + } + self->reader = (const guchar *) g_utf8_next_char ((const char *) self->reader); + } + else if (*self->reader == '"') + { + self->reader++; + return TRUE; + } + else if (*self->reader == '\\') + { + if (gtk_json_parser_remaining (self) < 2) + { + self->reader = self->end; + goto end; + } + switch (self->reader[1]) + { + case '"': + case '\\': + case '/': + case 'b': + case 'f': + case 'n': + case 'r': + case 't': + break; + + case 'u': + /* lots of work necessary to validate the unicode escapes here */ + if (gtk_json_parser_remaining (self) < 6 || + !g_ascii_isxdigit (self->reader[2]) || + !g_ascii_isxdigit (self->reader[3]) || + !g_ascii_isxdigit (self->reader[4]) || + !g_ascii_isxdigit (self->reader[5])) + { + const guchar *end; + for (end = self->reader + 2; + end < self->reader + 6 && end < self->end; + end++) + { + if (!g_ascii_isxdigit (*end)) + break; + } + gtk_json_parser_syntax_error_at (self, self->reader, end, "Invalid Unicode escape sequence"); + return FALSE; + } + else + { + gsize escape_size = 6; + gunichar unichar = (g_ascii_xdigit_value (self->reader[2]) << 12) | + (g_ascii_xdigit_value (self->reader[3]) << 8) | + (g_ascii_xdigit_value (self->reader[4]) << 4) | + (g_ascii_xdigit_value (self->reader[5])); + + /* resolve UTF-16 surrogates for Unicode characters not in the BMP, + * as per ECMA 404, § 9, "String" + */ + if (g_unichar_type (unichar) == G_UNICODE_SURROGATE) + { + if (gtk_json_parser_remaining (self) >= 12 && + self->reader[6] == '\\' && + self->reader[7] == 'u' && + g_ascii_isxdigit (self->reader[8]) && + g_ascii_isxdigit (self->reader[9]) && + g_ascii_isxdigit (self->reader[10]) && + g_ascii_isxdigit (self->reader[11])) + { + unichar = decode_utf16_surrogate_pair (unichar, + (g_ascii_xdigit_value (self->reader[8]) << 12) | + (g_ascii_xdigit_value (self->reader[9]) << 8) | + (g_ascii_xdigit_value (self->reader[10]) << 4) | + (g_ascii_xdigit_value (self->reader[11]))); + escape_size += 6; + } + else + { + unichar = 0; + } + + if (unichar == 0) + { + gtk_json_parser_syntax_error_at (self, self->reader, self->reader + escape_size, "Invalid UTF-16 surrogate pair"); + return FALSE; + } + + self->reader += escape_size - 2; + } + } + break; + default: + if (g_utf8_get_char_validated ((const char *) self->reader + 1, self->end - self->reader - 1) < (gunichar) -2) + gtk_json_parser_syntax_error_at (self, self->reader, (const guchar *) g_utf8_next_char (self->reader + 1), "Unknown escape sequence"); + else + gtk_json_parser_syntax_error_at (self, self->reader, self->reader + 1, "Unknown escape sequence"); + return FALSE; + } + self->reader += 2; + } + + self->reader = json_skip_characters (self->reader, self->end, STRING_ELEMENT); + } + +end: + gtk_json_parser_syntax_error_at (self, start, self->reader, "Unterminated string literal"); + return FALSE; +} + +static gboolean +gtk_json_parser_parse_number (GtkJsonParser *self) +{ + const guchar *start = self->reader; + gboolean have_sign; + + /* sign */ + have_sign = gtk_json_parser_try_char (self, '-'); + + /* integer part */ + if (gtk_json_parser_try_char (self, '0')) + { + /* Technically, "01" in the JSON grammar would be 2 numbers: + * "0" followed by "1". + * Practically, nobody understands that it's 2 numbers, so we + * special-purpose an error message for it, because 2 numbers + * can never follow each other. + */ + if (!gtk_json_parser_is_eof (self) && + g_ascii_isdigit (*self->reader)) + { + do + { + self->reader++; + } + while (!gtk_json_parser_is_eof (self) && + g_ascii_isdigit (*self->reader)); + + gtk_json_parser_syntax_error_at (self, start, self->reader, "Numbers may not start with leading 0s"); + return FALSE; + } + } + else + { + if (gtk_json_parser_is_eof (self) || + !g_ascii_isdigit (*self->reader)) + { + if (have_sign) + gtk_json_parser_syntax_error_at (self, start, self->reader, "Expected a number after '-' character"); + else + gtk_json_parser_type_error (self, "Not a number"); + return FALSE; + } + + self->reader++; + + while (!gtk_json_parser_is_eof (self) && g_ascii_isdigit (*self->reader)) + self->reader++; + } + + /* fractional part */ + if (gtk_json_parser_try_char (self, '.')) + { + if (!g_ascii_isdigit (*self->reader)) + { + gtk_json_parser_syntax_error_at (self, start, self->reader, "Expected a digit after '.'"); + return FALSE; + } + + do + { + self->reader++; + } + while (!gtk_json_parser_is_eof (self) && g_ascii_isdigit (*self->reader)); + } + + /* exponent */ + if (gtk_json_parser_try_char (self, 'e') || + gtk_json_parser_try_char (self, 'E')) + { + if (!gtk_json_parser_try_char (self, '-')) + gtk_json_parser_try_char (self, '+'); + + if (!g_ascii_isdigit (*self->reader)) + { + gtk_json_parser_syntax_error_at (self, start, self->reader, "Expected a digit in exponent"); + return FALSE; + } + + do + { + self->reader++; + } + while (!gtk_json_parser_is_eof (self) && g_ascii_isdigit (*self->reader)); + } + return TRUE; +} + +static gboolean +gtk_json_parser_parse_value (GtkJsonParser *self) +{ + if (gtk_json_parser_is_eof (self)) + { + gtk_json_parser_syntax_error (self, "Unexpected end of document"); + return FALSE; + } + + switch (json_character_table[*self->block->value] & JSON_CHARACTER_NODE_MASK) + { + case GTK_JSON_STRING: + return gtk_json_parser_parse_string (self); + + case GTK_JSON_NUMBER: + return gtk_json_parser_parse_number (self); + + case GTK_JSON_NULL: + if (gtk_json_parser_try_identifier (self, "null")) + return TRUE; + break; + + case GTK_JSON_BOOLEAN: + if (gtk_json_parser_try_identifier (self, "true") || + gtk_json_parser_try_identifier (self, "false")) + return TRUE; + break; + + case GTK_JSON_OBJECT: + case GTK_JSON_ARRAY: + /* don't preparse objects */ + return TRUE; + + default: + break; + } + + if (gtk_json_parser_remaining (self) >= 2 && + (self->block->value[0] == '.' || self->block->value[0] == '+') && + g_ascii_isdigit (self->block->value[1])) + { + const guchar *end = self->block->value + 2; + while (end < self->end && g_ascii_isalnum (*end)) + end++; + gtk_json_parser_syntax_error_at (self, self->block->value, end, "Numbers may not start with '%c'", *self->block->value); + } + else if (*self->reader == 0) + gtk_json_parser_syntax_error (self, "Unexpected nul byte in document"); + else + gtk_json_parser_syntax_error (self, "Expected a value"); + return FALSE; +} + +static void +gtk_json_parser_push_block (GtkJsonParser *self, + GtkJsonBlockType type) +{ + self->block++; + if (self->block == self->blocks_end) + { + gsize old_size = self->blocks_end - self->blocks; + gsize new_size = old_size + 128; + + if (self->blocks == self->blocks_preallocated) + { + self->blocks = g_new (GtkJsonBlock, new_size); + memcpy (self->blocks, self->blocks_preallocated, sizeof (GtkJsonBlock) * G_N_ELEMENTS (self->blocks_preallocated)); + } + else + { + self->blocks = g_renew (GtkJsonBlock, self->blocks, new_size); + } + self->blocks_end = self->blocks + new_size; + self->block = self->blocks + old_size; + } + + self->block->type = type; + self->block->member_name = 0; + self->block->value = 0; + self->block->index = 0; +} + +static void +gtk_json_parser_pop_block (GtkJsonParser *self) +{ + g_assert (self->block > self->blocks); + self->block--; +} + +GtkJsonParser * +gtk_json_parser_new_for_string (const char *string, + gssize size) +{ + GtkJsonParser *self; + GBytes *bytes; + + bytes = g_bytes_new (string, size >= 0 ? size : strlen (string)); + + self = gtk_json_parser_new_for_bytes (bytes); + + g_bytes_unref (bytes); + + return self; +} + +GtkJsonParser * +gtk_json_parser_new_for_bytes (GBytes *bytes) +{ + GtkJsonParser *self; + gsize size; + + g_return_val_if_fail (bytes != NULL, NULL); + + self = g_slice_new0 (GtkJsonParser); + + self->bytes = g_bytes_ref (bytes); + self->reader = g_bytes_get_data (bytes, &size); + self->end = self->reader + size; + + self->blocks = self->blocks_preallocated; + self->blocks_end = self->blocks + G_N_ELEMENTS (self->blocks_preallocated); + self->block = self->blocks; + self->block->type = GTK_JSON_BLOCK_TOPLEVEL; + + gtk_json_parser_skip_bom (self); + self->start = self->reader; + gtk_json_parser_rewind (self); + + return self; +} + +void +gtk_json_parser_free (GtkJsonParser *self) +{ + if (self == NULL) + return; + + g_bytes_unref (self->bytes); + + if (self->blocks != self->blocks_preallocated) + g_free (self->blocks); + + if (self->error) + g_error_free (self->error); + + g_slice_free (GtkJsonParser, self); +} + +static gboolean +gtk_json_parser_skip_block (GtkJsonParser *self) +{ + gsize depth; + + if (self->reader != self->block->value) + return TRUE; + + depth = gtk_json_parser_get_depth (self); + while (TRUE) + { + if (*self->reader == '{') + { + if (!gtk_json_parser_start_object (self)) + return FALSE; + } + else if (*self->reader == '[') + { + if (!gtk_json_parser_start_array (self)) + return FALSE; + } + + while (self->reader != self->block->value) + { + /* This should never be reentrant to this function or we might + * loop causing stack overflow */ + if (!gtk_json_parser_next (self)) + { + if (!gtk_json_parser_end (self)) + return FALSE; + if (depth >= gtk_json_parser_get_depth (self)) + return TRUE; + } + } + } + + return TRUE; +} + +gboolean +gtk_json_parser_next (GtkJsonParser *self) +{ + if (self->error) + return FALSE; + + if (self->block->value == NULL) + return FALSE; + + if (!gtk_json_parser_skip_block (self)) + { + g_assert (self->error); + return FALSE; + } + + switch (self->block->type) + { + case GTK_JSON_BLOCK_TOPLEVEL: + gtk_json_parser_skip_whitespace (self); + if (gtk_json_parser_is_eof (self)) + { + self->block->value = NULL; + } + else if (*self->reader == 0) + { + gtk_json_parser_syntax_error (self, "Unexpected nul byte in document"); + } + else + { + gtk_json_parser_syntax_error_at (self, self->reader, self->end, "Data at end of document"); + } + return FALSE; + + case GTK_JSON_BLOCK_OBJECT: + gtk_json_parser_skip_whitespace (self); + if (gtk_json_parser_is_eof (self)) + { + gtk_json_parser_syntax_error_at (self, + self->block[-1].value, + self->reader, + "Unterminated object"); + self->block->member_name = NULL; + self->block->value = NULL; + } + if (gtk_json_parser_has_char (self, '}')) + { + self->block->member_name = NULL; + self->block->value = NULL; + return FALSE; + } + if (!gtk_json_parser_try_char (self, ',')) + { + gtk_json_parser_syntax_error (self, "Expected a ',' to separate object members"); + return FALSE; + } + gtk_json_parser_skip_whitespace (self); + if (!gtk_json_parser_has_char (self, '"')) + { + gtk_json_parser_syntax_error (self, "Expected a string for object member name"); + return FALSE; + } + self->block->member_name = self->reader; + + if (!gtk_json_parser_parse_string (self)) + return FALSE; + gtk_json_parser_skip_whitespace (self); + if (!gtk_json_parser_try_char (self, ':')) + { + gtk_json_parser_syntax_error (self, "Missing ':' after member name"); + return FALSE; + } + + gtk_json_parser_skip_whitespace (self); + self->block->value = self->reader; + if (!gtk_json_parser_parse_value (self)) + return FALSE; + break; + + case GTK_JSON_BLOCK_ARRAY: + gtk_json_parser_skip_whitespace (self); + if (gtk_json_parser_is_eof (self)) + { + gtk_json_parser_syntax_error_at (self, + self->block[-1].value, + self->reader, + "Unterminated array"); + self->block->member_name = NULL; + self->block->value = NULL; + } + if (gtk_json_parser_has_char (self, ']')) + { + self->block->value = NULL; + return FALSE; + } + + if (!gtk_json_parser_try_char (self, ',')) + { + gtk_json_parser_syntax_error (self, "Expected a ',' to separate array members"); + return FALSE; + } + + gtk_json_parser_skip_whitespace (self); + self->block->value = self->reader; + if (!gtk_json_parser_parse_value (self)) + return FALSE; + break; + + default: + g_assert_not_reached (); + break; + } + + return TRUE; +} + +void +gtk_json_parser_rewind (GtkJsonParser *self) +{ + if (self->error) + return; + + switch (self->block->type) + { + case GTK_JSON_BLOCK_OBJECT: + gtk_json_parser_pop_block (self); + self->reader = self->block->value; + gtk_json_parser_start_object (self); + break; + + case GTK_JSON_BLOCK_ARRAY: + gtk_json_parser_pop_block (self); + self->reader = self->block->value; + gtk_json_parser_start_array (self); + break; + + case GTK_JSON_BLOCK_TOPLEVEL: + self->reader = self->start; + gtk_json_parser_skip_whitespace (self); + if (gtk_json_parser_is_eof (self)) + { + gtk_json_parser_syntax_error_at (self, self->start, self->reader, "Empty document"); + } + else + { + self->block->value = self->reader; + gtk_json_parser_parse_value (self); + } + break; + + default: + g_assert_not_reached (); + return; + } +} + +gsize +gtk_json_parser_get_depth (GtkJsonParser *self) +{ + return self->block - self->blocks; +} + +GtkJsonNode +gtk_json_parser_get_node (GtkJsonParser *self) +{ + if (self->error) + return GTK_JSON_NONE; + + if (self->block->value == NULL) + return GTK_JSON_NONE; + + return (json_character_table[*self->block->value] & JSON_CHARACTER_NODE_MASK); +} + +const GError * +gtk_json_parser_get_error (GtkJsonParser *self) +{ + return self->error; +} + +void +gtk_json_parser_get_error_offset (GtkJsonParser *self, + gsize *start, + gsize *end) +{ + const guchar *data; + + if (self->error == NULL) + { + if (start) + *start = 0; + if (end) + *end = 0; + return; + } + + data = g_bytes_get_data (self->bytes, NULL); + if (start) + *start = self->error_start - data; + if (end) + *end = self->error_end - data; +} + +void +gtk_json_parser_get_error_location (GtkJsonParser *self, + gsize *start_line, + gsize *start_line_bytes, + gsize *end_line, + gsize *end_line_bytes) +{ + const guchar *s, *line_start; + gsize lines; + + if (self->error == NULL) + { + if (start_line) + *start_line = 0; + if (start_line_bytes) + *start_line_bytes = 0; + if (end_line) + *end_line = 0; + if (end_line_bytes) + *end_line_bytes = 0; + return; + } + + line_start = self->start; + lines = 0; + + for (s = json_skip_characters_until (line_start, self->error_start, NEWLINE); + s < self->error_start; + s = json_skip_characters_until (line_start, self->error_start, NEWLINE)) + { + if (s[0] == '\r' && s + 1 < self->error_start && s[1] == '\n') + s++; + lines++; + line_start = s + 1; + } + + if (start_line) + *start_line = lines; + if (start_line_bytes) + *start_line_bytes = s - line_start; + + if (end_line == NULL && end_line_bytes == NULL) + return; + + for (s = json_skip_characters_until (s, self->error_end, NEWLINE); + s < self->error_end; + s = json_skip_characters_until (line_start, self->error_end, NEWLINE)) + { + if (s[0] == '\r' && s + 1 < self->error_start && s[1] == '\n') + s++; + lines++; + line_start = s + 1; + } + + if (end_line) + *end_line = lines; + if (end_line_bytes) + *end_line_bytes = s - line_start; +} + +static gboolean +gtk_json_parser_supports_member (GtkJsonParser *self) +{ + if (self->error) + return FALSE; + + if (self->block->type != GTK_JSON_BLOCK_OBJECT) + return FALSE; + + if (self->block->member_name == NULL) + return FALSE; + + return TRUE; +} + +char * +gtk_json_parser_get_member_name (GtkJsonParser *self) +{ + if (!gtk_json_parser_supports_member (self)) + return NULL; + + return gtk_json_unescape_string (self->block->member_name); +} + +gboolean +gtk_json_parser_has_member (GtkJsonParser *self, + const char *name) +{ + JsonStringIter iter; + gsize found, len; + + if (!gtk_json_parser_supports_member (self)) + return FALSE; + + found = 0; + + for (len = json_string_iter_init (&iter, self->block->member_name); + len > 0; + len = json_string_iter_next (&iter)) + { + const char *s = json_string_iter_get (&iter); + + if (strncmp (name + found, s, len) != 0) + return FALSE; + + found += len; + } + + return TRUE; +} + +gboolean +gtk_json_parser_find_member (GtkJsonParser *self, + const char *name) +{ + if (!gtk_json_parser_supports_member (self)) + { + while (gtk_json_parser_next (self)); + return FALSE; + } + + gtk_json_parser_rewind (self); + + do + { + if (gtk_json_parser_has_member (self, name)) + return TRUE; + } + while (gtk_json_parser_next (self)); + + return FALSE; +} + +static gssize +json_string_iter_run_select (const guchar *string_data, + const char * const *options) +{ + JsonStringIter iter; + gssize i, j; + gsize found, len; + + if (options == NULL || options[0] == NULL) + return -1; + + found = 0; + i = 0; + + for (len = json_string_iter_init (&iter, string_data); + len > 0; + len = json_string_iter_next (&iter)) + { + const char *s = json_string_iter_get (&iter); + + if (strncmp (options[i] + found, s, len) != 0) + { + for (j = i + 1; options[j]; j++) + { + if (strncmp (options[j], options[i], found) == 0 && + strncmp (options[j] + found, s, len) == 0) + { + i = j; + break; + } + } + if (j != i) + return -1; + } + found += len; + } + + if (options[i][found] == 0) + return i; + + for (j = i + 1; options[j]; i++) + { + if (strncmp (options[j], options[i], found) != 0) + continue; + if (options[j][found] == 0) + return j; + } + + return -1; +} + +gssize +gtk_json_parser_select_member (GtkJsonParser *self, + const char * const *options) +{ + if (!gtk_json_parser_supports_member (self)) + return -1; + + return json_string_iter_run_select (self->block->member_name, options); +} + +gboolean +gtk_json_parser_get_boolean (GtkJsonParser *self) +{ + if (self->error) + return FALSE; + + if (self->block->value == NULL) + return FALSE; + + if (*self->block->value == 't') + return TRUE; + else if (*self->block->value == 'f') + return FALSE; + + gtk_json_parser_type_error (self, "Expected a boolean value"); + return FALSE; +} + +double +gtk_json_parser_get_number (GtkJsonParser *self) +{ + double result; + + if (self->error) + return 0; + + if (self->block->value == NULL) + return 0; + + if (!strchr ("-0123456789", *self->block->value)) + { + gtk_json_parser_type_error (self, "Expected a number"); + return 0; + } + + errno = 0; + result = g_ascii_strtod ((const char *) self->block->value, NULL); + + if (errno) + { + if (errno == ERANGE) + gtk_json_parser_value_error (self, "Number out of range"); + else + gtk_json_parser_value_error (self, "%s", g_strerror (errno)); + + return 0; + } + + return result; +} + +int +gtk_json_parser_get_int (GtkJsonParser *self) +{ + long result; + char *end; + + if (self->error) + return 0; + + if (self->block->value == NULL) + return 0; + + if (!strchr ("-0123456789", *self->block->value)) + { + gtk_json_parser_type_error (self, "Expected an intereger"); + return 0; + } + + errno = 0; + result = strtol ((const char *) self->block->value, &end, 10); + if (*end == '.' || *end == 'e' || *end == 'E') + { + gtk_json_parser_type_error (self, "Expected an intereger"); + return 0; + } + + if (errno) + { + if (errno == ERANGE) + gtk_json_parser_value_error (self, "Number out of integer range"); + else + gtk_json_parser_value_error (self, "%s", g_strerror (errno)); + + return 0; + } + else if (result > G_MAXINT || result < G_MININT) + { + gtk_json_parser_value_error (self, "Number out of integer range"); + return 0; + } + + return result; +} + +guint +gtk_json_parser_get_uint (GtkJsonParser *self) +{ + gulong result; + char *end; + + if (self->error) + return 0; + + if (self->block->value == NULL) + return 0; + + if (!strchr ("0123456789", *self->block->value)) + { + gtk_json_parser_type_error (self, "Expected an unsigned intereger"); + return 0; + } + + errno = 0; + result = strtoul ((const char *) self->block->value, &end, 10); + if (*end == '.' || *end == 'e' || *end == 'E') + { + gtk_json_parser_type_error (self, "Expected an unsigned intereger"); + return 0; + } + + if (errno) + { + if (errno == ERANGE) + gtk_json_parser_value_error (self, "Number out of unsignedinteger range"); + else + gtk_json_parser_value_error (self, "%s", g_strerror (errno)); + + return 0; + } + else if (result > G_MAXUINT) + { + gtk_json_parser_value_error (self, "Number out of unsigned integer range"); + return 0; + } + + return result; +} + +char * +gtk_json_parser_get_string (GtkJsonParser *self) +{ + if (self->error) + return g_strdup (""); + + if (self->block->value == NULL) + return g_strdup (""); + + if (*self->block->value != '"') + { + gtk_json_parser_type_error (self, "Expected a string"); + return g_strdup (""); + } + + return gtk_json_unescape_string (self->block->value); +} + +gssize +gtk_json_parser_select_string (GtkJsonParser *self, + const char * const *options) +{ + if (self->error) + return -1; + + if (self->block->value == NULL) + return -1; + + if (*self->block->value != '"') + { + gtk_json_parser_type_error (self, "Expected a string"); + return -1; + } + + return json_string_iter_run_select (self->block->value, options); +} + +gboolean +gtk_json_parser_start_object (GtkJsonParser *self) +{ + if (self->error) + return FALSE; + + if (!gtk_json_parser_try_char (self, '{')) + { + gtk_json_parser_type_error (self, "Expected an object"); + return FALSE; + } + + gtk_json_parser_push_block (self, GTK_JSON_BLOCK_OBJECT); + + gtk_json_parser_skip_whitespace (self); + if (gtk_json_parser_is_eof (self)) + { + gtk_json_parser_syntax_error_at (self, + self->block[-1].value, + self->reader, + "Unterminated object"); + return FALSE; + } + if (gtk_json_parser_has_char (self, '}')) + return TRUE; + + if (!gtk_json_parser_has_char (self, '"')) + { + gtk_json_parser_syntax_error (self, "Expected a string for object member name"); + return FALSE; + } + self->block->member_name = self->reader; + + if (!gtk_json_parser_parse_string (self)) + return FALSE; + gtk_json_parser_skip_whitespace (self); + if (!gtk_json_parser_try_char (self, ':')) + { + gtk_json_parser_syntax_error (self, "Missing ':' after member name"); + return FALSE; + } + + gtk_json_parser_skip_whitespace (self); + self->block->value = self->reader; + if (!gtk_json_parser_parse_value (self)) + return FALSE; + + return TRUE; +} + +gboolean +gtk_json_parser_start_array (GtkJsonParser *self) +{ + if (self->error) + return FALSE; + + if (!gtk_json_parser_try_char (self, '[')) + { + gtk_json_parser_type_error (self, "Expected an array"); + return FALSE; + } + + gtk_json_parser_push_block (self, GTK_JSON_BLOCK_ARRAY); + gtk_json_parser_skip_whitespace (self); + if (gtk_json_parser_is_eof (self)) + { + gtk_json_parser_syntax_error_at (self, + self->block[-1].value, + self->reader, + "Unterminated array"); + return FALSE; + } + if (gtk_json_parser_has_char (self, ']')) + { + self->block->value = NULL; + return TRUE; + } + self->block->value = self->reader; + if (!gtk_json_parser_parse_value (self)) + return FALSE; + + return TRUE; +} + +gboolean +gtk_json_parser_end (GtkJsonParser *self) +{ + char bracket; + + g_return_val_if_fail (self != NULL, FALSE); + + while (gtk_json_parser_next (self)); + + if (self->error) + return FALSE; + + switch (self->block->type) + { + case GTK_JSON_BLOCK_OBJECT: + bracket = '}'; + break; + case GTK_JSON_BLOCK_ARRAY: + bracket = ']'; + break; + case GTK_JSON_BLOCK_TOPLEVEL: + default: + g_return_val_if_reached (FALSE); + } + + if (!gtk_json_parser_try_char (self, bracket)) + { + gtk_json_parser_syntax_error (self, "No terminating '%c'", bracket); + return FALSE; + } + + gtk_json_parser_pop_block (self); + + return TRUE; +} + diff --git a/pango2/json/gtkjsonparserprivate.h b/pango2/json/gtkjsonparserprivate.h new file mode 100644 index 00000000..83f72374 --- /dev/null +++ b/pango2/json/gtkjsonparserprivate.h @@ -0,0 +1,99 @@ +/* + * Copyright © 2021 Benjamin Otte + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + * + * Authors: Benjamin Otte <otte@gnome.org> + */ + + +#ifndef __GTK_JSON_PARSER_H__ +#define __GTK_JSON_PARSER_H__ + +#include <glib.h> + +G_BEGIN_DECLS + +typedef enum { + GTK_JSON_NONE, + GTK_JSON_NULL, + GTK_JSON_BOOLEAN, + GTK_JSON_NUMBER, + GTK_JSON_STRING, + GTK_JSON_OBJECT, + GTK_JSON_ARRAY +} GtkJsonNode; + +typedef enum { + GTK_JSON_ERROR_FAILED, + GTK_JSON_ERROR_SYNTAX, + GTK_JSON_ERROR_TYPE, + GTK_JSON_ERROR_VALUE, + GTK_JSON_ERROR_SCHEMA, +} GtkJsonError; + +typedef struct _GtkJsonParser GtkJsonParser; + +#define GTK_JSON_ERROR (gtk_json_error_quark ()) +GQuark gtk_json_error_quark (void); + +GtkJsonParser * gtk_json_parser_new_for_bytes (GBytes *bytes); +GtkJsonParser * gtk_json_parser_new_for_string (const char *string, + gssize size); + +void gtk_json_parser_free (GtkJsonParser *self); + +gboolean gtk_json_parser_next (GtkJsonParser *self); +void gtk_json_parser_rewind (GtkJsonParser *self); +gsize gtk_json_parser_get_depth (GtkJsonParser *self); +GtkJsonNode gtk_json_parser_get_node (GtkJsonParser *self); +char * gtk_json_parser_get_member_name (GtkJsonParser *self); +gboolean gtk_json_parser_has_member (GtkJsonParser *self, + const char *name); +gboolean gtk_json_parser_find_member (GtkJsonParser *self, + const char *name); +gssize gtk_json_parser_select_member (GtkJsonParser *self, + const char * const *options); + +gboolean gtk_json_parser_get_boolean (GtkJsonParser *self); +double gtk_json_parser_get_number (GtkJsonParser *self); +int gtk_json_parser_get_int (GtkJsonParser *self); +guint gtk_json_parser_get_uint (GtkJsonParser *self); +char * gtk_json_parser_get_string (GtkJsonParser *self); +gssize gtk_json_parser_select_string (GtkJsonParser *self, + const char * const *options); + +gboolean gtk_json_parser_start_object (GtkJsonParser *self); +gboolean gtk_json_parser_start_array (GtkJsonParser *self); +gboolean gtk_json_parser_end (GtkJsonParser *self); + +const GError * gtk_json_parser_get_error (GtkJsonParser *self) G_GNUC_PURE; +void gtk_json_parser_get_error_offset (GtkJsonParser *self, + gsize *start, + gsize *end); +void gtk_json_parser_get_error_location (GtkJsonParser *self, + gsize *start_line, + gsize *start_line_bytes, + gsize *end_line, + gsize *end_line_bytes); +void gtk_json_parser_value_error (GtkJsonParser *self, + const char *format, + ...) G_GNUC_PRINTF(2, 3); +void gtk_json_parser_schema_error (GtkJsonParser *self, + const char *format, + ...) G_GNUC_PRINTF(2, 3); + +G_END_DECLS + +#endif /* __GTK_JSON_PARSER_H__ */ diff --git a/pango2/json/gtkjsonprinter.c b/pango2/json/gtkjsonprinter.c new file mode 100644 index 00000000..e9ca03a3 --- /dev/null +++ b/pango2/json/gtkjsonprinter.c @@ -0,0 +1,405 @@ +/* + * Copyright © 2021 Benjamin Otte + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + * + * Authors: Benjamin Otte <otte@gnome.org> + */ + + +#include "config.h" + +#include "gtkjsonprinterprivate.h" + +typedef struct _GtkJsonBlock GtkJsonBlock; + +typedef enum { + GTK_JSON_BLOCK_TOPLEVEL, + GTK_JSON_BLOCK_OBJECT, + GTK_JSON_BLOCK_ARRAY, +} GtkJsonBlockType; + +struct _GtkJsonBlock +{ + GtkJsonBlockType type; + gsize n_elements; /* number of elements already written */ +}; + +struct _GtkJsonPrinter +{ + GtkJsonPrinterFlags flags; + char *indentation; + + GtkJsonPrinterWriteFunc write_func; + gpointer user_data; + GDestroyNotify user_destroy; + + GtkJsonBlock *block; /* current block */ + GtkJsonBlock *blocks; /* blocks array */ + GtkJsonBlock *blocks_end; /* blocks array */ + GtkJsonBlock blocks_preallocated[128]; /* preallocated */ +}; + +static void +gtk_json_printer_push_block (GtkJsonPrinter *self, + GtkJsonBlockType type) +{ + self->block++; + if (self->block == self->blocks_end) + { + gsize old_size = self->blocks_end - self->blocks; + gsize new_size = old_size + 128; + + if (self->blocks == self->blocks_preallocated) + { + self->blocks = g_new (GtkJsonBlock, new_size); + memcpy (self->blocks, self->blocks_preallocated, sizeof (GtkJsonBlock) * G_N_ELEMENTS (self->blocks_preallocated)); + } + else + { + self->blocks = g_renew (GtkJsonBlock, self->blocks, new_size); + } + self->blocks_end = self->blocks + new_size; + self->block = self->blocks + old_size; + } + + self->block->type = type; + self->block->n_elements = 0; +} + +static void +gtk_json_printer_pop_block (GtkJsonPrinter *self) +{ + g_assert (self->block > self->blocks); + self->block--; +} + +GtkJsonPrinter * +gtk_json_printer_new (GtkJsonPrinterWriteFunc write_func, + gpointer data, + GDestroyNotify destroy) +{ + GtkJsonPrinter *self; + + g_return_val_if_fail (write_func, NULL); + + self = g_slice_new0 (GtkJsonPrinter); + + self->flags = 0; + self->indentation = g_strdup (" "); + + self->write_func = write_func; + self->user_data = data; + self->user_destroy = destroy; + + self->blocks = self->blocks_preallocated; + self->blocks_end = self->blocks + G_N_ELEMENTS (self->blocks_preallocated); + self->block = self->blocks; + self->block->type = GTK_JSON_BLOCK_TOPLEVEL; + + return self; +} + +void +gtk_json_printer_free (GtkJsonPrinter *self) +{ + g_return_if_fail (self != NULL); + + g_free (self->indentation); + + if (self->user_destroy) + self->user_destroy (self->user_data); + + if (self->blocks != self->blocks_preallocated) + g_free (self->blocks); + + g_slice_free (GtkJsonPrinter, self); +} + +static gboolean +gtk_json_printer_has_flag (GtkJsonPrinter *self, + GtkJsonPrinterFlags flag) +{ + return (self->flags & flag) ? TRUE : FALSE; +} + +gsize +gtk_json_printer_get_depth (GtkJsonPrinter *self) +{ + return self->block - self->blocks; +} + +gsize +gtk_json_printer_get_n_elements (GtkJsonPrinter *self) +{ + return self->block->n_elements; +} + +void +gtk_json_printer_set_flags (GtkJsonPrinter *self, + GtkJsonPrinterFlags flags) +{ + g_return_if_fail (self != NULL); + + self->flags = flags; +} + +GtkJsonPrinterFlags +gtk_json_printer_get_flags (GtkJsonPrinter *self) +{ + g_return_val_if_fail (self != NULL, 0); + + return self->flags; +} + +void +gtk_json_printer_set_indentation (GtkJsonPrinter *self, + gsize amount) +{ + g_return_if_fail (self != NULL); + + g_free (self->indentation); + + self->indentation = g_malloc (amount + 1); + memset (self->indentation, ' ', amount); + self->indentation[amount] = 0; +} + +gsize +gtk_json_printer_get_indentation (GtkJsonPrinter *self) +{ + g_return_val_if_fail (self != NULL, 2); + + return strlen (self->indentation); +} + +static void +gtk_json_printer_write (GtkJsonPrinter *self, + const char *s) +{ + self->write_func (self, s, self->user_data); +} + +static char * +gtk_json_printer_escape_string (GtkJsonPrinter *self, + const char *str) +{ + GString *string; + + string = g_string_new (NULL); + string = g_string_append_c (string, '"'); + + for (; *str != '\0'; str = g_utf8_next_char (str)) + { + switch (*str) + { + case '"': + g_string_append (string, "\\\""); + break; + case '\\': + g_string_append (string, "\\\\"); + break; + case '\b': + g_string_append (string, "\\b"); + break; + case '\f': + g_string_append (string, "\\f"); + break; + case '\n': + g_string_append (string, "\\n"); + break; + case '\r': + g_string_append (string, "\\r"); + break; + case '\t': + g_string_append (string, "\\t"); + break; + default: + if ((int) *str < 0x20 || (int) *str >= 0x80) + { + if ((guint) *str < 0x20 || gtk_json_printer_has_flag (self, GTK_JSON_PRINTER_ASCII)) + g_string_append_printf (string, "\\u%04x", g_utf8_get_char (str)); + else + g_string_append_unichar (string, g_utf8_get_char (str)); + } + else + g_string_append_c (string, *str); + } + } + + string = g_string_append_c (string, '"'); + return g_string_free (string, FALSE); +} + +static void +gtk_json_printer_newline (GtkJsonPrinter *self) +{ + gsize depth; + + if (!gtk_json_printer_has_flag (self, GTK_JSON_PRINTER_PRETTY)) + return; + + gtk_json_printer_write (self, "\n"); + for (depth = gtk_json_printer_get_depth (self); depth-->0;) + gtk_json_printer_write (self, self->indentation); +} + +static void +gtk_json_printer_begin_member (GtkJsonPrinter *self, + const char *name) +{ + if (gtk_json_printer_get_n_elements (self) > 0) + gtk_json_printer_write (self, ","); + if (self->block->type != GTK_JSON_BLOCK_TOPLEVEL || gtk_json_printer_get_n_elements (self) > 0) + gtk_json_printer_newline (self); + + self->block->n_elements++; + + if (name) + { + char *escaped = gtk_json_printer_escape_string (self, name); + gtk_json_printer_write (self, escaped); + g_free (escaped); + if (gtk_json_printer_has_flag (self, GTK_JSON_PRINTER_PRETTY)) + gtk_json_printer_write (self, " : "); + else + gtk_json_printer_write (self, ":"); + } +} + +void +gtk_json_printer_add_boolean (GtkJsonPrinter *self, + const char *name, + gboolean value) +{ + g_return_if_fail (self != NULL); + g_return_if_fail ((self->block->type == GTK_JSON_BLOCK_OBJECT) == (name != NULL)); + + gtk_json_printer_begin_member (self, name); + gtk_json_printer_write (self, value ? "true" : "false"); +} + +void +gtk_json_printer_add_number (GtkJsonPrinter *self, + const char *name, + double value) +{ + char buf[G_ASCII_DTOSTR_BUF_SIZE]; + + g_return_if_fail (self != NULL); + g_return_if_fail ((self->block->type == GTK_JSON_BLOCK_OBJECT) == (name != NULL)); + + gtk_json_printer_begin_member (self, name); + g_ascii_dtostr (buf, G_ASCII_DTOSTR_BUF_SIZE, value); + gtk_json_printer_write (self, buf); +} + +void +gtk_json_printer_add_integer (GtkJsonPrinter *self, + const char *name, + int value) +{ + char buf[128]; + + g_return_if_fail (self != NULL); + g_return_if_fail ((self->block->type == GTK_JSON_BLOCK_OBJECT) == (name != NULL)); + + gtk_json_printer_begin_member (self, name); + g_snprintf (buf, sizeof (buf), "%d", value); + gtk_json_printer_write (self, buf); +} + +void +gtk_json_printer_add_string (GtkJsonPrinter *self, + const char *name, + const char *s) +{ + char *escaped; + + g_return_if_fail (self != NULL); + g_return_if_fail ((self->block->type == GTK_JSON_BLOCK_OBJECT) == (name != NULL)); + g_return_if_fail (s != NULL); + + gtk_json_printer_begin_member (self, name); + escaped = gtk_json_printer_escape_string (self, s); + gtk_json_printer_write (self, escaped); + g_free (escaped); +} + +void +gtk_json_printer_add_null (GtkJsonPrinter *self, + const char *name) +{ + g_return_if_fail (self != NULL); + g_return_if_fail ((self->block->type == GTK_JSON_BLOCK_OBJECT) == (name != NULL)); + + gtk_json_printer_begin_member (self, name); + gtk_json_printer_write (self, "null"); +} + +void +gtk_json_printer_start_object (GtkJsonPrinter *self, + const char *name) +{ + g_return_if_fail (self != NULL); + g_return_if_fail ((self->block->type == GTK_JSON_BLOCK_OBJECT) == (name != NULL)); + + gtk_json_printer_begin_member (self, name); + gtk_json_printer_write (self, "{"); + gtk_json_printer_push_block (self, GTK_JSON_BLOCK_OBJECT); +} + +void +gtk_json_printer_start_array (GtkJsonPrinter *self, + const char *name) +{ + g_return_if_fail (self != NULL); + g_return_if_fail ((self->block->type == GTK_JSON_BLOCK_OBJECT) == (name != NULL)); + + gtk_json_printer_begin_member (self, name); + gtk_json_printer_write (self, "["); + gtk_json_printer_push_block (self, GTK_JSON_BLOCK_ARRAY); +} + +void +gtk_json_printer_end (GtkJsonPrinter *self) +{ + const char *bracket; + gboolean empty; + + g_return_if_fail (self != NULL); + + switch (self->block->type) + { + case GTK_JSON_BLOCK_OBJECT: + bracket = "}"; + break; + case GTK_JSON_BLOCK_ARRAY: + bracket = "]"; + break; + case GTK_JSON_BLOCK_TOPLEVEL: + default: + g_return_if_reached (); + } + + empty = gtk_json_printer_get_n_elements (self) == 0; + gtk_json_printer_pop_block (self); + + if (!empty) + { + gtk_json_printer_newline (self); + } + gtk_json_printer_write (self, bracket); +} + diff --git a/pango2/json/gtkjsonprinterprivate.h b/pango2/json/gtkjsonprinterprivate.h new file mode 100644 index 00000000..e25a1b1d --- /dev/null +++ b/pango2/json/gtkjsonprinterprivate.h @@ -0,0 +1,79 @@ +/* + * Copyright © 2021 Benjamin Otte + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + * + * Authors: Benjamin Otte <otte@gnome.org> + */ + + +#ifndef __GTK_JSON_PRINTER_H__ +#define __GTK_JSON_PRINTER_H__ + +#include <glib.h> + +G_BEGIN_DECLS + +typedef struct _GtkJsonPrinter GtkJsonPrinter; + +typedef enum { + GTK_JSON_PRINTER_PRETTY = (1 << 0), + GTK_JSON_PRINTER_ASCII = (1 << 1), +} GtkJsonPrinterFlags; + +typedef void (* GtkJsonPrinterWriteFunc) (GtkJsonPrinter *printer, + const char *s, + gpointer user_data); + + +GtkJsonPrinter * gtk_json_printer_new (GtkJsonPrinterWriteFunc write_func, + gpointer data, + GDestroyNotify destroy); +void gtk_json_printer_free (GtkJsonPrinter *self); + +void gtk_json_printer_set_flags (GtkJsonPrinter *self, + GtkJsonPrinterFlags flags); +GtkJsonPrinterFlags gtk_json_printer_get_flags (GtkJsonPrinter *self); +void gtk_json_printer_set_indentation (GtkJsonPrinter *self, + gsize amount); +gsize gtk_json_printer_get_indentation (GtkJsonPrinter *self); + +gsize gtk_json_printer_get_depth (GtkJsonPrinter *self); +gsize gtk_json_printer_get_n_elements (GtkJsonPrinter *self); + +void gtk_json_printer_add_boolean (GtkJsonPrinter *self, + const char *name, + gboolean value); +void gtk_json_printer_add_number (GtkJsonPrinter *self, + const char *name, + double value); +void gtk_json_printer_add_integer (GtkJsonPrinter *self, + const char *name, + int value); +void gtk_json_printer_add_string (GtkJsonPrinter *self, + const char *name, + const char *s); +void gtk_json_printer_add_null (GtkJsonPrinter *self, + const char *name); + +void gtk_json_printer_start_object (GtkJsonPrinter *self, + const char *name); +void gtk_json_printer_start_array (GtkJsonPrinter *self, + const char *name); +void gtk_json_printer_end (GtkJsonPrinter *self); + + +G_END_DECLS + +#endif /* __GTK_JSON_PRINTER_H__ */ diff --git a/pango2/meson.build b/pango2/meson.build new file mode 100644 index 00000000..8dce43d8 --- /dev/null +++ b/pango2/meson.build @@ -0,0 +1,298 @@ +pango_sources = [ + 'break.c', + 'ellipsize.c', + 'glyphstring.c', + 'itemize.c', + 'pango-attr.c', + 'pango-attr-list.c', + 'pango-attr-iterator.c', + 'pango-attributes.c', + 'pango-bidi.c', + 'pango-color.c', + 'pango-context.c', + 'pango-emoji.c', + 'pango-font.c', + 'pango-font-description.c', + 'pango-font-face.c', + 'pango-font-family.c', + 'pango-font-metrics.c', + 'pango-fontmap.c', + 'pango-fontset.c', + 'pango-fontset-cached.c', + 'pango-glyph-item.c', + 'pango-gravity.c', + 'pango-item.c', + 'pango-language.c', + 'pango-language-set.c', + 'pango-language-set-simple.c', + 'pango-layout.c', + 'pango-markup.c', + 'pango-matrix.c', + 'pango-renderer.c', + 'pango-script.c', + 'pango-tabs.c', + 'pango-trace.c', + 'pango-utils.c', + 'shape.c', + 'serializer.c', + 'json/gtkjsonparser.c', + 'json/gtkjsonprinter.c', + 'pango-line.c', + 'pango-run.c', + 'pango-line-breaker.c', + 'pango-lines.c', + 'pango-line-iter.c', + 'pango-hbface.c', + 'pango-hbfamily.c', + 'pango-generic-family.c', + 'pango-hbfont.c', + 'pango-userface.c', + 'pango-userfont.c', +] + +pango_headers = [ + 'pango.h', + 'pango-attr.h', + 'pango-attr-list.h', + 'pango-attr-iterator.h', + 'pango-attributes.h', + 'pango-break.h', + 'pango-color.h', + 'pango-context.h', + 'pango-direction.h', + 'pango-font.h', + 'pango-font-description.h', + 'pango-font-face.h', + 'pango-font-family.h', + 'pango-font-metrics.h', + 'pango-fontmap.h', + 'pango-fontset.h', + 'pango-generic-family.h', + 'pango-glyph.h', + 'pango-gravity.h', + 'pango-item.h', + 'pango-language.h', + 'pango-line.h', + 'pango-run.h', + 'pango-line-breaker.h', + 'pango-line-iter.h', + 'pango-lines.h', + 'pango-layout.h', + 'pango-matrix.h', + 'pango-markup.h', + 'pango-renderer.h', + 'pango-script.h', + 'pango-tabs.h', + 'pango-types.h', + 'pango-utils.h', + 'pango-hbface.h', + 'pango-hbfont.h', + 'pango-userface.h', + 'pango-userfont.h', +] + +pango_gir_includes = [ + 'HarfBuzz-0.0', + 'GObject-2.0', + 'Gio-2.0' +] + +if cairo_dep.found() + pango_headers += [ + 'pangocairo.h', + 'pangocairo-context.h', + 'pangocairo-font.h', + 'pangocairo-render.h', + ] + + pango_sources += [ + 'pangocairo-context.c', + 'pangocairo-font.c', + 'pangocairo-render.c', + ] + + pango_gir_includes += [ + 'cairo-1.0', + ] +endif + +if host_system == 'darwin' + pango_headers += [ + 'pangocoretext-fontmap.h', + ] + + pango_sources += [ + 'pangocoretext-fontmap.c', + ] +endif + +if host_system == 'linux' + pango_headers += [ + 'pangofc-fontmap.h', + ] + + pango_sources += [ + 'pangofc-fontmap.c', + 'pangofc-language-set.c', + ] + + pango_gir_includes += [ + 'fontconfig-2.0', + ] +endif + +if host_system == 'windows' + pango_headers += [ + 'pangodwrite-fontmap.h', + ] + + pango_sources += [ + 'pangodwrite-fontmap.cpp', + ] + if cairo_dep.found() + pango_sources += [ + 'pangocairo-dwrite-font.cpp', + ] + endif + + pango_deps += [ + cc.find_library('gdi32'), + cc.find_library('dwrite'), + ] +endif + +pango_installed_headers = pango_headers + [ 'pango-version-macros.h' ] + +install_headers(pango_installed_headers, subdir: pango_api_path) + +# Features header +pango_features_conf = configuration_data() +pango_features_conf.set('PANGO2_VERSION_MAJOR', pango_major_version) +pango_features_conf.set('PANGO2_VERSION_MINOR', pango_minor_version) +pango_features_conf.set('PANGO2_VERSION_MICRO', pango_micro_version) +pango_features_conf.set('PANGO2_API_VERSION', pango_api_version) +pango_features_conf.set('PANGO2_CURRENT_MINUS_AGE', '0') +if cairo_dep.found() + pango_features_conf.set('PANGO2_RENDERING_CAIRO', 1) +endif + +pango_features_h = configure_file( + input: 'pango-features.h.meson', + output: 'pango-features.h', + configuration: pango_features_conf, + install: true, + install_dir: join_paths(pango_includedir, pango_api_path), +) + +# Enumerations for GType +pango_enums = gnome.mkenums( + 'pango-enum-types', + sources: pango_headers, + identifier_prefix: 'Pango2', + symbol_prefix: 'PANGO2', + c_template: 'pango-enum-types.c.template', + h_template: 'pango-enum-types.h.template', + install_dir: join_paths(pango_includedir, pango_api_path), + install_header: true, +) +pango_enum_h = pango_enums[1] + +if host_system == 'windows' + pango_rc = configure_file( + input: 'pango.rc.in', + output: 'pango.rc', + configuration: pango_features_conf, + ) + pango_res = import('windows').compile_resources(pango_rc) + pango_sources += pango_res +endif + +pango_cflags = [ + '-DG_LOG_DOMAIN="Pango"', + '-DG_LOG_USE_STRUCTURED=1', + '-DPANGO_COMPILATION', + '-DSYSCONFDIR="@0@"'.format(pango_sysconfdir), + '-DLIBDIR="@0@"'.format(pango_libdir), +] + +pango_inc = include_directories('.') + +libpango = library( + pango_api_name, + sources: pango_sources + pango_enums, + version: pango_libversion, + soversion: pango_soversion, + darwin_versions : pango_osxversion, + install: true, + dependencies: pango_deps, + include_directories: [ root_inc, pango_inc ], + c_args: common_cflags + pango_debug_cflags + pango_cflags, + cpp_args: common_cflags + pango_debug_cflags + pango_cflags, + link_args: common_ldflags, +) + +pango_dep_sources = [ pango_enum_h ] + +gir = find_program('g-ir-scanner', required : get_option('introspection')) +build_gir = gir.found() and (not meson.is_cross_build() or get_option('introspection').enabled()) + +if build_gir + gir_args = [ + '--quiet', + ] + harfbuzz_gobject_dep = dependency('harfbuzz-gobject', + version: harfbuzz_req_version, + required: false, + fallback: ['harfbuzz', 'libharfbuzz_gobject_dep']) + + if harfbuzz_gobject_dep.found() + pango_deps += harfbuzz_gobject_dep + endif + pango_gir = gnome.generate_gir( + libpango, + sources: pango_sources + pango_headers + [ pango_enum_h, pango_features_h ], + namespace: 'Pango2', + nsversion: pango_api_version, + identifier_prefix: 'Pango2', + symbol_prefix: 'pango2', + export_packages: 'pango', + dependencies: pango_deps, + includes: pango_gir_includes, + header: 'pango/pango.h', + install: true, + extra_args: gir_args, + ) + pango_gir_dep = declare_dependency(sources: pango_gir) + pango_dep_sources += pango_gir +endif + +libpango_dep = declare_dependency( + link_with: libpango, + include_directories: pango_inc, + dependencies: pango_deps, + sources: pango_dep_sources, +) +meson.override_dependency('pango', libpango_dep) + +pango_pkg_requires = ['gobject-2.0'] +if harfbuzz_dep.type_name() == 'pkgconfig' + pango_pkg_requires += 'harfbuzz' +endif + +pkgconfig.generate(libpango, + name: 'Pango2', + description: 'Internationalized text handling', + requires: pango_pkg_requires, + filebase: 'pango2', + subdirs: pango_api_name, +) + +if cairo_dep.found() + pkgconfig.generate(libpango, + name: 'Pango2 Cairo', + description: 'Cairo rendering support for Pango2', + requires: ['pango2'], + filebase: 'pango2cairo', + subdirs: pango_api_name, + ) +endif diff --git a/pango2/pango-attr-iterator-private.h b/pango2/pango-attr-iterator-private.h new file mode 100644 index 00000000..5fead454 --- /dev/null +++ b/pango2/pango-attr-iterator-private.h @@ -0,0 +1,39 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-attr-iterator.h" + + +struct _Pango2AttrIterator +{ + GPtrArray *attrs; /* From the list */ + guint n_attrs; /* Copied from the list */ + + GPtrArray *attribute_stack; + + guint attr_index; + guint start_index; + guint end_index; +}; + +void pango2_attr_iterator_clear (Pango2AttrIterator *iterator); +gboolean pango2_attr_iterator_advance (Pango2AttrIterator *iterator, + int index); diff --git a/pango2/pango-attr-iterator.c b/pango2/pango-attr-iterator.c new file mode 100644 index 00000000..59bb1255 --- /dev/null +++ b/pango2/pango-attr-iterator.c @@ -0,0 +1,509 @@ +/* Pango2 + * pango-attr-iterator.c: Attribute iterator + * + * Copyright (C) 2000-2002 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include <string.h> + +#include "pango-attr-iterator-private.h" +#include "pango-attr-list-private.h" +#include "pango-attr-private.h" +#include "pango-impl-utils.h" + + +G_DEFINE_BOXED_TYPE (Pango2AttrIterator, + pango2_attr_iterator, + pango2_attr_iterator_copy, + pango2_attr_iterator_destroy) + +/* {{{ Private API */ + +void +pango2_attr_list_init_iterator (Pango2AttrList *list, + Pango2AttrIterator *iterator) +{ + iterator->attribute_stack = NULL; + iterator->attrs = list->attributes; + iterator->n_attrs = iterator->attrs ? iterator->attrs->len : 0; + + iterator->attr_index = 0; + iterator->start_index = 0; + iterator->end_index = 0; + + if (!pango2_attr_iterator_next (iterator)) + iterator->end_index = G_MAXUINT; +} + +void +pango2_attr_iterator_clear (Pango2AttrIterator *iterator) +{ + if (iterator->attribute_stack) + g_ptr_array_free (iterator->attribute_stack, TRUE); +} + +gboolean +pango2_attr_iterator_advance (Pango2AttrIterator *iterator, + int index) +{ + int start_range, end_range; + + pango2_attr_iterator_range (iterator, &start_range, &end_range); + + while (index >= end_range) + { + if (!pango2_attr_iterator_next (iterator)) + return FALSE; + pango2_attr_iterator_range (iterator, &start_range, &end_range); + } + + if (start_range > index) + g_warning ("pango2_attr_iterator_advance(): iterator had already " + "moved beyond the index"); + + return TRUE; +} + +/* }}} */ +/* {{{ Public API */ + +/** + * pango2_attr_list_get_iterator: + * @list: a `Pango2AttrList` + * + * Create a iterator initialized to the beginning of the list. + * + * @list must not be modified until this iterator is freed. + * + * Return value: (transfer full): the newly allocated + * `Pango2AttrIterator`, which should be freed with + * [method@Pango2.AttrIterator.destroy] + */ +Pango2AttrIterator * +pango2_attr_list_get_iterator (Pango2AttrList *list) +{ + Pango2AttrIterator *iterator; + + g_return_val_if_fail (list != NULL, NULL); + + iterator = g_slice_new (Pango2AttrIterator); + pango2_attr_list_init_iterator (list, iterator); + + return iterator; +} + +/** + * pango2_attr_iterator_destroy: + * @iterator: a `Pango2AttrIterator` + * + * Destroy a `Pango2AttrIterator` and free all associated memory. + */ +void +pango2_attr_iterator_destroy (Pango2AttrIterator *iterator) +{ + g_return_if_fail (iterator != NULL); + + pango2_attr_iterator_clear (iterator); + g_slice_free (Pango2AttrIterator, iterator); +} + +/** + * pango2_attr_iterator_copy: + * @iterator: a `Pango2AttrIterator` + * + * Copy a `Pango2AttrIterator`. + * + * Return value: (transfer full): the newly allocated + * `Pango2AttrIterator`, which should be freed with + * [method@Pango2.AttrIterator.destroy] + */ +Pango2AttrIterator * +pango2_attr_iterator_copy (Pango2AttrIterator *iterator) +{ + Pango2AttrIterator *copy; + + g_return_val_if_fail (iterator != NULL, NULL); + + copy = g_slice_new (Pango2AttrIterator); + + *copy = *iterator; + + if (iterator->attribute_stack) + copy->attribute_stack = g_ptr_array_copy (iterator->attribute_stack, NULL, NULL); + else + copy->attribute_stack = NULL; + + return copy; +} + +/** + * pango2_attr_iterator_range: + * @iterator: a Pango2AttrIterator + * @start: (out): location to store the start of the range + * @end: (out): location to store the end of the range + * + * Get the range of the current segment. + * + * Note that the stored return values are signed, not unsigned + * like the values in `Pango2Attribute`. To deal with this API + * oversight, stored return values that wouldn't fit into + * a signed integer are clamped to %G_MAXINT. + */ +void +pango2_attr_iterator_range (Pango2AttrIterator *iterator, + int *start, + int *end) +{ + g_return_if_fail (iterator != NULL); + + if (start) + *start = MIN (iterator->start_index, G_MAXINT); + if (end) + *end = MIN (iterator->end_index, G_MAXINT); +} + +/** + * pango2_attr_iterator_next: + * @iterator: a `Pango2AttrIterator` + * + * Advance the iterator until the next change of style. + * + * Return value: %FALSE if the iterator is at the end + * of the list, otherwise %TRUE + */ +gboolean +pango2_attr_iterator_next (Pango2AttrIterator *iterator) +{ + int i; + + g_return_val_if_fail (iterator != NULL, FALSE); + + if (iterator->attr_index >= iterator->n_attrs && + (!iterator->attribute_stack || iterator->attribute_stack->len == 0)) + return FALSE; + + iterator->start_index = iterator->end_index; + iterator->end_index = G_MAXUINT; + + if (iterator->attribute_stack) + { + for (i = iterator->attribute_stack->len - 1; i>= 0; i--) + { + const Pango2Attribute *attr = g_ptr_array_index (iterator->attribute_stack, i); + + if (attr->end_index == iterator->start_index) + g_ptr_array_remove_index (iterator->attribute_stack, i); /* Can't use index_fast :( */ + else + iterator->end_index = MIN (iterator->end_index, attr->end_index); + } + } + + while (1) + { + Pango2Attribute *attr; + + if (iterator->attr_index >= iterator->n_attrs) + break; + + attr = g_ptr_array_index (iterator->attrs, iterator->attr_index); + + if (attr->start_index != iterator->start_index) + break; + + if (attr->end_index > iterator->start_index) + { + if (G_UNLIKELY (!iterator->attribute_stack)) + iterator->attribute_stack = g_ptr_array_new (); + + g_ptr_array_add (iterator->attribute_stack, attr); + + iterator->end_index = MIN (iterator->end_index, attr->end_index); + } + + iterator->attr_index++; /* NEXT! */ + } + + if (iterator->attr_index < iterator->n_attrs) + { + Pango2Attribute *attr = g_ptr_array_index (iterator->attrs, iterator->attr_index); + + iterator->end_index = MIN (iterator->end_index, attr->start_index); + } + + return TRUE; +} + +/** + * pango2_attr_iterator_get: + * @iterator: a `Pango2AttrIterator` + * @type: the type of attribute to find + * + * Find the current attribute of a particular type + * at the iterator location. + * + * When multiple attributes of the same type overlap, + * the attribute whose range starts closest to the + * current location is used. + * + * Return value: (nullable) (transfer none): the current + * attribute of the given type, or %NULL if no attribute + * of that type applies to the current location. + */ +Pango2Attribute * +pango2_attr_iterator_get (Pango2AttrIterator *iterator, + guint type) +{ + int i; + + g_return_val_if_fail (iterator != NULL, NULL); + + if (!iterator->attribute_stack) + return NULL; + + for (i = iterator->attribute_stack->len - 1; i>= 0; i--) + { + Pango2Attribute *attr = g_ptr_array_index (iterator->attribute_stack, i); + + if (attr->type == type) + return attr; + } + + return NULL; +} + +/** + * pango2_attr_iterator_get_font: + * @iterator: a `Pango2AttrIterator` + * @desc: (out caller-allocates): a `Pango2FontDescription` to fill in with the current + * values. The family name in this structure will be set using + * [method@Pango2.FontDescription.set_family_static] using + * values from an attribute in the `Pango2AttrList` associated + * with the iterator, so if you plan to keep it around, you + * must call: + * `pango2_font_description_set_family (desc, pango2_font_description_get_family (desc))`. + * @language: (out) (optional): location to store language tag + * for item, or %NULL if none is found. + * @extra_attrs: (out) (optional) (element-type Pango2.Attribute) (transfer full): + * location in which to store a list of non-font attributes + * at the the current position; only the highest priority + * value of each attribute will be added to this list. In + * order to free this value, you must call + * [method@Pango2.Attribute.destroy] on each member. + * + * Get the font and other attributes at the current + * iterator position. + */ +void +pango2_attr_iterator_get_font (Pango2AttrIterator *iterator, + Pango2FontDescription *desc, + Pango2Language **language, + GSList **extra_attrs) +{ + Pango2FontMask mask = 0; + gboolean have_language = FALSE; + double scale = 0; + gboolean have_scale = FALSE; + int i; + + g_return_if_fail (iterator != NULL); + g_return_if_fail (desc != NULL); + + if (language) + *language = NULL; + + if (extra_attrs) + *extra_attrs = NULL; + + if (!iterator->attribute_stack) + return; + + for (i = iterator->attribute_stack->len - 1; i >= 0; i--) + { + const Pango2Attribute *attr = g_ptr_array_index (iterator->attribute_stack, i); + + switch ((int) attr->type) + { + case PANGO2_ATTR_FONT_DESC: + { + Pango2FontMask new_mask = pango2_font_description_get_set_fields (attr->font_value) & ~mask; + mask |= new_mask; + pango2_font_description_unset_fields (desc, new_mask); + pango2_font_description_merge_static (desc, attr->font_value, FALSE); + + break; + } + case PANGO2_ATTR_FAMILY: + if (!(mask & PANGO2_FONT_MASK_FAMILY)) + { + mask |= PANGO2_FONT_MASK_FAMILY; + pango2_font_description_set_family (desc, attr->str_value); + } + break; + case PANGO2_ATTR_STYLE: + if (!(mask & PANGO2_FONT_MASK_STYLE)) + { + mask |= PANGO2_FONT_MASK_STYLE; + pango2_font_description_set_style (desc, attr->int_value); + } + break; + case PANGO2_ATTR_VARIANT: + if (!(mask & PANGO2_FONT_MASK_VARIANT)) + { + mask |= PANGO2_FONT_MASK_VARIANT; + pango2_font_description_set_variant (desc, attr->int_value); + } + break; + case PANGO2_ATTR_WEIGHT: + if (!(mask & PANGO2_FONT_MASK_WEIGHT)) + { + mask |= PANGO2_FONT_MASK_WEIGHT; + pango2_font_description_set_weight (desc, attr->int_value); + } + break; + case PANGO2_ATTR_STRETCH: + if (!(mask & PANGO2_FONT_MASK_STRETCH)) + { + mask |= PANGO2_FONT_MASK_STRETCH; + pango2_font_description_set_stretch (desc, attr->int_value); + } + break; + case PANGO2_ATTR_SIZE: + if (!(mask & PANGO2_FONT_MASK_SIZE)) + { + mask |= PANGO2_FONT_MASK_SIZE; + pango2_font_description_set_size (desc, attr->int_value); + } + break; + case PANGO2_ATTR_ABSOLUTE_SIZE: + if (!(mask & PANGO2_FONT_MASK_SIZE)) + { + mask |= PANGO2_FONT_MASK_SIZE; + pango2_font_description_set_absolute_size (desc, attr->int_value); + } + break; + case PANGO2_ATTR_SCALE: + if (!have_scale) + { + have_scale = TRUE; + scale = attr->double_value; + } + break; + case PANGO2_ATTR_LANGUAGE: + if (language) + { + if (!have_language) + { + have_language = TRUE; + *language = attr->lang_value; + } + } + break; + default: + if (extra_attrs) + { + gboolean found = FALSE; + + if (PANGO2_ATTR_MERGE (attr) == PANGO2_ATTR_MERGE_OVERRIDES) + { + GSList *tmp_list = *extra_attrs; + while (tmp_list) + { + Pango2Attribute *old_attr = tmp_list->data; + if (attr->type == old_attr->type) + { + found = TRUE; + break; + } + + tmp_list = tmp_list->next; + } + } + + if (!found) + *extra_attrs = g_slist_prepend (*extra_attrs, pango2_attribute_copy (attr)); + } + } + } + + if (have_scale) + { + /* We need to use a local variable to ensure that the compiler won't + * implicitly cast it to integer while the result is kept in registers, + * leading to a wrong approximation in i386 (with 387 FPU) + */ + volatile double size = scale * pango2_font_description_get_size (desc); + + if (pango2_font_description_get_size_is_absolute (desc)) + pango2_font_description_set_absolute_size (desc, size); + else + pango2_font_description_set_size (desc, size); + } +} + +/** + * pango2_attr_iterator_get_attrs: + * @iterator: a `Pango2AttrIterator` + * + * Gets a list of all attributes at the current position of the + * iterator. + * + * Return value: (element-type Pango2.Attribute) (transfer full): + * a list of all attributes for the current range. To free + * this value, call [method@Pango2.Attribute.destroy] on each + * value and [func@GLib.SList.free] on the list. + */ +GSList * +pango2_attr_iterator_get_attrs (Pango2AttrIterator *iterator) +{ + GSList *attrs = NULL; + int i; + + if (!iterator->attribute_stack || + iterator->attribute_stack->len == 0) + return NULL; + + for (i = iterator->attribute_stack->len - 1; i >= 0; i--) + { + Pango2Attribute *attr = g_ptr_array_index (iterator->attribute_stack, i); + GSList *tmp_list2; + gboolean found = FALSE; + + if (PANGO2_ATTR_MERGE (attr) == PANGO2_ATTR_MERGE_OVERRIDES) + { + for (tmp_list2 = attrs; tmp_list2; tmp_list2 = tmp_list2->next) + { + Pango2Attribute *old_attr = tmp_list2->data; + if (attr->type == old_attr->type) + { + found = TRUE; + break; + } + } + } + + if (!found) + attrs = g_slist_prepend (attrs, pango2_attribute_copy (attr)); + } + + return attrs; +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pango-attr-iterator.h b/pango2/pango-attr-iterator.h new file mode 100644 index 00000000..f22ba499 --- /dev/null +++ b/pango2/pango-attr-iterator.h @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2000 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-attr-list.h> +#include <glib-object.h> + +G_BEGIN_DECLS + + +/** + * Pango2AttrIterator: + * + * A `Pango2AttrIterator` is used to iterate through a `Pango2AttrList`. + * + * A new iterator is created with [method@Pango2.AttrList.get_iterator]. + * Once the iterator is created, it can be advanced through the style + * changes in the text using [method@Pango2.AttrIterator.next]. At each + * style change, the range of the current style segment and the attributes + * currently in effect can be queried. + */ + +PANGO2_AVAILABLE_IN_ALL +GType pango2_attr_iterator_get_type (void) G_GNUC_CONST; + +PANGO2_AVAILABLE_IN_ALL +void pango2_attr_iterator_range (Pango2AttrIterator *iterator, + int *start, + int *end); +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_attr_iterator_next (Pango2AttrIterator *iterator); +PANGO2_AVAILABLE_IN_ALL +Pango2AttrIterator * pango2_attr_iterator_copy (Pango2AttrIterator *iterator); +PANGO2_AVAILABLE_IN_ALL +void pango2_attr_iterator_destroy (Pango2AttrIterator *iterator); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_iterator_get (Pango2AttrIterator *iterator, + guint type); +PANGO2_AVAILABLE_IN_ALL +void pango2_attr_iterator_get_font (Pango2AttrIterator *iterator, + Pango2FontDescription *desc, + Pango2Language **language, + GSList **extra_attrs); +PANGO2_AVAILABLE_IN_ALL +GSList * pango2_attr_iterator_get_attrs (Pango2AttrIterator *iterator); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(Pango2AttrIterator, pango2_attr_iterator_destroy) + +G_END_DECLS diff --git a/pango2/pango-attr-list-private.h b/pango2/pango-attr-list-private.h new file mode 100644 index 00000000..e010febd --- /dev/null +++ b/pango2/pango-attr-list-private.h @@ -0,0 +1,36 @@ +/* + * Copyright 2022 RedHat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-attr-list.h" + + +struct _Pango2AttrList +{ + guint ref_count; + GPtrArray *attributes; +}; + +void pango2_attr_list_init (Pango2AttrList *list); +void pango2_attr_list_destroy (Pango2AttrList *list); +gboolean pango2_attr_list_has_attributes (const Pango2AttrList *list); + +void pango2_attr_list_init_iterator (Pango2AttrList *list, + Pango2AttrIterator *iterator); diff --git a/pango2/pango-attr-list.c b/pango2/pango-attr-list.c new file mode 100644 index 00000000..6f0f47d0 --- /dev/null +++ b/pango2/pango-attr-list.c @@ -0,0 +1,1286 @@ +/* Pango2 + * pango-attr-list.c: Attribute lists + * + * Copyright (C) 2000-2002 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include <string.h> + +#include "pango-attr-list-private.h" +#include "pango-attr-private.h" +#include "pango-impl-utils.h" + + +G_DEFINE_BOXED_TYPE (Pango2AttrList, pango2_attr_list, + pango2_attr_list_copy, + pango2_attr_list_unref); + +/* {{{ Utilities */ + +static void +pango2_attr_list_insert_internal (Pango2AttrList *list, + Pango2Attribute *attr, + gboolean before) +{ + const guint start_index = attr->start_index; + Pango2Attribute *last_attr; + + if (G_UNLIKELY (!list->attributes)) + list->attributes = g_ptr_array_new (); + + if (list->attributes->len == 0) + { + g_ptr_array_add (list->attributes, attr); + return; + } + + g_assert (list->attributes->len > 0); + + last_attr = g_ptr_array_index (list->attributes, list->attributes->len - 1); + + if (last_attr->start_index < start_index || + (!before && last_attr->start_index == start_index)) + { + g_ptr_array_add (list->attributes, attr); + } + else + { + guint i, p; + + for (i = 0, p = list->attributes->len; i < p; i++) + { + Pango2Attribute *cur = g_ptr_array_index (list->attributes, i); + + if (cur->start_index > start_index || + (before && cur->start_index == start_index)) + { + g_ptr_array_insert (list->attributes, i, attr); + break; + } + } + } +} + +/* }}} */ +/* {{{ Private API */ + +void +pango2_attr_list_init (Pango2AttrList *list) +{ + list->ref_count = 1; + list->attributes = NULL; +} + +void +pango2_attr_list_destroy (Pango2AttrList *list) +{ + if (!list->attributes) + return; + + g_ptr_array_free (list->attributes, TRUE); +} + +gboolean +pango2_attr_list_has_attributes (const Pango2AttrList *list) +{ + return list && list->attributes != NULL && list->attributes->len > 0; +} + +/* }}} */ +/* {{{ Public API */ + +/** + * pango2_attr_list_new: + * + * Create a new empty attribute list with a reference + * count of one. + * + * Return value: (transfer full): the newly allocated + * `Pango2AttrList`, which should be freed with + * [method@Pango2.AttrList.unref] + */ +Pango2AttrList * +pango2_attr_list_new (void) +{ + Pango2AttrList *list = g_slice_new (Pango2AttrList); + + pango2_attr_list_init (list); + + return list; +} + +/** + * pango2_attr_list_ref: + * @list: (nullable): a `Pango2AttrList` + * + * Increase the reference count of the given attribute + * list by one. + * + * Return value: The attribute list passed in + */ +Pango2AttrList * +pango2_attr_list_ref (Pango2AttrList *list) +{ + if (list == NULL) + return NULL; + + g_atomic_int_inc ((int *) &list->ref_count); + + return list; +} + +/** + * pango2_attr_list_unref: + * @list: (nullable): a `Pango2AttrList` + * + * Decrease the reference count of the given attribute + * list by one. + * + * If the result is zero, free the attribute list + * and the attributes it contains. + */ +void +pango2_attr_list_unref (Pango2AttrList *list) +{ + if (list == NULL) + return; + + g_return_if_fail (list->ref_count > 0); + + if (g_atomic_int_dec_and_test ((int *) &list->ref_count)) + { + pango2_attr_list_destroy (list); + g_slice_free (Pango2AttrList, list); + } +} + +/** + * pango2_attr_list_copy: + * @list: (nullable): a `Pango2AttrList` + * + * Copy @list and return an identical new list. + * + * Return value: (nullable): the newly allocated + * `Pango2AttrList`, with a reference count of one, + * which should be freed with [method@Pango2.AttrList.unref] + */ +Pango2AttrList * +pango2_attr_list_copy (Pango2AttrList *list) +{ + Pango2AttrList *new; + + if (list == NULL) + return NULL; + + new = pango2_attr_list_new (); + if (!list->attributes || list->attributes->len == 0) + return new; + + new->attributes = g_ptr_array_copy (list->attributes, (GCopyFunc)pango2_attribute_copy, NULL); + + return new; +} + +/** + * pango2_attr_list_insert: + * @list: a `Pango2AttrList` + * @attr: (transfer full): the attribute to insert + * + * Insert the given attribute into the `Pango2AttrList`. + * + * It will be inserted after all other attributes with a + * matching @start_index. + */ +void +pango2_attr_list_insert (Pango2AttrList *list, + Pango2Attribute *attr) +{ + g_return_if_fail (list != NULL); + g_return_if_fail (attr != NULL); + + pango2_attr_list_insert_internal (list, attr, FALSE); +} + +/** + * pango2_attr_list_insert_before: + * @list: a `Pango2AttrList` + * @attr: (transfer full): the attribute to insert + * + * Insert the given attribute into the `Pango2AttrList`. + * + * It will be inserted before all other attributes with a + * matching @start_index. + */ +void +pango2_attr_list_insert_before (Pango2AttrList *list, + Pango2Attribute *attr) +{ + g_return_if_fail (list != NULL); + g_return_if_fail (attr != NULL); + + pango2_attr_list_insert_internal (list, attr, TRUE); +} + +/** + * pango2_attr_list_change: + * @list: a `Pango2AttrList` + * @attr: (transfer full): the attribute to insert + * + * Insert the given attribute into the `Pango2AttrList`. + * + * It will replace any attributes of the same type + * on that segment and be merged with any adjoining + * attributes that are identical. + * + * This function is slower than [method@Pango2.AttrList.insert] + * for creating an attribute list in order (potentially + * much slower for large lists). However, + * [method@Pango2.AttrList.insert] is not suitable for + * continually changing a set of attributes since it + * never removes or combines existing attributes. + */ +void +pango2_attr_list_change (Pango2AttrList *list, + Pango2Attribute *attr) +{ + guint i, p; + guint start_index = attr->start_index; + guint end_index = attr->end_index; + gboolean inserted; + + g_return_if_fail (list != NULL); + + if (start_index == end_index) /* empty, nothing to do */ + { + pango2_attribute_destroy (attr); + return; + } + + if (!list->attributes || list->attributes->len == 0) + { + pango2_attr_list_insert (list, attr); + return; + } + + inserted = FALSE; + for (i = 0, p = list->attributes->len; i < p; i++) + { + Pango2Attribute *tmp_attr = g_ptr_array_index (list->attributes, i); + + if (tmp_attr->start_index > start_index) + { + g_ptr_array_insert (list->attributes, i, attr); + inserted = TRUE; + break; + } + + if (tmp_attr->type != attr->type) + continue; + + if (tmp_attr->end_index < start_index) + continue; /* This attr does not overlap with the new one */ + + g_assert (tmp_attr->start_index <= start_index); + g_assert (tmp_attr->end_index >= start_index); + + if (pango2_attribute_equal (tmp_attr, attr)) + { + /* We can merge the new attribute with this attribute + */ + if (tmp_attr->end_index >= end_index) + { + /* We are totally overlapping the previous attribute. + * No action is needed. + */ + pango2_attribute_destroy (attr); + return; + } + + tmp_attr->end_index = end_index; + pango2_attribute_destroy (attr); + + attr = tmp_attr; + inserted = TRUE; + break; + } + else + { + /* Split, truncate, or remove the old attribute + */ + if (tmp_attr->end_index > end_index) + { + Pango2Attribute *end_attr = pango2_attribute_copy (tmp_attr); + + end_attr->start_index = end_index; + pango2_attr_list_insert (list, end_attr); + } + + if (tmp_attr->start_index == start_index) + { + pango2_attribute_destroy (tmp_attr); + g_ptr_array_remove_index (list->attributes, i); + break; + } + else + { + tmp_attr->end_index = start_index; + } + } + } + + if (!inserted) + /* we didn't insert attr yet */ + pango2_attr_list_insert (list, attr); + + /* We now have the range inserted into the list one way or the + * other. Fix up the remainder + */ + /* Attention: No i = 0 here. */ + for (i = i + 1, p = list->attributes->len; i < p; i++) + { + Pango2Attribute *tmp_attr = g_ptr_array_index (list->attributes, i); + + if (tmp_attr->start_index > end_index) + break; + + if (tmp_attr->type != attr->type) + continue; + + if (tmp_attr == attr) + continue; + + if (tmp_attr->end_index <= attr->end_index || + pango2_attribute_equal (tmp_attr, attr)) + { + /* We can merge the new attribute with this attribute. */ + attr->end_index = MAX (end_index, tmp_attr->end_index); + pango2_attribute_destroy (tmp_attr); + g_ptr_array_remove_index (list->attributes, i); + i--; + p--; + continue; + } + else + { + /* Trim the start of this attribute that it begins at the end + * of the new attribute. This may involve moving it in the list + * to maintain the required non-decreasing order of start indices. + */ + int k, m; + + tmp_attr->start_index = attr->end_index; + + for (k = i + 1, m = list->attributes->len; k < m; k++) + { + Pango2Attribute *tmp_attr2 = g_ptr_array_index (list->attributes, k); + + if (tmp_attr2->start_index >= tmp_attr->start_index) + break; + + g_ptr_array_index (list->attributes, k - 1) = tmp_attr2; + g_ptr_array_index (list->attributes, k) = tmp_attr; + } + } + } +} + +/** + * pango2_attr_list_update: + * @list: a `Pango2AttrList` + * @pos: the position of the change + * @remove: the number of removed bytes + * @add: the number of added bytes + * + * Update indices of attributes in @list for a change in the + * text they refer to. + * + * The change that this function applies is removing @remove + * bytes at position @pos and inserting @add bytes instead. + * + * Attributes that fall entirely in the (@pos, @pos + @remove) + * range are removed. + * + * Attributes that start or end inside the (@pos, @pos + @remove) + * range are shortened to reflect the removal. + * + * Attributes start and end positions are updated if they are + * behind @pos + @remove. + */ +void +pango2_attr_list_update (Pango2AttrList *list, + int pos, + int remove, + int add) +{ + guint i, p; + + g_return_if_fail (pos >= 0); + g_return_if_fail (remove >= 0); + g_return_if_fail (add >= 0); + + if (list->attributes) + for (i = 0, p = list->attributes->len; i < p; i++) + { + Pango2Attribute *attr = g_ptr_array_index (list->attributes, i); + + if (attr->start_index >= pos && + attr->end_index < pos + remove) + { + pango2_attribute_destroy (attr); + g_ptr_array_remove_index (list->attributes, i); + i--; /* Look at this index again */ + p--; + continue; + } + + if (attr->start_index != PANGO2_ATTR_INDEX_FROM_TEXT_BEGINNING) + { + if (attr->start_index >= pos && + attr->start_index < pos + remove) + { + attr->start_index = pos + add; + } + else if (attr->start_index >= pos + remove) + { + attr->start_index += add - remove; + } + } + + if (attr->end_index != PANGO2_ATTR_INDEX_TO_TEXT_END) + { + if (attr->end_index >= pos && + attr->end_index < pos + remove) + { + attr->end_index = pos; + } + else if (attr->end_index >= pos + remove) + { + if (add > remove && + G_MAXUINT - attr->end_index < add - remove) + attr->end_index = G_MAXUINT; + else + attr->end_index += add - remove; + } + } + } +} + +/** + * pango2_attr_list_splice: + * @list: a `Pango2AttrList` + * @other: another `Pango2AttrList` + * @pos: the position in @list at which to insert @other + * @len: the length of the spliced segment. (Note that this + * must be specified since the attributes in @other may only + * be present at some subsection of this range) + * + * This function opens up a hole in @list, fills it + * in with attributes from the left, and then merges + * @other on top of the hole. + * + * This operation is equivalent to stretching every attribute + * that applies at position @pos in @list by an amount @len, + * and then calling [method@Pango2.AttrList.change] with a copy + * of each attribute in @other in sequence (offset in position + * by @pos, and limited in length to @len). + * + * This operation proves useful for, for instance, inserting + * a pre-edit string in the middle of an edit buffer. + * + * For backwards compatibility, the function behaves differently + * when @len is 0. In this case, the attributes from @other are + * not imited to @len, and are just overlayed on top of @list. + * + * This mode is useful for merging two lists of attributes together. + */ +void +pango2_attr_list_splice (Pango2AttrList *list, + Pango2AttrList *other, + int pos, + int len) +{ + guint i, p; + guint upos, ulen; + guint end; + + g_return_if_fail (list != NULL); + g_return_if_fail (other != NULL); + g_return_if_fail (pos >= 0); + g_return_if_fail (len >= 0); + + upos = (guint)pos; + ulen = (guint)len; + +/* This definition only works when a and b are unsigned; overflow + * isn't defined in the C standard for signed integers + */ +#define CLAMP_ADD(a,b) (((a) + (b) < (a)) ? G_MAXUINT : (a) + (b)) + + end = CLAMP_ADD (upos, ulen); + + if (list->attributes) + for (i = 0, p = list->attributes->len; i < p; i++) + { + Pango2Attribute *attr = g_ptr_array_index (list->attributes, i);; + + if (attr->start_index <= upos) + { + if (attr->end_index > upos) + attr->end_index = CLAMP_ADD (attr->end_index, ulen); + } + else + { + /* This could result in a zero length attribute if it + * gets squashed up against G_MAXUINT, but deleting such + * an element could (in theory) suprise the caller, so + * we don't delete it. + */ + attr->start_index = CLAMP_ADD (attr->start_index, ulen); + attr->end_index = CLAMP_ADD (attr->end_index, ulen); + } + } + + if (!other->attributes || other->attributes->len == 0) + return; + + for (i = 0, p = other->attributes->len; i < p; i++) + { + Pango2Attribute *attr = pango2_attribute_copy (g_ptr_array_index (other->attributes, i)); + if (ulen > 0) + { + attr->start_index = MIN (CLAMP_ADD (attr->start_index, upos), end); + attr->end_index = MIN (CLAMP_ADD (attr->end_index, upos), end); + } + else + { + attr->start_index = CLAMP_ADD (attr->start_index, upos); + attr->end_index = CLAMP_ADD (attr->end_index, upos); + } + + /* Same as above, the attribute could be squashed to zero-length; here + * pango2_attr_list_change() will take care of deleting it. + */ + pango2_attr_list_change (list, attr); + } +#undef CLAMP_ADD +} + +/** + * pango2_attr_list_get_attributes: + * @list: a `Pango2AttrList` + * + * Gets a list of all attributes in @list. + * + * Return value: (element-type Pango2.Attribute) (transfer full): + * a list of all attributes in @list. To free this value, + * call [method@Pango2.Attribute.destroy] on each value and + * [func@GLib.SList.free] on the list + */ +GSList * +pango2_attr_list_get_attributes (Pango2AttrList *list) +{ + GSList *result = NULL; + guint i, p; + + g_return_val_if_fail (list != NULL, NULL); + + if (!list->attributes || list->attributes->len == 0) + return NULL; + + for (i = 0, p = list->attributes->len; i < p; i++) + { + Pango2Attribute *attr = g_ptr_array_index (list->attributes, i); + + result = g_slist_prepend (result, pango2_attribute_copy (attr)); + } + + return g_slist_reverse (result); +} + +/** + * pango2_attr_list_equal: + * @list: a `Pango2AttrList` + * @other_list: the other `Pango2AttrList` + * + * Checks whether @list and @other_list contain the same + * attributes and whether those attributes apply to the + * same ranges. + * + * Beware that this will return wrong values if any list + * contains duplicates. + * + * Return value: %TRUE if the lists are equal, %FALSE if + * they aren't + */ +gboolean +pango2_attr_list_equal (Pango2AttrList *list, + Pango2AttrList *other_list) +{ + GPtrArray *attrs, *other_attrs; + guint64 skip_bitmask = 0; + guint i; + + if (list == other_list) + return TRUE; + + if (list == NULL || other_list == NULL) + return FALSE; + + if (list->attributes == NULL || other_list->attributes == NULL) + return list->attributes == other_list->attributes; + + attrs = list->attributes; + other_attrs = other_list->attributes; + + if (attrs->len != other_attrs->len) + return FALSE; + + for (i = 0; i < attrs->len; i++) + { + Pango2Attribute *attr = g_ptr_array_index (attrs, i); + gboolean attr_equal = FALSE; + guint other_attr_index; + + for (other_attr_index = 0; other_attr_index < other_attrs->len; other_attr_index++) + { + Pango2Attribute *other_attr = g_ptr_array_index (other_attrs, other_attr_index); + guint64 other_attr_bitmask = other_attr_index < 64 ? 1 << other_attr_index : 0; + + if ((skip_bitmask & other_attr_bitmask) != 0) + continue; + + if (attr->start_index == other_attr->start_index && + attr->end_index == other_attr->end_index && + pango2_attribute_equal (attr, other_attr)) + { + skip_bitmask |= other_attr_bitmask; + attr_equal = TRUE; + break; + } + + } + + if (!attr_equal) + return FALSE; + } + + return TRUE; +} + +/** + * pango2_attr_list_filter: + * @list: a `Pango2AttrList` + * @func: (scope call) (closure data): callback function; + * returns %TRUE if an attribute should be filtered out + * @data: (closure): Data to be passed to @func + * + * Given a `Pango2AttrList` and callback function, removes + * any elements of @list for which @func returns %TRUE and + * inserts them into a new list. + * + * Return value: (transfer full) (nullable): the new + * `Pango2AttrList` or %NULL if no attributes of the + * given types were found + */ +Pango2AttrList * +pango2_attr_list_filter (Pango2AttrList *list, + Pango2AttrFilterFunc func, + gpointer data) + +{ + Pango2AttrList *new = NULL; + guint i, p; + + g_return_val_if_fail (list != NULL, NULL); + + if (!list->attributes || list->attributes->len == 0) + return NULL; + + for (i = 0, p = list->attributes->len; i < p; i++) + { + Pango2Attribute *tmp_attr = g_ptr_array_index (list->attributes, i); + + if ((*func) (tmp_attr, data)) + { + g_ptr_array_remove_index (list->attributes, i); + i--; /* Need to look at this index again */ + p--; + + if (G_UNLIKELY (!new)) + { + new = pango2_attr_list_new (); + new->attributes = g_ptr_array_new (); + } + + g_ptr_array_add (new->attributes, tmp_attr); + } + } + + return new; +} + +/* }}} */ +/* {{{ Serialization */ + +/* We serialize attribute lists to strings. The format + * is a comma-separated list of the attributes in the order + * in which they are in the list, with each attribute having + * this format: + * + * START END NICK VALUE + * + * Values that can contain a comma, such as font descriptions + * are quoted with "". + */ + +static GType +get_attr_value_type (Pango2AttrType type) +{ + switch ((int)type) + { + case PANGO2_ATTR_STYLE: return PANGO2_TYPE_STYLE; + case PANGO2_ATTR_WEIGHT: return PANGO2_TYPE_WEIGHT; + case PANGO2_ATTR_VARIANT: return PANGO2_TYPE_VARIANT; + case PANGO2_ATTR_STRETCH: return PANGO2_TYPE_STRETCH; + case PANGO2_ATTR_GRAVITY: return PANGO2_TYPE_GRAVITY; + case PANGO2_ATTR_GRAVITY_HINT: return PANGO2_TYPE_GRAVITY_HINT; + case PANGO2_ATTR_UNDERLINE: return PANGO2_TYPE_LINE_STYLE; + case PANGO2_ATTR_STRIKETHROUGH: return PANGO2_TYPE_LINE_STYLE; + case PANGO2_ATTR_OVERLINE: return PANGO2_TYPE_LINE_STYLE; + case PANGO2_ATTR_BASELINE_SHIFT: return PANGO2_TYPE_BASELINE_SHIFT; + case PANGO2_ATTR_FONT_SCALE: return PANGO2_TYPE_FONT_SCALE; + case PANGO2_ATTR_TEXT_TRANSFORM: return PANGO2_TYPE_TEXT_TRANSFORM; + default: return G_TYPE_INVALID; + } +} + +static void +append_enum_value (GString *str, + GType type, + int value) +{ + GEnumClass *enum_class; + GEnumValue *enum_value; + + enum_class = g_type_class_ref (type); + enum_value = g_enum_get_value (enum_class, value); + g_type_class_unref (enum_class); + + if (enum_value) + g_string_append_printf (str, " %s", enum_value->value_nick); + else + g_string_append_printf (str, " %d", value); +} + +static void +attr_print (GString *str, + Pango2Attribute *attr) +{ + const char *name; + + name = pango2_attr_type_get_name (attr->type); + if (!name) + return; + + g_string_append_printf (str, "%u %u %s", attr->start_index, attr->end_index, name); + + switch (PANGO2_ATTR_VALUE_TYPE (attr)) + { + case PANGO2_ATTR_VALUE_INT: + if (attr->type == PANGO2_ATTR_WEIGHT || + attr->type == PANGO2_ATTR_STYLE || + attr->type == PANGO2_ATTR_STRETCH || + attr->type == PANGO2_ATTR_VARIANT || + attr->type == PANGO2_ATTR_GRAVITY || + attr->type == PANGO2_ATTR_GRAVITY_HINT || + attr->type == PANGO2_ATTR_UNDERLINE || + attr->type == PANGO2_ATTR_OVERLINE || + attr->type == PANGO2_ATTR_BASELINE_SHIFT || + attr->type == PANGO2_ATTR_FONT_SCALE || + attr->type == PANGO2_ATTR_TEXT_TRANSFORM) + append_enum_value (str, get_attr_value_type (attr->type), attr->int_value); + else + g_string_append_printf (str, " %d", attr->int_value); + break; + + case PANGO2_ATTR_VALUE_BOOLEAN: + g_string_append (str, attr->int_value ? " true" : " false"); + break; + + case PANGO2_ATTR_VALUE_STRING: + g_string_append_printf (str, " \"%s\"", attr->str_value); + break; + + case PANGO2_ATTR_VALUE_LANGUAGE: + g_string_append_printf (str, " %s", pango2_language_to_string (attr->lang_value)); + break; + + case PANGO2_ATTR_VALUE_FLOAT: + { + char buf[20]; + g_ascii_formatd (buf, 20, "%f", attr->double_value); + g_string_append_printf (str, " %s", buf); + } + break; + + case PANGO2_ATTR_VALUE_FONT_DESC: + { + char *s = pango2_font_description_to_string (attr->font_value); + g_string_append_printf (str, " \"%s\"", s); + g_free (s); + } + break; + + case PANGO2_ATTR_VALUE_COLOR: + { + char *s = pango2_color_to_string (&attr->color_value); + g_string_append_printf (str, " %s", s); + g_free (s); + } + break; + + case PANGO2_ATTR_VALUE_POINTER: + { + char *s = pango2_attr_value_serialize (attr); + g_string_append_printf (str, " %s", s); + g_free (s); + } + break; + + default: + g_assert_not_reached (); + } +} + +/** + * pango2_attr_list_to_string: + * @list: a `Pango2AttrList` + * + * Serializes a `Pango2AttrList` to a string. + * + * No guarantees are made about the format of the string, + * it may change between Pango2 versions. + * + * The intended use of this function is testing and + * debugging. The format is not meant as a permanent + * storage format. + * + * Returns: (transfer full): a newly allocated string + */ +char * +pango2_attr_list_to_string (Pango2AttrList *list) +{ + GString *s; + + s = g_string_new (""); + + if (list->attributes) + for (int i = 0; i < list->attributes->len; i++) + { + Pango2Attribute *attr = g_ptr_array_index (list->attributes, i); + + if (i > 0) + g_string_append (s, "\n"); + attr_print (s, attr); + } + + return g_string_free (s, FALSE); +} + +static Pango2AttrType +get_attr_type_by_nick (const char *nick, + int len) +{ + GEnumClass *enum_class; + + enum_class = g_type_class_ref (pango2_attr_type_get_type ()); + for (GEnumValue *ev = enum_class->values; ev->value_name; ev++) + { + if (ev->value_nick && strncmp (ev->value_nick, nick, len) == 0) + { + g_type_class_unref (enum_class); + return (Pango2AttrType) ev->value; + } + } + + g_type_class_unref (enum_class); + return PANGO2_ATTR_INVALID; +} + +static int +get_attr_value (Pango2AttrType type, + const char *str, + int len) +{ + GEnumClass *enum_class; + char *endp; + int value; + + enum_class = g_type_class_ref (get_attr_value_type (type)); + for (GEnumValue *ev = enum_class->values; ev->value_name; ev++) + { + if (ev->value_nick && strncmp (ev->value_nick, str, len) == 0) + { + g_type_class_unref (enum_class); + return ev->value; + } + } + g_type_class_unref (enum_class); + + value = g_ascii_strtoll (str, &endp, 10); + if (endp - str == len) + return value; + + return -1; +} + +static gboolean +is_valid_end_char (char c) +{ + return c == ',' || c == '\n' || c == '\0'; +} + +/** + * pango2_attr_list_from_string: + * @text: a string + * + * Deserializes a `Pango2AttrList` from a string. + * + * This is the counterpart to [method@Pango2.AttrList.to_string]. + * See that functions for details about the format. + * + * Returns: (transfer full) (nullable): a new `Pango2AttrList` + */ +Pango2AttrList * +pango2_attr_list_from_string (const char *text) +{ + Pango2AttrList *list; + const char *p; + + g_return_val_if_fail (text != NULL, NULL); + + list = pango2_attr_list_new (); + + if (*text == '\0') + return list; + + list->attributes = g_ptr_array_new (); + + p = text + strspn (text, " \t\n"); + while (*p) + { + char *endp; + gint64 start_index; + gint64 end_index; + char *str; + Pango2AttrType attr_type; + Pango2Attribute *attr; + Pango2Language *lang; + gint64 integer; + Pango2FontDescription *desc; + Pango2Color color; + double num; + + start_index = g_ascii_strtoll (p, &endp, 10); + if (*endp != ' ') + goto fail; + + p = endp + strspn (endp, " "); + if (!*p) + goto fail; + + end_index = g_ascii_strtoll (p, &endp, 10); + if (*endp != ' ') + goto fail; + + p = endp + strspn (endp, " "); + + endp = (char *)p + strcspn (p, " "); + attr_type = get_attr_type_by_nick (p, endp - p); + + p = endp + strspn (endp, " "); + if (*p == '\0') + goto fail; + +#define INT_ATTR(name,type) \ + integer = g_ascii_strtoll (p, &endp, 10); \ + if (!is_valid_end_char (*endp)) goto fail; \ + attr = pango2_attr_##name##_new ((type)integer); + +#define BOOLEAN_ATTR(name,type) \ + if (strncmp (p, "true", strlen ("true")) == 0) \ + { \ + integer = 1; \ + endp = (char *)(p + strlen ("true")); \ + } \ + else if (strncmp (p, "false", strlen ("false")) == 0) \ + { \ + integer = 0; \ + endp = (char *)(p + strlen ("false")); \ + } \ + else \ + integer = g_ascii_strtoll (p, &endp, 10); \ + if (!is_valid_end_char (*endp)) goto fail; \ + attr = pango2_attr_##name##_new ((type)integer); + +#define ENUM_ATTR(name, type, min, max) \ + endp = (char *)p + strcspn (p, ",\n"); \ + integer = get_attr_value (attr_type, p, endp - p); \ + attr = pango2_attr_##name##_new ((type) CLAMP (integer, min, max)); + +#define FLOAT_ATTR(name) \ + num = g_ascii_strtod (p, &endp); \ + if (!is_valid_end_char (*endp)) goto fail; \ + attr = pango2_attr_##name##_new ((float)num); + +#define COLOR_ATTR(name) \ + endp = (char *)p + strcspn (p, ",\n"); \ + if (!is_valid_end_char (*endp)) goto fail; \ + str = g_strndup (p, endp - p); \ + if (!pango2_color_parse (&color, str)) \ + { \ + g_free (str); \ + goto fail; \ + } \ + attr = pango2_attr_##name##_new (&color); \ + g_free (str); + + switch (attr_type) + { + case PANGO2_ATTR_INVALID: + pango2_attr_list_unref (list); + return NULL; + + case PANGO2_ATTR_LANGUAGE: + endp = (char *)p + strcspn (p, ",\n"); + if (!is_valid_end_char (*endp)) goto fail; + str = g_strndup (p, endp - p); + lang = pango2_language_from_string (str); + attr = pango2_attr_language_new (lang); + g_free (str); + break; + + case PANGO2_ATTR_FAMILY: + p++; + endp = strchr (p, '"'); + if (!endp) goto fail; + str = g_strndup (p, endp - p); + attr = pango2_attr_family_new (str); + g_free (str); + endp++; + if (!is_valid_end_char (*endp)) goto fail; + break; + + case PANGO2_ATTR_STYLE: + ENUM_ATTR(style, Pango2Style, PANGO2_STYLE_NORMAL, PANGO2_STYLE_ITALIC); + break; + + case PANGO2_ATTR_WEIGHT: + ENUM_ATTR(weight, Pango2Weight, PANGO2_WEIGHT_THIN, PANGO2_WEIGHT_ULTRAHEAVY); + break; + + case PANGO2_ATTR_VARIANT: + ENUM_ATTR(variant, Pango2Variant, PANGO2_VARIANT_NORMAL, PANGO2_VARIANT_TITLE_CAPS); + break; + + case PANGO2_ATTR_STRETCH: + ENUM_ATTR(stretch, Pango2Stretch, PANGO2_STRETCH_ULTRA_CONDENSED, PANGO2_STRETCH_ULTRA_EXPANDED); + break; + + case PANGO2_ATTR_SIZE: + INT_ATTR(size, int); + break; + + case PANGO2_ATTR_FONT_DESC: + p++; + endp = strchr (p, '"'); + if (!endp) goto fail; + str = g_strndup (p, endp - p); + desc = pango2_font_description_from_string (str); + attr = pango2_attr_font_desc_new (desc); + pango2_font_description_free (desc); + g_free (str); + endp++; + if (!is_valid_end_char (*endp)) goto fail; + break; + + case PANGO2_ATTR_FOREGROUND: + COLOR_ATTR(foreground); + break; + + case PANGO2_ATTR_BACKGROUND: + COLOR_ATTR(background); + break; + + case PANGO2_ATTR_UNDERLINE: + ENUM_ATTR(underline, Pango2LineStyle, PANGO2_LINE_STYLE_NONE, PANGO2_LINE_STYLE_WAVY); + break; + + case PANGO2_ATTR_UNDERLINE_POSITION: + ENUM_ATTR(underline_position, Pango2UnderlinePosition, PANGO2_UNDERLINE_POSITION_NORMAL, PANGO2_UNDERLINE_POSITION_UNDER); + break; + + case PANGO2_ATTR_STRIKETHROUGH: + ENUM_ATTR(strikethrough, Pango2LineStyle, PANGO2_LINE_STYLE_NONE, PANGO2_LINE_STYLE_WAVY); + break; + + case PANGO2_ATTR_RISE: + INT_ATTR(rise, int); + break; + + case PANGO2_ATTR_SCALE: + FLOAT_ATTR(scale); + break; + + case PANGO2_ATTR_FALLBACK: + BOOLEAN_ATTR(fallback, gboolean); + break; + + case PANGO2_ATTR_LETTER_SPACING: + INT_ATTR(letter_spacing, int); + break; + + case PANGO2_ATTR_UNDERLINE_COLOR: + COLOR_ATTR(underline_color); + break; + + case PANGO2_ATTR_STRIKETHROUGH_COLOR: + COLOR_ATTR(strikethrough_color); + break; + + case PANGO2_ATTR_ABSOLUTE_SIZE: + integer = g_ascii_strtoll (p, &endp, 10); + if (!is_valid_end_char (*endp)) goto fail; + attr = pango2_attr_size_new_absolute (integer); + break; + + case PANGO2_ATTR_GRAVITY: + ENUM_ATTR(gravity, Pango2Gravity, PANGO2_GRAVITY_SOUTH, PANGO2_GRAVITY_WEST); + break; + + case PANGO2_ATTR_FONT_FEATURES: + p++; + endp = strchr (p, '"'); + if (!endp) goto fail; + str = g_strndup (p, endp - p); + attr = pango2_attr_font_features_new (str); + g_free (str); + endp++; + if (!is_valid_end_char (*endp)) goto fail; + break; + + case PANGO2_ATTR_GRAVITY_HINT: + ENUM_ATTR(gravity_hint, Pango2GravityHint, PANGO2_GRAVITY_HINT_NATURAL, PANGO2_GRAVITY_HINT_LINE); + break; + + case PANGO2_ATTR_ALLOW_BREAKS: + BOOLEAN_ATTR(allow_breaks, gboolean); + break; + + case PANGO2_ATTR_SHOW: + INT_ATTR(show, Pango2ShowFlags); + break; + + case PANGO2_ATTR_INSERT_HYPHENS: + BOOLEAN_ATTR(insert_hyphens, gboolean); + break; + + case PANGO2_ATTR_OVERLINE: + ENUM_ATTR(overline, Pango2LineStyle, PANGO2_LINE_STYLE_NONE, PANGO2_LINE_STYLE_WAVY); + break; + + case PANGO2_ATTR_OVERLINE_COLOR: + COLOR_ATTR(overline_color); + break; + + case PANGO2_ATTR_LINE_HEIGHT: + FLOAT_ATTR(line_height); + break; + + case PANGO2_ATTR_ABSOLUTE_LINE_HEIGHT: + integer = g_ascii_strtoll (p, &endp, 10); + if (!is_valid_end_char (*endp)) goto fail; + attr = pango2_attr_line_height_new_absolute (integer); + break; + + case PANGO2_ATTR_TEXT_TRANSFORM: + ENUM_ATTR(text_transform, Pango2TextTransform, PANGO2_TEXT_TRANSFORM_NONE, PANGO2_TEXT_TRANSFORM_CAPITALIZE); + break; + + case PANGO2_ATTR_WORD: + integer = g_ascii_strtoll (p, &endp, 10); + if (!is_valid_end_char (*endp)) goto fail; + attr = pango2_attr_word_new (); + break; + + case PANGO2_ATTR_SENTENCE: + integer = g_ascii_strtoll (p, &endp, 10); + if (!is_valid_end_char (*endp)) goto fail; + attr = pango2_attr_sentence_new (); + break; + + case PANGO2_ATTR_PARAGRAPH: + integer = g_ascii_strtoll (p, &endp, 10); + if (!is_valid_end_char (*endp)) goto fail; + attr = pango2_attr_paragraph_new (); + break; + + case PANGO2_ATTR_BASELINE_SHIFT: + ENUM_ATTR(baseline_shift, Pango2BaselineShift, 0, G_MAXINT); + break; + + case PANGO2_ATTR_FONT_SCALE: + ENUM_ATTR(font_scale, Pango2FontScale, PANGO2_FONT_SCALE_NONE, PANGO2_FONT_SCALE_SMALL_CAPS); + break; + + case PANGO2_ATTR_LINE_SPACING: + INT_ATTR(line_spacing, int); + break; + + case PANGO2_ATTR_SHAPE: + default: + g_assert_not_reached (); + } + + attr->start_index = (guint)start_index; + attr->end_index = (guint)end_index; + g_ptr_array_add (list->attributes, attr); + + p = endp; + if (*p) + { + if (*p == ',') + p++; + p += strspn (p, " \n"); + } + } + + goto success; + +fail: + pango2_attr_list_unref (list); + list = NULL; + +success: + return list; +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pango-attr-list.h b/pango2/pango-attr-list.h new file mode 100644 index 00000000..e51dc5ff --- /dev/null +++ b/pango2/pango-attr-list.h @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2000 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-attributes.h> +#include <glib-object.h> + +G_BEGIN_DECLS + + +typedef struct _Pango2AttrList Pango2AttrList; +typedef struct _Pango2AttrIterator Pango2AttrIterator; + +#define PANGO2_TYPE_ATTR_LIST pango2_attr_list_get_type () + +/** + * Pango2AttrList: + * + * A `Pango2AttrList` represents a list of attributes that apply to a section + * of text. + * + * The attributes in a `Pango2AttrList` are, in general, allowed to overlap in + * an arbitrary fashion. However, if the attributes are manipulated only through + * [method@Pango2.AttrList.change], the overlap between properties will meet + * stricter criteria. + */ + +PANGO2_AVAILABLE_IN_ALL +GType pango2_attr_list_get_type (void) G_GNUC_CONST; + +PANGO2_AVAILABLE_IN_ALL +Pango2AttrList * pango2_attr_list_new (void); +PANGO2_AVAILABLE_IN_ALL +Pango2AttrList * pango2_attr_list_ref (Pango2AttrList *list); +PANGO2_AVAILABLE_IN_ALL +void pango2_attr_list_unref (Pango2AttrList *list); +PANGO2_AVAILABLE_IN_ALL +Pango2AttrList * pango2_attr_list_copy (Pango2AttrList *list); +PANGO2_AVAILABLE_IN_ALL +void pango2_attr_list_insert (Pango2AttrList *list, + Pango2Attribute *attr); +PANGO2_AVAILABLE_IN_ALL +void pango2_attr_list_insert_before (Pango2AttrList *list, + Pango2Attribute *attr); +PANGO2_AVAILABLE_IN_ALL +void pango2_attr_list_change (Pango2AttrList *list, + Pango2Attribute *attr); +PANGO2_AVAILABLE_IN_ALL +void pango2_attr_list_splice (Pango2AttrList *list, + Pango2AttrList *other, + int pos, + int len); +PANGO2_AVAILABLE_IN_ALL +void pango2_attr_list_update (Pango2AttrList *list, + int pos, + int remove, + int add); + +/** + * Pango2AttrFilterFunc: + * @attribute: a Pango2 attribute + * @user_data: user data passed to the function + * + * Callback to filter a list of attributes. + * + * Return value: `TRUE` if the attribute should be selected for + * filtering, `FALSE` otherwise. + */ +typedef gboolean (*Pango2AttrFilterFunc) (Pango2Attribute *attribute, + gpointer user_data); + +PANGO2_AVAILABLE_IN_ALL +Pango2AttrList * pango2_attr_list_filter (Pango2AttrList *list, + Pango2AttrFilterFunc func, + gpointer data); + +PANGO2_AVAILABLE_IN_ALL +GSList * pango2_attr_list_get_attributes (Pango2AttrList *list); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_attr_list_equal (Pango2AttrList *list, + Pango2AttrList *other_list); + +PANGO2_AVAILABLE_IN_ALL +char * pango2_attr_list_to_string (Pango2AttrList *list); +PANGO2_AVAILABLE_IN_ALL +Pango2AttrList * pango2_attr_list_from_string (const char *text); + +PANGO2_AVAILABLE_IN_ALL +Pango2AttrIterator * pango2_attr_list_get_iterator (Pango2AttrList *list); + + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(Pango2AttrList, pango2_attr_list_unref) + +G_END_DECLS diff --git a/pango2/pango-attr-private.h b/pango2/pango-attr-private.h new file mode 100644 index 00000000..f8152f8d --- /dev/null +++ b/pango2/pango-attr-private.h @@ -0,0 +1,41 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-attr.h" + +struct _Pango2Attribute +{ + guint type; + guint start_index; + guint end_index; + union { + char *str_value; + int int_value; + gboolean boolean_value; + double double_value; + Pango2Color color_value; + Pango2Language *lang_value; + Pango2FontDescription *font_value; + gpointer pointer_value; + }; +}; + +char * pango2_attr_value_serialize (Pango2Attribute *attr); diff --git a/pango2/pango-attr.c b/pango2/pango-attr.c new file mode 100644 index 00000000..5f7876d3 --- /dev/null +++ b/pango2/pango-attr.c @@ -0,0 +1,761 @@ +/* Pango2 + * pango-attr.c: Attributed text + * + * Copyright (C) 2000-2002 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include <string.h> + +#include "pango-attr-private.h" +#include "pango-attributes-private.h" +#include "pango-impl-utils.h" + +/** + * Pango2Attribute: + * + * A `Pango2Attribute` structure associates a named attribute with + * a certain range of a text. + * + * Attributes can have basic types, such as int, float, boolean + * `Pango2Color`, `Pango2FontDescription or `Pango2Language`. It is + * also possible to define new, custom attributes. + * + * By default, an attribute will have an all-inclusive range of + * `[0,G_MAXUINT]`. + */ + +/* {{{ Generic attribute code */ + +G_LOCK_DEFINE_STATIC (attr_type); +static GArray *attr_type; + +#define MIN_CUSTOM 1000 + +typedef struct _Pango2AttrClass Pango2AttrClass; + +struct _Pango2AttrClass { + guint type; + const char *name; + Pango2AttrDataCopyFunc copy; + GDestroyNotify destroy; + GEqualFunc equal; + Pango2AttrDataSerializeFunc serialize; +}; + +static const char * +get_attr_type_nick (Pango2AttrType type) +{ + GEnumClass *enum_class; + GEnumValue *enum_value; + + enum_class = g_type_class_ref (pango2_attr_type_get_type ()); + enum_value = g_enum_get_value (enum_class, type); + g_type_class_unref (enum_class); + + return enum_value->value_nick; +} + +static gboolean +is_valid_attr_type (guint type) +{ + switch (type) + { + case PANGO2_ATTR_INVALID: + return FALSE; + case PANGO2_ATTR_LANGUAGE: + case PANGO2_ATTR_FAMILY: + case PANGO2_ATTR_STYLE: + case PANGO2_ATTR_WEIGHT: + case PANGO2_ATTR_VARIANT: + case PANGO2_ATTR_STRETCH: + case PANGO2_ATTR_SIZE: + case PANGO2_ATTR_FONT_DESC: + case PANGO2_ATTR_FOREGROUND: + case PANGO2_ATTR_BACKGROUND: + case PANGO2_ATTR_UNDERLINE: + case PANGO2_ATTR_STRIKETHROUGH: + case PANGO2_ATTR_RISE: + case PANGO2_ATTR_SCALE: + case PANGO2_ATTR_FALLBACK: + case PANGO2_ATTR_LETTER_SPACING: + case PANGO2_ATTR_UNDERLINE_COLOR: + case PANGO2_ATTR_STRIKETHROUGH_COLOR: + case PANGO2_ATTR_ABSOLUTE_SIZE: + case PANGO2_ATTR_GRAVITY: + case PANGO2_ATTR_GRAVITY_HINT: + case PANGO2_ATTR_FONT_FEATURES: + case PANGO2_ATTR_ALLOW_BREAKS: + case PANGO2_ATTR_SHOW: + case PANGO2_ATTR_INSERT_HYPHENS: + case PANGO2_ATTR_OVERLINE: + case PANGO2_ATTR_OVERLINE_COLOR: + case PANGO2_ATTR_LINE_HEIGHT: + case PANGO2_ATTR_ABSOLUTE_LINE_HEIGHT: + case PANGO2_ATTR_TEXT_TRANSFORM: + case PANGO2_ATTR_WORD: + case PANGO2_ATTR_SENTENCE: + case PANGO2_ATTR_BASELINE_SHIFT: + case PANGO2_ATTR_FONT_SCALE: + case PANGO2_ATTR_SHAPE: + return TRUE; + default: + if (!attr_type) + return FALSE; + else + { + gboolean result = FALSE; + + G_LOCK (attr_type); + + for (int i = 0; i < attr_type->len; i++) + { + Pango2AttrClass *class = &g_array_index (attr_type, Pango2AttrClass, i); + if (class->type == type) + { + result = TRUE; + break; + } + } + + G_UNLOCK (attr_type); + + return result; + } + } +} + +/** + * pango2_attr_type_register: + * @name: (nullable): an identifier for the type + * @value_type: the `Pango2AttrValueType` for the attribute + * @affects: `Pango2AttrAffects` flags for the attribute + * @merge: `Pango2AttrMerge` flags for the attribute + * @copy: (scope forever) (nullable): function to copy the data of an attribute + * of this type + * @destroy: (scope forever) (nullable): function to free the data of an attribute + * of this type + * @equal: (scope forever) (nullable): function to compare the data of two attributes + * of this type + * @serialize: (scope forever) (nullable): function to serialize the data + * of an attribute of this type + * + * Register a new attribute type. + * + * The attribute type name can be accessed later + * by using [func@Pango2.AttrType.get_name]. + * + * The callbacks are only needed if @type is `PANGO2_ATTR_VALUE_POINTER`, + * Pango2 knows how to handle other value types and will only use the + * callbacks for generic pointer values. + * + * If @name and @serialize are provided, they will be used + * to serialize attributes of this type. + * + * To create attributes with the new type, use [ctor@Pango2.Attribute.new]. + * + * Return value: the attribute type ID + */ +guint +pango2_attr_type_register (const char *name, + Pango2AttrValueType value_type, + Pango2AttrAffects affects, + Pango2AttrMerge merge, + Pango2AttrDataCopyFunc copy, + GDestroyNotify destroy, + GEqualFunc equal, + Pango2AttrDataSerializeFunc serialize) +{ + static guint current_id = MIN_CUSTOM; /* MT-safe */ + Pango2AttrClass class; + + G_LOCK (attr_type); + + class.type = value_type | (affects << 8) | (merge << 12) | (current_id << 16); + current_id++; + + class.copy = copy; + class.destroy = destroy; + class.equal = equal; + class.serialize = serialize; + + if (name) + class.name = g_intern_string (name); + + if (attr_type == NULL) + attr_type = g_array_new (FALSE, FALSE, sizeof (Pango2AttrClass)); + + g_array_append_val (attr_type, class); + + G_UNLOCK (attr_type); + + return class.type; +} + +/** + * pango2_attr_type_get_name: + * @type: an attribute type ID to fetch the name for + * + * Fetches the attribute type name. + * + * The attribute type name is the string passed in + * when registering the type using + * [func@Pango2.AttrType.register]. + * + * The returned value is an interned string (see + * [func@GLib.intern_string] for what that means) that should + * not be modified or freed. + * + * Return value: (nullable): the type ID name (which + * may be %NULL), or %NULL if @type is a built-in Pango2 + * attribute type or invalid. + */ +const char * +pango2_attr_type_get_name (guint type) +{ + const char *result = NULL; + + if ((type >> 16) < MIN_CUSTOM) + return get_attr_type_nick (type); + + G_LOCK (attr_type); + + for (int i = 0; i < attr_type->len; i++) + { + Pango2AttrClass *class = &g_array_index (attr_type, Pango2AttrClass, i); + if (class->type == type) + { + result = class->name; + break; + } + } + + G_UNLOCK (attr_type); + + return result; +} + +/** + * pango2_attribute_copy: + * @attr: a `Pango2Attribute` + * + * Make a copy of an attribute. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy]. + */ +Pango2Attribute * +pango2_attribute_copy (const Pango2Attribute *attr) +{ + Pango2Attribute *result; + + g_return_val_if_fail (attr != NULL, NULL); + + result = g_slice_dup (Pango2Attribute, attr); + + switch (PANGO2_ATTR_VALUE_TYPE (attr)) + { + case PANGO2_ATTR_VALUE_STRING: + result->str_value = g_strdup (attr->str_value); + break; + + case PANGO2_ATTR_VALUE_FONT_DESC: + result->font_value = pango2_font_description_copy (attr->font_value); + break; + + case PANGO2_ATTR_VALUE_POINTER: + if (attr->type == PANGO2_ATTR_SHAPE) + { + ShapeData *shape_data; + + shape_data = g_memdup2 (attr->pointer_value, sizeof (ShapeData)); + if (shape_data->copy) + shape_data->data = shape_data->copy (shape_data->data); + + result->pointer_value = shape_data; + } + else + { + Pango2AttrDataCopyFunc copy = NULL; + + G_LOCK (attr_type); + + g_assert (attr_type != NULL); + + for (int i = 0; i < attr_type->len; i++) + { + Pango2AttrClass *class = &g_array_index (attr_type, Pango2AttrClass, i); + if (class->type == attr->type) + { + copy = class->copy; + break; + } + } + + G_UNLOCK (attr_type); + + if (copy) + result->pointer_value = copy (attr->pointer_value); + } + break; + + case PANGO2_ATTR_VALUE_INT: + case PANGO2_ATTR_VALUE_BOOLEAN: + case PANGO2_ATTR_VALUE_FLOAT: + case PANGO2_ATTR_VALUE_COLOR: + case PANGO2_ATTR_VALUE_LANGUAGE: + default: ; + } + + return result; +} + +/** + * pango2_attribute_destroy: + * @attr: a `Pango2Attribute`. + * + * Destroy a `Pango2Attribute` and free all associated memory. + */ +void +pango2_attribute_destroy (Pango2Attribute *attr) +{ + g_return_if_fail (attr != NULL); + + switch (PANGO2_ATTR_VALUE_TYPE (attr)) + { + case PANGO2_ATTR_VALUE_STRING: + g_free (attr->str_value); + break; + + case PANGO2_ATTR_VALUE_FONT_DESC: + pango2_font_description_free (attr->font_value); + break; + + case PANGO2_ATTR_VALUE_POINTER: + if (attr->type == PANGO2_ATTR_SHAPE) + { + ShapeData *shape_data = attr->pointer_value; + + if (shape_data->destroy) + shape_data->destroy (shape_data->data); + + g_free (shape_data); + } + else + { + GDestroyNotify destroy = NULL; + + G_LOCK (attr_type); + + g_assert (attr_type != NULL); + + for (int i = 0; i < attr_type->len; i++) + { + Pango2AttrClass *class = &g_array_index (attr_type, Pango2AttrClass, i); + if (class->type == attr->type) + { + destroy = class->destroy; + break; + } + } + + G_UNLOCK (attr_type); + + if (destroy) + destroy (attr->pointer_value); + } + break; + + case PANGO2_ATTR_VALUE_INT: + case PANGO2_ATTR_VALUE_BOOLEAN: + case PANGO2_ATTR_VALUE_FLOAT: + case PANGO2_ATTR_VALUE_COLOR: + case PANGO2_ATTR_VALUE_LANGUAGE: + default: ; + } + + g_slice_free (Pango2Attribute, attr); +} + +G_DEFINE_BOXED_TYPE (Pango2Attribute, pango2_attribute, + pango2_attribute_copy, + pango2_attribute_destroy); + +/** + * pango2_attribute_equal: + * @attr1: a `Pango2Attribute` + * @attr2: another `Pango2Attribute` + * + * Compare two attributes for equality. + * + * This compares only the actual value of the two + * attributes and not the ranges that the attributes + * apply to. + * + * Return value: %TRUE if the two attributes have the same value + */ +gboolean +pango2_attribute_equal (const Pango2Attribute *attr1, + const Pango2Attribute *attr2) +{ + g_return_val_if_fail (attr1 != NULL, FALSE); + g_return_val_if_fail (attr2 != NULL, FALSE); + + if (attr1->type != attr2->type) + return FALSE; + + switch (PANGO2_ATTR_VALUE_TYPE (attr1)) + { + case PANGO2_ATTR_VALUE_STRING: + return strcmp (attr1->str_value, attr2->str_value) == 0; + + case PANGO2_ATTR_VALUE_INT: + return attr1->int_value == attr2->int_value; + + case PANGO2_ATTR_VALUE_BOOLEAN: + return attr1->boolean_value == attr2->boolean_value; + + case PANGO2_ATTR_VALUE_FLOAT: + return attr1->double_value == attr2->double_value; + + case PANGO2_ATTR_VALUE_COLOR: + return memcmp (&attr1->color_value, &attr2->color_value, sizeof (Pango2Color)) == 0; + + case PANGO2_ATTR_VALUE_LANGUAGE: + return attr1->lang_value == attr2->lang_value; + + case PANGO2_ATTR_VALUE_FONT_DESC: + return pango2_font_description_equal (attr1->font_value, attr2->font_value); + + case PANGO2_ATTR_VALUE_POINTER: + { + GEqualFunc equal = NULL; + + G_LOCK (attr_type); + + g_assert (attr_type != NULL); + + for (int i = 0; i < attr_type->len; i++) + { + Pango2AttrClass *class = &g_array_index (attr_type, Pango2AttrClass, i); + if (class->type == attr1->type) + { + equal = class->equal; + break; + } + } + + G_UNLOCK (attr_type); + + if (equal) + return equal (attr1->pointer_value, attr2->pointer_value); + + return attr1->pointer_value == attr2->pointer_value; + } + + default: + g_assert_not_reached (); + } +} + +/** + * pango2_attribute_new: + * @type: the attribute type + * @value: pointer to the value to be copied, must be of the right type + * + * Creates a new attribute for the given type. + * + * The type must be one of the `Pango2AttrType` values, or + * have been registered with [func@Pango2.AttrType.register]. + * + * Pango2 will initialize @start_index and @end_index to an + * all-inclusive range of `[0,G_MAXUINT]`. The value is copied. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attribute_new (guint type, + gconstpointer value) +{ + Pango2Attribute attr; + + g_return_val_if_fail (is_valid_attr_type (type), NULL); + + attr.type = type; + attr.start_index = PANGO2_ATTR_INDEX_FROM_TEXT_BEGINNING; + attr.end_index = PANGO2_ATTR_INDEX_TO_TEXT_END; + + switch (PANGO2_ATTR_TYPE_VALUE_TYPE (type)) + { + case PANGO2_ATTR_VALUE_STRING: + attr.str_value = (char *)value; + break; + case PANGO2_ATTR_VALUE_INT: + attr.int_value = *(int *)value; + break; + case PANGO2_ATTR_VALUE_BOOLEAN: + attr.boolean_value = *(gboolean *)value; + break; + case PANGO2_ATTR_VALUE_FLOAT: + attr.double_value = *(double *)value; + break; + case PANGO2_ATTR_VALUE_COLOR: + attr.color_value = *(Pango2Color *)value; + break; + case PANGO2_ATTR_VALUE_LANGUAGE: + attr.lang_value = (Pango2Language *)value; + break; + case PANGO2_ATTR_VALUE_FONT_DESC: + attr.font_value = (Pango2FontDescription *)value; + break; + case PANGO2_ATTR_VALUE_POINTER: + attr.pointer_value = (gpointer)value; + break; + default: + g_assert_not_reached (); + } + + /* Copy the value */ + return pango2_attribute_copy (&attr); +} + +/** + * pango2_attribute_type: + * @attribute: a `Pango2Attribute` + * + * Returns the type of @attribute, either a + * value from the [enum@Pango2.AttrType] enumeration + * or a registered custom type. + * + * Return value: the type of `Pango2Attribute` + */ +guint +pango2_attribute_type (const Pango2Attribute *attribute) +{ + return attribute->type; +} + +/* }}} */ + /* {{{ Private API */ + +char * +pango2_attr_value_serialize (Pango2Attribute *attr) +{ + Pango2AttrDataSerializeFunc serialize = NULL; + + G_LOCK (attr_type); + + g_assert (attr_type != NULL); + + for (int i = 0; i < attr_type->len; i++) + { + Pango2AttrClass *class = &g_array_index (attr_type, Pango2AttrClass, i); + if (class->type == attr->type) + { + serialize = class->serialize; + break; + } + } + + G_UNLOCK (attr_type); + + if (serialize) + return serialize (attr->pointer_value); + + return g_strdup_printf ("%p", attr->pointer_value); +} + +/* }}} */ +/* {{{ Binding Helpers */ + +/** + * pango2_attribute_set_range: + * @attribute: a `Pango2Attribute` + * @start_index: start index + * @end_index: end index + * + * Sets the range of the attribute. + */ +void +pango2_attribute_set_range (Pango2Attribute *attribute, + guint start_index, + guint end_index) +{ + attribute->start_index = start_index; + attribute->end_index = end_index; +} + +/** + * pango2_attribute_get_range: + * @attribute: a `Pango2Attribute` + * @start_index: (out caller-allocates): return location for start index + * @end_index: (out caller-allocates): return location for end index + * + * Gets the range of the attribute. + */ +void +pango2_attribute_get_range (Pango2Attribute *attribute, + guint *start_index, + guint *end_index) +{ + *start_index = attribute->start_index; + *end_index = attribute->end_index; +} + +/** + * pango2_attribute_get_string: + * @attribute: a `Pango2Attribute` + * + * Gets the value of an attribute whose value type is string. + * + * Returns: (nullable): the string value + */ +const char * +pango2_attribute_get_string (Pango2Attribute *attribute) +{ + if (PANGO2_ATTR_VALUE_TYPE (attribute) != PANGO2_ATTR_VALUE_STRING) + return NULL; + + return attribute->str_value; +} + +/** + * pango2_attribute_get_language: + * @attribute: a `Pango2Attribute` + * + * Gets the value of an attribute whose value type is `Pango2Language`. + * + * Returns: (nullable): the language value + */ +Pango2Language * +pango2_attribute_get_language (Pango2Attribute *attribute) +{ + if (PANGO2_ATTR_VALUE_TYPE (attribute) != PANGO2_ATTR_VALUE_LANGUAGE) + return NULL; + + return attribute->lang_value; +} + +/** + * pango2_attribute_get_int: + * @attribute: a `Pango2Attribute` + * + * Gets the value of an attribute whose value type is `int`. + * + * Returns: the integer value, or 0 + */ +int +pango2_attribute_get_int (Pango2Attribute *attribute) +{ + if (PANGO2_ATTR_VALUE_TYPE (attribute) != PANGO2_ATTR_VALUE_INT) + return 0; + + return attribute->int_value; +} + +/** + * pango2_attribute_get_boolean: + * @attribute: a `Pango2Attribute` + * + * Gets the value of an attribute whose value type is `gboolean`. + * + * Returns: the boolean value, or `FALSE` + */ +gboolean +pango2_attribute_get_boolean (Pango2Attribute *attribute) +{ + if (PANGO2_ATTR_VALUE_TYPE (attribute) != PANGO2_ATTR_VALUE_BOOLEAN) + return FALSE; + + return attribute->boolean_value; +} + +/** + * pango2_attribute_get_float: + * @attribute: a `Pango2Attribute` + * + * Gets the value of an attribute whose value type is `double`. + * + * Returns: the float value, or 0 + */ +double +pango2_attribute_get_float (Pango2Attribute *attribute) +{ + if (PANGO2_ATTR_VALUE_TYPE (attribute) != PANGO2_ATTR_VALUE_FLOAT) + return 0.; + + return attribute->double_value; +} + +/** + * pango2_attribute_get_color: + * @attribute: a `Pango2Attribute` + * + * Gets the value of an attribute whose value type is `Pango2Color`. + * + * Returns: (nullable): pointer to the color value + */ +Pango2Color * +pango2_attribute_get_color (Pango2Attribute *attribute) +{ + if (PANGO2_ATTR_VALUE_TYPE (attribute) != PANGO2_ATTR_VALUE_COLOR) + return NULL; + + return &attribute->color_value; +} + +/** + * pango2_attribute_get_font_desc: + * @attribute: a `Pango2Attribute` + * + * Gets the value of an attribute whose value type is `Pango2FontDescription`. + * + * Returns: (nullable): the font description + */ +Pango2FontDescription * +pango2_attribute_get_font_desc (Pango2Attribute *attribute) +{ + if (PANGO2_ATTR_VALUE_TYPE (attribute) != PANGO2_ATTR_VALUE_FONT_DESC) + return NULL; + + return attribute->font_value; +} + +/** + * pango2_attribute_get_pointer: + * @attribute: a `Pango2Attribute` + * + * Gets the value of an attribute whose value type is `gpointer`. + * + * Returns: (nullable): the pointer value + */ +gpointer +pango2_attribute_get_pointer (Pango2Attribute *attribute) +{ + if (PANGO2_ATTR_VALUE_TYPE (attribute) != PANGO2_ATTR_VALUE_POINTER) + return NULL; + + return attribute->pointer_value; +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pango-attr.h b/pango2/pango-attr.h new file mode 100644 index 00000000..1dda2924 --- /dev/null +++ b/pango2/pango-attr.h @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2000 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-font.h> +#include <pango2/pango-color.h> +#include <glib-object.h> + +G_BEGIN_DECLS + + +typedef struct _Pango2Attribute Pango2Attribute; + +/** + * Pango2AttrValueType: + * @PANGO2_ATTR_VALUE_STRING: A string + * @PANGO2_ATTR_VALUE_INT: An integer + * @PANGO2_ATTR_VALUE_BOOLEAN: A boolean + * @PANGO2_ATTR_VALUE_FLOAT: A floating point number + * @PANGO2_ATTR_VALUE_COLOR: A `Pango2Color` + * @PANGO2_ATTR_VALUE_LANGUAGE: A `Pango2Language` + * @PANGO2_ATTR_VALUE_FONT_DESC: A `Pango2FontDescription` + * @PANGO2_ATTR_VALUE_POINTER: A generic pointer + * + * `Pango2AttrValueType` enumerates the types of values + * that a `Pango2Attribute` can contain. + * + * The `Pango2AttrValueType` of a `Pango2Attribute` is part + * of its type, and can be obtained with `PANGO2_ATTR_VALUE_TYPE()`. + */ +typedef enum +{ + PANGO2_ATTR_VALUE_STRING, + PANGO2_ATTR_VALUE_INT, + PANGO2_ATTR_VALUE_BOOLEAN, + PANGO2_ATTR_VALUE_FLOAT, + PANGO2_ATTR_VALUE_COLOR, + PANGO2_ATTR_VALUE_LANGUAGE, + PANGO2_ATTR_VALUE_FONT_DESC, + PANGO2_ATTR_VALUE_POINTER +} Pango2AttrValueType; + +/** + * Pango2AttrAffects: + * @PANGO2_ATTR_AFFECTS_NONE: The attribute does not affect rendering + * @PANGO2_ATTR_AFFECTS_ITEMIZATION: The attribute affecs itemization + * @PANGO2_ATTR_AFFECTS_BREAKING: The attribute affects `Pango2LogAttr` determination + * @PANGO2_ATTR_AFFECTS_SHAPING: The attribute affects shaping + * @PANGO2_ATTR_AFFECTS_RENDERING: The attribute affects rendering + * + * A `Pango2AttrAffects` value indicates what part of Pango2's processing + * pipeline is affected by an attribute. + * + * Marking an attribute with `PANGO2_ATTR_AFFECTS_ITEMIZATION` ensures + * that the attribute values are constant across items. + */ +typedef enum +{ + PANGO2_ATTR_AFFECTS_NONE = 0, + PANGO2_ATTR_AFFECTS_ITEMIZATION = 1 << 0, + PANGO2_ATTR_AFFECTS_BREAKING = 1 << 1, + PANGO2_ATTR_AFFECTS_SHAPING = 1 << 2, + PANGO2_ATTR_AFFECTS_RENDERING = 1 << 3 +} Pango2AttrAffects; + +/** + * Pango2AttrMerge: + * @PANGO2_ATTR_MERGE_OVERRIDES: Only the attribute with the narrowest range is used + * @PANGO2_ATTR_MERGE_ACCUMULATES: All attributes with overlapping range are kept + * + * A `Pango2AttrMerge` value indicates how overlapping attribute values + * should be reconciled to determine the effective attribute value. + * + * These options influence the @extra_attrs returned by + * [method@Pango2.AttrIterator.get_font]. + */ +typedef enum +{ + PANGO2_ATTR_MERGE_OVERRIDES, + PANGO2_ATTR_MERGE_ACCUMULATES +} Pango2AttrMerge; + +/** + * PANGO2_ATTR_TYPE_VALUE_TYPE: + * @type: an attribute type + * + * Extracts the `Pango2AttrValueType` from an attribute type. + */ +#define PANGO2_ATTR_TYPE_VALUE_TYPE(type) ((Pango2AttrValueType)((type) & 0xff)) + +/** + * PANGO2_ATTR_TYPE_AFFECTS: + * @type: an attribute type + * + * Extracts the `Pango2AttrAffects` flags from an attribute type. + */ +#define PANGO2_ATTR_TYPE_AFFECTS(type) ((Pango2AttrAffects)(((type) >> 8) & 0xf)) + +/** + * PANGO2_ATTR_TYPE_MERGE: + * @type: an attribute type + * + * Extracts the `Pango2AttrMerge` flags from an attribute type. + */ +#define PANGO2_ATTR_TYPE_MERGE(type) ((Pango2AttrMerge)(((type) >> 12) & 0xf)) + +/** + * PANGO2_ATTR_VALUE_TYPE: + * @attr: a `Pango2Attribute` + * + * Obtains the `Pango2AttrValueType of a `Pango2Attribute`. + */ +#define PANGO2_ATTR_VALUE_TYPE(attr) PANGO2_ATTR_TYPE_VALUE_TYPE (pango2_attribute_type (attr)) + +/** + * PANGO2_ATTR_AFFECTS: + * @attr: a `Pango2Attribute` + * + * Obtains the `Pango2AttrAffects` flags of a `Pango2Attribute`. + */ +#define PANGO2_ATTR_AFFECTS(attr) PANGO2_ATTR_TYPE_AFFECTS (pango2_attribute_type (attr)) + +/** + * PANGO2_ATTR_MERGE: + * @attr: a `Pango2Attribute` + * + * Obtains the `Pango2AttrMerge` flags of a `Pango2Attribute`. + */ +#define PANGO2_ATTR_MERGE(attr) PANGO2_ATTR_TYPE_MERGE (pango2_attribute_type (attr)) + +/** + * PANGO2_ATTR_INDEX_FROM_TEXT_BEGINNING: + * + * Value for @start_index in `Pango2Attribute` that indicates + * the beginning of the text. + */ +#define PANGO2_ATTR_INDEX_FROM_TEXT_BEGINNING ((guint)0) + +/** + * PANGO2_ATTR_INDEX_TO_TEXT_END: (value 4294967295) + * + * Value for @end_index in `Pango2Attribute` that indicates + * the end of the text. + */ +#define PANGO2_ATTR_INDEX_TO_TEXT_END ((guint)(G_MAXUINT + 0)) + +/** + * Pango2AttrDataCopyFunc: + * @value: value to copy + * + * Callback to duplicate the value of an attribute. + * + * Return value: new copy of @value. + **/ +typedef gpointer (*Pango2AttrDataCopyFunc) (gconstpointer value); + +/** + * Pango2AttrDataSerializeFunc: + * @value: value to serialize + * + * Callback to serialize the value of an attribute. + * + * Return value: a newly allocated string holding the serialization of @value + */ +typedef char * (*Pango2AttrDataSerializeFunc) (gconstpointer value); + +PANGO2_AVAILABLE_IN_ALL +GType pango2_attribute_get_type (void) G_GNUC_CONST; + +PANGO2_AVAILABLE_IN_ALL +guint pango2_attr_type_register (const char *name, + Pango2AttrValueType value_type, + Pango2AttrAffects affects, + Pango2AttrMerge merge, + Pango2AttrDataCopyFunc copy, + GDestroyNotify destroy, + GEqualFunc equal, + Pango2AttrDataSerializeFunc serialize); + +PANGO2_AVAILABLE_IN_ALL +const char * pango2_attr_type_get_name (guint type) G_GNUC_CONST; +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attribute_copy (const Pango2Attribute *attr); +PANGO2_AVAILABLE_IN_ALL +void pango2_attribute_destroy (Pango2Attribute *attr); +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_attribute_equal (const Pango2Attribute *attr1, + const Pango2Attribute *attr2) G_GNUC_PURE; + +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attribute_new (guint type, + gconstpointer value); + +PANGO2_AVAILABLE_IN_ALL +guint pango2_attribute_type (const Pango2Attribute *attribute); + +PANGO2_AVAILABLE_IN_ALL +void pango2_attribute_set_range (Pango2Attribute *attribute, + guint start_index, + guint end_index); + +PANGO2_AVAILABLE_IN_ALL +void pango2_attribute_get_range (Pango2Attribute *attribute, + guint *start_index, + guint *end_index); + +PANGO2_AVAILABLE_IN_ALL +const char * pango2_attribute_get_string (Pango2Attribute *attribute); +PANGO2_AVAILABLE_IN_ALL +Pango2Language * pango2_attribute_get_language (Pango2Attribute *attribute); +PANGO2_AVAILABLE_IN_ALL +int pango2_attribute_get_int (Pango2Attribute *attribute); +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_attribute_get_boolean (Pango2Attribute *attribute); +PANGO2_AVAILABLE_IN_ALL +double pango2_attribute_get_float (Pango2Attribute *attribute); +PANGO2_AVAILABLE_IN_ALL +Pango2Color * pango2_attribute_get_color (Pango2Attribute *attribute); +PANGO2_AVAILABLE_IN_ALL +Pango2FontDescription * pango2_attribute_get_font_desc (Pango2Attribute *attribute); + +PANGO2_AVAILABLE_IN_ALL +gpointer pango2_attribute_get_pointer (Pango2Attribute *attribute); + + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(Pango2Attribute, pango2_attribute_destroy) + +G_END_DECLS diff --git a/pango2/pango-attributes-private.h b/pango2/pango-attributes-private.h new file mode 100644 index 00000000..ef1ab2c3 --- /dev/null +++ b/pango2/pango-attributes-private.h @@ -0,0 +1,36 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-attributes.h" + +gboolean pango2_attribute_affects_itemization (Pango2Attribute *attr, + gpointer data); +gboolean pango2_attribute_affects_break_or_shape (Pango2Attribute *attr, + gpointer data); + +typedef struct { + Pango2Rectangle ink_rect; + Pango2Rectangle logical_rect; + gpointer data; + Pango2AttrDataCopyFunc copy; + GDestroyNotify destroy; +} ShapeData; + diff --git a/pango2/pango-attributes.c b/pango2/pango-attributes.c new file mode 100644 index 00000000..ea782a07 --- /dev/null +++ b/pango2/pango-attributes.c @@ -0,0 +1,866 @@ +/* Pango2 + * pango-attributes.c: Attributed text + * + * Copyright (C) 2000-2002 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include <string.h> + +#include "pango-attributes-private.h" +#include "pango-attr-private.h" +#include "pango-impl-utils.h" + +/* {{{ Attribute value types */ + +static inline Pango2Attribute * +pango2_attr_init (Pango2AttrType type) +{ + Pango2Attribute *attr; + + attr = g_slice_new (Pango2Attribute); + attr->type = type; + attr->start_index = PANGO2_ATTR_INDEX_FROM_TEXT_BEGINNING; + attr->end_index = PANGO2_ATTR_INDEX_TO_TEXT_END; + + return attr; +} + +static inline Pango2Attribute * +pango2_attr_string_new (Pango2AttrType type, + const char *value) +{ + Pango2Attribute *attr; + + g_return_val_if_fail (PANGO2_ATTR_TYPE_VALUE_TYPE (type) == PANGO2_ATTR_VALUE_STRING, NULL); + + attr = pango2_attr_init (type); + attr->str_value = g_strdup (value); + + return attr; +} + +static inline Pango2Attribute * +pango2_attr_int_new (Pango2AttrType type, + int value) +{ + Pango2Attribute *attr; + + g_return_val_if_fail (PANGO2_ATTR_TYPE_VALUE_TYPE (type) == PANGO2_ATTR_VALUE_INT, NULL); + + attr = pango2_attr_init (type); + attr->int_value = value; + + return attr; +} + +static inline Pango2Attribute * +pango2_attr_boolean_new (Pango2AttrType type, + gboolean value) +{ + Pango2Attribute *attr; + + g_return_val_if_fail (PANGO2_ATTR_TYPE_VALUE_TYPE (type) == PANGO2_ATTR_VALUE_BOOLEAN, NULL); + + attr = pango2_attr_init (type); + attr->boolean_value = value; + + return attr; +} + +static inline Pango2Attribute * +pango2_attr_float_new (Pango2AttrType type, + double value) +{ + Pango2Attribute *attr; + + g_return_val_if_fail (PANGO2_ATTR_TYPE_VALUE_TYPE (type) == PANGO2_ATTR_VALUE_FLOAT, NULL); + + attr = pango2_attr_init (type); + attr->double_value = value; + + return attr; +} + +static inline Pango2Attribute * +pango2_attr_color_new (Pango2AttrType type, + Pango2Color *color) +{ + Pango2Attribute *attr; + + g_return_val_if_fail (PANGO2_ATTR_TYPE_VALUE_TYPE (type) == PANGO2_ATTR_VALUE_COLOR, NULL); + + attr = pango2_attr_init (type); + attr->color_value = *color; + + return attr; +} + +static inline Pango2Attribute * +pango2_attr_lang_new (Pango2AttrType type, + Pango2Language *value) +{ + Pango2Attribute *attr; + + g_return_val_if_fail (PANGO2_ATTR_TYPE_VALUE_TYPE (type) == PANGO2_ATTR_VALUE_LANGUAGE, NULL); + + attr = pango2_attr_init (type); + attr->lang_value = value; + + return attr; +} + +static inline Pango2Attribute * +pango2_attr_font_description_new (Pango2AttrType type, + const Pango2FontDescription *value) +{ + Pango2Attribute *attr; + + g_return_val_if_fail (PANGO2_ATTR_TYPE_VALUE_TYPE (type) == PANGO2_ATTR_VALUE_FONT_DESC, NULL); + + attr = pango2_attr_init (type); + attr->font_value = pango2_font_description_copy (value); + + return attr; +} + +/* }}} */ +/* {{{ Attribute types */ + +/** + * pango2_attr_family_new: + * @family: the family or comma-separated list of families + * + * Create a new font family attribute. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_family_new (const char *family) +{ + return pango2_attr_string_new (PANGO2_ATTR_FAMILY, family); +} + +/** + * pango2_attr_language_new: + * @language: language tag + * + * Create a new language tag attribute. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_language_new (Pango2Language *language) +{ + return pango2_attr_lang_new (PANGO2_ATTR_LANGUAGE, language); +} + +/** + * pango2_attr_foreground_new: + * @color: the color + * + * Create a new foreground color attribute. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_foreground_new (Pango2Color *color) +{ + return pango2_attr_color_new (PANGO2_ATTR_FOREGROUND, color); +} + +/** + * pango2_attr_background_new: + * @color: the color + * + * Create a new background color attribute. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_background_new (Pango2Color *color) +{ + return pango2_attr_color_new (PANGO2_ATTR_BACKGROUND, color); +} + +/** + * pango2_attr_size_new: + * @size: the font size, in %PANGO2_SCALE-ths of a point + * + * Create a new font-size attribute in fractional points. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_size_new (int value) +{ + return pango2_attr_int_new (PANGO2_ATTR_SIZE, value); +} + + +/** + * pango2_attr_size_new_absolute: + * @size: the font size, in %PANGO2_SCALE-ths of a device unit + * + * Create a new font-size attribute in device units. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_size_new_absolute (int size) +{ + return pango2_attr_int_new (PANGO2_ATTR_ABSOLUTE_SIZE, size); +} + +/** + * pango2_attr_style_new: + * @style: the slant style + * + * Create a new font slant style attribute. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_style_new (Pango2Style style) +{ + return pango2_attr_int_new (PANGO2_ATTR_STYLE, (int)style); +} + +/** + * pango2_attr_weight_new: + * @weight: the weight + * + * Create a new font weight attribute. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_weight_new (Pango2Weight weight) +{ + return pango2_attr_int_new (PANGO2_ATTR_WEIGHT, (int)weight); +} + +/** + * pango2_attr_variant_new: + * @variant: the variant + * + * Create a new font variant attribute (normal or small caps). + * + * Return value: (transfer full): the newly allocated `Pango2Attribute`, + * which should be freed with [method@Pango2.Attribute.destroy]. + */ +Pango2Attribute * +pango2_attr_variant_new (Pango2Variant variant) +{ + return pango2_attr_int_new (PANGO2_ATTR_VARIANT, (int)variant); +} + +/** + * pango2_attr_stretch_new: + * @stretch: the stretch + * + * Create a new font stretch attribute. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_stretch_new (Pango2Stretch stretch) +{ + return pango2_attr_int_new (PANGO2_ATTR_STRETCH, (int)stretch); +} + +/** + * pango2_attr_font_desc_new: + * @desc: the font description + * + * Create a new font description attribute. + * + * This attribute allows setting family, style, weight, variant, + * stretch, and size simultaneously. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_font_desc_new (const Pango2FontDescription *desc) +{ + return pango2_attr_font_description_new (PANGO2_ATTR_FONT_DESC, desc); +} + +/** + * pango2_attr_underline_new: + * @style: the line style + * + * Create a new underline attribute. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_underline_new (Pango2LineStyle style) +{ + return pango2_attr_int_new (PANGO2_ATTR_UNDERLINE, (int)style); +} + +/** + * pango2_attr_underline_color_new: + * @color: the color + * + * Create a new underline color attribute. + * + * This attribute modifies the color of underlines. + * If not set, underlines will use the foreground color. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_underline_color_new (Pango2Color *color) +{ + return pango2_attr_color_new (PANGO2_ATTR_UNDERLINE_COLOR, color); +} + +/** + * pango2_attr_underline_position_new: + * @position: the underline position + * + * Create a new underline position attribute. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_underline_position_new (Pango2UnderlinePosition position) +{ + return pango2_attr_int_new (PANGO2_ATTR_UNDERLINE_POSITION, (int)position); +} +/** + * pango2_attr_strikethrough_new: + * @style: the line style + * + * Create a new strike-through attribute. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_strikethrough_new (Pango2LineStyle style) +{ + return pango2_attr_int_new (PANGO2_ATTR_STRIKETHROUGH, (int)style); +} + +/** + * pango2_attr_strikethrough_color_new: + * @color: the color + * + * Create a new strikethrough color attribute. + * + * This attribute modifies the color of strikethrough lines. + * If not set, strikethrough lines will use the foreground color. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_strikethrough_color_new (Pango2Color *color) +{ + return pango2_attr_color_new (PANGO2_ATTR_STRIKETHROUGH_COLOR, color); +} + +/** + * pango2_attr_rise_new: + * @rise: the amount that the text should be displaced vertically, + * in Pango2 units. Positive values displace the text upwards. + * + * Create a new baseline displacement attribute. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_rise_new (int rise) +{ + return pango2_attr_int_new (PANGO2_ATTR_RISE, (int)rise); +} + +/** + * pango2_attr_baseline_shift_new: + * @shift: either a `Pango2BaselineShift` enumeration value or an absolute value (> 1024) + * in Pango2 units, relative to the baseline of the previous run. + * Positive values displace the text upwards. + * + * Create a new baseline displacement attribute. + * + * The effect of this attribute is to shift the baseline of a run, + * relative to the run of preceding run. + * + * <picture> + * <source srcset="baseline-shift-dark.png" media="(prefers-color-scheme: dark)"> + * <img alt="Baseline Shift" src="baseline-shift-light.png"> + * </picture> + + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_baseline_shift_new (int rise) +{ + return pango2_attr_int_new (PANGO2_ATTR_BASELINE_SHIFT, (int)rise); +} + +/** + * pango2_attr_font_scale_new: + * @scale: a `Pango2FontScale` value, which indicates font size change relative + * to the size of the previous run. + * + * Create a new font scale attribute. + * + * The effect of this attribute is to change the font size of a run, + * relative to the size of preceding run. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_font_scale_new (Pango2FontScale scale) +{ + return pango2_attr_int_new (PANGO2_ATTR_FONT_SCALE, (int)scale); +} + +/** + * pango2_attr_scale_new: + * @scale_factor: factor to scale the font + * + * Create a new font size scale attribute. + * + * The base font for the affected text will have + * its size multiplied by @scale_factor. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute* +pango2_attr_scale_new (double scale_factor) +{ + return pango2_attr_float_new (PANGO2_ATTR_SCALE, scale_factor); +} + +/** + * pango2_attr_fallback_new: + * @enable_fallback: %TRUE if we should fall back on other fonts + * for characters the active font is missing + * + * Create a new font fallback attribute. + * + * If fallback is disabled, characters will only be + * used from the closest matching font on the system. + * No fallback will be done to other fonts on the system + * that might contain the characters in the text. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_fallback_new (gboolean enable_fallback) +{ + return pango2_attr_boolean_new (PANGO2_ATTR_FALLBACK, enable_fallback); +} + +/** + * pango2_attr_letter_spacing_new: + * @letter_spacing: amount of extra space to add between + * graphemes of the text, in Pango2 units + * + * Create a new letter-spacing attribute. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_letter_spacing_new (int letter_spacing) +{ + return pango2_attr_int_new (PANGO2_ATTR_LETTER_SPACING, letter_spacing); +} + +/** + * pango2_attr_gravity_new: + * @gravity: the gravity value; should not be %PANGO2_GRAVITY_AUTO + * + * Create a new gravity attribute. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_gravity_new (Pango2Gravity gravity) +{ + g_return_val_if_fail (gravity != PANGO2_GRAVITY_AUTO, NULL); + + return pango2_attr_int_new (PANGO2_ATTR_GRAVITY, (int)gravity); +} + +/** + * pango2_attr_gravity_hint_new: + * @hint: the gravity hint value + * + * Create a new gravity hint attribute. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_gravity_hint_new (Pango2GravityHint hint) +{ + return pango2_attr_int_new (PANGO2_ATTR_GRAVITY_HINT, (int)hint); +} + +/** + * pango2_attr_font_features_new: + * @features: a string with OpenType font features, with the syntax of the [CSS + * font-feature-settings property](https://www.w3.org/TR/css-fonts-4/#font-rend-desc) + * + * Create a new font features tag attribute. + * + * You can use this attribute to select OpenType font features like small-caps, + * alternative glyphs, ligatures, etc. for fonts that support them. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_font_features_new (const char *features) +{ + g_return_val_if_fail (features != NULL, NULL); + + return pango2_attr_string_new (PANGO2_ATTR_FONT_FEATURES, features); +} + +/** + * pango2_attr_allow_breaks_new: + * @allow_breaks: %TRUE if we line breaks are allowed + * + * Create a new allow-breaks attribute. + * + * If breaks are disabled, the range will be kept in a + * single run, as far as possible. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_allow_breaks_new (gboolean allow_breaks) +{ + return pango2_attr_boolean_new (PANGO2_ATTR_ALLOW_BREAKS, allow_breaks); +} + +/** + * pango2_attr_insert_hyphens_new: + * @insert_hyphens: %TRUE if hyphens should be inserted + * + * Create a new insert-hyphens attribute. + * + * Pango2 will insert hyphens when breaking lines in + * the middle of a word. This attribute can be used + * to suppress the hyphen. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_insert_hyphens_new (gboolean insert_hyphens) +{ + return pango2_attr_boolean_new (PANGO2_ATTR_INSERT_HYPHENS, insert_hyphens); +} + +/** + * pango2_attr_show_new: + * @flags: `Pango2ShowFlags` to apply + * + * Create a new attribute that influences how invisible + * characters are rendered. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + **/ +Pango2Attribute * +pango2_attr_show_new (Pango2ShowFlags flags) +{ + return pango2_attr_int_new (PANGO2_ATTR_SHOW, (int)flags); +} + +/** + * pango2_attr_word_new: + * + * Create a new attribute that marks its range + * as a single word. + * + * Note that this may require adjustments to word and + * sentence classification around the range. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_word_new (void) +{ + return pango2_attr_boolean_new (PANGO2_ATTR_WORD, TRUE); +} + +/** + * pango2_attr_sentence_new: + * + * Create a new attribute that marks its range + * as a single sentence. + * + * Note that this may require adjustments to word and + * sentence classification around the range. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_sentence_new (void) +{ + return pango2_attr_boolean_new (PANGO2_ATTR_SENTENCE, TRUE); +} + +/** + * pango2_attr_paragraph_new: + * + * Create a new attribute that marks its range as a single paragraph. + * + * Newlines and similar characters in the range of the attribute + * will not be treated as paragraph separators. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_paragraph_new (void) +{ + return pango2_attr_boolean_new (PANGO2_ATTR_SENTENCE, TRUE); +} + +/** + * pango2_attr_overline_new: + * @style: the line style + * + * Create a new overline-style attribute. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_overline_new (Pango2LineStyle style) +{ + return pango2_attr_int_new (PANGO2_ATTR_OVERLINE, (int)style); +} + +/** + * pango2_attr_overline_color_new: + * @color: the color + * + * Create a new overline color attribute. + * + * This attribute modifies the color of overlines. + * If not set, overlines will use the foreground color. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_overline_color_new (Pango2Color *color) +{ + return pango2_attr_color_new (PANGO2_ATTR_OVERLINE_COLOR, color); +} + +/** + * pango2_attr_line_height_new: + * @factor: the scaling factor to apply to the logical height + * + * Create a new attribute that modifies the height + * of logical line extents by a factor. + * + * This affects the values returned by + * [method@Pango2.Line.get_extents] and + * [method@Pango2.LineIter.get_line_extents]. + */ +Pango2Attribute * +pango2_attr_line_height_new (double factor) +{ + return pango2_attr_float_new (PANGO2_ATTR_LINE_HEIGHT, factor); +} + +/** + * pango2_attr_line_height_new_absolute: + * @height: the line height, in %PANGO2_SCALE-ths of a point + * + * Create a new attribute that overrides the height + * of logical line extents to be @height. + * + * This affects the values returned by + * [method@Pango2.Line.get_extents], + * [method@Pango2.LineIter.get_line_extents]. + */ +Pango2Attribute * +pango2_attr_line_height_new_absolute (int height) +{ + return pango2_attr_int_new (PANGO2_ATTR_ABSOLUTE_LINE_HEIGHT, height); +} + +/** + * pango2_attr_line_spacing_new: + * @spacing: the line spacing, in %PANGO2_SCALE-ths of a point + * + * Create a new attribute that adds space to the + * leading from font metrics, if not overridden + * by line spacing attributes. + * + * This affects the values returned by + * [method@Pango2.Line.get_extents], + * [method@Pango2.LineIter.get_line_extents]. + */ +Pango2Attribute * +pango2_attr_line_spacing_new (int spacing) +{ + return pango2_attr_int_new (PANGO2_ATTR_LINE_SPACING, spacing); +} + +/** + * pango2_attr_text_transform_new: + * @transform: `Pango2TextTransform` to apply + * + * Create a new attribute that influences how characters + * are transformed during shaping. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_text_transform_new (Pango2TextTransform transform) +{ + return pango2_attr_int_new (PANGO2_ATTR_TEXT_TRANSFORM, transform); +} + +/** + * pango2_attr_shape_new: + * @ink_rect: ink rectangle to use for each character + * @logical_rect: logical rectangle to use for each character + * @data: user data + * @copy: (nullable): function to copy @data when the attribute + * is copied + * @destroy: (nullable): function to free @data when the attribute + * is freed + * + * Creates a new shape attribute. + * + * Shape attributes override the extents for a glyph and can + * trigger custom rendering in a `Pango2Renderer`. This might + * be used, for instance, for embedding a picture or a widget + * inside a `Pango2Layout`. + * + * Return value: (transfer full): the newly allocated + * `Pango2Attribute`, which should be freed with + * [method@Pango2.Attribute.destroy] + */ +Pango2Attribute * +pango2_attr_shape_new (Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect, + gpointer data, + Pango2AttrDataCopyFunc copy, + GDestroyNotify destroy) +{ + Pango2Attribute *attr; + ShapeData *shape_data; + + shape_data = g_new0 (ShapeData, 1); + shape_data->ink_rect = *ink_rect; + shape_data->logical_rect = *logical_rect; + shape_data->data = data; + shape_data->copy = copy; + shape_data->destroy = destroy; + + attr = pango2_attr_init (PANGO2_ATTR_SHAPE); + attr->pointer_value = shape_data; + + return attr; +} + +/* }}} */ +/* {{{ Private API */ + +gboolean +pango2_attribute_affects_itemization (Pango2Attribute *attr, + gpointer data) +{ + return (PANGO2_ATTR_AFFECTS (attr) & PANGO2_ATTR_AFFECTS_ITEMIZATION) != 0; +} + +gboolean +pango2_attribute_affects_break_or_shape (Pango2Attribute *attr, + gpointer data) +{ + return (PANGO2_ATTR_AFFECTS (attr) & (PANGO2_ATTR_AFFECTS_BREAKING | + PANGO2_ATTR_AFFECTS_SHAPING)) != 0; +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pango-attributes.h b/pango2/pango-attributes.h new file mode 100644 index 00000000..98e89d7b --- /dev/null +++ b/pango2/pango-attributes.h @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2000 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-font.h> +#include <pango2/pango-color.h> +#include <pango2/pango-attr.h> + +G_BEGIN_DECLS + +#ifndef __GI_SCANNER__ + +#define PANGO2_ATTR_TYPE(value, affects, merge) (PANGO2_ATTR_VALUE_##value | (PANGO2_ATTR_AFFECTS_##affects << 8) | (PANGO2_ATTR_MERGE_##merge << 12) | (__COUNTER__ << 16)) + +#endif + +/** + * Pango2AttrType: + * @PANGO2_ATTR_INVALID: does not happen + * @PANGO2_ATTR_LANGUAGE: language + * @PANGO2_ATTR_FAMILY: font family name + * @PANGO2_ATTR_STYLE: font style + * @PANGO2_ATTR_WEIGHT: font weight + * @PANGO2_ATTR_VARIANT: font variant + * @PANGO2_ATTR_STRETCH: font stretch + * @PANGO2_ATTR_SIZE: font size in points scaled by `PANGO2_SCALE` + * @PANGO2_ATTR_FONT_DESC: font description + * @PANGO2_ATTR_FOREGROUND: foreground color + * @PANGO2_ATTR_BACKGROUND: background color + * @PANGO2_ATTR_UNDERLINE: underline style + * @PANGO2_ATTR_UNDERLINE_POSITION: underline position + * @PANGO2_ATTR_STRIKETHROUGH: whether the text is struck-through + * @PANGO2_ATTR_RISE: baseline displacement + * @PANGO2_ATTR_SCALE: font size scale factor + * @PANGO2_ATTR_FALLBACK: whether font fallback is enabled + * @PANGO2_ATTR_LETTER_SPACING: letter spacing in Pango2 units + * @PANGO2_ATTR_UNDERLINE_COLOR: underline color + * @PANGO2_ATTR_STRIKETHROUGH_COLOR: strikethrough color + * @PANGO2_ATTR_ABSOLUTE_SIZE: font size in pixels scaled by `PANGO2_SCALE` + * @PANGO2_ATTR_GRAVITY: base text gravity + * @PANGO2_ATTR_GRAVITY_HINT: gravity hint + * @PANGO2_ATTR_FONT_FEATURES: OpenType font features + * @PANGO2_ATTR_ALLOW_BREAKS: whether line breaks are allowed + * @PANGO2_ATTR_SHOW: how to render invisible characters + * @PANGO2_ATTR_INSERT_HYPHENS: whether to insert hyphens at intra-word line breaks + * @PANGO2_ATTR_OVERLINE: whether the text has an overline + * @PANGO2_ATTR_OVERLINE_COLOR: overline color + * @PANGO2_ATTR_LINE_HEIGHT: line height factor + * @PANGO2_ATTR_ABSOLUTE_LINE_HEIGHT: line height in Pango2 units + * @PANGO2_ATTR_WORD: mark the range of the attribute as a single word + * @PANGO2_ATTR_SENTENCE: mark the range of the attribute as a single sentence + * @PANGO2_ATTR_PARAGRAPH: mark the range of the attribute as a single paragraph + * @PANGO2_ATTR_BASELINE_SHIFT: baseline displacement + * @PANGO2_ATTR_FONT_SCALE: font-relative size change + * @PANGO2_ATTR_LINE_SPACING: space to add to the leading from the + * font metrics (if not overridden by a line height attribute) + * @PANGO2_ATTR_SHAPE: override glyph shapes (requires renderer support) + * + * `Pango2AttrType` contains predefined attribute types. + * + * Along with the predefined values, it is possible to allocate additional + * values for custom attributes using [func@AttrType.register]. The predefined + * values are given below. + */ +typedef enum +{ + PANGO2_ATTR_INVALID, + PANGO2_ATTR_LANGUAGE = PANGO2_ATTR_TYPE (LANGUAGE, ITEMIZATION, OVERRIDES), + PANGO2_ATTR_FAMILY = PANGO2_ATTR_TYPE (STRING, ITEMIZATION, OVERRIDES), + PANGO2_ATTR_STYLE = PANGO2_ATTR_TYPE (INT, ITEMIZATION, OVERRIDES), + PANGO2_ATTR_WEIGHT = PANGO2_ATTR_TYPE (INT, ITEMIZATION, OVERRIDES), + PANGO2_ATTR_VARIANT = PANGO2_ATTR_TYPE (INT, ITEMIZATION, OVERRIDES), + PANGO2_ATTR_STRETCH = PANGO2_ATTR_TYPE (INT, ITEMIZATION, OVERRIDES), + PANGO2_ATTR_SIZE = PANGO2_ATTR_TYPE (INT, ITEMIZATION, OVERRIDES), + PANGO2_ATTR_FONT_DESC = PANGO2_ATTR_TYPE (FONT_DESC, ITEMIZATION, ACCUMULATES), + PANGO2_ATTR_FOREGROUND = PANGO2_ATTR_TYPE (COLOR, RENDERING, OVERRIDES), + PANGO2_ATTR_BACKGROUND = PANGO2_ATTR_TYPE (COLOR, RENDERING, OVERRIDES), + PANGO2_ATTR_UNDERLINE = PANGO2_ATTR_TYPE (INT, RENDERING, OVERRIDES), + PANGO2_ATTR_UNDERLINE_POSITION = PANGO2_ATTR_TYPE (INT, RENDERING, OVERRIDES), + PANGO2_ATTR_STRIKETHROUGH = PANGO2_ATTR_TYPE (INT, RENDERING, OVERRIDES), + PANGO2_ATTR_RISE = PANGO2_ATTR_TYPE (INT, ITEMIZATION, OVERRIDES), + PANGO2_ATTR_SCALE = PANGO2_ATTR_TYPE (FLOAT, ITEMIZATION, OVERRIDES), + PANGO2_ATTR_FALLBACK = PANGO2_ATTR_TYPE (BOOLEAN, ITEMIZATION, OVERRIDES), + PANGO2_ATTR_LETTER_SPACING = PANGO2_ATTR_TYPE (INT, ITEMIZATION, OVERRIDES), + PANGO2_ATTR_UNDERLINE_COLOR = PANGO2_ATTR_TYPE (COLOR, RENDERING, OVERRIDES), + PANGO2_ATTR_STRIKETHROUGH_COLOR = PANGO2_ATTR_TYPE (COLOR, RENDERING, OVERRIDES), + PANGO2_ATTR_ABSOLUTE_SIZE = PANGO2_ATTR_TYPE (INT, ITEMIZATION, OVERRIDES), + PANGO2_ATTR_GRAVITY = PANGO2_ATTR_TYPE (INT, ITEMIZATION, OVERRIDES), + PANGO2_ATTR_GRAVITY_HINT = PANGO2_ATTR_TYPE (INT, ITEMIZATION, OVERRIDES), + PANGO2_ATTR_FONT_FEATURES = PANGO2_ATTR_TYPE (STRING, SHAPING, ACCUMULATES), + PANGO2_ATTR_ALLOW_BREAKS = PANGO2_ATTR_TYPE (BOOLEAN, BREAKING, OVERRIDES), + PANGO2_ATTR_SHOW = PANGO2_ATTR_TYPE (INT, SHAPING, OVERRIDES), + PANGO2_ATTR_INSERT_HYPHENS = PANGO2_ATTR_TYPE (BOOLEAN, SHAPING, OVERRIDES), + PANGO2_ATTR_OVERLINE = PANGO2_ATTR_TYPE (INT, RENDERING, OVERRIDES), + PANGO2_ATTR_OVERLINE_COLOR = PANGO2_ATTR_TYPE (COLOR, RENDERING, OVERRIDES), + PANGO2_ATTR_LINE_HEIGHT = PANGO2_ATTR_TYPE (FLOAT, ITEMIZATION, OVERRIDES), + PANGO2_ATTR_ABSOLUTE_LINE_HEIGHT = PANGO2_ATTR_TYPE (INT, ITEMIZATION, OVERRIDES), + PANGO2_ATTR_TEXT_TRANSFORM = PANGO2_ATTR_TYPE (INT, ITEMIZATION, OVERRIDES), + PANGO2_ATTR_WORD = PANGO2_ATTR_TYPE (BOOLEAN, BREAKING, OVERRIDES), + PANGO2_ATTR_SENTENCE = PANGO2_ATTR_TYPE (BOOLEAN, BREAKING, OVERRIDES), + PANGO2_ATTR_PARAGRAPH = PANGO2_ATTR_TYPE (BOOLEAN, BREAKING, OVERRIDES), + PANGO2_ATTR_BASELINE_SHIFT = PANGO2_ATTR_TYPE (INT, ITEMIZATION, ACCUMULATES), + PANGO2_ATTR_FONT_SCALE = PANGO2_ATTR_TYPE (INT, ITEMIZATION, ACCUMULATES), + PANGO2_ATTR_LINE_SPACING = PANGO2_ATTR_TYPE (INT, ITEMIZATION, OVERRIDES), + PANGO2_ATTR_SHAPE = PANGO2_ATTR_TYPE (POINTER, ITEMIZATION, OVERRIDES), +} Pango2AttrType; + +#undef PANGO2_ATTR_TYPE + +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_language_new (Pango2Language *language); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_family_new (const char *family); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_foreground_new (Pango2Color *color); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_background_new (Pango2Color *color); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_size_new (int size); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_size_new_absolute (int size); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_style_new (Pango2Style style); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_weight_new (Pango2Weight weight); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_variant_new (Pango2Variant variant); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_stretch_new (Pango2Stretch stretch); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_font_desc_new (const Pango2FontDescription *desc); + +/** + * Pango2LineStyle: + * @PANGO2_LINE_STYLE_NONE: No visible line + * @PANGO2_LINE_STYLE_SOLID: A single line + * @PANGO2_LINE_STYLE_DOUBLE: A double line + * @PANGO2_LINE_STYLE_DASHED: A dashed line + * @PANGO2_LINE_STYLE_DOTTED: A dotted line + * @PANGO2_LINE_STYLE_WAVY: A wavy line + * + * `Pango2LineStyle specifies how lines should be drawn. + */ +typedef enum { + PANGO2_LINE_STYLE_NONE, + PANGO2_LINE_STYLE_SOLID, + PANGO2_LINE_STYLE_DOUBLE, + PANGO2_LINE_STYLE_DASHED, + PANGO2_LINE_STYLE_DOTTED, + PANGO2_LINE_STYLE_WAVY, +} Pango2LineStyle; + +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_underline_new (Pango2LineStyle style); + +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_underline_color_new (Pango2Color *color); + +/** + * Pango2UnderlinePosition: + * @PANGO2_UNDERLINE_POSITION_NORMAL: As specified by font metrics + * @PANGO2_UNDERLINE_POSITION_UNDER: Below the ink extents of the run + * + * `Pango2UnderlinePosition` specifies where underlines should be drawn. + */ +typedef enum { + PANGO2_UNDERLINE_POSITION_NORMAL, + PANGO2_UNDERLINE_POSITION_UNDER +} Pango2UnderlinePosition; + +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_underline_position_new (Pango2UnderlinePosition position); + +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_strikethrough_new (Pango2LineStyle style); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_strikethrough_color_new (Pango2Color *color); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_rise_new (int rise); + +/** + * Pango2BaselineShift: + * @PANGO2_BASELINE_SHIFT_NONE: Leave the baseline unchanged + * @PANGO2_BASELINE_SHIFT_SUPERSCRIPT: Shift the baseline to the superscript position, + * relative to the previous run + * @PANGO2_BASELINE_SHIFT_SUBSCRIPT: Shift the baseline to the subscript position, + * relative to the previous run + * + * `Pango2BaselineShift` influences how baselines are changed between runs. + */ +typedef enum { + PANGO2_BASELINE_SHIFT_NONE, + PANGO2_BASELINE_SHIFT_SUPERSCRIPT, + PANGO2_BASELINE_SHIFT_SUBSCRIPT, +} Pango2BaselineShift; + +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_baseline_shift_new (int shift); + +/** + * Pango2FontScale: + * @PANGO2_FONT_SCALE_NONE: Leave the font size unchanged + * @PANGO2_FONT_SCALE_SUPERSCRIPT: Change the font to a size suitable for superscripts + * @PANGO2_FONT_SCALE_SUBSCRIPT: Change the font to a size suitable for subscripts + * @PANGO2_FONT_SCALE_SMALL_CAPS: Change the font to a size suitable for Small Caps + * + * `Pango2FontScale` influences the font size of a run. + */ +typedef enum { + PANGO2_FONT_SCALE_NONE, + PANGO2_FONT_SCALE_SUPERSCRIPT, + PANGO2_FONT_SCALE_SUBSCRIPT, + PANGO2_FONT_SCALE_SMALL_CAPS, +} Pango2FontScale; + +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_font_scale_new (Pango2FontScale scale); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_scale_new (double scale_factor); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_fallback_new (gboolean enable_fallback); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_letter_spacing_new (int letter_spacing); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_gravity_new (Pango2Gravity gravity); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_gravity_hint_new (Pango2GravityHint hint); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_font_features_new (const char *features); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_allow_breaks_new (gboolean allow_breaks); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_word_new (void); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_sentence_new (void); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_paragraph_new (void); + +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_insert_hyphens_new (gboolean insert_hyphens); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_overline_new (Pango2LineStyle style); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_overline_color_new (Pango2Color *color); + +/** + * Pango2ShowFlags: + * @PANGO2_SHOW_NONE: No special treatment for invisible characters + * @PANGO2_SHOW_SPACES: Render spaces, tabs and newlines visibly + * @PANGO2_SHOW_LINE_BREAKS: Render line breaks visibly + * @PANGO2_SHOW_IGNORABLES: Render default-ignorable Unicode + * characters visibly + * + * `Pango2ShowFlags` affect how Pango2 treats characters that are normally + * not visible in the output. + */ +typedef enum { + PANGO2_SHOW_NONE = 0, + PANGO2_SHOW_SPACES = 1 << 0, + PANGO2_SHOW_LINE_BREAKS = 1 << 1, + PANGO2_SHOW_IGNORABLES = 1 << 2 +} Pango2ShowFlags; + +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_show_new (Pango2ShowFlags flags); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_line_height_new (double factor); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_line_height_new_absolute (int height); +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_line_spacing_new (int spacing); + +/** + * Pango2TextTransform: + * @PANGO2_TEXT_TRANSFORM_NONE: Leave text unchanged + * @PANGO2_TEXT_TRANSFORM_LOWERCASE: Display letters and numbers as lowercase + * @PANGO2_TEXT_TRANSFORM_UPPERCASE: Display letters and numbers as uppercase + * @PANGO2_TEXT_TRANSFORM_CAPITALIZE: Display the first character of a word + * in titlecase + * + * `Pango2TextTransform` determines if Pango2 changes the case of characters + * during shaping. + */ +typedef enum { + PANGO2_TEXT_TRANSFORM_NONE, + PANGO2_TEXT_TRANSFORM_LOWERCASE, + PANGO2_TEXT_TRANSFORM_UPPERCASE, + PANGO2_TEXT_TRANSFORM_CAPITALIZE, +} Pango2TextTransform; + +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_text_transform_new (Pango2TextTransform transform); + +PANGO2_AVAILABLE_IN_ALL +Pango2Attribute * pango2_attr_shape_new (Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect, + gpointer data, + Pango2AttrDataCopyFunc copy, + GDestroyNotify destroy); + +G_END_DECLS diff --git a/pango2/pango-bidi-private.h b/pango2/pango-bidi-private.h new file mode 100644 index 00000000..3a0b1ab5 --- /dev/null +++ b/pango2/pango-bidi-private.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2000 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdio.h> +#include <glib.h> +#include "pango-font.h" +#include "pango-direction.h" + +guint8 * pango2_log2vis_get_embedding_levels (const char *text, + int length, + Pango2Direction *pbase_dir); + +Pango2Direction pango2_find_base_dir (const char *text, + int length); diff --git a/pango2/pango-bidi.c b/pango2/pango-bidi.c new file mode 100644 index 00000000..1bf1a436 --- /dev/null +++ b/pango2/pango-bidi.c @@ -0,0 +1,223 @@ +/* Pango2 + * pango-bidi-type.c: Bidirectional Character Types + * + * Copyright (C) 2008 Jürg Billeter <j@bitron.ch> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include <string.h> + +#include <fribidi.h> + +#include "pango-bidi-private.h" +#include "pango-utils.h" + +/* Some bidi-related functions */ + +/*< private > + * pango2_log2vis_get_embedding_levels: + * @text: the text to itemize. + * @length: the number of bytes (not characters) to process, or -1 + * if @text is nul-terminated and the length should be calculated. + * @pbase_dir: input base direction, and output resolved direction. + * + * Return the bidirectional embedding levels of the input paragraph. + * + * The bidirectional embedding levels are defined by the [Unicode Bidirectional + * Algorithm](http://www.unicode.org/reports/tr9/). + * + * If the input base direction is a weak direction, the direction of the + * characters in the text will determine the final resolved direction. + * + * Return value: a newly allocated array of embedding levels, one item per + * character (not byte), that should be freed using [func@GLib.free]. + */ +guint8 * +pango2_log2vis_get_embedding_levels (const char *text, + int length, + Pango2Direction *pbase_dir) +{ + glong n_chars, i; + guint8 *embedding_levels_list; + const char *p; + FriBidiParType fribidi_base_dir; + FriBidiCharType *bidi_types; + FriBidiBracketType *bracket_types; + FriBidiLevel max_level; + FriBidiCharType ored_types = 0; + FriBidiCharType anded_strongs = FRIBIDI_TYPE_RLE; + + G_STATIC_ASSERT (sizeof (FriBidiLevel) == sizeof (guint8)); + G_STATIC_ASSERT (sizeof (FriBidiChar) == sizeof (gunichar)); + + switch (*pbase_dir) + { + case PANGO2_DIRECTION_LTR: + fribidi_base_dir = FRIBIDI_PAR_LTR; + break; + case PANGO2_DIRECTION_RTL: + fribidi_base_dir = FRIBIDI_PAR_RTL; + break; + case PANGO2_DIRECTION_WEAK_RTL: + fribidi_base_dir = FRIBIDI_PAR_WRTL; + break; + case PANGO2_DIRECTION_WEAK_LTR: + case PANGO2_DIRECTION_NEUTRAL: + default: + fribidi_base_dir = FRIBIDI_PAR_WLTR; + break; + } + + if (length < 0) + length = strlen (text); + + n_chars = g_utf8_strlen (text, length); + + bidi_types = g_new (FriBidiCharType, n_chars); + bracket_types = g_new (FriBidiBracketType, n_chars); + embedding_levels_list = g_new (guint8, n_chars); + + for (i = 0, p = text; p < text + length; p = g_utf8_next_char(p), i++) + { + gunichar ch = g_utf8_get_char (p); + FriBidiCharType char_type = fribidi_get_bidi_type (ch); + + if (i == n_chars) + break; + + bidi_types[i] = char_type; + ored_types |= char_type; + if (FRIBIDI_IS_STRONG (char_type)) + anded_strongs &= char_type; + if (G_UNLIKELY(bidi_types[i] == FRIBIDI_TYPE_ON)) + bracket_types[i] = fribidi_get_bracket (ch); + else + bracket_types[i] = FRIBIDI_NO_BRACKET; + } + + /* Short-circuit (malloc-expensive) FriBidi call for unidirectional + * text. + * + * For details see: + * https://bugzilla.gnome.org/show_bug.cgi?id=590183 + */ + + /* The case that all resolved levels will be ltr. + * No isolates, all strongs be LTR, there should be no Arabic numbers + * (or letters for that matter), and one of the following: + * + * o base_dir doesn't have an RTL taste. + * o there are letters, and base_dir is weak. + */ + if (!FRIBIDI_IS_ISOLATE (ored_types) && + !FRIBIDI_IS_RTL (ored_types) && + !FRIBIDI_IS_ARABIC (ored_types) && + (!FRIBIDI_IS_RTL (fribidi_base_dir) || + (FRIBIDI_IS_WEAK (fribidi_base_dir) && + FRIBIDI_IS_LETTER (ored_types)) + )) + { + /* all LTR */ + fribidi_base_dir = FRIBIDI_PAR_LTR; + memset (embedding_levels_list, 0, n_chars); + goto resolved; + } + /* The case that all resolved levels will be RTL is much more complex. + * No isolates, no numbers, all strongs are RTL, and one of + * the following: + * + * o base_dir has an RTL taste (may be weak). + * o there are letters, and base_dir is weak. + */ + else if (!FRIBIDI_IS_ISOLATE (ored_types) && + !FRIBIDI_IS_NUMBER (ored_types) && + FRIBIDI_IS_RTL (anded_strongs) && + (FRIBIDI_IS_RTL (fribidi_base_dir) || + (FRIBIDI_IS_WEAK (fribidi_base_dir) && + FRIBIDI_IS_LETTER (ored_types)) + )) + { + /* all RTL */ + fribidi_base_dir = FRIBIDI_PAR_RTL; + memset (embedding_levels_list, 1, n_chars); + goto resolved; + } + + + max_level = fribidi_get_par_embedding_levels_ex (bidi_types, bracket_types, n_chars, + &fribidi_base_dir, + (FriBidiLevel*)embedding_levels_list); + + if (G_UNLIKELY(max_level == 0)) + { + /* fribidi_get_par_embedding_levels() failed. */ + memset (embedding_levels_list, 0, length); + } + +resolved: + g_free (bidi_types); + g_free (bracket_types); + + *pbase_dir = (fribidi_base_dir == FRIBIDI_PAR_LTR) ? PANGO2_DIRECTION_LTR : PANGO2_DIRECTION_RTL; + + return embedding_levels_list; +} + +static Pango2Direction +pango2_unichar_direction (gunichar ch) +{ + FriBidiCharType fribidi_ch_type; + + G_STATIC_ASSERT (sizeof (FriBidiChar) == sizeof (gunichar)); + + fribidi_ch_type = fribidi_get_bidi_type (ch); + + if (!FRIBIDI_IS_STRONG (fribidi_ch_type)) + return PANGO2_DIRECTION_NEUTRAL; + else if (FRIBIDI_IS_RTL (fribidi_ch_type)) + return PANGO2_DIRECTION_RTL; + else + return PANGO2_DIRECTION_LTR; +} + +Pango2Direction +pango2_find_base_dir (const char *text, + int length) +{ + Pango2Direction dir = PANGO2_DIRECTION_NEUTRAL; + const char *p; + + g_return_val_if_fail (text != NULL || length == 0, PANGO2_DIRECTION_NEUTRAL); + + p = text; + while ((length < 0 || p < text + length) && *p) + { + gunichar wc = g_utf8_get_char (p); + + + dir = pango2_unichar_direction (wc); + + if (dir != PANGO2_DIRECTION_NEUTRAL) + break; + + p = g_utf8_next_char (p); + } + + return dir; +} diff --git a/pango2/pango-break-table.h b/pango2/pango-break-table.h new file mode 100644 index 00000000..0ee37cdf --- /dev/null +++ b/pango2/pango-break-table.h @@ -0,0 +1,864 @@ +/* == Start of generated table == */ +/* + * The following tables are generated by running: + * + * ./gen-break-table.py SentenceBreakProperty.txt IndicSyllabicCategory.txt EastAsianWidth.txt | indent + * + * on files with these headers: + * + * # SentenceBreakProperty-14.0.0.txt + * # Date: 2021-08-12, 23:13:21 GMT + * # © 2021 Unicode®, Inc. + * # Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. + * # For terms of use, see http://www.unicode.org/terms_of_use.html + * # + * # Unicode Character Database + * # For documentation, see http://www.unicode.org/reports/tr44/ + * # IndicSyllabicCategory-14.0.0.txt + * # Date: 2021-05-22, 01:01:00 GMT [KW, RP] + * # © 2021 Unicode®, Inc. + * # Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. + * # For terms of use, see http://www.unicode.org/terms_of_use.html + * # + * # For documentation, see UAX #44: Unicode Character Database, + * # at http://www.unicode.org/reports/tr44/ + * # + * # This file defines the following property: + * # + * # Indic_Syllabic_Category enumerated property + * # + * # Scope: This property is aimed at two general problem + * # areas involving the analysis and processing of Indic scripts: + * # + * # 1. Specification of syllabic structure. + * # 2. Specification of segmentation rules. + * # + * # Both of these problem areas may benefit from having defined subtypes + * # of Indic script characters which are relevant to how Indic + * # syllables (or aksaras) are constructed. Note that rules for + * # syllabic structure in Indic scripts may differ significantly + * # from how phonological syllables are defined. + * # + * # Format: + * # Field 0 Unicode code point value or range of code point values + * # Field 1 Indic_Syllabic_Category property value + * # + * # Field 1 is followed by a comment field, starting with the number sign '#', + * # which shows the General_Category property value, the Unicode character name + * # or names, and, in lines with ranges of code points, the code point count in + * # square brackets. + * # + * # The scripts assessed as Indic in the structural sense used for the + * # Indic_Syllabic_Category are the following: + * # + * # Ahom, Balinese, Batak, Bengali, Bhaiksuki, Brahmi, Buginese, Buhid, + * # Chakma, Cham, Devanagari, Dives Akuru, Dogra, Grantha, Gujarati, + * # Gunjala Gondi, Gurmukhi, Hanunoo, Javanese, Kaithi, Kannada, + * # Kayah Li, Kharoshthi, Khmer, Khojki, Khudawadi, Lao, Lepcha, Limbu, + * # Mahajani, Makasar, Malayalam, Marchen, Masaram Gondi, Meetei Mayek, + * # Modi, Multani, Myanmar, Nandinagari, Newa, New Tai Lue, Oriya, + * # Phags-pa, Rejang, Saurashtra, Sharada, Siddham, Sinhala, Soyombo, + * # Sundanese, Syloti Nagri, Tagalog, Tagbanwa, Tai Le, Tai Tham, + * # Tai Viet, Takri, Tamil, Telugu, Thai, Tibetan, Tirhuta, and + * # Zanabazar Square. + * # + * # All characters for all other scripts not in that list + * # take the default value for this property, unless they + * # are individually listed in this data file. + * # + * # EastAsianWidth-14.0.0.txt + * # Date: 2021-07-06, 09:58:53 GMT [KW, LI] + * # © 2021 Unicode®, Inc. + * # Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. + * # For terms of use, see https://www.unicode.org/terms_of_use.html + * # + * # Unicode Character Database + * # For documentation, see https://www.unicode.org/reports/tr44/ + * # + * # East_Asian_Width Property + * # + * # This file is a normative contributory data file in the + * # Unicode Character Database. + * # + * # The format is two fields separated by a semicolon. + * # Field 0: Unicode code point value or range of code point values + * # Field 1: East_Asian_Width property, consisting of one of the following values: + * # "A", "F", "H", "N", "Na", "W" + * # - All code points, assigned or unassigned, that are not listed + * # explicitly are given the value "N". + * # - The unassigned code points in the following blocks default to "W": + * # CJK Unified Ideographs Extension A: U+3400..U+4DBF + * # CJK Unified Ideographs: U+4E00..U+9FFF + * # CJK Compatibility Ideographs: U+F900..U+FAFF + * # - All undesignated code points in Planes 2 and 3, whether inside or + * # outside of allocated blocks, default to "W": + * # Plane 2: U+20000..U+2FFFD + * # Plane 3: U+30000..U+3FFFD + * # + * # Character ranges are specified as for other property files in the + * # Unicode Character Database. + * # + * # For legacy reasons, there are no spaces before or after the semicolon + * # which separates the two fields. The comments following the number sign + * # "#" list the General_Category property value or the L& alias of the + * # derived value LC, the Unicode character name or names, and, in lines + * # with ranges of code points, the code point count in square brackets. + * # + * # For more information, see UAX #11: East Asian Width, + * # at https://www.unicode.org/reports/tr11/ + * # + * # @missing: 0000..10FFFF; N + */ + +#ifndef PANGO2_BREAK_TABLE_H +#define PANGO2_BREAK_TABLE_H + +#include <glib.h> + + +static inline gboolean +_pango2_is_STerm (gunichar wc) +{ + if ((wc >= 0x0021 && wc <= 0x1945)) + { + if (wc == 0x0021 || + wc == 0x003F || wc == 0x0589 || (wc >= 0x061D && wc <= 0x061F)) + return TRUE; + if ((wc >= 0x06D4 && wc <= 0x0839)) + { + if (wc == 0x06D4) + return TRUE; + if ((wc >= 0x0700 && wc <= 0x0702)) + return TRUE; + if (wc == 0x07F9) + return TRUE; + if (wc == 0x0837 || wc == 0x0839) + return TRUE; + return FALSE; + } + if ((wc >= 0x083D && wc <= 0x083E) || + (wc >= 0x0964 && wc <= 0x0965) || + (wc >= 0x104A && wc <= 0x104B) || wc == 0x1362) + return TRUE; + if ((wc >= 0x1367 && wc <= 0x1945)) + { + if ((wc >= 0x1367 && wc <= 0x1368)) + return TRUE; + if (wc == 0x166E || (wc >= 0x1735 && wc <= 0x1736)) + return TRUE; + if (wc == 0x1803) + return TRUE; + if (wc == 0x1809 || (wc >= 0x1944 && wc <= 0x1945)) + return TRUE; + return FALSE; + } + return FALSE; + } + if ((wc >= 0x1AA8 && wc <= 0xA92F)) + { + if ((wc >= 0x1AA8 && wc <= 0x1AAB) || + (wc >= 0x1B5A && wc <= 0x1B5B) || + (wc >= 0x1B5E && wc <= 0x1B5F) || (wc >= 0x1B7D && wc <= 0x1B7E)) + return TRUE; + if ((wc >= 0x1C3B && wc <= 0x2E2E)) + { + if ((wc >= 0x1C3B && wc <= 0x1C3C)) + return TRUE; + if ((wc >= 0x1C7E && wc <= 0x1C7F)) + return TRUE; + if ((wc >= 0x203C && wc <= 0x203D)) + return TRUE; + if ((wc >= 0x2047 && wc <= 0x2049) || wc == 0x2E2E) + return TRUE; + return FALSE; + } + if (wc == 0x2E3C || + (wc >= 0x2E53 && wc <= 0x2E54) || wc == 0x3002 || wc == 0xA4FF) + return TRUE; + if ((wc >= 0xA60E && wc <= 0xA92F)) + { + if ((wc >= 0xA60E && wc <= 0xA60F)) + return TRUE; + if (wc == 0xA6F3 || wc == 0xA6F7) + return TRUE; + if ((wc >= 0xA876 && wc <= 0xA877)) + return TRUE; + if ((wc >= 0xA8CE && wc <= 0xA8CF) || wc == 0xA92F) + return TRUE; + return FALSE; + } + return FALSE; + } + if ((wc >= 0xA9C8 && wc <= 0x1123C)) + { + if ((wc >= 0xA9C8 && wc <= 0xA9C9) || + (wc >= 0xAA5D && wc <= 0xAA5F) || + (wc >= 0xAAF0 && wc <= 0xAAF1) || wc == 0xABEB) + return TRUE; + if ((wc >= 0xFE56 && wc <= 0x10A57)) + { + if ((wc >= 0xFE56 && wc <= 0xFE57)) + return TRUE; + if (wc == 0xFF01) + return TRUE; + if (wc == 0xFF1F) + return TRUE; + if (wc == 0xFF61 || (wc >= 0x10A56 && wc <= 0x10A57)) + return TRUE; + return FALSE; + } + if ((wc >= 0x10F55 && wc <= 0x10F59) || + (wc >= 0x10F86 && wc <= 0x10F89) || + (wc >= 0x11047 && wc <= 0x11048) || + (wc >= 0x110BE && wc <= 0x110C1)) + return TRUE; + if ((wc >= 0x11141 && wc <= 0x1123C)) + { + if ((wc >= 0x11141 && wc <= 0x11143)) + return TRUE; + if ((wc >= 0x111C5 && wc <= 0x111C6) || wc == 0x111CD) + return TRUE; + if ((wc >= 0x111DE && wc <= 0x111DF)) + return TRUE; + if ((wc >= 0x11238 && wc <= 0x11239) || + (wc >= 0x1123B && wc <= 0x1123C)) + return TRUE; + return FALSE; + } + return FALSE; + } + if ((wc >= 0x112A9 && wc <= 0x1DA88)) + { + if (wc == 0x112A9 || + (wc >= 0x1144B && wc <= 0x1144C) || + (wc >= 0x115C2 && wc <= 0x115C3) || + (wc >= 0x115C9 && wc <= 0x115D7)) + return TRUE; + if ((wc >= 0x11641 && wc <= 0x11A43)) + { + if ((wc >= 0x11641 && wc <= 0x11642)) + return TRUE; + if ((wc >= 0x1173C && wc <= 0x1173E)) + return TRUE; + if (wc == 0x11944) + return TRUE; + if (wc == 0x11946 || (wc >= 0x11A42 && wc <= 0x11A43)) + return TRUE; + return FALSE; + } + if ((wc >= 0x11A9B && wc <= 0x11A9C) || + (wc >= 0x11C41 && wc <= 0x11C42) || + (wc >= 0x11EF7 && wc <= 0x11EF8) || + (wc >= 0x16A6E && wc <= 0x16A6F)) + return TRUE; + if ((wc >= 0x16AF5 && wc <= 0x1DA88)) + { + if (wc == 0x16AF5) + return TRUE; + if ((wc >= 0x16B37 && wc <= 0x16B38) || wc == 0x16B44) + return TRUE; + if (wc == 0x16E98) + return TRUE; + if (wc == 0x1BC9F || wc == 0x1DA88) + return TRUE; + return FALSE; + } + return FALSE; + } + return FALSE; +} + +static inline gboolean +_pango2_is_Virama (gunichar wc) +{ + if ((wc >= 0x094D && wc <= 0x0BCD)) + { + if (wc == 0x094D) + return TRUE; + if (wc == 0x09CD || wc == 0x0A4D) + return TRUE; + if (wc == 0x0ACD) + return TRUE; + if (wc == 0x0B4D || wc == 0x0BCD) + return TRUE; + return FALSE; + } + if ((wc >= 0x0C4D && wc <= 0xA8C4)) + { + if (wc == 0x0C4D) + return TRUE; + if (wc == 0x0CCD || wc == 0x0D4D) + return TRUE; + if (wc == 0x0DCA) + return TRUE; + if (wc == 0x1B44 || wc == 0xA806 || wc == 0xA8C4) + return TRUE; + return FALSE; + } + if ((wc >= 0xA9C0 && wc <= 0x1134D)) + { + if (wc == 0xA9C0) + return TRUE; + if (wc == 0x11046 || wc == 0x110B9) + return TRUE; + if (wc == 0x111C0) + return TRUE; + if (wc == 0x11235 || wc == 0x1134D) + return TRUE; + return FALSE; + } + if ((wc >= 0x11442 && wc <= 0x11C3F)) + { + if (wc == 0x11442 || wc == 0x114C2) + return TRUE; + if (wc == 0x115BF || wc == 0x1163F) + return TRUE; + if (wc == 0x116B6 || wc == 0x11839) + return TRUE; + if (wc == 0x119E0 || wc == 0x11C3F) + return TRUE; + return FALSE; + } + return FALSE; +} + +static inline gboolean +_pango2_is_Vowel_Dependent (gunichar wc) +{ + if ((wc >= 0x093A && wc <= 0x0CC8)) + { + if ((wc >= 0x093A && wc <= 0x09CC)) + { + if ((wc >= 0x093A && wc <= 0x093B) || + (wc >= 0x093E && wc <= 0x094C)) + return TRUE; + if ((wc >= 0x094E && wc <= 0x094F) || + (wc >= 0x0955 && wc <= 0x0957)) + return TRUE; + if ((wc >= 0x0962 && wc <= 0x0963) || + (wc >= 0x09BE && wc <= 0x09C4)) + return TRUE; + if ((wc >= 0x09C7 && wc <= 0x09C8) || + (wc >= 0x09CB && wc <= 0x09CC)) + return TRUE; + return FALSE; + } + if ((wc >= 0x09D7 && wc <= 0x0ACC)) + { + if (wc == 0x09D7 || (wc >= 0x09E2 && wc <= 0x09E3)) + return TRUE; + if ((wc >= 0x0A3E && wc <= 0x0A42) || + (wc >= 0x0A47 && wc <= 0x0A48)) + return TRUE; + if ((wc >= 0x0A4B && wc <= 0x0A4C) || + (wc >= 0x0ABE && wc <= 0x0AC5)) + return TRUE; + if ((wc >= 0x0AC7 && wc <= 0x0AC9) || + (wc >= 0x0ACB && wc <= 0x0ACC)) + return TRUE; + return FALSE; + } + if ((wc >= 0x0AE2 && wc <= 0x0BC8)) + { + if ((wc >= 0x0AE2 && wc <= 0x0AE3) || + (wc >= 0x0B3E && wc <= 0x0B44)) + return TRUE; + if ((wc >= 0x0B47 && wc <= 0x0B48) || + (wc >= 0x0B4B && wc <= 0x0B4C)) + return TRUE; + if ((wc >= 0x0B55 && wc <= 0x0B57) || + (wc >= 0x0B62 && wc <= 0x0B63)) + return TRUE; + if ((wc >= 0x0BBE && wc <= 0x0BC2) || + (wc >= 0x0BC6 && wc <= 0x0BC8)) + return TRUE; + return FALSE; + } + if ((wc >= 0x0BCA && wc <= 0x0CC8)) + { + if ((wc >= 0x0BCA && wc <= 0x0BCC) || wc == 0x0BD7) + return TRUE; + if ((wc >= 0x0C3E && wc <= 0x0C44) || + (wc >= 0x0C46 && wc <= 0x0C48)) + return TRUE; + if ((wc >= 0x0C4A && wc <= 0x0C4C) || + (wc >= 0x0C55 && wc <= 0x0C56)) + return TRUE; + if ((wc >= 0x0C62 && wc <= 0x0C63) || + (wc >= 0x0CBE && wc <= 0x0CC4) || + (wc >= 0x0CC6 && wc <= 0x0CC8)) + return TRUE; + return FALSE; + } + return FALSE; + } + if ((wc >= 0x0CCA && wc <= 0x1928)) + { + if ((wc >= 0x0CCA && wc <= 0x0D63)) + { + if ((wc >= 0x0CCA && wc <= 0x0CCC) || + (wc >= 0x0CD5 && wc <= 0x0CD6)) + return TRUE; + if ((wc >= 0x0CE2 && wc <= 0x0CE3) || + (wc >= 0x0D3E && wc <= 0x0D44)) + return TRUE; + if ((wc >= 0x0D46 && wc <= 0x0D48) || + (wc >= 0x0D4A && wc <= 0x0D4C)) + return TRUE; + if (wc == 0x0D57 || (wc >= 0x0D62 && wc <= 0x0D63)) + return TRUE; + return FALSE; + } + if ((wc >= 0x0DCF && wc <= 0x0EBB)) + { + if ((wc >= 0x0DCF && wc <= 0x0DD4) || wc == 0x0DD6) + return TRUE; + if ((wc >= 0x0DD8 && wc <= 0x0DDF) || + (wc >= 0x0DF2 && wc <= 0x0DF3)) + return TRUE; + if ((wc >= 0x0E30 && wc <= 0x0E39) || + (wc >= 0x0E40 && wc <= 0x0E45)) + return TRUE; + if (wc == 0x0E47 || (wc >= 0x0EB0 && wc <= 0x0EB9) || wc == 0x0EBB) + return TRUE; + return FALSE; + } + if ((wc >= 0x0EC0 && wc <= 0x1074)) + { + if ((wc >= 0x0EC0 && wc <= 0x0EC4) || + (wc >= 0x0F71 && wc <= 0x0F7D)) + return TRUE; + if ((wc >= 0x0F80 && wc <= 0x0F81) || + (wc >= 0x102B && wc <= 0x1035)) + return TRUE; + if ((wc >= 0x1056 && wc <= 0x1059) || wc == 0x1062) + return TRUE; + if ((wc >= 0x1067 && wc <= 0x1068) || + (wc >= 0x1071 && wc <= 0x1074)) + return TRUE; + return FALSE; + } + if ((wc >= 0x1083 && wc <= 0x1928)) + { + if ((wc >= 0x1083 && wc <= 0x1086) || + (wc >= 0x109C && wc <= 0x109D)) + return TRUE; + if ((wc >= 0x1712 && wc <= 0x1713) || + (wc >= 0x1732 && wc <= 0x1733)) + return TRUE; + if ((wc >= 0x1752 && wc <= 0x1753) || + (wc >= 0x1772 && wc <= 0x1773)) + return TRUE; + if ((wc >= 0x17B6 && wc <= 0x17C5) || + wc == 0x17C8 || (wc >= 0x1920 && wc <= 0x1928)) + return TRUE; + return FALSE; + } + return FALSE; + } + if ((wc >= 0x193A && wc <= 0x112E8)) + { + if ((wc >= 0x193A && wc <= 0x1C2C)) + { + if (wc == 0x193A || (wc >= 0x19B0 && wc <= 0x19C0)) + return TRUE; + if ((wc >= 0x1A17 && wc <= 0x1A1B) || + (wc >= 0x1A61 && wc <= 0x1A73)) + return TRUE; + if ((wc >= 0x1B35 && wc <= 0x1B43) || + (wc >= 0x1BA4 && wc <= 0x1BA9)) + return TRUE; + if ((wc >= 0x1BE7 && wc <= 0x1BEF) || + (wc >= 0x1C26 && wc <= 0x1C2C)) + return TRUE; + return FALSE; + } + if ((wc >= 0xA802 && wc <= 0xAA32)) + { + if (wc == 0xA802 || (wc >= 0xA823 && wc <= 0xA827)) + return TRUE; + if ((wc >= 0xA8B5 && wc <= 0xA8C3) || wc == 0xA8FF) + return TRUE; + if ((wc >= 0xA947 && wc <= 0xA94E) || + (wc >= 0xA9B4 && wc <= 0xA9BC)) + return TRUE; + if (wc == 0xA9E5 || (wc >= 0xAA29 && wc <= 0xAA32)) + return TRUE; + return FALSE; + } + if ((wc >= 0xAAB0 && wc <= 0x11074)) + { + if ((wc >= 0xAAB0 && wc <= 0xAABE) || + (wc >= 0xAAEB && wc <= 0xAAEF)) + return TRUE; + if ((wc >= 0xABE3 && wc <= 0xABEA) || + (wc >= 0x10A01 && wc <= 0x10A03)) + return TRUE; + if ((wc >= 0x10A05 && wc <= 0x10A06) || + (wc >= 0x10A0C && wc <= 0x10A0D)) + return TRUE; + if ((wc >= 0x11038 && wc <= 0x11045) || + (wc >= 0x11073 && wc <= 0x11074)) + return TRUE; + return FALSE; + } + if ((wc >= 0x110B0 && wc <= 0x112E8)) + { + if ((wc >= 0x110B0 && wc <= 0x110B8) || wc == 0x110C2) + return TRUE; + if ((wc >= 0x11127 && wc <= 0x11132) || + (wc >= 0x11145 && wc <= 0x11146)) + return TRUE; + if ((wc >= 0x111B3 && wc <= 0x111BF) || + (wc >= 0x111CB && wc <= 0x111CC)) + return TRUE; + if (wc == 0x111CE || + (wc >= 0x1122C && wc <= 0x11233) || + (wc >= 0x112E0 && wc <= 0x112E8)) + return TRUE; + return FALSE; + } + return FALSE; + } + if ((wc >= 0x1133E && wc <= 0x11EF6)) + { + if ((wc >= 0x1133E && wc <= 0x115B5)) + { + if ((wc >= 0x1133E && wc <= 0x11344) || + (wc >= 0x11347 && wc <= 0x11348)) + return TRUE; + if ((wc >= 0x1134B && wc <= 0x1134C) || wc == 0x11357) + return TRUE; + if ((wc >= 0x11362 && wc <= 0x11363) || + (wc >= 0x11435 && wc <= 0x11441)) + return TRUE; + if ((wc >= 0x114B0 && wc <= 0x114BE) || + (wc >= 0x115AF && wc <= 0x115B5)) + return TRUE; + return FALSE; + } + if ((wc >= 0x115B8 && wc <= 0x11938)) + { + if ((wc >= 0x115B8 && wc <= 0x115BB) || + (wc >= 0x115DC && wc <= 0x115DD)) + return TRUE; + if ((wc >= 0x11630 && wc <= 0x1163C) || wc == 0x11640) + return TRUE; + if ((wc >= 0x116AD && wc <= 0x116B5) || + (wc >= 0x11720 && wc <= 0x1172A)) + return TRUE; + if ((wc >= 0x1182C && wc <= 0x11836) || + (wc >= 0x11930 && wc <= 0x11935) || + (wc >= 0x11937 && wc <= 0x11938)) + return TRUE; + return FALSE; + } + if ((wc >= 0x119D1 && wc <= 0x11CB4)) + { + if ((wc >= 0x119D1 && wc <= 0x119D7) || + (wc >= 0x119DA && wc <= 0x119DD)) + return TRUE; + if (wc == 0x119E4 || (wc >= 0x11A01 && wc <= 0x11A0A)) + return TRUE; + if ((wc >= 0x11A51 && wc <= 0x11A5B) || + (wc >= 0x11C2F && wc <= 0x11C36)) + return TRUE; + if ((wc >= 0x11C38 && wc <= 0x11C3B) || + (wc >= 0x11CB0 && wc <= 0x11CB4)) + return TRUE; + return FALSE; + } + if ((wc >= 0x11D31 && wc <= 0x11EF6)) + { + if ((wc >= 0x11D31 && wc <= 0x11D36) || wc == 0x11D3A) + return TRUE; + if ((wc >= 0x11D3C && wc <= 0x11D3D) || wc == 0x11D3F) + return TRUE; + if (wc == 0x11D43 || (wc >= 0x11D8A && wc <= 0x11D8E)) + return TRUE; + if ((wc >= 0x11D90 && wc <= 0x11D91) || + (wc >= 0x11D93 && wc <= 0x11D94) || + (wc >= 0x11EF3 && wc <= 0x11EF6)) + return TRUE; + return FALSE; + } + return FALSE; + } + return FALSE; +} + +static inline gboolean +_pango2_is_Consonant_Prefixed (gunichar wc) +{ + if ((wc >= 0x111C2 && wc <= 0x111C3) || + wc == 0x1193F || wc == 0x11A3A || (wc >= 0x11A84 && wc <= 0x11A89)) + return TRUE; + return FALSE; +} + +static inline gboolean +_pango2_is_Consonant_Preceding_Repha (gunichar wc) +{ + if (wc == 0x0D4E || wc == 0x11941 || wc == 0x11D46) + return TRUE; + return FALSE; +} + +static inline gboolean +_pango2_is_EastAsianWide (gunichar wc) +{ + if ((wc >= 0x1100 && wc <= 0x27B0)) + { + if ((wc >= 0x1100 && wc <= 0x25FE)) + { + if ((wc >= 0x1100 && wc <= 0x115F) || wc == 0x20A9) + return TRUE; + if ((wc >= 0x231A && wc <= 0x231B) || + (wc >= 0x2329 && wc <= 0x232A)) + return TRUE; + if ((wc >= 0x23E9 && wc <= 0x23EC) || wc == 0x23F0) + return TRUE; + if (wc == 0x23F3 || (wc >= 0x25FD && wc <= 0x25FE)) + return TRUE; + return FALSE; + } + if ((wc >= 0x2614 && wc <= 0x26C5)) + { + if ((wc >= 0x2614 && wc <= 0x2615) || + (wc >= 0x2648 && wc <= 0x2653)) + return TRUE; + if (wc == 0x267F || wc == 0x2693) + return TRUE; + if (wc == 0x26A1 || (wc >= 0x26AA && wc <= 0x26AB)) + return TRUE; + if ((wc >= 0x26BD && wc <= 0x26BE) || + (wc >= 0x26C4 && wc <= 0x26C5)) + return TRUE; + return FALSE; + } + if ((wc >= 0x26CE && wc <= 0x2705)) + { + if (wc == 0x26CE || wc == 0x26D4) + return TRUE; + if (wc == 0x26EA || (wc >= 0x26F2 && wc <= 0x26F3)) + return TRUE; + if (wc == 0x26F5 || wc == 0x26FA) + return TRUE; + if (wc == 0x26FD || wc == 0x2705) + return TRUE; + return FALSE; + } + if ((wc >= 0x270A && wc <= 0x27B0)) + { + if ((wc >= 0x270A && wc <= 0x270B) || wc == 0x2728) + return TRUE; + if (wc == 0x274C || wc == 0x274E) + return TRUE; + if ((wc >= 0x2753 && wc <= 0x2755) || wc == 0x2757) + return TRUE; + if ((wc >= 0x2795 && wc <= 0x2797) || wc == 0x27B0) + return TRUE; + return FALSE; + } + return FALSE; + } + if ((wc >= 0x27BF && wc <= 0xFFD7)) + { + if ((wc >= 0x27BF && wc <= 0x2FFB)) + { + if (wc == 0x27BF || (wc >= 0x2B1B && wc <= 0x2B1C)) + return TRUE; + if (wc == 0x2B50 || wc == 0x2B55) + return TRUE; + if ((wc >= 0x2E80 && wc <= 0x2E99) || + (wc >= 0x2E9B && wc <= 0x2EF3)) + return TRUE; + if ((wc >= 0x2F00 && wc <= 0x2FD5) || + (wc >= 0x2FF0 && wc <= 0x2FFB)) + return TRUE; + return FALSE; + } + if ((wc >= 0x3000 && wc <= 0x321E)) + { + if (wc == 0x3000 || (wc >= 0x3001 && wc <= 0x303E)) + return TRUE; + if ((wc >= 0x3041 && wc <= 0x3096) || + (wc >= 0x3099 && wc <= 0x30FF)) + return TRUE; + if ((wc >= 0x3105 && wc <= 0x312F) || + (wc >= 0x3131 && wc <= 0x318E)) + return TRUE; + if ((wc >= 0x3190 && wc <= 0x31E3) || + (wc >= 0x31F0 && wc <= 0x321E)) + return TRUE; + return FALSE; + } + if ((wc >= 0x3220 && wc <= 0xFE19)) + { + if ((wc >= 0x3220 && wc <= 0x3247) || + (wc >= 0x3250 && wc <= 0x4DBF)) + return TRUE; + if ((wc >= 0x4E00 && wc <= 0xA48C) || + (wc >= 0xA490 && wc <= 0xA4C6)) + return TRUE; + if ((wc >= 0xA960 && wc <= 0xA97C) || + (wc >= 0xAC00 && wc <= 0xD7A3)) + return TRUE; + if ((wc >= 0xF900 && wc <= 0xFAFF) || + (wc >= 0xFE10 && wc <= 0xFE19)) + return TRUE; + return FALSE; + } + if ((wc >= 0xFE30 && wc <= 0xFFD7)) + { + if ((wc >= 0xFE30 && wc <= 0xFE52) || + (wc >= 0xFE54 && wc <= 0xFE66)) + return TRUE; + if ((wc >= 0xFE68 && wc <= 0xFE6B) || + (wc >= 0xFF01 && wc <= 0xFF60)) + return TRUE; + if ((wc >= 0xFF61 && wc <= 0xFFBE) || + (wc >= 0xFFC2 && wc <= 0xFFC7)) + return TRUE; + if ((wc >= 0xFFCA && wc <= 0xFFCF) || + (wc >= 0xFFD2 && wc <= 0xFFD7)) + return TRUE; + return FALSE; + } + return FALSE; + } + if ((wc >= 0xFFDA && wc <= 0x1F3F4)) + { + if ((wc >= 0xFFDA && wc <= 0x18D08)) + { + if ((wc >= 0xFFDA && wc <= 0xFFDC) || + (wc >= 0xFFE0 && wc <= 0xFFE6)) + return TRUE; + if ((wc >= 0xFFE8 && wc <= 0xFFEE) || + (wc >= 0x16FE0 && wc <= 0x16FE4)) + return TRUE; + if ((wc >= 0x16FF0 && wc <= 0x16FF1) || + (wc >= 0x17000 && wc <= 0x187F7)) + return TRUE; + if ((wc >= 0x18800 && wc <= 0x18CD5) || + (wc >= 0x18D00 && wc <= 0x18D08)) + return TRUE; + return FALSE; + } + if ((wc >= 0x1AFF0 && wc <= 0x1F004)) + { + if ((wc >= 0x1AFF0 && wc <= 0x1AFF3) || + (wc >= 0x1AFF5 && wc <= 0x1AFFB)) + return TRUE; + if ((wc >= 0x1AFFD && wc <= 0x1AFFE) || + (wc >= 0x1B000 && wc <= 0x1B122)) + return TRUE; + if ((wc >= 0x1B150 && wc <= 0x1B152) || + (wc >= 0x1B164 && wc <= 0x1B167)) + return TRUE; + if ((wc >= 0x1B170 && wc <= 0x1B2FB) || wc == 0x1F004) + return TRUE; + return FALSE; + } + if ((wc >= 0x1F0CF && wc <= 0x1F265)) + { + if (wc == 0x1F0CF || wc == 0x1F18E) + return TRUE; + if ((wc >= 0x1F191 && wc <= 0x1F19A) || + (wc >= 0x1F200 && wc <= 0x1F202)) + return TRUE; + if ((wc >= 0x1F210 && wc <= 0x1F23B) || + (wc >= 0x1F240 && wc <= 0x1F248)) + return TRUE; + if ((wc >= 0x1F250 && wc <= 0x1F251) || + (wc >= 0x1F260 && wc <= 0x1F265)) + return TRUE; + return FALSE; + } + if ((wc >= 0x1F300 && wc <= 0x1F3F4)) + { + if ((wc >= 0x1F300 && wc <= 0x1F320) || + (wc >= 0x1F32D && wc <= 0x1F335)) + return TRUE; + if ((wc >= 0x1F337 && wc <= 0x1F37C) || + (wc >= 0x1F37E && wc <= 0x1F393)) + return TRUE; + if ((wc >= 0x1F3A0 && wc <= 0x1F3CA) || + (wc >= 0x1F3CF && wc <= 0x1F3D3)) + return TRUE; + if ((wc >= 0x1F3E0 && wc <= 0x1F3F0) || wc == 0x1F3F4) + return TRUE; + return FALSE; + } + return FALSE; + } + if ((wc >= 0x1F3F8 && wc <= 0x3FFFD)) + { + if ((wc >= 0x1F3F8 && wc <= 0x1F596)) + { + if ((wc >= 0x1F3F8 && wc <= 0x1F43E) || wc == 0x1F440) + return TRUE; + if ((wc >= 0x1F442 && wc <= 0x1F4FC) || + (wc >= 0x1F4FF && wc <= 0x1F53D)) + return TRUE; + if ((wc >= 0x1F54B && wc <= 0x1F54E) || + (wc >= 0x1F550 && wc <= 0x1F567)) + return TRUE; + if (wc == 0x1F57A || (wc >= 0x1F595 && wc <= 0x1F596)) + return TRUE; + return FALSE; + } + if ((wc >= 0x1F5A4 && wc <= 0x1F6EC)) + { + if (wc == 0x1F5A4 || (wc >= 0x1F5FB && wc <= 0x1F64F)) + return TRUE; + if ((wc >= 0x1F680 && wc <= 0x1F6C5) || wc == 0x1F6CC) + return TRUE; + if ((wc >= 0x1F6D0 && wc <= 0x1F6D2) || + (wc >= 0x1F6D5 && wc <= 0x1F6D7)) + return TRUE; + if ((wc >= 0x1F6DD && wc <= 0x1F6DF) || + (wc >= 0x1F6EB && wc <= 0x1F6EC)) + return TRUE; + return FALSE; + } + if ((wc >= 0x1F6F4 && wc <= 0x1FA7C)) + { + if ((wc >= 0x1F6F4 && wc <= 0x1F6FC) || + (wc >= 0x1F7E0 && wc <= 0x1F7EB)) + return TRUE; + if (wc == 0x1F7F0 || (wc >= 0x1F90C && wc <= 0x1F93A)) + return TRUE; + if ((wc >= 0x1F93C && wc <= 0x1F945) || + (wc >= 0x1F947 && wc <= 0x1F9FF)) + return TRUE; + if ((wc >= 0x1FA70 && wc <= 0x1FA74) || + (wc >= 0x1FA78 && wc <= 0x1FA7C)) + return TRUE; + return FALSE; + } + if ((wc >= 0x1FA80 && wc <= 0x3FFFD)) + { + if ((wc >= 0x1FA80 && wc <= 0x1FA86) || + (wc >= 0x1FA90 && wc <= 0x1FAAC)) + return TRUE; + if ((wc >= 0x1FAB0 && wc <= 0x1FABA) || + (wc >= 0x1FAC0 && wc <= 0x1FAC5)) + return TRUE; + if ((wc >= 0x1FAD0 && wc <= 0x1FAD9) || + (wc >= 0x1FAE0 && wc <= 0x1FAE7)) + return TRUE; + if ((wc >= 0x1FAF0 && wc <= 0x1FAF6) || + (wc >= 0x20000 && wc <= 0x2FFFD) || + (wc >= 0x30000 && wc <= 0x3FFFD)) + return TRUE; + return FALSE; + } + return FALSE; + } + return FALSE; +} + +#endif /* PANGO2_BREAK_TABLE_H */ + +/* == End of generated table == */ diff --git a/pango2/pango-break.h b/pango2/pango-break.h new file mode 100644 index 00000000..e3e479e4 --- /dev/null +++ b/pango2/pango-break.h @@ -0,0 +1,131 @@ +/* + * Copyright (C) 1999 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <glib.h> +#include <pango2/pango-item.h> + +G_BEGIN_DECLS + + +/* Logical attributes of a character. + */ +/** + * Pango2LogAttr: + * @is_line_break: if set, can break line in front of character + * @is_mandatory_break: if set, must break line in front of character + * @is_char_break: if set, can break here when doing character wrapping + * @is_white: is whitespace character + * @is_cursor_position: if set, cursor can appear in front of character. + * i.e. this is a grapheme boundary, or the first character in the text. + * This flag implements Unicode's + * [Grapheme Cluster Boundaries](http://www.unicode.org/reports/tr29/) + * semantics. + * @is_word_start: is first character in a word + * @is_word_end: is first non-word char after a word + * Note that in degenerate cases, you could have both @is_word_start + * and @is_word_end set for some character. + * @is_sentence_boundary: is a sentence boundary. + * There are two ways to divide sentences. The first assigns all + * inter-sentence whitespace/control/format chars to some sentence, + * so all chars are in some sentence; @is_sentence_boundary denotes + * the boundaries there. The second way doesn't assign + * between-sentence spaces, etc. to any sentence, so + * @is_sentence_start/@is_sentence_end mark the boundaries of those sentences. + * @is_sentence_start: is first character in a sentence + * @is_sentence_end: is first char after a sentence. + * Note that in degenerate cases, you could have both @is_sentence_start + * and @is_sentence_end set for some character. (e.g. no space after a + * period, so the next sentence starts right away) + * @backspace_deletes_character: if set, backspace deletes one character + * rather than the entire grapheme cluster. This field is only meaningful + * on grapheme boundaries (where @is_cursor_position is set). In some languages, + * the full grapheme (e.g. letter + diacritics) is considered a unit, while in + * others, each decomposed character in the grapheme is a unit. In the default + * implementation of [func@default_break], this bit is set on all grapheme + * boundaries except those following Latin, Cyrillic or Greek base characters. + * @is_expandable_space: is a whitespace character that can possibly be + * expanded for justification purposes. + * @is_word_boundary: is a word boundary, as defined by UAX#29. + * More specifically, means that this is not a position in the middle of a word. + * For example, both sides of a punctuation mark are considered word boundaries. + * This flag is particularly useful when selecting text word-by-word. This flag + * implements Unicode's [Word Boundaries](http://www.unicode.org/reports/tr29/) + * semantics. + * @break_inserts_hyphen: when breaking lines before this char, insert a hyphen. + * @break_removes_preceding: when breaking lines before this char, remove the + * preceding char. + * + * The `Pango2LogAttr` structure stores information about the attributes of a + * single character. + */ +struct _Pango2LogAttr +{ + guint is_line_break : 1; + guint is_mandatory_break : 1; + guint is_char_break : 1; + guint is_white : 1; + guint is_cursor_position : 1; + guint is_word_start : 1; + guint is_word_end : 1; + guint is_sentence_boundary : 1; + guint is_sentence_start : 1; + guint is_sentence_end : 1; + guint backspace_deletes_character : 1; + guint is_expandable_space : 1; + guint is_word_boundary : 1; + guint break_inserts_hyphen : 1; + guint break_removes_preceding : 1; + + guint reserved : 17; +}; + +PANGO2_AVAILABLE_IN_ALL +void pango2_get_log_attrs (const char *text, + int length, + Pango2AttrList *attr_list, + int level, + Pango2Language *language, + Pango2LogAttr *attrs, + int attrs_len); + +PANGO2_AVAILABLE_IN_ALL +void pango2_default_break (const char *text, + int length, + Pango2LogAttr *attrs, + int attrs_len); + +PANGO2_AVAILABLE_IN_ALL +void pango2_tailor_break (const char *text, + int length, + Pango2Analysis *analysis, + int offset, + Pango2LogAttr *attrs, + int attrs_len); + +PANGO2_AVAILABLE_IN_ALL +void pango2_attr_break (const char *text, + int length, + Pango2AttrList *attr_list, + int offset, + Pango2LogAttr *attrs, + int attrs_len); + +G_END_DECLS diff --git a/pango2/pango-color-table.h b/pango2/pango-color-table.h new file mode 100644 index 00000000..a3b95927 --- /dev/null +++ b/pango2/pango-color-table.h @@ -0,0 +1,1349 @@ +/* pango-color-table.h: Generated by gen-color-table.pl from rgb.txt + * + * Date: Thu May 5 15:35:35 2016 + * + * Do not edit. + */ +static const char color_names[] = + "AliceBlue\0" + "AntiqueWhite\0" + "AntiqueWhite1\0" + "AntiqueWhite2\0" + "AntiqueWhite3\0" + "AntiqueWhite4\0" + "aqua\0" + "aquamarine\0" + "aquamarine1\0" + "aquamarine2\0" + "aquamarine3\0" + "aquamarine4\0" + "azure\0" + "azure1\0" + "azure2\0" + "azure3\0" + "azure4\0" + "beige\0" + "bisque\0" + "bisque1\0" + "bisque2\0" + "bisque3\0" + "bisque4\0" + "black\0" + "BlanchedAlmond\0" + "blue\0" + "blue1\0" + "blue2\0" + "blue3\0" + "blue4\0" + "BlueViolet\0" + "brown\0" + "brown1\0" + "brown2\0" + "brown3\0" + "brown4\0" + "burlywood\0" + "burlywood1\0" + "burlywood2\0" + "burlywood3\0" + "burlywood4\0" + "CadetBlue\0" + "CadetBlue1\0" + "CadetBlue2\0" + "CadetBlue3\0" + "CadetBlue4\0" + "chartreuse\0" + "chartreuse1\0" + "chartreuse2\0" + "chartreuse3\0" + "chartreuse4\0" + "chocolate\0" + "chocolate1\0" + "chocolate2\0" + "chocolate3\0" + "chocolate4\0" + "coral\0" + "coral1\0" + "coral2\0" + "coral3\0" + "coral4\0" + "CornflowerBlue\0" + "cornsilk\0" + "cornsilk1\0" + "cornsilk2\0" + "cornsilk3\0" + "cornsilk4\0" + "crimson\0" + "cyan\0" + "cyan1\0" + "cyan2\0" + "cyan3\0" + "cyan4\0" + "DarkBlue\0" + "DarkCyan\0" + "DarkGoldenrod\0" + "DarkGoldenrod1\0" + "DarkGoldenrod2\0" + "DarkGoldenrod3\0" + "DarkGoldenrod4\0" + "DarkGray\0" + "DarkGreen\0" + "DarkGrey\0" + "DarkKhaki\0" + "DarkMagenta\0" + "DarkOliveGreen\0" + "DarkOliveGreen1\0" + "DarkOliveGreen2\0" + "DarkOliveGreen3\0" + "DarkOliveGreen4\0" + "DarkOrange\0" + "DarkOrange1\0" + "DarkOrange2\0" + "DarkOrange3\0" + "DarkOrange4\0" + "DarkOrchid\0" + "DarkOrchid1\0" + "DarkOrchid2\0" + "DarkOrchid3\0" + "DarkOrchid4\0" + "DarkRed\0" + "DarkSalmon\0" + "DarkSeaGreen\0" + "DarkSeaGreen1\0" + "DarkSeaGreen2\0" + "DarkSeaGreen3\0" + "DarkSeaGreen4\0" + "DarkSlateBlue\0" + "DarkSlateGray\0" + "DarkSlateGray1\0" + "DarkSlateGray2\0" + "DarkSlateGray3\0" + "DarkSlateGray4\0" + "DarkSlateGrey\0" + "DarkTurquoise\0" + "DarkViolet\0" + "DeepPink\0" + "DeepPink1\0" + "DeepPink2\0" + "DeepPink3\0" + "DeepPink4\0" + "DeepSkyBlue\0" + "DeepSkyBlue1\0" + "DeepSkyBlue2\0" + "DeepSkyBlue3\0" + "DeepSkyBlue4\0" + "DimGray\0" + "DimGrey\0" + "DodgerBlue\0" + "DodgerBlue1\0" + "DodgerBlue2\0" + "DodgerBlue3\0" + "DodgerBlue4\0" + "firebrick\0" + "firebrick1\0" + "firebrick2\0" + "firebrick3\0" + "firebrick4\0" + "FloralWhite\0" + "ForestGreen\0" + "fuchsia\0" + "gainsboro\0" + "GhostWhite\0" + "gold\0" + "gold1\0" + "gold2\0" + "gold3\0" + "gold4\0" + "goldenrod\0" + "goldenrod1\0" + "goldenrod2\0" + "goldenrod3\0" + "goldenrod4\0" + "gray\0" + "gray0\0" + "gray1\0" + "gray10\0" + "gray100\0" + "gray11\0" + "gray12\0" + "gray13\0" + "gray14\0" + "gray15\0" + "gray16\0" + "gray17\0" + "gray18\0" + "gray19\0" + "gray2\0" + "gray20\0" + "gray21\0" + "gray22\0" + "gray23\0" + "gray24\0" + "gray25\0" + "gray26\0" + "gray27\0" + "gray28\0" + "gray29\0" + "gray3\0" + "gray30\0" + "gray31\0" + "gray32\0" + "gray33\0" + "gray34\0" + "gray35\0" + "gray36\0" + "gray37\0" + "gray38\0" + "gray39\0" + "gray4\0" + "gray40\0" + "gray41\0" + "gray42\0" + "gray43\0" + "gray44\0" + "gray45\0" + "gray46\0" + "gray47\0" + "gray48\0" + "gray49\0" + "gray5\0" + "gray50\0" + "gray51\0" + "gray52\0" + "gray53\0" + "gray54\0" + "gray55\0" + "gray56\0" + "gray57\0" + "gray58\0" + "gray59\0" + "gray6\0" + "gray60\0" + "gray61\0" + "gray62\0" + "gray63\0" + "gray64\0" + "gray65\0" + "gray66\0" + "gray67\0" + "gray68\0" + "gray69\0" + "gray7\0" + "gray70\0" + "gray71\0" + "gray72\0" + "gray73\0" + "gray74\0" + "gray75\0" + "gray76\0" + "gray77\0" + "gray78\0" + "gray79\0" + "gray8\0" + "gray80\0" + "gray81\0" + "gray82\0" + "gray83\0" + "gray84\0" + "gray85\0" + "gray86\0" + "gray87\0" + "gray88\0" + "gray89\0" + "gray9\0" + "gray90\0" + "gray91\0" + "gray92\0" + "gray93\0" + "gray94\0" + "gray95\0" + "gray96\0" + "gray97\0" + "gray98\0" + "gray99\0" + "green\0" + "green1\0" + "green2\0" + "green3\0" + "green4\0" + "GreenYellow\0" + "grey\0" + "grey0\0" + "grey1\0" + "grey10\0" + "grey100\0" + "grey11\0" + "grey12\0" + "grey13\0" + "grey14\0" + "grey15\0" + "grey16\0" + "grey17\0" + "grey18\0" + "grey19\0" + "grey2\0" + "grey20\0" + "grey21\0" + "grey22\0" + "grey23\0" + "grey24\0" + "grey25\0" + "grey26\0" + "grey27\0" + "grey28\0" + "grey29\0" + "grey3\0" + "grey30\0" + "grey31\0" + "grey32\0" + "grey33\0" + "grey34\0" + "grey35\0" + "grey36\0" + "grey37\0" + "grey38\0" + "grey39\0" + "grey4\0" + "grey40\0" + "grey41\0" + "grey42\0" + "grey43\0" + "grey44\0" + "grey45\0" + "grey46\0" + "grey47\0" + "grey48\0" + "grey49\0" + "grey5\0" + "grey50\0" + "grey51\0" + "grey52\0" + "grey53\0" + "grey54\0" + "grey55\0" + "grey56\0" + "grey57\0" + "grey58\0" + "grey59\0" + "grey6\0" + "grey60\0" + "grey61\0" + "grey62\0" + "grey63\0" + "grey64\0" + "grey65\0" + "grey66\0" + "grey67\0" + "grey68\0" + "grey69\0" + "grey7\0" + "grey70\0" + "grey71\0" + "grey72\0" + "grey73\0" + "grey74\0" + "grey75\0" + "grey76\0" + "grey77\0" + "grey78\0" + "grey79\0" + "grey8\0" + "grey80\0" + "grey81\0" + "grey82\0" + "grey83\0" + "grey84\0" + "grey85\0" + "grey86\0" + "grey87\0" + "grey88\0" + "grey89\0" + "grey9\0" + "grey90\0" + "grey91\0" + "grey92\0" + "grey93\0" + "grey94\0" + "grey95\0" + "grey96\0" + "grey97\0" + "grey98\0" + "grey99\0" + "honeydew\0" + "honeydew1\0" + "honeydew2\0" + "honeydew3\0" + "honeydew4\0" + "HotPink\0" + "HotPink1\0" + "HotPink2\0" + "HotPink3\0" + "HotPink4\0" + "IndianRed\0" + "IndianRed1\0" + "IndianRed2\0" + "IndianRed3\0" + "IndianRed4\0" + "indigo\0" + "ivory\0" + "ivory1\0" + "ivory2\0" + "ivory3\0" + "ivory4\0" + "khaki\0" + "khaki1\0" + "khaki2\0" + "khaki3\0" + "khaki4\0" + "lavender\0" + "LavenderBlush\0" + "LavenderBlush1\0" + "LavenderBlush2\0" + "LavenderBlush3\0" + "LavenderBlush4\0" + "LawnGreen\0" + "LemonChiffon\0" + "LemonChiffon1\0" + "LemonChiffon2\0" + "LemonChiffon3\0" + "LemonChiffon4\0" + "LightBlue\0" + "LightBlue1\0" + "LightBlue2\0" + "LightBlue3\0" + "LightBlue4\0" + "LightCoral\0" + "LightCyan\0" + "LightCyan1\0" + "LightCyan2\0" + "LightCyan3\0" + "LightCyan4\0" + "LightGoldenrod\0" + "LightGoldenrod1\0" + "LightGoldenrod2\0" + "LightGoldenrod3\0" + "LightGoldenrod4\0" + "LightGoldenrodYellow\0" + "LightGray\0" + "LightGreen\0" + "LightGrey\0" + "LightPink\0" + "LightPink1\0" + "LightPink2\0" + "LightPink3\0" + "LightPink4\0" + "LightSalmon\0" + "LightSalmon1\0" + "LightSalmon2\0" + "LightSalmon3\0" + "LightSalmon4\0" + "LightSeaGreen\0" + "LightSkyBlue\0" + "LightSkyBlue1\0" + "LightSkyBlue2\0" + "LightSkyBlue3\0" + "LightSkyBlue4\0" + "LightSlateBlue\0" + "LightSlateGray\0" + "LightSlateGrey\0" + "LightSteelBlue\0" + "LightSteelBlue1\0" + "LightSteelBlue2\0" + "LightSteelBlue3\0" + "LightSteelBlue4\0" + "LightYellow\0" + "LightYellow1\0" + "LightYellow2\0" + "LightYellow3\0" + "LightYellow4\0" + "lime\0" + "LimeGreen\0" + "linen\0" + "magenta\0" + "magenta1\0" + "magenta2\0" + "magenta3\0" + "magenta4\0" + "maroon\0" + "maroon1\0" + "maroon2\0" + "maroon3\0" + "maroon4\0" + "MediumAquamarine\0" + "MediumBlue\0" + "MediumOrchid\0" + "MediumOrchid1\0" + "MediumOrchid2\0" + "MediumOrchid3\0" + "MediumOrchid4\0" + "MediumPurple\0" + "MediumPurple1\0" + "MediumPurple2\0" + "MediumPurple3\0" + "MediumPurple4\0" + "MediumSeaGreen\0" + "MediumSlateBlue\0" + "MediumSpringGreen\0" + "MediumTurquoise\0" + "MediumVioletRed\0" + "MidnightBlue\0" + "MintCream\0" + "MistyRose\0" + "MistyRose1\0" + "MistyRose2\0" + "MistyRose3\0" + "MistyRose4\0" + "moccasin\0" + "NavajoWhite\0" + "NavajoWhite1\0" + "NavajoWhite2\0" + "NavajoWhite3\0" + "NavajoWhite4\0" + "navy\0" + "NavyBlue\0" + "OldLace\0" + "olive\0" + "OliveDrab\0" + "OliveDrab1\0" + "OliveDrab2\0" + "OliveDrab3\0" + "OliveDrab4\0" + "orange\0" + "orange1\0" + "orange2\0" + "orange3\0" + "orange4\0" + "OrangeRed\0" + "OrangeRed1\0" + "OrangeRed2\0" + "OrangeRed3\0" + "OrangeRed4\0" + "orchid\0" + "orchid1\0" + "orchid2\0" + "orchid3\0" + "orchid4\0" + "PaleGoldenrod\0" + "PaleGreen\0" + "PaleGreen1\0" + "PaleGreen2\0" + "PaleGreen3\0" + "PaleGreen4\0" + "PaleTurquoise\0" + "PaleTurquoise1\0" + "PaleTurquoise2\0" + "PaleTurquoise3\0" + "PaleTurquoise4\0" + "PaleVioletRed\0" + "PaleVioletRed1\0" + "PaleVioletRed2\0" + "PaleVioletRed3\0" + "PaleVioletRed4\0" + "PapayaWhip\0" + "PeachPuff\0" + "PeachPuff1\0" + "PeachPuff2\0" + "PeachPuff3\0" + "PeachPuff4\0" + "peru\0" + "pink\0" + "pink1\0" + "pink2\0" + "pink3\0" + "pink4\0" + "plum\0" + "plum1\0" + "plum2\0" + "plum3\0" + "plum4\0" + "PowderBlue\0" + "purple\0" + "purple1\0" + "purple2\0" + "purple3\0" + "purple4\0" + "rebeccapurple\0" + "red\0" + "red1\0" + "red2\0" + "red3\0" + "red4\0" + "RosyBrown\0" + "RosyBrown1\0" + "RosyBrown2\0" + "RosyBrown3\0" + "RosyBrown4\0" + "RoyalBlue\0" + "RoyalBlue1\0" + "RoyalBlue2\0" + "RoyalBlue3\0" + "RoyalBlue4\0" + "SaddleBrown\0" + "salmon\0" + "salmon1\0" + "salmon2\0" + "salmon3\0" + "salmon4\0" + "SandyBrown\0" + "SeaGreen\0" + "SeaGreen1\0" + "SeaGreen2\0" + "SeaGreen3\0" + "SeaGreen4\0" + "seashell\0" + "seashell1\0" + "seashell2\0" + "seashell3\0" + "seashell4\0" + "sienna\0" + "sienna1\0" + "sienna2\0" + "sienna3\0" + "sienna4\0" + "silver\0" + "SkyBlue\0" + "SkyBlue1\0" + "SkyBlue2\0" + "SkyBlue3\0" + "SkyBlue4\0" + "SlateBlue\0" + "SlateBlue1\0" + "SlateBlue2\0" + "SlateBlue3\0" + "SlateBlue4\0" + "SlateGray\0" + "SlateGray1\0" + "SlateGray2\0" + "SlateGray3\0" + "SlateGray4\0" + "SlateGrey\0" + "snow\0" + "snow1\0" + "snow2\0" + "snow3\0" + "snow4\0" + "SpringGreen\0" + "SpringGreen1\0" + "SpringGreen2\0" + "SpringGreen3\0" + "SpringGreen4\0" + "SteelBlue\0" + "SteelBlue1\0" + "SteelBlue2\0" + "SteelBlue3\0" + "SteelBlue4\0" + "tan\0" + "tan1\0" + "tan2\0" + "tan3\0" + "tan4\0" + "teal\0" + "thistle\0" + "thistle1\0" + "thistle2\0" + "thistle3\0" + "thistle4\0" + "tomato\0" + "tomato1\0" + "tomato2\0" + "tomato3\0" + "tomato4\0" + "turquoise\0" + "turquoise1\0" + "turquoise2\0" + "turquoise3\0" + "turquoise4\0" + "violet\0" + "VioletRed\0" + "VioletRed1\0" + "VioletRed2\0" + "VioletRed3\0" + "VioletRed4\0" + "wheat\0" + "wheat1\0" + "wheat2\0" + "wheat3\0" + "wheat4\0" + "white\0" + "WhiteSmoke\0" + "yellow\0" + "yellow1\0" + "yellow2\0" + "yellow3\0" + "yellow4\0" + "YellowGreen\0"; + +typedef struct { + guint16 name_offset; + guchar red; + guchar green; + guchar blue; +} ColorEntry; + +static const ColorEntry color_entries[] = { + { 0, 240, 248, 255 }, + { 10, 250, 235, 215 }, + { 23, 255, 239, 219 }, + { 37, 238, 223, 204 }, + { 51, 205, 192, 176 }, + { 65, 139, 131, 120 }, + { 79, 0, 255, 255 }, + { 84, 127, 255, 212 }, + { 95, 127, 255, 212 }, + { 107, 118, 238, 198 }, + { 119, 102, 205, 170 }, + { 131, 69, 139, 116 }, + { 143, 240, 255, 255 }, + { 149, 240, 255, 255 }, + { 156, 224, 238, 238 }, + { 163, 193, 205, 205 }, + { 170, 131, 139, 139 }, + { 177, 245, 245, 220 }, + { 183, 255, 228, 196 }, + { 190, 255, 228, 196 }, + { 198, 238, 213, 183 }, + { 206, 205, 183, 158 }, + { 214, 139, 125, 107 }, + { 222, 0, 0, 0 }, + { 228, 255, 235, 205 }, + { 243, 0, 0, 255 }, + { 248, 0, 0, 255 }, + { 254, 0, 0, 238 }, + { 260, 0, 0, 205 }, + { 266, 0, 0, 139 }, + { 272, 138, 43, 226 }, + { 283, 165, 42, 42 }, + { 289, 255, 64, 64 }, + { 296, 238, 59, 59 }, + { 303, 205, 51, 51 }, + { 310, 139, 35, 35 }, + { 317, 222, 184, 135 }, + { 327, 255, 211, 155 }, + { 338, 238, 197, 145 }, + { 349, 205, 170, 125 }, + { 360, 139, 115, 85 }, + { 371, 95, 158, 160 }, + { 381, 152, 245, 255 }, + { 392, 142, 229, 238 }, + { 403, 122, 197, 205 }, + { 414, 83, 134, 139 }, + { 425, 127, 255, 0 }, + { 436, 127, 255, 0 }, + { 448, 118, 238, 0 }, + { 460, 102, 205, 0 }, + { 472, 69, 139, 0 }, + { 484, 210, 105, 30 }, + { 494, 255, 127, 36 }, + { 505, 238, 118, 33 }, + { 516, 205, 102, 29 }, + { 527, 139, 69, 19 }, + { 538, 255, 127, 80 }, + { 544, 255, 114, 86 }, + { 551, 238, 106, 80 }, + { 558, 205, 91, 69 }, + { 565, 139, 62, 47 }, + { 572, 100, 149, 237 }, + { 587, 255, 248, 220 }, + { 596, 255, 248, 220 }, + { 606, 238, 232, 205 }, + { 616, 205, 200, 177 }, + { 626, 139, 136, 120 }, + { 636, 220, 20, 60 }, + { 644, 0, 255, 255 }, + { 649, 0, 255, 255 }, + { 655, 0, 238, 238 }, + { 661, 0, 205, 205 }, + { 667, 0, 139, 139 }, + { 673, 0, 0, 139 }, + { 682, 0, 139, 139 }, + { 691, 184, 134, 11 }, + { 705, 255, 185, 15 }, + { 720, 238, 173, 14 }, + { 735, 205, 149, 12 }, + { 750, 139, 101, 8 }, + { 765, 169, 169, 169 }, + { 774, 0, 100, 0 }, + { 784, 169, 169, 169 }, + { 793, 189, 183, 107 }, + { 803, 139, 0, 139 }, + { 815, 85, 107, 47 }, + { 830, 202, 255, 112 }, + { 846, 188, 238, 104 }, + { 862, 162, 205, 90 }, + { 878, 110, 139, 61 }, + { 894, 255, 140, 0 }, + { 905, 255, 127, 0 }, + { 917, 238, 118, 0 }, + { 929, 205, 102, 0 }, + { 941, 139, 69, 0 }, + { 953, 153, 50, 204 }, + { 964, 191, 62, 255 }, + { 976, 178, 58, 238 }, + { 988, 154, 50, 205 }, + { 1000, 104, 34, 139 }, + { 1012, 139, 0, 0 }, + { 1020, 233, 150, 122 }, + { 1031, 143, 188, 143 }, + { 1044, 193, 255, 193 }, + { 1058, 180, 238, 180 }, + { 1072, 155, 205, 155 }, + { 1086, 105, 139, 105 }, + { 1100, 72, 61, 139 }, + { 1114, 47, 79, 79 }, + { 1128, 151, 255, 255 }, + { 1143, 141, 238, 238 }, + { 1158, 121, 205, 205 }, + { 1173, 82, 139, 139 }, + { 1188, 47, 79, 79 }, + { 1202, 0, 206, 209 }, + { 1216, 148, 0, 211 }, + { 1227, 255, 20, 147 }, + { 1236, 255, 20, 147 }, + { 1246, 238, 18, 137 }, + { 1256, 205, 16, 118 }, + { 1266, 139, 10, 80 }, + { 1276, 0, 191, 255 }, + { 1288, 0, 191, 255 }, + { 1301, 0, 178, 238 }, + { 1314, 0, 154, 205 }, + { 1327, 0, 104, 139 }, + { 1340, 105, 105, 105 }, + { 1348, 105, 105, 105 }, + { 1356, 30, 144, 255 }, + { 1367, 30, 144, 255 }, + { 1379, 28, 134, 238 }, + { 1391, 24, 116, 205 }, + { 1403, 16, 78, 139 }, + { 1415, 178, 34, 34 }, + { 1425, 255, 48, 48 }, + { 1436, 238, 44, 44 }, + { 1447, 205, 38, 38 }, + { 1458, 139, 26, 26 }, + { 1469, 255, 250, 240 }, + { 1481, 34, 139, 34 }, + { 1493, 255, 0, 255 }, + { 1501, 220, 220, 220 }, + { 1511, 248, 248, 255 }, + { 1522, 255, 215, 0 }, + { 1527, 255, 215, 0 }, + { 1533, 238, 201, 0 }, + { 1539, 205, 173, 0 }, + { 1545, 139, 117, 0 }, + { 1551, 218, 165, 32 }, + { 1561, 255, 193, 37 }, + { 1572, 238, 180, 34 }, + { 1583, 205, 155, 29 }, + { 1594, 139, 105, 20 }, + { 1605, 128, 128, 128 }, + { 1610, 0, 0, 0 }, + { 1616, 3, 3, 3 }, + { 1622, 26, 26, 26 }, + { 1629, 255, 255, 255 }, + { 1637, 28, 28, 28 }, + { 1644, 31, 31, 31 }, + { 1651, 33, 33, 33 }, + { 1658, 36, 36, 36 }, + { 1665, 38, 38, 38 }, + { 1672, 41, 41, 41 }, + { 1679, 43, 43, 43 }, + { 1686, 46, 46, 46 }, + { 1693, 48, 48, 48 }, + { 1700, 5, 5, 5 }, + { 1706, 51, 51, 51 }, + { 1713, 54, 54, 54 }, + { 1720, 56, 56, 56 }, + { 1727, 59, 59, 59 }, + { 1734, 61, 61, 61 }, + { 1741, 64, 64, 64 }, + { 1748, 66, 66, 66 }, + { 1755, 69, 69, 69 }, + { 1762, 71, 71, 71 }, + { 1769, 74, 74, 74 }, + { 1776, 8, 8, 8 }, + { 1782, 77, 77, 77 }, + { 1789, 79, 79, 79 }, + { 1796, 82, 82, 82 }, + { 1803, 84, 84, 84 }, + { 1810, 87, 87, 87 }, + { 1817, 89, 89, 89 }, + { 1824, 92, 92, 92 }, + { 1831, 94, 94, 94 }, + { 1838, 97, 97, 97 }, + { 1845, 99, 99, 99 }, + { 1852, 10, 10, 10 }, + { 1858, 102, 102, 102 }, + { 1865, 105, 105, 105 }, + { 1872, 107, 107, 107 }, + { 1879, 110, 110, 110 }, + { 1886, 112, 112, 112 }, + { 1893, 115, 115, 115 }, + { 1900, 117, 117, 117 }, + { 1907, 120, 120, 120 }, + { 1914, 122, 122, 122 }, + { 1921, 125, 125, 125 }, + { 1928, 13, 13, 13 }, + { 1934, 127, 127, 127 }, + { 1941, 130, 130, 130 }, + { 1948, 133, 133, 133 }, + { 1955, 135, 135, 135 }, + { 1962, 138, 138, 138 }, + { 1969, 140, 140, 140 }, + { 1976, 143, 143, 143 }, + { 1983, 145, 145, 145 }, + { 1990, 148, 148, 148 }, + { 1997, 150, 150, 150 }, + { 2004, 15, 15, 15 }, + { 2010, 153, 153, 153 }, + { 2017, 156, 156, 156 }, + { 2024, 158, 158, 158 }, + { 2031, 161, 161, 161 }, + { 2038, 163, 163, 163 }, + { 2045, 166, 166, 166 }, + { 2052, 168, 168, 168 }, + { 2059, 171, 171, 171 }, + { 2066, 173, 173, 173 }, + { 2073, 176, 176, 176 }, + { 2080, 18, 18, 18 }, + { 2086, 179, 179, 179 }, + { 2093, 181, 181, 181 }, + { 2100, 184, 184, 184 }, + { 2107, 186, 186, 186 }, + { 2114, 189, 189, 189 }, + { 2121, 191, 191, 191 }, + { 2128, 194, 194, 194 }, + { 2135, 196, 196, 196 }, + { 2142, 199, 199, 199 }, + { 2149, 201, 201, 201 }, + { 2156, 20, 20, 20 }, + { 2162, 204, 204, 204 }, + { 2169, 207, 207, 207 }, + { 2176, 209, 209, 209 }, + { 2183, 212, 212, 212 }, + { 2190, 214, 214, 214 }, + { 2197, 217, 217, 217 }, + { 2204, 219, 219, 219 }, + { 2211, 222, 222, 222 }, + { 2218, 224, 224, 224 }, + { 2225, 227, 227, 227 }, + { 2232, 23, 23, 23 }, + { 2238, 229, 229, 229 }, + { 2245, 232, 232, 232 }, + { 2252, 235, 235, 235 }, + { 2259, 237, 237, 237 }, + { 2266, 240, 240, 240 }, + { 2273, 242, 242, 242 }, + { 2280, 245, 245, 245 }, + { 2287, 247, 247, 247 }, + { 2294, 250, 250, 250 }, + { 2301, 252, 252, 252 }, + { 2308, 0, 128, 0 }, + { 2314, 0, 255, 0 }, + { 2321, 0, 238, 0 }, + { 2328, 0, 205, 0 }, + { 2335, 0, 139, 0 }, + { 2342, 173, 255, 47 }, + { 2354, 128, 128, 128 }, + { 2359, 0, 0, 0 }, + { 2365, 3, 3, 3 }, + { 2371, 26, 26, 26 }, + { 2378, 255, 255, 255 }, + { 2386, 28, 28, 28 }, + { 2393, 31, 31, 31 }, + { 2400, 33, 33, 33 }, + { 2407, 36, 36, 36 }, + { 2414, 38, 38, 38 }, + { 2421, 41, 41, 41 }, + { 2428, 43, 43, 43 }, + { 2435, 46, 46, 46 }, + { 2442, 48, 48, 48 }, + { 2449, 5, 5, 5 }, + { 2455, 51, 51, 51 }, + { 2462, 54, 54, 54 }, + { 2469, 56, 56, 56 }, + { 2476, 59, 59, 59 }, + { 2483, 61, 61, 61 }, + { 2490, 64, 64, 64 }, + { 2497, 66, 66, 66 }, + { 2504, 69, 69, 69 }, + { 2511, 71, 71, 71 }, + { 2518, 74, 74, 74 }, + { 2525, 8, 8, 8 }, + { 2531, 77, 77, 77 }, + { 2538, 79, 79, 79 }, + { 2545, 82, 82, 82 }, + { 2552, 84, 84, 84 }, + { 2559, 87, 87, 87 }, + { 2566, 89, 89, 89 }, + { 2573, 92, 92, 92 }, + { 2580, 94, 94, 94 }, + { 2587, 97, 97, 97 }, + { 2594, 99, 99, 99 }, + { 2601, 10, 10, 10 }, + { 2607, 102, 102, 102 }, + { 2614, 105, 105, 105 }, + { 2621, 107, 107, 107 }, + { 2628, 110, 110, 110 }, + { 2635, 112, 112, 112 }, + { 2642, 115, 115, 115 }, + { 2649, 117, 117, 117 }, + { 2656, 120, 120, 120 }, + { 2663, 122, 122, 122 }, + { 2670, 125, 125, 125 }, + { 2677, 13, 13, 13 }, + { 2683, 127, 127, 127 }, + { 2690, 130, 130, 130 }, + { 2697, 133, 133, 133 }, + { 2704, 135, 135, 135 }, + { 2711, 138, 138, 138 }, + { 2718, 140, 140, 140 }, + { 2725, 143, 143, 143 }, + { 2732, 145, 145, 145 }, + { 2739, 148, 148, 148 }, + { 2746, 150, 150, 150 }, + { 2753, 15, 15, 15 }, + { 2759, 153, 153, 153 }, + { 2766, 156, 156, 156 }, + { 2773, 158, 158, 158 }, + { 2780, 161, 161, 161 }, + { 2787, 163, 163, 163 }, + { 2794, 166, 166, 166 }, + { 2801, 168, 168, 168 }, + { 2808, 171, 171, 171 }, + { 2815, 173, 173, 173 }, + { 2822, 176, 176, 176 }, + { 2829, 18, 18, 18 }, + { 2835, 179, 179, 179 }, + { 2842, 181, 181, 181 }, + { 2849, 184, 184, 184 }, + { 2856, 186, 186, 186 }, + { 2863, 189, 189, 189 }, + { 2870, 191, 191, 191 }, + { 2877, 194, 194, 194 }, + { 2884, 196, 196, 196 }, + { 2891, 199, 199, 199 }, + { 2898, 201, 201, 201 }, + { 2905, 20, 20, 20 }, + { 2911, 204, 204, 204 }, + { 2918, 207, 207, 207 }, + { 2925, 209, 209, 209 }, + { 2932, 212, 212, 212 }, + { 2939, 214, 214, 214 }, + { 2946, 217, 217, 217 }, + { 2953, 219, 219, 219 }, + { 2960, 222, 222, 222 }, + { 2967, 224, 224, 224 }, + { 2974, 227, 227, 227 }, + { 2981, 23, 23, 23 }, + { 2987, 229, 229, 229 }, + { 2994, 232, 232, 232 }, + { 3001, 235, 235, 235 }, + { 3008, 237, 237, 237 }, + { 3015, 240, 240, 240 }, + { 3022, 242, 242, 242 }, + { 3029, 245, 245, 245 }, + { 3036, 247, 247, 247 }, + { 3043, 250, 250, 250 }, + { 3050, 252, 252, 252 }, + { 3057, 240, 255, 240 }, + { 3066, 240, 255, 240 }, + { 3076, 224, 238, 224 }, + { 3086, 193, 205, 193 }, + { 3096, 131, 139, 131 }, + { 3106, 255, 105, 180 }, + { 3114, 255, 110, 180 }, + { 3123, 238, 106, 167 }, + { 3132, 205, 96, 144 }, + { 3141, 139, 58, 98 }, + { 3150, 205, 92, 92 }, + { 3160, 255, 106, 106 }, + { 3171, 238, 99, 99 }, + { 3182, 205, 85, 85 }, + { 3193, 139, 58, 58 }, + { 3204, 75, 0, 130 }, + { 3211, 255, 255, 240 }, + { 3217, 255, 255, 240 }, + { 3224, 238, 238, 224 }, + { 3231, 205, 205, 193 }, + { 3238, 139, 139, 131 }, + { 3245, 240, 230, 140 }, + { 3251, 255, 246, 143 }, + { 3258, 238, 230, 133 }, + { 3265, 205, 198, 115 }, + { 3272, 139, 134, 78 }, + { 3279, 230, 230, 250 }, + { 3288, 255, 240, 245 }, + { 3302, 255, 240, 245 }, + { 3317, 238, 224, 229 }, + { 3332, 205, 193, 197 }, + { 3347, 139, 131, 134 }, + { 3362, 124, 252, 0 }, + { 3372, 255, 250, 205 }, + { 3385, 255, 250, 205 }, + { 3399, 238, 233, 191 }, + { 3413, 205, 201, 165 }, + { 3427, 139, 137, 112 }, + { 3441, 173, 216, 230 }, + { 3451, 191, 239, 255 }, + { 3462, 178, 223, 238 }, + { 3473, 154, 192, 205 }, + { 3484, 104, 131, 139 }, + { 3495, 240, 128, 128 }, + { 3506, 224, 255, 255 }, + { 3516, 224, 255, 255 }, + { 3527, 209, 238, 238 }, + { 3538, 180, 205, 205 }, + { 3549, 122, 139, 139 }, + { 3560, 238, 221, 130 }, + { 3575, 255, 236, 139 }, + { 3591, 238, 220, 130 }, + { 3607, 205, 190, 112 }, + { 3623, 139, 129, 76 }, + { 3639, 250, 250, 210 }, + { 3660, 211, 211, 211 }, + { 3670, 144, 238, 144 }, + { 3681, 211, 211, 211 }, + { 3691, 255, 182, 193 }, + { 3701, 255, 174, 185 }, + { 3712, 238, 162, 173 }, + { 3723, 205, 140, 149 }, + { 3734, 139, 95, 101 }, + { 3745, 255, 160, 122 }, + { 3757, 255, 160, 122 }, + { 3770, 238, 149, 114 }, + { 3783, 205, 129, 98 }, + { 3796, 139, 87, 66 }, + { 3809, 32, 178, 170 }, + { 3823, 135, 206, 250 }, + { 3836, 176, 226, 255 }, + { 3850, 164, 211, 238 }, + { 3864, 141, 182, 205 }, + { 3878, 96, 123, 139 }, + { 3892, 132, 112, 255 }, + { 3907, 119, 136, 153 }, + { 3922, 119, 136, 153 }, + { 3937, 176, 196, 222 }, + { 3952, 202, 225, 255 }, + { 3968, 188, 210, 238 }, + { 3984, 162, 181, 205 }, + { 4000, 110, 123, 139 }, + { 4016, 255, 255, 224 }, + { 4028, 255, 255, 224 }, + { 4041, 238, 238, 209 }, + { 4054, 205, 205, 180 }, + { 4067, 139, 139, 122 }, + { 4080, 0, 255, 0 }, + { 4085, 50, 205, 50 }, + { 4095, 250, 240, 230 }, + { 4101, 255, 0, 255 }, + { 4109, 255, 0, 255 }, + { 4118, 238, 0, 238 }, + { 4127, 205, 0, 205 }, + { 4136, 139, 0, 139 }, + { 4145, 128, 0, 0 }, + { 4152, 255, 52, 179 }, + { 4160, 238, 48, 167 }, + { 4168, 205, 41, 144 }, + { 4176, 139, 28, 98 }, + { 4184, 102, 205, 170 }, + { 4201, 0, 0, 205 }, + { 4212, 186, 85, 211 }, + { 4225, 224, 102, 255 }, + { 4239, 209, 95, 238 }, + { 4253, 180, 82, 205 }, + { 4267, 122, 55, 139 }, + { 4281, 147, 112, 219 }, + { 4294, 171, 130, 255 }, + { 4308, 159, 121, 238 }, + { 4322, 137, 104, 205 }, + { 4336, 93, 71, 139 }, + { 4350, 60, 179, 113 }, + { 4365, 123, 104, 238 }, + { 4381, 0, 250, 154 }, + { 4399, 72, 209, 204 }, + { 4415, 199, 21, 133 }, + { 4431, 25, 25, 112 }, + { 4444, 245, 255, 250 }, + { 4454, 255, 228, 225 }, + { 4464, 255, 228, 225 }, + { 4475, 238, 213, 210 }, + { 4486, 205, 183, 181 }, + { 4497, 139, 125, 123 }, + { 4508, 255, 228, 181 }, + { 4517, 255, 222, 173 }, + { 4529, 255, 222, 173 }, + { 4542, 238, 207, 161 }, + { 4555, 205, 179, 139 }, + { 4568, 139, 121, 94 }, + { 4581, 0, 0, 128 }, + { 4586, 0, 0, 128 }, + { 4595, 253, 245, 230 }, + { 4603, 128, 128, 0 }, + { 4609, 107, 142, 35 }, + { 4619, 192, 255, 62 }, + { 4630, 179, 238, 58 }, + { 4641, 154, 205, 50 }, + { 4652, 105, 139, 34 }, + { 4663, 255, 165, 0 }, + { 4670, 255, 165, 0 }, + { 4678, 238, 154, 0 }, + { 4686, 205, 133, 0 }, + { 4694, 139, 90, 0 }, + { 4702, 255, 69, 0 }, + { 4712, 255, 69, 0 }, + { 4723, 238, 64, 0 }, + { 4734, 205, 55, 0 }, + { 4745, 139, 37, 0 }, + { 4756, 218, 112, 214 }, + { 4763, 255, 131, 250 }, + { 4771, 238, 122, 233 }, + { 4779, 205, 105, 201 }, + { 4787, 139, 71, 137 }, + { 4795, 238, 232, 170 }, + { 4809, 152, 251, 152 }, + { 4819, 154, 255, 154 }, + { 4830, 144, 238, 144 }, + { 4841, 124, 205, 124 }, + { 4852, 84, 139, 84 }, + { 4863, 175, 238, 238 }, + { 4877, 187, 255, 255 }, + { 4892, 174, 238, 238 }, + { 4907, 150, 205, 205 }, + { 4922, 102, 139, 139 }, + { 4937, 219, 112, 147 }, + { 4951, 255, 130, 171 }, + { 4966, 238, 121, 159 }, + { 4981, 205, 104, 137 }, + { 4996, 139, 71, 93 }, + { 5011, 255, 239, 213 }, + { 5022, 255, 218, 185 }, + { 5032, 255, 218, 185 }, + { 5043, 238, 203, 173 }, + { 5054, 205, 175, 149 }, + { 5065, 139, 119, 101 }, + { 5076, 205, 133, 63 }, + { 5081, 255, 192, 203 }, + { 5086, 255, 181, 197 }, + { 5092, 238, 169, 184 }, + { 5098, 205, 145, 158 }, + { 5104, 139, 99, 108 }, + { 5110, 221, 160, 221 }, + { 5115, 255, 187, 255 }, + { 5121, 238, 174, 238 }, + { 5127, 205, 150, 205 }, + { 5133, 139, 102, 139 }, + { 5139, 176, 224, 230 }, + { 5150, 128, 0, 128 }, + { 5157, 155, 48, 255 }, + { 5165, 145, 44, 238 }, + { 5173, 125, 38, 205 }, + { 5181, 85, 26, 139 }, + { 5189, 102, 51, 153 }, + { 5203, 255, 0, 0 }, + { 5207, 255, 0, 0 }, + { 5212, 238, 0, 0 }, + { 5217, 205, 0, 0 }, + { 5222, 139, 0, 0 }, + { 5227, 188, 143, 143 }, + { 5237, 255, 193, 193 }, + { 5248, 238, 180, 180 }, + { 5259, 205, 155, 155 }, + { 5270, 139, 105, 105 }, + { 5281, 65, 105, 225 }, + { 5291, 72, 118, 255 }, + { 5302, 67, 110, 238 }, + { 5313, 58, 95, 205 }, + { 5324, 39, 64, 139 }, + { 5335, 139, 69, 19 }, + { 5347, 250, 128, 114 }, + { 5354, 255, 140, 105 }, + { 5362, 238, 130, 98 }, + { 5370, 205, 112, 84 }, + { 5378, 139, 76, 57 }, + { 5386, 244, 164, 96 }, + { 5397, 46, 139, 87 }, + { 5406, 84, 255, 159 }, + { 5416, 78, 238, 148 }, + { 5426, 67, 205, 128 }, + { 5436, 46, 139, 87 }, + { 5446, 255, 245, 238 }, + { 5455, 255, 245, 238 }, + { 5465, 238, 229, 222 }, + { 5475, 205, 197, 191 }, + { 5485, 139, 134, 130 }, + { 5495, 160, 82, 45 }, + { 5502, 255, 130, 71 }, + { 5510, 238, 121, 66 }, + { 5518, 205, 104, 57 }, + { 5526, 139, 71, 38 }, + { 5534, 192, 192, 192 }, + { 5541, 135, 206, 235 }, + { 5549, 135, 206, 255 }, + { 5558, 126, 192, 238 }, + { 5567, 108, 166, 205 }, + { 5576, 74, 112, 139 }, + { 5585, 106, 90, 205 }, + { 5595, 131, 111, 255 }, + { 5606, 122, 103, 238 }, + { 5617, 105, 89, 205 }, + { 5628, 71, 60, 139 }, + { 5639, 112, 128, 144 }, + { 5649, 198, 226, 255 }, + { 5660, 185, 211, 238 }, + { 5671, 159, 182, 205 }, + { 5682, 108, 123, 139 }, + { 5693, 112, 128, 144 }, + { 5703, 255, 250, 250 }, + { 5708, 255, 250, 250 }, + { 5714, 238, 233, 233 }, + { 5720, 205, 201, 201 }, + { 5726, 139, 137, 137 }, + { 5732, 0, 255, 127 }, + { 5744, 0, 255, 127 }, + { 5757, 0, 238, 118 }, + { 5770, 0, 205, 102 }, + { 5783, 0, 139, 69 }, + { 5796, 70, 130, 180 }, + { 5806, 99, 184, 255 }, + { 5817, 92, 172, 238 }, + { 5828, 79, 148, 205 }, + { 5839, 54, 100, 139 }, + { 5850, 210, 180, 140 }, + { 5854, 255, 165, 79 }, + { 5859, 238, 154, 73 }, + { 5864, 205, 133, 63 }, + { 5869, 139, 90, 43 }, + { 5874, 0, 128, 128 }, + { 5879, 216, 191, 216 }, + { 5887, 255, 225, 255 }, + { 5896, 238, 210, 238 }, + { 5905, 205, 181, 205 }, + { 5914, 139, 123, 139 }, + { 5923, 255, 99, 71 }, + { 5930, 255, 99, 71 }, + { 5938, 238, 92, 66 }, + { 5946, 205, 79, 57 }, + { 5954, 139, 54, 38 }, + { 5962, 64, 224, 208 }, + { 5972, 0, 245, 255 }, + { 5983, 0, 229, 238 }, + { 5994, 0, 197, 205 }, + { 6005, 0, 134, 139 }, + { 6016, 238, 130, 238 }, + { 6023, 208, 32, 144 }, + { 6033, 255, 62, 150 }, + { 6044, 238, 58, 140 }, + { 6055, 205, 50, 120 }, + { 6066, 139, 34, 82 }, + { 6077, 245, 222, 179 }, + { 6083, 255, 231, 186 }, + { 6090, 238, 216, 174 }, + { 6097, 205, 186, 150 }, + { 6104, 139, 126, 102 }, + { 6111, 255, 255, 255 }, + { 6117, 245, 245, 245 }, + { 6128, 255, 255, 0 }, + { 6135, 255, 255, 0 }, + { 6143, 238, 238, 0 }, + { 6151, 205, 205, 0 }, + { 6159, 139, 139, 0 }, + { 6167, 154, 205, 50 } +}; diff --git a/pango2/pango-color.c b/pango2/pango-color.c new file mode 100644 index 00000000..adc9a0d1 --- /dev/null +++ b/pango2/pango-color.c @@ -0,0 +1,337 @@ +/* pango + * pango-color.c: Color handling + * + * Copyright (C) 2000 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "pango-attributes.h" +#include "pango-impl-utils.h" + + +G_DEFINE_BOXED_TYPE (Pango2Color, pango2_color, + pango2_color_copy, + pango2_color_free); + +/** + * pango2_color_copy: + * @src: (nullable): color to copy + * + * Creates a copy of @src. + * + * The copy should be freed with [method@Pango2.Color.free]. + * Primarily used by language bindings, not that useful + * otherwise (since colors can just be copied by assignment + * in C). + * + * Return value: (nullable): the newly allocated `Pango2Color`, + * which should be freed with [method@Pango2.Color.free] + */ +Pango2Color* +pango2_color_copy (const Pango2Color *src) +{ + Pango2Color *ret; + + if (src == NULL) + return NULL; + + ret = g_slice_new (Pango2Color); + + *ret = *src; + + return ret; +} + +/** + * pango2_color_free: + * @color: (nullable): an allocated `Pango2Color` + * + * Frees a color allocated by [method@Pango2.Color.copy]. + */ +void +pango2_color_free (Pango2Color *color) +{ + if (color == NULL) + return; + + g_slice_free (Pango2Color, color); +} + +/** + * pango2_color_equal: + * @color1: (nullable): a `Pango2Color` + * @color2: (nullable): another `Pango2Color` + * + * Compares two colors for quality. + * + * Returns: `TRUE` if the colors are equal + */ +gboolean +pango2_color_equal (const Pango2Color *color1, + const Pango2Color *color2) +{ + return color1 == color2 || + (color1 && color2 && + color1->red == color2->red && + color1->green == color2->green && + color1->blue == color2->blue && + color1->alpha == color2->alpha); +} + +/** + * pango2_color_to_string: + * @color: a `Pango2Color` + * + * Returns a textual specification of @color. + * + * The string is in the hexadecimal form `#rrrrggggbbbbaaaa`, + * where `r`, `g`, `b` and `a` are hex digits representing the + * red, green, blue and alpha components respectively. + * + * Return value: a newly-allocated text string that must + * be freed with [GLib.free] + */ +char * +pango2_color_to_string (const Pango2Color *color) +{ + g_return_val_if_fail (color != NULL, NULL); + + return g_strdup_printf ("#%04x%04x%04x%04x", + color->red, + color->green, + color->blue, + color->alpha); +} + +/* Color parsing + */ + +/* The following 2 routines (parse_color, find_color) come from Tk, via the Win32 + * port of GDK. The licensing terms on these (longer than the functions) is: + * + * This software is copyrighted by the Regents of the University of + * California, Sun Microsystems, Inc., and other parties. The following + * terms apply to all files associated with the software unless explicitly + * disclaimed in individual files. + * + * The authors hereby grant permission to use, copy, modify, distribute, + * and license this software and its documentation for any purpose, provided + * that existing copyright notices are retained in all copies and that this + * notice is included verbatim in any distributions. No written agreement, + * license, or royalty fee is required for any of the authorized uses. + * Modifications to this software may be copyrighted by their authors + * and need not follow the licensing terms described here, provided that + * the new terms are clearly indicated on the first page of each file where + * they apply. + * + * IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY + * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES + * ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY + * DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE + * IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE + * NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR + * MODIFICATIONS. + * + * GOVERNMENT USE: If you are acquiring this software on behalf of the + * U.S. government, the Government shall have only "Restricted Rights" + * in the software and related documentation as defined in the Federal + * Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you + * are acquiring the software on behalf of the Department of Defense, the + * software shall be classified as "Commercial Computer Software" and the + * Government shall have only "Restricted Rights" as defined in Clause + * 252.227-7013 (c) (1) of DFARs. Notwithstanding the foregoing, the + * authors grant the U.S. Government and others acting in its behalf + * permission to use and distribute the software in accordance with the + * terms specified in this license. + */ + +#include "pango-color-table.h" + +#define ISUPPER(c) ((c) >= 'A' && (c) <= 'Z') +#define TOLOWER(c) (ISUPPER (c) ? (c) - 'A' + 'a' : (c)) + +static int +compare_xcolor_entries (const void *a, const void *b) +{ + const guchar *s1 = (const guchar *) a; + const guchar *s2 = (const guchar *) (color_names + ((const ColorEntry *) b)->name_offset); + + while (*s1 && *s2) + { + int c1, c2; + while (*s1 == ' ') s1++; + while (*s2 == ' ') s1++; + c1 = (int)(guchar) TOLOWER (*s1); + c2 = (int)(guchar) TOLOWER (*s2); + if (c1 != c2) + return (c1 - c2); + s1++; s2++; + } + + return ((int) *s1) - ((int) *s2); +} + +static gboolean +find_color(const char *name, + Pango2Color *color) +{ + ColorEntry *found; + + found = bsearch (name, color_entries, G_N_ELEMENTS (color_entries), + sizeof (ColorEntry), + compare_xcolor_entries); + if (found == NULL) + return FALSE; + + if (color) + { + color->red = (found->red * 65535) / 255; + color->green = (found->green * 65535) / 255; + color->blue = (found->blue * 65535) / 255; + } + + return TRUE; +} + +static gboolean +hex (const char *spec, + int len, + unsigned int *c) +{ + const char *end; + *c = 0; + for (end = spec + len; spec != end; spec++) + if (g_ascii_isxdigit (*spec)) + *c = (*c << 4) | g_ascii_xdigit_value (*spec); + else + return FALSE; + return TRUE; +} + + +/** + * pango2_color_parse: + * @color: a `Pango2Color` structure in which to store the result + * @spec: a string specifying the new color + * + * Fill in the fields of a color from a string specification. + * + * The string can either one of a large set of standard names. + * (Taken from the CSS Color [specification](https://www.w3.org/TR/css-color-4/#named-colors), + * or it can be a hexadecimal value in the form `#rgb`, + * `#rrggbb`, `#rrrgggbbb` or `#rrrrggggbbbb` where `r`, `g` + * and `b` are hex digits of the red, green, and blue components + * of the color, respectively. (White in the four forms is + * `#fff`, `#ffffff`, `#fffffffff` and `#ffffffffffff`.) + * + * Additionally, parse strings of the form `#rgba`, `#rrggbbaa`, + * `#rrrrggggbbbbaaaa`, and set the alpha component of @color + * to the value specified by the hex digits for `a`. If no alpha + * component is found in @spec, alpha is set to 0xffff (for a + * solid color). + * + * Return value: %TRUE if parsing of the specifier succeeded, + * otherwise %FALSE + */ +gboolean +pango2_color_parse (Pango2Color *color, + const char *spec) +{ + g_return_val_if_fail (spec != NULL, FALSE); + + color->alpha = 0xffff; + + if (spec[0] == '#') + { + size_t len; + unsigned int r, g, b, a; + gboolean has_alpha; + + spec++; + len = strlen (spec); + switch (len) + { + case 3: + case 6: + case 9: + case 12: + len /= 3; + has_alpha = FALSE; + break; + case 4: + case 8: + case 16: + len /= 4; + has_alpha = TRUE; + break; + default: + return FALSE; + } + + if (!hex (spec, len, &r) || + !hex (spec + len, len, &g) || + !hex (spec + len * 2, len, &b) || + (has_alpha && !hex (spec + len * 3, len, &a))) + return FALSE; + + if (color) + { + int bits = len * 4; + r <<= 16 - bits; + g <<= 16 - bits; + b <<= 16 - bits; + while (bits < 16) + { + r |= (r >> bits); + g |= (g >> bits); + b |= (b >> bits); + bits *= 2; + } + color->red = r; + color->green = g; + color->blue = b; + } + + if (has_alpha) + { + int bits = len * 4; + a <<= 16 - bits; + while (bits < 16) + { + a |= (a >> bits); + bits *= 2; + } + color->alpha = a; + } + } + else + { + if (!find_color (spec, color)) + return FALSE; + } + return TRUE; +} diff --git a/pango2/pango-color.h b/pango2/pango-color.h new file mode 100644 index 00000000..f62a3dd5 --- /dev/null +++ b/pango2/pango-color.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2000 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-types.h> +#include <glib-object.h> + +G_BEGIN_DECLS + + +typedef struct _Pango2Color Pango2Color; + +/** + * Pango2Color: + * @red: value of the red component + * @green: value of the green component + * @blue: value of the blue component + * @alpha: value of the alpha component + * + * The `Pango2Color` structure is used to + * represent a color in an uncalibrated RGB color-space. + */ +struct _Pango2Color +{ + guint16 red; + guint16 green; + guint16 blue; + guint16 alpha; +}; + +#define PANGO2_TYPE_COLOR (pango2_color_get_type ()) + +PANGO2_AVAILABLE_IN_ALL +GType pango2_color_get_type (void) G_GNUC_CONST; + +PANGO2_AVAILABLE_IN_ALL +Pango2Color *pango2_color_copy (const Pango2Color *src); + +PANGO2_AVAILABLE_IN_ALL +void pango2_color_free (Pango2Color *color); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_color_equal (const Pango2Color *color1, + const Pango2Color *color2); +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_color_parse (Pango2Color *color, + const char *spec); + +PANGO2_AVAILABLE_IN_ALL +char * pango2_color_to_string (const Pango2Color *color); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(Pango2Color, pango2_color_free) + +G_END_DECLS diff --git a/pango2/pango-context-private.h b/pango2/pango-context-private.h new file mode 100644 index 00000000..07b4b506 --- /dev/null +++ b/pango2/pango-context-private.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2004 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-context.h" + +#ifdef HAVE_CAIRO +#include <cairo.h> +#endif + +struct _Pango2Context +{ + GObject parent_instance; + guint serial; + guint fontmap_serial; + + Pango2Language *set_language; + Pango2Language *language; + Pango2Direction base_dir; + Pango2Gravity base_gravity; + Pango2Gravity resolved_gravity; + Pango2GravityHint gravity_hint; + + Pango2FontDescription *font_desc; + + Pango2Matrix *matrix; + + Pango2FontMap *font_map; + + Pango2FontMetrics *metrics; + + gboolean round_glyph_positions; + +#ifdef HAVE_CAIRO + gboolean set_options_explicit; + + cairo_font_options_t *set_options; + cairo_font_options_t *surface_options; + cairo_font_options_t *merged_options; +#endif +}; diff --git a/pango2/pango-context.c b/pango2/pango-context.c new file mode 100644 index 00000000..58f2719a --- /dev/null +++ b/pango2/pango-context.c @@ -0,0 +1,1068 @@ +/* Pango2 + * pango-context.c: Contexts for itemization and shaping + * + * Copyright (C) 2000, 2006 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include <string.h> +#include <stdlib.h> + +#include <gio/gio.h> + +#include "pango-context.h" +#include "pango-context-private.h" +#include "pango-impl-utils.h" + +#include "pango-font-private.h" +#include "pango-font-metrics-private.h" +#include "pango-item-private.h" +#include "pango-fontset-private.h" +#include "pango-fontmap-private.h" +#include "pango-script-private.h" +#include "pango-emoji-private.h" + +/** + * Pango2Context: + * + * A `Pango2Context` stores global information used to control the + * itemization process. + * + * The information stored by `Pango2Context` includes the fontmap used + * to look up fonts, and default values such as the default language, + * default gravity, or default font. + * + * To obtain a `Pango2Context`, use [ctor@Pango2.Context.new] + * or [func@Pango2.cairo_create_context]. + */ + +struct _Pango2ContextClass +{ + GObjectClass parent_class; + +}; + +static void pango2_context_finalize (GObject *object); +static void context_changed (Pango2Context *context); + +enum { + PROP_FONT_MAP = 1, + PROP_FONT_DESCRIPTION, + PROP_LANGUAGE, + PROP_BASE_DIR, + PROP_BASE_GRAVITY, + PROP_GRAVITY_HINT, + PROP_MATRIX, + PROP_ROUND_GLYPH_POSITIONS, + N_PROPERTIES +}; + +static GParamSpec *properties[N_PROPERTIES] = { NULL, }; + +G_DEFINE_FINAL_TYPE (Pango2Context, pango2_context, G_TYPE_OBJECT) + +static void +pango2_context_init (Pango2Context *context) +{ + context->base_dir = PANGO2_DIRECTION_WEAK_LTR; + context->resolved_gravity = context->base_gravity = PANGO2_GRAVITY_SOUTH; + context->gravity_hint = PANGO2_GRAVITY_HINT_NATURAL; + + context->serial = 1; + context->set_language = NULL; + context->language = pango2_language_get_default (); + context->font_map = NULL; + context->round_glyph_positions = TRUE; + + context->font_desc = pango2_font_description_new (); + pango2_font_description_set_family_static (context->font_desc, "serif"); + pango2_font_description_set_style (context->font_desc, PANGO2_STYLE_NORMAL); + pango2_font_description_set_variant (context->font_desc, PANGO2_VARIANT_NORMAL); + pango2_font_description_set_weight (context->font_desc, PANGO2_WEIGHT_NORMAL); + pango2_font_description_set_stretch (context->font_desc, PANGO2_STRETCH_NORMAL); + pango2_font_description_set_size (context->font_desc, 12 * PANGO2_SCALE); +} + +static gboolean +pango2_has_mixed_deps (void) +{ + GModule *module; + gpointer func; + gboolean result = FALSE; + + module = g_module_open (NULL, 0); + + if (g_module_symbol (module, "pango2_ot_info_find_script", &func)) + result = TRUE; + + g_module_close (module); + + return result; +} + +static void +pango2_context_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + Pango2Context *context = PANGO2_CONTEXT (object); + + switch (property_id) + { + case PROP_FONT_MAP: + pango2_context_set_font_map (context, g_value_get_object (value)); + break; + + case PROP_FONT_DESCRIPTION: + pango2_context_set_font_description (context, g_value_get_boxed (value)); + break; + + case PROP_LANGUAGE: + pango2_context_set_language (context, g_value_get_boxed (value)); + break; + + case PROP_BASE_DIR: + pango2_context_set_base_dir (context, g_value_get_enum (value)); + break; + + case PROP_BASE_GRAVITY: + pango2_context_set_base_gravity (context, g_value_get_enum (value)); + break; + + case PROP_GRAVITY_HINT: + pango2_context_set_gravity_hint (context, g_value_get_enum (value)); + break; + + case PROP_MATRIX: + pango2_context_set_matrix (context, g_value_get_boxed (value)); + break; + + case PROP_ROUND_GLYPH_POSITIONS: + pango2_context_set_round_glyph_positions (context, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +pango2_context_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + Pango2Context *context = PANGO2_CONTEXT (object); + + switch (property_id) + { + case PROP_FONT_MAP: + g_value_set_object (value, pango2_context_get_font_map (context)); + break; + + case PROP_FONT_DESCRIPTION: + g_value_set_boxed (value, pango2_context_get_font_description (context)); + break; + + case PROP_LANGUAGE: + g_value_set_boxed (value, pango2_context_get_language (context)); + break; + + case PROP_BASE_DIR: + g_value_set_enum (value, pango2_context_get_base_dir (context)); + break; + + case PROP_BASE_GRAVITY: + g_value_set_enum (value, pango2_context_get_base_gravity (context)); + break; + + case PROP_GRAVITY_HINT: + g_value_set_enum (value, pango2_context_get_gravity_hint (context)); + break; + + case PROP_MATRIX: + g_value_set_boxed (value, pango2_context_get_matrix (context)); + break; + + case PROP_ROUND_GLYPH_POSITIONS: + g_value_set_boolean (value, pango2_context_get_round_glyph_positions (context)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +pango2_context_class_init (Pango2ContextClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + /* Put the check for mixed linkage here, for lack of a better place */ + if (pango2_has_mixed_deps ()) + g_error ("Pango2 1.x symbols detected.\n" + "Using Pango2 1.x and 2 in the same process is not supported."); + + object_class->finalize = pango2_context_finalize; + object_class->set_property = pango2_context_set_property; + object_class->get_property = pango2_context_get_property; + + /** + * Pango2Context:font-map: (attributes org.gtk.Property.get=pango2_context_get_font_map org.gtk.Property.set=pango2_context_set_font_map) + * + * The font map to be searched when fonts are looked up + * in this context. + */ + properties[PROP_FONT_MAP] = + g_param_spec_object ("font-map", NULL, NULL, PANGO2_TYPE_FONT_MAP, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2Context:font-description: (attributes org.gtk.Property.get=pango2_context_get_font_description org.gtk.Property.set=pango2_context_set_font_description) + * + * The default font description for the context. + */ + properties[PROP_FONT_DESCRIPTION] = + g_param_spec_boxed ("font-description", NULL, NULL, PANGO2_TYPE_FONT_DESCRIPTION, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2Context:language: (attributes org.gtk.Property.get=pango2_context_get_language org.gtk.Property.set=pango2_context_set_language) + * + * The global language for the context. + */ + properties[PROP_LANGUAGE] = + g_param_spec_boxed ("language", NULL, NULL, PANGO2_TYPE_LANGUAGE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2Context:base-direction: (attributes org.gtk.Property.get=pango2_context_get_base_dir org.gtk.Property.set=pango2_context_set_base_dir) + * + * The base direction for the context. + * + * The base direction is used in applying the Unicode bidirectional + * algorithm; if the @direction is `PANGO2_DIRECTION_LTR` or + * `PANGO2_DIRECTION_RTL`, then the value will be used as the paragraph + * direction in the Unicode bidirectional algorithm. A value of + * `PANGO2_DIRECTION_WEAK_LTR` or `PANGO2_DIRECTION_WEAK_RTL` is used only + * for paragraphs that do not contain any strong characters themselves. + */ + properties[PROP_BASE_DIR] = + g_param_spec_enum ("base-direction", NULL, NULL, PANGO2_TYPE_DIRECTION, + PANGO2_DIRECTION_LTR, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2Context:base-gravity: (attributes org.gtk.Property.get=pango2_context_get_base_gravity org.gtk.Property.set=pango2_context_set_base_gravity) + * + * The base gravity for the context. + * + * The base gravity is used in laying vertical text out. + */ + properties[PROP_BASE_GRAVITY] = + g_param_spec_enum ("base-gravity", NULL, NULL, PANGO2_TYPE_GRAVITY, + PANGO2_GRAVITY_SOUTH, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2Context:gravity-hint: (attributes org.gtk.Property.get=pango2_context_get_gravity_hint org.gtk.Property.set=pango2_context_set_gravity_hint) + * + * The gravity hint for the context. + * + * The gravity hint is used in laying vertical text out, and + * is only relevant if gravity of the context as returned by + * [method@Pango2.Context.get_gravity] is set to `PANGO2_GRAVITY_EAST` + * or `PANGO2_GRAVITY_WEST`. + */ + properties[PROP_GRAVITY_HINT] = + g_param_spec_enum ("gravity-hint", NULL, NULL, PANGO2_TYPE_GRAVITY_HINT, + PANGO2_GRAVITY_HINT_NATURAL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2Context:matrix: (attributes org.gtk.Property.get=pango2_context_get_matrix org.gtk.Property.set=pango2_context_set_matrix) + * + * The 'user to device' transformation that will be applied when rendering + * with this context. + * + * This matrix is also known as the current transformation matrix, or 'ctm'. + * + * The transformation is needed in cases where the font rendering applies + * hinting that depends on knowing the position of text with respect to + * the pixel grid. + */ + properties[PROP_MATRIX] = + g_param_spec_boxed ("matrix", NULL, NULL, PANGO2_TYPE_MATRIX, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2Context:round-glyph-positions: (attributes org.gtk.Property.get=pango2_context_get_round_glyph_positions org.gtk.Property.set=pango2_context_set_round_glyph_positions) + * + * Determines whether font rendering with this context should + * round glyph positions and widths to integral positions, + * in device units. + * + * This is useful when the renderer can't handle subpixel + * positioning of glyphs. + */ + properties[PROP_ROUND_GLYPH_POSITIONS] = + g_param_spec_boolean ("round-glyph-positions", NULL, NULL, TRUE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPERTIES, properties); +} + +static void +pango2_context_finalize (GObject *object) +{ + Pango2Context *context; + + context = PANGO2_CONTEXT (object); + + if (context->font_map) + g_object_unref (context->font_map); + + pango2_font_description_free (context->font_desc); + if (context->matrix) + pango2_matrix_free (context->matrix); + + if (context->metrics) + pango2_font_metrics_free (context->metrics); + + G_OBJECT_CLASS (pango2_context_parent_class)->finalize (object); +} + +/** + * pango2_context_new: + * + * Creates a new `Pango2Context` initialized to default values. + * + * If you want to use a specific [class@Pango2.FontMap] other than + * the default one, you should use [ctor@Pango2.Context.new_with_font_map] + * instead. + * + * If you are using Pango2 as part of a higher-level system, + * that system may have it's own way of create a `Pango2Context`. + * Pango2's own cairo support for instance, has [func@Pango2.cairo_create_context], + * and the GTK toolkit has, among others, gtk_widget_get_pango2_context(). + * Use those instead. + * + * Return value: the newly allocated `Pango2Context` + */ +Pango2Context * +pango2_context_new (void) +{ + return g_object_new (PANGO2_TYPE_CONTEXT, + "font-map", pango2_font_map_get_default (), + NULL); +} + +/** + * pango2_context_new_with_font_map: + * @font_map: the `Pango2FontMap` to use + * + * Creates a new `Pango2Context` with the given font map. + * + * If you are using Pango2 as part of a higher-level system, + * that system may have it's own way of create a `Pango2Context`. + * Pango2's own cairo support for instance, has [func@Pango2.cairo_create_context], + * and the GTK toolkit has, among others, gtk_widget_get_pango2_context(). + * Use those instead. + * + * Return value: the newly allocated `Pango2Context` + */ +Pango2Context * +pango2_context_new_with_font_map (Pango2FontMap *font_map) +{ + return g_object_new (PANGO2_TYPE_CONTEXT, + "font-map", font_map, + NULL); +} + +static void +update_resolved_gravity (Pango2Context *context) +{ + if (context->base_gravity == PANGO2_GRAVITY_AUTO) + context->resolved_gravity = pango2_gravity_get_for_matrix (context->matrix); + else + context->resolved_gravity = context->base_gravity; +} + +/** + * pango2_context_set_matrix: + * @context: a `Pango2Context` + * @matrix: (nullable): a `Pango2Matrix`, or %NULL to unset any existing + * matrix. (No matrix set is the same as setting the identity matrix.) + * + * Sets the 'user to device' transformation that will be applied when rendering + * with this context. + * + * This matrix is also known as the current transformation matrix, or 'ctm'. + * + * The transformation is needed in cases where the font rendering applies + * hinting that depends on knowing the position of text with respect to + * the pixel grid. + * + * Note that reported metrics are in the user space coordinates before + * the application of the matrix, not device-space coordinates after the + * application of the matrix. So, they don't scale with the matrix, though + * they may change slightly for different matrices, depending on how the + * text is fit to the pixel grid. + */ +void +pango2_context_set_matrix (Pango2Context *context, + const Pango2Matrix *matrix) +{ + g_return_if_fail (PANGO2_IS_CONTEXT (context)); + + if (context->matrix || matrix) + context_changed (context); + + if (context->matrix) + pango2_matrix_free (context->matrix); + if (matrix) + context->matrix = pango2_matrix_copy (matrix); + else + context->matrix = NULL; + + update_resolved_gravity (context); + g_object_notify_by_pspec (G_OBJECT (context), properties[PROP_MATRIX]); +} + +/** + * pango2_context_get_matrix: + * @context: a `Pango2Context` + * + * Gets the transformation matrix that will be applied when + * rendering with this context. + * + * See [method@Pango2.Context.set_matrix]. + * + * Return value: (nullable): the matrix, or %NULL if no matrix has + * been set (which is the same as the identity matrix). The returned + * matrix is owned by Pango2 and must not be modified or freed. + */ +const Pango2Matrix * +pango2_context_get_matrix (Pango2Context *context) +{ + g_return_val_if_fail (PANGO2_IS_CONTEXT (context), NULL); + + return context->matrix; +} + +/** + * pango2_context_set_font_map: + * @context: a `Pango2Context` + * @font_map: the `Pango2FontMap` to set. + * + * Sets the font map to be searched when fonts are looked-up + * in this context. + * + * This is only for internal use by Pango2 backends, a `Pango2Context` + * obtained via one of the recommended methods should already have a + * suitable font map. + */ +void +pango2_context_set_font_map (Pango2Context *context, + Pango2FontMap *font_map) +{ + g_return_if_fail (PANGO2_IS_CONTEXT (context)); + g_return_if_fail (!font_map || PANGO2_IS_FONT_MAP (font_map)); + + if (font_map == context->font_map) + return; + + context_changed (context); + + if (font_map) + g_object_ref (font_map); + + if (context->font_map) + g_object_unref (context->font_map); + + context->font_map = font_map; + context->fontmap_serial = pango2_font_map_get_serial (font_map); + + g_object_notify_by_pspec (G_OBJECT (context), properties[PROP_FONT_MAP]); +} + +/** + * pango2_context_get_font_map: + * @context: a `Pango2Context` + * + * Gets the `Pango2FontMap` used to look up fonts for this context. + * + * Return value: (transfer none): the font map for the `Pango2Context`. + * This value is owned by Pango2 and should not be unreferenced. + */ +Pango2FontMap * +pango2_context_get_font_map (Pango2Context *context) +{ + g_return_val_if_fail (PANGO2_IS_CONTEXT (context), NULL); + + return context->font_map; +} + +/** + * pango2_context_load_font: + * @context: a `Pango2Context` + * @desc: a `Pango2FontDescription` describing the font to load + * + * Loads the font in one of the fontmaps in the context + * that is the closest match for @desc. + * + * Returns: (transfer full) (nullable): the newly allocated `Pango2Font` + * that was loaded, or %NULL if no font matched. + */ +Pango2Font * +pango2_context_load_font (Pango2Context *context, + const Pango2FontDescription *desc) +{ + g_return_val_if_fail (context != NULL, NULL); + g_return_val_if_fail (context->font_map != NULL, NULL); + + return pango2_font_map_load_font (context->font_map, context, desc); +} + +/** + * pango2_context_load_fontset: + * @context: a `Pango2Context` + * @desc: a `Pango2FontDescription` describing the fonts to load + * @language: a `Pango2Language` the fonts will be used for + * + * Load a set of fonts in the context that can be used to render + * a font matching @desc. + * + * Returns: (transfer full) (nullable): the newly allocated + * `Pango2Fontset` loaded, or %NULL if no font matched. + */ +Pango2Fontset * +pango2_context_load_fontset (Pango2Context *context, + const Pango2FontDescription *desc, + Pango2Language *language) +{ + g_return_val_if_fail (context != NULL, NULL); + + return pango2_font_map_load_fontset (context->font_map, context, desc, language); +} + +/** + * pango2_context_set_font_description: + * @context: a `Pango2Context` + * @desc: the new pango font description + * + * Set the default font description for the context + */ +void +pango2_context_set_font_description (Pango2Context *context, + const Pango2FontDescription *desc) +{ + g_return_if_fail (context != NULL); + g_return_if_fail (desc != NULL); + + if (desc != context->font_desc && + (!desc || !context->font_desc || !pango2_font_description_equal(desc, context->font_desc))) + { + context_changed (context); + + pango2_font_description_free (context->font_desc); + context->font_desc = pango2_font_description_copy (desc); + + g_object_notify_by_pspec (G_OBJECT (context), properties[PROP_FONT_DESCRIPTION]); + } +} + +/** + * pango2_context_get_font_description: + * @context: a `Pango2Context` + * + * Retrieve the default font description for the context. + * + * Return value: (transfer none): a pointer to the context's default font + * description. This value must not be modified or freed. + */ +Pango2FontDescription * +pango2_context_get_font_description (Pango2Context *context) +{ + g_return_val_if_fail (context != NULL, NULL); + + return context->font_desc; +} + +/** + * pango2_context_set_language: + * @context: a `Pango2Context` + * @language: the new language tag. + * + * Sets the global language tag for the context. + * + * The default language for the locale of the running process + * can be found using [func@Pango2.Language.get_default]. + */ +void +pango2_context_set_language (Pango2Context *context, + Pango2Language *language) +{ + g_return_if_fail (context != NULL); + + if (language != context->language) + context_changed (context); + + context->set_language = language; + if (language) + context->language = language; + else + context->language = pango2_language_get_default (); + + g_object_notify_by_pspec (G_OBJECT (context), properties[PROP_LANGUAGE]); +} + +/** + * pango2_context_get_language: + * @context: a `Pango2Context` + * + * Retrieves the global language tag for the context. + * + * Return value: the global language tag. + */ +Pango2Language * +pango2_context_get_language (Pango2Context *context) +{ + g_return_val_if_fail (context != NULL, NULL); + + return context->set_language; +} + +/** + * pango2_context_set_base_dir: + * @context: a `Pango2Context` + * @direction: the new base direction + * + * Sets the base direction for the context. + * + * The base direction is used in applying the Unicode bidirectional + * algorithm; if the @direction is %PANGO2_DIRECTION_LTR or + * %PANGO2_DIRECTION_RTL, then the value will be used as the paragraph + * direction in the Unicode bidirectional algorithm. A value of + * %PANGO2_DIRECTION_WEAK_LTR or %PANGO2_DIRECTION_WEAK_RTL is used only + * for paragraphs that do not contain any strong characters themselves. + */ +void +pango2_context_set_base_dir (Pango2Context *context, + Pango2Direction direction) +{ + g_return_if_fail (context != NULL); + + if (direction == context->base_dir) + return; + + context_changed (context); + + context->base_dir = direction; + g_object_notify_by_pspec (G_OBJECT (context), properties[PROP_BASE_DIR]); +} + +/** + * pango2_context_get_base_dir: + * @context: a `Pango2Context` + * + * Retrieves the base direction for the context. + * + * See [method@Pango2.Context.set_base_dir]. + * + * Return value: the base direction for the context. + */ +Pango2Direction +pango2_context_get_base_dir (Pango2Context *context) +{ + g_return_val_if_fail (context != NULL, PANGO2_DIRECTION_LTR); + + return context->base_dir; +} + +/** + * pango2_context_set_base_gravity: + * @context: a `Pango2Context` + * @gravity: the new base gravity + * + * Sets the base gravity for the context. + * + * The base gravity is used in laying vertical text out. + */ +void +pango2_context_set_base_gravity (Pango2Context *context, + Pango2Gravity gravity) +{ + g_return_if_fail (context != NULL); + + if (gravity == context->base_gravity) + return; + + context_changed (context); + + context->base_gravity = gravity; + + update_resolved_gravity (context); + g_object_notify_by_pspec (G_OBJECT (context), properties[PROP_BASE_GRAVITY]); +} + +/** + * pango2_context_get_base_gravity: + * @context: a `Pango2Context` + * + * Retrieves the base gravity for the context. + * + * See [method@Pango2.Context.set_base_gravity]. + * + * Return value: the base gravity for the context. + */ +Pango2Gravity +pango2_context_get_base_gravity (Pango2Context *context) +{ + g_return_val_if_fail (context != NULL, PANGO2_GRAVITY_SOUTH); + + return context->base_gravity; +} + +/** + * pango2_context_get_gravity: + * @context: a `Pango2Context` + * + * Retrieves the gravity for the context. + * + * This is similar to [method@Pango2.Context.get_base_gravity], + * except for when the base gravity is %PANGO2_GRAVITY_AUTO for + * which [func@Pango2.Gravity.get_for_matrix] is used to return the + * gravity from the current context matrix. + * + * Return value: the resolved gravity for the context. + */ +Pango2Gravity +pango2_context_get_gravity (Pango2Context *context) +{ + g_return_val_if_fail (context != NULL, PANGO2_GRAVITY_SOUTH); + + return context->resolved_gravity; +} + +/** + * pango2_context_set_gravity_hint: + * @context: a `Pango2Context` + * @hint: the new gravity hint + * + * Sets the gravity hint for the context. + * + * The gravity hint is used in laying vertical text out, and + * is only relevant if gravity of the context as returned by + * [method@Pango2.Context.get_gravity] is set to %PANGO2_GRAVITY_EAST + * or %PANGO2_GRAVITY_WEST. + */ +void +pango2_context_set_gravity_hint (Pango2Context *context, + Pango2GravityHint hint) +{ + g_return_if_fail (context != NULL); + + if (hint == context->gravity_hint) + return; + + context_changed (context); + + context->gravity_hint = hint; + g_object_notify_by_pspec (G_OBJECT (context), properties[PROP_GRAVITY_HINT]); +} + +/** + * pango2_context_get_gravity_hint: + * @context: a `Pango2Context` + * + * Retrieves the gravity hint for the context. + * + * See [method@Pango2.Context.set_gravity_hint] for details. + * + * Return value: the gravity hint for the context. + */ +Pango2GravityHint +pango2_context_get_gravity_hint (Pango2Context *context) +{ + g_return_val_if_fail (context != NULL, PANGO2_GRAVITY_HINT_NATURAL); + + return context->gravity_hint; +} + + +static gboolean +get_first_metrics_foreach (Pango2Fontset *fontset, + Pango2Font *font, + gpointer data) +{ + Pango2FontMetrics **fontset_metrics = data; + Pango2Language *language = pango2_fontset_get_language (fontset); + + *fontset_metrics = pango2_font_get_metrics (font, language); + + return TRUE; /* Stops iteration */ +} + +static Pango2FontMetrics * +get_base_metrics (Pango2Fontset *fontset) +{ + Pango2FontMetrics *metrics = NULL; + + /* Initialize the metrics from the first font in the fontset */ + pango2_fontset_foreach (fontset, get_first_metrics_foreach, &metrics); + + return metrics; +} + +static void +update_metrics_from_items (Pango2FontMetrics *metrics, + Pango2Language *language, + const char *text, + unsigned int text_len, + GList *items) + +{ + GHashTable *fonts_seen = g_hash_table_new (NULL, NULL); + Pango2GlyphString *glyphs = pango2_glyph_string_new (); + GList *l; + glong text_width; + + /* This should typically be called with a sample text string. */ + g_return_if_fail (text_len > 0); + + metrics->approximate_char_width = 0; + + for (l = items; l; l = l->next) + { + Pango2Item *item = l->data; + Pango2Font *font = item->analysis.font; + + if (font != NULL && g_hash_table_lookup (fonts_seen, font) == NULL) + { + Pango2FontMetrics *raw_metrics = pango2_font_get_metrics (font, language); + g_hash_table_insert (fonts_seen, font, font); + + /* metrics will already be initialized from the first font in the fontset */ + metrics->ascent = MAX (metrics->ascent, raw_metrics->ascent); + metrics->descent = MAX (metrics->descent, raw_metrics->descent); + metrics->height = MAX (metrics->height, raw_metrics->height); + pango2_font_metrics_free (raw_metrics); + } + + pango2_shape (text + item->offset, item->length, + text, text_len, + &item->analysis, glyphs, + PANGO2_SHAPE_NONE); + + metrics->approximate_char_width += pango2_glyph_string_get_width (glyphs); + } + + pango2_glyph_string_free (glyphs); + g_hash_table_destroy (fonts_seen); + + text_width = pango2_utf8_strwidth (text); + g_assert (text_width > 0); + metrics->approximate_char_width /= text_width; +} + +/** + * pango2_context_get_metrics: + * @context: a `Pango2Context` + * @desc: (nullable): a `Pango2FontDescription` structure. %NULL means that the + * font description from the context will be used. + * @language: (nullable): language tag used to determine which script to get + * the metrics for. %NULL means that the language tag from the context + * will be used. If no language tag is set on the context, metrics + * for the default language (as determined by [func@Pango2.Language.get_default] + * will be returned. + * + * Get overall metric information for a particular font description. + * + * Since the metrics may be substantially different for different scripts, + * a language tag can be provided to indicate that the metrics should be + * retrieved that correspond to the script(s) used by that language. + * + * The `Pango2FontDescription` is interpreted in the same way as by [func@itemize], + * and the family name may be a comma separated list of names. If characters + * from multiple of these families would be used to render the string, then + * the returned fonts would be a composite of the metrics for the fonts loaded + * for the individual families. + * + * Return value: a `Pango2FontMetrics` object. The caller must call + * [method@Pango2.FontMetrics.free] when finished using the object. + */ +Pango2FontMetrics * +pango2_context_get_metrics (Pango2Context *context, + const Pango2FontDescription *desc, + Pango2Language *language) +{ + Pango2Fontset *current_fonts = NULL; + Pango2FontMetrics *metrics; + const char *sample_str; + unsigned int text_len; + GList *items; + + g_return_val_if_fail (PANGO2_IS_CONTEXT (context), NULL); + + if (!desc) + desc = context->font_desc; + + if (!language) + language = context->language; + + if (desc == context->font_desc && + language == context->language && + context->metrics != NULL) + return pango2_font_metrics_copy (context->metrics); + + current_fonts = pango2_font_map_load_fontset (context->font_map, context, desc, language); + metrics = get_base_metrics (current_fonts); + + sample_str = pango2_language_get_sample_string (language); + text_len = strlen (sample_str); + items = pango2_itemize_with_font (context, context->base_dir, + sample_str, 0, text_len, + NULL, NULL, + desc); + + update_metrics_from_items (metrics, language, sample_str, text_len, items); + + g_list_foreach (items, (GFunc)pango2_item_free, NULL); + g_list_free (items); + + g_object_unref (current_fonts); + + if (desc == context->font_desc && + language == context->language) + { + pango2_font_metrics_free (context->metrics); + context->metrics = pango2_font_metrics_copy (metrics); + } + + return metrics; +} + +static void +context_changed (Pango2Context *context) +{ + context->serial++; + if (context->serial == 0) + context->serial++; + + g_clear_pointer (&context->metrics, pango2_font_metrics_free); +} + +/** + * pango2_context_changed: + * @context: a `Pango2Context` + * + * Forces a change in the context, which will cause any `Pango2Layout` + * using this context to re-layout. + * + * This function is only useful when implementing a new backend + * for Pango2, something applications won't do. Backends should + * call this function if they have attached extra data to the context + * and such data is changed. + */ +void +pango2_context_changed (Pango2Context *context) +{ + context_changed (context); +} + +static void +check_fontmap_changed (Pango2Context *context) +{ + guint old_serial = context->fontmap_serial; + + if (!context->font_map) + return; + + context->fontmap_serial = pango2_font_map_get_serial (context->font_map); + + if (old_serial != context->fontmap_serial) + context_changed (context); +} + +/** + * pango2_context_get_serial: + * @context: a `Pango2Context` + * + * Returns the current serial number of @context. + * + * The serial number is initialized to an small number larger than zero + * when a new context is created and is increased whenever the context + * is changed using any of the setter functions, or the `Pango2FontMap` it + * uses to find fonts has changed. The serial may wrap, but will never + * have the value 0. Since it can wrap, never compare it with "less than", + * always use "not equals". + * + * This can be used to automatically detect changes to a `Pango2Context`, + * and is only useful when implementing objects that need update when their + * `Pango2Context` changes, like `Pango2Layout`. + * + * Return value: The current serial number of @context. + */ +guint +pango2_context_get_serial (Pango2Context *context) +{ + check_fontmap_changed (context); + return context->serial; +} + +/** + * pango2_context_set_round_glyph_positions: + * @context: a `Pango2Context` + * @round_positions: whether to round glyph positions + * + * Sets whether font rendering with this context should + * round glyph positions and widths to integral positions, + * in device units. + * + * This is useful when the renderer can't handle subpixel + * positioning of glyphs. + * + * The default value is to round glyph positions, to remain + * compatible with previous Pango2 behavior. + */ +void +pango2_context_set_round_glyph_positions (Pango2Context *context, + gboolean round_positions) +{ + if (context->round_glyph_positions == round_positions) + return; + + context->round_glyph_positions = round_positions; + context_changed (context); + g_object_notify_by_pspec (G_OBJECT (context), properties[PROP_ROUND_GLYPH_POSITIONS]); +} + +/** + * pango2_context_get_round_glyph_positions: + * @context: a `Pango2Context` + * + * Returns whether font rendering with this context should + * round glyph positions and widths. + */ +gboolean +pango2_context_get_round_glyph_positions (Pango2Context *context) +{ + return context->round_glyph_positions; +} diff --git a/pango2/pango-context.h b/pango2/pango-context.h new file mode 100644 index 00000000..902e840d --- /dev/null +++ b/pango2/pango-context.h @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2000 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-types.h> +#include <pango2/pango-font.h> +#include <pango2/pango-fontmap.h> +#include <pango2/pango-attributes.h> +#include <pango2/pango-direction.h> + +G_BEGIN_DECLS + +#define PANGO2_TYPE_CONTEXT (pango2_context_get_type ()) + +PANGO2_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (Pango2Context, pango2_context, PANGO2, CONTEXT, GObject); + +PANGO2_AVAILABLE_IN_ALL +Pango2Context * pango2_context_new (void); +PANGO2_AVAILABLE_IN_ALL +Pango2Context * pango2_context_new_with_font_map (Pango2FontMap *font_map); + +PANGO2_AVAILABLE_IN_ALL +void pango2_context_changed (Pango2Context *context); +PANGO2_AVAILABLE_IN_ALL +void pango2_context_set_font_map (Pango2Context *context, + Pango2FontMap *font_map); +PANGO2_AVAILABLE_IN_ALL +Pango2FontMap * pango2_context_get_font_map (Pango2Context *context); +PANGO2_AVAILABLE_IN_ALL +guint pango2_context_get_serial (Pango2Context *context); +PANGO2_AVAILABLE_IN_ALL +Pango2Font * pango2_context_load_font (Pango2Context *context, + const Pango2FontDescription *desc); +PANGO2_AVAILABLE_IN_ALL +Pango2Fontset * pango2_context_load_fontset (Pango2Context *context, + const Pango2FontDescription *desc, + Pango2Language *language); + +PANGO2_AVAILABLE_IN_ALL +Pango2FontMetrics * pango2_context_get_metrics (Pango2Context *context, + const Pango2FontDescription *desc, + Pango2Language *language); + +PANGO2_AVAILABLE_IN_ALL +void pango2_context_set_font_description (Pango2Context *context, + const Pango2FontDescription *desc); +PANGO2_AVAILABLE_IN_ALL +Pango2FontDescription * pango2_context_get_font_description (Pango2Context *context); +PANGO2_AVAILABLE_IN_ALL +Pango2Language * pango2_context_get_language (Pango2Context *context); +PANGO2_AVAILABLE_IN_ALL +void pango2_context_set_language (Pango2Context *context, + Pango2Language *language); +PANGO2_AVAILABLE_IN_ALL +void pango2_context_set_base_dir (Pango2Context *context, + Pango2Direction direction); +PANGO2_AVAILABLE_IN_ALL +Pango2Direction pango2_context_get_base_dir (Pango2Context *context); +PANGO2_AVAILABLE_IN_ALL +void pango2_context_set_base_gravity (Pango2Context *context, + Pango2Gravity gravity); +PANGO2_AVAILABLE_IN_ALL +Pango2Gravity pango2_context_get_base_gravity (Pango2Context *context); +PANGO2_AVAILABLE_IN_ALL +Pango2Gravity pango2_context_get_gravity (Pango2Context *context); +PANGO2_AVAILABLE_IN_ALL +void pango2_context_set_gravity_hint (Pango2Context *context, + Pango2GravityHint hint); +PANGO2_AVAILABLE_IN_ALL +Pango2GravityHint pango2_context_get_gravity_hint (Pango2Context *context); + +PANGO2_AVAILABLE_IN_ALL +void pango2_context_set_matrix (Pango2Context *context, + const Pango2Matrix *matrix); +PANGO2_AVAILABLE_IN_ALL +const Pango2Matrix * pango2_context_get_matrix (Pango2Context *context); + +PANGO2_AVAILABLE_IN_ALL +void pango2_context_set_round_glyph_positions (Pango2Context *context, + gboolean round_positions); +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_context_get_round_glyph_positions (Pango2Context *context); + +G_END_DECLS diff --git a/pango2/pango-direction.h b/pango2/pango-direction.h new file mode 100644 index 00000000..cf32c48a --- /dev/null +++ b/pango2/pango-direction.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2018 Matthias Clasen + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <glib.h> + +G_BEGIN_DECLS + +/** + * Pango2Direction: + * @PANGO2_DIRECTION_LTR: A strong left-to-right direction + * @PANGO2_DIRECTION_RTL: A strong right-to-left direction + * @PANGO2_DIRECTION_WEAK_LTR: A weak left-to-right direction + * @PANGO2_DIRECTION_WEAK_RTL: A weak right-to-left direction + * @PANGO2_DIRECTION_NEUTRAL: No direction specified + * + * `Pango2Direction` represents a direction in the Unicode bidirectional + * algorithm. + * + * Not every value in this enumeration makes sense for every usage of + * `Pango2Direction`; for example, the direction of characters cannot be + * `PANGO2_DIRECTION_WEAK_LTR` or `PANGO2_DIRECTION_WEAK_RTL`, since every + * character is either neutral or has a strong direction; on the other hand + * `PANGO2_DIRECTION_NEUTRAL` doesn't make sense to pass to [func@itemize]. + * + * See `Pango2Gravity` for how vertical text is handled in Pango2. + * + * If you are interested in text direction, you should really use + * [fribidi](http://fribidi.org/) directly. `Pango2Direction` is only + * retained because it is used in some public apis. + */ +typedef enum { + PANGO2_DIRECTION_LTR, + PANGO2_DIRECTION_RTL, + PANGO2_DIRECTION_WEAK_LTR, + PANGO2_DIRECTION_WEAK_RTL, + PANGO2_DIRECTION_NEUTRAL +} Pango2Direction; + +G_END_DECLS diff --git a/pango2/pango-emoji-private.h b/pango2/pango-emoji-private.h new file mode 100644 index 00000000..f7578bf4 --- /dev/null +++ b/pango2/pango-emoji-private.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2017 Google, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <glib.h> + +gboolean +_pango2_Is_Emoji_Base_Character (gunichar ch); + +gboolean +_pango2_Is_Emoji_Extended_Pictographic (gunichar ch); + +typedef struct _Pango2EmojiIter Pango2EmojiIter; + +struct _Pango2EmojiIter +{ + const char *text_start; + const char *text_end; + const char *start; + const char *end; + gboolean is_emoji; + + unsigned char *types; + unsigned int n_chars; + unsigned int cursor; +}; + +Pango2EmojiIter * +_pango2_emoji_iter_init (Pango2EmojiIter *iter, + const char *text, + int length); + +gboolean +_pango2_emoji_iter_next (Pango2EmojiIter *iter); + +void +_pango2_emoji_iter_fini (Pango2EmojiIter *iter); diff --git a/pango2/pango-emoji-table.h b/pango2/pango-emoji-table.h new file mode 100644 index 00000000..00f1dd13 --- /dev/null +++ b/pango2/pango-emoji-table.h @@ -0,0 +1,407 @@ +/* == Start of generated table == */ +/* + * The following tables are generated by running: + * + * ./gen-emoji-table.py emoji-data.txt + * + * on file with this header: + * + * # emoji-data-14.0.0.txt + * # Date: 2021-08-26, 17:22:22 GMT + * # © 2021 Unicode®, Inc. + * # Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. + * # For terms of use, see http://www.unicode.org/terms_of_use.html + * # + * # Emoji Data for UTS #51 + * # Used with Emoji Version 14.0 and subsequent minor revisions (if any) + * # + * # For documentation and usage, see http://www.unicode.org/reports/tr51 + */ + +#ifndef PANGO2_EMOJI_TABLE_H +#define PANGO2_EMOJI_TABLE_H + +#include <glib.h> + +struct Interval { + gunichar start, end; +}; + +static const struct Interval _pango2_Emoji_table[] = +{ + {0x0023, 0x0023}, + {0x002A, 0x002A}, + {0x0030, 0x0039}, + {0x00A9, 0x00A9}, + {0x00AE, 0x00AE}, + {0x203C, 0x203C}, + {0x2049, 0x2049}, + {0x2122, 0x2122}, + {0x2139, 0x2139}, + {0x2194, 0x2199}, + {0x21A9, 0x21AA}, + {0x231A, 0x231B}, + {0x2328, 0x2328}, + {0x23CF, 0x23CF}, + {0x23E9, 0x23F3}, + {0x23F8, 0x23FA}, + {0x24C2, 0x24C2}, + {0x25AA, 0x25AB}, + {0x25B6, 0x25B6}, + {0x25C0, 0x25C0}, + {0x25FB, 0x25FE}, + {0x2600, 0x2604}, + {0x260E, 0x260E}, + {0x2611, 0x2611}, + {0x2614, 0x2615}, + {0x2618, 0x2618}, + {0x261D, 0x261D}, + {0x2620, 0x2620}, + {0x2622, 0x2623}, + {0x2626, 0x2626}, + {0x262A, 0x262A}, + {0x262E, 0x262F}, + {0x2638, 0x263A}, + {0x2640, 0x2640}, + {0x2642, 0x2642}, + {0x2648, 0x2653}, + {0x265F, 0x2660}, + {0x2663, 0x2663}, + {0x2665, 0x2666}, + {0x2668, 0x2668}, + {0x267B, 0x267B}, + {0x267E, 0x267F}, + {0x2692, 0x2697}, + {0x2699, 0x2699}, + {0x269B, 0x269C}, + {0x26A0, 0x26A1}, + {0x26A7, 0x26A7}, + {0x26AA, 0x26AB}, + {0x26B0, 0x26B1}, + {0x26BD, 0x26BE}, + {0x26C4, 0x26C5}, + {0x26C8, 0x26C8}, + {0x26CE, 0x26CF}, + {0x26D1, 0x26D1}, + {0x26D3, 0x26D4}, + {0x26E9, 0x26EA}, + {0x26F0, 0x26F5}, + {0x26F7, 0x26FA}, + {0x26FD, 0x26FD}, + {0x2702, 0x2702}, + {0x2705, 0x2705}, + {0x2708, 0x270D}, + {0x270F, 0x270F}, + {0x2712, 0x2712}, + {0x2714, 0x2714}, + {0x2716, 0x2716}, + {0x271D, 0x271D}, + {0x2721, 0x2721}, + {0x2728, 0x2728}, + {0x2733, 0x2734}, + {0x2744, 0x2744}, + {0x2747, 0x2747}, + {0x274C, 0x274C}, + {0x274E, 0x274E}, + {0x2753, 0x2755}, + {0x2757, 0x2757}, + {0x2763, 0x2764}, + {0x2795, 0x2797}, + {0x27A1, 0x27A1}, + {0x27B0, 0x27B0}, + {0x27BF, 0x27BF}, + {0x2934, 0x2935}, + {0x2B05, 0x2B07}, + {0x2B1B, 0x2B1C}, + {0x2B50, 0x2B50}, + {0x2B55, 0x2B55}, + {0x3030, 0x3030}, + {0x303D, 0x303D}, + {0x3297, 0x3297}, + {0x3299, 0x3299}, + {0x1F004, 0x1F004}, + {0x1F0CF, 0x1F0CF}, + {0x1F170, 0x1F171}, + {0x1F17E, 0x1F17F}, + {0x1F18E, 0x1F18E}, + {0x1F191, 0x1F19A}, + {0x1F1E6, 0x1F1FF}, + {0x1F201, 0x1F202}, + {0x1F21A, 0x1F21A}, + {0x1F22F, 0x1F22F}, + {0x1F232, 0x1F23A}, + {0x1F250, 0x1F251}, + {0x1F300, 0x1F321}, + {0x1F324, 0x1F393}, + {0x1F396, 0x1F397}, + {0x1F399, 0x1F39B}, + {0x1F39E, 0x1F3F0}, + {0x1F3F3, 0x1F3F5}, + {0x1F3F7, 0x1F4FD}, + {0x1F4FF, 0x1F53D}, + {0x1F549, 0x1F54E}, + {0x1F550, 0x1F567}, + {0x1F56F, 0x1F570}, + {0x1F573, 0x1F57A}, + {0x1F587, 0x1F587}, + {0x1F58A, 0x1F58D}, + {0x1F590, 0x1F590}, + {0x1F595, 0x1F596}, + {0x1F5A4, 0x1F5A5}, + {0x1F5A8, 0x1F5A8}, + {0x1F5B1, 0x1F5B2}, + {0x1F5BC, 0x1F5BC}, + {0x1F5C2, 0x1F5C4}, + {0x1F5D1, 0x1F5D3}, + {0x1F5DC, 0x1F5DE}, + {0x1F5E1, 0x1F5E1}, + {0x1F5E3, 0x1F5E3}, + {0x1F5E8, 0x1F5E8}, + {0x1F5EF, 0x1F5EF}, + {0x1F5F3, 0x1F5F3}, + {0x1F5FA, 0x1F64F}, + {0x1F680, 0x1F6C5}, + {0x1F6CB, 0x1F6D2}, + {0x1F6D5, 0x1F6D7}, + {0x1F6DD, 0x1F6E5}, + {0x1F6E9, 0x1F6E9}, + {0x1F6EB, 0x1F6EC}, + {0x1F6F0, 0x1F6F0}, + {0x1F6F3, 0x1F6FC}, + {0x1F7E0, 0x1F7EB}, + {0x1F7F0, 0x1F7F0}, + {0x1F90C, 0x1F93A}, + {0x1F93C, 0x1F945}, + {0x1F947, 0x1F9FF}, + {0x1FA70, 0x1FA74}, + {0x1FA78, 0x1FA7C}, + {0x1FA80, 0x1FA86}, + {0x1FA90, 0x1FAAC}, + {0x1FAB0, 0x1FABA}, + {0x1FAC0, 0x1FAC5}, + {0x1FAD0, 0x1FAD9}, + {0x1FAE0, 0x1FAE7}, + {0x1FAF0, 0x1FAF6}, +}; + +static const struct Interval _pango2_Emoji_Presentation_table[] = +{ + {0x231A, 0x231B}, + {0x23E9, 0x23EC}, + {0x23F0, 0x23F0}, + {0x23F3, 0x23F3}, + {0x25FD, 0x25FE}, + {0x2614, 0x2615}, + {0x2648, 0x2653}, + {0x267F, 0x267F}, + {0x2693, 0x2693}, + {0x26A1, 0x26A1}, + {0x26AA, 0x26AB}, + {0x26BD, 0x26BE}, + {0x26C4, 0x26C5}, + {0x26CE, 0x26CE}, + {0x26D4, 0x26D4}, + {0x26EA, 0x26EA}, + {0x26F2, 0x26F3}, + {0x26F5, 0x26F5}, + {0x26FA, 0x26FA}, + {0x26FD, 0x26FD}, + {0x2705, 0x2705}, + {0x270A, 0x270B}, + {0x2728, 0x2728}, + {0x274C, 0x274C}, + {0x274E, 0x274E}, + {0x2753, 0x2755}, + {0x2757, 0x2757}, + {0x2795, 0x2797}, + {0x27B0, 0x27B0}, + {0x27BF, 0x27BF}, + {0x2B1B, 0x2B1C}, + {0x2B50, 0x2B50}, + {0x2B55, 0x2B55}, + {0x1F004, 0x1F004}, + {0x1F0CF, 0x1F0CF}, + {0x1F18E, 0x1F18E}, + {0x1F191, 0x1F19A}, + {0x1F1E6, 0x1F1FF}, + {0x1F201, 0x1F201}, + {0x1F21A, 0x1F21A}, + {0x1F22F, 0x1F22F}, + {0x1F232, 0x1F236}, + {0x1F238, 0x1F23A}, + {0x1F250, 0x1F251}, + {0x1F300, 0x1F320}, + {0x1F32D, 0x1F335}, + {0x1F337, 0x1F37C}, + {0x1F37E, 0x1F393}, + {0x1F3A0, 0x1F3CA}, + {0x1F3CF, 0x1F3D3}, + {0x1F3E0, 0x1F3F0}, + {0x1F3F4, 0x1F3F4}, + {0x1F3F8, 0x1F43E}, + {0x1F440, 0x1F440}, + {0x1F442, 0x1F4FC}, + {0x1F4FF, 0x1F53D}, + {0x1F54B, 0x1F54E}, + {0x1F550, 0x1F567}, + {0x1F57A, 0x1F57A}, + {0x1F595, 0x1F596}, + {0x1F5A4, 0x1F5A4}, + {0x1F5FB, 0x1F64F}, + {0x1F680, 0x1F6C5}, + {0x1F6CC, 0x1F6CC}, + {0x1F6D0, 0x1F6D2}, + {0x1F6D5, 0x1F6D7}, + {0x1F6DD, 0x1F6DF}, + {0x1F6EB, 0x1F6EC}, + {0x1F6F4, 0x1F6FC}, + {0x1F7E0, 0x1F7EB}, + {0x1F7F0, 0x1F7F0}, + {0x1F90C, 0x1F93A}, + {0x1F93C, 0x1F945}, + {0x1F947, 0x1F9FF}, + {0x1FA70, 0x1FA74}, + {0x1FA78, 0x1FA7C}, + {0x1FA80, 0x1FA86}, + {0x1FA90, 0x1FAAC}, + {0x1FAB0, 0x1FABA}, + {0x1FAC0, 0x1FAC5}, + {0x1FAD0, 0x1FAD9}, + {0x1FAE0, 0x1FAE7}, + {0x1FAF0, 0x1FAF6}, +}; + +static const struct Interval _pango2_Emoji_Modifier_table[] = +{ + {0x1F3FB, 0x1F3FF}, +}; + +static const struct Interval _pango2_Emoji_Modifier_Base_table[] = +{ + {0x261D, 0x261D}, + {0x26F9, 0x26F9}, + {0x270A, 0x270D}, + {0x1F385, 0x1F385}, + {0x1F3C2, 0x1F3C4}, + {0x1F3C7, 0x1F3C7}, + {0x1F3CA, 0x1F3CC}, + {0x1F442, 0x1F443}, + {0x1F446, 0x1F450}, + {0x1F466, 0x1F478}, + {0x1F47C, 0x1F47C}, + {0x1F481, 0x1F483}, + {0x1F485, 0x1F487}, + {0x1F48F, 0x1F48F}, + {0x1F491, 0x1F491}, + {0x1F4AA, 0x1F4AA}, + {0x1F574, 0x1F575}, + {0x1F57A, 0x1F57A}, + {0x1F590, 0x1F590}, + {0x1F595, 0x1F596}, + {0x1F645, 0x1F647}, + {0x1F64B, 0x1F64F}, + {0x1F6A3, 0x1F6A3}, + {0x1F6B4, 0x1F6B6}, + {0x1F6C0, 0x1F6C0}, + {0x1F6CC, 0x1F6CC}, + {0x1F90C, 0x1F90C}, + {0x1F90F, 0x1F90F}, + {0x1F918, 0x1F91F}, + {0x1F926, 0x1F926}, + {0x1F930, 0x1F939}, + {0x1F93C, 0x1F93E}, + {0x1F977, 0x1F977}, + {0x1F9B5, 0x1F9B6}, + {0x1F9B8, 0x1F9B9}, + {0x1F9BB, 0x1F9BB}, + {0x1F9CD, 0x1F9CF}, + {0x1F9D1, 0x1F9DD}, + {0x1FAC3, 0x1FAC5}, + {0x1FAF0, 0x1FAF6}, +}; + +static const struct Interval _pango2_Extended_Pictographic_table[] = +{ + {0x00A9, 0x00A9}, + {0x00AE, 0x00AE}, + {0x203C, 0x203C}, + {0x2049, 0x2049}, + {0x2122, 0x2122}, + {0x2139, 0x2139}, + {0x2194, 0x2199}, + {0x21A9, 0x21AA}, + {0x231A, 0x231B}, + {0x2328, 0x2328}, + {0x2388, 0x2388}, + {0x23CF, 0x23CF}, + {0x23E9, 0x23F3}, + {0x23F8, 0x23FA}, + {0x24C2, 0x24C2}, + {0x25AA, 0x25AB}, + {0x25B6, 0x25B6}, + {0x25C0, 0x25C0}, + {0x25FB, 0x25FE}, + {0x2600, 0x2605}, + {0x2607, 0x2612}, + {0x2614, 0x2685}, + {0x2690, 0x2705}, + {0x2708, 0x2712}, + {0x2714, 0x2714}, + {0x2716, 0x2716}, + {0x271D, 0x271D}, + {0x2721, 0x2721}, + {0x2728, 0x2728}, + {0x2733, 0x2734}, + {0x2744, 0x2744}, + {0x2747, 0x2747}, + {0x274C, 0x274C}, + {0x274E, 0x274E}, + {0x2753, 0x2755}, + {0x2757, 0x2757}, + {0x2763, 0x2767}, + {0x2795, 0x2797}, + {0x27A1, 0x27A1}, + {0x27B0, 0x27B0}, + {0x27BF, 0x27BF}, + {0x2934, 0x2935}, + {0x2B05, 0x2B07}, + {0x2B1B, 0x2B1C}, + {0x2B50, 0x2B50}, + {0x2B55, 0x2B55}, + {0x3030, 0x3030}, + {0x303D, 0x303D}, + {0x3297, 0x3297}, + {0x3299, 0x3299}, + {0x1F000, 0x1F0FF}, + {0x1F10D, 0x1F10F}, + {0x1F12F, 0x1F12F}, + {0x1F16C, 0x1F171}, + {0x1F17E, 0x1F17F}, + {0x1F18E, 0x1F18E}, + {0x1F191, 0x1F19A}, + {0x1F1AD, 0x1F1E5}, + {0x1F201, 0x1F20F}, + {0x1F21A, 0x1F21A}, + {0x1F22F, 0x1F22F}, + {0x1F232, 0x1F23A}, + {0x1F23C, 0x1F23F}, + {0x1F249, 0x1F3FA}, + {0x1F400, 0x1F53D}, + {0x1F546, 0x1F64F}, + {0x1F680, 0x1F6FF}, + {0x1F774, 0x1F77F}, + {0x1F7D5, 0x1F7FF}, + {0x1F80C, 0x1F80F}, + {0x1F848, 0x1F84F}, + {0x1F85A, 0x1F85F}, + {0x1F888, 0x1F88F}, + {0x1F8AE, 0x1F8FF}, + {0x1F90C, 0x1F93A}, + {0x1F93C, 0x1F945}, + {0x1F947, 0x1FAFF}, + {0x1FC00, 0x1FFFD}, +}; + +#endif /* PANGO2_EMOJI_TABLE_H */ + +/* == End of generated table == */ diff --git a/pango2/pango-emoji.c b/pango2/pango-emoji.c new file mode 100644 index 00000000..aa82e9ec --- /dev/null +++ b/pango2/pango-emoji.c @@ -0,0 +1,290 @@ +/* Pango2 + * pango-emoji.c: Emoji handling + * + * Copyright (C) 2017 Google, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Implementation of pango2_emoji_iter is based on Chromium's Ragel-based + * parser: + * + * https://chromium-review.googlesource.com/c/chromium/src/+/1264577 + * + * The grammar file emoji_presentation_scanner.rl was just modified to + * adapt the function signature and variables to our usecase. The + * grammar itself was NOT modified: + * + * https://chromium-review.googlesource.com/c/chromium/src/+/1264577/3/third_party/blink/renderer/platform/fonts/emoji_presentation_scanner.rl + * + * The emoji_presentation_scanner.c is generated from .rl file by + * running ragel on it. + * + * The categorization is also based on: + * + * https://chromium-review.googlesource.com/c/chromium/src/+/1264577/3/third_party/blink/renderer/platform/fonts/utf16_ragel_iterator.h + * + * The iterator next() is based on: + * + * https://chromium-review.googlesource.com/c/chromium/src/+/1264577/3/third_party/blink/renderer/platform/fonts/symbols_iterator.cc + * + * // Copyright 2015 The Chromium Authors. All rights reserved. + * // Use of this source code is governed by a BSD-style license that can be + * // found in the LICENSE file. + */ + +#include "config.h" +#include <stdlib.h> +#include <string.h> + +#include "pango-emoji-private.h" +#include "pango-emoji-table.h" + +static inline gboolean +bsearch_interval (gunichar c, + const struct Interval table[], + guint n) +{ + guint lower = 0; + guint upper = n - 1; + + while (lower <= upper) + { + int mid = (lower + upper) / 2; + + if (c < table[mid].start) + upper = mid - 1; + else if (c > table[mid].end) + lower = mid + 1; + else + return TRUE; + } + + return FALSE; +} + +#define DEFINE_pango2_Is_(name) \ +static inline gboolean \ +_pango2_Is_##name (gunichar ch) \ +{ \ + return ch >= _pango2_##name##_table[0].start && \ + bsearch_interval (ch, \ + _pango2_##name##_table, \ + G_N_ELEMENTS (_pango2_##name##_table)); \ +} + +DEFINE_pango2_Is_(Emoji) +DEFINE_pango2_Is_(Emoji_Presentation) +DEFINE_pango2_Is_(Emoji_Modifier) +DEFINE_pango2_Is_(Emoji_Modifier_Base) +DEFINE_pango2_Is_(Extended_Pictographic) + +gboolean +_pango2_Is_Emoji_Base_Character (gunichar ch) +{ + return _pango2_Is_Emoji (ch); +} + +gboolean +_pango2_Is_Emoji_Extended_Pictographic (gunichar ch) +{ + return _pango2_Is_Extended_Pictographic (ch); +} + +static inline gboolean +_pango2_Is_Emoji_Emoji_Default (gunichar ch) +{ + return _pango2_Is_Emoji_Presentation (ch); +} + +static inline gboolean +_pango2_Is_Emoji_Keycap_Base (gunichar ch) +{ + return (ch >= '0' && ch <= '9') || ch == '#' || ch == '*'; +} + +static inline gboolean +_pango2_Is_Regional_Indicator (gunichar ch) +{ + return (ch >= 0x1F1E6 && ch <= 0x1F1FF); +} + + +#define kCombiningEnclosingCircleBackslashCharacter 0x20E0 +#define kCombiningEnclosingKeycapCharacter 0x20E3 +#define kVariationSelector15Character 0xFE0E +#define kVariationSelector16Character 0xFE0F +#define kZeroWidthJoinerCharacter 0x200D + +enum Pango2EmojiScannerCategory { + EMOJI = 0, + EMOJI_TEXT_PRESENTATION = 1, + EMOJI_EMOJI_PRESENTATION = 2, + EMOJI_MODIFIER_BASE = 3, + EMOJI_MODIFIER = 4, + EMOJI_VS_BASE = 5, + REGIONAL_INDICATOR = 6, + KEYCAP_BASE = 7, + COMBINING_ENCLOSING_KEYCAP = 8, + COMBINING_ENCLOSING_CIRCLE_BACKSLASH = 9, + ZWJ = 10, + VS15 = 11, + VS16 = 12, + TAG_BASE = 13, + TAG_SEQUENCE = 14, + TAG_TERM = 15, + kMaxEmojiScannerCategory = 16 +}; + +static inline unsigned char +_pango2_EmojiSegmentationCategory (gunichar codepoint) +{ + /* Specific ones first. */ + if (('a' <= codepoint && codepoint <= 'z') || + ('A' <= codepoint && codepoint <= 'Z') || + codepoint == ' ') + return kMaxEmojiScannerCategory; + + if ('0' <= codepoint && codepoint <= '9') + return KEYCAP_BASE; + + switch (codepoint) + { + case kCombiningEnclosingKeycapCharacter: + return COMBINING_ENCLOSING_KEYCAP; + case kCombiningEnclosingCircleBackslashCharacter: + return COMBINING_ENCLOSING_CIRCLE_BACKSLASH; + case kZeroWidthJoinerCharacter: + return ZWJ; + case kVariationSelector15Character: + return VS15; + case kVariationSelector16Character: + return VS16; + case 0x1F3F4: + return TAG_BASE; + case 0xE007F: + return TAG_TERM; + default: ; + } + + if ((0xE0030 <= codepoint && codepoint <= 0xE0039) || + (0xE0061 <= codepoint && codepoint <= 0xE007A)) + return TAG_SEQUENCE; + + if (_pango2_Is_Emoji_Modifier_Base (codepoint)) + return EMOJI_MODIFIER_BASE; + if (_pango2_Is_Emoji_Modifier (codepoint)) + return EMOJI_MODIFIER; + if (_pango2_Is_Regional_Indicator (codepoint)) + return REGIONAL_INDICATOR; + if (_pango2_Is_Emoji_Keycap_Base (codepoint)) + return KEYCAP_BASE; + if (_pango2_Is_Emoji_Emoji_Default (codepoint)) + return EMOJI_EMOJI_PRESENTATION; + if (_pango2_Is_Emoji (codepoint)) + return EMOJI_TEXT_PRESENTATION; + + /* Ragel state machine will interpret unknown category as "any". */ + return kMaxEmojiScannerCategory; +} + + +typedef gboolean bool; +enum { false = FALSE, true = TRUE }; +typedef unsigned char *emoji_text_iter_t; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wswitch-default" +#include "emoji_presentation_scanner.c" +#pragma GCC diagnostic pop + + +Pango2EmojiIter * +_pango2_emoji_iter_init (Pango2EmojiIter *iter, + const char *text, + int length) +{ + unsigned int n_chars = g_utf8_strlen (text, length); + unsigned char *types = g_malloc (n_chars); + unsigned int i; + const char *p; + + p = text; + for (i = 0; i < n_chars; i++) + { + types[i] = _pango2_EmojiSegmentationCategory (g_utf8_get_char (p)); + p = g_utf8_next_char (p); + } + + iter->text_start = iter->start = iter->end = text; + if (length >= 0) + iter->text_end = text + length; + else + iter->text_end = text + strlen (text); + iter->is_emoji = FALSE; + + iter->types = types; + iter->n_chars = n_chars; + iter->cursor = 0; + + _pango2_emoji_iter_next (iter); + + return iter; +} + +void +_pango2_emoji_iter_fini (Pango2EmojiIter *iter) +{ + g_free (iter->types); +} + +gboolean +_pango2_emoji_iter_next (Pango2EmojiIter *iter) +{ + unsigned int old_cursor, cursor; + gboolean is_emoji; + + if (iter->end >= iter->text_end) + return FALSE; + + iter->start = iter->end; + + old_cursor = cursor = iter->cursor; + cursor = scan_emoji_presentation (iter->types + cursor, + iter->types + iter->n_chars, + &is_emoji) - iter->types; + do + { + iter->cursor = cursor; + iter->is_emoji = is_emoji; + + if (cursor == iter->n_chars) + break; + + cursor = scan_emoji_presentation (iter->types + cursor, + iter->types + iter->n_chars, + &is_emoji) - iter->types; + } + while (iter->is_emoji == is_emoji); + + iter->end = g_utf8_offset_to_pointer (iter->start, iter->cursor - old_cursor); + + return TRUE; +} + + +/********************************************************** + * End of code from Chromium + **********************************************************/ diff --git a/pango2/pango-enum-types.c.template b/pango2/pango-enum-types.c.template new file mode 100644 index 00000000..b71b0a39 --- /dev/null +++ b/pango2/pango-enum-types.c.template @@ -0,0 +1,38 @@ +/*** BEGIN file-header ***/ +#include "config.h" + +#include <pango.h> + +/*** END file-header ***/ + +/*** BEGIN file-production ***/ +/* enumerations from "@basename@" */ +/*** END file-production ***/ + +/*** BEGIN value-header ***/ +GType +@enum_name@_get_type (void) +{ + static gsize g_define_type_id__volatile = 0; + + if (g_once_init_enter (&g_define_type_id__volatile)) + { + static const G@Type@Value values[] = { +/*** END value-header ***/ + +/*** BEGIN value-production ***/ + { @VALUENAME@, "@VALUENAME@", "@valuenick@" }, +/*** END value-production ***/ + +/*** BEGIN value-tail ***/ + { 0, NULL, NULL } + }; + GType g_define_type_id = + g_@type@_register_static (g_intern_static_string ("@EnumName@"), values); + g_once_init_leave (&g_define_type_id__volatile, g_define_type_id); + } + + return g_define_type_id__volatile; +} + +/*** END value-tail ***/ diff --git a/pango2/pango-enum-types.h.template b/pango2/pango-enum-types.h.template new file mode 100644 index 00000000..ff369b96 --- /dev/null +++ b/pango2/pango-enum-types.h.template @@ -0,0 +1,25 @@ +/*** BEGIN file-header ***/ +#pragma once + +#include <glib-object.h> + +#include <pango2/pango-version-macros.h> + +G_BEGIN_DECLS +/*** END file-header ***/ + +/*** BEGIN file-production ***/ + +/* enumerations from "@basename@" */ +/*** END file-production ***/ + +/*** BEGIN value-header ***/ +PANGO2_AVAILABLE_IN_ALL +GType @enum_name@_get_type (void) G_GNUC_CONST; +#define @ENUMPREFIX@_TYPE_@ENUMSHORT@ (@enum_name@_get_type ()) +/*** END value-header ***/ + +/*** BEGIN file-tail ***/ +G_END_DECLS + +/*** END file-tail ***/ diff --git a/pango2/pango-features.h.meson b/pango2/pango-features.h.meson new file mode 100644 index 00000000..d89f5ad8 --- /dev/null +++ b/pango2/pango-features.h.meson @@ -0,0 +1,9 @@ +#pragma once + +#mesondefine PANGO2_VERSION_MAJOR +#mesondefine PANGO2_VERSION_MINOR +#mesondefine PANGO2_VERSION_MICRO + +#define PANGO2_VERSION_STRING "@PANGO2_VERSION_MAJOR@.@PANGO2_VERSION_MINOR@.@PANGO2_VERSION_MICRO@" +# +#mesondefine PANGO2_RENDERING_CAIRO diff --git a/pango2/pango-font-description-private.h b/pango2/pango-font-description-private.h new file mode 100644 index 00000000..11b09d00 --- /dev/null +++ b/pango2/pango-font-description-private.h @@ -0,0 +1,43 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-font-description.h" + + +gboolean pango2_font_description_is_similar (const Pango2FontDescription *a, + const Pango2FontDescription *b); + +int pango2_font_description_compute_distance (const Pango2FontDescription *a, + const Pango2FontDescription *b); + +gboolean pango2_parse_style (const char *str, + Pango2Style *style, + gboolean warn); +gboolean pango2_parse_variant (const char *str, + Pango2Variant *variant, + gboolean warn); +gboolean pango2_parse_weight (const char *str, + Pango2Weight *weight, + gboolean warn); +gboolean pango2_parse_stretch (const char *str, + Pango2Stretch *stretch, + gboolean warn); + diff --git a/pango2/pango-font-description.c b/pango2/pango-font-description.c new file mode 100644 index 00000000..db22853b --- /dev/null +++ b/pango2/pango-font-description.c @@ -0,0 +1,1743 @@ +/* Pango2 + * pango-font-description.c: + * + * Copyright (C) 1999 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include <stdlib.h> + +#include "pango-font-description-private.h" + +struct _Pango2FontDescription +{ + char *family_name; + + Pango2Style style; + Pango2Variant variant; + Pango2Weight weight; + Pango2Stretch stretch; + Pango2Gravity gravity; + + int size; + + char *variations; + char *faceid; + + guint16 mask; + guint static_family : 1; + guint static_variations : 1; + guint static_faceid : 1; + guint size_is_absolute : 1; +}; + +G_DEFINE_BOXED_TYPE (Pango2FontDescription, pango2_font_description, + pango2_font_description_copy, + pango2_font_description_free); + +static const Pango2FontDescription pfd_defaults = { + NULL, /* family_name */ + + PANGO2_STYLE_NORMAL, /* style */ + PANGO2_VARIANT_NORMAL, /* variant */ + PANGO2_WEIGHT_NORMAL, /* weight */ + PANGO2_STRETCH_NORMAL, /* stretch */ + PANGO2_GRAVITY_SOUTH, /* gravity */ + 0, /* size */ + NULL, /* variations */ + NULL, /* faceid */ + + 0, /* mask */ + 0, /* static_family */ + 0, /* static_variations */ + 0, /* static_faceid */ + 0, /* size_is_absolute */ +}; + +/** + * pango2_font_description_new: + * + * Creates a new font description structure with all fields unset. + * + * Return value: the newly allocated `Pango2FontDescription`, which + * should be freed using [method@Pango2.FontDescription.free] + */ +Pango2FontDescription * +pango2_font_description_new (void) +{ + Pango2FontDescription *desc = g_slice_new (Pango2FontDescription); + + *desc = pfd_defaults; + + return desc; +} + +/** + * pango2_font_description_set_family: + * @desc: a `Pango2FontDescription`. + * @family: a string representing the family name. + * + * Sets the family name field of a font description. + * + * The family + * name represents a family of related font styles, and will + * resolve to a particular `Pango2FontFamily`. In some uses of + * `Pango2FontDescription`, it is also possible to use a comma + * separated list of family names for this field. + */ +void +pango2_font_description_set_family (Pango2FontDescription *desc, + const char *family) +{ + g_return_if_fail (desc != NULL); + + pango2_font_description_set_family_static (desc, family ? g_strdup (family) : NULL); + if (family) + desc->static_family = FALSE; +} + +/** + * pango2_font_description_set_family_static: + * @desc: a `Pango2FontDescription` + * @family: a string representing the family name + * + * Sets the family name field of a font description, without copying the string. + * + * This is like [method@Pango2.FontDescription.set_family], except that no + * copy of @family is made. The caller must make sure that the + * string passed in stays around until @desc has been freed or the + * name is set again. This function can be used if @family is a static + * string such as a C string literal, or if @desc is only needed temporarily. + */ +void +pango2_font_description_set_family_static (Pango2FontDescription *desc, + const char *family) +{ + g_return_if_fail (desc != NULL); + + if (desc->family_name == family) + return; + + if (desc->family_name && !desc->static_family) + g_free (desc->family_name); + + if (family) + { + desc->family_name = (char *)family; + desc->static_family = TRUE; + desc->mask |= PANGO2_FONT_MASK_FAMILY; + } + else + { + desc->family_name = pfd_defaults.family_name; + desc->static_family = pfd_defaults.static_family; + desc->mask &= ~PANGO2_FONT_MASK_FAMILY; + } +} + +/** + * pango2_font_description_get_family: + * @desc: a `Pango2FontDescription`. + * + * Gets the family name field of a font description. + * + * See [method@Pango2.FontDescription.set_family]. + * + * Return value: (nullable): the family name field for the font + * description, or %NULL if not previously set. This has the same + * life-time as the font description itself and should not be freed. + */ +const char * +pango2_font_description_get_family (const Pango2FontDescription *desc) +{ + g_return_val_if_fail (desc != NULL, NULL); + + return desc->family_name; +} + +/** + * pango2_font_description_set_style: + * @desc: a `Pango2FontDescription` + * @style: the style for the font description + * + * Sets the style field of a `Pango2FontDescription`. + * + * The [enum@Pango2.Style] enumeration describes whether the font is + * slanted and the manner in which it is slanted; it can be either + * %PANGO2_STYLE_NORMAL, %PANGO2_STYLE_ITALIC, or %PANGO2_STYLE_OBLIQUE. + * + * Most fonts will either have a italic style or an oblique style, + * but not both, and font matching in Pango2 will match italic + * specifications with oblique fonts and vice-versa if an exact + * match is not found. + */ +void +pango2_font_description_set_style (Pango2FontDescription *desc, + Pango2Style style) +{ + g_return_if_fail (desc != NULL); + + desc->style = style; + desc->mask |= PANGO2_FONT_MASK_STYLE; +} + +/** + * pango2_font_description_get_style: + * @desc: a `Pango2FontDescription` + * + * Gets the style field of a `Pango2FontDescription`. + * + * See [method@Pango2.FontDescription.set_style]. + * + * Return value: the style field for the font description. + * Use [method@Pango2.FontDescription.get_set_fields] to + * find out if the field was explicitly set or not. + */ +Pango2Style +pango2_font_description_get_style (const Pango2FontDescription *desc) +{ + g_return_val_if_fail (desc != NULL, pfd_defaults.style); + + return desc->style; +} + +/** + * pango2_font_description_set_variant: + * @desc: a `Pango2FontDescription` + * @variant: the variant type for the font description. + * + * Sets the variant field of a font description. + * + * The [enum@Pango2.Variant] can either be %PANGO2_VARIANT_NORMAL + * or %PANGO2_VARIANT_SMALL_CAPS. + */ +void +pango2_font_description_set_variant (Pango2FontDescription *desc, + Pango2Variant variant) +{ + g_return_if_fail (desc != NULL); + + desc->variant = variant; + desc->mask |= PANGO2_FONT_MASK_VARIANT; +} + +/** + * pango2_font_description_get_variant: + * @desc: a `Pango2FontDescription`. + * + * Gets the variant field of a `Pango2FontDescription`. + * + * See [method@Pango2.FontDescription.set_variant]. + * + * Return value: the variant field for the font description. + * Use [method@Pango2.FontDescription.get_set_fields] to find + * out if the field was explicitly set or not. + */ +Pango2Variant +pango2_font_description_get_variant (const Pango2FontDescription *desc) +{ + g_return_val_if_fail (desc != NULL, pfd_defaults.variant); + + return desc->variant; +} + +/** + * pango2_font_description_set_weight: + * @desc: a `Pango2FontDescription` + * @weight: the weight for the font description. + * + * Sets the weight field of a font description. + * + * The weight field + * specifies how bold or light the font should be. In addition + * to the values of the [enum@Pango2.Weight] enumeration, other + * intermediate numeric values are possible. + */ +void +pango2_font_description_set_weight (Pango2FontDescription *desc, + Pango2Weight weight) +{ + g_return_if_fail (desc != NULL); + + desc->weight = weight; + desc->mask |= PANGO2_FONT_MASK_WEIGHT; +} + +/** + * pango2_font_description_get_weight: + * @desc: a `Pango2FontDescription` + * + * Gets the weight field of a font description. + * + * See [method@Pango2.FontDescription.set_weight]. + * + * Return value: the weight field for the font description. + * Use [method@Pango2.FontDescription.get_set_fields] to find + * out if the field was explicitly set or not. + */ +Pango2Weight +pango2_font_description_get_weight (const Pango2FontDescription *desc) +{ + g_return_val_if_fail (desc != NULL, pfd_defaults.weight); + + return desc->weight; +} + +/** + * pango2_font_description_set_stretch: + * @desc: a `Pango2FontDescription` + * @stretch: the stretch for the font description + * + * Sets the stretch field of a font description. + * + * The [enum@Pango2.Stretch] field specifies how narrow or + * wide the font should be. + */ +void +pango2_font_description_set_stretch (Pango2FontDescription *desc, + Pango2Stretch stretch) +{ + g_return_if_fail (desc != NULL); + + desc->stretch = stretch; + desc->mask |= PANGO2_FONT_MASK_STRETCH; +} + +/** + * pango2_font_description_get_stretch: + * @desc: a `Pango2FontDescription`. + * + * Gets the stretch field of a font description. + * + * See [method@Pango2.FontDescription.set_stretch]. + * + * Return value: the stretch field for the font description. + * Use [method@Pango2.FontDescription.get_set_fields] to find + * out if the field was explicitly set or not. + */ +Pango2Stretch +pango2_font_description_get_stretch (const Pango2FontDescription *desc) +{ + g_return_val_if_fail (desc != NULL, pfd_defaults.stretch); + + return desc->stretch; +} + +/** + * pango2_font_description_set_size: + * @desc: a `Pango2FontDescription` + * @size: the size of the font in points, scaled by %PANGO2_SCALE. + * (That is, a @size value of 10 * PANGO2_SCALE is a 10 point font. + * The conversion factor between points and device units depends on + * system configuration and the output device. For screen display, a + * logical DPI of 96 is common, in which case a 10 point font corresponds + * to a 10 * (96 / 72) = 13.3 pixel font. + * Use [method@Pango2.FontDescription.set_absolute_size] if you need + * a particular size in device units. + * + * Sets the size field of a font description in fractional points. + * + * This is mutually exclusive with + * [method@Pango2.FontDescription.set_absolute_size]. + */ +void +pango2_font_description_set_size (Pango2FontDescription *desc, + int size) +{ + g_return_if_fail (desc != NULL); + g_return_if_fail (size >= 0); + + desc->size = size; + desc->size_is_absolute = FALSE; + desc->mask |= PANGO2_FONT_MASK_SIZE; +} + +/** + * pango2_font_description_get_size: + * @desc: a `Pango2FontDescription` + * + * Gets the size field of a font description. + * + * See [method@Pango2.FontDescription.set_size]. + * + * Return value: the size field for the font description in points + * or device units. You must call + * [method@Pango2.FontDescription.get_size_is_absolute] to find out + * which is the case. Returns 0 if the size field has not previously + * been set or it has been set to 0 explicitly. + * Use [method@Pango2.FontDescription.get_set_fields] to find out + * if the field was explicitly set or not. + */ +int +pango2_font_description_get_size (const Pango2FontDescription *desc) +{ + g_return_val_if_fail (desc != NULL, pfd_defaults.size); + + return desc->size; +} + +/** + * pango2_font_description_set_absolute_size: + * @desc: a `Pango2FontDescription` + * @size: the new size, in Pango2 units. There are %PANGO2_SCALE Pango2 units + * in one device unit. For an output backend where a device unit is a pixel, + * a @size value of 10 * PANGO2_SCALE gives a 10 pixel font. + * + * Sets the size field of a font description, in device units. + * + * This is mutually exclusive with [method@Pango2.FontDescription.set_size] + * which sets the font size in points. + */ +void +pango2_font_description_set_absolute_size (Pango2FontDescription *desc, + double size) +{ + g_return_if_fail (desc != NULL); + g_return_if_fail (size >= 0); + + desc->size = size; + desc->size_is_absolute = TRUE; + desc->mask |= PANGO2_FONT_MASK_SIZE; +} + +/** + * pango2_font_description_get_size_is_absolute: + * @desc: a `Pango2FontDescription` + * + * Determines whether the size of the font is in points (not absolute) + * or device units (absolute). + * + * See [method@Pango2.FontDescription.set_size] + * and [method@Pango2.FontDescription.set_absolute_size]. + * + * Return value: whether the size for the font description is in + * points or device units. Use [method@Pango2.FontDescription.get_set_fields] + * to find out if the size field of the font description was explicitly + * set or not. + */ +gboolean +pango2_font_description_get_size_is_absolute (const Pango2FontDescription *desc) +{ + g_return_val_if_fail (desc != NULL, pfd_defaults.size_is_absolute); + + return desc->size_is_absolute; +} + +/** + * pango2_font_description_set_gravity: + * @desc: a `Pango2FontDescription` + * @gravity: the gravity for the font description. + * + * Sets the gravity field of a font description. + * + * The gravity field + * specifies how the glyphs should be rotated. If @gravity is + * %PANGO2_GRAVITY_AUTO, this actually unsets the gravity mask on + * the font description. + * + * This function is seldom useful to the user. Gravity should normally + * be set on a `Pango2Context`. + */ +void +pango2_font_description_set_gravity (Pango2FontDescription *desc, + Pango2Gravity gravity) +{ + g_return_if_fail (desc != NULL); + + if (gravity == PANGO2_GRAVITY_AUTO) + { + pango2_font_description_unset_fields (desc, PANGO2_FONT_MASK_GRAVITY); + return; + } + + desc->gravity = gravity; + desc->mask |= PANGO2_FONT_MASK_GRAVITY; +} + +/** + * pango2_font_description_get_gravity: + * @desc: a `Pango2FontDescription` + * + * Gets the gravity field of a font description. + * + * See [method@Pango2.FontDescription.set_gravity]. + * + * Return value: the gravity field for the font description. + * Use [method@Pango2.FontDescription.get_set_fields] to find out + * if the field was explicitly set or not. + */ +Pango2Gravity +pango2_font_description_get_gravity (const Pango2FontDescription *desc) +{ + g_return_val_if_fail (desc != NULL, pfd_defaults.gravity); + + return desc->gravity; +} + +/** + * pango2_font_description_set_variations_static: + * @desc: a `Pango2FontDescription` + * @variations: a string representing the variations + * + * Sets the variations field of a font description. + * + * This is like [method@Pango2.FontDescription.set_variations], except + * that no copy of @variations is made. The caller must make sure that + * the string passed in stays around until @desc has been freed + * or the name is set again. This function can be used if + * @variations is a static string such as a C string literal, + * or if @desc is only needed temporarily. + */ +void +pango2_font_description_set_variations_static (Pango2FontDescription *desc, + const char *variations) +{ + g_return_if_fail (desc != NULL); + + if (desc->variations == variations) + return; + + if (desc->variations && !desc->static_variations) + g_free (desc->variations); + + if (variations) + { + desc->variations = (char *)variations; + desc->static_variations = TRUE; + desc->mask |= PANGO2_FONT_MASK_VARIATIONS; + } + else + { + desc->variations = pfd_defaults.variations; + desc->static_variations = pfd_defaults.static_variations; + desc->mask &= ~PANGO2_FONT_MASK_VARIATIONS; + } +} + +/** + * pango2_font_description_set_variations: + * @desc: a `Pango2FontDescription`. + * @variations: (nullable): a string representing the variations + * + * Sets the variations field of a font description. + * + * OpenType font variations allow to select a font instance by + * specifying values for a number of axes, such as width or weight. + * + * The format of the variations string is + * + * AXIS1=VALUE,AXIS2=VALUE... + * + * with each AXIS a 4 character tag that identifies a font axis, + * and each VALUE a floating point number. Unknown axes are ignored, + * and values are clamped to their allowed range. + * + * Pango2 does not currently have a way to find supported axes of + * a font. Both harfbuzz and freetype have API for this. See + * for example [hb_ot_var_get_axis_infos](https://harfbuzz.github.io/harfbuzz-hb-ot-var.html#hb-ot-var-get-axis-infos). + */ +void +pango2_font_description_set_variations (Pango2FontDescription *desc, + const char *variations) +{ + g_return_if_fail (desc != NULL); + + pango2_font_description_set_variations_static (desc, g_strdup (variations)); + if (variations) + desc->static_variations = FALSE; +} + +/** + * pango2_font_description_get_variations: + * @desc: a `Pango2FontDescription` + * + * Gets the variations field of a font description. + * + * See [method@Pango2.FontDescription.set_variations]. + * + * Return value: (nullable): the variations field for the font + * description, or %NULL if not previously set. This has the same + * life-time as the font description itself and should not be freed. + */ +const char * +pango2_font_description_get_variations (const Pango2FontDescription *desc) +{ + g_return_val_if_fail (desc != NULL, NULL); + + return desc->variations; +} + +/** + * pango2_font_description_get_set_fields: + * @desc: a `Pango2FontDescription` + * + * Determines which fields in a font description have been set. + * + * Return value: a bitmask with bits set corresponding to the + * fields in @desc that have been set. + */ +Pango2FontMask +pango2_font_description_get_set_fields (const Pango2FontDescription *desc) +{ + g_return_val_if_fail (desc != NULL, pfd_defaults.mask); + + return desc->mask; +} + +/** + * pango2_font_description_unset_fields: + * @desc: a `Pango2FontDescription` + * @to_unset: bitmask of fields in the @desc to unset. + * + * Unsets some of the fields in a `Pango2FontDescription`. + * + * The unset fields will get back to their default values. + */ +void +pango2_font_description_unset_fields (Pango2FontDescription *desc, + Pango2FontMask to_unset) +{ + Pango2FontDescription unset_desc; + + g_return_if_fail (desc != NULL); + + unset_desc = pfd_defaults; + unset_desc.mask = to_unset; + + pango2_font_description_merge_static (desc, &unset_desc, TRUE); + + desc->mask &= ~to_unset; +} + +/** + * pango2_font_description_merge: + * @desc: a `Pango2FontDescription` + * @desc_to_merge: (nullable): the `Pango2FontDescription` to merge from, + * or %NULL + * @replace_existing: if %TRUE, replace fields in @desc with the + * corresponding values from @desc_to_merge, even if they + * are already exist. + * + * Merges the fields that are set in @desc_to_merge into the fields in + * @desc. + * + * If @replace_existing is %FALSE, only fields in @desc that + * are not already set are affected. If %TRUE, then fields that are + * already set will be replaced as well. + * + * If @desc_to_merge is %NULL, this function performs nothing. + */ +void +pango2_font_description_merge (Pango2FontDescription *desc, + const Pango2FontDescription *desc_to_merge, + gboolean replace_existing) +{ + gboolean family_merged; + gboolean variations_merged; + gboolean faceid_merged; + + g_return_if_fail (desc != NULL); + + if (desc_to_merge == NULL) + return; + + family_merged = desc_to_merge->family_name && (replace_existing || !desc->family_name); + variations_merged = desc_to_merge->variations && (replace_existing || !desc->variations); + faceid_merged = desc_to_merge->faceid && (replace_existing || !desc->faceid); + + pango2_font_description_merge_static (desc, desc_to_merge, replace_existing); + + if (family_merged) + { + desc->family_name = g_strdup (desc->family_name); + desc->static_family = FALSE; + } + + if (variations_merged) + { + desc->variations = g_strdup (desc->variations); + desc->static_variations = FALSE; + } + + if (faceid_merged) + { + desc->faceid = g_strdup (desc->faceid); + desc->static_faceid = FALSE; + } +} + +/** + * pango2_font_description_merge_static: + * @desc: a `Pango2FontDescription` + * @desc_to_merge: the `Pango2FontDescription` to merge from + * @replace_existing: if %TRUE, replace fields in @desc with the + * corresponding values from @desc_to_merge, even if they + * are already exist. + * + * Merges the fields that are set in @desc_to_merge into the fields in + * @desc, without copying allocated fields. + * + * This is like [method@Pango2.FontDescription.merge], but only a shallow copy + * is made of the family name and other allocated fields. @desc can only + * be used until @desc_to_merge is modified or freed. This is meant to + * be used when the merged font description is only needed temporarily. + */ +void +pango2_font_description_merge_static (Pango2FontDescription *desc, + const Pango2FontDescription *desc_to_merge, + gboolean replace_existing) +{ + Pango2FontMask new_mask; + + g_return_if_fail (desc != NULL); + g_return_if_fail (desc_to_merge != NULL); + + if (replace_existing) + new_mask = desc_to_merge->mask; + else + new_mask = desc_to_merge->mask & ~desc->mask; + + if (new_mask & PANGO2_FONT_MASK_FAMILY) + pango2_font_description_set_family_static (desc, desc_to_merge->family_name); + if (new_mask & PANGO2_FONT_MASK_STYLE) + desc->style = desc_to_merge->style; + if (new_mask & PANGO2_FONT_MASK_VARIANT) + desc->variant = desc_to_merge->variant; + if (new_mask & PANGO2_FONT_MASK_WEIGHT) + desc->weight = desc_to_merge->weight; + if (new_mask & PANGO2_FONT_MASK_STRETCH) + desc->stretch = desc_to_merge->stretch; + if (new_mask & PANGO2_FONT_MASK_SIZE) + { + desc->size = desc_to_merge->size; + desc->size_is_absolute = desc_to_merge->size_is_absolute; + } + if (new_mask & PANGO2_FONT_MASK_GRAVITY) + desc->gravity = desc_to_merge->gravity; + if (new_mask & PANGO2_FONT_MASK_VARIATIONS) + pango2_font_description_set_variations_static (desc, desc_to_merge->variations); + if (new_mask & PANGO2_FONT_MASK_FACEID) + pango2_font_description_set_faceid_static (desc, desc_to_merge->faceid); + + desc->mask |= new_mask; +} + +gboolean +pango2_font_description_is_similar (const Pango2FontDescription *a, + const Pango2FontDescription *b) +{ + return a->variant == b->variant && + a->gravity == b->gravity; +} + +int +pango2_font_description_compute_distance (const Pango2FontDescription *a, + const Pango2FontDescription *b) +{ + if (a->style == b->style) + { + return abs ((int)(a->weight) - (int)(b->weight)) + + abs ((int)(a->stretch) - (int)(b->stretch)); + } + else if (a->style != PANGO2_STYLE_NORMAL && + b->style != PANGO2_STYLE_NORMAL) + { + /* Equate oblique and italic, but with a big penalty + */ + return 1000000 + abs ((int)(a->weight) - (int)(b->weight)) + + abs ((int)(a->stretch) - (int)(b->stretch)); + } + else + return G_MAXINT; +} + +/** + * pango2_font_description_copy: + * @desc: (nullable): a `Pango2FontDescription`, may be %NULL + * + * Make a copy of a `Pango2FontDescription`. + * + * Return value: (nullable): the newly allocated `Pango2FontDescription`, + * which should be freed with [method@Pango2.FontDescription.free] + */ +Pango2FontDescription * +pango2_font_description_copy (const Pango2FontDescription *desc) +{ + Pango2FontDescription *result; + + if (desc == NULL) + return NULL; + + result = g_slice_new (Pango2FontDescription); + + *result = *desc; + + if (result->family_name) + { + result->family_name = g_strdup (result->family_name); + result->static_family = FALSE; + } + + result->variations = g_strdup (result->variations); + result->static_variations = FALSE; + + result->faceid = g_strdup (result->faceid); + result->static_faceid = FALSE; + + return result; +} + +/** + * pango2_font_description_copy_static: + * @desc: (nullable): a `Pango2FontDescription`, may be %NULL + * + * Make a copy of a `Pango2FontDescription`, but don't duplicate + * allocated fields. + * + * This is like [method@Pango2.FontDescription.copy], but only a shallow + * copy is made of the family name and other allocated fields. The result + * can only be used until @desc is modified or freed. This is meant + * to be used when the copy is only needed temporarily. + * + * Return value: (nullable): the newly allocated `Pango2FontDescription`, + * which should be freed with [method@Pango2.FontDescription.free] + */ +Pango2FontDescription * +pango2_font_description_copy_static (const Pango2FontDescription *desc) +{ + Pango2FontDescription *result; + + if (desc == NULL) + return NULL; + + result = g_slice_new (Pango2FontDescription); + + *result = *desc; + if (result->family_name) + result->static_family = TRUE; + + if (result->variations) + result->static_variations = TRUE; + + if (result->faceid) + result->static_faceid = TRUE; + + return result; +} + +/** + * pango2_font_description_equal: + * @desc1: a `Pango2FontDescription` + * @desc2: another `Pango2FontDescription` + * + * Compares two font descriptions for equality. + * + * Two font descriptions are considered equal if the fonts they describe + * are provably identical. This means that their masks do not have to match, + * as long as other fields are all the same. (Two font descriptions may + * result in identical fonts being loaded, but still compare %FALSE.) + * + * Return value: %TRUE if the two font descriptions are identical, + * %FALSE otherwise. + */ +gboolean +pango2_font_description_equal (const Pango2FontDescription *desc1, + const Pango2FontDescription *desc2) +{ + g_return_val_if_fail (desc1 != NULL, FALSE); + g_return_val_if_fail (desc2 != NULL, FALSE); + + return desc1->style == desc2->style && + desc1->variant == desc2->variant && + desc1->weight == desc2->weight && + desc1->stretch == desc2->stretch && + desc1->size == desc2->size && + desc1->size_is_absolute == desc2->size_is_absolute && + desc1->gravity == desc2->gravity && + (desc1->family_name == desc2->family_name || + (desc1->family_name && desc2->family_name && g_ascii_strcasecmp (desc1->family_name, desc2->family_name) == 0)) && + (g_strcmp0 (desc1->variations, desc2->variations) == 0) && + (g_strcmp0 (desc1->faceid, desc2->faceid) == 0); +} + +#define TOLOWER(c) \ + (((c) >= 'A' && (c) <= 'Z') ? (c) - 'A' + 'a' : (c)) + +static guint +case_insensitive_hash (const char *key) +{ + const char *p = key; + guint h = TOLOWER (*p); + + if (h) + { + for (p += 1; *p != '\0'; p++) + h = (h << 5) - h + TOLOWER (*p); + } + + return h; +} + +/** + * pango2_font_description_hash: + * @desc: a `Pango2FontDescription` + * + * Computes a hash of a `Pango2FontDescription` structure. + * + * This is suitable to be used, for example, as an argument + * to [GLib.HashTable.new]. The hash value is independent of @desc->mask. + * + * Return value: the hash value. + */ +guint +pango2_font_description_hash (const Pango2FontDescription *desc) +{ + guint hash = 0; + + g_return_val_if_fail (desc != NULL, 0); + + if (desc->family_name) + hash = case_insensitive_hash (desc->family_name); + if (desc->variations) + hash ^= g_str_hash (desc->variations); + if (desc->faceid) + hash ^= g_str_hash (desc->faceid); + hash ^= desc->size; + hash ^= desc->size_is_absolute ? 0xc33ca55a : 0; + hash ^= desc->style << 16; + hash ^= desc->variant << 18; + hash ^= desc->weight << 16; + hash ^= desc->stretch << 26; + hash ^= desc->gravity << 28; + + return hash; +} + +/** + * pango2_font_description_free: + * @desc: (nullable): a `Pango2FontDescription`, may be %NULL + * + * Frees a font description. + */ +void +pango2_font_description_free (Pango2FontDescription *desc) +{ + if (desc == NULL) + return; + + if (desc->family_name && !desc->static_family) + g_free (desc->family_name); + + if (desc->variations && !desc->static_variations) + g_free (desc->variations); + + if (desc->faceid && !desc->static_faceid) + g_free (desc->faceid); + + g_slice_free (Pango2FontDescription, desc); +} + +typedef struct +{ + int value; + const char str[16]; +} FieldMap; + +static const FieldMap style_map[] = { + { PANGO2_STYLE_NORMAL, "" }, + { PANGO2_STYLE_NORMAL, "Roman" }, + { PANGO2_STYLE_OBLIQUE, "Oblique" }, + { PANGO2_STYLE_ITALIC, "Italic" } +}; + +static const FieldMap variant_map[] = { + { PANGO2_VARIANT_NORMAL, "" }, + { PANGO2_VARIANT_SMALL_CAPS, "Small-Caps" }, + { PANGO2_VARIANT_ALL_SMALL_CAPS, "All-Small-Caps" }, + { PANGO2_VARIANT_PETITE_CAPS, "Petite-Caps" }, + { PANGO2_VARIANT_ALL_PETITE_CAPS, "All-Petite-Caps" }, + { PANGO2_VARIANT_UNICASE, "Unicase" }, + { PANGO2_VARIANT_TITLE_CAPS, "Title-Caps" } +}; + +static const FieldMap weight_map[] = { + { PANGO2_WEIGHT_THIN, "Thin" }, + { PANGO2_WEIGHT_ULTRALIGHT, "Ultra-Light" }, + { PANGO2_WEIGHT_ULTRALIGHT, "Extra-Light" }, + { PANGO2_WEIGHT_LIGHT, "Light" }, + { PANGO2_WEIGHT_SEMILIGHT, "Semi-Light" }, + { PANGO2_WEIGHT_SEMILIGHT, "Demi-Light" }, + { PANGO2_WEIGHT_BOOK, "Book" }, + { PANGO2_WEIGHT_NORMAL, "" }, + { PANGO2_WEIGHT_NORMAL, "Regular" }, + { PANGO2_WEIGHT_MEDIUM, "Medium" }, + { PANGO2_WEIGHT_SEMIBOLD, "Semi-Bold" }, + { PANGO2_WEIGHT_SEMIBOLD, "Demi-Bold" }, + { PANGO2_WEIGHT_BOLD, "Bold" }, + { PANGO2_WEIGHT_ULTRABOLD, "Ultra-Bold" }, + { PANGO2_WEIGHT_ULTRABOLD, "Extra-Bold" }, + { PANGO2_WEIGHT_HEAVY, "Heavy" }, + { PANGO2_WEIGHT_HEAVY, "Black" }, + { PANGO2_WEIGHT_ULTRAHEAVY, "Ultra-Heavy" }, + { PANGO2_WEIGHT_ULTRAHEAVY, "Extra-Heavy" }, + { PANGO2_WEIGHT_ULTRAHEAVY, "Ultra-Black" }, + { PANGO2_WEIGHT_ULTRAHEAVY, "Extra-Black" } +}; + +static const FieldMap stretch_map[] = { + { PANGO2_STRETCH_ULTRA_CONDENSED, "Ultra-Condensed" }, + { PANGO2_STRETCH_EXTRA_CONDENSED, "Extra-Condensed" }, + { PANGO2_STRETCH_CONDENSED, "Condensed" }, + { PANGO2_STRETCH_SEMI_CONDENSED, "Semi-Condensed" }, + { PANGO2_STRETCH_NORMAL, "" }, + { PANGO2_STRETCH_SEMI_EXPANDED, "Semi-Expanded" }, + { PANGO2_STRETCH_EXPANDED, "Expanded" }, + { PANGO2_STRETCH_EXTRA_EXPANDED, "Extra-Expanded" }, + { PANGO2_STRETCH_ULTRA_EXPANDED, "Ultra-Expanded" } +}; + +static const FieldMap gravity_map[] = { + { PANGO2_GRAVITY_SOUTH, "Not-Rotated" }, + { PANGO2_GRAVITY_SOUTH, "South" }, + { PANGO2_GRAVITY_NORTH, "Upside-Down" }, + { PANGO2_GRAVITY_NORTH, "North" }, + { PANGO2_GRAVITY_EAST, "Rotated-Left" }, + { PANGO2_GRAVITY_EAST, "East" }, + { PANGO2_GRAVITY_WEST, "Rotated-Right" }, + { PANGO2_GRAVITY_WEST, "West" } +}; + +static gboolean +field_matches (const char *s1, + const char *s2, + gsize n) +{ + int c1, c2; + + g_return_val_if_fail (s1 != NULL, 0); + g_return_val_if_fail (s2 != NULL, 0); + + while (n && *s1 && *s2) + { + c1 = (int)(guchar) TOLOWER (*s1); + c2 = (int)(guchar) TOLOWER (*s2); + if (c1 != c2) { + if (c1 == '-') { + s1++; + continue; + } + return FALSE; + } + s1++; s2++; + n--; + } + + return n == 0 && *s1 == '\0'; +} + +static gboolean +parse_int (const char *word, + size_t wordlen, + int *out) +{ + char *end; + long val = strtol (word, &end, 10); + int i = val; + + if (end != word && (end == word + wordlen) && val >= 0 && val == i) + { + if (out) + *out = i; + + return TRUE; + } + + return FALSE; +} + +static gboolean +find_field (const char *what, + const FieldMap *map, + int n_elements, + const char *str, + int len, + int *val) +{ + int i; + gboolean had_prefix = FALSE; + + if (what) + { + i = strlen (what); + if (len > i && 0 == strncmp (what, str, i) && str[i] == '=') + { + str += i + 1; + len -= i + 1; + had_prefix = TRUE; + } + } + + for (i=0; i<n_elements; i++) + { + if (map[i].str[0] && field_matches (map[i].str, str, len)) + { + if (val) + *val = map[i].value; + return TRUE; + } + } + + if (!what || had_prefix) + return parse_int (str, len, val); + + return FALSE; +} + +static gboolean +find_field_any (const char *str, + int len, + Pango2FontDescription *desc) +{ + if (field_matches ("Normal", str, len)) + return TRUE; + +#define FIELD(NAME, MASK) \ + G_STMT_START { \ + if (find_field (G_STRINGIFY (NAME), NAME##_map, G_N_ELEMENTS (NAME##_map), str, len, \ + desc ? (int *)(void *)&desc->NAME : NULL)) \ + { \ + if (desc) \ + desc->mask |= MASK; \ + return TRUE; \ + } \ + } G_STMT_END + + FIELD (weight, PANGO2_FONT_MASK_WEIGHT); + FIELD (style, PANGO2_FONT_MASK_STYLE); + FIELD (stretch, PANGO2_FONT_MASK_STRETCH); + FIELD (variant, PANGO2_FONT_MASK_VARIANT); + FIELD (gravity, PANGO2_FONT_MASK_GRAVITY); + +#undef FIELD + + return FALSE; +} + +static const char * +getword (const char *str, + const char *last, + size_t *wordlen, + const char *stop) +{ + const char *result; + + while (last > str && g_ascii_isspace (*(last - 1))) + last--; + + result = last; + while (result > str && !g_ascii_isspace (*(result - 1)) && !strchr (stop, *(result - 1))) + result--; + + *wordlen = last - result; + + return result; +} + +static gboolean +parse_size (const char *word, + size_t wordlen, + int *pango2_size, + gboolean *size_is_absolute) +{ + char *end; + double size = g_ascii_strtod (word, &end); + + if (end != word && + (end == word + wordlen || + (end + 2 == word + wordlen && !strncmp (end, "px", 2)) + ) && size >= 0 && size <= 1000000) /* word is a valid float */ + { + if (pango2_size) + *pango2_size = (int)(size * PANGO2_SCALE + 0.5); + + if (size_is_absolute) + *size_is_absolute = end < word + wordlen; + + return TRUE; + } + + return FALSE; +} + +static gboolean +parse_variations (const char *word, + size_t wordlen, + char **variations) +{ + if (word[0] != '@') + { + *variations = NULL; + return FALSE; + } + + /* XXX: actually validate here */ + *variations = g_strndup (word + 1, wordlen - 1); + + return TRUE; +} + +static void +faceid_from_variations (Pango2FontDescription *desc) +{ + const char *p, *q; + + p = desc->variations; + + if (g_str_has_prefix (p, "faceid=")) + { + p += strlen ("faceid="); + q = strchr (p, ','); + if (q) + { + desc->faceid = g_strndup (p, q - p); + p = q + 1; + } + else + { + desc->faceid = g_strdup (p); + p = NULL; + } + desc->mask |= PANGO2_FONT_MASK_FACEID; + } + + if (p != desc->variations) + { + char *variations = g_strdup (p); + g_free (desc->variations); + desc->variations = variations; + if (variations == NULL || *variations == '\0') + desc->mask &= ~PANGO2_FONT_MASK_VARIATIONS; + } +} + +/** + * pango2_font_description_from_string: + * @str: string representation of a font description. + * + * Creates a new font description from a string representation. + * + * The string must have the form + * + * "\[FAMILY-LIST] \[STYLE-OPTIONS] \[SIZE] \[VARIATIONS]", + * + * where FAMILY-LIST is a comma-separated list of families optionally + * terminated by a comma, STYLE_OPTIONS is a whitespace-separated list + * of words where each word describes one of style, variant, weight, + * stretch, or gravity, and SIZE is a decimal number (size in points) + * or optionally followed by the unit modifier "px" for absolute size. + * VARIATIONS is a comma-separated list of font variation + * specifications of the form "\@axis=value" (the = sign is optional). + * + * The following words are understood as styles: + * "Normal", "Roman", "Oblique", "Italic". + * + * The following words are understood as variants: + * "Small-Caps", "All-Small-Caps", "Petite-Caps", "All-Petite-Caps", + * "Unicase", "Title-Caps". + * + * The following words are understood as weights: + * "Thin", "Ultra-Light", "Extra-Light", "Light", "Semi-Light", + * "Demi-Light", "Book", "Regular", "Medium", "Semi-Bold", "Demi-Bold", + * "Bold", "Ultra-Bold", "Extra-Bold", "Heavy", "Black", "Ultra-Black", + * "Extra-Black". + * + * The following words are understood as stretch values: + * "Ultra-Condensed", "Extra-Condensed", "Condensed", "Semi-Condensed", + * "Semi-Expanded", "Expanded", "Extra-Expanded", "Ultra-Expanded". + * + * The following words are understood as gravity values: + * "Not-Rotated", "South", "Upside-Down", "North", "Rotated-Left", + * "East", "Rotated-Right", "West". + * + * Any one of the options may be absent. If FAMILY-LIST is absent, then + * the family_name field of the resulting font description will be + * initialized to %NULL. If STYLE-OPTIONS is missing, then all style + * options will be set to the default values. If SIZE is missing, the + * size in the resulting font description will be set to 0. + * + * A typical example: + * + * "Cantarell Italic Light 15 \@wght=200" + * + * Return value: a new `Pango2FontDescription`. + */ +Pango2FontDescription * +pango2_font_description_from_string (const char *str) +{ + Pango2FontDescription *desc; + const char *p, *last; + size_t len, wordlen; + + g_return_val_if_fail (str != NULL, NULL); + + desc = pango2_font_description_new (); + + desc->mask = PANGO2_FONT_MASK_STYLE | + PANGO2_FONT_MASK_WEIGHT | + PANGO2_FONT_MASK_VARIANT | + PANGO2_FONT_MASK_STRETCH; + + len = strlen (str); + last = str + len; + p = getword (str, last, &wordlen, ""); + /* Look for variations at the end of the string */ + if (wordlen != 0) + { + if (parse_variations (p, wordlen, &desc->variations)) + { + desc->mask |= PANGO2_FONT_MASK_VARIATIONS; + last = p; + + faceid_from_variations (desc); + } + } + + p = getword (str, last, &wordlen, ","); + /* Look for a size */ + if (wordlen != 0) + { + gboolean size_is_absolute; + if (parse_size (p, wordlen, &desc->size, &size_is_absolute)) + { + desc->size_is_absolute = size_is_absolute; + desc->mask |= PANGO2_FONT_MASK_SIZE; + last = p; + } + } + + /* Now parse style words + */ + p = getword (str, last, &wordlen, ","); + while (wordlen != 0) + { + if (!find_field_any (p, wordlen, desc)) + break; + else + { + last = p; + p = getword (str, last, &wordlen, ","); + } + } + + /* Remainder (str => p) is family list. Trim off trailing commas and leading and trailing white space + */ + + while (last > str && g_ascii_isspace (*(last - 1))) + last--; + + if (last > str && *(last - 1) == ',') + last--; + + while (last > str && g_ascii_isspace (*(last - 1))) + last--; + + while (last > str && g_ascii_isspace (*str)) + str++; + + if (str != last) + { + int i; + char **families; + + desc->family_name = g_strndup (str, last - str); + + /* Now sanitize it to trim space from around individual family names. + * bug #499624 */ + + families = g_strsplit (desc->family_name, ",", -1); + + for (i = 0; families[i]; i++) + g_strstrip (families[i]); + + g_free (desc->family_name); + desc->family_name = g_strjoinv (",", families); + g_strfreev (families); + + desc->mask |= PANGO2_FONT_MASK_FAMILY; + } + + return desc; +} + +static void +append_field (GString *str, + const char *what, + const FieldMap *map, + int n_elements, + int val) +{ + int i; + for (i=0; i<n_elements; i++) + { + if (map[i].value != val) + continue; + + if (G_LIKELY (map[i].str[0])) + { + if (G_LIKELY (str->len > 0 && str->str[str->len -1] != ' ')) + g_string_append_c (str, ' '); + g_string_append (str, map[i].str); + } + return; + } + + if (G_LIKELY (str->len > 0 || str->str[str->len -1] != ' ')) + g_string_append_c (str, ' '); + g_string_append_printf (str, "%s=%d", what, val); +} + +/** + * pango2_font_description_to_string: + * @desc: a `Pango2FontDescription` + * + * Creates a string representation of a font description. + * + * See [func@Pango2.FontDescription.from_string] for a description + * of the format of the string representation. The family list in + * the string description will only have a terminating comma if + * the last word of the list is a valid style option. + * + * Return value: a newly allocated string + */ +char * +pango2_font_description_to_string (const Pango2FontDescription *desc) +{ + GString *result; + gboolean in_variations = FALSE; + + g_return_val_if_fail (desc != NULL, NULL); + + result = g_string_new (NULL); + + if (G_LIKELY (desc->family_name && desc->mask & PANGO2_FONT_MASK_FAMILY)) + { + const char *p; + size_t wordlen; + + g_string_append (result, desc->family_name); + + /* We need to add a trailing comma if the family name ends + * in a keyword like "Bold", or if the family name ends in + * a number and no keywords will be added. + */ + p = getword (desc->family_name, desc->family_name + strlen(desc->family_name), &wordlen, ","); + if (wordlen != 0 && + (find_field_any (p, wordlen, NULL) || + (parse_size (p, wordlen, NULL, NULL) && + desc->weight == PANGO2_WEIGHT_NORMAL && + desc->style == PANGO2_STYLE_NORMAL && + desc->stretch == PANGO2_STRETCH_NORMAL && + desc->variant == PANGO2_VARIANT_NORMAL && + (desc->mask & (PANGO2_FONT_MASK_GRAVITY | PANGO2_FONT_MASK_SIZE)) == 0))) + g_string_append_c (result, ','); + } + +#define FIELD(NAME, MASK) \ + append_field (result, G_STRINGIFY (NAME), NAME##_map, G_N_ELEMENTS (NAME##_map), desc->NAME) + + FIELD (weight, PANGO2_FONT_MASK_WEIGHT); + FIELD (style, PANGO2_FONT_MASK_STYLE); + FIELD (stretch, PANGO2_FONT_MASK_STRETCH); + FIELD (variant, PANGO2_FONT_MASK_VARIANT); + if (desc->mask & PANGO2_FONT_MASK_GRAVITY) + FIELD (gravity, PANGO2_FONT_MASK_GRAVITY); + +#undef FIELD + + if (result->len == 0) + g_string_append (result, "Normal"); + + if (desc->mask & PANGO2_FONT_MASK_SIZE) + { + char buf[G_ASCII_DTOSTR_BUF_SIZE]; + + if (result->len > 0 || result->str[result->len -1] != ' ') + g_string_append_c (result, ' '); + + g_ascii_dtostr (buf, sizeof (buf), (double)desc->size / PANGO2_SCALE); + g_string_append (result, buf); + + if (desc->size_is_absolute) + g_string_append (result, "px"); + } + + if (desc->mask & PANGO2_FONT_MASK_FACEID) + { + in_variations = TRUE; + g_string_append (result, " @"); + g_string_append_printf (result, "faceid=%s", desc->faceid); + } + + if ((desc->variations && desc->mask & PANGO2_FONT_MASK_VARIATIONS) && + desc->variations[0] != '\0') + { + if (!in_variations) + g_string_append (result, " @"); + else + g_string_append (result, ","); + g_string_append (result, desc->variations); + } + + return g_string_free (result, FALSE); +} + +static gboolean +parse_field (const char *what, + const FieldMap *map, + int n_elements, + const char *str, + int *val, + gboolean warn) +{ + gboolean found; + int len = strlen (str); + + if (G_UNLIKELY (*str == '\0')) + return FALSE; + + if (field_matches ("Normal", str, len)) + { + /* find the map entry with empty string */ + int i; + + for (i = 0; i < n_elements; i++) + if (map[i].str[0] == '\0') + { + *val = map[i].value; + return TRUE; + } + + *val = 0; + return TRUE; + } + + found = find_field (NULL, map, n_elements, str, len, val); + + if (!found && warn) + { + int i; + GString *s = g_string_new (NULL); + + for (i = 0; i < n_elements; i++) + { + if (i) + g_string_append_c (s, '/'); + g_string_append (s, map[i].str[0] == '\0' ? "Normal" : map[i].str); + } + + g_warning ("%s must be one of %s or a number", + what, + s->str); + + g_string_free (s, TRUE); + } + + return found; +} + +#define FIELD(NAME, MASK) \ + parse_field (G_STRINGIFY (NAME), NAME##_map, G_N_ELEMENTS (NAME##_map), str, (int *)(void *)NAME, warn) + +/*< private > + * pango2_parse_style: + * @str: a string to parse. + * @style: (out): a `Pango2Style` to store the result in. + * @warn: if %TRUE, issue a g_warning() on bad input. + * + * Parses a font style. + * + * The allowed values are "normal", "italic" and "oblique", case + * variations being + * ignored. + * + * Return value: %TRUE if @str was successfully parsed. + */ +gboolean +pango2_parse_style (const char *str, + Pango2Style *style, + gboolean warn) +{ + return FIELD (style, PANGO2_FONT_MASK_STYLE); +} + +/*< private > + * pango2_parse_variant: + * @str: a string to parse. + * @variant: (out): a `Pango2Variant` to store the result in. + * @warn: if %TRUE, issue a g_warning() on bad input. + * + * Parses a font variant. + * + * The allowed values are "normal", "small-caps", "all-small-caps", + * "petite-caps", "all-petite-caps", "unicase" and "title-caps", + * case variations being ignored. + * + * Return value: %TRUE if @str was successfully parsed. + */ +gboolean +pango2_parse_variant (const char *str, + Pango2Variant *variant, + gboolean warn) +{ + return FIELD (variant, PANGO2_FONT_MASK_VARIANT); +} + +/*< private > + * pango2_parse_weight: + * @str: a string to parse. + * @weight: (out): a `Pango2Weight` to store the result in. + * @warn: if %TRUE, issue a g_warning() on bad input. + * + * Parses a font weight. + * + * The allowed values are "heavy", + * "ultrabold", "bold", "normal", "light", "ultraleight" + * and integers. Case variations are ignored. + * + * Return value: %TRUE if @str was successfully parsed. + */ +gboolean +pango2_parse_weight (const char *str, + Pango2Weight *weight, + gboolean warn) +{ + return FIELD (weight, PANGO2_FONT_MASK_WEIGHT); +} + +/*< private > + * pango2_parse_stretch: + * @str: a string to parse. + * @stretch: (out): a `Pango2Stretch` to store the result in. + * @warn: if %TRUE, issue a g_warning() on bad input. + * + * Parses a font stretch. + * + * The allowed values are + * "ultra_condensed", "extra_condensed", "condensed", + * "semi_condensed", "normal", "semi_expanded", "expanded", + * "extra_expanded" and "ultra_expanded". Case variations are + * ignored and the '_' characters may be omitted. + * + * Return value: %TRUE if @str was successfully parsed. + */ +gboolean +pango2_parse_stretch (const char *str, + Pango2Stretch *stretch, + gboolean warn) +{ + return FIELD (stretch, PANGO2_FONT_MASK_STRETCH); +} + +/** + * pango2_font_description_set_faceid_static: + * @desc: a `Pango2FontDescription` + * @faceid: the faceid string + * + * Sets the faceid field of a font description. + * + * This is like [method@Pango2.FontDescription.set_faceid], except + * that no copy of @faceid is made. The caller must make sure that + * the string passed in stays around until @desc has been freed + * or the name is set again. This function can be used if + * @faceid is a static string such as a C string literal, + * or if @desc is only needed temporarily. + */ +void +pango2_font_description_set_faceid_static (Pango2FontDescription *desc, + const char *faceid) +{ + g_return_if_fail (desc != NULL); + + if (desc->faceid == faceid) + return; + + if (desc->faceid && !desc->static_faceid) + g_free (desc->faceid); + + if (faceid) + { + desc->faceid = (char *)faceid; + desc->static_faceid = TRUE; + desc->mask |= PANGO2_FONT_MASK_FACEID; + } + else + { + desc->faceid = pfd_defaults.faceid; + desc->static_faceid = pfd_defaults.static_faceid; + desc->mask &= ~PANGO2_FONT_MASK_FACEID; + } +} + +/** + * pango2_font_description_set_faceid: + * @desc: a `Pango2FontDescription`. + * @faceid: (nullable): the faceid string + * + * Sets the faceid field of a font description. + * + * The faceid is mainly for internal use by Pango2, to ensure + * that font -> description -> font roundtrips end up with + * the same font they started with, if possible. + * + * Font descriptions originating from [method@Pango2.FontFace.describe] + * should ideally include a faceid. Pango2 takes the faceid + * into account when looking for the best matching face while + * loading a fontset or font. + * + * The format of this string is not guaranteed. + */ +void +pango2_font_description_set_faceid (Pango2FontDescription *desc, + const char *faceid) +{ + g_return_if_fail (desc != NULL); + + pango2_font_description_set_faceid_static (desc, g_strdup (faceid)); + if (faceid) + desc->static_faceid = FALSE; +} + +/** + * pango2_font_description_get_faceid: + * @desc: a `Pango2FontDescription` + * + * Gets the faceid field of a font description. + * + * See [method@Pango2.FontDescription.set_faceid]. + * + * Return value: (nullable): the faceid field for the font + * description, or %NULL if not previously set. This has the same + * life-time as the font description itself and should not be freed. + */ +const char * +pango2_font_description_get_faceid (const Pango2FontDescription *desc) +{ + g_return_val_if_fail (desc != NULL, NULL); + + return desc->faceid; +} diff --git a/pango2/pango-font-description.h b/pango2/pango-font-description.h new file mode 100644 index 00000000..160ee50d --- /dev/null +++ b/pango2/pango-font-description.h @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2000 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-types.h> + +#include <glib-object.h> +#include <hb.h> + +G_BEGIN_DECLS + +/** + * Pango2FontDescription: + * + * A `Pango2FontDescription` describes a font in an implementation-independent + * manner. + * + * `Pango2FontDescription` structures are used both to list what fonts are + * available on the system and also for specifying the characteristics of + * a font to load. + */ + +/** + * Pango2Style: + * @PANGO2_STYLE_NORMAL: the font is upright. + * @PANGO2_STYLE_OBLIQUE: the font is slanted, but in a roman style. + * @PANGO2_STYLE_ITALIC: the font is slanted in an italic style. + * + * An enumeration specifying the various slant styles possible for a font. + **/ +typedef enum { + PANGO2_STYLE_NORMAL, + PANGO2_STYLE_OBLIQUE, + PANGO2_STYLE_ITALIC +} Pango2Style; + +/** + * Pango2Variant: + * @PANGO2_VARIANT_NORMAL: A normal font. + * @PANGO2_VARIANT_SMALL_CAPS: A font with the lower case characters + * replaced by smaller variants of the capital characters. + * @PANGO2_VARIANT_ALL_SMALL_CAPS: A font with all characters + * replaced by smaller variants of the capital characters. + * @PANGO2_VARIANT_PETITE_CAPS: A font with the lower case characters + * replaced by smaller variants of the capital characters. + * Petite Caps can be even smaller than Small Caps. + * @PANGO2_VARIANT_ALL_PETITE_CAPS: A font with all characters + * replaced by smaller variants of the capital characters. + * Petite Caps can be even smaller than Small Caps. + * @PANGO2_VARIANT_UNICASE: A font with the upper case characters + * replaced by smaller variants of the capital letters. + * @PANGO2_VARIANT_TITLE_CAPS: A font with capital letters that + * are more suitable for all-uppercase titles. + * + * An enumeration specifying capitalization variant of the font. + */ +typedef enum { + PANGO2_VARIANT_NORMAL, + PANGO2_VARIANT_SMALL_CAPS, + PANGO2_VARIANT_ALL_SMALL_CAPS, + PANGO2_VARIANT_PETITE_CAPS, + PANGO2_VARIANT_ALL_PETITE_CAPS, + PANGO2_VARIANT_UNICASE, + PANGO2_VARIANT_TITLE_CAPS +} Pango2Variant; + +/** + * Pango2Weight: + * @PANGO2_WEIGHT_THIN: the thin weight (= 100) + * @PANGO2_WEIGHT_ULTRALIGHT: the ultralight weight (= 200) + * @PANGO2_WEIGHT_LIGHT: the light weight (= 300) + * @PANGO2_WEIGHT_SEMILIGHT: the semilight weight (= 350) + * @PANGO2_WEIGHT_BOOK: the book weight (= 380) + * @PANGO2_WEIGHT_NORMAL: the default weight (= 400) + * @PANGO2_WEIGHT_MEDIUM: the normal weight (= 500) + * @PANGO2_WEIGHT_SEMIBOLD: the semibold weight (= 600) + * @PANGO2_WEIGHT_BOLD: the bold weight (= 700) + * @PANGO2_WEIGHT_ULTRABOLD: the ultrabold weight (= 800) + * @PANGO2_WEIGHT_HEAVY: the heavy weight (= 900) + * @PANGO2_WEIGHT_ULTRAHEAVY: the ultraheavy weight (= 1000) + * + * A `Pango2Weight` specifes the weight (boldness) of a font. + * + * Weight is specified as a numeric value ranging from 100 to 1000. + * This enumeration simply provides some common, predefined values. + */ +typedef enum { + PANGO2_WEIGHT_THIN = 100, + PANGO2_WEIGHT_ULTRALIGHT = 200, + PANGO2_WEIGHT_LIGHT = 300, + PANGO2_WEIGHT_SEMILIGHT = 350, + PANGO2_WEIGHT_BOOK = 380, + PANGO2_WEIGHT_NORMAL = 400, + PANGO2_WEIGHT_MEDIUM = 500, + PANGO2_WEIGHT_SEMIBOLD = 600, + PANGO2_WEIGHT_BOLD = 700, + PANGO2_WEIGHT_ULTRABOLD = 800, + PANGO2_WEIGHT_HEAVY = 900, + PANGO2_WEIGHT_ULTRAHEAVY = 1000 +} Pango2Weight; + +/** + * Pango2Stretch: + * @PANGO2_STRETCH_ULTRA_CONDENSED: ultra-condensed width (= 500) + * @PANGO2_STRETCH_EXTRA_CONDENSED: extra-condensed width (= 625) + * @PANGO2_STRETCH_CONDENSED: condensed width (= 750) + * @PANGO2_STRETCH_SEMI_CONDENSED: semi-condensed width (= 875) + * @PANGO2_STRETCH_NORMAL: the normal width (= 1000) + * @PANGO2_STRETCH_SEMI_EXPANDED: semi-expanded width (= 1125) + * @PANGO2_STRETCH_EXPANDED: expanded width (= 1250) + * @PANGO2_STRETCH_EXTRA_EXPANDED: extra-expanded width (= 1500) + * @PANGO2_STRETCH_ULTRA_EXPANDED: ultra-expanded width (= 2000) + * + * A `Pango2Stretch` specifes the width of the font relative + * to other designs within a family. + * + * Stretch is specified as a numeric value ranging from 500 to 2000. + * This enumeration simply provides some common, predefined values. + */ +typedef enum { + PANGO2_STRETCH_ULTRA_CONDENSED = 500, + PANGO2_STRETCH_EXTRA_CONDENSED = 625, + PANGO2_STRETCH_CONDENSED = 750, + PANGO2_STRETCH_SEMI_CONDENSED = 875, + PANGO2_STRETCH_NORMAL = 1000, + PANGO2_STRETCH_SEMI_EXPANDED = 1125, + PANGO2_STRETCH_EXPANDED = 1250, + PANGO2_STRETCH_EXTRA_EXPANDED = 1500, + PANGO2_STRETCH_ULTRA_EXPANDED = 2000 +} Pango2Stretch; + +/** + * Pango2FontMask: + * @PANGO2_FONT_MASK_FAMILY: the font family is specified. + * @PANGO2_FONT_MASK_STYLE: the font style is specified. + * @PANGO2_FONT_MASK_VARIANT: the font variant is specified. + * @PANGO2_FONT_MASK_WEIGHT: the font weight is specified. + * @PANGO2_FONT_MASK_STRETCH: the font stretch is specified. + * @PANGO2_FONT_MASK_SIZE: the font size is specified. + * @PANGO2_FONT_MASK_GRAVITY: the font gravity is specified + * @PANGO2_FONT_MASK_VARIATIONS: OpenType font variations are specified + * @PANGO2_FONT_MASK_FACEID: the face ID is specified + * + * The bits in a `Pango2FontMask` correspond to the set fields in a + * `Pango2FontDescription`. + */ +typedef enum { + PANGO2_FONT_MASK_FAMILY = 1 << 0, + PANGO2_FONT_MASK_STYLE = 1 << 1, + PANGO2_FONT_MASK_VARIANT = 1 << 2, + PANGO2_FONT_MASK_WEIGHT = 1 << 3, + PANGO2_FONT_MASK_STRETCH = 1 << 4, + PANGO2_FONT_MASK_SIZE = 1 << 5, + PANGO2_FONT_MASK_GRAVITY = 1 << 6, + PANGO2_FONT_MASK_VARIATIONS = 1 << 7, + PANGO2_FONT_MASK_FACEID = 1 << 8, +} Pango2FontMask; + +/* CSS scale factors (1.2 factor between each size) */ +/** + * PANGO2_SCALE_XX_SMALL: + * + * The scale factor for three shrinking steps (1 / (1.2 * 1.2 * 1.2)). + */ +/** + * PANGO2_SCALE_X_SMALL: + * + * The scale factor for two shrinking steps (1 / (1.2 * 1.2)). + */ +/** + * PANGO2_SCALE_SMALL: + * + * The scale factor for one shrinking step (1 / 1.2). + */ +/** + * PANGO2_SCALE_MEDIUM: + * + * The scale factor for normal size (1.0). + */ +/** + * PANGO2_SCALE_LARGE: + * + * The scale factor for one magnification step (1.2). + */ +/** + * PANGO2_SCALE_X_LARGE: + * + * The scale factor for two magnification steps (1.2 * 1.2). + */ +/** + * PANGO2_SCALE_XX_LARGE: + * + * The scale factor for three magnification steps (1.2 * 1.2 * 1.2). + */ +#define PANGO2_SCALE_XX_SMALL ((double)0.5787037037037) +#define PANGO2_SCALE_X_SMALL ((double)0.6944444444444) +#define PANGO2_SCALE_SMALL ((double)0.8333333333333) +#define PANGO2_SCALE_MEDIUM ((double)1.0) +#define PANGO2_SCALE_LARGE ((double)1.2) +#define PANGO2_SCALE_X_LARGE ((double)1.44) +#define PANGO2_SCALE_XX_LARGE ((double)1.728) + + +#define PANGO2_TYPE_FONT_DESCRIPTION (pango2_font_description_get_type ()) + +PANGO2_AVAILABLE_IN_ALL +GType pango2_font_description_get_type (void) G_GNUC_CONST; +PANGO2_AVAILABLE_IN_ALL +Pango2FontDescription * pango2_font_description_new (void); +PANGO2_AVAILABLE_IN_ALL +Pango2FontDescription * pango2_font_description_copy (const Pango2FontDescription *desc); +PANGO2_AVAILABLE_IN_ALL +Pango2FontDescription * pango2_font_description_copy_static (const Pango2FontDescription *desc); +PANGO2_AVAILABLE_IN_ALL +guint pango2_font_description_hash (const Pango2FontDescription *desc) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_font_description_equal (const Pango2FontDescription *desc1, + const Pango2FontDescription *desc2) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +void pango2_font_description_free (Pango2FontDescription *desc); + +PANGO2_AVAILABLE_IN_ALL +void pango2_font_description_set_family (Pango2FontDescription *desc, + const char *family); +PANGO2_AVAILABLE_IN_ALL +void pango2_font_description_set_family_static (Pango2FontDescription *desc, + const char *family); +PANGO2_AVAILABLE_IN_ALL +const char * pango2_font_description_get_family (const Pango2FontDescription *desc) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +void pango2_font_description_set_style (Pango2FontDescription *desc, + Pango2Style style); +PANGO2_AVAILABLE_IN_ALL +Pango2Style pango2_font_description_get_style (const Pango2FontDescription *desc) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +void pango2_font_description_set_variant (Pango2FontDescription *desc, + Pango2Variant variant); +PANGO2_AVAILABLE_IN_ALL +Pango2Variant pango2_font_description_get_variant (const Pango2FontDescription *desc) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +void pango2_font_description_set_weight (Pango2FontDescription *desc, + Pango2Weight weight); +PANGO2_AVAILABLE_IN_ALL +Pango2Weight pango2_font_description_get_weight (const Pango2FontDescription *desc) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +void pango2_font_description_set_stretch (Pango2FontDescription *desc, + Pango2Stretch stretch); +PANGO2_AVAILABLE_IN_ALL +Pango2Stretch pango2_font_description_get_stretch (const Pango2FontDescription *desc) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +void pango2_font_description_set_size (Pango2FontDescription *desc, + int size); +PANGO2_AVAILABLE_IN_ALL +int pango2_font_description_get_size (const Pango2FontDescription *desc) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +void pango2_font_description_set_absolute_size (Pango2FontDescription *desc, + double size); +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_font_description_get_size_is_absolute (const Pango2FontDescription *desc) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +void pango2_font_description_set_gravity (Pango2FontDescription *desc, + Pango2Gravity gravity); +PANGO2_AVAILABLE_IN_ALL +Pango2Gravity pango2_font_description_get_gravity (const Pango2FontDescription *desc) G_GNUC_PURE; + +PANGO2_AVAILABLE_IN_ALL +void pango2_font_description_set_variations_static (Pango2FontDescription *desc, + const char *variations); +PANGO2_AVAILABLE_IN_ALL +void pango2_font_description_set_variations (Pango2FontDescription *desc, + const char *variations); +PANGO2_AVAILABLE_IN_ALL +const char * pango2_font_description_get_variations (const Pango2FontDescription *desc) G_GNUC_PURE; + +PANGO2_AVAILABLE_IN_ALL +void pango2_font_description_set_faceid (Pango2FontDescription *desc, + const char *faceid); +PANGO2_AVAILABLE_IN_ALL +void pango2_font_description_set_faceid_static (Pango2FontDescription *desc, + const char *faceid); +PANGO2_AVAILABLE_IN_ALL +const char * pango2_font_description_get_faceid (const Pango2FontDescription *desc) G_GNUC_PURE; + +PANGO2_AVAILABLE_IN_ALL +Pango2FontMask pango2_font_description_get_set_fields (const Pango2FontDescription *desc) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +void pango2_font_description_unset_fields (Pango2FontDescription *desc, + Pango2FontMask to_unset); + +PANGO2_AVAILABLE_IN_ALL +void pango2_font_description_merge (Pango2FontDescription *desc, + const Pango2FontDescription *desc_to_merge, + gboolean replace_existing); +PANGO2_AVAILABLE_IN_ALL +void pango2_font_description_merge_static (Pango2FontDescription *desc, + const Pango2FontDescription *desc_to_merge, + gboolean replace_existing); + +PANGO2_AVAILABLE_IN_ALL +Pango2FontDescription * pango2_font_description_from_string (const char *str); +PANGO2_AVAILABLE_IN_ALL +char * pango2_font_description_to_string (const Pango2FontDescription *desc); + + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(Pango2FontDescription, pango2_font_description_free) + +G_END_DECLS diff --git a/pango2/pango-font-face-private.h b/pango2/pango-font-face-private.h new file mode 100644 index 00000000..b7031a26 --- /dev/null +++ b/pango2/pango-font-face-private.h @@ -0,0 +1,85 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-font-face.h" +#include "pango-font-description.h" + + +struct _Pango2FontFace +{ + GObject parent_instance; + + Pango2FontFamily *family; + Pango2FontDescription *description; + char *name; + char *faceid; +}; + +typedef struct _Pango2FontFaceClass Pango2FontFaceClass; + +struct _Pango2FontFaceClass +{ + GObjectClass parent_class; + + gboolean (* is_synthesized) (Pango2FontFace *face); + gboolean (* is_monospace) (Pango2FontFace *face); + gboolean (* is_variable) (Pango2FontFace *face); + gboolean (* supports_language) (Pango2FontFace *face, + Pango2Language *language); + Pango2Language ** (* get_languages) (Pango2FontFace *face); + gboolean (* has_char) (Pango2FontFace *face, + gunichar wc); + const char * (* get_faceid) (Pango2FontFace *face); + Pango2Font * (* create_font) (Pango2FontFace *face, + const Pango2FontDescription *desc, + float dpi, + const Pango2Matrix *ctm); +}; + +#define PANGO2_FONT_FACE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), PANGO2_TYPE_FONT_FACE, Pango2FontFaceClass)) +#define PANGO2_FONT_FACE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), PANGO2_TYPE_FONT_FACE, Pango2FontFaceClass)) + +const char * pango2_font_face_get_faceid (Pango2FontFace *face); +Pango2Font * pango2_font_face_create_font (Pango2FontFace *face, + const Pango2FontDescription *desc, + float dpi, + const Pango2Matrix *ctm); + +static inline void +pango2_font_face_set_name (Pango2FontFace *face, + const char *name) +{ + face->name = g_strdup (name); +} + +static inline void +pango2_font_face_set_description (Pango2FontFace *face, + const Pango2FontDescription *description) +{ + face->description = pango2_font_description_copy (description); +} + +static inline void +pango2_font_face_set_family (Pango2FontFace *face, + Pango2FontFamily *family) +{ + face->family = family; +} diff --git a/pango2/pango-font-face.c b/pango2/pango-font-face.c new file mode 100644 index 00000000..475cb871 --- /dev/null +++ b/pango2/pango-font-face.c @@ -0,0 +1,543 @@ +/* Pango2 + * + * Copyright (C) 1999 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include "pango-font-face-private.h" +#include "pango-font-family.h" + +#ifdef __linux__ +#include <execinfo.h> + +static inline char * +get_backtrace (void) +{ + void *buffer[1024]; + int size; + char **symbols; + GString *s; + + size = backtrace (buffer, 1024); + symbols = backtrace_symbols (buffer, size); + + s = g_string_new (""); + + for (int i = 0; i < size; i++) + { + g_string_append (s, symbols[i]); + g_string_append_c (s, '\n'); + } + + free (symbols); + + return g_string_free (s, FALSE); +} +#endif + +/** + * Pango2FontFace: + * + * A `Pango2FontFace` is used to represent a group of fonts with + * the same family, slant, weight, and width, but varying sizes. + * + * `Pango2FontFace` provides APIs to determine coverage information, + * such as [method@Pango2.FontFace.has_char] and + * [method@Pango2.FontFace.supports_language], as well as general + * information about the font face like [method@Pango2.FontFace.is_monospace] + * or [method@Pango2.FontFace.is_variable]. + */ + +/* {{{ Pango2FontFace implementation */ + +enum { + PROP_NAME = 1, + PROP_DESCRIPTION, + PROP_FAMILY, + PROP_SYNTHESIZED, + PROP_MONOSPACE, + PROP_VARIABLE, + N_PROPERTIES +}; + +static GParamSpec *properties[N_PROPERTIES] = { NULL, }; + +G_DEFINE_ABSTRACT_TYPE (Pango2FontFace, pango2_font_face, G_TYPE_OBJECT) + +static gboolean +pango2_font_face_default_is_monospace (Pango2FontFace *face) +{ + return FALSE; +} + +static gboolean +pango2_font_face_default_is_variable (Pango2FontFace *face) +{ + return FALSE; +} + +static gboolean +pango2_font_face_default_supports_language (Pango2FontFace *face, + Pango2Language *language) +{ + return TRUE; +} + +static Pango2Language ** +pango2_font_face_default_get_languages (Pango2FontFace *face) +{ + return NULL; +} + +static gboolean +pango2_font_face_default_has_char (Pango2FontFace *face, + gunichar wc) +{ + return FALSE; +} + +static const char * +pango2_font_face_default_get_faceid (Pango2FontFace *face) +{ + return ""; +} + +static Pango2Font * +pango2_font_face_default_create_font (Pango2FontFace *face, + const Pango2FontDescription *desc, + float dpi, + const Pango2Matrix *ctm) +{ + return NULL; +} + +static void +pango2_font_face_finalize (GObject *object) +{ + Pango2FontFace *face = PANGO2_FONT_FACE (object); + + pango2_font_description_free (face->description); + g_free (face->name); + + G_OBJECT_CLASS (pango2_font_face_parent_class)->finalize (object); +} + +static void +pango2_font_face_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + Pango2FontFace *face = PANGO2_FONT_FACE (object); + + switch (property_id) + { + case PROP_NAME: + g_value_set_string (value, face->name); + break; + + case PROP_DESCRIPTION: + if ((pango2_font_description_get_set_fields (face->description) & PANGO2_FONT_MASK_FACEID) == 0) + pango2_font_description_set_faceid (face->description, + pango2_font_face_get_faceid (face)); + + g_value_set_boxed (value, face->description); + break; + + case PROP_FAMILY: + g_value_set_object (value, face->family); + break; + + case PROP_SYNTHESIZED: + g_value_set_boolean (value, pango2_font_face_is_synthesized (face)); + break; + + case PROP_MONOSPACE: + g_value_set_boolean (value, pango2_font_face_is_monospace (face)); + break; + + case PROP_VARIABLE: + g_value_set_boolean (value, pango2_font_face_is_variable (face)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +pango2_font_face_class_init (Pango2FontFaceClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->finalize = pango2_font_face_finalize; + object_class->get_property = pango2_font_face_get_property; + + class->is_monospace = pango2_font_face_default_is_monospace; + class->is_variable = pango2_font_face_default_is_variable; + class->get_languages = pango2_font_face_default_get_languages; + class->supports_language = pango2_font_face_default_supports_language; + class->has_char = pango2_font_face_default_has_char; + class->get_faceid = pango2_font_face_default_get_faceid; + class->create_font = pango2_font_face_default_create_font; + + /** + * Pango2FontFace:name: (attributes org.gtk.Property.get=pango2_font_face_get_name) + * + * A name representing the style of this face. + */ + properties[PROP_NAME] = + g_param_spec_string ("name", NULL, NULL, NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2FontFace:description: (attributes org.gtk.Property.get=pango2_font_face_describe) + * + * A font description that matches the face. + * + * The font description will have the family, style, + * variant, weight and stretch of the face, but its size field + * will be unset. + */ + properties[PROP_DESCRIPTION] = + g_param_spec_boxed ("description", NULL, NULL, + PANGO2_TYPE_FONT_DESCRIPTION, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2FontFace:family: (attributes org.gtk.Property.get=pango2_font_face_get_family) + * + * The `Pango2FontFamily` that the face belongs to. + */ + properties[PROP_FAMILY] = + g_param_spec_object ("family", NULL, NULL, + PANGO2_TYPE_FONT_FAMILY, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2FontFace:synthesized: (attributes org.gtk.Property.get=pango2_font_face_is_synthesized) + * + * `TRUE` if the face is synthesized. + * + * This will be the case if the underlying font rendering engine + * creates this face from another face, by shearing, emboldening, + * lightening or modifying it in some other way. + */ + properties[PROP_SYNTHESIZED] = + g_param_spec_boolean ("synthesized", NULL, NULL, FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2FontFace:monospace: (attributes org.gtk.Property.get=pango2_font_face_is_monospace) + * + * `TRUE` if the face is designed for text display where the the + * characters form a regular grid. + */ + properties[PROP_MONOSPACE] = + g_param_spec_boolean ("monospace", NULL, NULL, FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2FontFace:variable: (attributes org.gtk.Property.get=pango2_font_face_is_variable) + * + * `TRUE` if the face has axes that can be modified + * to produce variations. + */ + properties[PROP_VARIABLE] = + g_param_spec_boolean ("variable", NULL, NULL, FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPERTIES, properties); +} + +static void +pango2_font_face_init (Pango2FontFace *face G_GNUC_UNUSED) +{ +} + +/* }}} */ +/* {{{ Private API */ + +Pango2Font * +pango2_font_face_create_font (Pango2FontFace *face, + const Pango2FontDescription *desc, + float dpi, + const Pango2Matrix *ctm) +{ + if (!PANGO2_IS_FONT_FACE (face)) + { + char *s = pango2_font_description_to_string (desc); +#ifdef __linux__ + char *bs = get_backtrace (); +#else + char *bs = g_strdup (""); +#endif + g_critical ("pango2_font_face_create_font called without a face for %s\n%s", s, bs); + g_free (s); + g_free (bs); + } + + g_return_val_if_fail (PANGO2_IS_FONT_FACE (face), NULL); + + return PANGO2_FONT_FACE_GET_CLASS (face)->create_font (face, desc, dpi, ctm); +} + +/*< private > + * pango2_font_face_get_faceid: + * @self: a `Pango2HbFace` + * + * Returns the faceid of the face. + * + * The faceid is meant to uniquely identify the face among the + * faces of its family. It includes an identifier for the font + * file that is used (currently, we use the PostScript name for + * this), the face index, the instance ID, as well as synthetic + * tweaks such as emboldening and transforms and variations. + * + * [method@Pango2.FontFace.describe] adds the faceid to the font + * description that it produces. + * + * See pango2_hb_family_find_face() for the code that takes the + * faceid into account when searching for a face. It is careful + * to fall back to approximate matching if an exact match for + * the faceid isn't found. That should make it safe to preserve + * faceids when saving font descriptions in configuration or + * other data. + * + * There are no guarantees about the format of the string that + * this function produces, except for that it does not contain + * ' ', ',' or '=', so it can be safely embedded in the '@' part + * of a serialized font description. + * + * Returns: (transfer none): the faceid + */ +const char * +pango2_font_face_get_faceid (Pango2FontFace *face) +{ + g_return_val_if_fail (PANGO2_IS_FONT_FACE (face), ""); + + return PANGO2_FONT_FACE_GET_CLASS (face)->get_faceid (face); +} + +/* }}} */ +/* {{{ Public API */ + +/** + * pango2_font_face_describe: + * @face: a `Pango2FontFace` + * + * Returns a font description that matches the face. + * + * The resulting font description will have the family, style, + * variant, weight and stretch of the face, but its size field + * will be unset. + * + * Return value: a newly-created `Pango2FontDescription` structure + * holding the description of the face. Use [method@Pango2.FontDescription.free] + * to free the result. + */ +Pango2FontDescription * +pango2_font_face_describe (Pango2FontFace *face) +{ + g_return_val_if_fail (PANGO2_IS_FONT_FACE (face), NULL); + + if ((pango2_font_description_get_set_fields (face->description) & PANGO2_FONT_MASK_FACEID) == 0) + pango2_font_description_set_faceid (face->description, + pango2_font_face_get_faceid (face)); + + return pango2_font_description_copy (face->description); +} + +/** + * pango2_font_face_is_synthesized: + * @face: a `Pango2FontFace` + * + * Returns whether a `Pango2FontFace` is synthesized. + * + * This will be the case if the underlying font rendering engine + * creates this face from another face, by shearing, emboldening, + * lightening or modifying it in some other way. + * + * Return value: whether @face is synthesized + */ +gboolean +pango2_font_face_is_synthesized (Pango2FontFace *face) +{ + g_return_val_if_fail (PANGO2_IS_FONT_FACE (face), FALSE); + + if (PANGO2_FONT_FACE_GET_CLASS (face)->is_synthesized != NULL) + return PANGO2_FONT_FACE_GET_CLASS (face)->is_synthesized (face); + else + return FALSE; +} + +/** + * pango2_font_face_get_name: + * @face: a `Pango2FontFace`. + * + * Gets a name representing the style of this face. + * + * Note that a font family may contain multiple faces + * with the same name (e.g. a variable and a non-variable + * face for the same style). + * + * Return value: the name for the face. This string is + * owned by the face object and must not be modified or freed. + */ +const char * +pango2_font_face_get_name (Pango2FontFace *face) +{ + g_return_val_if_fail (PANGO2_IS_FONT_FACE (face), NULL); + + return face->name; +} + +/** + * pango2_font_face_get_family: + * @face: a `Pango2FontFace` + * + * Gets the `Pango2FontFamily` that the face belongs to. + * + * Returns: (transfer none): the `Pango2FontFamily` + */ +Pango2FontFamily * +pango2_font_face_get_family (Pango2FontFace *face) +{ + g_return_val_if_fail (PANGO2_IS_FONT_FACE (face), NULL); + + return face->family; +} + +/** + * pango2_font_face_is_monospace: + * @face: a `Pango2FontFace` + * + * Returns whether this font face is monospace. + * + * A monospace font is a font designed for text display where the the + * characters form a regular grid. + * + * For Western languages this would mean that the advance width of all + * characters are the same, but this categorization also includes Asian + * fonts which include double-width characters: characters that occupy + * two grid cells. [func@GLib.unichar_iswide] returns a result that + * indicates whether a character is typically double-width in a monospace + * font. + * + * The best way to find out the grid-cell size is to call + * [method@Pango2.FontMetrics.get_approximate_digit_width], since the + * results of [method@Pango2.FontMetrics.get_approximate_char_width] may + * be affected by double-width characters. + * + * Returns: `TRUE` if @face is monospace + */ +gboolean +pango2_font_face_is_monospace (Pango2FontFace *face) +{ + g_return_val_if_fail (PANGO2_IS_FONT_FACE (face), FALSE); + + return PANGO2_FONT_FACE_GET_CLASS (face)->is_monospace (face); +} + +/** + * pango2_font_face_is_variable: + * @face: a `Pango2FontFace` + * + * Returns whether this font face is variable. + * + * A variable font is a font which has axes that can be modified + * to produce variations. + * + * Such axes are also known as _variations_; see + * [method@Pango2.FontDescription.set_variations] for more information. + * + * Returns: `TRUE` if @face is variable + */ +gboolean +pango2_font_face_is_variable (Pango2FontFace *face) +{ + g_return_val_if_fail (PANGO2_IS_FONT_FACE (face), FALSE); + + return PANGO2_FONT_FACE_GET_CLASS (face)->is_variable (face); +} + +/** + * pango2_font_face_supports_language: + * @face: a `Pango2FontFace` + * @language: a `Pango2Language` + * + * Returns whether the face has all the glyphs necessary to + * support this language. + * + * Returns: `TRUE` if @face supports @language + */ +gboolean +pango2_font_face_supports_language (Pango2FontFace *face, + Pango2Language *language) +{ + g_return_val_if_fail (PANGO2_IS_FONT_FACE (face), FALSE); + + return PANGO2_FONT_FACE_GET_CLASS (face)->supports_language (face, language); +} + +/** + * pango2_font_face_get_languages: + * @face: a `Pango2FontFace` + * + * Returns the languages that are supported by the face. + * + * If the font backend does not provide this information, + * `NULL` is returned. For the fontconfig backend, this + * corresponds to the `FC_LANG` member of the `FcPattern`. + * + * The returned array is only valid as long as the face + * and its fontmap are valid. + * + * Returns: (transfer none) (nullable) (array zero-terminated=1) (element-type Pango2Language): + * an array of `Pango2Language` + */ +Pango2Language ** +pango2_font_face_get_languages (Pango2FontFace *face) +{ + g_return_val_if_fail (PANGO2_IS_FONT_FACE (face), FALSE); + + return PANGO2_FONT_FACE_GET_CLASS (face)->get_languages (face); +} + +/** + * pango2_font_face_has_char: + * @face: a `Pango2FontFace` + * @wc: a Unicode character + * + * Returns whether the face provides a glyph for this character. + * + * Returns: `TRUE` if @font can render @wc + */ +gboolean +pango2_font_face_has_char (Pango2FontFace *face, + gunichar wc) +{ + g_return_val_if_fail (PANGO2_IS_FONT_FACE (face), FALSE); + + return PANGO2_FONT_FACE_GET_CLASS (face)->has_char (face, wc); +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pango-font-face.h b/pango2/pango-font-face.h new file mode 100644 index 00000000..dea2b6df --- /dev/null +++ b/pango2/pango-font-face.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2000 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-types.h> + +#include <glib-object.h> + +G_BEGIN_DECLS + + +#define PANGO2_TYPE_FONT_FACE (pango2_font_face_get_type ()) + +PANGO2_AVAILABLE_IN_ALL +PANGO2_DECLARE_INTERNAL_TYPE (Pango2FontFace, pango2_font_face, PANGO2, FONT_FACE, GObject) + +PANGO2_AVAILABLE_IN_ALL +Pango2FontDescription * pango2_font_face_describe (Pango2FontFace *face); +PANGO2_AVAILABLE_IN_ALL +const char * pango2_font_face_get_name (Pango2FontFace *face) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_font_face_is_synthesized (Pango2FontFace *face) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_font_face_is_monospace (Pango2FontFace *face); +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_font_face_is_variable (Pango2FontFace *face); + +PANGO2_AVAILABLE_IN_ALL +Pango2FontFamily * pango2_font_face_get_family (Pango2FontFace *face); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_font_face_supports_language + (Pango2FontFace *face, + Pango2Language *language); + +PANGO2_AVAILABLE_IN_ALL +Pango2Language ** pango2_font_face_get_languages (Pango2FontFace *face); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_font_face_has_char (Pango2FontFace *face, + gunichar wc); + + +G_END_DECLS diff --git a/pango2/pango-font-family-private.h b/pango2/pango-font-family-private.h new file mode 100644 index 00000000..2c2c28fa --- /dev/null +++ b/pango2/pango-font-family-private.h @@ -0,0 +1,66 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-font-family.h" + + +typedef struct _Pango2FontFamilyClass Pango2FontFamilyClass; + +struct _Pango2FontFamily +{ + GObject parent_instance; + + Pango2FontMap *map; + char *name; +}; + +struct _Pango2FontFamilyClass +{ + GObjectClass parent_class; + + Pango2FontFace * (* get_face) (Pango2FontFamily *family, + const char *name); +}; + +#define PANGO2_FONT_FAMILY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), PANGO2_TYPE_FONT_FAMILY, Pango2FontFamilyClass)) +#define PANGO2_FONT_FAMILY_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), PANGO2_TYPE_FONT_FAMILY, Pango2FontFamilyClass)) + +static inline void +pango2_font_family_set_name (Pango2FontFamily *family, + const char *name) +{ + family->name = g_strdup (name); +} + +static inline void +pango2_font_family_set_font_map (Pango2FontFamily *family, + Pango2FontMap *map) +{ + if (family->map) + g_object_remove_weak_pointer (G_OBJECT (family->map), + (gpointer *)&family->map); + + family->map = map; + + if (family->map) + g_object_add_weak_pointer (G_OBJECT (family->map), + (gpointer *)&family->map); +} diff --git a/pango2/pango-font-family.c b/pango2/pango-font-family.c new file mode 100644 index 00000000..1042c7d3 --- /dev/null +++ b/pango2/pango-font-family.c @@ -0,0 +1,267 @@ +/* Pango2 + * + * Copyright (C) 1999 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include <gio/gio.h> + +#include "pango-font-family-private.h" +#include "pango-font-face.h" +#include "pango-font.h" + +/** + * Pango2FontFamily: + * + * A `Pango2FontFamily` is used to represent a family of related + * font faces. + * + * The font faces in a family share a common design, but differ in + * slant, weight, width or other aspects. + * + * `Pango2FontFamily` implements the [iface@Gio.ListModel] interface, + * to provide a list of font faces. + */ + +/* {{{ GListModel implementation */ + +static GType +pango2_font_family_get_item_type (GListModel *list) +{ + return PANGO2_TYPE_FONT_FACE; +} + +static guint +pango2_font_family_get_n_items (GListModel *list) +{ + g_assert_not_reached (); + return 0; +} + +static gpointer +pango2_font_family_get_item (GListModel *list, + guint position) +{ + g_assert_not_reached (); + return NULL; +} + +static void +pango2_font_family_list_model_init (GListModelInterface *iface) +{ + iface->get_item_type = pango2_font_family_get_item_type; + iface->get_n_items = pango2_font_family_get_n_items; + iface->get_item = pango2_font_family_get_item; +} + +/* }}} */ +/* {{{ Pango2FontFamily implementation */ + +enum { + PROP_NAME = 1, + PROP_ITEM_TYPE, + PROP_N_ITEMS, + N_PROPERTIES +}; + +static GParamSpec *properties[N_PROPERTIES] = { NULL, }; + +G_DEFINE_ABSTRACT_TYPE_WITH_CODE (Pango2FontFamily, pango2_font_family, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, pango2_font_family_list_model_init)) + +static Pango2FontFace * +pango2_font_family_real_get_face (Pango2FontFamily *family, + const char *name) +{ + Pango2FontFace *face; + + face = NULL; + + for (int i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (family)); i++) + { + Pango2FontFace *f = g_list_model_get_item (G_LIST_MODEL (family), i); + g_object_unref (f); + if (name == NULL || + strcmp (name, pango2_font_face_get_name (f)) == 0) + { + face = f; + break; + } + } + + return face; +} + +static void +pango2_font_family_finalize (GObject *object) +{ + Pango2FontFamily *family = PANGO2_FONT_FAMILY (object); + + g_free (family->name); + if (family->map) + g_object_remove_weak_pointer (G_OBJECT (family->map), (gpointer *)&family->map); + + G_OBJECT_CLASS (pango2_font_family_parent_class)->finalize (object); +} + +static void +pango2_font_family_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + Pango2FontFamily *family = PANGO2_FONT_FAMILY (object); + + switch (property_id) + { + case PROP_NAME: + g_value_set_string (value, family->name); + break; + + case PROP_ITEM_TYPE: + g_value_set_gtype (value, PANGO2_TYPE_FONT); + break; + + case PROP_N_ITEMS: + g_value_set_uint (value, pango2_font_family_get_n_items (G_LIST_MODEL (object))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +pango2_font_family_class_init (Pango2FontFamilyClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->finalize = pango2_font_family_finalize; + object_class->get_property = pango2_font_family_get_property; + + class->get_face = pango2_font_family_real_get_face; + + /** + * Pango2FontFamily:name: (attributes org.gtk.Property.get=pango2_font_family_get_name) + * + * The name of the family. + */ + properties[PROP_NAME] = + g_param_spec_string ("name", NULL, NULL, NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2FontFamily:item-type: + * + * The type of objects that the family contains. + */ + properties[PROP_ITEM_TYPE] = + g_param_spec_gtype ("item-type", NULL, NULL, PANGO2_TYPE_FONT_FACE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2FontFamily:n-items: + * + * The number of faces contained in the family. + */ + properties[PROP_N_ITEMS] = + g_param_spec_uint ("n-items", "", "", 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPERTIES, properties); +} + +static void +pango2_font_family_init (Pango2FontFamily *family G_GNUC_UNUSED) +{ +} + +/* }}} */ +/* {{{ Public API */ + +/** + * pango2_font_family_get_font_map: + * @family: a `Pango2FontFamily` + * + * Returns the `Pango2FontMap that @family belongs to. + * + * Note that the family maintains a *weak* reference to + * the font map, so if all references to font map are + * dropped, the font map will be finalized even if there + * are fonts created with the font map that are still alive. + * In that case this function will return %NULL. + * + * It is the responsibility of the user to ensure that the + * font map is kept alive. In most uses this is not an issue + * as a `Pango2Context` holds a reference to the font map. + + * + * Return value: (transfer none) (nullable): the `Pango2FontMap + */ +Pango2FontMap * +pango2_font_family_get_font_map (Pango2FontFamily *family) +{ + return family->map; +} + +/** + * pango2_font_family_get_name: + * @family: a `Pango2FontFamily` + * + * Gets the name of the family. + * + * The name is unique among all fonts for the font backend and can + * be used in a `Pango2FontDescription` to specify that a face from + * this family is desired. + * + * Return value: the name of the family. This string is owned + * by the family object and must not be modified or freed. + */ +const char * +pango2_font_family_get_name (Pango2FontFamily *family) +{ + g_return_val_if_fail (PANGO2_IS_FONT_FAMILY (family), NULL); + + return family->name; +} + +/** + * pango2_font_family_get_face: + * @family: a `Pango2FontFamily` + * @name: (nullable): the name of a face. If the name is `NULL`, + * the family's default face (fontconfig calls it "Regular") + * will be returned. + * + * Gets the `Pango2FontFace` of the family with the given name. + * + * Returns: (transfer none) (nullable): the `Pango2FontFace`, + * or `NULL` if no face with the given name exists. + */ +Pango2FontFace * +pango2_font_family_get_face (Pango2FontFamily *family, + const char *name) +{ + g_return_val_if_fail (PANGO2_IS_FONT_FAMILY (family), NULL); + + return PANGO2_FONT_FAMILY_GET_CLASS (family)->get_face (family, name); +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pango-font-family.h b/pango2/pango-font-family.h new file mode 100644 index 00000000..f6896530 --- /dev/null +++ b/pango2/pango-font-family.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2000 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-types.h> + +#include <glib-object.h> + +G_BEGIN_DECLS + + +#define PANGO2_TYPE_FONT_FAMILY (pango2_font_family_get_type ()) + +PANGO2_AVAILABLE_IN_ALL +PANGO2_DECLARE_INTERNAL_TYPE (Pango2FontFamily, pango2_font_family, PANGO2, FONT_FAMILY, GObject) + +PANGO2_AVAILABLE_IN_ALL +Pango2FontMap * pango2_font_family_get_font_map (Pango2FontFamily *family); + +PANGO2_AVAILABLE_IN_ALL +const char * pango2_font_family_get_name (Pango2FontFamily *family) G_GNUC_PURE; + +PANGO2_AVAILABLE_IN_ALL +Pango2FontFace * pango2_font_family_get_face (Pango2FontFamily *family, + const char *name); + +G_END_DECLS diff --git a/pango2/pango-font-metrics-private.h b/pango2/pango-font-metrics-private.h new file mode 100644 index 00000000..3ad27a3a --- /dev/null +++ b/pango2/pango-font-metrics-private.h @@ -0,0 +1,40 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-font-metrics.h" + +struct _Pango2FontMetrics +{ + /* <private> */ + guint ref_count; + + int ascent; + int descent; + int height; + int approximate_char_width; + int approximate_digit_width; + int underline_position; + int underline_thickness; + int strikethrough_position; + int strikethrough_thickness; +}; + +Pango2FontMetrics *pango2_font_metrics_new (void); diff --git a/pango2/pango-font-metrics.c b/pango2/pango-font-metrics.c new file mode 100644 index 00000000..d2b61d44 --- /dev/null +++ b/pango2/pango-font-metrics.c @@ -0,0 +1,249 @@ +/* Pango2 + * pango-font-metrics.c: + * + * Copyright (C) 1999 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include "pango-font-metrics-private.h" + + +G_DEFINE_BOXED_TYPE (Pango2FontMetrics, pango2_font_metrics, + pango2_font_metrics_copy, + pango2_font_metrics_free); + +/** + * pango2_font_metrics_new: + * + * Creates a new `Pango2FontMetrics` structure. + * + * This is only for internal use by Pango2 backends and there is + * no public way to set the fields of the structure. + * + * Return value: a newly-created `Pango2FontMetrics` structure + */ +Pango2FontMetrics * +pango2_font_metrics_new (void) +{ + Pango2FontMetrics *metrics = g_slice_new0 (Pango2FontMetrics); + + return metrics; +} + +/** + * pango2_font_metrics_copy: + * @metrics: (nullable): a `Pango2FontMetrics` structure, may be %NULL + * + * Create a copy of @metrics. + * + * Return value: (nullable): @metrics + */ +Pango2FontMetrics * +pango2_font_metrics_copy (Pango2FontMetrics *metrics) +{ + return g_slice_dup (Pango2FontMetrics, metrics); +} + +/** + * pango2_font_metrics_free: + * @metrics: (nullable): a `Pango2FontMetrics` structure, may be %NULL + * + * Free the @metrics. + */ +void +pango2_font_metrics_free (Pango2FontMetrics *metrics) +{ + g_slice_free (Pango2FontMetrics, metrics); +} + +/** + * pango2_font_metrics_get_ascent: + * @metrics: a `Pango2FontMetrics` structure + * + * Gets the ascent from a font metrics structure. + * + * The ascent is the distance from the baseline to the logical top + * of a line of text. (The logical top may be above or below the top + * of the actual drawn ink. It is necessary to lay out the text to + * figure where the ink will be.) + * + * Return value: the ascent, in Pango2 units. + */ +int +pango2_font_metrics_get_ascent (Pango2FontMetrics *metrics) +{ + g_return_val_if_fail (metrics != NULL, 0); + + return metrics->ascent; +} + +/** + * pango2_font_metrics_get_descent: + * @metrics: a `Pango2FontMetrics` structure + * + * Gets the descent from a font metrics structure. + * + * The descent is the distance from the baseline to the logical bottom + * of a line of text. (The logical bottom may be above or below the + * bottom of the actual drawn ink. It is necessary to lay out the text + * to figure where the ink will be.) + * + * Return value: the descent, in Pango2 units. + */ +int +pango2_font_metrics_get_descent (Pango2FontMetrics *metrics) +{ + g_return_val_if_fail (metrics != NULL, 0); + + return metrics->descent; +} + +/** + * pango2_font_metrics_get_height: + * @metrics: a `Pango2FontMetrics` structure + * + * Gets the line height from a font metrics structure. + * + * The line height is the recommended distance between successive + * baselines in wrapped text using this font. + * + * If the line height is not available, 0 is returned. + * + * Return value: the height, in Pango2 units + */ +int +pango2_font_metrics_get_height (Pango2FontMetrics *metrics) +{ + g_return_val_if_fail (metrics != NULL, 0); + + return metrics->height; +} + +/** + * pango2_font_metrics_get_approximate_char_width: + * @metrics: a `Pango2FontMetrics` structure + * + * Gets the approximate character width for a font metrics structure. + * + * This is merely a representative value useful, for example, for + * determining the initial size for a window. Actual characters in + * text will be wider and narrower than this. + * + * Return value: the character width, in Pango2 units. + */ +int +pango2_font_metrics_get_approximate_char_width (Pango2FontMetrics *metrics) +{ + g_return_val_if_fail (metrics != NULL, 0); + + return metrics->approximate_char_width; +} + +/** + * pango2_font_metrics_get_approximate_digit_width: + * @metrics: a `Pango2FontMetrics` structure + * + * Gets the approximate digit width for a font metrics structure. + * + * This is merely a representative value useful, for example, for + * determining the initial size for a window. Actual digits in + * text can be wider or narrower than this, though this value + * is generally somewhat more accurate than the result of + * pango2_font_metrics_get_approximate_char_width() for digits. + * + * Return value: the digit width, in Pango2 units. + */ +int +pango2_font_metrics_get_approximate_digit_width (Pango2FontMetrics *metrics) +{ + g_return_val_if_fail (metrics != NULL, 0); + + return metrics->approximate_digit_width; +} + +/** + * pango2_font_metrics_get_underline_position: + * @metrics: a `Pango2FontMetrics` structure + * + * Gets the suggested position to draw the underline. + * + * The value returned is the distance *above* the baseline of the top + * of the underline. Since most fonts have underline positions beneath + * the baseline, this value is typically negative. + * + * Return value: the suggested underline position, in Pango2 units. + */ +int +pango2_font_metrics_get_underline_position (Pango2FontMetrics *metrics) +{ + g_return_val_if_fail (metrics != NULL, 0); + + return metrics->underline_position; +} + +/** + * pango2_font_metrics_get_underline_thickness: + * @metrics: a `Pango2FontMetrics` structure + * + * Gets the suggested thickness to draw for the underline. + * + * Return value: the suggested underline thickness, in Pango2 units. + */ +int +pango2_font_metrics_get_underline_thickness (Pango2FontMetrics *metrics) +{ + g_return_val_if_fail (metrics != NULL, 0); + + return metrics->underline_thickness; +} + +/** + * pango2_font_metrics_get_strikethrough_position: + * @metrics: a `Pango2FontMetrics` structure + * + * Gets the suggested position to draw the strikethrough. + * + * The value returned is the distance *above* the + * baseline of the top of the strikethrough. + * + * Return value: the suggested strikethrough position, in Pango2 units. + */ +int +pango2_font_metrics_get_strikethrough_position (Pango2FontMetrics *metrics) +{ + g_return_val_if_fail (metrics != NULL, 0); + + return metrics->strikethrough_position; +} + +/** + * pango2_font_metrics_get_strikethrough_thickness: + * @metrics: a `Pango2FontMetrics` structure + * + * Gets the suggested thickness to draw for the strikethrough. + * + * Return value: the suggested strikethrough thickness, in Pango2 units. + */ +int +pango2_font_metrics_get_strikethrough_thickness (Pango2FontMetrics *metrics) +{ + g_return_val_if_fail (metrics != NULL, 0); + + return metrics->strikethrough_thickness; +} diff --git a/pango2/pango-font-metrics.h b/pango2/pango-font-metrics.h new file mode 100644 index 00000000..e138be7d --- /dev/null +++ b/pango2/pango-font-metrics.h @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2000 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-types.h> + +#include <glib-object.h> + +G_BEGIN_DECLS + +/** + * Pango2FontMetrics: + * + * A `Pango2FontMetrics` structure holds the overall metric information + * for a font. + * + * The information in a `Pango2FontMetrics` structure may be restricted + * to a script. The fields of this structure are private to implementations + * of a font backend. See the documentation of the corresponding getters + * for documentation of their meaning. + * + * For an overview of the most important metrics, see: + * + * <picture> + * <source srcset="fontmetrics-dark.png" media="(prefers-color-scheme: dark)"> + * <img alt="Font metrics" src="fontmetrics-light.png"> + * </picture> + + */ +typedef struct _Pango2FontMetrics Pango2FontMetrics; + +#define PANGO2_TYPE_FONT_METRICS (pango2_font_metrics_get_type ()) + +PANGO2_AVAILABLE_IN_ALL +GType pango2_font_metrics_get_type (void) G_GNUC_CONST; +PANGO2_AVAILABLE_IN_ALL +Pango2FontMetrics *pango2_font_metrics_copy (Pango2FontMetrics *metrics); +PANGO2_AVAILABLE_IN_ALL +void pango2_font_metrics_free (Pango2FontMetrics *metrics); +PANGO2_AVAILABLE_IN_ALL +int pango2_font_metrics_get_ascent (Pango2FontMetrics *metrics) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +int pango2_font_metrics_get_descent (Pango2FontMetrics *metrics) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +int pango2_font_metrics_get_height (Pango2FontMetrics *metrics) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +int pango2_font_metrics_get_approximate_char_width (Pango2FontMetrics *metrics) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +int pango2_font_metrics_get_approximate_digit_width (Pango2FontMetrics *metrics) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +int pango2_font_metrics_get_underline_position (Pango2FontMetrics *metrics) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +int pango2_font_metrics_get_underline_thickness (Pango2FontMetrics *metrics) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +int pango2_font_metrics_get_strikethrough_position (Pango2FontMetrics *metrics) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +int pango2_font_metrics_get_strikethrough_thickness (Pango2FontMetrics *metrics) G_GNUC_PURE; + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(Pango2FontMetrics, pango2_font_metrics_free) + +G_END_DECLS diff --git a/pango2/pango-font-private.h b/pango2/pango-font-private.h new file mode 100644 index 00000000..ca8d3a77 --- /dev/null +++ b/pango2/pango-font-private.h @@ -0,0 +1,128 @@ +/* + * Copyright 2000 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-font-family.h" +#include "pango-font.h" +#include "pango-types.h" + +#include <glib-object.h> + +#ifdef HAVE_CAIRO +#include <cairo.h> +#endif + +struct _Pango2Font +{ + GObject parent_instance; + + Pango2FontFace *face; + + hb_font_t *hb_font; + + int size; /* point size, scaled by PANGO2_SCALE */ + float dpi; + Pango2Gravity gravity; + Pango2Matrix ctm; + +#ifdef HAVE_CAIRO + cairo_font_options_t *options; +#endif +}; + +typedef struct _Pango2FontClass Pango2FontClass; +struct _Pango2FontClass +{ + GObjectClass parent_class; + + Pango2FontDescription * (* describe) (Pango2Font *font); + void (* get_glyph_extents) (Pango2Font *font, + Pango2Glyph glyph, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect); + Pango2FontMetrics * (* get_metrics) (Pango2Font *font, + Pango2Language *language); + hb_font_t * (* create_hb_font) (Pango2Font *font); + gboolean (* is_hinted) (Pango2Font *font); + void (* get_scale_factors) (Pango2Font *font, + double *x_scale, + double *y_scale); + void (* get_transform) (Pango2Font *font, + Pango2Matrix *matrix); +}; + +#define PANGO2_FONT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), PANGO2_TYPE_FONT, Pango2FontClass)) +#define PANGO2_FONT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), PANGO2_TYPE_FONT, Pango2FontClass)) + +static inline void +pango2_font_set_face (Pango2Font *font, + Pango2FontFace *face) +{ + font->face = (Pango2FontFace *) g_object_ref (face); +} + +static inline void +pango2_font_set_size (Pango2Font *font, + int size) +{ + font->size = size; +} + +static inline void +pango2_font_set_dpi (Pango2Font *font, + float dpi) +{ + font->dpi = dpi; +} + +static inline void +pango2_font_set_gravity (Pango2Font *font, + Pango2Gravity gravity) +{ + font->gravity = gravity; +} + +static inline void +pango2_font_set_ctm (Pango2Font *font, + const Pango2Matrix *ctm) +{ + const Pango2Matrix matrix_init = PANGO2_MATRIX_INIT; + font->ctm = ctm ? *ctm : matrix_init; +} + +gboolean pango2_font_is_hinted (Pango2Font *font); +void pango2_font_get_scale_factors (Pango2Font *font, + double *x_scale, + double *y_scale); +void pango2_font_get_transform (Pango2Font *font, + Pango2Matrix *matrix); + +gboolean pango2_font_description_is_similar (const Pango2FontDescription *a, + const Pango2FontDescription *b); + +int pango2_font_description_compute_distance (const Pango2FontDescription *a, + const Pango2FontDescription *b); + +/* We use these values in a few places as a fallback size for an + * unknown glyph, if we have no better information. + */ + +#define PANGO2_UNKNOWN_GLYPH_WIDTH 10 +#define PANGO2_UNKNOWN_GLYPH_HEIGHT 14 diff --git a/pango2/pango-font.c b/pango2/pango-font.c new file mode 100644 index 00000000..cdf2fc42 --- /dev/null +++ b/pango2/pango-font.c @@ -0,0 +1,478 @@ +/* Pango2 + * fonts.c: + * + * Copyright (C) 1999 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include <stdlib.h> +#include <math.h> +#include <string.h> + +#include <gio/gio.h> + +#include "pango-types.h" +#include "pango-font-private.h" +#include "pango-font-metrics-private.h" +#include "pango-fontmap-private.h" +#include "pango-impl-utils.h" + +#include <hb-gobject.h> + +/** + * Pango2Font: + * + * A `Pango2Font` is used to represent a font in a + * rendering-system-independent manner. + */ + +/* }}} */ +/* {{{ Pango2Font implementation */ + +enum { + PROP_FACE = 1, + PROP_HB_FONT, + PROP_SIZE, + PROP_DPI, + PROP_GRAVITY, + N_PROPERTIES +}; + +static GParamSpec *properties[N_PROPERTIES] = { NULL, }; + +G_DEFINE_ABSTRACT_TYPE (Pango2Font, pango2_font, G_TYPE_OBJECT) + +static void +pango2_font_finalize (GObject *object) +{ + Pango2Font *font = PANGO2_FONT (object); + + g_object_unref (font->face); + hb_font_destroy (font->hb_font); + + G_OBJECT_CLASS (pango2_font_parent_class)->finalize (object); +} + +static void +pango2_font_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + Pango2Font *font = PANGO2_FONT (object); + + switch (property_id) + { + case PROP_FACE: + g_value_set_object (value, font->face); + break; + + case PROP_HB_FONT: + g_value_set_boxed (value, pango2_font_get_hb_font (font)); + break; + + case PROP_SIZE: + g_value_set_int (value, font->size); + break; + + case PROP_DPI: + g_value_set_float (value, font->dpi); + break; + + case PROP_GRAVITY: + g_value_set_enum (value, font->gravity); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static gboolean +pango2_font_default_is_hinted (Pango2Font *font) +{ + return FALSE; +} + +static void +pango2_font_default_get_scale_factors (Pango2Font *font, + double *x_scale, + double *y_scale) +{ + *x_scale = *y_scale = 1.0; +} + +static void +pango2_font_default_get_transform (Pango2Font *font, + Pango2Matrix *matrix) +{ + *matrix = (Pango2Matrix) PANGO2_MATRIX_INIT; +} + +static void +pango2_font_class_init (Pango2FontClass *class G_GNUC_UNUSED) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->finalize = pango2_font_finalize; + object_class->get_property = pango2_font_get_property; + + class->is_hinted = pango2_font_default_is_hinted; + class->get_scale_factors = pango2_font_default_get_scale_factors; + class->get_transform = pango2_font_default_get_transform; + + /** + * Pango2Font:face: (attributes org.gtk.Property.get=pango2_font_get_face) + * + * The face to which the font belongs. + */ + properties[PROP_FACE] = + g_param_spec_object ("face", NULL, NULL, PANGO2_TYPE_FONT_FACE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2Font:hb-font: (attributes org.gtk.Property.get=pango2_font_get_hb_font) + * + * A `hb_font_t` object backing this font. + */ + properties[PROP_HB_FONT] = + g_param_spec_boxed ("hb-font", NULL, NULL, HB_GOBJECT_TYPE_FONT, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2Font:size: (attributes org.gtk.Property.get=pango2_font_get_size) + * + * The size of the font, scaled by `PANGO2_SCALE`. + */ + properties[PROP_SIZE] = + g_param_spec_int ("size", NULL, NULL, 0, G_MAXINT, 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2Font:dpi: + * + * The resolution at which the font is rendered. + * + * The pixel size of the font is computed as + * + * size * dpi / 72. + */ + properties[PROP_DPI] = + g_param_spec_float ("dpi", NULL, NULL, 0, G_MAXFLOAT, 96.0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2Font:gravity: (attributes org.gtk.Property.get=pango2_font_get_gravity) + * + * The gravity of the font. + */ + properties[PROP_GRAVITY] = + g_param_spec_enum ("gravity", NULL, NULL, PANGO2_TYPE_GRAVITY, + PANGO2_GRAVITY_AUTO, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPERTIES, properties); +} + +static void +pango2_font_init (Pango2Font *font) +{ + font->gravity = PANGO2_GRAVITY_AUTO; + font->ctm = (Pango2Matrix) PANGO2_MATRIX_INIT; +} + +/* }}} */ +/* {{{ Private API */ + +/*< private > + * pango2_font_get_transform: + * @font: a `Pango2Font` + * @matrix: the matrix to fill in + * + * Gets the matrix for the transformation from 'font space' to 'user space'. + */ +void +pango2_font_get_transform (Pango2Font *font, + Pango2Matrix *matrix) +{ + PANGO2_FONT_GET_CLASS (font)->get_transform (font, matrix); +} + +/*< private > + * pango2_font_is_hinted: + * @font: a `Pango2Font` + * + * Gets whether this font is hinted. + * + * Returns: %TRUE if @font is hinted + */ +gboolean +pango2_font_is_hinted (Pango2Font *font) +{ + return PANGO2_FONT_GET_CLASS (font)->is_hinted (font); +} + +/*< private > + * pango2_font_get_scale_factors: + * @font: a `Pango2Font` + * @x_scale: return location for X scale + * @y_scale: return location for Y scale + * + * Gets the font scale factors of the ctm for this font. + * + * The ctm is the matrix set on the context that this font was + * loaded for. + */ +void +pango2_font_get_scale_factors (Pango2Font *font, + double *x_scale, + double *y_scale) +{ + PANGO2_FONT_GET_CLASS (font)->get_scale_factors (font, x_scale, y_scale); +} + +/* }}} */ +/* {{{ Public API */ + +/** + * pango2_font_describe: + * @font: a `Pango2Font` + * + * Returns a description of the font, with font size set in points. + * + * Use [method@Pango2.Font.describe_with_absolute_size] if you want + * the font size in device units. + * + * Return value: a newly-allocated `Pango2FontDescription` object. + */ +Pango2FontDescription * +pango2_font_describe (Pango2Font *font) +{ + g_return_val_if_fail (font != NULL, NULL); + + return PANGO2_FONT_GET_CLASS (font)->describe (font); +} + +/** + * pango2_font_describe_with_absolute_size: + * @font: a `Pango2Font` + * + * Returns a description of the font, with absolute font size set + * in device units. + * + * Use [method@Pango2.Font.describe] if you want the font size in points. + * + * Return value: a newly-allocated `Pango2FontDescription` object. + */ +Pango2FontDescription * +pango2_font_describe_with_absolute_size (Pango2Font *font) +{ + Pango2FontDescription *desc; + + g_return_val_if_fail (font != NULL, NULL); + + desc = pango2_font_describe (font); + pango2_font_description_set_absolute_size (desc, font->size * font->dpi / 72.); + + return desc; +} + +/** + * pango2_font_get_glyph_extents: + * @font: (nullable): a `Pango2Font` + * @glyph: the glyph index + * @ink_rect: (out) (optional): rectangle used to store the extents of the glyph as drawn + * @logical_rect: (out) (optional): rectangle used to store the logical extents of the glyph + * + * Gets the logical and ink extents of a glyph within a font. + * + * The coordinate system for each rectangle has its origin at the + * base line and horizontal origin of the character with increasing + * coordinates extending to the right and down. The macros PANGO2_ASCENT(), + * PANGO2_DESCENT(), PANGO2_LBEARING(), and PANGO2_RBEARING() can be used to convert + * from the extents rectangle to more traditional font metrics. The units + * of the rectangles are in 1/PANGO2_SCALE of a device unit. + * + * If @font is %NULL, this function gracefully sets some sane values in the + * output variables and returns. + */ +void +pango2_font_get_glyph_extents (Pango2Font *font, + Pango2Glyph glyph, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect) +{ + if (G_UNLIKELY (!font)) + { + if (ink_rect) + { + ink_rect->x = PANGO2_SCALE; + ink_rect->y = - (PANGO2_UNKNOWN_GLYPH_HEIGHT - 1) * PANGO2_SCALE; + ink_rect->height = (PANGO2_UNKNOWN_GLYPH_HEIGHT - 2) * PANGO2_SCALE; + ink_rect->width = (PANGO2_UNKNOWN_GLYPH_WIDTH - 2) * PANGO2_SCALE; + } + if (logical_rect) + { + logical_rect->x = 0; + logical_rect->y = - PANGO2_UNKNOWN_GLYPH_HEIGHT * PANGO2_SCALE; + logical_rect->height = PANGO2_UNKNOWN_GLYPH_HEIGHT * PANGO2_SCALE; + logical_rect->width = PANGO2_UNKNOWN_GLYPH_WIDTH * PANGO2_SCALE; + } + return; + } + + PANGO2_FONT_GET_CLASS (font)->get_glyph_extents (font, glyph, ink_rect, logical_rect); +} + +/** + * pango2_font_get_metrics: + * @font: (nullable): a `Pango2Font` + * @language: (nullable): language tag used to determine which script + * to get the metrics for, or %NULL to indicate to get the metrics for + * the entire font. + * + * Gets overall metric information for a font. + * + * Since the metrics may be substantially different for different scripts, + * a language tag can be provided to indicate that the metrics should be + * retrieved that correspond to the script(s) used by that language. + * + * If @font is %NULL, this function gracefully sets some sane values in the + * output variables and returns. + * + * Return value: a `Pango2FontMetrics` object. The caller must call + * [method@Pango2.FontMetrics.free] when finished using the object. + */ +Pango2FontMetrics * +pango2_font_get_metrics (Pango2Font *font, + Pango2Language *language) +{ + if (G_UNLIKELY (!font)) + { + Pango2FontMetrics *metrics = pango2_font_metrics_new (); + + metrics->ascent = PANGO2_SCALE * PANGO2_UNKNOWN_GLYPH_HEIGHT; + metrics->descent = 0; + metrics->height = 0; + metrics->approximate_char_width = PANGO2_SCALE * PANGO2_UNKNOWN_GLYPH_WIDTH; + metrics->approximate_digit_width = PANGO2_SCALE * PANGO2_UNKNOWN_GLYPH_WIDTH; + metrics->underline_position = -PANGO2_SCALE; + metrics->underline_thickness = PANGO2_SCALE; + metrics->strikethrough_position = PANGO2_SCALE * PANGO2_UNKNOWN_GLYPH_HEIGHT / 2; + metrics->strikethrough_thickness = PANGO2_SCALE; + + return metrics; + } + + return PANGO2_FONT_GET_CLASS (font)->get_metrics (font, language); +} + +/** + * pango2_font_get_face: + * @font: a `Pango2Font` + * + * Gets the `Pango2FontFace` to which the font belongs. + * + * Returns: (transfer none): the `Pango2FontFace` + */ +Pango2FontFace * +pango2_font_get_face (Pango2Font *font) +{ + g_return_val_if_fail (PANGO2_IS_FONT (font), NULL); + + return font->face; +} + +/** + * pango2_font_get_hb_font: + * @font: a `Pango2Font` + * + * Get a `hb_font_t` object backing this font. + * + * Note that the objects returned by this function are cached + * and immutable. If you need to make changes to the `hb_font_t`, + * use [hb_font_create_sub_font()](https://harfbuzz.github.io/harfbuzz-hb-font.html#hb-font-create-sub-font). + * + * Returns: (transfer none) (nullable): the `hb_font_t` object + * backing the font + */ +hb_font_t * +pango2_font_get_hb_font (Pango2Font *font) +{ + g_return_val_if_fail (PANGO2_IS_FONT (font), NULL); + + if (!font->hb_font) + { + font->hb_font = PANGO2_FONT_GET_CLASS (font)->create_hb_font (font); + hb_font_make_immutable (font->hb_font); + } + + return font->hb_font; +} + +/** + * pango2_font_get_size: + * @font: a `Pango2Font` + * + * Returns the size of the font, scaled by `PANGO2_SCALE`. + * + * Return value: the size of the font + */ +int +pango2_font_get_size (Pango2Font *font) +{ + g_return_val_if_fail (PANGO2_IS_FONT (font), 0); + + return font->size; +} + +/** + * pango2_font_get_absolute_size: + * @font: a `Pango2Font` + * + * Returns the pixel size of the font, scaled by `PANGO2_SCALE`. + * + * Return value: the pixel size of the font + */ +double +pango2_font_get_absolute_size (Pango2Font *font) +{ + g_return_val_if_fail (PANGO2_IS_FONT (font), 0.); + + return font->size * font->dpi / 72.; +} + +/** + * pango2_font_get_gravity: + * @font: a `Pango2Font` + * + * Returns the gravity of the font. + * + * Return value: the gravity of the font + */ +Pango2Gravity +pango2_font_get_gravity (Pango2Font *font) +{ + g_return_val_if_fail (PANGO2_IS_FONT (font), PANGO2_GRAVITY_SOUTH); + + return font->gravity; +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pango-font.h b/pango2/pango-font.h new file mode 100644 index 00000000..63e98351 --- /dev/null +++ b/pango2/pango-font.h @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2000 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-types.h> +#include <pango2/pango-font-description.h> +#include <pango2/pango-font-metrics.h> +#include <pango2/pango-font-family.h> + +#include <glib-object.h> +#include <hb.h> + +G_BEGIN_DECLS + + +#define PANGO2_TYPE_FONT (pango2_font_get_type ()) + +PANGO2_AVAILABLE_IN_ALL +PANGO2_DECLARE_INTERNAL_TYPE (Pango2Font, pango2_font, PANGO2, FONT, GObject) + +PANGO2_AVAILABLE_IN_ALL +Pango2FontDescription * pango2_font_describe (Pango2Font *font); +PANGO2_AVAILABLE_IN_ALL +Pango2FontDescription * pango2_font_describe_with_absolute_size (Pango2Font *font); +PANGO2_AVAILABLE_IN_ALL +Pango2FontMetrics * pango2_font_get_metrics (Pango2Font *font, + Pango2Language *language); +PANGO2_AVAILABLE_IN_ALL +void pango2_font_get_glyph_extents (Pango2Font *font, + Pango2Glyph glyph, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect); + +PANGO2_AVAILABLE_IN_ALL +Pango2FontFace * pango2_font_get_face (Pango2Font *font); + +PANGO2_AVAILABLE_IN_ALL +hb_font_t * pango2_font_get_hb_font (Pango2Font *font); + +PANGO2_AVAILABLE_IN_ALL +int pango2_font_get_size (Pango2Font *font); + +PANGO2_AVAILABLE_IN_ALL +double pango2_font_get_absolute_size (Pango2Font *font); + +PANGO2_AVAILABLE_IN_ALL +Pango2Gravity pango2_font_get_gravity (Pango2Font *font); + +PANGO2_AVAILABLE_IN_ALL +GBytes * pango2_font_serialize (Pango2Font *font); + +PANGO2_AVAILABLE_IN_ALL +Pango2Font * pango2_font_deserialize (Pango2Context *context, + GBytes *bytes, + GError **error); + +G_END_DECLS diff --git a/pango2/pango-fontmap-private.h b/pango2/pango-fontmap-private.h new file mode 100644 index 00000000..b8388836 --- /dev/null +++ b/pango2/pango-fontmap-private.h @@ -0,0 +1,74 @@ +/* + * Copyright 2021 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-fontmap.h" +#include "pango-hbfamily-private.h" +#include "pango-fontmap-private.h" + + +G_BEGIN_DECLS + +struct _Pango2FontMap +{ + GObject parent_instance; + + GPtrArray *added_faces; + GPtrArray *added_families; + GHashTable *families_hash; + GPtrArray *families; + GHashTable *fontsets; + GQueue fontset_cache; + Pango2FontMap *fallback; + + float dpi; + gboolean in_populate; + guint serial; +}; + +/** + * Pango2FontMapClass: + * @populate: Subclasses should call pango2_font_map_add_face to populate + * the map with faces and families in this vfunc. + */ +struct _Pango2FontMapClass +{ + GObjectClass parent_class; + + Pango2Font * (* load_font) (Pango2FontMap *self, + Pango2Context *context, + const Pango2FontDescription *desc); + Pango2Fontset * (* load_fontset) (Pango2FontMap *self, + Pango2Context *context, + const Pango2FontDescription *desc, + Pango2Language *language); + guint (* get_serial) (Pango2FontMap *self); + void (* changed) (Pango2FontMap *self); + Pango2FontFamily * (* get_family) (Pango2FontMap *self, + const char *name); + void (* populate) (Pango2FontMap *self); +}; + +void pango2_font_map_repopulate (Pango2FontMap *self, + gboolean add_synthetic); + +void pango2_font_map_changed (Pango2FontMap *self); + +G_END_DECLS diff --git a/pango2/pango-fontmap.c b/pango2/pango-fontmap.c new file mode 100644 index 00000000..fd87c9d0 --- /dev/null +++ b/pango2/pango-fontmap.c @@ -0,0 +1,1294 @@ +/* Pango2 + * + * Copyright (C) 2021 Matthias Clasen + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include <math.h> + +#include <gio/gio.h> + +#include "pango-fontmap-private.h" +#include "pango-hbfamily-private.h" +#include "pango-generic-family-private.h" +#include "pango-hbface-private.h" +#include "pango-hbfont-private.h" +#include "pango-fontset-cached-private.h" +#include "pango-userface-private.h" +#include "pango-userfont-private.h" +#include "pango-fontset.h" +#include "pango-font-face-private.h" +#include "pango-trace-private.h" +#include "pango-context.h" + +#ifdef HAVE_CORE_TEXT +#include "pangocoretext-fontmap.h" +#endif + +#ifdef HAVE_DIRECT_WRITE +#include "pangodwrite-fontmap.h" +#endif + +#ifdef HAVE_FONTCONFIG +#include "pangofc-fontmap.h" +#endif + +#ifdef HAVE_CAIRO +#include <cairo.h> +#include <pangocairo-private.h> +#endif + +#include <hb-ot.h> + + +/** + * Pango2FontMap: + * + * `Pango2FontMap` is the base class for font enumeration. + * It also handles caching and lookup of faces and fonts. + * To obtain fonts from a `Pango2FontMap`, use [method@Pango2.FontMap.load_font] + * or [method@Pango2.FontMap.load_fontset]. + * + * Subclasses populate the fontmap using backend-specific APIs + * to enumerate the available fonts on the sytem, but it is + * also possible to create an instance of `Pango2FontMap` and + * populate it manually using [method@Pango2.FontMap.add_file] + * and [method@Pango2.FontMap.add_face]. + * + * Fontmaps can be combined using [method@Pango2.FontMap.set_fallback]. + * This can be useful to add custom fonts to the default fonts + * without making them available to every user of the default + * fontmap. + * + * Note that to be fully functional, a fontmap needs to provide + * generic families for monospace and sans-serif. These can + * be added using [method@Pango2.FontMap.add_family] and + * [ctor@Pango2.GenericFamily.new]. + * + * `Pango2FontMap` implements the [iface@Gio.ListModel] interface, + * to provide a list of font families. + */ + + +/* The central api is load_fontset, which takes a font description + * and language, finds the matching faces, and creates a Pango2Fontset + * for them. To speed this operation up, the font map maintains a + * cache of fontsets (in the form of a GQueue) and has a hash table + * for looking up existing fontsets. + * + * The Pango2FontsetCached object is the fontset subclass that is used + * here, and it contains the necessary data for the cashing and hashing. + * Pango2FontsetCached also caches the character-to-font mapping that is + * used when itemizing text. + */ + + +/* {{{ GListModel implementation */ + +static GType +pango2_font_map_get_item_type (GListModel *list) +{ + return PANGO2_TYPE_FONT_FAMILY; +} + +static guint +pango2_font_map_get_n_items (GListModel *list) +{ + Pango2FontMap *self = PANGO2_FONT_MAP (list); + + if (self->fallback) + return self->families->len + g_list_model_get_n_items (G_LIST_MODEL (self->fallback)); + else + return self->families->len; +} + +static gpointer +pango2_font_map_get_item (GListModel *list, + guint position) +{ + Pango2FontMap *self = PANGO2_FONT_MAP (list); + + if (position < self->families->len) + return g_object_ref (g_ptr_array_index (self->families, position)); + else if (self->fallback) + return g_list_model_get_item (G_LIST_MODEL (self->fallback), position); + + return NULL; +} + +static void +pango2_font_map_list_model_init (GListModelInterface *iface) +{ + iface->get_item_type = pango2_font_map_get_item_type; + iface->get_n_items = pango2_font_map_get_n_items; + iface->get_item = pango2_font_map_get_item; +} + +/* }}} */ +/* {{{ Fontset caching */ + +/* The number of fontsets we keep in the fontset cache */ +#define FONTSET_CACHE_SIZE 256 + +#define FNV_32_PRIME ((guint32)0x01000193) +#define FNV1_32_INIT ((guint32)0x811c9dc5) + +static guint32 +hash_bytes_fnv (unsigned char *buffer, + int len, + guint32 hval) +{ + while (len--) + { + hval *= FNV_32_PRIME; + hval ^= *buffer++; + } + + return hval; +} + +static guint +pango2_fontset_cached_hash (const Pango2FontsetCached *fontset) +{ + guint32 hash = FNV1_32_INIT; + + if (fontset->ctm) + hash = hash_bytes_fnv ((unsigned char *)(fontset->ctm), sizeof (double) * 4, hash); + + return (hash ^ + GPOINTER_TO_UINT (fontset->language) ^ +#ifdef HAVE_CAIRO + cairo_font_options_hash (fontset->font_options) ^ +#endif + pango2_font_description_hash (fontset->description)); +} + +static gboolean +pango2_fontset_cached_equal (const Pango2FontsetCached *a, + const Pango2FontsetCached *b) +{ + return a->language == b->language && +#ifdef HAVE_CAIRO + cairo_font_options_equal (a->font_options, b->font_options) && +#endif + (a->ctm == b->ctm || + (a->ctm && b->ctm && memcmp (a->ctm, b->ctm, 4 * sizeof (double)) == 0)) && + pango2_font_description_equal (a->description, b->description); +} + +static void +pango2_fontset_cache (Pango2FontsetCached *fontset, + Pango2FontMap *self) +{ + GQueue *cache = &self->fontset_cache; + GList *link = &fontset->cache_link; + + if (link->data == fontset) + { + /* Already in cache, move to head */ + if (link == cache->head) + return; + + g_queue_unlink (cache, link); + } + else + { + /* Not in cache yet. Make room... */ + if (cache->length == FONTSET_CACHE_SIZE) + { + GList *old = g_queue_pop_tail_link (cache); + g_hash_table_remove (self->fontsets, old->data); + old->data = NULL; + } + } + + link->data = fontset; + link->prev = NULL; + link->next = NULL; + g_queue_push_head_link (cache, link); +} + +/* }}} */ +/* {{{ Utilities */ + +static guint +pango2_family_hash (const Pango2FontFamily *key) +{ + const char *p; + guint32 h = 5381; + + for (p = (const char *)key->name; *p != '\0'; p++) + h = (h << 5) + h + g_ascii_tolower (*p); + + return h; +} + +static gboolean +pango2_family_equal (const Pango2FontFamily *a, + const Pango2FontFamily *b) +{ + return g_ascii_strcasecmp (a->name, b->name) == 0; +} + +static Pango2FontFamily * +find_family (Pango2FontMap *self, + const char *family_name) +{ + Pango2FontFamily lookup; + Pango2FontFamily *family; + + lookup.name = (char *)family_name; + + family = PANGO2_FONT_FAMILY (g_hash_table_lookup (self->families_hash, &lookup)); + + return family; +} + +static void +clear_caches (Pango2FontMap *self) +{ + g_hash_table_remove_all (self->fontsets); + g_queue_init (&self->fontset_cache); +} + +static void +add_style_variation (Pango2HbFamily *family, + Pango2HbFace *face, + Pango2Style style, + Pango2Weight weight) +{ + Pango2Matrix italic_matrix = { 1, 0.2, 0, 1, 0, 0 }; + Pango2FontDescription *desc; + Pango2HbFace *variation; + + desc = pango2_font_description_new (); + pango2_font_description_set_family (desc, pango2_font_family_get_name (PANGO2_FONT_FAMILY (family))); + pango2_font_description_set_style (desc, style); + pango2_font_description_set_weight (desc, weight); + + variation = pango2_hb_face_new_synthetic (face, + style == PANGO2_STYLE_ITALIC ? &italic_matrix : NULL, + weight == PANGO2_WEIGHT_BOLD, + NULL, + desc); + pango2_hb_family_add_face (family, PANGO2_FONT_FACE (variation)); + + pango2_font_description_free (desc); +} + +static void +synthesize_bold_and_italic_faces (Pango2FontMap *map) +{ + for (int i = 0; i < map->families->len; i++) + { + Pango2FontFamily *family = g_ptr_array_index (map->families, i); + Pango2FontFace *regular_face = NULL; + int regular_dist = G_MAXINT; + int bold_dist = G_MAXINT; + gboolean has_italic = FALSE; + gboolean has_bold = FALSE; + gboolean has_bold_italic = FALSE; + + if (PANGO2_IS_GENERIC_FAMILY (family)) + continue; + + for (int j = 0; j < g_list_model_get_n_items (G_LIST_MODEL (family)); j++) + { + Pango2FontFace *face = g_list_model_get_item (G_LIST_MODEL (family), j); + int weight; + Pango2Style style; + int dist; + + if (!PANGO2_IS_HB_FACE (face)) + continue; + + weight = pango2_font_description_get_weight (face->description); + style = pango2_font_description_get_style (face->description); + + if (style == PANGO2_STYLE_NORMAL) + { + dist = abs (weight - (int)PANGO2_WEIGHT_NORMAL); + + if (dist < regular_dist) + { + regular_dist = dist; + if (dist < 150) + regular_face = face; + } + + dist = abs (weight - (int)PANGO2_WEIGHT_BOLD); + + if (dist < bold_dist) + { + bold_dist = dist; + has_bold = dist < 150; + } + } + else + { + if (weight < PANGO2_WEIGHT_SEMIBOLD) + has_italic = TRUE; + else + has_bold_italic = TRUE; + } + + g_object_unref (face); + } + + if (regular_face) + { + if (!has_italic) + add_style_variation (PANGO2_HB_FAMILY (family), + PANGO2_HB_FACE (regular_face), + PANGO2_STYLE_ITALIC, + PANGO2_WEIGHT_NORMAL); + + if (!has_bold) + add_style_variation (PANGO2_HB_FAMILY (family), + PANGO2_HB_FACE (regular_face), + PANGO2_STYLE_NORMAL, + PANGO2_WEIGHT_BOLD); + + if (!has_bold_italic) + add_style_variation (PANGO2_HB_FAMILY (family), + PANGO2_HB_FACE (regular_face), + PANGO2_STYLE_ITALIC, + PANGO2_WEIGHT_BOLD); + } + } +} + +/* }}} */ +/* {{{ Pango2FontMap implementation */ + +enum { + PROP_RESOLUTION = 1, + PROP_FALLBACK, + PROP_ITEM_TYPE, + PROP_N_ITEMS, + N_PROPERTIES +}; + +static GParamSpec *properties[N_PROPERTIES] = { NULL }; + +G_DEFINE_TYPE_WITH_CODE (Pango2FontMap, pango2_font_map, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, pango2_font_map_list_model_init)) + + +static void +pango2_font_map_init (Pango2FontMap *self) +{ + self->added_faces = g_ptr_array_new_with_free_func (g_object_unref); + self->added_families = g_ptr_array_new_with_free_func (g_object_unref); + self->families = g_ptr_array_new_with_free_func (g_object_unref); + self->families_hash = g_hash_table_new_full ((GHashFunc) pango2_family_hash, + (GEqualFunc) pango2_family_equal, + NULL, + NULL); + self->fontsets = g_hash_table_new_full ((GHashFunc) pango2_fontset_cached_hash, + (GEqualFunc) pango2_fontset_cached_equal, + NULL, + (GDestroyNotify) g_object_unref); + g_queue_init (&self->fontset_cache); + self->dpi = 96.; +} + +static void +pango2_font_map_finalize (GObject *object) +{ + Pango2FontMap *self = PANGO2_FONT_MAP (object); + + g_clear_object (&self->fallback); + g_ptr_array_unref (self->added_faces); + g_ptr_array_unref (self->added_families); + g_hash_table_unref (self->families_hash); + g_ptr_array_unref (self->families); + g_hash_table_unref (self->fontsets); + + G_OBJECT_CLASS (pango2_font_map_parent_class)->finalize (object); +} + +static void +pango2_font_map_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + Pango2FontMap *map = PANGO2_FONT_MAP (object); + + switch (property_id) + { + case PROP_RESOLUTION: + pango2_font_map_set_resolution (map, g_value_get_float (value)); + break; + + case PROP_FALLBACK: + pango2_font_map_set_fallback (map, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +pango2_font_map_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + Pango2FontMap *map = PANGO2_FONT_MAP (object); + + switch (property_id) + { + case PROP_RESOLUTION: + g_value_set_float (value, map->dpi); + break; + + case PROP_FALLBACK: + g_value_set_object (value, map->fallback); + break; + + case PROP_ITEM_TYPE: + g_value_set_gtype (value, PANGO2_TYPE_FONT_FAMILY); + break; + + case PROP_N_ITEMS: + g_value_set_uint (value, pango2_font_map_get_n_items (G_LIST_MODEL (object))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +/* Load a font from the first matching family */ +static Pango2Font * +pango2_font_map_default_load_font (Pango2FontMap *self, + Pango2Context *context, + const Pango2FontDescription *description) +{ + Pango2FontsetCached *fontset; + Pango2Language *language; + Pango2Font *font = NULL; + + if (self->families->len == 0) + return NULL; + + if (context) + language = pango2_context_get_language (context); + else + language = NULL; + + fontset = (Pango2FontsetCached *)pango2_font_map_load_fontset (self, context, description, language); + if (pango2_fontset_cached_size (fontset) > 0) + font = pango2_fontset_cached_get_first_font (fontset); + g_object_unref (fontset); + + return font; +} + +/* Add one font for each family we find */ +static Pango2Fontset * +pango2_font_map_default_load_fontset (Pango2FontMap *self, + Pango2Context *context, + const Pango2FontDescription *description, + Pango2Language *language) +{ + Pango2FontsetCached lookup; + Pango2FontsetCached *fontset; + const Pango2Matrix *ctm; + const char *family_name; + char **families; + Pango2FontDescription *copy; + Pango2FontFamily *family; + Pango2FontFace *face; + gboolean has_generic = FALSE; + Pango2Fontset *fallback; + gint64 before G_GNUC_UNUSED; + + before = PANGO2_TRACE_CURRENT_TIME; + + if (!language) + language = pango2_context_get_language (context); + + family_name = pango2_font_description_get_family (description); + ctm = pango2_context_get_matrix (context); + + lookup.language = language; + lookup.description = (Pango2FontDescription *)description; + lookup.ctm = ctm; +#ifdef HAVE_CAIRO + lookup.font_options = (cairo_font_options_t *)pango2_cairo_context_get_merged_font_options (context); +#endif + fontset = g_hash_table_lookup (self->fontsets, &lookup); + + if (fontset) + goto done; + + fontset = pango2_fontset_cached_new (description, language, self->dpi, ctm); +#ifdef HAVE_CAIRO + fontset->font_options = cairo_font_options_copy (pango2_cairo_context_get_merged_font_options (context)); +#endif + + if (self->families->len == 0) + { + if (self->fallback) + goto add_fallback; + else + { + g_warning ("Font map contains no fonts!!!!"); + goto done_no_cache; + } + } + + families = g_strsplit (family_name ? family_name : "", ",", -1); + + /* Unset gravity and variant since Pango2HbFace does not have these fields */ + copy = pango2_font_description_copy_static (description); + pango2_font_description_unset_fields (copy, PANGO2_FONT_MASK_VARIATIONS | + PANGO2_FONT_MASK_GRAVITY | + PANGO2_FONT_MASK_VARIANT); + + for (int i = 0; families[i]; i++) + { + family = find_family (self, families[i]); + if (!family) + continue; + + if (PANGO2_IS_GENERIC_FAMILY (family)) + { + pango2_fontset_cached_add_family (fontset, PANGO2_GENERIC_FAMILY (family)); + has_generic = TRUE; + } + else + { + face = pango2_hb_family_find_face (PANGO2_HB_FAMILY (family), copy, language, 0); + if (face) + pango2_fontset_cached_add_face (fontset, face); + } + } + + g_strfreev (families); + + pango2_font_description_free (copy); + + /* Returning an empty fontset leads to bad outcomes. + * + * We always include a generic family in order + * to produce fontsets with good coverage. + * + * If we have a fallback fontmap, this is where we bring + * it in and just add its results to ours. + */ + if (!has_generic) + { + family = find_family (self, "sans-serif"); + if (PANGO2_IS_GENERIC_FAMILY (family)) + pango2_fontset_cached_add_family (fontset, PANGO2_GENERIC_FAMILY (family)); + else if (self->fallback) + { +add_fallback: + fallback = pango2_font_map_load_fontset (self->fallback, context, description, language); + pango2_fontset_cached_append (fontset, PANGO2_FONTSET_CACHED (fallback)); + g_object_unref (fallback); + } + } + + g_hash_table_add (self->fontsets, fontset); + +done: + pango2_fontset_cache (fontset, self); + + if (pango2_fontset_cached_size (fontset) == 0) + { + char *s = pango2_font_description_to_string (description); + g_warning ("All font fallbacks failed for \"%s\", in %s !!!!", s, pango2_language_to_string (language)); + g_free (s); + } + +done_no_cache: + pango2_trace_mark (before, "pango2_fontmap_load_fontset", "%s", family_name); + + return g_object_ref (PANGO2_FONTSET (fontset)); +} + +static Pango2FontFamily * +pango2_font_map_default_get_family (Pango2FontMap *self, + const char *name) +{ + return PANGO2_FONT_FAMILY (find_family (self, name)); +} + +static void +pango2_font_map_default_populate (Pango2FontMap *self) +{ +} + +static guint +pango2_font_map_default_get_serial (Pango2FontMap *self) +{ + return self->serial; +} + +static void +pango2_font_map_default_changed (Pango2FontMap *self) +{ + self->serial++; + if (self->serial == 0) + self->serial++; +} + +static void +pango2_font_map_class_init (Pango2FontMapClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->finalize = pango2_font_map_finalize; + object_class->set_property = pango2_font_map_set_property; + object_class->get_property = pango2_font_map_get_property; + + class->load_font = pango2_font_map_default_load_font; + class->load_fontset = pango2_font_map_default_load_fontset; + class->get_serial = pango2_font_map_default_get_serial; + class->changed = pango2_font_map_default_changed; + class->get_family = pango2_font_map_default_get_family; + class->populate = pango2_font_map_default_populate; + + /** + * Pango2FontMap:resolution: (attributes org.gtk.Property.get=pango2_font_map_get_resolution org.gtk.Property.set=pango2_font_map_set_resolution) + * + * The resolution for the fontmap. + * + * This is a scale factor between points specified in a + * `Pango2FontDescription` and Cairo units. The default value + * is 96, meaning that a 10 point font will be 13 units high. + * (10 * 96. / 72. = 13.3). + */ + properties[PROP_RESOLUTION] = + g_param_spec_float ("resolution", NULL, NULL, 0, G_MAXFLOAT, 96.0, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2FontMap:fallback: (attributes org.gtk.Property.get=pango2_font_map_get_fallback org.gtk.Property.set=pango2_font_map_set_fallback) + * + * The fallback fontmap is used to look up fonts that + * this map does not have itself. + */ + properties[PROP_FALLBACK] = + g_param_spec_object ("fallback", NULL, NULL, PANGO2_TYPE_FONT_MAP, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2FontMap:item-type: + * + * The type of items contained in this list. + */ + properties[PROP_ITEM_TYPE] = + g_param_spec_gtype ("item-type", "", "", G_TYPE_OBJECT, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2FontMap:n-items: + * + * The number of items contained in this list. + */ + properties[PROP_N_ITEMS] = + g_param_spec_uint ("n-items", "", "", 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPERTIES, properties); +} + +/* }}} */ +/* {{{ Private API */ + +/*< private > + * pango2_font_map_repopulate: + * @self: a `Pango2FontMap` + * @add_synthetic: if `TRUE`, missing bold and italic faces will be synthesized + * + * Clear all cached information and repopulate the fontmap with + * families and faces. + * + * Subclasses should call this when their configuration changes + * in a way that requires recreating families and faces. + */ +void +pango2_font_map_repopulate (Pango2FontMap *self, + gboolean add_synthetic) +{ + int removed, added; + + clear_caches (self); + + removed = self->families->len; + + g_hash_table_remove_all (self->families_hash); + g_ptr_array_set_size (self->families, 0); + + self->in_populate = TRUE; + + PANGO2_FONT_MAP_GET_CLASS (self)->populate (self); + + if (add_synthetic) + synthesize_bold_and_italic_faces (self); + + for (int i = 0; i < self->added_faces->len; i++) + { + Pango2FontFace *face = g_ptr_array_index (self->added_faces, i); + pango2_font_map_add_face (self, face); + } + + for (int i = 0; i < self->added_families->len; i++) + { + Pango2FontFamily *family = PANGO2_FONT_FAMILY (g_ptr_array_index (self->added_families, i)); + pango2_font_map_add_family (self, family); + } + + self->in_populate = FALSE; + + added = self->families->len; + + g_list_model_items_changed (G_LIST_MODEL (self), 0, removed, added); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_N_ITEMS]); + pango2_font_map_changed (self); +} + +/*< private > + * pango2_font_map_changed: + * @self: a `Pango2FontMap` + * + * Forces a change in the context, which will cause any `Pango2Context` + * using this fontmap to change. + * + * This function is only useful when implementing a new backend + * for Pango2, something applications won't do. Backends should + * call this function if they have attached extra data to the + * context and such data is changed. + */ +void +pango2_font_map_changed (Pango2FontMap *self) +{ + g_return_if_fail (PANGO2_IS_FONT_MAP (self)); + + PANGO2_FONT_MAP_GET_CLASS (self)->changed (self); +} + +/* }}} */ +/* {{{ Public API */ + +/** + * pango2_font_map_load_font: + * @self: a `Pango2FontMap` + * @context: the `Pango2Context` the font will be used with + * @desc: a `Pango2FontDescription` describing the font to load + * + * Load the font in the fontmap that is the closest match for + * a font description. + * + * Returns: (transfer full) (nullable): the `Pango2Font` that + * was found + */ +Pango2Font * +pango2_font_map_load_font (Pango2FontMap *self, + Pango2Context *context, + const Pango2FontDescription *desc) +{ + g_return_val_if_fail (PANGO2_IS_FONT_MAP (self), NULL); + g_return_val_if_fail (PANGO2_IS_CONTEXT (context), NULL); + g_return_val_if_fail (desc != NULL, NULL); + + return PANGO2_FONT_MAP_GET_CLASS (self)->load_font (self, context, desc); +} + +/** + * pango2_font_map_load_fontset: + * @self: a `Pango2FontMap` + * @context: the `Pango2Context` the font will be used with + * @desc: a `Pango2FontDescription` describing the font to load + * @language: (nullable): a `Pango2Language` the fonts will be used for, + * or `NULL` to use the language of @context + * + * Load a set of fonts in the fontmap that can be used to render + * a font matching a font description. + * + * Returns: (transfer full) (nullable): the `Pango2Fontset` containing + * the fonts that were found + */ +Pango2Fontset * +pango2_font_map_load_fontset (Pango2FontMap *self, + Pango2Context *context, + const Pango2FontDescription *desc, + Pango2Language *language) +{ + g_return_val_if_fail (PANGO2_IS_FONT_MAP (self), NULL); + g_return_val_if_fail (PANGO2_IS_CONTEXT (context), NULL); + g_return_val_if_fail (desc != NULL, NULL); + + return PANGO2_FONT_MAP_GET_CLASS (self)->load_fontset (self, context, desc, language); +} + +/** + * pango2_font_map_get_serial: + * @self: a `Pango2FontMap` + * + * Returns the current serial number of the fontmap. + * + * The serial number is initialized to an small number larger than zero + * when a new fontmap is created and is increased whenever the fontmap + * is changed. It may wrap, but will never have the value 0. Since it can + * wrap, never compare it with "less than", always use "not equals". + * + * The fontmap can only be changed using backend-specific API, like changing + * fontmap resolution. + * + * This can be used to automatically detect changes to a `Pango2FontMap`, + * like in `Pango2Context`. + * + * Return value: The current serial number of @fontmap. + */ +guint +pango2_font_map_get_serial (Pango2FontMap *self) +{ + g_return_val_if_fail (PANGO2_IS_FONT_MAP (self), 0); + + return PANGO2_FONT_MAP_GET_CLASS (self)->get_serial (self); +} + +/** + * pango2_font_map_get_family: + * @self: a `Pango2FontMap` + * @name: a family name + * + * Gets a font family by name. + * + * Returns: (transfer none): the `Pango2FontFamily` + */ +Pango2FontFamily * +pango2_font_map_get_family (Pango2FontMap *self, + const char *name) +{ + g_return_val_if_fail (PANGO2_IS_FONT_MAP (self), NULL); + + return PANGO2_FONT_MAP_GET_CLASS (self)->get_family (self, name); +} + +/** + * pango2_font_map_new: + * + * Creates a new `Pango2FontMap`. + * + * A fontmap is used to cache information about available fonts, + * and holds certain global parameters such as the resolution. + * + * Returns: A newly created `Pango2FontMap + */ +Pango2FontMap * +pango2_font_map_new (void) +{ + return g_object_new (PANGO2_TYPE_FONT_MAP, NULL); +} + +/** + * pango2_font_map_add_face: + * @self: a `Pango2FontMap` + * @face: (transfer full): a `Pango2FontFace` + * + * Adds a face to the fontmap. + * + * This is most useful for creating transformed faces or aliases. + * See [method@Pango2.HbFace.new_synthetic] and [method@Pango2.HbFace.new_instance]. + */ +void +pango2_font_map_add_face (Pango2FontMap *self, + Pango2FontFace *face) +{ + const char *family_name; + Pango2HbFamily *family; + const Pango2FontDescription *description; + + g_return_if_fail (PANGO2_IS_FONT_MAP (self)); + g_return_if_fail (PANGO2_IS_HB_FACE (face) || PANGO2_IS_USER_FACE (face)); + + description = face->description; + + if (pango2_font_description_get_set_fields (description) & PANGO2_FONT_MASK_GRAVITY) + g_warning ("Font description for Pango2FontFace includes things that it shouldn't"); + + if (!self->in_populate) + g_ptr_array_add (self->added_faces, g_object_ref (face)); + + family_name = pango2_font_description_get_family (description); + family = PANGO2_HB_FAMILY (pango2_font_map_get_family (self, family_name)); + if (!family) + { + family = pango2_hb_family_new (family_name); + pango2_font_map_add_family (self, PANGO2_FONT_FAMILY (family)); + } + + pango2_hb_family_add_face (family, face); + + pango2_font_map_changed (self); + + clear_caches (self); +} + +/** + * pango2_font_map_remove_face: + * @self: a `Pango2FontMap` + * @face: a `Pango2FontFace` that belongs to @map + * + * Removes @face from the fontmap. + * + * @face must have been added with [method@Pango2.FontMap.add_face]. + */ +void +pango2_font_map_remove_face (Pango2FontMap *self, + Pango2FontFace *face) +{ + Pango2HbFamily *family; + unsigned int position; + + g_return_if_fail (PANGO2_IS_FONT_MAP (self)); + g_return_if_fail (PANGO2_IS_HB_FACE (face) || PANGO2_IS_USER_FACE (face)); + + if (!g_ptr_array_find (self->added_faces, face, &position)) + return; + + family = PANGO2_HB_FAMILY (pango2_font_face_get_family (face)); + + pango2_hb_family_remove_face (family, face); + + if (family->faces->len == 0) + pango2_font_map_remove_family (self, PANGO2_FONT_FAMILY (family)); + + pango2_font_map_changed (self); + + clear_caches (self); + + g_ptr_array_remove_index (self->added_faces, position); +} + +/** + * pango2_font_map_add_file: + * @self: a `Pango2FontMap` + * @file: font filename + * + * Creates a new `Pango2HbFace` and adds it. + * + * If you need to specify an instance id or other + * parameters, use [ctor@Pango2.HbFace.new_from_file]. + */ +void +pango2_font_map_add_file (Pango2FontMap *self, + const char *file) +{ + Pango2HbFace *face; + + face = pango2_hb_face_new_from_file (file, 0, -1, NULL, NULL); + pango2_font_map_add_face (self, PANGO2_FONT_FACE (face)); +} + +/** + * pango2_font_map_add_family: + * @self: a `Pango2FontMap` + * @family: (transfer full): a `Pango2FontFamily` + * + * Adds a family to the `Pango2FontMap`. + * + * The fontmap must not contain a family with the + * same name as @family yet. + * + * This is mostly useful for adding generic families + * using [ctor@Pango2.GenericFamily.new]. + */ +void +pango2_font_map_add_family (Pango2FontMap *self, + Pango2FontFamily *family) +{ + const char *name; + int position; + + g_return_if_fail (PANGO2_IS_FONT_MAP (self)); + g_return_if_fail (PANGO2_IS_HB_FAMILY (family) || PANGO2_IS_GENERIC_FAMILY (family)); + g_return_if_fail (family->map == NULL); + + if (!self->in_populate) + g_ptr_array_add (self->added_families, g_object_ref (family)); + + name = family->name; + + position = 0; + while (position < self->families->len) + { + Pango2FontFamily *f = g_ptr_array_index (self->families, position); + if (g_ascii_strcasecmp (name, f->name) < 0) + break; + position++; + } + + pango2_font_family_set_font_map (family, self); + + g_ptr_array_insert (self->families, position, family); + g_hash_table_add (self->families_hash, family); + + if (!self->in_populate) + { + g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_N_ITEMS]); + } + + pango2_font_map_changed (self); +} + +/** + * pango2_font_map_remove_family: + * @self: a `Pango2FontMap` + * @family: a `Pango2FontFamily` that belongs to @self + * + * Removes a family from the fontmap. + */ +void +pango2_font_map_remove_family (Pango2FontMap *self, + Pango2FontFamily *family) +{ + unsigned int position; + + g_return_if_fail (PANGO2_IS_FONT_MAP (self)); + g_return_if_fail (PANGO2_IS_HB_FAMILY (family) || PANGO2_IS_GENERIC_FAMILY (family)); + g_return_if_fail (family->map == self); + + if (!g_ptr_array_find (self->added_families, family, &position)) + return; + + g_hash_table_remove (self->families_hash, family); + g_ptr_array_remove_index (self->families, position); + + pango2_font_family_set_font_map (family, NULL); + + if (!self->in_populate) + { + g_list_model_items_changed (G_LIST_MODEL (self), position, 1, 0); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_N_ITEMS]); + } + + pango2_font_map_changed (self); + + g_ptr_array_remove_index (self->added_families, position); +} + +/** + * pango2_font_map_set_fallback: + * @self: a `Pango2FontMap` + * @fallback: (nullable): the `Pango2FontMap` to use as fallback + * + * Sets the fontmap to use as fallback when a font isn't found. + * + * This can be used to make a custom font available only via a + * special fontmap, while still having all the regular fonts + * from the fallback fontmap. + * + * Note that families are *not* merged. If you are iterating + * the families, you will first get all the families in @self, + * followed by all the families in the @fallback. + */ +void +pango2_font_map_set_fallback (Pango2FontMap *self, + Pango2FontMap *fallback) +{ + g_return_if_fail (PANGO2_IS_FONT_MAP (self)); + g_return_if_fail (fallback == NULL || PANGO2_IS_FONT_MAP (fallback)); + + if (!g_set_object (&self->fallback, fallback)) + return; + + clear_caches (self); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_FALLBACK]); +} + +/** + * pango2_font_map_get_fallback: + * @self: a `Pango2FontMap` + * + * Returns the fallback fontmap of the fontmap. + * + * See [method@Pango2.FontMap.set_fallback]. + * + * Returns: (nullable) (transfer none): the fallback fontmap + */ +Pango2FontMap * +pango2_font_map_get_fallback (Pango2FontMap *self) +{ + g_return_val_if_fail (PANGO2_IS_FONT_MAP (self), NULL); + + return self->fallback; +} + +/** + * pango2_font_map_set_resolution: + * @self: a `Pango2FontMap` + * @dpi: the new resolution, in "dots per inch" + * + * Sets the resolution for the fontmap. + * + * This is a scale factor between points specified in a + * `Pango2FontDescription` and Cairo units. The default value + * is 96, meaning that a 10 point font will be 13 units high. + * (10 * 96. / 72. = 13.3). + */ +void +pango2_font_map_set_resolution (Pango2FontMap *self, + float dpi) +{ + g_return_if_fail (PANGO2_IS_FONT_MAP (self)); + g_return_if_fail (dpi > 0); + + if (self->dpi == dpi) + return; + + self->dpi = dpi; + + clear_caches (self); + pango2_font_map_changed (self); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_RESOLUTION]); +} + +/** + * pango2_font_map_get_resolution: + * @self: a `Pango2FontMap` + * + * Returns the resolution for the fontmap. + * + * See [method@Pango2.FontMap.set_resolution]. + * + * Return value: the resolution for the fontmap + */ +float +pango2_font_map_get_resolution (Pango2FontMap *self) +{ + g_return_val_if_fail (PANGO2_IS_FONT_MAP (self), 0); + + return self->dpi; +} + +/* }}} */ +/* {{{ Default handling */ + +static GPrivate default_font_map = G_PRIVATE_INIT (g_object_unref); /* MT-safe */ + +/** + * pango2_font_map_new_default: + * + * Creates a new `Pango2FontMap` object. + * + * Note that the type of the returned object will depend + * on the platform that Pango2 is used on. If you want to + * explicitly create an instance of `Pango2FontMap` itself + * (and not a platform-specific subclass), see [ctor@Pango2.FontMap.new]. + * + * In most cases, you should use [func@Pango2.FontMap.get_default], + * which will automatically create a new default fontmap, + * if one has not been created for the current thread yet. + * + * Return value: (transfer full): the newly allocated `Pango2FontMap` + */ +Pango2FontMap * +pango2_font_map_new_default (void) +{ +/* If we ever go back to having multiple backends built + * at the same time, bring back a PANGO2_BACKEND env var + * here. + */ +#if defined (HAVE_CORE_TEXT) + return PANGO2_FONT_MAP (pango2_core_text_font_map_new ()); +#elif defined (HAVE_DIRECT_WRITE) + return PANGO2_FONT_MAP (pango2_direct_write_font_map_new ()); +#elif defined (HAVE_FONTCONFIG) + return PANGO2_FONT_MAP (pango2_fc_font_map_new ()); +#else + return NULL; +#endif +} + +/** + * pango2_font_map_get_default: + * + * Gets the default `Pango2FontMap`. + * + * The type of the returned object will depend on the + * platform that Pango2 is used on. + * + * Note that the default fontmap is per-thread. Each thread gets + * its own default fontmap. In this way, Pango2 can be used safely + * from multiple threads. + * + * The default fontmap can be changed by using + * [method@Pango2.FontMap.set_default]. + * + * Return value: (transfer none): the default fontmap + * for the current thread. This object is owned by Pango2 and must + * not be freed. + */ +Pango2FontMap * +pango2_font_map_get_default (void) +{ + Pango2FontMap *fontmap = g_private_get (&default_font_map); + + if (G_UNLIKELY (!fontmap)) + { + fontmap = pango2_font_map_new_default (); + g_private_replace (&default_font_map, fontmap); + } + + return fontmap; +} + +/** + * pango2_font_map_set_default: + * @fontmap: (nullable): The new default font map + * + * Sets a default `Pango2FontMap`. + * + * The old default font map is unreffed and the new font map referenced. + * + * Note that the default fontmap is per-thread. + * This function only changes the default fontmap for + * the current thread. Default fontmaps of existing threads + * are not changed. Default fontmaps of any new threads will + * still be created using [ctor@Pango2.FontMap.new_default]. + * + * A value of %NULL for @fontmap will cause the current default + * font map to be released and a new default font map to be created + * on demand, using [ctor@Pango2.FontMap.new_default]. + */ +void +pango2_font_map_set_default (Pango2FontMap *fontmap) +{ + g_return_if_fail (fontmap == NULL || PANGO2_IS_FONT_MAP (fontmap)); + + if (fontmap) + g_object_ref (fontmap); + + g_private_replace (&default_font_map, fontmap); +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pango-fontmap.h b/pango2/pango-fontmap.h new file mode 100644 index 00000000..a8e9a778 --- /dev/null +++ b/pango2/pango-fontmap.h @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2021 Matthias Clasen + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-types.h> +#include <pango2/pango-fontset.h> +#include <pango2/pango-hbface.h> + +G_BEGIN_DECLS + +#define PANGO2_TYPE_FONT_MAP (pango2_font_map_get_type ()) + +PANGO2_AVAILABLE_IN_ALL +PANGO2_DECLARE_INTERNAL_TYPE (Pango2FontMap, pango2_font_map, PANGO2, FONT_MAP, GObject) + +PANGO2_AVAILABLE_IN_ALL +Pango2Font * pango2_font_map_load_font (Pango2FontMap *self, + Pango2Context *context, + const Pango2FontDescription *desc); +PANGO2_AVAILABLE_IN_ALL +Pango2Fontset * pango2_font_map_load_fontset (Pango2FontMap *self, + Pango2Context *context, + const Pango2FontDescription *desc, + Pango2Language *language); +PANGO2_AVAILABLE_IN_ALL +guint pango2_font_map_get_serial (Pango2FontMap *self); + +PANGO2_AVAILABLE_IN_ALL +Pango2FontFamily * pango2_font_map_get_family (Pango2FontMap *self, + const char *name); + +PANGO2_AVAILABLE_IN_ALL +Pango2FontMap * pango2_font_map_new (void); + +PANGO2_AVAILABLE_IN_ALL +void pango2_font_map_add_file (Pango2FontMap *self, + const char *file); + +PANGO2_AVAILABLE_IN_ALL +void pango2_font_map_add_face (Pango2FontMap *self, + Pango2FontFace *face); + +PANGO2_AVAILABLE_IN_ALL +void pango2_font_map_remove_face (Pango2FontMap *self, + Pango2FontFace *face); + +PANGO2_AVAILABLE_IN_ALL +void pango2_font_map_add_family (Pango2FontMap *self, + Pango2FontFamily *family); + +PANGO2_AVAILABLE_IN_ALL +void pango2_font_map_remove_family (Pango2FontMap *self, + Pango2FontFamily *family); + +PANGO2_AVAILABLE_IN_ALL +void pango2_font_map_set_fallback (Pango2FontMap *self, + Pango2FontMap *fallback); + +PANGO2_AVAILABLE_IN_ALL +Pango2FontMap * pango2_font_map_get_fallback (Pango2FontMap *self); + +PANGO2_AVAILABLE_IN_ALL +float pango2_font_map_get_resolution (Pango2FontMap *self); + +PANGO2_AVAILABLE_IN_ALL +void pango2_font_map_set_resolution (Pango2FontMap *self, + float dpi); + +PANGO2_AVAILABLE_IN_ALL +Pango2FontMap * pango2_font_map_new_default (void); +PANGO2_AVAILABLE_IN_ALL +Pango2FontMap * pango2_font_map_get_default (void); +PANGO2_AVAILABLE_IN_ALL +void pango2_font_map_set_default (Pango2FontMap *fontmap); + +G_END_DECLS diff --git a/pango2/pango-fontset-cached-private.h b/pango2/pango-fontset-cached-private.h new file mode 100644 index 00000000..95059a79 --- /dev/null +++ b/pango2/pango-fontset-cached-private.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2021 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-types.h" +#include "pango-fontset-private.h" +#include "pango-generic-family.h" +#include <glib-object.h> + +#ifdef HAVE_CAIRO +#include <cairo.h> +#endif + +#define PANGO2_TYPE_FONTSET_CACHED (pango2_fontset_cached_get_type ()) + +G_DECLARE_FINAL_TYPE (Pango2FontsetCached, pango2_fontset_cached, PANGO2, FONTSET_CACHED, Pango2Fontset) + +struct _Pango2FontsetCached +{ + Pango2Fontset parent_instance; + + GPtrArray *items; /* contains Pango2HbFont or Pango2GenericFamily */ + Pango2Language *language; + Pango2FontDescription *description; + float dpi; + const Pango2Matrix *ctm; + GList cache_link; + GHashTable *cache; + +#ifdef HAVE_CAIRO + cairo_font_options_t *font_options; +#endif +}; + +Pango2FontsetCached * pango2_fontset_cached_new (const Pango2FontDescription *description, + Pango2Language *language, + float dpi, + const Pango2Matrix *ctm); + +void pango2_fontset_cached_add_face (Pango2FontsetCached *self, + Pango2FontFace *face); +void pango2_fontset_cached_add_family (Pango2FontsetCached *self, + Pango2GenericFamily *family); +int pango2_fontset_cached_size (Pango2FontsetCached *self); +Pango2Font * pango2_fontset_cached_get_first_font (Pango2FontsetCached *self); + +void pango2_fontset_cached_append (Pango2FontsetCached *self, + Pango2FontsetCached *other); diff --git a/pango2/pango-fontset-cached.c b/pango2/pango-fontset-cached.c new file mode 100644 index 00000000..9feaca79 --- /dev/null +++ b/pango2/pango-fontset-cached.c @@ -0,0 +1,347 @@ +/* Pango2 + * + * Copyright (C) 2021 Matthias Clasen + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include <math.h> + +#include <gio/gio.h> + +#include "pango-fontset-cached-private.h" +#include "pango-font-private.h" +#include "pango-font-face-private.h" +#include "pango-generic-family-private.h" + +#ifdef HAVE_CAIRO +#include "pangocairo-font.h" +#endif + +G_DEFINE_FINAL_TYPE (Pango2FontsetCached, pango2_fontset_cached, PANGO2_TYPE_FONTSET); + +static void +pango2_fontset_cached_init (Pango2FontsetCached *fontset) +{ + fontset->items = g_ptr_array_new_with_free_func (g_object_unref); + fontset->cache = g_hash_table_new_full (NULL, NULL, NULL, g_object_unref); + fontset->language = NULL; +#ifdef HAVE_CAIRO + fontset->font_options = NULL; +#endif +} + +static void +pango2_fontset_cached_finalize (GObject *object) +{ + Pango2FontsetCached *self = (Pango2FontsetCached *)object; + + g_ptr_array_free (self->items, TRUE); + g_hash_table_unref (self->cache); + pango2_font_description_free (self->description); +#ifdef HAVE_CAIRO + if (self->font_options) + cairo_font_options_destroy (self->font_options); +#endif + + G_OBJECT_CLASS (pango2_fontset_cached_parent_class)->finalize (object); +} + +static Pango2Font * +find_font_for_face (Pango2FontsetCached *self, + Pango2FontFace *face) +{ + GHashTableIter iter; + Pango2Font *font; + + g_hash_table_iter_init (&iter, self->cache); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&font)) + { + if (pango2_font_get_face (font) == face) + { + return font; + break; + } + } + + return NULL; +} + +static Pango2Font * +pango2_fontset_cached_get_font (Pango2Fontset *fontset, + guint wc) +{ + Pango2FontsetCached *self = (Pango2FontsetCached *)fontset; + Pango2Font *retval; + int i; + + retval = g_hash_table_lookup (self->cache, GUINT_TO_POINTER (wc)); + if (retval) + return g_object_ref (retval); + + for (i = 0; i < self->items->len; i++) + { + gpointer item = g_ptr_array_index (self->items, i); + + if (PANGO2_IS_FONT (item)) + { + Pango2Font *font = PANGO2_FONT (item); + if (pango2_font_face_has_char (font->face, wc)) + { + retval = g_object_ref (font); + break; + } + } + else if (PANGO2_IS_GENERIC_FAMILY (item)) + { + Pango2GenericFamily *family = PANGO2_GENERIC_FAMILY (item); + Pango2FontDescription *copy; + Pango2FontFace *face; + + /* Here is where we implement delayed picking for generic families. + * If a face does not cover the character and its family is generic, + * choose a different face from the same family and create a font to use. + */ + copy = pango2_font_description_copy_static (self->description); + pango2_font_description_unset_fields (copy, PANGO2_FONT_MASK_VARIATIONS | + PANGO2_FONT_MASK_GRAVITY | + PANGO2_FONT_MASK_VARIANT); + face = pango2_generic_family_find_face (family, copy, self->language, wc); + pango2_font_description_free (copy); + + if (face) + { + Pango2Font *font; + + font = find_font_for_face (self, face); + if (font) + { + retval = g_object_ref (font); + } + else + { + retval = pango2_font_face_create_font (face, + self->description, + self->dpi, + self->ctm); +#ifdef HAVE_CAIRO + pango2_cairo_font_set_font_options (retval, self->font_options); +#endif + } + break; + } + } + } + + if (retval) + g_hash_table_insert (self->cache, GUINT_TO_POINTER (wc), g_object_ref (retval)); + + return retval; +} + +Pango2Font * +pango2_fontset_cached_get_first_font (Pango2FontsetCached *self) +{ + gpointer item; + + item = g_ptr_array_index (self->items, 0); + + if (PANGO2_IS_FONT (item)) + return g_object_ref (PANGO2_FONT (item)); + else if (PANGO2_IS_GENERIC_FAMILY (item)) + { + Pango2FontFace *face; + Pango2Font *font; + + face = pango2_generic_family_find_face (PANGO2_GENERIC_FAMILY (item), + self->description, + self->language, + 0); + font = find_font_for_face (self, face); + if (font) + g_object_ref (font); + else + { + font = pango2_font_face_create_font (face, + self->description, + self->dpi, + self->ctm); +#ifdef HAVE_CAIRO + pango2_cairo_font_set_font_options (font, self->font_options); +#endif + } + + return font; + } + + return NULL; +} + +static Pango2FontMetrics * +pango2_fontset_cached_get_metrics (Pango2Fontset *fontset) +{ + Pango2FontsetCached *self = (Pango2FontsetCached *)fontset; + + if (self->items->len == 1) + { + Pango2Font *font; + Pango2FontMetrics *ret; + + font = pango2_fontset_cached_get_first_font (self); + ret = pango2_font_get_metrics (font, self->language); + g_object_unref (font); + + return ret; + } + + return PANGO2_FONTSET_CLASS (pango2_fontset_cached_parent_class)->get_metrics (fontset); +} + +static Pango2Language * +pango2_fontset_cached_get_language (Pango2Fontset *fontset) +{ + Pango2FontsetCached *self = (Pango2FontsetCached *)fontset; + + return self->language; +} + +static void +pango2_fontset_cached_foreach (Pango2Fontset *fontset, + Pango2FontsetForeachFunc func, + gpointer data) +{ + Pango2FontsetCached *self = (Pango2FontsetCached *)fontset; + unsigned int i; + + for (i = 0; i < self->items->len; i++) + { + gpointer item = g_ptr_array_index (self->items, i); + Pango2Font *font = NULL; + + if (PANGO2_IS_FONT (item)) + { + font = g_object_ref (PANGO2_FONT (item)); + } + else if (PANGO2_IS_GENERIC_FAMILY (item)) + { + Pango2FontFace *face; + + face = pango2_generic_family_find_face (PANGO2_GENERIC_FAMILY (item), + self->description, + self->language, + 0); + + font = find_font_for_face (self, face); + if (font) + g_object_ref (font); + else + { + font = pango2_font_face_create_font (face, + self->description, + self->dpi, + self->ctm); +#ifdef HAVE_CAIRO + pango2_cairo_font_set_font_options (font, self->font_options); +#endif + } + } + + if ((*func) (fontset, font, data)) + { + g_object_unref (font); + return; + } + g_object_unref (font); + } +} + +static void +pango2_fontset_cached_class_init (Pango2FontsetCachedClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + Pango2FontsetClass *fontset_class = PANGO2_FONTSET_CLASS (class); + + object_class->finalize = pango2_fontset_cached_finalize; + + fontset_class->get_font = pango2_fontset_cached_get_font; + fontset_class->get_metrics = pango2_fontset_cached_get_metrics; + fontset_class->get_language = pango2_fontset_cached_get_language; + fontset_class->foreach = pango2_fontset_cached_foreach; +} + +Pango2FontsetCached * +pango2_fontset_cached_new (const Pango2FontDescription *description, + Pango2Language *language, + float dpi, + const Pango2Matrix *ctm) +{ + Pango2FontsetCached *self; + + self = g_object_new (pango2_fontset_cached_get_type (), NULL); + self->language = language; + self->description = pango2_font_description_copy (description); + self->dpi = dpi; + self->ctm = ctm; + + return self; +} + +void +pango2_fontset_cached_add_face (Pango2FontsetCached *self, + Pango2FontFace *face) +{ + Pango2Font *font; + + font = pango2_font_face_create_font (face, + self->description, + self->dpi, + self->ctm); +#ifdef HAVE_CAIRO + pango2_cairo_font_set_font_options (font, self->font_options); +#endif + g_ptr_array_add (self->items, font); +} + +void +pango2_fontset_cached_add_family (Pango2FontsetCached *self, + Pango2GenericFamily *family) +{ + g_ptr_array_add (self->items, g_object_ref (family)); +} + +int +pango2_fontset_cached_size (Pango2FontsetCached *self) +{ + return self->items->len; +} + +void +pango2_fontset_cached_append (Pango2FontsetCached *self, + Pango2FontsetCached *other) +{ + for (int i = 0; i < other->items->len; i++) + { + gpointer item = g_ptr_array_index (other->items, i); + + if (PANGO2_IS_FONT (item)) + pango2_fontset_cached_add_face (self, pango2_font_get_face (PANGO2_FONT (item))); + else if (PANGO2_IS_GENERIC_FAMILY (item)) + pango2_fontset_cached_add_family (self, PANGO2_GENERIC_FAMILY (item)); + } +} + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pango-fontset-private.h b/pango2/pango-fontset-private.h new file mode 100644 index 00000000..06e0e7c0 --- /dev/null +++ b/pango2/pango-fontset-private.h @@ -0,0 +1,51 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-fontset.h" + + +#define PANGO2_FONTSET_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), PANGO2_TYPE_FONTSET, Pango2FontsetClass)) +#define PANGO2_FONTSET_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), PANGO2_TYPE_FONTSET, Pango2FontsetClass)) + + +typedef struct _Pango2FontsetClass Pango2FontsetClass; + +struct _Pango2Fontset +{ + GObject parent_instance; +}; + +struct _Pango2FontsetClass +{ + GObjectClass parent_class; + + Pango2Font * (* get_font) (Pango2Fontset *fontset, + guint wc); + + Pango2FontMetrics * (* get_metrics) (Pango2Fontset *fontset); + Pango2Language * (* get_language) (Pango2Fontset *fontset); + void (* foreach) (Pango2Fontset *fontset, + Pango2FontsetForeachFunc func, + gpointer data); +}; + +Pango2Language * pango2_fontset_get_language (Pango2Fontset *fontset); + diff --git a/pango2/pango-fontset.c b/pango2/pango-fontset.c new file mode 100644 index 00000000..b3ef1e8e --- /dev/null +++ b/pango2/pango-fontset.c @@ -0,0 +1,212 @@ +/* Pango2 + * pango-fontset.c: + * + * Copyright (C) 2001 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +/** + * Pango2Fontset: + * + * A `Pango2Fontset` represents a set of `Pango2Font` to use when rendering text. + * + * A `Pango2Fontset` is the result of resolving a `Pango2FontDescription` + * against a particular `Pango2Context`. It has operations for finding the + * component font for a particular Unicode character, and for finding a + * composite set of metrics for the entire fontset. + * + * To obtain a `Pango2Fontset`, use [method@Pango2.Context.load_fontset] or + * [method@Pango2.FontMap.load_fontset]. + */ + +#include "pango-fontset-private.h" +#include "pango-types.h" +#include "pango-font-metrics-private.h" +#include "pango-impl-utils.h" + +static Pango2FontMetrics *pango2_fontset_real_get_metrics (Pango2Fontset *fontset); + +G_DEFINE_ABSTRACT_TYPE (Pango2Fontset, pango2_fontset, G_TYPE_OBJECT); + +static void +pango2_fontset_init (Pango2Fontset *self) +{ +} + +static void +pango2_fontset_class_init (Pango2FontsetClass *class) +{ + class->get_metrics = pango2_fontset_real_get_metrics; +} + + +/** + * pango2_fontset_get_font: + * @fontset: a `Pango2Fontset` + * @wc: a Unicode character + * + * Returns the font in the fontset that contains the best + * glyph for a Unicode character. + * + * Return value: (transfer full): a `Pango2Font` + */ +Pango2Font * +pango2_fontset_get_font (Pango2Fontset *fontset, + guint wc) +{ + + g_return_val_if_fail (PANGO2_IS_FONTSET (fontset), NULL); + + return PANGO2_FONTSET_GET_CLASS (fontset)->get_font (fontset, wc); +} + +/** + * pango2_fontset_get_metrics: + * @fontset: a `Pango2Fontset` + * + * Get overall metric information for the fonts in the fontset. + * + * Return value: a `Pango2FontMetrics` object + */ +Pango2FontMetrics * +pango2_fontset_get_metrics (Pango2Fontset *fontset) +{ + g_return_val_if_fail (PANGO2_IS_FONTSET (fontset), NULL); + + return PANGO2_FONTSET_GET_CLASS (fontset)->get_metrics (fontset); +} + +/*< private > + * pango2_fontset_get_language: + * @fontset: a `Pango2Fontset` + * + * Gets the language that the fontset was created for. + * + * Returns: the language that @fontset was created for + */ +Pango2Language * +pango2_fontset_get_language (Pango2Fontset *fontset) +{ + g_return_val_if_fail (PANGO2_IS_FONTSET (fontset), NULL); + + return PANGO2_FONTSET_GET_CLASS (fontset)->get_language (fontset); +} + +/** + * pango2_fontset_foreach: + * @fontset: a `Pango2Fontset` + * @func: (closure data) (scope call): Callback function + * @data: (closure): data to pass to the callback function + * + * Iterates through all the fonts in a fontset, calling @func for + * each one. + * + * If @func returns %TRUE, that stops the iteration. + */ +void +pango2_fontset_foreach (Pango2Fontset *fontset, + Pango2FontsetForeachFunc func, + gpointer data) +{ + g_return_if_fail (PANGO2_IS_FONTSET (fontset)); + g_return_if_fail (func != NULL); + + PANGO2_FONTSET_GET_CLASS (fontset)->foreach (fontset, func, data); +} + +static gboolean +get_first_metrics_foreach (Pango2Fontset *fontset, + Pango2Font *font, + gpointer data) +{ + Pango2FontMetrics **fontset_metrics = data; + Pango2Language *language = pango2_fontset_get_language (fontset); + + *fontset_metrics = pango2_font_get_metrics (font, language); + + return TRUE; /* Stops iteration */ +} + +static Pango2FontMetrics * +pango2_fontset_real_get_metrics (Pango2Fontset *fontset) +{ + Pango2FontMetrics *metrics, *raw_metrics; + const char *sample_str; + const char *p; + int count; + GHashTable *fonts_seen; + Pango2Font *font; + Pango2Language *language; + + language = pango2_fontset_get_language (fontset); + sample_str = pango2_language_get_sample_string (language); + + count = 0; + metrics = NULL; + fonts_seen = g_hash_table_new_full (NULL, NULL, g_object_unref, NULL); + + /* Initialize the metrics from the first font in the fontset */ + pango2_fontset_foreach (fontset, get_first_metrics_foreach, &metrics); + + p = sample_str; + while (*p) + { + gunichar wc = g_utf8_get_char (p); + font = pango2_fontset_get_font (fontset, wc); + if (font) + { + if (g_hash_table_lookup (fonts_seen, font) == NULL) + { + raw_metrics = pango2_font_get_metrics (font, language); + g_hash_table_insert (fonts_seen, font, font); + + if (count == 0) + { + metrics->ascent = raw_metrics->ascent; + metrics->descent = raw_metrics->descent; + metrics->approximate_char_width = raw_metrics->approximate_char_width; + metrics->approximate_digit_width = raw_metrics->approximate_digit_width; + } + else + { + metrics->ascent = MAX (metrics->ascent, raw_metrics->ascent); + metrics->descent = MAX (metrics->descent, raw_metrics->descent); + metrics->approximate_char_width += raw_metrics->approximate_char_width; + metrics->approximate_digit_width += raw_metrics->approximate_digit_width; + } + count++; + pango2_font_metrics_free (raw_metrics); + } + else + g_object_unref (font); + } + + p = g_utf8_next_char (p); + } + + g_hash_table_destroy (fonts_seen); + + if (count) + { + metrics->approximate_char_width /= count; + metrics->approximate_digit_width /= count; + } + + return metrics; +} diff --git a/pango2/pango-fontset.h b/pango2/pango-fontset.h new file mode 100644 index 00000000..5e6ed716 --- /dev/null +++ b/pango2/pango-fontset.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2001 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-types.h> +#include <pango2/pango-font-metrics.h> + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define PANGO2_TYPE_FONTSET (pango2_fontset_get_type ()) + +PANGO2_AVAILABLE_IN_ALL +PANGO2_DECLARE_INTERNAL_TYPE (Pango2Fontset, pango2_fontset, PANGO2, FONTSET, GObject) + +/** + * Pango2FontsetForeachFunc: + * @fontset: a `Pango2Fontset` + * @font: a font from @fontset + * @user_data: callback data + * + * Callback used when enumerating fonts in a fontset. + * + * See [method@Pango2.Fontset.foreach]. + * + * Returns: if %TRUE, stop iteration and return immediately. + */ +typedef gboolean (*Pango2FontsetForeachFunc) (Pango2Fontset *fontset, + Pango2Font *font, + gpointer user_data); + +PANGO2_AVAILABLE_IN_ALL +Pango2Font * pango2_fontset_get_font (Pango2Fontset *fontset, + guint wc); +PANGO2_AVAILABLE_IN_ALL +Pango2FontMetrics * pango2_fontset_get_metrics (Pango2Fontset *fontset); +PANGO2_AVAILABLE_IN_ALL +void pango2_fontset_foreach (Pango2Fontset *fontset, + Pango2FontsetForeachFunc func, + gpointer data); + +G_END_DECLS diff --git a/pango2/pango-generic-family-private.h b/pango2/pango-generic-family-private.h new file mode 100644 index 00000000..8c406d81 --- /dev/null +++ b/pango2/pango-generic-family-private.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 Matthias Clasen + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-font.h" +#include "pango-generic-family.h" +#include "pango-font-family-private.h" + + +struct _Pango2GenericFamily +{ + Pango2FontFamily parent_instance; + + GPtrArray *families; +}; + + +Pango2FontFace * pango2_generic_family_find_face (Pango2GenericFamily *self, + Pango2FontDescription *description, + Pango2Language *language, + gunichar wc); diff --git a/pango2/pango-generic-family.c b/pango2/pango-generic-family.c new file mode 100644 index 00000000..c0697f00 --- /dev/null +++ b/pango2/pango-generic-family.c @@ -0,0 +1,226 @@ +/* Pango2 + * + * Copyright (C) 2022 Matthias Clasen + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include <gio/gio.h> + +#include "pango-generic-family-private.h" +#include "pango-hbfamily-private.h" +#include "pango-impl-utils.h" +#include "pango-hbface-private.h" +#include "pango-font-private.h" + +/** + * Pango2GenericFamily: + * + * An implementation of `Pango2FontFamily` + * that provides faces from other families. + * + * `Pango2GenericFamily can be used to e.g. assemble a + * generic 'sans-serif' family from a number of other + * font families. + */ + + +/* {{{ GListModel implementation */ + +static GType +pango2_generic_family_get_item_type (GListModel *list) +{ + return PANGO2_TYPE_FONT_FACE; +} + +static guint +pango2_generic_family_get_n_items (GListModel *list) +{ + Pango2GenericFamily *self = PANGO2_GENERIC_FAMILY (list); + guint n; + + n = 0; + for (int i = 0; i < self->families->len; i++) + { + Pango2HbFamily *family = g_ptr_array_index (self->families, i); + + n += g_list_model_get_n_items (G_LIST_MODEL (family)); + } + + return n; +} + +static gpointer +pango2_generic_family_get_item (GListModel *list, + guint position) +{ + Pango2GenericFamily *self = PANGO2_GENERIC_FAMILY (list); + guint pos; + + pos = position; + for (int i = 0; i < self->families->len; i++) + { + Pango2HbFamily *family = g_ptr_array_index (self->families, i); + + if (pos < g_list_model_get_n_items (G_LIST_MODEL (family))) + return g_list_model_get_item (G_LIST_MODEL (family), pos); + + pos -= g_list_model_get_n_items (G_LIST_MODEL (family)); + } + + return NULL; +} + +static void +pango2_generic_family_list_model_init (GListModelInterface *iface) +{ + iface->get_item_type = pango2_generic_family_get_item_type; + iface->get_n_items = pango2_generic_family_get_n_items; + iface->get_item = pango2_generic_family_get_item; +} + +/* }}} */ +/* {{{ Pango2FontFamily implementation */ + +struct _Pango2GenericFamilyClass +{ + Pango2FontFamilyClass parent_class; +}; + +G_DEFINE_FINAL_TYPE_WITH_CODE (Pango2GenericFamily, pango2_generic_family, PANGO2_TYPE_FONT_FAMILY, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, pango2_generic_family_list_model_init)) + +static void +pango2_generic_family_init (Pango2GenericFamily *self) +{ + self->families = g_ptr_array_new_with_free_func (g_object_unref); +} + +static void +pango2_generic_family_finalize (GObject *object) +{ + Pango2GenericFamily *self = PANGO2_GENERIC_FAMILY (object); + + g_ptr_array_unref (self->families); + + G_OBJECT_CLASS (pango2_generic_family_parent_class)->finalize (object); +} + +static void +pango2_generic_family_class_init (Pango2GenericFamilyClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->finalize = pango2_generic_family_finalize; +} + +/* }}} */ +/* {{{ Private API */ + +/*< private > + * pango2_generic_family_find_face: + * @family: a `Pango2GenericFamily` + * @description: `Pango2FontDescription` to match + * @language: (nullable): `Pango2Language` to support + * @wc: a Unicode character, or 0 to ignore + * + * Finds the face that best matches the font description while + * also supporting the given language and character, from the first + * family in @family that has any matching faces. + * + * Returns: (transfer none) (nullable): the face + */ +Pango2FontFace * +pango2_generic_family_find_face (Pango2GenericFamily *self, + Pango2FontDescription *description, + Pango2Language *language, + gunichar wc) +{ + Pango2FontFace *face = NULL; + + for (int i = 0; i < self->families->len; i++) + { + Pango2HbFamily *family = g_ptr_array_index (self->families, i); + + face = pango2_hb_family_find_face (family, description, language, wc); + if (face) + break; + } + + /* last resort */ + if (!face && self->families->len > 0) + { + Pango2HbFamily *family = g_ptr_array_index (self->families, 0); + face = g_list_model_get_item (G_LIST_MODEL (family), 0); + g_object_unref (face); + } + + return face; +} + +/* }}} */ +/* {{{ Public API */ + +/** + * pango2_generic_family_new: + * @name: the family name + * + * Creates a new `Pango2GenericFamily`. + * + * A generic family does not contain faces, but will return + * faces from other families that match a given query. + * + * Returns: a newly created `Pango2GenericFamily` + */ +Pango2GenericFamily * +pango2_generic_family_new (const char *name) +{ + Pango2GenericFamily *self; + + g_return_val_if_fail (name != NULL, NULL); + + self = g_object_new (PANGO2_TYPE_GENERIC_FAMILY, NULL); + + pango2_font_family_set_name (PANGO2_FONT_FAMILY (self), name); + + return self; +} + +/** + * pango2_generic_family_add_family: + * @self: a `Pango2GenericFamily` + * @family: (transfer none): a `Pango2FontFamily` to add + * + * Adds a `Pango2FontFamily` to a `Pango2GenericFamily`. + * + * It is an error to call this function more than + * once for the same family. + */ +void +pango2_generic_family_add_family (Pango2GenericFamily *self, + Pango2FontFamily *family) +{ + g_return_if_fail (PANGO2_IS_GENERIC_FAMILY (self)); + g_return_if_fail (PANGO2_IS_FONT_FAMILY (family)); + + g_ptr_array_add (self->families, g_object_ref (family)); +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pango-generic-family.h b/pango2/pango-generic-family.h new file mode 100644 index 00000000..ab1d0fd2 --- /dev/null +++ b/pango2/pango-generic-family.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 Matthias Clasen + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-font.h" + +G_BEGIN_DECLS + +#define PANGO2_TYPE_GENERIC_FAMILY (pango2_generic_family_get_type ()) + +PANGO2_AVAILABLE_IN_ALL +PANGO2_DECLARE_INTERNAL_TYPE (Pango2GenericFamily, pango2_generic_family, PANGO2, GENERIC_FAMILY, Pango2FontFamily) + +PANGO2_AVAILABLE_IN_ALL +Pango2GenericFamily * pango2_generic_family_new (const char *name); + +PANGO2_AVAILABLE_IN_ALL +void pango2_generic_family_add_family (Pango2GenericFamily *self, + Pango2FontFamily *family); + +G_END_DECLS diff --git a/pango2/pango-glyph-item-private.h b/pango2/pango-glyph-item-private.h new file mode 100644 index 00000000..1fa80871 --- /dev/null +++ b/pango2/pango-glyph-item-private.h @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2002 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-attributes.h" +#include "pango-break.h" +#include "pango-item.h" +#include "pango-glyph.h" + +G_BEGIN_DECLS + +/** + * Pango2GlyphItem: + * @item: corresponding `Pango2Item` + * @glyphs: corresponding `Pango2GlyphString` + * @y_offset: shift of the baseline, relative to the baseline + * of the containing line. Positive values shift upwards + * @start_x_offset: horizontal displacement to apply before the + * glyph item. Positive values shift right + * @end_x_offset: horizontal displacement to apply after th + * glyph item. Positive values shift right + * + * A `Pango2GlyphItem` is a pair of a `Pango2Item` and the glyphs + * resulting from shaping the items text. + * + * As an example of the usage of `Pango2GlyphItem`, the results + * of shaping text with `Pango2Layout` is a list of `Pango2Line`, + * each of which contains a list of `Pango2GlyphItem`. + */ +typedef struct _Pango2GlyphItem Pango2GlyphItem; + +struct _Pango2GlyphItem +{ + Pango2Item *item; + Pango2GlyphString *glyphs; + int y_offset; + int start_x_offset; + int end_x_offset; +}; + +#define PANGO2_TYPE_GLYPH_ITEM (pango2_glyph_item_get_type ()) + +PANGO2_AVAILABLE_IN_ALL +GType pango2_glyph_item_get_type (void) G_GNUC_CONST; + +PANGO2_AVAILABLE_IN_ALL +Pango2GlyphItem * pango2_glyph_item_split (Pango2GlyphItem *orig, + const char *text, + int split_index); +PANGO2_AVAILABLE_IN_ALL +Pango2GlyphItem * pango2_glyph_item_copy (Pango2GlyphItem *orig); +PANGO2_AVAILABLE_IN_ALL +void pango2_glyph_item_free (Pango2GlyphItem *glyph_item); +PANGO2_AVAILABLE_IN_ALL +GSList * pango2_glyph_item_apply_attrs (Pango2GlyphItem *glyph_item, + const char *text, + Pango2AttrList *list); +PANGO2_AVAILABLE_IN_ALL +void pango2_glyph_item_letter_space (Pango2GlyphItem *glyph_item, + const char *text, + Pango2LogAttr *log_attrs, + int letter_spacing); +PANGO2_AVAILABLE_IN_ALL +void pango2_glyph_item_get_logical_widths (Pango2GlyphItem *glyph_item, + const char *text, + int *logical_widths); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(Pango2GlyphItem, pango2_glyph_item_free) + + +G_END_DECLS diff --git a/pango2/pango-glyph-item.c b/pango2/pango-glyph-item.c new file mode 100644 index 00000000..6ba75961 --- /dev/null +++ b/pango2/pango-glyph-item.c @@ -0,0 +1,837 @@ +/* Pango2 + * pango-glyph-item.c: Pair of Pango2Item and a glyph string + * + * Copyright (C) 2002 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include <string.h> + +#include "pango-glyph-item-private.h" +#include "pango-glyph-iter-private.h" +#include "pango-item-private.h" +#include "pango-impl-utils.h" +#include "pango-attr-list-private.h" +#include "pango-attr-iterator-private.h" + +#define LTR(glyph_item) (((glyph_item)->item->analysis.level % 2) == 0) + +/** + * pango2_glyph_item_split: + * @orig: a `Pango2Item` + * @text: text to which positions in @orig apply + * @split_index: byte index of position to split item, relative to the + * start of the item + * + * Modifies @orig to cover only the text after @split_index, and + * returns a new item that covers the text before @split_index that + * used to be in @orig. + * + * You can think of @split_index as the length of the returned item. + * @split_index may not be 0, and it may not be greater than or equal + * to the length of @orig (that is, there must be at least one byte + * assigned to each item, you can't create a zero-length item). + * + * Return value: the newly allocated item representing text before + * @split_index, which should be freed with [method@Pango2.GlyphItem.free] + */ +Pango2GlyphItem * +pango2_glyph_item_split (Pango2GlyphItem *orig, + const char *text, + int split_index) +{ + Pango2GlyphItem *new; + int i; + int num_glyphs; + int num_remaining; + int split_offset; + + g_return_val_if_fail (orig != NULL, NULL); + g_return_val_if_fail (orig->item->length > 0, NULL); + g_return_val_if_fail (split_index > 0, NULL); + g_return_val_if_fail (split_index < orig->item->length, NULL); + + if (LTR (orig)) + { + for (i = 0; i < orig->glyphs->num_glyphs; i++) + { + if (orig->glyphs->log_clusters[i] >= split_index) + break; + } + + if (i == orig->glyphs->num_glyphs) /* No splitting necessary */ + return NULL; + + split_index = orig->glyphs->log_clusters[i]; + num_glyphs = i; + } + else + { + for (i = orig->glyphs->num_glyphs - 1; i >= 0; i--) + { + if (orig->glyphs->log_clusters[i] >= split_index) + break; + } + + if (i < 0) /* No splitting necessary */ + return NULL; + + split_index = orig->glyphs->log_clusters[i]; + num_glyphs = orig->glyphs->num_glyphs - 1 - i; + } + + num_remaining = orig->glyphs->num_glyphs - num_glyphs; + + new = g_slice_new (Pango2GlyphItem); + split_offset = g_utf8_pointer_to_offset (text + orig->item->offset, + text + orig->item->offset + split_index); + new->item = pango2_item_split (orig->item, split_index, split_offset); + + new->glyphs = pango2_glyph_string_new (); + pango2_glyph_string_set_size (new->glyphs, num_glyphs); + + if (LTR (orig)) + { + memcpy (new->glyphs->glyphs, orig->glyphs->glyphs, num_glyphs * sizeof (Pango2GlyphInfo)); + memcpy (new->glyphs->log_clusters, orig->glyphs->log_clusters, num_glyphs * sizeof (int)); + + memmove (orig->glyphs->glyphs, orig->glyphs->glyphs + num_glyphs, + num_remaining * sizeof (Pango2GlyphInfo)); + for (i = num_glyphs; i < orig->glyphs->num_glyphs; i++) + orig->glyphs->log_clusters[i - num_glyphs] = orig->glyphs->log_clusters[i] - split_index; + } + else + { + memcpy (new->glyphs->glyphs, orig->glyphs->glyphs + num_remaining, num_glyphs * sizeof (Pango2GlyphInfo)); + memcpy (new->glyphs->log_clusters, orig->glyphs->log_clusters + num_remaining, num_glyphs * sizeof (int)); + + for (i = 0; i < num_remaining; i++) + orig->glyphs->log_clusters[i] = orig->glyphs->log_clusters[i] - split_index; + } + + pango2_glyph_string_set_size (orig->glyphs, orig->glyphs->num_glyphs - num_glyphs); + + new->y_offset = orig->y_offset; + new->start_x_offset = orig->start_x_offset; + new->end_x_offset = -orig->start_x_offset; + + return new; +} + +/** + * pango2_glyph_item_copy: + * @orig: (nullable): a `Pango2GlyphItem` + * + * Make a deep copy of an existing `Pango2GlyphItem` structure. + * + * Return value: (nullable): the newly allocated `Pango2GlyphItem` + */ +Pango2GlyphItem * +pango2_glyph_item_copy (Pango2GlyphItem *orig) +{ + Pango2GlyphItem *result; + + if (orig == NULL) + return NULL; + + result = g_slice_new (Pango2GlyphItem); + + result->item = pango2_item_copy (orig->item); + result->glyphs = pango2_glyph_string_copy (orig->glyphs); + result->y_offset = orig->y_offset; + result->start_x_offset = orig->start_x_offset; + result->end_x_offset = orig->end_x_offset; + + return result; +} + +/** + * pango2_glyph_item_free: + * @glyph_item: (nullable): a `Pango2GlyphItem` + * + * Frees a `Pango2GlyphItem` and resources to which it points. + */ +void +pango2_glyph_item_free (Pango2GlyphItem *glyph_item) +{ + if (glyph_item == NULL) + return; + + if (glyph_item->item) + pango2_item_free (glyph_item->item); + if (glyph_item->glyphs) + pango2_glyph_string_free (glyph_item->glyphs); + + g_slice_free (Pango2GlyphItem, glyph_item); +} + +G_DEFINE_BOXED_TYPE (Pango2GlyphItem, pango2_glyph_item, + pango2_glyph_item_copy, + pango2_glyph_item_free); + + +/** + * pango2_glyph_item_iter_copy: + * @orig: (nullable): a `Pango2GlyphItem`Iter + * + * Make a shallow copy of an existing `Pango2GlyphItemIter` structure. + * + * Return value: (nullable): the newly allocated `Pango2GlyphItemIter` + */ +Pango2GlyphItemIter * +pango2_glyph_item_iter_copy (Pango2GlyphItemIter *orig) +{ + Pango2GlyphItemIter *result; + + if (orig == NULL) + return NULL; + + result = g_slice_new (Pango2GlyphItemIter); + + *result = *orig; + + return result; +} + +/** + * pango2_glyph_item_iter_free: + * @iter: (nullable): a `Pango2GlyphItemIter` + * + * Frees a `Pango2GlyphItemIter`. + */ +void +pango2_glyph_item_iter_free (Pango2GlyphItemIter *iter) +{ + if (iter == NULL) + return; + + g_slice_free (Pango2GlyphItemIter, iter); +} + +G_DEFINE_BOXED_TYPE (Pango2GlyphItemIter, pango2_glyph_item_iter, + pango2_glyph_item_iter_copy, + pango2_glyph_item_iter_free) + +/** + * pango2_glyph_item_iter_next_cluster: + * @iter: a `Pango2GlyphItemIter` + * + * Advances the iterator to the next cluster in the glyph item. + * + * See `Pango2GlyphItemIter` for details of cluster orders. + * + * Return value: %TRUE if the iterator was advanced, + * %FALSE if we were already on the last cluster. + */ +gboolean +pango2_glyph_item_iter_next_cluster (Pango2GlyphItemIter *iter) +{ + int glyph_index = iter->end_glyph; + Pango2GlyphString *glyphs = iter->glyph_item->glyphs; + int cluster; + Pango2Item *item = iter->glyph_item->item; + + if (LTR (iter->glyph_item)) + { + if (glyph_index == glyphs->num_glyphs) + return FALSE; + } + else + { + if (glyph_index < 0) + return FALSE; + } + + iter->start_glyph = iter->end_glyph; + iter->start_index = iter->end_index; + iter->start_char = iter->end_char; + + if (LTR (iter->glyph_item)) + { + cluster = glyphs->log_clusters[glyph_index]; + while (TRUE) + { + glyph_index++; + + if (glyph_index == glyphs->num_glyphs) + { + iter->end_index = item->offset + item->length; + iter->end_char = item->num_chars; + break; + } + + if (glyphs->log_clusters[glyph_index] > cluster) + { + iter->end_index = item->offset + glyphs->log_clusters[glyph_index]; + iter->end_char += pango2_utf8_strlen (iter->text + iter->start_index, + iter->end_index - iter->start_index); + break; + } + } + } + else /* RTL */ + { + cluster = glyphs->log_clusters[glyph_index]; + while (TRUE) + { + glyph_index--; + + if (glyph_index < 0) + { + iter->end_index = item->offset + item->length; + iter->end_char = item->num_chars; + break; + } + + if (glyphs->log_clusters[glyph_index] > cluster) + { + iter->end_index = item->offset + glyphs->log_clusters[glyph_index]; + iter->end_char += pango2_utf8_strlen (iter->text + iter->start_index, + iter->end_index - iter->start_index); + break; + } + } + } + + iter->end_glyph = glyph_index; + + g_assert (iter->start_char <= iter->end_char); + g_assert (iter->end_char <= item->num_chars); + + return TRUE; +} + +/** + * pango2_glyph_item_iter_prev_cluster: + * @iter: a `Pango2GlyphItemIter` + * + * Moves the iterator to the preceding cluster in the glyph item. + * See `Pango2GlyphItemIter` for details of cluster orders. + * + * Return value: %TRUE if the iterator was moved, + * %FALSE if we were already on the first cluster. + */ +gboolean +pango2_glyph_item_iter_prev_cluster (Pango2GlyphItemIter *iter) +{ + int glyph_index = iter->start_glyph; + Pango2GlyphString *glyphs = iter->glyph_item->glyphs; + int cluster; + Pango2Item *item = iter->glyph_item->item; + + if (LTR (iter->glyph_item)) + { + if (glyph_index == 0) + return FALSE; + } + else + { + if (glyph_index == glyphs->num_glyphs - 1) + return FALSE; + + } + + iter->end_glyph = iter->start_glyph; + iter->end_index = iter->start_index; + iter->end_char = iter->start_char; + + if (LTR (iter->glyph_item)) + { + cluster = glyphs->log_clusters[glyph_index - 1]; + while (TRUE) + { + if (glyph_index == 0) + { + iter->start_index = item->offset; + iter->start_char = 0; + break; + } + + glyph_index--; + + if (glyphs->log_clusters[glyph_index] < cluster) + { + glyph_index++; + iter->start_index = item->offset + glyphs->log_clusters[glyph_index]; + iter->start_char -= pango2_utf8_strlen (iter->text + iter->start_index, + iter->end_index - iter->start_index); + break; + } + } + } + else /* RTL */ + { + cluster = glyphs->log_clusters[glyph_index + 1]; + while (TRUE) + { + if (glyph_index == glyphs->num_glyphs - 1) + { + iter->start_index = item->offset; + iter->start_char = 0; + break; + } + + glyph_index++; + + if (glyphs->log_clusters[glyph_index] < cluster) + { + glyph_index--; + iter->start_index = item->offset + glyphs->log_clusters[glyph_index]; + iter->start_char -= pango2_utf8_strlen (iter->text + iter->start_index, + iter->end_index - iter->start_index); + break; + } + } + } + + iter->start_glyph = glyph_index; + + g_assert (iter->start_char <= iter->end_char); + g_assert (0 <= iter->start_char); + + return TRUE; +} + +/** + * pango2_glyph_item_iter_init_start: + * @iter: a `Pango2GlyphItemIter` + * @glyph_item: the glyph item to iterate over + * @text: text corresponding to the glyph item + * + * Initializes a `Pango2GlyphItemIter` structure to point to the + * first cluster in a glyph item. + * + * See `Pango2GlyphItemIter` for details of cluster orders. + * + * Return value: %FALSE if there are no clusters in the glyph item + */ +gboolean +pango2_glyph_item_iter_init_start (Pango2GlyphItemIter *iter, + Pango2GlyphItem *glyph_item, + const char *text) +{ + iter->glyph_item = glyph_item; + iter->text = text; + + if (LTR (glyph_item)) + iter->end_glyph = 0; + else + iter->end_glyph = glyph_item->glyphs->num_glyphs - 1; + + iter->end_index = glyph_item->item->offset; + iter->end_char = 0; + + iter->start_glyph = iter->end_glyph; + iter->start_index = iter->end_index; + iter->start_char = iter->end_char; + + /* Advance onto the first cluster of the glyph item */ + return pango2_glyph_item_iter_next_cluster (iter); +} + +/** + * pango2_glyph_item_iter_init_end: + * @iter: a `Pango2GlyphItemIter` + * @glyph_item: the glyph item to iterate over + * @text: text corresponding to the glyph item + * + * Initializes a `Pango2GlyphItemIter` structure to point to the + * last cluster in a glyph item. + * + * See `Pango2GlyphItemIter` for details of cluster orders. + * + * Return value: %FALSE if there are no clusters in the glyph item + */ +gboolean +pango2_glyph_item_iter_init_end (Pango2GlyphItemIter *iter, + Pango2GlyphItem *glyph_item, + const char *text) +{ + iter->glyph_item = glyph_item; + iter->text = text; + + if (LTR (glyph_item)) + iter->start_glyph = glyph_item->glyphs->num_glyphs; + else + iter->start_glyph = -1; + + iter->start_index = glyph_item->item->offset + glyph_item->item->length; + iter->start_char = glyph_item->item->num_chars; + + iter->end_glyph = iter->start_glyph; + iter->end_index = iter->start_index; + iter->end_char = iter->start_char; + + /* Advance onto the first cluster of the glyph item */ + return pango2_glyph_item_iter_prev_cluster (iter); +} + +typedef struct +{ + Pango2GlyphItemIter iter; + + GSList *segment_attrs; +} ApplyAttrsState; + +/* Tack @attrs onto the attributes of glyph_item + */ +static void +append_attrs (Pango2GlyphItem *glyph_item, + GSList *attrs) +{ + glyph_item->item->analysis.extra_attrs = + g_slist_concat (glyph_item->item->analysis.extra_attrs, attrs); +} + +/* Make a deep copy of a GSList of Pango2Attribute + */ +static GSList * +attr_slist_copy (GSList *attrs) +{ + GSList *tmp_list; + GSList *new_attrs; + + new_attrs = g_slist_copy (attrs); + + for (tmp_list = new_attrs; tmp_list; tmp_list = tmp_list->next) + tmp_list->data = pango2_attribute_copy (tmp_list->data); + + return new_attrs; +} + +/* Split the glyph item at the start of the current cluster + */ +static Pango2GlyphItem * +split_before_cluster_start (ApplyAttrsState *state) +{ + Pango2GlyphItem *split_item; + int split_len = state->iter.start_index - state->iter.glyph_item->item->offset; + + split_item = pango2_glyph_item_split (state->iter.glyph_item, state->iter.text, split_len); + append_attrs (split_item, state->segment_attrs); + + /* Adjust iteration to account for the split + */ + if (LTR (state->iter.glyph_item)) + { + state->iter.start_glyph -= split_item->glyphs->num_glyphs; + state->iter.end_glyph -= split_item->glyphs->num_glyphs; + } + + state->iter.start_char -= split_item->item->num_chars; + state->iter.end_char -= split_item->item->num_chars; + + return split_item; +} + +/** + * pango2_glyph_item_apply_attrs: + * @glyph_item: a shaped item + * @text: text that @list applies to + * @list: a `Pango2AttrList` + * + * Splits a shaped item (`Pango2GlyphItem`) into multiple items based + * on an attribute list. + * + * The idea is that if you have attributes that don't affect shaping, + * such as color or underline, to avoid affecting shaping, you filter + * them out ([method@Pango2.AttrList.filter]), apply the shaping process + * and then reapply them to the result using this function. + * + * All attributes that start or end inside a cluster are applied + * to that cluster; for instance, if half of a cluster is underlined + * and the other-half strikethrough, then the cluster will end + * up with both underline and strikethrough attributes. In these + * cases, it may happen that @item->extra_attrs for some of the + * result items can have multiple attributes of the same type. + * + * This function takes ownership of @glyph_item; it will be reused + * as one of the elements in the list. + * + * Return value: (transfer full) (element-type Pango2.GlyphItem): a + * list of glyph items resulting from splitting @glyph_item. Free + * the elements using [method@Pango2.GlyphItem.free], the list using + * [func@GLib.SList.free] + */ +GSList * +pango2_glyph_item_apply_attrs (Pango2GlyphItem *glyph_item, + const char *text, + Pango2AttrList *list) +{ + Pango2AttrIterator iter; + GSList *result = NULL; + ApplyAttrsState state; + gboolean start_new_segment = FALSE; + gboolean have_cluster; + int range_start, range_end; + gboolean is_ellipsis; + + /* This routine works by iterating through the item cluster by + * cluster; we accumulate the attributes that we need to + * add to the next output item, and decide when to split + * off an output item based on two criteria: + * + * A) If start_index < attribute_start < end_index + * (attribute starts within cluster) then we need + * to split between the last cluster and this cluster. + * B) If start_index < attribute_end <= end_index, + * (attribute ends within cluster) then we need to + * split between this cluster and the next one. + */ + + /* Advance the attr iterator to the start of the item + */ + pango2_attr_list_init_iterator (list, &iter); + do + { + pango2_attr_iterator_range (&iter, &range_start, &range_end); + if (range_end > glyph_item->item->offset) + break; + } + while (pango2_attr_iterator_next (&iter)); + + state.segment_attrs = pango2_attr_iterator_get_attrs (&iter); + + is_ellipsis = (glyph_item->item->analysis.flags & PANGO2_ANALYSIS_FLAG_IS_ELLIPSIS) != 0; + + /* Short circuit the case when we don't actually need to + * split the item + */ + if (is_ellipsis || + (range_start <= glyph_item->item->offset && + range_end >= glyph_item->item->offset + glyph_item->item->length)) + goto out; + + for (have_cluster = pango2_glyph_item_iter_init_start (&state.iter, glyph_item, text); + have_cluster; + have_cluster = pango2_glyph_item_iter_next_cluster (&state.iter)) + { + gboolean have_next; + + /* [range_start,range_end] is the first range that intersects + * the current cluster. + */ + + /* Split item into two, if this cluster isn't a continuation + * of the last cluster + */ + if (start_new_segment) + { + result = g_slist_prepend (result, + split_before_cluster_start (&state)); + state.segment_attrs = pango2_attr_iterator_get_attrs (&iter); + } + + start_new_segment = FALSE; + + /* Loop over all ranges that intersect this cluster; exiting + * leaving [range_start,range_end] being the first range that + * intersects the next cluster. + */ + do + { + if (range_end > state.iter.end_index) /* Range intersects next cluster */ + break; + + /* Since ranges end in this cluster, the next cluster goes into a + * separate segment + */ + start_new_segment = TRUE; + + have_next = pango2_attr_iterator_next (&iter); + pango2_attr_iterator_range (&iter, &range_start, &range_end); + + if (range_start >= state.iter.end_index) /* New range doesn't intersect this cluster */ + { + /* No gap between ranges, so previous range must of ended + * at cluster boundary. + */ + g_assert (range_start == state.iter.end_index && start_new_segment); + break; + } + + /* If any ranges start *inside* this cluster, then we need + * to split the previous cluster into a separate segment + */ + if (range_start > state.iter.start_index && + state.iter.start_index != glyph_item->item->offset) + { + GSList *new_attrs = attr_slist_copy (state.segment_attrs); + result = g_slist_prepend (result, + split_before_cluster_start (&state)); + state.segment_attrs = new_attrs; + } + + state.segment_attrs = g_slist_concat (state.segment_attrs, + pango2_attr_iterator_get_attrs (&iter)); + } + while (have_next); + } + + out: + /* What's left in glyph_item is the remaining portion + */ + append_attrs (glyph_item, state.segment_attrs); + result = g_slist_prepend (result, glyph_item); + + if (LTR (glyph_item)) + result = g_slist_reverse (result); + + pango2_attr_iterator_clear (&iter); + + return result; +} + +/** + * pango2_glyph_item_letter_space: + * @glyph_item: a `Pango2GlyphItem` + * @text: text that @glyph_item corresponds to + * (glyph_item->item->offset is an offset from the + * start of @text) + * @log_attrs: (array): logical attributes for the item + * (the first logical attribute refers to the position + * before the first character in the item) + * @letter_spacing: amount of letter spacing to add + * in Pango2 units. May be negative, though too large + * negative values will give ugly results. + * + * Adds spacing between the graphemes of @glyph_item to + * give the effect of typographic letter spacing. + */ +void +pango2_glyph_item_letter_space (Pango2GlyphItem *glyph_item, + const char *text, + Pango2LogAttr *log_attrs, + int letter_spacing) +{ + Pango2GlyphItemIter iter; + Pango2GlyphInfo *glyphs = glyph_item->glyphs->glyphs; + gboolean have_cluster; + int space_left, space_right; + + space_left = letter_spacing / 2; + + /* hinting */ + if ((letter_spacing & (PANGO2_SCALE - 1)) == 0) + { + space_left = PANGO2_UNITS_ROUND (space_left); + } + + space_right = letter_spacing - space_left; + + for (have_cluster = pango2_glyph_item_iter_init_start (&iter, glyph_item, text); + have_cluster; + have_cluster = pango2_glyph_item_iter_next_cluster (&iter)) + { + if (!log_attrs[iter.start_char].is_cursor_position) + { + if (glyphs[iter.start_glyph].geometry.width == 0) + { + if (iter.start_glyph < iter.end_glyph) /* LTR */ + glyphs[iter.start_glyph].geometry.x_offset -= space_right; + else + glyphs[iter.start_glyph].geometry.x_offset += space_left; + } + continue; + } + + if (iter.start_glyph < iter.end_glyph) /* LTR */ + { + if (iter.start_char > 0) + { + glyphs[iter.start_glyph].geometry.width += space_left ; + glyphs[iter.start_glyph].geometry.x_offset += space_left ; + } + if (iter.end_char < glyph_item->item->num_chars) + { + glyphs[iter.end_glyph-1].geometry.width += space_right; + } + } + else /* RTL */ + { + if (iter.start_char > 0) + { + glyphs[iter.start_glyph].geometry.width += space_right; + } + if (iter.end_char < glyph_item->item->num_chars) + { + glyphs[iter.end_glyph+1].geometry.x_offset += space_left ; + glyphs[iter.end_glyph+1].geometry.width += space_left ; + } + } + } +} + +/** + * pango2_glyph_item_get_logical_widths: + * @glyph_item: a `Pango2GlyphItem` + * @text: text that @glyph_item corresponds to + * (glyph_item->item->offset is an offset from the + * start of @text) + * @logical_widths: (array): an array whose length is the number of + * characters in glyph_item (equal to glyph_item->item->num_chars) + * to be filled in with the resulting character widths. + * + * Given a `Pango2GlyphItem` and the corresponding text, determine the + * width corresponding to each character. + * + * When multiple characters compose a single cluster, the width of the + * entire cluster is divided equally among the characters. + * + * See also [method@Pango2.GlyphString.get_logical_widths]. + */ +void +pango2_glyph_item_get_logical_widths (Pango2GlyphItem *glyph_item, + const char *text, + int *logical_widths) +{ + Pango2GlyphItemIter iter; + gboolean has_cluster; + int dir; + + dir = glyph_item->item->analysis.level % 2 == 0 ? +1 : -1; + for (has_cluster = pango2_glyph_item_iter_init_start (&iter, glyph_item, text); + has_cluster; + has_cluster = pango2_glyph_item_iter_next_cluster (&iter)) + { + int glyph_index, char_index, num_chars, cluster_width = 0, char_width; + + for (glyph_index = iter.start_glyph; + glyph_index != iter.end_glyph; + glyph_index += dir) + { + cluster_width += glyph_item->glyphs->glyphs[glyph_index].geometry.width; + } + + num_chars = iter.end_char - iter.start_char; + if (num_chars) /* pedantic */ + { + char_width = cluster_width / num_chars; + + for (char_index = iter.start_char; + char_index < iter.end_char; + char_index++) + { + logical_widths[char_index] = char_width; + } + + /* add any residues to the first char */ + logical_widths[iter.start_char] += cluster_width - (char_width * num_chars); + } + } +} diff --git a/pango2/pango-glyph-iter-private.h b/pango2/pango-glyph-iter-private.h new file mode 100644 index 00000000..5321b33d --- /dev/null +++ b/pango2/pango-glyph-iter-private.h @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2002 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-glyph-item-private.h" + +G_BEGIN_DECLS + +/** + * Pango2GlyphItemIter: + * + * A `Pango2GlyphItemIter` is an iterator over the clusters in a + * `Pango2GlyphItem`. + * + * The *forward direction* of the iterator is the logical direction of text. + * That is, with increasing @start_index and @start_char values. If @glyph_item + * is right-to-left (that is, if `glyph_item->item->analysis.level` is odd), + * then @start_glyph decreases as the iterator moves forward. Moreover, + * in right-to-left cases, @start_glyph is greater than @end_glyph. + * + * An iterator should be initialized using either + * [method@Pango2.GlyphItemIter.init_start] or + * [method@Pango2.GlyphItemIter.init_end], for forward and backward iteration + * respectively, and walked over using any desired mixture of + * [method@Pango2.GlyphItemIter.next_cluster] and + * [method@Pango2.GlyphItemIter.prev_cluster]. + * + * A common idiom for doing a forward iteration over the clusters is: + * + * ``` + * Pango2GlyphItemIter cluster_iter; + * gboolean have_cluster; + * + * for (have_cluster = pango2_glyph_item_iter_init_start (&cluster_iter, + * glyph_item, text); + * have_cluster; + * have_cluster = pango2_glyph_item_iter_next_cluster (&cluster_iter)) + * { + * ... + * } + * ``` + * + * Note that @text is the start of the text for layout, which is then + * indexed by `glyph_item->item->offset` to get to the text of @glyph_item. + * The @start_index and @end_index values can directly index into @text. The + * @start_glyph, @end_glyph, @start_char, and @end_char values however are + * zero-based for the @glyph_item. For each cluster, the item pointed at by + * the start variables is included in the cluster while the one pointed at by + * end variables is not. + * + * None of the members of a `Pango2GlyphItemIter` should be modified manually. + */ +typedef struct _Pango2GlyphItemIter Pango2GlyphItemIter; + +struct _Pango2GlyphItemIter +{ + Pango2GlyphItem *glyph_item; + const char *text; + + int start_glyph; + int start_index; + int start_char; + + int end_glyph; + int end_index; + int end_char; +}; + +#define PANGO2_TYPE_GLYPH_ITEM_ITER (pango2_glyph_item_iter_get_type ()) + +PANGO2_AVAILABLE_IN_ALL +GType pango2_glyph_item_iter_get_type (void) G_GNUC_CONST; +PANGO2_AVAILABLE_IN_ALL +Pango2GlyphItemIter * pango2_glyph_item_iter_copy (Pango2GlyphItemIter *orig); +PANGO2_AVAILABLE_IN_ALL +void pango2_glyph_item_iter_free (Pango2GlyphItemIter *iter); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_glyph_item_iter_init_start (Pango2GlyphItemIter *iter, + Pango2GlyphItem *glyph_item, + const char *text); +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_glyph_item_iter_init_end (Pango2GlyphItemIter *iter, + Pango2GlyphItem *glyph_item, + const char *text); +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_glyph_item_iter_next_cluster (Pango2GlyphItemIter *iter); +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_glyph_item_iter_prev_cluster (Pango2GlyphItemIter *iter); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(Pango2GlyphItemIter, pango2_glyph_item_iter_free) + +G_END_DECLS diff --git a/pango2/pango-glyph.h b/pango2/pango-glyph.h new file mode 100644 index 00000000..7cc014a2 --- /dev/null +++ b/pango2/pango-glyph.h @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2000 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-types.h> +#include <pango2/pango-item.h> +#include <pango2/pango-break.h> + +G_BEGIN_DECLS + +typedef struct _Pango2GlyphGeometry Pango2GlyphGeometry; +typedef struct _Pango2GlyphVisAttr Pango2GlyphVisAttr; +typedef struct _Pango2GlyphInfo Pango2GlyphInfo; +typedef struct _Pango2GlyphString Pango2GlyphString; + +/* 1024ths of a device unit */ +/** + * Pango2GlyphUnit: + * + * The `Pango2GlyphUnit` type is used to store dimensions within + * Pango2. + * + * Dimensions are stored in 1/PANGO2_SCALE of a device unit. + * (A device unit might be a pixel for screen display, or + * a point on a printer.) PANGO2_SCALE is currently 1024, and + * may change in the future (unlikely though), but you should not + * depend on its exact value. + * + * The PANGO2_PIXELS() macro can be used to convert from glyph units + * into device units with correct rounding. + */ +typedef gint32 Pango2GlyphUnit; + +/* Positioning information about a glyph + */ +/** + * Pango2GlyphGeometry: + * @width: the logical width to use for the the character. + * @x_offset: horizontal offset from nominal character position. + * @y_offset: vertical offset from nominal character position. + * + * The `Pango2GlyphGeometry` structure contains width and positioning + * information for a single glyph. + * + * Note that @width is not guaranteed to be the same as the glyph + * extents. Kerning and other positioning applied during shaping will + * affect both the @width and the @x_offset for the glyphs in the + * glyph string that results from shaping. + * + * The information in this struct is intended for rendering the glyphs, + * as follows: + * + * 1. Assume the current point is (x, y) + * 2. Render the current glyph at (x + x_offset, y + y_offset), + * 3. Advance the current point to (x + width, y) + * 4. Render the next glyph + */ +struct _Pango2GlyphGeometry +{ + Pango2GlyphUnit width; + Pango2GlyphUnit x_offset; + Pango2GlyphUnit y_offset; +}; + +/* Visual attributes of a glyph + */ +/** + * Pango2GlyphVisAttr: + * @is_cluster_start: set for the first logical glyph in each cluster. + * @is_color: set if the the font will render this glyph with color. Since 1.50 + * + * A `Pango2GlyphVisAttr` structure communicates information between + * the shaping and rendering phases. + * + * Currently, it contains cluster start and color information. + * More attributes may be added in the future. + * + * Clusters are stored in visual order, within the cluster, glyphs + * are always ordered in logical order, since visual order is meaningless; + * that is, in Arabic text, accent glyphs follow the glyphs for the + * base character. + */ +struct _Pango2GlyphVisAttr +{ + guint is_cluster_start : 1; + guint is_color : 1; +}; + +/* A single glyph + */ +/** + * Pango2GlyphInfo: + * @glyph: the glyph itself. + * @geometry: the positional information about the glyph. + * @attr: the visual attributes of the glyph. + * + * A `Pango2GlyphInfo` structure represents a single glyph with + * positioning information and visual attributes. + */ +struct _Pango2GlyphInfo +{ + Pango2Glyph glyph; + Pango2GlyphGeometry geometry; + Pango2GlyphVisAttr attr; +}; + +/** + * Pango2GlyphString: + * @num_glyphs: number of glyphs in this glyph string + * @glyphs: (array length=num_glyphs): array of glyph information + * @log_clusters: logical cluster info, indexed by the byte index + * within the text corresponding to the glyph string + * + * A `Pango2GlyphString` is used to store strings of glyphs with geometry + * and visual attribute information. + * + * The storage for the glyph information is owned by the structure + * which simplifies memory management. + */ +struct _Pango2GlyphString { + int num_glyphs; + + Pango2GlyphInfo *glyphs; + int *log_clusters; + + /*< private >*/ + int space; +}; + +#define PANGO2_TYPE_GLYPH_STRING (pango2_glyph_string_get_type ()) + +PANGO2_AVAILABLE_IN_ALL +GType pango2_glyph_string_get_type (void) G_GNUC_CONST; + +PANGO2_AVAILABLE_IN_ALL +Pango2GlyphString * pango2_glyph_string_new (void); +PANGO2_AVAILABLE_IN_ALL +void pango2_glyph_string_set_size (Pango2GlyphString *string, + int new_len); + +PANGO2_AVAILABLE_IN_ALL +Pango2GlyphString * pango2_glyph_string_copy (Pango2GlyphString *string); +PANGO2_AVAILABLE_IN_ALL +void pango2_glyph_string_free (Pango2GlyphString *string); + +PANGO2_AVAILABLE_IN_ALL +void pango2_glyph_string_extents (Pango2GlyphString *glyphs, + Pango2Font *font, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect); +PANGO2_AVAILABLE_IN_ALL +int pango2_glyph_string_get_width (Pango2GlyphString *glyphs); + +PANGO2_AVAILABLE_IN_ALL +void pango2_glyph_string_extents_range (Pango2GlyphString *glyphs, + int start, + int end, + Pango2Font *font, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect); + +PANGO2_AVAILABLE_IN_ALL +void pango2_glyph_string_get_logical_widths (Pango2GlyphString *glyphs, + const char *text, + int length, + int embedding_level, + int *logical_widths); + +PANGO2_AVAILABLE_IN_ALL +void pango2_glyph_string_index_to_x (Pango2GlyphString *glyphs, + const char *text, + int length, + const Pango2Analysis *analysis, + int index_, + gboolean trailing, + int *x_pos); +PANGO2_AVAILABLE_IN_ALL +void pango2_glyph_string_x_to_index (Pango2GlyphString *glyphs, + const char *text, + int length, + const Pango2Analysis *analysis, + int x_pos, + int *index_, + int *trailing); + +PANGO2_AVAILABLE_IN_ALL +void pango2_glyph_string_index_to_x_full (Pango2GlyphString *glyphs, + const char *text, + int length, + const Pango2Analysis *analysis, + Pango2LogAttr *attrs, + int index_, + gboolean trailing, + int *x_pos); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(Pango2GlyphString, pango2_glyph_string_free) + + +/* Shaping */ + +/** + * Pango2ShapeFlags: + * @PANGO2_SHAPE_NONE: Default value + * @PANGO2_SHAPE_ROUND_POSITIONS: Round glyph positions and widths to whole device units + * This option should be set if the target renderer can't do subpixel positioning of glyphs + * + * `Pango2ShapeFlags` influence the shaping process. + * + * These flags can be passed to [func@Pango2.shape]. + */ +typedef enum { + PANGO2_SHAPE_NONE = 0, + PANGO2_SHAPE_ROUND_POSITIONS = 1 << 0, +} Pango2ShapeFlags; + +PANGO2_AVAILABLE_IN_ALL +void pango2_shape (const char *item_text, + int item_length, + const char *paragraph_text, + int paragraph_length, + const Pango2Analysis *analysis, + Pango2GlyphString *glyphs, + Pango2ShapeFlags flags); + + +PANGO2_AVAILABLE_IN_ALL +void pango2_shape_item (Pango2Item *item, + const char *paragraph_text, + int paragraph_length, + Pango2LogAttr *log_attrs, + Pango2GlyphString *glyphs, + Pango2ShapeFlags flags); + +/** + * PANGO2_GLYPH_EMPTY: + * + * A `Pango2Glyph` value that indicates a zero-width empty glpyh. + * + * This is useful for example in shaper modules, to use as the glyph for + * various zero-width Unicode characters (those passing [func@is_zero_width]). + */ +#define PANGO2_GLYPH_EMPTY ((Pango2Glyph)0x0FFFFFFF) + +/** + * PANGO2_GLYPH_INVALID_INPUT: + * + * A `Pango2Glyph` value for invalid input. + * + * `Pango2Layout` produces one such glyph per invalid input UTF-8 byte and such + * a glyph is rendered as a crossed box. + * + * Note that this value is defined such that it has the %PANGO2_GLYPH_UNKNOWN_FLAG + * set. + */ +#define PANGO2_GLYPH_INVALID_INPUT ((Pango2Glyph)0xFFFFFFFF) + +/** + * PANGO2_GLYPH_UNKNOWN_FLAG: + * + * Flag used in `Pango2Glyph` to turn a `gunichar` value of a valid Unicode + * character into an unknown-character glyph for that `gunichar`. + * + * Such unknown-character glyphs may be rendered as a 'hex box'. + */ +#define PANGO2_GLYPH_UNKNOWN_FLAG ((Pango2Glyph)0x10000000) + +/** + * PANGO2_GET_UNKNOWN_GLYPH: + * @wc: a Unicode character + * + * The way this unknown glyphs are rendered is backend specific. For example, + * a box with the hexadecimal Unicode code-point of the character written in it + * is what is done in the most common backends. + * + * Returns: a `Pango2Glyph` value that means no glyph was found for @wc. + */ +#define PANGO2_GET_UNKNOWN_GLYPH(wc) ((Pango2Glyph)(wc)|PANGO2_GLYPH_UNKNOWN_FLAG) + + +G_END_DECLS diff --git a/pango2/pango-gravity.c b/pango2/pango-gravity.c new file mode 100644 index 00000000..0e9b4299 --- /dev/null +++ b/pango2/pango-gravity.c @@ -0,0 +1,457 @@ +/* Pango2 + * pango-gravity.c: Gravity routines + * + * Copyright (C) 2006, 2007 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include "pango-gravity.h" + +#include <math.h> + +/** + * pango2_gravity_to_rotation: + * @gravity: gravity to query, should not be %PANGO2_GRAVITY_AUTO + * + * Converts a `Pango2Gravity` value to its natural rotation in radians. + * + * Note that [method@Pango2.Matrix.rotate] takes angle in degrees, not radians. + * So, to call [method@Pango2.Matrix,rotate] with the output of this function + * you should multiply it by (180. / G_PI). + * + * Return value: the rotation value corresponding to @gravity. + */ +double +pango2_gravity_to_rotation (Pango2Gravity gravity) +{ + double rotation; + + g_return_val_if_fail (gravity != PANGO2_GRAVITY_AUTO, 0); + + switch (gravity) + { + default: + case PANGO2_GRAVITY_AUTO: /* shut gcc up */ + case PANGO2_GRAVITY_SOUTH: + rotation = 0; + break; + case PANGO2_GRAVITY_NORTH: + rotation = G_PI; + break; + case PANGO2_GRAVITY_EAST: + rotation = -G_PI_2; + break; + case PANGO2_GRAVITY_WEST: + rotation = +G_PI_2; + break; + } + + return rotation; +} + +/** + * pango2_gravity_get_for_matrix: + * @matrix: (nullable): a `Pango2Matrix` + * + * Finds the gravity that best matches the rotation component + * in a `Pango2Matrix`. + * + * Return value: the gravity of @matrix, which will never be + * %PANGO2_GRAVITY_AUTO, or %PANGO2_GRAVITY_SOUTH if @matrix is %NULL + */ +Pango2Gravity +pango2_gravity_get_for_matrix (const Pango2Matrix *matrix) +{ + Pango2Gravity gravity; + double x; + double y; + + if (!matrix) + return PANGO2_GRAVITY_SOUTH; + + x = matrix->xy; + y = matrix->yy; + + if (fabs (x) > fabs (y)) + gravity = x > 0 ? PANGO2_GRAVITY_WEST : PANGO2_GRAVITY_EAST; + else + gravity = y < 0 ? PANGO2_GRAVITY_NORTH : PANGO2_GRAVITY_SOUTH; + + return gravity; +} + + + +typedef enum +{ + PANGO2_VERTICAL_DIRECTION_NONE, + PANGO2_VERTICAL_DIRECTION_TTB, + PANGO2_VERTICAL_DIRECTION_BTT +} Pango2VerticalDirection; + +typedef struct { + /* Pango2Direction */ + guint8 horiz_dir; /* Orientation in horizontal context */ + + /* Pango2VerticalDirection */ + guint8 vert_dir; /* Orientation in vertical context */ + + /* Pango2Gravity */ + guint8 preferred_gravity; /* Preferred context gravity */ + + /* gboolean */ + guint8 wide; /* Whether script is mostly wide. + * Wide characters are upright (ie. + * not rotated) in foreign context */ +} Pango2ScriptProperties; + +#define NONE PANGO2_VERTICAL_DIRECTION_NONE +#define TTB PANGO2_VERTICAL_DIRECTION_TTB +#define BTT PANGO2_VERTICAL_DIRECTION_BTT + +#define LTR PANGO2_DIRECTION_LTR +#define RTL PANGO2_DIRECTION_RTL +#define WEAK PANGO2_DIRECTION_WEAK_LTR + +#define S PANGO2_GRAVITY_SOUTH +#define E PANGO2_GRAVITY_EAST +#define W PANGO2_GRAVITY_WEST + +const Pango2ScriptProperties script_properties[] = + { /* ISO 15924 code */ + {LTR, NONE, S, FALSE}, /* Zyyy */ + {LTR, NONE, S, FALSE}, /* Qaai */ + {RTL, NONE, S, FALSE}, /* Arab */ + {LTR, NONE, S, FALSE}, /* Armn */ + {LTR, NONE, S, FALSE}, /* Beng */ + {LTR, TTB, E, TRUE }, /* Bopo */ + {LTR, NONE, S, FALSE}, /* Cher */ + {LTR, NONE, S, FALSE}, /* Qaac */ + {LTR, NONE, S, FALSE}, /* Cyrl (Cyrs) */ + {LTR, NONE, S, FALSE}, /* Dsrt */ + {LTR, NONE, S, FALSE}, /* Deva */ + {LTR, NONE, S, FALSE}, /* Ethi */ + {LTR, NONE, S, FALSE}, /* Geor (Geon, Geoa) */ + {LTR, NONE, S, FALSE}, /* Goth */ + {LTR, NONE, S, FALSE}, /* Grek */ + {LTR, NONE, S, FALSE}, /* Gujr */ + {LTR, NONE, S, FALSE}, /* Guru */ + {LTR, TTB, E, TRUE }, /* Hani */ + {LTR, TTB, E, TRUE }, /* Hang */ + {RTL, NONE, S, FALSE}, /* Hebr */ + {LTR, TTB, E, TRUE }, /* Hira */ + {LTR, NONE, S, FALSE}, /* Knda */ + {LTR, TTB, E, TRUE }, /* Kana */ + {LTR, NONE, S, FALSE}, /* Khmr */ + {LTR, NONE, S, FALSE}, /* Laoo */ + {LTR, NONE, S, FALSE}, /* Latn (Latf, Latg) */ + {LTR, NONE, S, FALSE}, /* Mlym */ + {WEAK,TTB, W, FALSE}, /* Mong */ + {LTR, NONE, S, FALSE}, /* Mymr */ + {LTR, BTT, W, FALSE}, /* Ogam */ + {LTR, NONE, S, FALSE}, /* Ital */ + {LTR, NONE, S, FALSE}, /* Orya */ + {LTR, NONE, S, FALSE}, /* Runr */ + {LTR, NONE, S, FALSE}, /* Sinh */ + {RTL, NONE, S, FALSE}, /* Syrc (Syrj, Syrn, Syre) */ + {LTR, NONE, S, FALSE}, /* Taml */ + {LTR, NONE, S, FALSE}, /* Telu */ + {RTL, NONE, S, FALSE}, /* Thaa */ + {LTR, NONE, S, FALSE}, /* Thai */ + {LTR, NONE, S, FALSE}, /* Tibt */ + {LTR, NONE, S, FALSE}, /* Cans */ + {LTR, TTB, S, TRUE }, /* Yiii */ + {LTR, NONE, S, FALSE}, /* Tglg */ + {LTR, NONE, S, FALSE}, /* Hano */ + {LTR, NONE, S, FALSE}, /* Buhd */ + {LTR, NONE, S, FALSE}, /* Tagb */ + + /* Unicode-4.0 additions */ + {LTR, NONE, S, FALSE}, /* Brai */ + {RTL, NONE, S, FALSE}, /* Cprt */ + {LTR, NONE, S, FALSE}, /* Limb */ + {LTR, NONE, S, FALSE}, /* Osma */ + {LTR, NONE, S, FALSE}, /* Shaw */ + {LTR, NONE, S, FALSE}, /* Linb */ + {LTR, NONE, S, FALSE}, /* Tale */ + {LTR, NONE, S, FALSE}, /* Ugar */ + + /* Unicode-4.1 additions */ + {LTR, NONE, S, FALSE}, /* Talu */ + {LTR, NONE, S, FALSE}, /* Bugi */ + {LTR, NONE, S, FALSE}, /* Glag */ + {LTR, NONE, S, FALSE}, /* Tfng */ + {LTR, NONE, S, FALSE}, /* Sylo */ + {LTR, NONE, S, FALSE}, /* Xpeo */ + {RTL, NONE, S, FALSE}, /* Khar */ + + /* Unicode-5.0 additions */ + {LTR, NONE, S, FALSE}, /* Zzzz */ + {LTR, NONE, S, FALSE}, /* Bali */ + {LTR, NONE, S, FALSE}, /* Xsux */ + {RTL, NONE, S, FALSE}, /* Phnx */ + {LTR, NONE, S, FALSE}, /* Phag */ + {RTL, NONE, S, FALSE}, /* Nkoo */ + + /* Unicode-5.1 additions */ + {LTR, NONE, S, FALSE}, /* Kali */ + {LTR, NONE, S, FALSE}, /* Lepc */ + {LTR, NONE, S, FALSE}, /* Rjng */ + {LTR, NONE, S, FALSE}, /* Sund */ + {LTR, NONE, S, FALSE}, /* Saur */ + {LTR, NONE, S, FALSE}, /* Cham */ + {LTR, NONE, S, FALSE}, /* Olck */ + {LTR, NONE, S, FALSE}, /* Vaii */ + {LTR, NONE, S, FALSE}, /* Cari */ + {LTR, NONE, S, FALSE}, /* Lyci */ + {RTL, NONE, S, FALSE}, /* Lydi */ + + /* Unicode-5.2 additions */ + {RTL, NONE, S, FALSE}, /* Avst */ + {LTR, NONE, S, FALSE}, /* Bamu */ + {LTR, NONE, S, FALSE}, /* Egyp */ + {RTL, NONE, S, FALSE}, /* Armi */ + {RTL, NONE, S, FALSE}, /* Phli */ + {RTL, NONE, S, FALSE}, /* Prti */ + {LTR, NONE, S, FALSE}, /* Java */ + {LTR, NONE, S, FALSE}, /* Kthi */ + {LTR, NONE, S, FALSE}, /* Lisu */ + {LTR, NONE, S, FALSE}, /* Mtei */ + {RTL, NONE, S, FALSE}, /* Sarb */ + {RTL, NONE, S, FALSE}, /* Orkh */ + {RTL, TTB, S, FALSE}, /* Samr */ + {LTR, NONE, S, FALSE}, /* Lana */ + {LTR, NONE, S, FALSE}, /* Tavt */ + + /* Unicode-6.0 additions */ + {LTR, NONE, S, FALSE}, /* Batk */ + {LTR, NONE, S, FALSE}, /* Brah */ + {RTL, NONE, S, FALSE}, /* Mand */ + + /* Unicode-6.1 additions */ + {LTR, NONE, S, FALSE}, /* Cakm */ + {RTL, NONE, S, FALSE}, /* Merc */ + {RTL, NONE, S, FALSE}, /* Mero */ + {LTR, NONE, S, FALSE}, /* Plrd */ + {LTR, NONE, S, FALSE}, /* Shrd */ + {LTR, NONE, S, FALSE}, /* Sora */ + {LTR, NONE, S, FALSE}, /* Takr */ + + /* Unicode-7.0 additions */ + {LTR, NONE, S, FALSE}, /* Bass */ + {LTR, NONE, S, FALSE}, /* Aghb */ + {LTR, NONE, S, FALSE}, /* Dupl */ + {LTR, NONE, S, FALSE}, /* Elba */ + {LTR, NONE, S, FALSE}, /* Gran */ + {LTR, NONE, S, FALSE}, /* Khoj */ + {LTR, NONE, S, FALSE}, /* Sind */ + {LTR, NONE, S, FALSE}, /* Lina */ + {LTR, NONE, S, FALSE}, /* Mahj */ + {RTL, NONE, S, FALSE}, /* Mani */ + {RTL, NONE, S, FALSE}, /* Mend */ + {LTR, NONE, S, FALSE}, /* Modi */ + {LTR, NONE, S, FALSE}, /* Mroo */ + {RTL, NONE, S, FALSE}, /* Nbat */ + {RTL, NONE, S, FALSE}, /* Narb */ + {LTR, NONE, S, FALSE}, /* Perm */ + {LTR, NONE, S, FALSE}, /* Hmng */ + {RTL, NONE, S, FALSE}, /* Palm */ + {LTR, NONE, S, FALSE}, /* Pauc */ + {RTL, NONE, S, FALSE}, /* Phlp */ + {LTR, NONE, S, FALSE}, /* Sidd */ + {LTR, NONE, S, FALSE}, /* Tirh */ + {LTR, NONE, S, FALSE}, /* Wara */ + + /* Unicode-8.0 additions */ + {LTR, NONE, S, FALSE}, /* Ahom */ + {LTR, NONE, S, FALSE}, /* Hluw */ + {RTL, NONE, S, FALSE}, /* Hatr */ + {LTR, NONE, S, FALSE}, /* Mult */ + {LTR, NONE, S, FALSE}, /* Hung */ + {LTR, NONE, S, FALSE}, /* Sgnw */ + + /* Unicode-9.0 additions */ + {RTL, NONE, S, FALSE}, /* Adlm */ + {LTR, NONE, S, FALSE}, /* Bhks */ + {LTR, NONE, S, FALSE}, /* Marc */ + {LTR, NONE, S, FALSE}, /* Newa */ + {LTR, NONE, S, FALSE}, /* Osge */ + {LTR, NONE, S, FALSE}, /* Tang */ + + /* Unicode-10.0 additions */ + {LTR, NONE, S, FALSE}, /* Gonm */ + {LTR, NONE, S, FALSE}, /* Nshu */ + {LTR, NONE, S, FALSE}, /* Soyo */ + {LTR, NONE, S, FALSE}, /* Zanb */ + + /* Unicode-11.0 additions */ + {LTR, NONE, S, FALSE}, /* Dogr */ + {LTR, NONE, S, FALSE}, /* Gong */ + {RTL, NONE, S, FALSE}, /* Rohg */ + {LTR, NONE, S, FALSE}, /* Maka */ + {LTR, NONE, S, FALSE}, /* Medf */ + {RTL, NONE, S, FALSE}, /* Sogo */ + {RTL, NONE, S, FALSE}, /* Sogd */ + + /* Unicode-12.0 additions */ + {RTL, NONE, S, FALSE}, /* Elym */ + {LTR, NONE, S, FALSE}, /* Nand */ + {LTR, NONE, S, FALSE}, /* Rohg */ + {LTR, NONE, S, FALSE}, /* Wcho */ + + /* Unicode-13.0 additions */ + {RTL, NONE, S, FALSE}, /* Chrs */ + {LTR, NONE, S, FALSE}, /* Diak */ + {LTR, NONE, S, FALSE}, /* Kits */ + {RTL, NONE, S, FALSE}, /* Yezi */ + + {LTR, NONE, S, FALSE}, /* Cpmn */ + {RTL, NONE, S, FALSE}, /* Ougr */ + {LTR, NONE, S, FALSE}, /* Tnsa */ + {LTR, NONE, S, FALSE}, /* Toto */ + {LTR, NONE, S, FALSE}, /* Vith */ +}; + +#undef NONE +#undef TTB +#undef BTT + +#undef LTR +#undef RTL +#undef WEAK + +#undef S +#undef E +#undef N +#undef W + +static Pango2ScriptProperties +get_script_properties (GUnicodeScript script) +{ + g_return_val_if_fail (script >= 0, script_properties[0]); + + if ((guint)script >= G_N_ELEMENTS (script_properties)) + return script_properties[0]; + + return script_properties[script]; +} + +/** + * pango2_gravity_get_for_script: + * @script: `Pango2Script` to query + * @base_gravity: base gravity of the paragraph + * @hint: orientation hint + * + * Returns the gravity to use in laying out a `Pango2Item`. + * + * The gravity is determined based on the script, base gravity, and hint. + * + * If @base_gravity is %PANGO2_GRAVITY_AUTO, it is first replaced with the + * preferred gravity of @script. To get the preferred gravity of a script, + * pass %PANGO2_GRAVITY_AUTO and %PANGO2_GRAVITY_HINT_STRONG in. + * + * Return value: resolved gravity suitable to use for a run of text + * with @script + */ +Pango2Gravity +pango2_gravity_get_for_script (GUnicodeScript script, + Pango2Gravity base_gravity, + Pango2GravityHint hint) +{ + Pango2ScriptProperties props = get_script_properties (script); + + return pango2_gravity_get_for_script_and_width (script, props.wide, + base_gravity, hint); +} + +/** + * pango2_gravity_get_for_script_and_width: + * @script: `Pango2Script` to query + * @wide: %TRUE for wide characters as returned by g_unichar_iswide() + * @base_gravity: base gravity of the paragraph + * @hint: orientation hint + * + * Returns the gravity to use in laying out a single character + * or `Pango2Item`. + * + * The gravity is determined based on the script, East Asian width, + * base gravity, and hint, + * + * This function is similar to [func@Pango2.Gravity.get_for_script] except + * that this function makes a distinction between narrow/half-width and + * wide/full-width characters also. Wide/full-width characters always + * stand *upright*, that is, they always take the base gravity, + * whereas narrow/full-width characters are always rotated in vertical + * context. + * + * If @base_gravity is %PANGO2_GRAVITY_AUTO, it is first replaced with the + * preferred gravity of @script. + * + * Return value: resolved gravity suitable to use for a run of text + * with @script and @wide. + */ +Pango2Gravity +pango2_gravity_get_for_script_and_width (GUnicodeScript script, + gboolean wide, + Pango2Gravity base_gravity, + Pango2GravityHint hint) +{ + Pango2ScriptProperties props = get_script_properties (script); + gboolean vertical; + + if (G_UNLIKELY (base_gravity == PANGO2_GRAVITY_AUTO)) + base_gravity = props.preferred_gravity; + + vertical = PANGO2_GRAVITY_IS_VERTICAL (base_gravity); + + /* Everything is designed such that a system with no vertical support + * renders everything correctly horizontally. So, if not in a vertical + * gravity, base and resolved gravities are always the same. + * + * Wide characters are always upright. + */ + if (G_LIKELY (!vertical || wide)) + return base_gravity; + + /* If here, we have a narrow character in a vertical gravity setting. + * Resolve depending on the hint. + */ + switch (hint) + { + default: + case PANGO2_GRAVITY_HINT_NATURAL: + if (props.vert_dir == PANGO2_VERTICAL_DIRECTION_NONE) + return PANGO2_GRAVITY_SOUTH; + if ((base_gravity == PANGO2_GRAVITY_EAST) ^ + (props.vert_dir == PANGO2_VERTICAL_DIRECTION_BTT)) + return PANGO2_GRAVITY_SOUTH; + else + return PANGO2_GRAVITY_NORTH; + + case PANGO2_GRAVITY_HINT_STRONG: + return base_gravity; + + case PANGO2_GRAVITY_HINT_LINE: + if ((base_gravity == PANGO2_GRAVITY_EAST) ^ + (props.horiz_dir == PANGO2_DIRECTION_RTL)) + return PANGO2_GRAVITY_SOUTH; + else + return PANGO2_GRAVITY_NORTH; + } +} diff --git a/pango2/pango-gravity.h b/pango2/pango-gravity.h new file mode 100644 index 00000000..b3e79d3d --- /dev/null +++ b/pango2/pango-gravity.h @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2006, 2007 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <glib.h> + + +G_BEGIN_DECLS + +/** + * Pango2Gravity: + * @PANGO2_GRAVITY_SOUTH: Glyphs stand upright (default) <img align="right" valign="center" src="m-south.png"> + * @PANGO2_GRAVITY_EAST: Glyphs are rotated 90 degrees counter-clockwise. <img align="right" valign="center" src="m-east.png"> + * @PANGO2_GRAVITY_NORTH: Glyphs are upside-down. <img align="right" valign="cener" src="m-north.png"> + * @PANGO2_GRAVITY_WEST: Glyphs are rotated 90 degrees clockwise. <img align="right" valign="center" src="m-west.png"> + * @PANGO2_GRAVITY_AUTO: Gravity is resolved from the context matrix + * + * `Pango2Gravity` represents the orientation of glyphs in a segment + * of text. + * + * This is useful when rendering vertical text layouts. In those situations, + * the layout is rotated using a non-identity [struct@Pango2.Matrix], and then + * glyph orientation is controlled using `Pango2Gravity`. + * + * Not every value in this enumeration makes sense for every usage of + * `Pango2Gravity`; for example, %PANGO2_GRAVITY_AUTO only can be passed to + * [method@Pango2.Context.set_base_gravity] and can only be returned by + * [method@Pango2.Context.get_base_gravity]. + * + * See also: [enum@Pango2.GravityHint] + */ +typedef enum { + PANGO2_GRAVITY_SOUTH, + PANGO2_GRAVITY_EAST, + PANGO2_GRAVITY_NORTH, + PANGO2_GRAVITY_WEST, + PANGO2_GRAVITY_AUTO +} Pango2Gravity; + +/** + * Pango2GravityHint: + * @PANGO2_GRAVITY_HINT_NATURAL: scripts will take their natural gravity based + * on the base gravity and the script. This is the default. + * @PANGO2_GRAVITY_HINT_STRONG: always use the base gravity set, regardless of + * the script. + * @PANGO2_GRAVITY_HINT_LINE: for scripts not in their natural direction (eg. + * Latin in East gravity), choose per-script gravity such that every script + * respects the line progression. This means, Latin and Arabic will take + * opposite gravities and both flow top-to-bottom for example. + * + * `Pango2GravityHint` defines how horizontal scripts should behave in a + * vertical context. + * + * That is, English excerpts in a vertical paragraph for example. + * + * See also [enum@Pango2.Gravity] + */ +typedef enum { + PANGO2_GRAVITY_HINT_NATURAL, + PANGO2_GRAVITY_HINT_STRONG, + PANGO2_GRAVITY_HINT_LINE +} Pango2GravityHint; + +/** + * PANGO2_GRAVITY_IS_VERTICAL: + * @gravity: the `Pango2Gravity` to check + * + * Whether a `Pango2Gravity` represents vertical writing directions. + * + * Returns: %TRUE if @gravity is %PANGO2_GRAVITY_EAST or %PANGO2_GRAVITY_WEST, + * %FALSE otherwise. + */ +#define PANGO2_GRAVITY_IS_VERTICAL(gravity) \ + ((gravity) == PANGO2_GRAVITY_EAST || (gravity) == PANGO2_GRAVITY_WEST) + +/** + * PANGO2_GRAVITY_IS_IMPROPER: + * @gravity: the `Pango2Gravity` to check + * + * Whether a `Pango2Gravity` represents a gravity that results in reversal + * of text direction. + * + * Returns: %TRUE if @gravity is %PANGO2_GRAVITY_WEST or %PANGO2_GRAVITY_NORTH, + * %FALSE otherwise. + */ +#define PANGO2_GRAVITY_IS_IMPROPER(gravity) \ + ((gravity) == PANGO2_GRAVITY_WEST || (gravity) == PANGO2_GRAVITY_NORTH) + +#include <pango2/pango-matrix.h> +#include <pango2/pango-script.h> + +PANGO2_AVAILABLE_IN_ALL +double pango2_gravity_to_rotation (Pango2Gravity gravity) G_GNUC_CONST; +PANGO2_AVAILABLE_IN_ALL +Pango2Gravity pango2_gravity_get_for_matrix (const Pango2Matrix *matrix) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +Pango2Gravity pango2_gravity_get_for_script (GUnicodeScript script, + Pango2Gravity base_gravity, + Pango2GravityHint hint) G_GNUC_CONST; +PANGO2_AVAILABLE_IN_ALL +Pango2Gravity pango2_gravity_get_for_script_and_width + (GUnicodeScript script, + gboolean wide, + Pango2Gravity base_gravity, + Pango2GravityHint hint) G_GNUC_CONST; + + +G_END_DECLS diff --git a/pango2/pango-hbface-private.h b/pango2/pango-hbface-private.h new file mode 100644 index 00000000..e2920388 --- /dev/null +++ b/pango2/pango-hbface-private.h @@ -0,0 +1,52 @@ +/* + * Copyright 2021 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-hbface.h" +#include "pango-font-face-private.h" +#include "pango-fontmap.h" +#include "pango-language-set-private.h" +#include <hb.h> + +struct _Pango2HbFace +{ + Pango2FontFace parent_instance; + + char *faceid; + unsigned int index; + int instance_id; + char *file; + hb_face_t *face; + hb_variation_t *variations; + unsigned int n_variations; + Pango2Matrix *transform; + double x_scale, y_scale; + Pango2LanguageSet *languages; + gboolean embolden; + gboolean synthetic; +}; + +Pango2LanguageSet * pango2_hb_face_get_language_set (Pango2HbFace *self); + +void pango2_hb_face_set_language_set (Pango2HbFace *self, + Pango2LanguageSet *languages); + +void pango2_hb_face_set_matrix (Pango2HbFace *self, + const Pango2Matrix *matrix); diff --git a/pango2/pango-hbface.c b/pango2/pango-hbface.c new file mode 100644 index 00000000..f549b41a --- /dev/null +++ b/pango2/pango-hbface.c @@ -0,0 +1,1087 @@ +/* Pango2 + * + * Copyright (C) 2021 Matthias Clasen + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include "pango-font-private.h" +#include "pango-hbface-private.h" +#include "pango-hbfont.h" + +#include "pango-language-set-simple-private.h" + +#include <string.h> +#include <hb-ot.h> +#include <hb-gobject.h> + +/** + * Pango2HbFace: + * + * `Pango2HbFace` is a `Pango2FontFace` implementation that wraps + * a `hb_face_t` object and implements all of the `Pango2FontFace` + * functionality using HarfBuzz. + * + * In addition to making a `hb_face_t` available for rendering + * glyphs with Pango2, `Pango2HbFace` allows some tweaks to the + * rendering, such as artificial slant (using a transformation + * matrix) or artificial emboldening. + * + * To get a font instance at a specific size from a `Pango2HbFace`, + * use [ctor@Pango2.HbFont.new]. + */ + + /* {{{ Utilities */ + +static void +get_name_from_hb_face (hb_face_t *face, + hb_ot_name_id_t name_id, + hb_ot_name_id_t fallback_id, + char *buf, + unsigned int len) +{ + unsigned int size = len; + + if (hb_ot_name_get_utf8 (face, name_id, HB_LANGUAGE_INVALID, &size, buf) > 0) + return; + + if (fallback_id != HB_OT_NAME_ID_INVALID) + { + size = len; + + if (hb_ot_name_get_utf8 (face, fallback_id, HB_LANGUAGE_INVALID, &size, buf) > 0) + return; + } + + strncpy (buf, "Unnamed", len); + buf[len - 1] = '\0'; +} + +static void +ensure_hb_face (Pango2HbFace *self) +{ + hb_blob_t *blob; + + if (self->face) + return; + + blob = hb_blob_create_from_file (self->file); + + if (blob == hb_blob_get_empty ()) + g_warning ("Failed to load %s", self->file); + + if (self->index >= hb_face_count (blob)) + g_warning ("Face index %d out of range for %s", self->index, self->file); + + self->face = hb_face_create (blob, self->index); + + if (self->instance_id >= (int)hb_ot_var_get_named_instance_count (self->face)) + g_warning ("Instance ID %d out of range for %s", self->instance_id, self->file); + + hb_blob_destroy (blob); + hb_face_make_immutable (self->face); +} + +static char * +variations_to_string (const hb_variation_t *variations, + unsigned int n_variations, + const char *equals, + const char *separator) +{ + GString *s; + char buf[128] = { 0, }; + + s = g_string_new (""); + + for (unsigned int i = 0; i < n_variations; i++) + { + if (s->len > 0) + g_string_append (s, separator); + + hb_tag_to_string (variations[i].tag, buf); + g_string_append (s, buf); + g_string_append (s, equals); + g_ascii_formatd (buf, sizeof (buf), "%g", variations[i].value); + g_string_append (s, buf); + } + + return g_string_free (s, FALSE); +} + +static void +set_name_and_description (Pango2HbFace *self, + const char *name, + const Pango2FontDescription *description) +{ + Pango2FontFace *face = PANGO2_FONT_FACE (self); + + if (name) + { + pango2_font_face_set_name (face, name); + } + else + { + hb_ot_name_id_t name_id; + char face_name[256] = { 0, }; + + ensure_hb_face (self); + + if (self->instance_id >= 0) + name_id = hb_ot_var_named_instance_get_subfamily_name_id (self->face, self->instance_id); + else + name_id = HB_OT_NAME_ID_TYPOGRAPHIC_SUBFAMILY; + + get_name_from_hb_face (self->face, + name_id, + HB_OT_NAME_ID_FONT_SUBFAMILY, + face_name, sizeof (face_name)); + + pango2_font_face_set_name (face, face_name); + } + + if (description) + { + face->description = pango2_font_description_copy (description); + } + else + { + char family[256] = { 0, }; + char *fullname; + + ensure_hb_face (self); + + get_name_from_hb_face (self->face, + HB_OT_NAME_ID_TYPOGRAPHIC_FAMILY, + HB_OT_NAME_ID_FONT_FAMILY, + family, sizeof (family)); + fullname = g_strconcat (family, " ", face->name, NULL); + + /* This is an attempt to parse style/weight/variant information + * out of the face name. FIXME: we should look at variation + * coordinates too, here, instead of these guessing games. + */ + face->description = pango2_font_description_from_string (fullname); + pango2_font_description_unset_fields (face->description, + PANGO2_FONT_MASK_VARIATIONS | + PANGO2_FONT_MASK_GRAVITY); + + /* Make sure we don't leave any leftovers misinterpreted + * as part of the family name. + */ + pango2_font_description_set_family (face->description, family); + + g_free (fullname); + } + + if (self->n_variations > 0) + { + char *str = variations_to_string (self->variations, self->n_variations, "=", ","); + pango2_font_description_set_variations (face->description, str); + g_free (str); + } +} + +typedef struct { + guint16 major; + guint16 minor; + gint32 italicAngle; + gint16 underlinePosition; + gint16 underlineThickness; + guint8 isFixedPitch[4]; +} PostTable; + +static gboolean +hb_face_is_monospace (hb_face_t *face) +{ + hb_blob_t *post_blob; + guint8 *raw_post; + unsigned int post_len; + gboolean res = FALSE; + + post_blob = hb_face_reference_table (face, HB_TAG ('p', 'o', 's', 't')); + raw_post = (guint8 *) hb_blob_get_data (post_blob, &post_len); + + if (post_len >= sizeof (PostTable)) + { + PostTable *post = (PostTable *)raw_post; + + res = post->isFixedPitch[0] != 0 || + post->isFixedPitch[1] != 0 || + post->isFixedPitch[2] != 0 || + post->isFixedPitch[3] != 0; + } + + hb_blob_destroy (post_blob); + + return res; +} + +static void +ensure_faceid (Pango2HbFace *self) +{ + double slant; + char buf0[32], buf1[32], buf2[32]; + char *str = NULL; + char psname[256] = { 0, }; + char *p; + + if (self->faceid) + return; + + ensure_hb_face (self); + + get_name_from_hb_face (self->face, + HB_OT_NAME_ID_POSTSCRIPT_NAME, + HB_OT_NAME_ID_INVALID, + psname, sizeof (psname)); + + /* PostScript name should not contain problematic chars, but just in case, + * make sure we don't have any ' ', '=' or ',' that would give us parsing + * problems. + */ + p = psname; + while ((p = strpbrk (p, " =,")) != NULL) + *p = '?'; + + if (self->transform) + slant = pango2_matrix_get_slant_ratio (self->transform); + else + slant = 0.; + + if (self->n_variations > 0) + str = variations_to_string (self->variations, self->n_variations, "_", ":"); + + self->faceid = g_strdup_printf ("hb:%s:%u:%d:%d:%s:%s:%s%s%s", + psname, + self->index, + self->instance_id, + self->embolden, + g_ascii_formatd (buf0, sizeof (buf0), "%g", self->x_scale), + g_ascii_formatd (buf1, sizeof (buf1), "%g", self->y_scale), + g_ascii_formatd (buf2, sizeof (buf2), "%g", slant), + self->n_variations > 0 ? ":" : "", + self->n_variations > 0 ? str : ""); + g_free (str); +} + +static const char * +style_from_font_description (const Pango2FontDescription *desc) +{ + Pango2Style style = pango2_font_description_get_style (desc); + Pango2Weight weight = pango2_font_description_get_weight (desc); + + switch (style) + { + case PANGO2_STYLE_ITALIC: + if (weight == PANGO2_WEIGHT_BOLD) + return "Bold Italic"; + else + return "Italic"; + break; + case PANGO2_STYLE_OBLIQUE: + if (weight == PANGO2_WEIGHT_BOLD) + return "Bold Oblique"; + else + return "Oblique"; + break; + case PANGO2_STYLE_NORMAL: + if (weight == PANGO2_WEIGHT_BOLD) + return "Bold"; + else + return "Regular"; + break; + default: ; + } + + return NULL; +} + +/* }}} */ +/* {{{ Pango2FontFace implementation */ + +struct _Pango2HbFaceClass +{ + Pango2FontFaceClass parent_class; +}; + +enum { + PROP_HB_FACE = 1, + PROP_FILE, + PROP_FACE_INDEX, + PROP_INSTANCE_ID, + PROP_VARIATIONS, + PROP_EMBOLDEN, + PROP_TRANSFORM, + N_PROPERTIES +}; + +static GParamSpec *properties[N_PROPERTIES] = { NULL, }; + +G_DEFINE_FINAL_TYPE (Pango2HbFace, pango2_hb_face, PANGO2_TYPE_FONT_FACE) + +static void +pango2_hb_face_init (Pango2HbFace *self) +{ + self->transform = NULL; + self->x_scale = self->y_scale = 1.; +} + +static void +pango2_hb_face_finalize (GObject *object) +{ + Pango2HbFace *self = PANGO2_HB_FACE (object); + + g_free (self->faceid); + if (self->face) + hb_face_destroy (self->face); + g_free (self->file); + if (self->languages) + g_object_unref (self->languages); + g_free (self->variations); + if (self->transform) + g_free (self->transform); + + G_OBJECT_CLASS (pango2_hb_face_parent_class)->finalize (object); +} + +static gboolean +pango2_hb_face_is_synthesized (Pango2FontFace *face) +{ + Pango2HbFace *self = PANGO2_HB_FACE (face); + + return self->synthetic; +} + +static gboolean +pango2_hb_face_is_monospace (Pango2FontFace *face) +{ + Pango2HbFace *self = PANGO2_HB_FACE (face); + + ensure_hb_face (self); + + return hb_face_is_monospace (self->face); +} + +static gboolean +pango2_hb_face_is_variable (Pango2FontFace *face) +{ + Pango2HbFace *self = PANGO2_HB_FACE (face); + + /* We don't consider named instances as variable, i.e. + * a font chooser UI should not expose axes for them. + * + * In theory, there could be multi-axis fonts where the + * variations only pin some of the axes, but we are not + * going to worry about possibility here. + */ + if (self->instance_id >= -1 || self->n_variations) + return FALSE; + + ensure_hb_face (self); + + return hb_ot_var_get_axis_count (self->face) > 0; +} + +static gboolean +pango2_hb_face_supports_language (Pango2FontFace *face, + Pango2Language *language) +{ + Pango2HbFace *self = PANGO2_HB_FACE (face); + Pango2LanguageSet *set = pango2_hb_face_get_language_set (self); + + if (set) + return pango2_language_set_matches_language (set, language); + + return TRUE; +} + +static Pango2Language ** +pango2_hb_face_get_languages (Pango2FontFace *face) +{ + Pango2HbFace *self = PANGO2_HB_FACE (face); + Pango2LanguageSet *set = pango2_hb_face_get_language_set (self); + + if (set) + return pango2_language_set_get_languages (set); + + return NULL; +} + +static gboolean +pango2_hb_face_has_char (Pango2FontFace *face, + gunichar wc) +{ + Pango2HbFace *self = PANGO2_HB_FACE (face); + hb_font_t *hb_font; + hb_codepoint_t glyph; + gboolean ret; + + ensure_hb_face (self); + + hb_font = hb_font_create (self->face); + ret = hb_font_get_nominal_glyph (hb_font, wc, &glyph); + hb_font_destroy (hb_font); + + return ret; +} + +static const char * +pango2_hb_face_get_faceid (Pango2FontFace *face) +{ + Pango2HbFace *self = PANGO2_HB_FACE (face); + + ensure_faceid (self); + + return self->faceid; +} + +static Pango2Font * +pango2_hb_face_create_font (Pango2FontFace *face, + const Pango2FontDescription *desc, + float dpi, + const Pango2Matrix *ctm) +{ + Pango2HbFace *self = PANGO2_HB_FACE (face); + + return PANGO2_FONT (pango2_hb_font_new_for_description (self, desc, dpi, ctm)); +} + +static void +pango2_hb_face_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + Pango2HbFace *self = PANGO2_HB_FACE (object); + + switch (property_id) + { + case PROP_HB_FACE: + g_value_set_boxed (value, pango2_hb_face_get_hb_face (self)); + break; + + case PROP_FILE: + g_value_set_string (value, pango2_hb_face_get_file (self)); + break; + + case PROP_FACE_INDEX: + g_value_set_uint (value, pango2_hb_face_get_face_index (self)); + break; + + case PROP_INSTANCE_ID: + g_value_set_int (value, pango2_hb_face_get_instance_id (self)); + break; + + case PROP_VARIATIONS: + { + char *str = NULL; + if (self->variations) + str = variations_to_string (self->variations, self->n_variations, "=", ","); + g_value_take_string (value, str); + } + break; + + case PROP_EMBOLDEN: + g_value_set_boolean (value, pango2_hb_face_get_embolden (self)); + break; + + case PROP_TRANSFORM: + g_value_set_boxed (value, pango2_hb_face_get_transform (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +pango2_hb_face_class_init (Pango2HbFaceClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + Pango2FontFaceClass *face_class = PANGO2_FONT_FACE_CLASS (class); + + object_class->finalize = pango2_hb_face_finalize; + object_class->get_property = pango2_hb_face_get_property; + + face_class->is_synthesized = pango2_hb_face_is_synthesized; + face_class->is_monospace = pango2_hb_face_is_monospace; + face_class->is_variable = pango2_hb_face_is_variable; + face_class->supports_language = pango2_hb_face_supports_language; + face_class->get_languages = pango2_hb_face_get_languages; + face_class->has_char = pango2_hb_face_has_char; + face_class->get_faceid = pango2_hb_face_get_faceid; + face_class->create_font = pango2_hb_face_create_font; + + /** + * Pango2HbFace:hb-face: (attributes org.gtk.Property.get=pango2_hb_face_get_hb_face) + * + * A `hb_face_t` object backing this face. + */ + properties[PROP_HB_FACE] = + g_param_spec_boxed ("hb-face", NULL, NULL, HB_GOBJECT_TYPE_FACE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2HbFace:file: (attributes org.gtk.Property.get=pango2_hb_face_get_file) + * + * The file that this face was created from, if any. + */ + properties[PROP_FILE] = + g_param_spec_string ("file", NULL, NULL, NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2HbFace:face-index: (attributes org.gtk.Property.get=pango2_hb_face_get_face_index) + * + * The index of the face, in case that it was created + * from a file containing data for multiple faces. + */ + properties[PROP_FACE_INDEX] = + g_param_spec_uint ("face-index", NULL, NULL, 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2HbFace:instance-id: (attributes org.gtk.Property.get=pango2_hb_face_get_instance_id) + * + * The ID of the named instance of this face. + * + * -1 represents the default instance, and + * -2 represents no instance, meaning that the face will + * be variable. + */ + properties[PROP_INSTANCE_ID] = + g_param_spec_int ("instance-id", NULL, NULL, -2, G_MAXINT, -1, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2HbFace:variations: (attributes org.gtk.Property.get=pango2_hb_face_get_variations) + * + * The variations that are applied for this face. + * + * This property contains a string representation of the variations. + */ + properties[PROP_VARIATIONS] = + g_param_spec_string ("variations", NULL, NULL, NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2HbFace:embolden: (attributes org.gtk.Property.get=pango2_hb_face_get_embolden) + * + * `TRUE` if the face is using synthetic emboldening. + */ + properties[PROP_EMBOLDEN] = + g_param_spec_boolean ("embolden", NULL, NULL, FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2HbFace:transform: (attributes org.gtk.Property.get=pango2_hb_face_get_transform) + * + * The transform from 'font space' to 'user space' that + * this face uses. + * + * This is the 'font matrix' which is used for + * sythetic italics and width variations. + */ + properties[PROP_TRANSFORM] = + g_param_spec_boxed ("transform", NULL, NULL, PANGO2_TYPE_MATRIX, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPERTIES, properties); +} + +/* }}} */ +/* {{{ Private API */ + +/*< private > + * pango2_hb_face_get_language_set: + * @face: a `Pango2HbFace` + * + * Returns the languages supported by @face. + * + * Returns: (transfer none): a `Pango2LanguageSet` + */ +Pango2LanguageSet * +pango2_hb_face_get_language_set (Pango2HbFace *face) +{ + return face->languages; +} + +/*< private > + * pango2_hb_face_set_language_set: + * @self: a `Pango2HbFace` + * @languages: a `Pango2LanguageSet` + * + * Sets the languages that are supported by @face. + * + * This should only be called by fontmap implementations. + */ +void +pango2_hb_face_set_language_set (Pango2HbFace *self, + Pango2LanguageSet *languages) +{ + g_set_object (&self->languages, languages); +} + +/*< private > + * pango2_hb_face_set_matrix: + * @self: a `Pango2HbFace` + * @matrix: the `Pango2Matrix` + * + * Sets the font matrix for @self. + * + * This should only be called by fontmap implementations. + */ +void +pango2_hb_face_set_matrix (Pango2HbFace *self, + const Pango2Matrix *matrix) +{ + if (!self->transform) + self->transform = g_new (Pango2Matrix, 1); + + *self->transform = *matrix; + + pango2_matrix_get_font_scale_factors (self->transform, &self->x_scale, &self->y_scale); + pango2_matrix_scale (self->transform, 1./self->x_scale, 1./self->y_scale); +} + +/* }}} */ + /* {{{ Public API */ + +/** + * pango2_hb_face_new_from_hb_face: + * @face: an immutable `hb_face_t` + * @instance_id: named instance id, or -1 for the default instance + * or -2 for no instance + * @name: (nullable): name for the face + * @description: (nullable): `Pango2FontDescription` for the font + * + * Creates a new `Pango2HbFace` by wrapping an existing `hb_face_t`. + * + * The @instance_id can be used to pick one of the available named + * instances in a variable font. See hb_ot_var_get_named_instance_count() + * to learn about the available named instances. + * + * If @instance_id is -2 and @face has variation axes, then + * [method@Pango2.FontFace.is_variable] will return `TRUE` for + * the returned `Pango2HbFace`. + * + * If @name is provided, it is used as the name for the face. + * Otherwise, Pango2 will use the named instance subfamily name + * or `HB_OT_NAME_ID_TYPOGRAPHIC_SUBFAMILY`. + * + * If @description is provided, it is used as the font description + * for the face. Otherwise, Pango2 creates a description using + * `HB_OT_NAME_ID_TYPOGRAPHIC_FAMILY` and the name of the face. + * + * Returns: a newly created `Pango2HbFace` + */ +Pango2HbFace * +pango2_hb_face_new_from_hb_face (hb_face_t *face, + int instance_id, + const char *name, + const Pango2FontDescription *description) +{ + Pango2HbFace *self; + + g_return_val_if_fail (face != NULL, NULL); + g_return_val_if_fail (hb_face_is_immutable (face), NULL); + g_return_val_if_fail (instance_id >= -2, NULL); + g_return_val_if_fail (description == NULL || + (pango2_font_description_get_set_fields (description) & + (PANGO2_FONT_MASK_SIZE|PANGO2_FONT_MASK_GRAVITY)) == 0, NULL); + + self = g_object_new (PANGO2_TYPE_HB_FACE, NULL); + + self->face = hb_face_reference (face); + self->index = hb_face_get_index (face) & 0xffff; + self->instance_id = instance_id; + + if (instance_id >= (int)hb_ot_var_get_named_instance_count (face)) + g_warning ("Instance ID %d out of range", instance_id); + + set_name_and_description (self, name, description); + + return self; +} + +/** + * pango2_hb_face_new_from_file: + * @file: font filename + * @index: face index + * @instance_id: named instance id, or -1 for the default instance + * or -2 for no instance + * @name: (nullable): name for the face + * @description: (nullable): `Pango2FontDescription` for the font + * + * Creates a new `Pango2HbFace` from a font file. + * + * The @index can be used to pick a face from a file containing + * multiple faces, such as TTC or DFont. + * + * The @instance_id can be used to pick one of the available named + * instances in a variable font. See hb_ot_var_get_named_instance_count() + * to learn about the available named instances. + * + * If @instance_id is -2 and @face has variation axes, then + * [method@Pango2.FontFace.is_variable] will return `TRUE` for + * the returned `Pango2HbFace`. + * + * If @name is provided, it is used as the name for the face. + * Otherwise, Pango2 will use the named instance subfamily name + * or `HB_OT_NAME_ID_TYPOGRAPHIC_SUBFAMILY`. + * + * If @description is provided, it is used as the font description + * for the face. Otherwise, Pango2 creates a description using + * `HB_OT_NAME_ID_TYPOGRAPHIC_FAMILY` and the name of the face. + * + * If @desc and @name are provided, then the returned `Pango2HbFace` + * object will be lazily initialized as needed. + * + * Returns: a newly created `Pango2HbFace` + */ +Pango2HbFace * +pango2_hb_face_new_from_file (const char *file, + unsigned int index, + int instance_id, + const char *name, + const Pango2FontDescription *description) +{ + Pango2HbFace *self; + + g_return_val_if_fail (file!= NULL, NULL); + g_return_val_if_fail (instance_id >= -2, NULL); + g_return_val_if_fail (description == NULL || + (pango2_font_description_get_set_fields (description) & + (PANGO2_FONT_MASK_VARIANT| + PANGO2_FONT_MASK_SIZE| + PANGO2_FONT_MASK_GRAVITY)) == 0, NULL); + + self = g_object_new (PANGO2_TYPE_HB_FACE, NULL); + + self->file = g_strdup (file); + self->index = index; + self->instance_id = instance_id; + + set_name_and_description (self, name, description); + + return self; +} + +/** + * pango2_hb_face_new_synthetic: + * @face: a `Pango2HbFace` + * @transform: (nullable): the transform to apply + * @embolden: `TRUE` to render the font bolder + * @name: (nullable): name for the face + * @description: a `Pango2FontDescription` to override fields from @face's description + * + * Creates a new `Pango2HbFace` that is a synthetic variant of @face. + * + * Here, 'synthetic' means that the variant is implemented by rendering + * the glyphs differently, not by using data from the original @face. + * See [method@Pango2.HbFace.new_instance] for that. + * + * @transform can be used to specify a non-trivial font matrix for creating + * synthetic italics or synthetic condensed variants of an existing face. + * + * If @embolden is `TRUE`, Pango2 will render the glyphs bolder, creating + * a synthetic bold variant of the face. + * + * If a @name is not specified, the name for the face will be derived + * from the @description. + * + * Apart from setting the style that this face will be used for, + * @description can provide an alternative family name. This can + * be used to create generic aliases such as 'sans' or 'monospace'. + * + * Note that only the following fields in @description should be set: + * + * + style or stretch, to indicate a transformed style + * + weight, to indicate a bolder weight + * + family, to provide an alternative family name + * + * [method@Pango2.FontFace.is_synthesized] will return `TRUE` for objects + * created by this function. + * + * Returns: (transfer full): a newly created `Pango2HbFace` + */ +Pango2HbFace * +pango2_hb_face_new_synthetic (Pango2HbFace *face, + const Pango2Matrix *transform, + gboolean embolden, + const char *name, + const Pango2FontDescription *description) +{ + Pango2HbFace *self; + Pango2FontDescription *desc; + + g_return_val_if_fail (PANGO2_IS_HB_FACE (face), NULL); + g_return_val_if_fail (description != NULL, NULL); + g_return_val_if_fail ((pango2_font_description_get_set_fields (description) & + ~(PANGO2_FONT_MASK_FAMILY| + PANGO2_FONT_MASK_STYLE| + PANGO2_FONT_MASK_STRETCH| + PANGO2_FONT_MASK_WEIGHT)) == 0, NULL); + + self = g_object_new (PANGO2_TYPE_HB_FACE, NULL); + + self->file = g_strdup (face->file); + if (face->face) + self->face = hb_face_reference (face->face); + + self->index = face->index; + self->instance_id = face->instance_id; + self->variations = g_memdup2 (face->variations, sizeof (hb_variation_t) * face->n_variations); + self->n_variations = face->n_variations; + + if (transform) + pango2_hb_face_set_matrix (self, transform); + + self->embolden = embolden; + self->synthetic = self->embolden || (self->transform != NULL); + + desc = pango2_font_description_copy (PANGO2_FONT_FACE (face)->description); + pango2_font_description_merge (desc, description, TRUE); + + if (!name) + name = style_from_font_description (desc); + + set_name_and_description (self, name, desc); + + pango2_hb_face_set_language_set (self, face->languages); + + pango2_font_description_free (desc); + + return self; +} + +/** + * pango2_hb_face_new_instance: + * @face: a `Pango2HbFace` + * @variations: (nullable) (array length=n_variations): font variations to apply + * @n_variations: length of @variations + * @name: (nullable): name for the face + * @description: a `Pango2FontDescription` to override fields from @face's description + * + * Creates a new `Pango2HbFace` that is a variant of @face. + * + * The @variations provide values for variation axes of @face. Axes that + * are not included in @variations will keep the values they have in @face. + * @variations that refer to axes that the face does not have are ignored. + * + * Conceptually, this is similar to a named instance of the face, except + * that the mapping of the name to a set of coordinates on the variation + * axes is provided externally, and not by the face itself. + * + * If a @name is not specified, the name for the face will be derived + * from the @description. + * + * Apart from setting the style that this face will be used for, + * @description can provide an alternative family name. This can + * be used to create generic aliases such as 'sans' or 'monospace'. + * + * Note that only the following fields in @description should be set: + * - style or stretch, to indicate a transformed style + * - weight, to indicate a bolder weight + * - family, to provide an alternative family name + * + * Returns: (transfer full): a newly created `Pango2HbFace` + */ +Pango2HbFace * +pango2_hb_face_new_instance (Pango2HbFace *face, + const hb_variation_t *variations, + unsigned int n_variations, + const char *name, + const Pango2FontDescription *description) +{ + Pango2HbFace *self; + Pango2FontDescription *desc; + + g_return_val_if_fail (PANGO2_IS_HB_FACE (face), NULL); + g_return_val_if_fail (description != NULL, NULL); + g_return_val_if_fail ((pango2_font_description_get_set_fields (description) & + ~(PANGO2_FONT_MASK_FAMILY| + PANGO2_FONT_MASK_STYLE| + PANGO2_FONT_MASK_STRETCH| + PANGO2_FONT_MASK_WEIGHT)) == 0, NULL); + + self = g_object_new (PANGO2_TYPE_HB_FACE, NULL); + + self->file = g_strdup (face->file); + if (face->face) + self->face = hb_face_reference (face->face); + + self->index = face->index; + self->instance_id = face->instance_id; + + if (face->transform) + { + self->transform = g_memdup2 (face->transform, sizeof (Pango2Matrix)); + self->x_scale = face->x_scale; + self->y_scale = face->y_scale; + } + + self->embolden = face->embolden; + self->synthetic = self->embolden || (self->transform != NULL); + + self->variations = g_memdup2 (variations, sizeof (hb_variation_t) * n_variations); + self->n_variations = n_variations; + + desc = pango2_font_description_copy (PANGO2_FONT_FACE (face)->description); + pango2_font_description_merge (desc, description, TRUE); + + if (!name) + name = style_from_font_description (desc); + + set_name_and_description (self, name, desc); + + pango2_font_description_free (desc); + + return self; +} + +/** + * pango2_hb_face_get_hb_face: + * @self: a `Pango2HbFace` + * + * Gets the `hb_face_t` object backing this face. + * + * Note that the objects returned by this function are cached + * and immutable, and may be shared between `Pango2HbFace` objects. + * + * Returns: (transfer none): the `hb_face_t` object + * backing the face + */ +hb_face_t * +pango2_hb_face_get_hb_face (Pango2HbFace *self) +{ + g_return_val_if_fail (PANGO2_IS_HB_FACE (self), NULL); + + ensure_hb_face (self); + + return self->face; +} + +/** + * pango2_hb_face_get_file: + * @self: a `Pango2HbFace` + * + * Gets the file that backs the face. + * + * Returns: (transfer none) (nullable): the file backing the face + */ +const char * +pango2_hb_face_get_file (Pango2HbFace *self) +{ + g_return_val_if_fail (PANGO2_IS_HB_FACE (self), NULL); + + return self->file; +} + +/** + * pango2_hb_face_get_face_index: + * @self: a `Pango2HbFace` + * + * Gets the face index of the face. + * + * Returns: the face indexx + */ +unsigned int +pango2_hb_face_get_face_index (Pango2HbFace *self) +{ + g_return_val_if_fail (PANGO2_IS_HB_FACE (self), 0); + + return self->index; +} + +/** + * pango2_hb_face_get_instance_id: + * @self: a `Pango2HbFace` + * + * Gets the instance id of the face. + * + * Returns: the instance id + */ +int +pango2_hb_face_get_instance_id (Pango2HbFace *self) +{ + g_return_val_if_fail (PANGO2_IS_HB_FACE (self), -1); + + return self->instance_id; +} + +/** + * pango2_hb_face_get_variations: + * @self: a `Pango2HbFace` + * @n_variations: (nullable) (out caller-allocates): return location for + * the length of the returned array + * + * Gets the variations of the face. + * + * Returns: (nullable) (transfer none): the variations + */ +const hb_variation_t * +pango2_hb_face_get_variations (Pango2HbFace *self, + unsigned int *n_variations) +{ + g_return_val_if_fail (PANGO2_IS_HB_FACE (self), NULL); + + if (n_variations) + *n_variations = self->n_variations; + + return self->variations; +} + +/** + * pango2_hb_face_get_embolden: + * @self: a `Pango2HbFace` + * + * Gets whether face is using synthetic emboldening. + * + * Returns: `TRUE` if the face is using synthetic embolding + */ +gboolean +pango2_hb_face_get_embolden (Pango2HbFace *self) +{ + g_return_val_if_fail (PANGO2_IS_HB_FACE (self), FALSE); + + return self->embolden; +} + +/** + * pango2_hb_face_get_transform: + * @self: a `Pango2HbFace` + * + * Gets the transform from 'font space' to 'user space' that this face uses. + * + * This is the 'font matrix' which is used for + * sythetic italics and width variations. + * + * Returns: (nullable) (transfer none): the transform of face + */ +const Pango2Matrix * +pango2_hb_face_get_transform (Pango2HbFace *self) +{ + g_return_val_if_fail (PANGO2_IS_HB_FACE (self), NULL); + + return self->transform; +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pango-hbface.h b/pango2/pango-hbface.h new file mode 100644 index 00000000..2d4662d7 --- /dev/null +++ b/pango2/pango-hbface.h @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2021 Matthias Clasen + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-types.h> +#include <pango2/pango-font-face.h> + +#include <hb.h> + +G_BEGIN_DECLS + +#define PANGO2_TYPE_HB_FACE (pango2_hb_face_get_type ()) + +PANGO2_AVAILABLE_IN_ALL +PANGO2_DECLARE_INTERNAL_TYPE (Pango2HbFace, pango2_hb_face, PANGO2, HB_FACE, Pango2FontFace) + +PANGO2_AVAILABLE_IN_ALL +Pango2HbFace * pango2_hb_face_new_from_hb_face (hb_face_t *face, + int instance_id, + const char *name, + const Pango2FontDescription *description); + +PANGO2_AVAILABLE_IN_ALL +Pango2HbFace * pango2_hb_face_new_from_file (const char *file, + unsigned int index, + int instance_id, + const char *name, + const Pango2FontDescription *description); + +PANGO2_AVAILABLE_IN_ALL +Pango2HbFace * pango2_hb_face_new_synthetic (Pango2HbFace *face, + const Pango2Matrix *transform, + gboolean embolden, + const char *name, + const Pango2FontDescription *description); + +PANGO2_AVAILABLE_IN_ALL +Pango2HbFace * pango2_hb_face_new_instance (Pango2HbFace *face, + const hb_variation_t *variations, + unsigned int n_variations, + const char *name, + const Pango2FontDescription *description); + +PANGO2_AVAILABLE_IN_ALL +hb_face_t * pango2_hb_face_get_hb_face (Pango2HbFace *self); + +PANGO2_AVAILABLE_IN_ALL +const char * pango2_hb_face_get_file (Pango2HbFace *self); + +PANGO2_AVAILABLE_IN_ALL +unsigned int pango2_hb_face_get_face_index (Pango2HbFace *self); + +PANGO2_AVAILABLE_IN_ALL +int pango2_hb_face_get_instance_id (Pango2HbFace *self); + +PANGO2_AVAILABLE_IN_ALL +const hb_variation_t * pango2_hb_face_get_variations (Pango2HbFace *self, + unsigned int *n_variations); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_hb_face_get_embolden (Pango2HbFace *self); + +PANGO2_AVAILABLE_IN_ALL +const Pango2Matrix * pango2_hb_face_get_transform (Pango2HbFace *self); + +G_END_DECLS diff --git a/pango2/pango-hbfamily-private.h b/pango2/pango-hbfamily-private.h new file mode 100644 index 00000000..fb92cfb6 --- /dev/null +++ b/pango2/pango-hbfamily-private.h @@ -0,0 +1,48 @@ +/* + * Copyright 2021 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-font-family-private.h" +#include "pango-hbface.h" + + +#define PANGO2_TYPE_HB_FAMILY (pango2_hb_family_get_type ()) + +G_DECLARE_FINAL_TYPE (Pango2HbFamily, pango2_hb_family, PANGO2, HB_FAMILY, Pango2FontFamily) + +struct _Pango2HbFamily +{ + Pango2FontFamily parent_instance; + + GPtrArray *faces; +}; + +Pango2HbFamily * pango2_hb_family_new (const char *name); + +void pango2_hb_family_add_face (Pango2HbFamily *self, + Pango2FontFace *face); + +void pango2_hb_family_remove_face (Pango2HbFamily *self, + Pango2FontFace *face); + +Pango2FontFace * pango2_hb_family_find_face (Pango2HbFamily *self, + Pango2FontDescription *description, + Pango2Language *language, + gunichar wc); diff --git a/pango2/pango-hbfamily.c b/pango2/pango-hbfamily.c new file mode 100644 index 00000000..dc990683 --- /dev/null +++ b/pango2/pango-hbfamily.c @@ -0,0 +1,332 @@ +/* Pango2 + * + * Copyright (C) 2021 Matthias Clasen + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include <gio/gio.h> + +#include "pango-hbfamily-private.h" +#include "pango-impl-utils.h" +#include "pango-hbface-private.h" +#include "pango-font-face-private.h" +#include "pango-font-description-private.h" +#include "pango-userface-private.h" + +/* {{{ GListModel implementation */ + +static GType +pango2_hb_family_get_item_type (GListModel *list) +{ + return PANGO2_TYPE_FONT_FACE; +} + +static guint +pango2_hb_family_get_n_items (GListModel *list) +{ + Pango2HbFamily *self = PANGO2_HB_FAMILY (list); + + return self->faces->len; +} + +static gpointer +pango2_hb_family_get_item (GListModel *list, + guint position) +{ + Pango2HbFamily *self = PANGO2_HB_FAMILY (list); + + if (position < self->faces->len) + return g_object_ref (g_ptr_array_index (self->faces, position)); + + return NULL; +} + +static void +pango2_hb_family_list_model_init (GListModelInterface *iface) +{ + iface->get_item_type = pango2_hb_family_get_item_type; + iface->get_n_items = pango2_hb_family_get_n_items; + iface->get_item = pango2_hb_family_get_item; +} + +/* }}} */ +/* {{{ Utilities */ + +static int +sort_face_func (Pango2FontFace *face1, + Pango2FontFace *face2) +{ + int a, b; + + a = pango2_font_description_get_style (face1->description); + b = pango2_font_description_get_style (face2->description); + if (a != b) + return a - b; + + a = pango2_font_description_get_weight (face1->description); + b = pango2_font_description_get_weight (face2->description); + if (a != b) + return a - b; + + a = pango2_font_description_get_stretch (face1->description); + b = pango2_font_description_get_stretch (face2->description); + if (a != b) + return a - b; + + return strcmp (face1->name, face2->name); +} + +/* return 2 if face is a named instance, + * 1 if it is variable + * 0 otherwise + */ +static int +face_get_variableness (Pango2FontFace *face) +{ + if (pango2_font_face_is_variable (PANGO2_FONT_FACE (face))) + { + if (PANGO2_HB_FACE (face)->instance_id != -1) + return 2; + else + return 1; + } + return 0; +} + +/* }}} */ +/* {{{ Pango2FontFamily implementation */ + +struct _Pango2HbFamilyClass +{ + Pango2FontFamilyClass parent_class; +}; + +G_DEFINE_TYPE_WITH_CODE (Pango2HbFamily, pango2_hb_family, PANGO2_TYPE_FONT_FAMILY, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, pango2_hb_family_list_model_init)) + +static void +pango2_hb_family_init (Pango2HbFamily *self) +{ + self->faces = g_ptr_array_new_with_free_func (g_object_unref); +} + +static void +pango2_hb_family_finalize (GObject *object) +{ + Pango2HbFamily *self = PANGO2_HB_FAMILY (object); + + for (int i = 0; i < self->faces->len; i++) + { + Pango2FontFace *face = g_ptr_array_index (self->faces, i); + face->family = NULL; + } + + g_ptr_array_unref (self->faces); + + G_OBJECT_CLASS (pango2_hb_family_parent_class)->finalize (object); +} + +static Pango2FontFace * +pango2_hb_family_get_face (Pango2FontFamily *family, + const char *name) +{ + Pango2HbFamily *self = PANGO2_HB_FAMILY (family); + + for (int i = 0; i < self->faces->len; i++) + { + Pango2FontFace *face = g_ptr_array_index (self->faces, i); + + if (name == NULL || strcmp (name, pango2_font_face_get_name (face)) == 0) + return face; + } + + return NULL; +} + +static void +pango2_hb_family_class_init (Pango2HbFamilyClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + Pango2FontFamilyClass *family_class = PANGO2_FONT_FAMILY_CLASS (class); + + object_class->finalize = pango2_hb_family_finalize; + + family_class->get_face = pango2_hb_family_get_face; +} + +/* }}} */ +/* {{{ Private API */ + +/*< private > + * pango2_hb_family_new: + * @name: the family name + * + * Creates a new `Pango2HbFamily`. + * + * Returns: a newly created `Pango2HbFamily` + */ +Pango2HbFamily * +pango2_hb_family_new (const char *name) +{ + Pango2HbFamily *self; + + self = g_object_new (PANGO2_TYPE_HB_FAMILY, NULL); + + pango2_font_family_set_name (PANGO2_FONT_FAMILY (self), name); + + return self; +} + +/*< private > + * pango2_hb_family_add_face: + * @self: a `Pango2HbFamily` + * @face: (transfer full): a `Pango2FontFace` to add + * + * Adds a `Pango2FontFace` to a `Pango2HbFamily`. + * + * The family takes ownership of the added face. + * + * It is an error to call this function more than + * once for the same face. + */ +void +pango2_hb_family_add_face (Pango2HbFamily *self, + Pango2FontFace *face) +{ + int position; + + g_return_if_fail (PANGO2_IS_HB_FACE (face) || PANGO2_IS_USER_FACE (face)); + + position = 0; + while (position < self->faces->len) + { + Pango2FontFace *f = g_ptr_array_index (self->faces, position); + if (sort_face_func (face, f) < 0) + break; + position++; + } + + g_ptr_array_insert (self->faces, position, face); + + pango2_font_face_set_family (face, PANGO2_FONT_FAMILY (self)); + + g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1); + g_object_notify (G_OBJECT (self), "n-items"); +} + +/*< private > + * pango2_hb_family_remove_face: + * @self: a `Pango2HbFamily` + * @face: a `Pango2FontFace` + * + * Remove a `Pango2FontFace` from a `Pango2HbFamily`. + */ +void +pango2_hb_family_remove_face (Pango2HbFamily *self, + Pango2FontFace *face) +{ + unsigned int position; + + g_return_if_fail (PANGO2_IS_HB_FACE (face) || PANGO2_IS_USER_FACE (face)); + + if (!g_ptr_array_find (self->faces, face, &position)) + return; + + pango2_font_face_set_family (face, NULL); + + g_ptr_array_remove_index (self->faces, position); + + g_list_model_items_changed (G_LIST_MODEL (self), position, 1, 0); + g_object_notify (G_OBJECT (self), "n-items"); +} + +/*< private > + * pango2_hb_family_find_face: + * @family: a `Pango2HbFamily` + * @description: `Pango2FontDescription` to match + * @language: (nullable): `Pango2Language` to support + * @wc: a Unicode character, or 0 to ignore + * + * Finds the face in @family that best matches the font description while + * also supporting the given language and character. + * + * Amongst equally matching faces, a variable face is preferred over a + * non-variable one, and a named instance is preferred over a bare variable + * face. + * + * Returns: (transfer none) (nullable): the face + */ +Pango2FontFace * +pango2_hb_family_find_face (Pango2HbFamily *family, + Pango2FontDescription *description, + Pango2Language *language, + gunichar wc) +{ + Pango2FontFace *face = NULL; + int best_distance = G_MAXINT; + int best_variableness = 0; + + /* First look for an exact match if the description has a faceid */ + if (pango2_font_description_get_set_fields (description) & PANGO2_FONT_MASK_FACEID) + { + const char *faceid = pango2_font_description_get_faceid (description); + + for (int i = 0; i < family->faces->len; i++) + { + Pango2FontFace *face2 = g_ptr_array_index (family->faces, i); + const char *faceid2 = pango2_font_face_get_faceid (face2); + + if (g_strcmp0 (faceid, faceid2) == 0) + return face2; + } + } + + for (int i = 0; i < family->faces->len; i++) + { + Pango2FontFace *face2 = g_ptr_array_index (family->faces, i); + int distance; + int variableness; + + if (language && !pango2_font_face_supports_language (face2, language)) + continue; + + if (wc && !pango2_font_face_has_char (face2, wc)) + continue; + + if (!pango2_font_description_is_similar (description, face2->description)) + continue; + + distance = pango2_font_description_compute_distance (description, face2->description); + + variableness = face_get_variableness (PANGO2_FONT_FACE (face2)); + if (distance < best_distance || + (distance == best_distance && variableness > best_variableness)) + { + face = face2; + best_distance = distance; + best_variableness = variableness; + } + } + + return face; +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pango-hbfont-private.h b/pango2/pango-hbfont-private.h new file mode 100644 index 00000000..6794357f --- /dev/null +++ b/pango2/pango-hbfont-private.h @@ -0,0 +1,56 @@ +/* + * Copyright 2021 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-hbfont.h" +#include "pango-hbface.h" +#include "pango-font-private.h" + +#include <hb.h> + + +typedef struct _HexBoxInfo HexBoxInfo; +struct _HexBoxInfo +{ + Pango2Font *font; + int rows; + double digit_width; + double digit_height; + double pad_x; + double pad_y; + double line_width; + double box_descent; + double box_height; +}; + +struct _Pango2HbFont +{ + Pango2Font parent_instance; + + hb_feature_t *features; + unsigned int n_features; + hb_variation_t *variations; + unsigned int n_variations; + + HexBoxInfo *hex_box_info; + Pango2Language *approximate_char_lang; + int approximate_char_width; + int approximate_digit_width; +}; diff --git a/pango2/pango-hbfont.c b/pango2/pango-hbfont.c new file mode 100644 index 00000000..a8876224 --- /dev/null +++ b/pango2/pango-hbfont.c @@ -0,0 +1,1097 @@ +/* Pango2 + * + * Copyright (C) 2021 Matthias Clasen + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include "pango-hbfont-private.h" + +#include "pango-font-private.h" +#include "pango-font-metrics-private.h" +#include "pango-hbface-private.h" +#include "pango-hbfamily-private.h" +#include "pango-impl-utils.h" +#include "pango-language-set-private.h" + +#include <hb-ot.h> + +/** + * Pango2HbFont: + * + * `Pango2HbFont` is a `Pango2Font` implementation that wraps + * a `hb_font_t` object and implements all of the `Pango2Font` + * functionality using HarfBuzz. + * + * In addition to a `Pango2HbFace` and a size, a number of optional + * parameters can be tweaked when creating a `Pango2HbFont`. First + * there are OpenType font features, which can be used to e.g. + * select Small Caps. If the face has variation axes, then + * coordinates for these axes can be provided. Finally, there are + * rendering parameters such as the dpi and the global transformation + * matrix. + */ + +/* {{{ Utilities */ + +static int +get_average_char_width (Pango2Font *font, + const char *text) +{ + hb_font_t *hb_font = pango2_font_get_hb_font (font); + int width = 0; + + for (const char *p = text; *p; p = g_utf8_next_char (p)) + { + gunichar wc; + hb_codepoint_t glyph; + Pango2Rectangle extents; + + wc = g_utf8_get_char (p); + if (!hb_font_get_nominal_glyph (hb_font, wc, &glyph)) + continue; + + pango2_font_get_glyph_extents (font, glyph, &extents, NULL); + + width += extents.x + extents.width; + } + + return width / pango2_utf8_strwidth (text); +} + +static void +get_max_char_size (Pango2Font *font, + const char *text, + int *width, + int *height) +{ + hb_font_t *hb_font = pango2_font_get_hb_font (font); + int w = 0; + int h = 0; + + for (const char *p = text; *p; p = g_utf8_next_char (p)) + { + gunichar wc; + hb_codepoint_t glyph; + Pango2Rectangle extents; + + wc = g_utf8_get_char (p); + if (!hb_font_get_nominal_glyph (hb_font, wc, &glyph)) + continue; + + pango2_font_get_glyph_extents (font, glyph, &extents, NULL); + + w = MAX (w, extents.x + extents.width); + h = MAX (h, extents.height); + } + + if (width) + *width = w; + + if (height) + *height = h; +} + +static Pango2Variant +pango2_variant_from_features (hb_feature_t *features, + unsigned int n_features) +{ + Pango2Variant variant = PANGO2_VARIANT_NORMAL; + gboolean all_caps = FALSE; + + for (int i = 0; i < n_features; i++) + { + if (features[i].value != 1) + continue; + + switch (features[i].tag) + { + case HB_TAG('s','m','c','p'): + if (all_caps) + variant = PANGO2_VARIANT_ALL_SMALL_CAPS; + else + variant = PANGO2_VARIANT_SMALL_CAPS; + break; + case HB_TAG('c','2','s','c'): + if (variant == PANGO2_VARIANT_SMALL_CAPS) + variant = PANGO2_VARIANT_ALL_SMALL_CAPS; + else + all_caps = TRUE; + break; + case HB_TAG('p','c','a','p'): + if (all_caps) + variant = PANGO2_VARIANT_ALL_PETITE_CAPS; + else + variant = PANGO2_VARIANT_PETITE_CAPS; + break; + case HB_TAG('c','2','p','c'): + if (variant == PANGO2_VARIANT_PETITE_CAPS) + variant = PANGO2_VARIANT_ALL_PETITE_CAPS; + else + all_caps = TRUE; + break; + case HB_TAG('u','n','i','c'): + variant = PANGO2_VARIANT_UNICASE; + break; + case HB_TAG('t','i','t','l'): + variant = PANGO2_VARIANT_TITLE_CAPS; + break; + default: + break; + } + } + + return variant; +} + +static void +font_description_get_features (const Pango2FontDescription *description, + hb_feature_t *features, + unsigned int length, + unsigned int *n_features) +{ + +#define ADD_FEATURE(name) \ + features[*n_features].tag = HB_TAG (name[0], name[1], name[2], name[3]); \ + features[*n_features].value = 1; \ + features[*n_features].start = 0; \ + features[*n_features].end = (unsigned int) -1; \ + (*n_features)++ + + g_assert (length >= 2); + + *n_features = 0; + switch (pango2_font_description_get_variant (description)) + { + case PANGO2_VARIANT_SMALL_CAPS: + ADD_FEATURE ("smcp"); + break; + case PANGO2_VARIANT_ALL_SMALL_CAPS: + ADD_FEATURE ("smcp"); + ADD_FEATURE ("c2sc"); + break; + case PANGO2_VARIANT_PETITE_CAPS: + ADD_FEATURE ("pcap"); + break; + case PANGO2_VARIANT_ALL_PETITE_CAPS: + ADD_FEATURE ("pcap"); + ADD_FEATURE ("c2pc"); + break; + case PANGO2_VARIANT_UNICASE: + ADD_FEATURE ("unic"); + break; + case PANGO2_VARIANT_TITLE_CAPS: + ADD_FEATURE ("titl"); + break; + case PANGO2_VARIANT_NORMAL: + break; + default: + g_assert_not_reached (); + } + +#undef ADD_FEATURE +} + +static unsigned int +count_variations (const char *string) +{ + unsigned int n; + const char *p; + + n = 1; + p = string; + while ((p = strchr (p, ',')) != NULL) + n++; + + return n; +} + +static void +parse_variations (const char *str, + hb_variation_t *variations, + unsigned int length, + unsigned int *n_variations) +{ + const char *p; + + *n_variations = 0; + + p = str; + while (p && *p && *n_variations < length) + { + const char *end = strchr (p, ','); + if (hb_variation_from_string (p, end ? end - p: -1, &variations[*n_variations])) + (*n_variations)++; + p = end ? end + 1 : NULL; + } +} + +static char * +variations_to_string (hb_variation_t *variations, + unsigned int n_variations) +{ + GString *s; + char buf[128]; + + s = g_string_new (""); + + for (unsigned int i = 0; i < n_variations; i++) + { + hb_variation_to_string (&variations[i], buf, sizeof (buf)); + if (s->len > 0) + g_string_append_c (s, ','); + g_string_append (s, buf); + } + + return g_string_free (s, FALSE); +} + +static inline void +replace_variation (hb_variation_t *values, + unsigned int *len, + const hb_variation_t *v) +{ + for (int i = 0; i < *len; i++) + { + if (values[i].tag == v->tag) + { + values[i].value = v->value; + return; + } + } + + values[(*len)++] = *v; +} + +static unsigned int +merge_variations (const hb_variation_t *v1, + unsigned int l1, + const hb_variation_t *v2, + unsigned int l2, + hb_variation_t *v, + unsigned int l) +{ + unsigned int len = 0; + + for (int i = 0; i < l1; i++) + replace_variation (v, &len, &v1[i]); + + for (int i = 0; i < l2; i++) + replace_variation (v, &len, &v2[i]); + + return len; +} + +static inline void +collect_variation (hb_variation_t *variation, + unsigned int n_axes, + hb_ot_var_axis_info_t *axes, + float *coords) +{ + for (int j = 0; j < n_axes; j++) + { + if (axes[j].tag == variation->tag) + { + coords[axes[j].axis_index] = variation->value; + break; + } + } +} + +static inline void +collect_variations (hb_variation_t *variations, + unsigned int n_variations, + unsigned int n_axes, + hb_ot_var_axis_info_t *axes, + float *coords) +{ + for (int i = 0; i < n_variations; i++) + collect_variation (&variations[i], n_axes, axes, coords); +} + +/* }}} */ +/* {{{ hex box sizing */ + +/* This code needs to stay in sync with the hexbox rendering code in pangocairo-render.c */ +static HexBoxInfo * +create_hex_box_info (Pango2HbFont *self) +{ + Pango2Font *font = PANGO2_FONT (self); + const char hexdigits[] = "0123456789ABCDEF"; + hb_font_t *hb_font; + Pango2Font *mini_font; + HexBoxInfo *hbi; + int rows; + double pad; + int width = 0; + int height = 0; + hb_font_extents_t font_extents; + double font_ascent, font_descent; + double mini_size; + Pango2FontDescription *desc; + Pango2Context *context; + Pango2FontMap *map; + + if (!PANGO2_FONT_FACE (font->face)->family) + return NULL; + + map = PANGO2_FONT_FACE (font->face)->family->map; + + if (!map) + return NULL; + + desc = pango2_font_describe_with_absolute_size (font); + hb_font = pango2_font_get_hb_font (font); + + /* Create mini_font description */ + + /* We inherit most font properties for the mini font. + * Just change family and size, so you get bold + * hex digits in the hexbox for a bold font. + */ + + /* We should rotate the box, not glyphs */ + pango2_font_description_unset_fields (desc, PANGO2_FONT_MASK_GRAVITY); + + pango2_font_description_set_family_static (desc, "monospace"); + + rows = 2; + mini_size = font->size / 2.2; + + if (mini_size < 6.0) + { + rows = 1; + mini_size = MIN (MAX (font->size - 1, 0), 6.0); + } + + pango2_font_description_set_size (desc, mini_size); + + /* Load mini_font */ + context = pango2_context_new_with_font_map (map); + pango2_context_set_matrix (context, &font->ctm); + pango2_context_set_language (context, pango2_script_get_sample_language (G_UNICODE_SCRIPT_LATIN)); + + mini_font = pango2_font_map_load_font (map, context, desc); + + g_object_unref (context); + + pango2_font_description_free (desc); + + get_max_char_size (mini_font, hexdigits, &width, &height); + + hb_font_get_extents_for_direction (hb_font, HB_DIRECTION_LTR, &font_extents); + font_ascent = font_extents.ascender / (double) PANGO2_SCALE; + font_descent = - font_extents.descender / (double) PANGO2_SCALE; + + pad = (font_ascent + font_descent) / 43.; + pad = MIN (pad, mini_size / (double) PANGO2_SCALE); + + hbi = g_new (HexBoxInfo, 1); + hbi->font = mini_font; + hbi->rows = rows; + + hbi->digit_width = width / (double) PANGO2_SCALE; + hbi->digit_height = height / (double) PANGO2_SCALE; + + hbi->pad_x = pad; + hbi->pad_y = pad; + + hbi->line_width = MIN (hbi->pad_x, hbi->pad_y); + + hbi->box_height = 3 * hbi->pad_y + rows * (hbi->pad_y + hbi->digit_height); + + if (rows == 1 || hbi->box_height <= font_ascent) + hbi->box_descent = 2 * hbi->pad_y; + else if (hbi->box_height <= font_ascent + font_descent - 2 * hbi->pad_y) + hbi->box_descent = 2 * hbi->pad_y + hbi->box_height - font_ascent; + else + hbi->box_descent = font_descent * hbi->box_height / (font_ascent + font_descent); + + return hbi; +} + +static void +get_space_extents (Pango2HbFont *self, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect) +{ + Pango2Font *font = PANGO2_FONT (self); + hb_font_t *hb_font = pango2_font_get_hb_font (font); + int width; + hb_direction_t direction; + hb_font_extents_t font_extents; + + direction = PANGO2_GRAVITY_IS_VERTICAL (font->gravity) + ? HB_DIRECTION_TTB + : HB_DIRECTION_LTR; + + hb_font_get_extents_for_direction (hb_font, direction, &font_extents); + + /* See https://docs.microsoft.com/en-us/typography/develop/character-design-standards/whitespace */ + + width = font->size / 4; + + if (ink_rect) + { + ink_rect->x = ink_rect->y = ink_rect->height = 0; + ink_rect->width = width; + } + + if (logical_rect) + { + logical_rect->x = 0; + logical_rect->y = - font_extents.ascender; + logical_rect->height = font_extents.ascender - font_extents.descender; + logical_rect->width = width; + } +} + +static void +get_glyph_extents_missing (Pango2HbFont *self, + Pango2Glyph glyph, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect) +{ + gunichar ch; + int rows, cols; + HexBoxInfo *hbi; + + ch = glyph & ~PANGO2_GLYPH_UNKNOWN_FLAG; + + if (!self->hex_box_info) + self->hex_box_info = create_hex_box_info (self); + + if (ch == 0x20 || ch == 0x2423) + { + get_space_extents (self, ink_rect, logical_rect); + return; + } + + hbi = self->hex_box_info; + + if (G_UNLIKELY (glyph == PANGO2_GLYPH_INVALID_INPUT || ch > 0x10FFFF)) + { + rows = hbi->rows; + cols = 1; + } + else if (pango2_get_ignorable_size (ch, &rows, &cols)) + { + /* We special-case ignorables when rendering hex boxes */ + } + else + { + rows = hbi->rows; + cols = (ch > 0xffff ? 6 : 4) / rows; + } + + if (ink_rect) + { + ink_rect->x = PANGO2_SCALE * hbi->pad_x; + ink_rect->y = PANGO2_SCALE * (hbi->box_descent - hbi->box_height); + ink_rect->width = PANGO2_SCALE * (3 * hbi->pad_x + cols * (hbi->digit_width + hbi->pad_x)); + ink_rect->height = PANGO2_SCALE * hbi->box_height; + } + + if (logical_rect) + { + logical_rect->x = 0; + logical_rect->y = PANGO2_SCALE * (hbi->box_descent - (hbi->box_height + hbi->pad_y)); + logical_rect->width = PANGO2_SCALE * (5 * hbi->pad_x + cols * (hbi->digit_width + hbi->pad_x)); + logical_rect->height = PANGO2_SCALE * (hbi->box_height + 2 * hbi->pad_y); + } +} + + /* }}} */ +/* {{{ Pango2Font implementation */ + +struct _Pango2HbFontClass +{ + Pango2FontClass parent_class; +}; + +enum { + PROP_VARIATIONS = 1, + N_PROPERTIES +}; + +static GParamSpec *properties[N_PROPERTIES] = { NULL, }; + +G_DEFINE_FINAL_TYPE (Pango2HbFont, pango2_hb_font, PANGO2_TYPE_FONT) + +static void +pango2_hb_font_init (Pango2HbFont *self) +{ +} + +static void +hex_box_info_destroy (HexBoxInfo *hex_box_info) +{ + g_object_unref (hex_box_info->font); + g_free (hex_box_info); +} + +static void +pango2_hb_font_finalize (GObject *object) +{ + Pango2HbFont *self = PANGO2_HB_FONT (object); + + g_free (self->variations); + g_clear_pointer (&self->hex_box_info, hex_box_info_destroy); + + G_OBJECT_CLASS (pango2_hb_font_parent_class)->finalize (object); +} + + +static void +pango2_hb_font_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + Pango2HbFont *self = PANGO2_HB_FONT (object); + + switch (property_id) + { + case PROP_VARIATIONS: + { + char *str = NULL; + if (self->variations) + str = variations_to_string (self->variations, self->n_variations); + g_value_take_string (value, str); + } + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static Pango2FontDescription * +pango2_hb_font_describe (Pango2Font *font) +{ + Pango2HbFont *self = PANGO2_HB_FONT (font); + Pango2HbFace *face = PANGO2_HB_FACE (font->face); + Pango2FontDescription *desc; + Pango2Variant variant; + + desc = pango2_font_face_describe (font->face); + pango2_font_description_set_gravity (desc, font->gravity); + pango2_font_description_set_size (desc, font->size); + + variant = pango2_variant_from_features (self->features, self->n_features); + if (variant != PANGO2_VARIANT_NORMAL) + pango2_font_description_set_variant (desc, variant); + if (self->n_variations > 0 || face->n_variations > 0) + { + hb_variation_t *variations; + unsigned int n_variations; + char *str; + + if (face->n_variations > 0) + { + variations = g_alloca (sizeof (hb_variation_t) * (self->n_variations + face->n_variations)); + n_variations = merge_variations (face->variations, face->n_variations, + self->variations, self->n_variations, + variations, self->n_variations + face->n_variations); + } + else + { + variations = self->variations; + n_variations = self->n_variations; + } + + str = variations_to_string (variations, n_variations); + pango2_font_description_set_variations (desc, str); + g_free (str); + } + + return desc; +} + +static void +pango2_hb_font_get_glyph_extents (Pango2Font *font, + Pango2Glyph glyph, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect) +{ + Pango2HbFont *self = PANGO2_HB_FONT (font); + Pango2HbFace *face = PANGO2_HB_FACE (font->face); + hb_font_t *hb_font = pango2_font_get_hb_font (font); + hb_glyph_extents_t extents; + hb_direction_t direction; + hb_font_extents_t font_extents; + + direction = PANGO2_GRAVITY_IS_VERTICAL (font->gravity) + ? HB_DIRECTION_TTB + : HB_DIRECTION_LTR; + + hb_font_get_extents_for_direction (hb_font, direction, &font_extents); + + if (glyph == PANGO2_GLYPH_EMPTY) + { + if (ink_rect) + ink_rect->x = ink_rect->y = ink_rect->width = ink_rect->height = 0; + + if (logical_rect) + { + logical_rect->x = logical_rect->width = 0; + logical_rect->y = - font_extents.ascender; + logical_rect->height = font_extents.ascender - font_extents.descender; + } + + return; + } + else if (glyph & PANGO2_GLYPH_UNKNOWN_FLAG) + { + get_glyph_extents_missing (self, glyph, ink_rect, logical_rect); + + return; + } + + hb_font_get_glyph_extents (hb_font, glyph, &extents); + + if (ink_rect) + { + Pango2Rectangle r; + Pango2Matrix m = PANGO2_MATRIX_INIT; + + r.x = extents.x_bearing; + r.y = - extents.y_bearing; + r.width = extents.width; + r.height = - extents.height; + + if (face->transform) + { + m.xx = face->transform->xx; + m.yx = - face->transform->yx; + m.xy = - face->transform->xy; + m.yy = face->transform->yy; + + pango2_matrix_transform_rectangle (&m, &r); + } + + switch (font->gravity) + { + case PANGO2_GRAVITY_AUTO: + case PANGO2_GRAVITY_SOUTH: + ink_rect->x = r.x; + ink_rect->y = r.y; + ink_rect->width = r.width; + ink_rect->height = r.height; + break; + case PANGO2_GRAVITY_NORTH: + ink_rect->x = - r.x; + ink_rect->y = - r.y; + ink_rect->width = - r.width; + ink_rect->height = - r.height; + break; + case PANGO2_GRAVITY_EAST: + ink_rect->x = r.y; + ink_rect->y = - r.x - r.width; + ink_rect->width = r.height; + ink_rect->height = r.width; + break; + case PANGO2_GRAVITY_WEST: + ink_rect->x = - r.y - r.height; + ink_rect->y = r.x; + ink_rect->width = r.height; + ink_rect->height = r.width; + break; + default: + g_assert_not_reached (); + } + + if (PANGO2_GRAVITY_IS_IMPROPER (font->gravity)) + { + Pango2Matrix matrix = (Pango2Matrix) PANGO2_MATRIX_INIT; + pango2_matrix_scale (&matrix, -1, -1); + pango2_matrix_transform_rectangle (&matrix, ink_rect); + } + } + + if (logical_rect) + { + hb_position_t h_advance; + hb_font_extents_t extents; + + h_advance = hb_font_get_glyph_h_advance (hb_font, glyph); + hb_font_get_h_extents (hb_font, &extents); + + logical_rect->x = 0; + logical_rect->height = extents.ascender - extents.descender; + + switch (font->gravity) + { + case PANGO2_GRAVITY_AUTO: + case PANGO2_GRAVITY_SOUTH: + logical_rect->y = - extents.ascender; + logical_rect->width = h_advance; + break; + case PANGO2_GRAVITY_NORTH: + logical_rect->y = extents.descender; + logical_rect->width = h_advance; + break; + case PANGO2_GRAVITY_EAST: + logical_rect->y = - logical_rect->height / 2; + logical_rect->width = logical_rect->height; + break; + case PANGO2_GRAVITY_WEST: + logical_rect->y = - logical_rect->height / 2; + logical_rect->width = - logical_rect->height; + break; + default: + g_assert_not_reached (); + } + + if (PANGO2_GRAVITY_IS_IMPROPER (font->gravity)) + { + logical_rect->height = - logical_rect->height; + logical_rect->y = - logical_rect->y; + } + } +} + +static Pango2FontMetrics * +pango2_hb_font_get_metrics (Pango2Font *font, + Pango2Language *language) +{ + Pango2HbFont *self = PANGO2_HB_FONT (font); + hb_font_t *hb_font = pango2_font_get_hb_font (font); + Pango2FontMetrics *metrics; + hb_font_extents_t extents; + hb_position_t position; + + metrics = pango2_font_metrics_new (); + + hb_font_get_extents_for_direction (hb_font, HB_DIRECTION_LTR, &extents); + + metrics->descent = - extents.descender; + metrics->ascent = extents.ascender; + metrics->height = extents.ascender - extents.descender + extents.line_gap; + + if (hb_ot_metrics_get_position (hb_font, HB_OT_METRICS_TAG_UNDERLINE_SIZE, &position) && position != 0) + metrics->underline_thickness = position; + else + metrics->underline_thickness = PANGO2_SCALE; + + if (hb_ot_metrics_get_position (hb_font, HB_OT_METRICS_TAG_UNDERLINE_OFFSET, &position) && position != 0) + metrics->underline_position = position; + else + metrics->underline_position = - PANGO2_SCALE; + + if (hb_ot_metrics_get_position (hb_font, HB_OT_METRICS_TAG_STRIKEOUT_SIZE, &position) && position != 0) + metrics->strikethrough_thickness = position; + else + metrics->strikethrough_thickness = PANGO2_SCALE; + + if (hb_ot_metrics_get_position (hb_font, HB_OT_METRICS_TAG_STRIKEOUT_OFFSET, &position) && position != 0) + metrics->strikethrough_position = position; + else + metrics->strikethrough_position = metrics->ascent / 2; + + if (self->approximate_char_width == 0 || self->approximate_char_lang != language) + { + self->approximate_char_width = get_average_char_width (font, pango2_language_get_sample_string (language)); + self->approximate_char_lang = language; + } + + if (self->approximate_digit_width == 0) + get_max_char_size (font, "0123456789", &self->approximate_digit_width, NULL); + + metrics->approximate_char_width = self->approximate_char_width; + metrics->approximate_digit_width = self->approximate_digit_width; + + return metrics; +} + +static hb_font_t * +pango2_hb_font_create_hb_font (Pango2Font *font) +{ + Pango2HbFont *self = PANGO2_HB_FONT (font); + Pango2HbFace *face = PANGO2_HB_FACE (font->face); + hb_font_t *hb_font; + double x_scale, y_scale; + unsigned int n_axes; + hb_ot_var_axis_info_t *axes; + float *coords; + int size; + + hb_font = hb_font_create (pango2_hb_face_get_hb_face (face)); + + size = font->size * font->dpi / 72.f; + x_scale = face->x_scale; + y_scale = face->y_scale; + + if (PANGO2_GRAVITY_IS_IMPROPER (font->gravity)) + { + x_scale = - x_scale; + y_scale = - y_scale; + } + + hb_font_set_scale (hb_font, size * x_scale, size * y_scale); + hb_font_set_ptem (hb_font, font->size / PANGO2_SCALE); + +#if HB_VERSION_ATLEAST (3, 3, 0) + hb_font_set_synthetic_slant (hb_font, pango2_matrix_get_slant_ratio (face->transform)); +#endif + + if (face->instance_id >= 0) + hb_font_set_var_named_instance (hb_font, face->instance_id); + + if (self->n_variations > 0 || face->n_variations > 0) + { + n_axes = hb_ot_var_get_axis_count (face->face); + axes = g_alloca (sizeof (hb_ot_var_axis_info_t) * n_axes); + coords = g_alloca (sizeof (float) * n_axes); + + hb_ot_var_get_axis_infos (face->face, 0, &n_axes, axes); + + if (face->instance_id >= 0) + hb_ot_var_named_instance_get_design_coords (face->face, face->instance_id, &n_axes, coords); + else + { + for (int i = 0; i < n_axes; i++) + coords[axes[i].axis_index] = axes[i].default_value; + } + + collect_variations (face->variations, face->n_variations, n_axes, axes, coords); + collect_variations (self->variations, self->n_variations, n_axes, axes, coords); + + hb_font_set_var_coords_design (hb_font, coords, n_axes); + } + + return hb_font; +} + +static void +pango2_hb_font_get_transform (Pango2Font *font, + Pango2Matrix *matrix) +{ + Pango2HbFace *face = PANGO2_HB_FACE (font->face); + + if (face->transform) + { + *matrix = *face->transform; + pango2_matrix_scale (matrix, face->x_scale, face->y_scale); + } + else + *matrix = (Pango2Matrix) PANGO2_MATRIX_INIT; +} + +static void +pango2_hb_font_class_init (Pango2HbFontClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + Pango2FontClass *font_class = PANGO2_FONT_CLASS (class); + + object_class->finalize = pango2_hb_font_finalize; + object_class->get_property = pango2_hb_font_get_property; + + font_class->describe = pango2_hb_font_describe; + font_class->get_glyph_extents = pango2_hb_font_get_glyph_extents; + font_class->get_metrics = pango2_hb_font_get_metrics; + font_class->create_hb_font = pango2_hb_font_create_hb_font; + font_class->get_transform = pango2_hb_font_get_transform; + + /** + * Pango2HbFont:variations: (attributes org.gtk.Property.get=pango2_hb_font_get_variations) + * + * The variations that are applied for this font. + * + * This property contains a string representation of the variations. + */ + properties[PROP_VARIATIONS] = + g_param_spec_string ("variations", NULL, NULL, NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPERTIES, properties); +} + +/* }}} */ + /* {{{ Public API */ + +/** + * pango2_hb_font_new: + * @face: the `Pango2HbFace` to use + * @size: the desired size in points, scaled by `PANGO2_SCALE` + * @features: (nullable) (array length=n_features): OpenType font features to use for this font + * @n_features: length of @features + * @variations: (nullable) (array length=n_variations): font variations to apply + * @n_variations: length of @variations + * @gravity: the gravity to use when rendering + * @dpi: the dpi used when rendering + * @ctm: (nullable): transformation matrix to use when rendering + * + * Creates a new `Pango2HbFont`. + * + * Returns: a newly created `Pango2HbFont` + */ +Pango2HbFont * +pango2_hb_font_new (Pango2HbFace *face, + int size, + hb_feature_t *features, + unsigned int n_features, + hb_variation_t *variations, + unsigned int n_variations, + Pango2Gravity gravity, + float dpi, + const Pango2Matrix *ctm) +{ + Pango2HbFont *self; + Pango2Font *font; + + g_return_val_if_fail (PANGO2_IS_HB_FACE (face), NULL); + g_return_val_if_fail (size > 0, NULL); + g_return_val_if_fail (dpi > 0, NULL); + + self = g_object_new (PANGO2_TYPE_HB_FONT, NULL); + + font = PANGO2_FONT (self); + + pango2_font_set_face (font, PANGO2_FONT_FACE (face)); + pango2_font_set_size (font, size); + pango2_font_set_dpi (font, dpi); + pango2_font_set_gravity (font, gravity); + pango2_font_set_ctm (font, ctm); + + self->features = g_memdup2 (features, sizeof (hb_feature_t) * n_features); + self->n_features = n_features; + self->variations = g_memdup2 (variations, sizeof (hb_variation_t) * n_variations); + self->n_variations = n_variations; + + return self; +} + +/** + * pango2_hb_font_new_for_description: + * @face: the `Pango2HbFace` to use + * @description: a `Pango2FontDescription` + * @dpi: the dpi used when rendering + * @ctm: (nullable): transformation matrix to use when rendering + * + * Creates a new `Pango2HbFont` with size, features, variations and + * gravity taken from a font description. + * + * Returns: a newly created `Pango2HbFont` + */ +Pango2HbFont * +pango2_hb_font_new_for_description (Pango2HbFace *face, + const Pango2FontDescription *description, + float dpi, + const Pango2Matrix *ctm) +{ + int size; + hb_feature_t features[10]; + unsigned int n_features; + hb_variation_t *variations; + unsigned int n_variations; + unsigned int length; + Pango2Gravity gravity; + const char *str; + + g_return_val_if_fail (PANGO2_IS_HB_FACE (face), NULL); + g_return_val_if_fail (description != NULL, NULL); + g_return_val_if_fail (dpi > 0, NULL); + + if ((pango2_font_description_get_set_fields (description) & PANGO2_FONT_MASK_SIZE) == 0) + size = 10 * PANGO2_SCALE; + else if (pango2_font_description_get_size_is_absolute (description)) + size = pango2_font_description_get_size (description) * 72. / dpi; + else + size = pango2_font_description_get_size (description); + + g_return_val_if_fail (size > 0, NULL); + + font_description_get_features (description, features, G_N_ELEMENTS (features), &n_features); + + if ((pango2_font_description_get_set_fields (description) & PANGO2_FONT_MASK_VARIATIONS) != 0) + { + str = pango2_font_description_get_variations (description); + length = count_variations (str); + variations = g_alloca (sizeof (hb_variation_t) * length); + parse_variations (str, variations, length, &n_variations); + } + else + { + variations = NULL; + n_variations = 0; + } + + if ((pango2_font_description_get_set_fields (description) & PANGO2_FONT_MASK_GRAVITY) != 0 && + pango2_font_description_get_gravity (description) != PANGO2_GRAVITY_SOUTH) + gravity = pango2_font_description_get_gravity (description); + else + gravity = PANGO2_GRAVITY_AUTO; + + return pango2_hb_font_new (face, size, features, n_features, variations, n_variations, gravity, dpi, ctm); +} + +/** + * pango2_hb_font_get_features: + * @self: a `Pango2Font` + * @n_features: (nullable) (out caller-allocates): return location for + * the length of the returned array + * + * Obtain the OpenType features that are provided by the font. + * + * These are passed to the rendering system, together with features + * that have been explicitly set via attributes. + * + * Note that this does not include OpenType features which the + * rendering system enables by default. + * + * Returns: (nullable) (transfer none): the features + */ +const hb_feature_t * +pango2_hb_font_get_features (Pango2HbFont *self, + unsigned int *n_features) +{ + g_return_val_if_fail (PANGO2_IS_HB_FONT (self), NULL); + + if (n_features) + *n_features = self->n_features; + + return self->features; +} + + +/** + * pango2_hb_font_get_variations: + * @self: a `Pango2HbFont` + * @n_variations: (nullable) (out caller-allocates): return location for + * the length of the returned array + * + * Gets the variations of the font. + * + * Returns: (nullable) (transfer none): the variations + */ +const hb_variation_t * +pango2_hb_font_get_variations (Pango2HbFont *self, + unsigned int *n_variations) +{ + g_return_val_if_fail (PANGO2_IS_HB_FONT (self), NULL); + + if (n_variations) + *n_variations = self->n_variations; + + return self->variations; +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pango-hbfont.h b/pango2/pango-hbfont.h new file mode 100644 index 00000000..1b98cd49 --- /dev/null +++ b/pango2/pango-hbfont.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2021 Matthias Clasen + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-types.h> +#include <pango2/pango-font.h> +#include <pango2/pango-hbface.h> +#include <hb.h> + +G_BEGIN_DECLS + +#define PANGO2_TYPE_HB_FONT (pango2_hb_font_get_type ()) + +PANGO2_AVAILABLE_IN_ALL +PANGO2_DECLARE_INTERNAL_TYPE (Pango2HbFont, pango2_hb_font, PANGO2, HB_FONT, Pango2Font) + +PANGO2_AVAILABLE_IN_ALL +Pango2HbFont * pango2_hb_font_new (Pango2HbFace *face, + int size, + hb_feature_t *features, + unsigned int n_features, + hb_variation_t *variations, + unsigned int n_variations, + Pango2Gravity gravity, + float dpi, + const Pango2Matrix *ctm); + +PANGO2_AVAILABLE_IN_ALL +Pango2HbFont * pango2_hb_font_new_for_description (Pango2HbFace *face, + const Pango2FontDescription *description, + float dpi, + const Pango2Matrix *ctm); + +PANGO2_AVAILABLE_IN_ALL +const hb_feature_t * pango2_hb_font_get_features (Pango2HbFont *self, + unsigned int *n_features); + +PANGO2_AVAILABLE_IN_ALL +const hb_variation_t * pango2_hb_font_get_variations (Pango2HbFont *self, + unsigned int *n_variations); + +G_END_DECLS diff --git a/pango2/pango-impl-utils.h b/pango2/pango-impl-utils.h new file mode 100644 index 00000000..cc21bc0b --- /dev/null +++ b/pango2/pango-impl-utils.h @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2003 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <glib.h> +#include <glib-object.h> +#include <pango2/pango.h> + +G_BEGIN_DECLS + + +/* String interning for static strings */ +#define I_(string) g_intern_static_string (string) + +/* We define these functions static here because we don't want to add public API + * for them (if anything, it belongs to glib, but glib found it trivial enough + * not to add API for). At some point metrics calculations will be + * centralized and this mess can be minimized. Or so I hope. + */ + +static inline G_GNUC_UNUSED int +pango2_unichar_width (gunichar c) +{ + return G_UNLIKELY (g_unichar_iszerowidth (c)) ? 0 : + G_UNLIKELY (g_unichar_iswide (c)) ? 2 : 1; +} + +static G_GNUC_UNUSED glong +pango2_utf8_strwidth (const char *p) +{ + glong len = 0; + g_return_val_if_fail (p != NULL, 0); + + while (*p) + { + len += pango2_unichar_width (g_utf8_get_char (p)); + p = g_utf8_next_char (p); + } + + return len; +} + +/* Glib's g_utf8_strlen() is broken and stops at embedded NUL's. + * Wrap it here. */ +static G_GNUC_UNUSED glong +pango2_utf8_strlen (const char *p, + gssize max) +{ + glong len = 0; + const char *start = p; + g_return_val_if_fail (p != NULL || max == 0, 0); + + if (max <= 0) + return g_utf8_strlen (p, max); + + p = g_utf8_next_char (p); + while (p - start < max) + { + ++len; + p = g_utf8_next_char (p); + } + + /* only do the last len increment if we got a complete + * char (don't count partial chars) + */ + if (p - start <= max) + ++len; + + return len; +} + + +/* To be made public at some point */ + +static G_GNUC_UNUSED void +pango2_glyph_string_reverse_range (Pango2GlyphString *glyphs, + int start, + int end) +{ + int i, j; + + for (i = start, j = end - 1; i < j; i++, j--) + { + Pango2GlyphInfo glyph_info; + int log_cluster; + + glyph_info = glyphs->glyphs[i]; + glyphs->glyphs[i] = glyphs->glyphs[j]; + glyphs->glyphs[j] = glyph_info; + + log_cluster = glyphs->log_clusters[i]; + glyphs->log_clusters[i] = glyphs->log_clusters[j]; + glyphs->log_clusters[j] = log_cluster; + } +} + +static inline gboolean +pango2_is_default_ignorable (gunichar ch) +{ + int plane = ch >> 16; + + if (G_LIKELY (plane == 0)) + { + int page = ch >> 8; + switch (page) + { + case 0x00: return ch == 0x00ad; + case 0x03: return ch == 0x034f; + case 0x06: return ch == 0x061c; + case 0x17: return (0x17b4 <= ch && ch <= 0x17b5); + case 0x18: return (0x180b <= ch && ch <= 0x180e); + case 0x20: return (0x200b <= ch && ch <= 0x200f) || + (0x202a <= ch && ch <= 0x202e) || + (0x2060 <= ch && ch <= 0x206f); + case 0xfe: return (0xfe00 <= ch && ch <= 0xfe0f) || ch == 0xfeff; + case 0xff: return (0xfff0 <= ch && ch <= 0xfff8); + default: return FALSE; + } + } + else + { + /* Other planes */ + switch (plane) + { + case 0x01: return (0x1d173 <= ch && ch <= 0x1d17a); + case 0x0e: return (0xe0000 <= ch && ch <= 0xe0fff); + default: return FALSE; + } + } +} + +/* These are the default ignorables that we render as hexboxes + * with nicks if PANGO2_SHOW_IGNORABLES is used. + * + * The cairo hexbox drawing code assumes that these nicks are + * 1-6 ASCII chars + */ +static struct { + gunichar ch; + const char *nick; +} ignorables[] = { + { 0x00ad, "SHY" }, /* SOFT HYPHEN */ + { 0x034f, "CGJ" }, /* COMBINING GRAPHEME JOINER */ + { 0x061c, "ALM" }, /* ARABIC LETTER MARK */ + { 0x200b, "ZWS" }, /* ZERO WIDTH SPACE */ + { 0x200c, "ZWNJ" }, /* ZERO WIDTH NON-JOINER */ + { 0x200d, "ZWJ" }, /* ZERO WIDTH JOINER */ + { 0x200e, "LRM" }, /* LEFT-TO-RIGHT MARK */ + { 0x200f, "RLM" }, /* RIGHT-TO-LEFT MARK */ + { 0x2028, "LS" }, /* LINE SEPARATOR */ + { 0x2029, "PS" }, /* PARAGRAPH SEPARATOR */ + { 0x202a, "LRE" }, /* LEFT-TO-RIGHT EMBEDDING */ + { 0x202b, "RLE" }, /* RIGHT-TO-LEFT EMBEDDING */ + { 0x202c, "PDF" }, /* POP DIRECTIONAL FORMATTING */ + { 0x202d, "LRO" }, /* LEFT-TO-RIGHT OVERRIDE */ + { 0x202e, "RLO" }, /* RIGHT-TO-LEFT OVERRIDE */ + { 0x2060, "WJ" }, /* WORD JOINER */ + { 0x2061, "FA" }, /* FUNCTION APPLICATION */ + { 0x2062, "IT" }, /* INVISIBLE TIMES */ + { 0x2063, "IS" }, /* INVISIBLE SEPARATOR */ + { 0x2066, "LRI" }, /* LEFT-TO-RIGHT ISOLATE */ + { 0x2067, "RLI" }, /* RIGHT-TO-LEFT ISOLATE */ + { 0x2068, "FSI" }, /* FIRST STRONG ISOLATE */ + { 0x2069, "PDI" }, /* POP DIRECTIONAL ISOLATE */ + { 0xfeff, "ZWNBS" }, /* ZERO WIDTH NO-BREAK SPACE */ +}; + +static inline G_GNUC_UNUSED const char * +pango2_get_ignorable (gunichar ch) +{ + for (guint i = 0; i < G_N_ELEMENTS (ignorables); i++) + { + if (ch < ignorables[i].ch) + return NULL; + + if (ch == ignorables[i].ch) + return ignorables[i].nick; + } + + return NULL; +} + +static inline G_GNUC_UNUSED const char * +pango2_get_ignorable_size (gunichar ch, + int *rows, + int *cols) +{ + const char *nick; + int len; + + nick = pango2_get_ignorable (ch); + if (nick) + { + len = strlen (nick); + if (len < 4) + { + *rows = 1; + *cols = len; + } + else if (len > 4) + { + *rows = 2; + *cols = 3; + } + else + { + *rows = 2; + *cols = 2; + } + } + + return nick; +} + +/* Backward compatibility shim, to avoid bumping up the minimum + * required version of GLib; most of our uses of g_memdup() are + * safe, and those that aren't have been fixed + */ +#if !GLIB_CHECK_VERSION (2, 67, 3) +# define g_memdup2(mem,size) g_memdup((mem),(size)) +#endif + +G_END_DECLS diff --git a/pango2/pango-item-private.h b/pango2/pango-item-private.h new file mode 100644 index 00000000..13925b32 --- /dev/null +++ b/pango2/pango-item-private.h @@ -0,0 +1,129 @@ +/* + * Copyright 2021 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-item.h" +#include "pango-break.h" + +/*< private > + * Pango2Analysis: + * @size_font: font to use for determining line height + * @font: the font for this segment + * @level: the bidirectional level for this segment. + * @gravity: the glyph orientation for this segment (A `Pango2Gravity`). + * @flags: boolean flags for this segment + * @script: the detected script for this segment (A `Pango2Script`) + * @language: the detected language for this segment. + * @extra_attrs: extra attributes for this segment. + * + * The `Pango2Analysis` structure stores information about + * the properties of a segment of text. + */ +struct _Pango2Analysis +{ + Pango2Font *size_font; + Pango2Font *font; + + guint8 level; + guint8 gravity; + guint8 flags; + + guint8 script; + Pango2Language *language; + + GSList *extra_attrs; +}; + +/*< private> + * Pango2Item: + * @offset: byte offset of the start of this item in text. + * @length: length of this item in bytes. + * @num_chars: number of Unicode characters in the item. + * @char_offset: character offset of the start of this item in text. Since 1.50 + * @analysis: analysis results for the item. + * + * The `Pango2Item` structure stores information about a segment of text. + * + * You typically obtain `Pango2Items` by itemizing a piece of text + * with [func@itemize]. + */ +struct _Pango2Item +{ + int offset; + int length; + int num_chars; + int char_offset; + Pango2Analysis analysis; +}; + + +void pango2_analysis_collect_features (const Pango2Analysis *analysis, + hb_feature_t *features, + guint length, + guint *num_features); + +void pango2_analysis_set_size_font (Pango2Analysis *analysis, + Pango2Font *font); +Pango2Font * pango2_analysis_get_size_font (const Pango2Analysis *analysis); + +GList * pango2_itemize_with_font (Pango2Context *context, + Pango2Direction base_dir, + const char *text, + int start_index, + int length, + Pango2AttrList *attrs, + Pango2AttrIterator *cached_iter, + const Pango2FontDescription *desc); + +GList * pango2_itemize_post_process_items (Pango2Context *context, + const char *text, + Pango2LogAttr *log_attrs, + GList *items); + +Pango2Item * pango2_item_new (void); +Pango2Item * pango2_item_split (Pango2Item *orig, + int split_index, + int split_offset); +void pango2_item_unsplit (Pango2Item *orig, + int split_index, + int split_offset); +void pango2_item_apply_attrs (Pango2Item *item, + Pango2AttrIterator *iter); + + +typedef struct _ItemProperties ItemProperties; +struct _ItemProperties +{ + Pango2LineStyle uline_style; + Pango2UnderlinePosition uline_position; + Pango2LineStyle strikethrough_style; + Pango2LineStyle oline_style; + guint oline_single : 1; + guint showing_space : 1; + guint no_paragraph_break : 1; + int letter_spacing; + int line_spacing; + int absolute_line_height; + double line_height; + Pango2Attribute *shape; +}; + +void pango2_item_get_properties (Pango2Item *item, + ItemProperties *properties); diff --git a/pango2/pango-item.c b/pango2/pango-item.c new file mode 100644 index 00000000..97b0a2b9 --- /dev/null +++ b/pango2/pango-item.c @@ -0,0 +1,652 @@ +/* Pango2 + * pango-item.c: Single run handling + * + * Copyright (C) 2000 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include "pango-attributes.h" +#include "pango-attr-private.h" +#include "pango-item-private.h" +#include "pango-impl-utils.h" + +/** + * Pango2Analysis: + * + * The `Pango2Analysis` structure stores information about + * the properties of a segment of text. + */ + +/** + * Pango2Item: + * + * The `Pango2Item` structure stores information about + * a segment of text. + * + * You typically obtain `Pango2Items` by itemizing a piece + * of text with [func@Pango2.itemize]. + */ + +/** + * pango2_item_new: + * + * Creates a new `Pango2Item` structure initialized to default values. + * + * Return value: the newly allocated `Pango2Item`, which should + * be freed with [method@Pango2.Item.free]. + */ +Pango2Item * +pango2_item_new (void) +{ + Pango2Item *result = g_slice_new0 (Pango2Item); + + return (Pango2Item *)result; +} + +/** + * pango2_item_copy: + * @item: (nullable): a `Pango2Item` + * + * Copy an existing `Pango2Item` structure. + * + * Return value: (nullable): the newly allocated `Pango2Item` + */ +Pango2Item * +pango2_item_copy (Pango2Item *item) +{ + GSList *extra_attrs, *tmp_list; + Pango2Item *result; + + if (item == NULL) + return NULL; + + result = pango2_item_new (); + + result->offset = item->offset; + result->length = item->length; + result->num_chars = item->num_chars; + result->char_offset = item->char_offset; + + result->analysis = item->analysis; + if (result->analysis.size_font) + g_object_ref (result->analysis.size_font); + + if (result->analysis.font) + g_object_ref (result->analysis.font); + + extra_attrs = NULL; + tmp_list = item->analysis.extra_attrs; + while (tmp_list) + { + extra_attrs = g_slist_prepend (extra_attrs, pango2_attribute_copy (tmp_list->data)); + tmp_list = tmp_list->next; + } + + result->analysis.extra_attrs = g_slist_reverse (extra_attrs); + + return result; +} + +/** + * pango2_item_free: + * @item: (nullable): a `Pango2Item`, may be %NULL + * + * Free a `Pango2Item` and all associated memory. + **/ +void +pango2_item_free (Pango2Item *item) +{ + if (item == NULL) + return; + + if (item->analysis.extra_attrs) + { + g_slist_foreach (item->analysis.extra_attrs, (GFunc)pango2_attribute_destroy, NULL); + g_slist_free (item->analysis.extra_attrs); + } + + if (item->analysis.size_font) + g_object_unref (item->analysis.size_font); + + if (item->analysis.font) + g_object_unref (item->analysis.font); + + g_slice_free (Pango2Item, item); +} + +G_DEFINE_BOXED_TYPE (Pango2Item, pango2_item, + pango2_item_copy, + pango2_item_free); + +/** + * pango2_item_split: + * @orig: a `Pango2Item` + * @split_index: byte index of position to split item, relative to the + * start of the item + * @split_offset: number of chars between start of @orig and @split_index + * + * Modifies @orig to cover only the text after @split_index, and + * returns a new item that covers the text before @split_index that + * used to be in @orig. + * + * You can think of @split_index as the length of the returned item. + * @split_index may not be 0, and it may not be greater than or equal + * to the length of @orig (that is, there must be at least one byte + * assigned to each item, you can't create a zero-length item). + * @split_offset is the length of the first item in chars, and must be + * provided because the text used to generate the item isn't available, + * so `pango2_item_split()` can't count the char length of the split items + * itself. + * + * Return value: new item representing text before @split_index, which + * should be freed with [method@Pango2.Item.free]. + */ +Pango2Item * +pango2_item_split (Pango2Item *orig, + int split_index, + int split_offset) +{ + Pango2Item *new_item; + + g_return_val_if_fail (orig != NULL, NULL); + g_return_val_if_fail (split_index > 0, NULL); + g_return_val_if_fail (split_index < orig->length, NULL); + g_return_val_if_fail (split_offset > 0, NULL); + g_return_val_if_fail (split_offset < orig->num_chars, NULL); + + new_item = pango2_item_copy (orig); + new_item->length = split_index; + new_item->num_chars = split_offset; + + orig->offset += split_index; + orig->length -= split_index; + orig->num_chars -= split_offset; + orig->char_offset += split_offset; + + return new_item; +} + +/*< private > + * pango2_item_unsplit: + * @orig: the item to unsplit + * @split_index: value passed to pango2_item_split() + * @split_offset: value passed to pango2_item_split() + * + * Undoes the effect of a pango2_item_split() call with + * the same arguments. + * + * You are expected to free the new item that was returned + * by pango2_item_split() yourself. + */ +void +pango2_item_unsplit (Pango2Item *orig, + int split_index, + int split_offset) +{ + orig->offset -= split_index; + orig->length += split_index; + orig->num_chars += split_offset; + orig->char_offset -= split_offset; +} + +static int +compare_attr (gconstpointer p1, gconstpointer p2) +{ + const Pango2Attribute *a1 = p1; + const Pango2Attribute *a2 = p2; + if (pango2_attribute_equal (a1, a2) && + a1->start_index == a2->start_index && + a1->end_index == a2->end_index) + return 0; + + return 1; +} + +/** + * pango2_item_apply_attrs: + * @item: a `Pango2Item` + * @iter: a `Pango2AttrIterator` + * + * Add attributes to a `Pango2Item`. + * + * The idea is that you have attributes that don't affect itemization, + * such as font features, so you filter them out using + * [method@Pango2.AttrList.filter], itemize your text, then reapply the + * attributes to the resulting items using this function. + * + * The @iter should be positioned before the range of the item, + * and will be advanced past it. This function is meant to be called + * in a loop over the items resulting from itemization, while passing + * the iter to each call. + */ +void +pango2_item_apply_attrs (Pango2Item *item, + Pango2AttrIterator *iter) +{ + int start, end; + GSList *attrs = NULL; + + do + { + pango2_attr_iterator_range (iter, &start, &end); + + if (start >= item->offset + item->length) + break; + + if (end >= item->offset) + { + GSList *list, *l; + + list = pango2_attr_iterator_get_attrs (iter); + for (l = list; l; l = l->next) + { + if (!g_slist_find_custom (attrs, l->data, compare_attr)) + + attrs = g_slist_prepend (attrs, pango2_attribute_copy (l->data)); + } + g_slist_free_full (list, (GDestroyNotify)pango2_attribute_destroy); + } + + if (end >= item->offset + item->length) + break; + } + while (pango2_attr_iterator_next (iter)); + + item->analysis.extra_attrs = g_slist_concat (item->analysis.extra_attrs, attrs); +} + +void +pango2_analysis_collect_features (const Pango2Analysis *analysis, + hb_feature_t *features, + guint length, + guint *num_features) +{ + GSList *l; + + if (PANGO2_IS_HB_FONT (analysis->font)) + { + const hb_feature_t *font_features; + guint n_font_features; + + font_features = pango2_hb_font_get_features (PANGO2_HB_FONT (analysis->font), + &n_font_features); + *num_features = MIN (length, n_font_features); + memcpy (features, font_features, sizeof (hb_feature_t) * *num_features); + } + + for (l = analysis->extra_attrs; l && *num_features < length; l = l->next) + { + Pango2Attribute *attr = l->data; + if (attr->type == PANGO2_ATTR_FONT_FEATURES) + { + const char *feat; + const char *end; + int len; + + feat = attr->str_value; + + while (feat != NULL && *num_features < length) + { + end = strchr (feat, ','); + if (end) + len = end - feat; + else + len = -1; + if (hb_feature_from_string (feat, len, &features[*num_features])) + { + features[*num_features].start = attr->start_index; + features[*num_features].end = attr->end_index; + (*num_features)++; + } + + if (end == NULL) + break; + + feat = end + 1; + } + } + } + + /* Turn off ligatures when letterspacing */ + for (l = analysis->extra_attrs; l && *num_features < length; l = l->next) + { + Pango2Attribute *attr = l->data; + if (attr->type == PANGO2_ATTR_LETTER_SPACING) + { + hb_tag_t tags[] = { + HB_TAG('l','i','g','a'), + HB_TAG('c','l','i','g'), + HB_TAG('d','l','i','g'), + HB_TAG('h','l','i','g'), + }; + int i; + for (i = 0; i < G_N_ELEMENTS (tags); i++) + { + features[*num_features].tag = tags[i]; + features[*num_features].value = 0; + features[*num_features].start = attr->start_index; + features[*num_features].end = attr->end_index; + (*num_features)++; + } + } + } +} + +/*< private > + * pango2_analysis_set_size_font: + * @analysis: a `Pango2Analysis` + * @font: a `Pango2Font` + * + * Sets the font to use for determining the line height. + * + * This is used when scaling fonts for emulated Small Caps, + * to preserve the original line height. + */ +void +pango2_analysis_set_size_font (Pango2Analysis *analysis, + Pango2Font *font) +{ + if (analysis->size_font) + g_object_unref (analysis->size_font); + analysis->size_font = font; + if (analysis->size_font) + g_object_ref (analysis->size_font); +} + +/*< private > + * pango2_analysis_get_size_font: + * @analysis: a `Pango2Analysis` + * + * Gets the font to use for determining line height. + * + * If this returns `NULL`, use analysis->font. + * + * Returns: (nullable) (transfer none): the font + */ +Pango2Font * +pango2_analysis_get_size_font (const Pango2Analysis *analysis) +{ + return analysis->size_font; +} + +/*< private > + * pango2_item_get_properties: + * @item: a `Pango2Item` + * @properties: `ItemProperties` struct to populate + * + * Extract useful information from the @item's attributes. + * + * Note that letter-spacing and shape are required to be constant + * across items. But underline and strikethrough can vary across + * an item, so we collect all the values that we find. + */ +void +pango2_item_get_properties (Pango2Item *item, + ItemProperties *properties) +{ + GSList *tmp_list = item->analysis.extra_attrs; + + properties->uline_style = PANGO2_LINE_STYLE_NONE; + properties->uline_position = PANGO2_UNDERLINE_POSITION_NORMAL; + properties->oline_style = PANGO2_LINE_STYLE_NONE; + properties->strikethrough_style = PANGO2_LINE_STYLE_NONE; + properties->showing_space = FALSE; + properties->no_paragraph_break = FALSE; + properties->letter_spacing = 0; + properties->line_height = 0.0; + properties->absolute_line_height = 0; + properties->line_spacing = 0; + properties->shape = NULL; + + while (tmp_list) + { + Pango2Attribute *attr = tmp_list->data; + + switch ((int) attr->type) + { + case PANGO2_ATTR_UNDERLINE: + properties->uline_style = attr->int_value; + break; + + case PANGO2_ATTR_UNDERLINE_POSITION: + properties->uline_position = attr->int_value; + break; + + case PANGO2_ATTR_OVERLINE: + properties->oline_style = attr->int_value; + break; + + case PANGO2_ATTR_STRIKETHROUGH: + properties->strikethrough_style = attr->int_value; + break; + + case PANGO2_ATTR_LETTER_SPACING: + properties->letter_spacing = attr->int_value; + break; + + case PANGO2_ATTR_LINE_HEIGHT: + properties->line_height = attr->double_value; + break; + + case PANGO2_ATTR_ABSOLUTE_LINE_HEIGHT: + properties->absolute_line_height = attr->int_value; + break; + + case PANGO2_ATTR_LINE_SPACING: + properties->line_spacing = attr->int_value; + break; + + case PANGO2_ATTR_SHOW: + properties->showing_space = (attr->int_value & PANGO2_SHOW_SPACES) != 0; + break; + + case PANGO2_ATTR_PARAGRAPH: + properties->no_paragraph_break = TRUE; + break; + + case PANGO2_ATTR_SHAPE: + properties->shape = attr; + break; + + default: + break; + } + tmp_list = tmp_list->next; + } +} + +/** + * pango2_analysis_get_font: + * @analysis: a `Pango2Analysis` + * + * Returns the font that will be used for text + * with this `Pango2Analysis`. + * + * Return value: (transfer none): the `Pango2Font` + */ +Pango2Font * +pango2_analysis_get_font (const Pango2Analysis *analysis) +{ + return analysis->font; +} + +/** + * pango2_analysis_get_bidi_level: + * @analysis: a `Pango2Analysis` + * + * Returns the bidi embedding level for text + * with this `Pango2Analysis`. + * + * Return value: the bidi embedding level + */ +int +pango2_analysis_get_bidi_level (const Pango2Analysis *analysis) +{ + return analysis->level; +} + +/** + * pango2_analysis_get_gravity: + * @analysis: a `Pango2Analysis` + * + * Returns the gravity for text with this `Pango2Analysis`. + * + * Return value: the gravity + */ +Pango2Gravity +pango2_analysis_get_gravity (const Pango2Analysis *analysis) +{ + return (Pango2Gravity) analysis->gravity; +} + +/** + * pango2_analysis_get_flags: + * @analysis: a `Pango2Analysis` + * + * Returns flags for this `Pango2Analysis`. + * + * Possible flag values are + * `PANGO2_ANALYSIS_FLAG_CENTERED_BASELINE`, + * `PANGO2_ANALYSIS_FLAG_IS_ELLIPSIS` and + * `PANGO2_ANALYSIS_FLAG_NEED_HYPHEN`. + * + * Return value: the flags + */ +guint +pango2_analysis_get_flags (const Pango2Analysis *analysis) +{ + return analysis->flags; +} + +/** + * pango2_analysis_get_script: + * @analysis: a `Pango2Analysis` + * + * Returns the script for text with this `Pango2Analysis`. + * + * Return value: the script + */ +GUnicodeScript +pango2_analysis_get_script (const Pango2Analysis *analysis) +{ + return (GUnicodeScript) analysis->script; +} + +/** + * pango2_analysis_get_language: + * @analysis: a `Pango2Analysis` + * + * Returns the language for text with this `Pango2Analysis`. + * + * Return value: the script + */ +Pango2Language * +pango2_analysis_get_language (const Pango2Analysis *analysis) +{ + return analysis->language; +} + +/** + * pango2_analysis_get_extra_attributes: + * @analysis: a `Pango2Analysis` + * + * Returns attributes to apply to text with this + * `Pango2Analysis`. + * + * Return value: (transfer none) (element-type Pango2Attribute): + * a `GSList` with `Pango2Attribute` values + */ +GSList * +pango2_analysis_get_extra_attributes (const Pango2Analysis *analysis) +{ + return analysis->extra_attrs; +} + +/** + * pango2_item_get_analysis: + * @item: a `Pango2Item` + * + * Returns the `Pango2Analysis` of @item. + * + * Return value: (transfer none): a `Pango2Analysis` + */ +const Pango2Analysis * +pango2_item_get_analysis (Pango2Item *item) +{ + return &item->analysis; +} + +/** + * pango2_item_get_byte_offset: + * @item: a `Pango2Item` + * + * Returns the byte offset of this items + * text in the overall paragraph text. + * + * Return value: the byte offset + */ +int +pango2_item_get_byte_offset (Pango2Item *item) +{ + return item->offset; +} + +/** + * pango2_item_get_byte_length: + * @item: a `Pango2Item` + * + * Returns the length of this items + * text in bytes. + * + * Return value: the length of @item + */ +int +pango2_item_get_byte_length (Pango2Item *item) +{ + return item->length; +} + +/** + * pango2_item_get_char_offset: + * @item: a `Pango2Item` + * + * Returns the offset of this items text + * in the overall paragraph text, in characters. + * + * Returns value: the character offset + */ +int +pango2_item_get_char_offset (Pango2Item *item) +{ + return item->char_offset; +} + +/** + * pango2_item_get_char_length: + * @item: a `Pango2Item` + * + * Returns the number of characters in this + * items text. + * + * Return value: the number of characters in @item + */ +int +pango2_item_get_char_length (Pango2Item *item) +{ + return item->num_chars; +} diff --git a/pango2/pango-item.h b/pango2/pango-item.h new file mode 100644 index 00000000..c5dccfd2 --- /dev/null +++ b/pango2/pango-item.h @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2000 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-types.h> +#include <pango2/pango-attr-list.h> +#include <pango2/pango-attr-list.h> + +G_BEGIN_DECLS + +typedef struct _Pango2Analysis Pango2Analysis; +typedef struct _Pango2Item Pango2Item; + +/** + * PANGO2_ANALYSIS_FLAG_CENTERED_BASELINE: + * + * Whether the segment should be shifted to center around the baseline. + * + * This is mainly used in vertical writing directions. + */ +#define PANGO2_ANALYSIS_FLAG_CENTERED_BASELINE (1 << 0) + +/** + * PANGO2_ANALYSIS_FLAG_IS_ELLIPSIS: + * + * Whether this run holds ellipsized text. + */ +#define PANGO2_ANALYSIS_FLAG_IS_ELLIPSIS (1 << 1) + +/** + * PANGO2_ANALYSIS_FLAG_NEED_HYPHEN: + * + * Whether to add a hyphen at the end of the run during shaping. + */ +#define PANGO2_ANALYSIS_FLAG_NEED_HYPHEN (1 << 2) + +#define PANGO2_TYPE_ITEM (pango2_item_get_type ()) + +PANGO2_AVAILABLE_IN_ALL +GType pango2_item_get_type (void) G_GNUC_CONST; + +PANGO2_AVAILABLE_IN_ALL +Pango2Item * pango2_item_copy (Pango2Item *item); +PANGO2_AVAILABLE_IN_ALL +void pango2_item_free (Pango2Item *item); + +PANGO2_AVAILABLE_IN_ALL +GList * pango2_itemize (Pango2Context *context, + Pango2Direction base_dir, + const char *text, + int start_index, + int length, + Pango2AttrList *attrs); + +PANGO2_AVAILABLE_IN_ALL +Pango2Font * pango2_analysis_get_font (const Pango2Analysis *analysis); +PANGO2_AVAILABLE_IN_ALL +int pango2_analysis_get_bidi_level (const Pango2Analysis *analysis); +PANGO2_AVAILABLE_IN_ALL +Pango2Gravity pango2_analysis_get_gravity (const Pango2Analysis *analysis); +PANGO2_AVAILABLE_IN_ALL +guint pango2_analysis_get_flags (const Pango2Analysis *analysis); +PANGO2_AVAILABLE_IN_ALL +GUnicodeScript pango2_analysis_get_script (const Pango2Analysis *analysis); +PANGO2_AVAILABLE_IN_ALL +Pango2Language * pango2_analysis_get_language (const Pango2Analysis *analysis); +PANGO2_AVAILABLE_IN_ALL +GSList * pango2_analysis_get_extra_attributes (const Pango2Analysis *analysis); +PANGO2_AVAILABLE_IN_ALL +const Pango2Analysis * pango2_item_get_analysis (Pango2Item *item); +PANGO2_AVAILABLE_IN_ALL +int pango2_item_get_byte_offset (Pango2Item *item); +PANGO2_AVAILABLE_IN_ALL +int pango2_item_get_byte_length (Pango2Item *item); +PANGO2_AVAILABLE_IN_ALL +int pango2_item_get_char_offset (Pango2Item *item); +PANGO2_AVAILABLE_IN_ALL +int pango2_item_get_char_length (Pango2Item *item); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(Pango2Item, pango2_item_free) + +G_END_DECLS diff --git a/pango2/pango-language-sample-table.h b/pango2/pango-language-sample-table.h new file mode 100644 index 00000000..59513209 --- /dev/null +++ b/pango2/pango-language-sample-table.h @@ -0,0 +1,675 @@ +/* Pango2 Language Sample Table + * + * Each entry is of the form: + * + * LANGUAGE( + * code |* Name *|, + * SOURCE, + * "Sample text for the language." + * |* Translation of the sample text to English *| + * ) + * + * Where code is the ISO639-1, ISO639-2, or ISO639-3 code for the language, + * the first one that exists. Name is the name of the language in English. + * + * Source is where the sample text comes from. One of: + * + * WP-PANG + * Wikipedia's List of Pangrams in Other Languages + * http://en.wikipedia.org/wiki/List_of_pangrams#Other_languages + * Fetched on 2008-08-19 + * + * WP-SFD + * Wikipedia's Sample Font Displays in Other Languages + * http://en.wikipedia.org/wiki/Sample_Font_Displays_In_Other_Languages + * Fetched on 2008-08-19 + * + * WP + * Wikipedia, Article about the language + * Fetched on 2020-09-08 + * + * GLASS + * Kermit project's "I Can Eat Glass" list, also available in pango-view/ + * http://www.columbia.edu/kermit/utf8.html#glass + * Fetched on 2008-08-19, updates on 2020-09-08 + * + * KERMIT + * Kermit project's Quick-Brown-Fox equivalents for other languages + * http://www.columbia.edu/kermit/utf8.html#quickbrownfox + * Fetched on 2008-08-19 + * + * GSPECI + * gnome-specimen's translations + * http://svn.gnome.org/viewvc/gnome-specimen/trunk/po/ + * Fetched on 2008-08-19 + * + * MISC + * Miscellaneous + * + * The sample text may be a pangram, but is not necessarily. It is chosen to + * be demonstrative of normal text in the language, as well as exposing font + * feature requirements unique to the language. It should be suitable for use + * as sample text in a font selection dialog. + * + * Needless to say, the list MUST be sorted on the language code. + */ +/* Sacrificial define to make introspection happy. */ +#ifndef LANGUAGE +#define LANGUAGE(x, y, z) +#endif +LANGUAGE( + af /* Afrikaans */, + GLASS, + "Ek kan glas eet, maar dit doen my nie skade nie." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + ar /* Arabic */, + WP-PANG, + "نص حكيم له سر قاطع وذو شأن عظيم مكتوب على ثوب أخضر ومغلف بجلد أزرق." + /* A wise text which has an absolute secret and great importance, written on a green tissue and covered with blue leather. */ + ) +LANGUAGE( + arn /* Mapudungun */, + WP-PANG, + "Gvxam mincetu apocikvyeh: ñizol ce mamvj ka raq kuse bafkeh mew." + /* Tale under the full moon: the chief chemamull and the clay old woman at the lake/sea. */ + ) +LANGUAGE( + bar /* Bavarian */, + GLASS, + "I koh Glos esa, und es duard ma ned wei." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + bg /* Bulgarian */, + WP-SFD, + "Под южно дърво, цъфтящо в синьо, бягаше малко пухкаво зайче." + /* A little fluffy young rabbit ran under a southern tree blooming in blue */ + ) +LANGUAGE( + bi /* Bislama */, + GLASS, + "Mi save kakae glas, hemi no save katem mi." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + bn /* Bengali */, + GLASS, + "আমি কাঁচ খেতে পারি, তাতে আমার কোনো ক্ষতি হয় না।" + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + bo /* Tibetan */, + GLASS, + "ཤེལ་སྒོ་ཟ་ནས་ང་ན་གི་མ་རེད།" + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + bs /* Bosnian */, + WP-PANG, + "Fin džip, gluh jež i čvrst konjić dođoše bez moljca." + /* A nice jeep, a deaf hedgehog and a tough horse came without a moth. */ + ) +LANGUAGE( + ca /* Catalan */, + WP-PANG, + "Jove xef, porti whisky amb quinze glaçons d'hidrogen, coi!" + /* Young chef, bring whisky with fifteen hydrogen ice cubes, damn! */ + ) +LANGUAGE( + ch /* Chamorro */, + GLASS, + "Siña yo' chumocho krestat, ti ha na'lalamen yo'." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + cs /* Czech */, + WP-SFD, + "Příliš žluťoučký kůň úpěl ďábelské ódy." + /* A too yellow horse moaned devil odes. */ + ) +LANGUAGE( + cy /* Welsh */, + GLASS, + "Dw i'n gallu bwyta gwydr, 'dyw e ddim yn gwneud dolur i mi." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + da /* Danish */, + WP-SFD, + "Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon." + /* The quiz contestants ate strawberries with cream while Walther the clown was playing the xylophone. */ + ) +LANGUAGE( + de /* German */, + WP-SFD, + "Zwölf Boxkämpfer jagen Viktor quer über den großen Sylter Deich." + /* Twelve boxing fighters drive Viktor over the great. */ + ) +LANGUAGE( + dv /* Maldivian */, + WP, + "މާއްދާ 1 – ހުރިހާ އިންސާނުން ވެސް އުފަންވަނީ، ދަރަޖަ އާއި ޙައްޤު ތަކުގައި މިނިވަންކަމާއި ހަމަހަމަކަން ލިބިގެންވާ ބައެއްގެ ގޮތުގައެވެ." + /* Beginning of UDHR */ + ) +LANGUAGE( + el /* Greek */, + WP-SFD, + "Θέλει αρετή και τόλμη η ελευθερία. (Ανδρέας Κάλβος)" + /* Liberty requires virtue and mettle. (Andreas Kalvos) */ + ) +LANGUAGE( + en /* English */, + GSPECI, + "The quick brown fox jumps over the lazy dog." + ) +LANGUAGE( + enm /* Middle English */, + GLASS, + "Ich canne glas eten and hit hirtiþ me nouȝt." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + eo /* Esperanto */, + WP-SFD, + "Eĥoŝanĝo ĉiuĵaŭde." + /* Change of echo every Thursday. */ + ) +LANGUAGE( + es /* Spanish */, + WP-PANG, + "Jovencillo emponzoñado de whisky: ¡qué figurota exhibe!" + /* Whisky-intoxicated youngster — what a figure he's showing! */ + ) +LANGUAGE( + et /* Estonian */, + WP-SFD, + "See väike mölder jõuab rongile hüpata." + /* This small miller is able to jump on the train. */ + ) +LANGUAGE( + eu /* Basque */, + GLASS, + "Kristala jan dezaket, ez dit minik ematen." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + fa /* Persian */, + MISC /* Behdad Esfahbod (#548730) */, + "«الا یا اَیُّها السّاقی! اَدِرْ کَأساً وَ ناوِلْها!» که عشق آسان نمود اوّل، ولی افتاد مشکلها!" + ) +LANGUAGE( + fi /* Finnish */, + WP-SFD, + "Viekas kettu punaturkki laiskan koiran takaa kurkki." + /* The cunning red-coated fox peeped from behind the lazy dog. */ + ) +LANGUAGE( + fr /* French */, + MISC /* Vincent Untz (#549520) http://fr.wikipedia.org/wiki/Pangramme */, + "Voix ambiguë d'un cœur qui, au zéphyr, préfère les jattes de kiwis." + /* Ambiguous voice of a heart that, in the wind, prefers bowls of kiwis. */ + ) +LANGUAGE( + fro /* Old French */, + GLASS, + "Je puis mangier del voirre. Ne me nuit." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + ga /* Irish */, + WP-PANG, + "Chuaigh bé mhórshách le dlúthspád fíorfhinn trí hata mo dhea-phorcáin bhig." + /* A maiden of large appetite with an intensely white, dense spade went through the hat of my good little porker. */ + ) +LANGUAGE( + gd /* Scottish Gaelic */, + GLASS, + "S urrainn dhomh gloinne ithe; cha ghoirtich i mi." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + gl /* Galician */, + GLASS, + "Eu podo xantar cristais e non cortarme." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + got /* Gothic */, + GLASS, + "𐌼𐌰𐌲 𐌲𐌻𐌴𐍃 𐌹̈𐍄𐌰𐌽, 𐌽𐌹 𐌼𐌹𐍃 𐍅𐌿 𐌽𐌳𐌰𐌽 𐌱𐍂𐌹𐌲𐌲𐌹𐌸." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + gu /* Gujarati */, + GLASS, + "હું કાચ ખાઇ શકુ છુ અને તેનાથી મને દર્દ નથી થતુ." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + gv /* Manx Gaelic */, + GLASS, + "Foddym gee glonney agh cha jean eh gortaghey mee." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + haw /* Hawaiian */, + GLASS, + "Hiki iaʻu ke ʻai i ke aniani; ʻaʻole nō lā au e ʻeha." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + he /* Hebrew */, + WP-SFD, + "דג סקרן שט לו בים זך אך לפתע פגש חבורה נחמדה שצצה כך." + /* A curious fish sailed a clear sea, and suddenly found nice company that just popped up. */ + ) +LANGUAGE( + hi /* Hindi */, + MISC /* G Karunakar (#549532) */, + "नहीं नजर किसी की बुरी नहीं किसी का मुँह काला जो करे सो उपर वाला" + /* its not in the sight or the face, but its all in god's grace. */ + ) +LANGUAGE( + hr /* Croatian */, + MISC, + "Deblji krojač: zgužvah smeđ filc u tanjušni džepić." + /* A fatter taylor: I’ve crumpled a brown felt in a slim pocket. */ + ) +LANGUAGE( + hu /* Hungarian */, + WP-SFD, + "Egy hűtlen vejét fülöncsípő, dühös mexikói úr Wesselényinél mázol Quitóban." + /* An angry Mexican man, who caught his faithless son-in-law, is painting Wesselényi's house in Quito. */ + ) +LANGUAGE( + hy /* Armenian */, + GLASS, + "Կրնամ ապակի ուտել և ինծի անհանգիստ չըներ։" + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + is /* Icelandic */, + WP-PANG, + "Kæmi ný öxi hér ykist þjófum nú bæði víl og ádrepa" + /* If a new axe were here, thieves would feel increasing deterrence and punishment. */ + ) +LANGUAGE( + it /* Italian */, + WP-SFD, + "Ma la volpe, col suo balzo, ha raggiunto il quieto Fido." + /* But the fox, with its jump, reached the calm dog */ + ) +LANGUAGE( + ja /* Japanese */, + KERMIT, + "いろはにほへと ちりぬるを 色は匂へど 散りぬるを" + ) +LANGUAGE( + jam /* Jamaican Creole English */, + KERMIT, + "Chruu, a kwik di kwik brong fox a jomp huova di liezi daag de, yu no siit?" + ) +LANGUAGE( + jbo /* Lojban */, + WP-PANG, + ".o'i mu xagji sofybakni cu zvati le purdi" + /* Watch out, five hungry Soviet-cows are in the garden! */ + ) +LANGUAGE( + jv /* Javanese */, + GLASS, + "Aku isa mangan beling tanpa lara." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + ka /* Georgian */, + GLASS, + "მინას ვჭამ და არა მტკივა." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + km /* Khmer */, + GLASS, + "ខ្ញុំអាចញុំកញ្ចក់បាន ដោយគ្មានបញ្ហារ" + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + kn /* Kannada */, + GLASS, + "ನಾನು ಗಾಜನ್ನು ತಿನ್ನಬಲ್ಲೆ ಮತ್ತು ಅದರಿಂದ ನನಗೆ ನೋವಾಗುವುದಿಲ್ಲ." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + ko /* Korean */, + WP-SFD, + "다람쥐 헌 쳇바퀴에 타고파" + /* I Wanna ride on the chipmunk's old hamster wheel. */ + ) +LANGUAGE( + kw /* Cornish */, + GLASS, + "Mý a yl dybry gwéder hag éf ny wra ow ankenya." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + la /* Latin */, + WP-PANG, + "Sic surgens, dux, zelotypos quam karus haberis" + ) +LANGUAGE( + lo /* Lao */, + GLASS, + "ຂອ້ຍກິນແກ້ວໄດ້ໂດຍທີ່ມັນບໍ່ໄດ້ເຮັດໃຫ້ຂອ້ຍເຈັບ" + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + lt /* Lithuanian */, + WP-PANG, + "Įlinkdama fechtuotojo špaga sublykčiojusi pragręžė apvalų arbūzą." + /* Incurving fencer sword sparkled and perforated a round watermelon. */ + ) +LANGUAGE( + lv /* Latvian */, + WP-SFD, + "Sarkanās jūrascūciņas peld pa jūru." + /* Red seapigs swim in the sea. */ + ) +LANGUAGE( + map /* Marquesan */, + GLASS, + "E koʻana e kai i te karahi, mea ʻā, ʻaʻe hauhau." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + mk /* Macedonian */, + GLASS, + "Можам да јадам стакло, а не ме штета." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + ml /* Malayalam */, + GLASS, + "വേദനയില്ലാതെ കുപ്പിചില്ലു് എനിയ്ക്കു് കഴിയ്ക്കാം." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + mn /* Mongolian */, + GLASS, + "ᠪᠢ ᠰᠢᠯᠢ ᠢᠳᠡᠶᠦ ᠴᠢᠳᠠᠨᠠ ᠂ ᠨᠠᠳᠤᠷ ᠬᠣᠤᠷᠠᠳᠠᠢ ᠪᠢᠰᠢ" + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + mr /* Marathi */, + GLASS, + "मी काच खाऊ शकतो, मला ते दुखत नाही." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + ms /* Malay */, + GLASS, + "Saya boleh makan kaca dan ia tidak mencederakan saya." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + my /* Burmese */, + WP, + "ဘာသာပြန်နှင့် စာပေပြုစုရေး ကော်မရှင်" + /* Literary and Translation Commission */ + ) +LANGUAGE( + nap /* Neapolitan */, + GLASS, + "M' pozz magna' o'vetr, e nun m' fa mal." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + nb /* Norwegian Bokmål */, + GSPECI, + "Vår sære Zulu fra badeøya spilte jo whist og quickstep i min taxi." + ) +LANGUAGE( + nl /* Dutch */, + WP-SFD, + "Pa's wijze lynx bezag vroom het fikse aquaduct." + /* Dad's wise lynx piously regarded the substantial aqueduct. */ + ) +LANGUAGE( + nn /* Norwegian Nynorsk */, + GLASS, + "Eg kan eta glas utan å skada meg." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + no /* Norwegian Bokmål */, + GSPECI, + "Vår sære Zulu fra badeøya spilte jo whist og quickstep i min taxi." + ) +LANGUAGE( + nv /* Navajo */, + GLASS, + "Tsésǫʼ yishą́ągo bííníshghah dóó doo shił neezgai da." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + oc /* Occitan */, + GLASS, + "Pòdi manjar de veire, me nafrariá pas." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + or /* Oriya */, + GLASS, + "ମୁଁ କାଚ ଖାଇପାରେ ଏବଂ ତାହା ମୋର କ୍ଷତି କରିନଥାଏ।." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + pa /* Punjabi */, + GLASS, + "ਮੈਂ ਗਲਾਸ ਖਾ ਸਕਦਾ ਹਾਂ ਅਤੇ ਇਸ ਨਾਲ ਮੈਨੂੰ ਕੋਈ ਤਕਲੀਫ ਨਹੀਂ." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + pcd /* Picard */, + GLASS, + "Ch'peux mingi du verre, cha m'foé mie n'ma." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + pl /* Polish */, + WP-SFD, + "Pchnąć w tę łódź jeża lub ośm skrzyń fig." + /* Push into this boat a hedgehog or eight boxes of figs. */ + ) +LANGUAGE( + pt /* Portuguese */, + WP-SFD, + "Vejam a bruxa da raposa Salta-Pocinhas e o cão feliz que dorme regalado." + /* Watch the witch of the Jump-Puddles fox and the happy dog that sleeps delighted. */ + ) +LANGUAGE( + pt-br /* Brazilian Portuguese */, + WP-PANG, + "À noite, vovô Kowalsky vê o ímã cair no pé do pingüim queixoso e vovó põe açúcar no chá de tâmaras do jabuti feliz." + /* At night, grandpa Kowalsky sees the magnet falling in the complaining penguin's foot and grandma puts sugar in the happy tortoise's date tea.*/ + ) +LANGUAGE( + ro /* Romanian */, + MISC /* Misu Moldovan (#552993) */, + "Fumegând hipnotic sașiul azvârle mreje în bălți." + /* Hypnotically smoking, the cross-eyed man throws fishing nets into ponds. */ + ) +LANGUAGE( + ru /* Russian */, + WP-PANG, + "В чащах юга жил бы цитрус? Да, но фальшивый экземпляр!" + /* Would a citrus live in the bushes of the south? Yes, but only a fake one! */ + ) +LANGUAGE( + sa /* Sanskrit */, + GLASS, + "काचं शक्नोम्यत्तुम् । नोपहिनस्ति माम् ॥" + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + scn /* Sicilian */, + GLASS, + "Puotsu mangiari u vitru, nun mi fa mali." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + si /* Sinhalese */, + WP, + "මනොපුබ්බඞ්ගමා ධම්මා, මනොසෙට්ඨා මනොමයා; මනසා චෙ පදුට්ඨෙන, භාසති වා කරොති වා; තතො නං දුක්ඛමන්වෙති, චක්කංව වහතො පදං." + ) +LANGUAGE( + sk /* Slovak */, + KERMIT, + "Starý kôň na hŕbe kníh žuje tíško povädnuté ruže, na stĺpe sa ďateľ učí kvákať novú ódu o živote." + ) +LANGUAGE( + sl /* Slovenian */, + WP-PANG, + "Šerif bo za vajo spet kuhal domače žgance." + /* For an exercise, sheriff will again make home-made mush. */ + ) +LANGUAGE( + sq /* Albanian */, + GLASS, + "Unë mund të ha qelq dhe nuk më gjen gjë." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + sr /* Serbian (Cyrillic) */, + WP-SFD, + "Чешће цeђење мрeжастим џаком побољшава фертилизацију генских хибрида." + /* More frequent filtering through the reticular bag improves fertilization of genetic hybrids. */ + ) +#if 0 +LANGUAGE( + sr-sr@latin /* Serbian (Latin) */, + WP-SFD, + "Češće ceđenje mrežastim džakom poboljšava fertilizaciju genskih hibrida." + /* More frequent filtering through the reticular bag improves fertilization of genetic hybrids. */ + ) +#endif +LANGUAGE( + sv /* Swedish */, + WP-SFD, + "Flygande bäckasiner söka strax hwila på mjuka tuvor." + /* Flying snipes soon look to rest on soft grass beds. */ + ) +LANGUAGE( + swg /* Swabian */, + GLASS, + "I kå Glas frässa, ond des macht mr nix!" + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + ta /* Tamil */, + GLASS, + "நான் கண்ணாடி சாப்பிடுவேன், அதனால் எனக்கு ஒரு கேடும் வராது." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + te /* Telugu */, + GLASS, + "నేను గాజు తినగలను అయినా నాకు యేమీ కాదు." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + th /* Thai */, + WP-SFD, + "เป็นมนุษย์สุดประเสริฐเลิศคุณค่า - กว่าบรรดาฝูงสัตว์เดรัจฉาน - จงฝ่าฟันพัฒนาวิชาการ อย่าล้างผลาญฤๅเข่นฆ่าบีฑาใคร - ไม่ถือโทษโกรธแช่งซัดฮึดฮัดด่า - หัดอภัยเหมือนกีฬาอัชฌาสัย - ปฏิบัติประพฤติกฎกำหนดใจ - พูดจาให้จ๊ะ ๆ จ๋า ๆ น่าฟังเอยฯ" + /* Being a man is worthy - Beyond senseless animal - Begin educate thyself - Begone from killing and trouble - Bear not thy grudge, damn and, curse - Bestow forgiving and sporting - Befit with rules - Benign speech speak thou */ + ) +LANGUAGE( + tl /* Tagalog */, + GLASS, + "Kaya kong kumain nang bubog at hindi ako masaktan." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + tr /* Turkish */, + WP-PANG, + "Pijamalı hasta yağız şoföre çabucak güvendi." + /* The patient in pajamas trusted the swarthy driver quickly. */ + ) +LANGUAGE( + tw /* Twi */, + GLASS, + "Metumi awe tumpan, ɜnyɜ me hwee." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + uk /* Ukrainian */, + WP-PANG, + "Чуєш їх, доцю, га? Кумедна ж ти, прощайся без ґольфів!" + /* Daughter, do you hear them, eh? Oh, you are funny! Say good-bye without knee-length socks. */ + ) +LANGUAGE( + ur /* Urdu */, + GLASS, + "میں کانچ کھا سکتا ہوں اور مجھے تکلیف نہیں ہوتی ۔" + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + vec /* Venetian */, + GLASS, + "Mi posso magnare el vetro, no'l me fa mae." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + vi /* Vietnamese */, + GSPECI, + "Con sói nâu nhảy qua con chó lười." + ) +LANGUAGE( + wa /* Walloon */, + GLASS, + "Dji pou magnî do vêre, çoula m' freut nén må." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + yi /* Yiddish */, + GLASS, + "איך קען עסן גלאָז און עס טוט מיר נישט װײ." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + yo /* Yoruba */, + GLASS, + "Mo lè je̩ dígí, kò ní pa mí lára." + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + zh-cn /* Chinese Simplified */, + GLASS, + "我能吞下玻璃而不伤身体。" + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + zh-mo /* Chinese Traditional */, + GLASS, + "我能吞下玻璃而不傷身體。" + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + zh-sg /* Chinese Simplified */, + GLASS, + "我能吞下玻璃而不伤身体。" + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + zh-tw /* Chinese Traditional */, + GLASS, + "我能吞下玻璃而不傷身體。" + /* I can eat glass and it doesn't hurt me. */ + ) +LANGUAGE( + zlm /* Malay */, + GLASS, + "Saya boleh makan kaca dan ia tidak mencederakan saya." + /* I can eat glass and it doesn't hurt me. */ + ) diff --git a/pango2/pango-language-set-private.h b/pango2/pango-language-set-private.h new file mode 100644 index 00000000..53aac1cc --- /dev/null +++ b/pango2/pango-language-set-private.h @@ -0,0 +1,44 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-language.h" + + +#define PANGO2_TYPE_LANGUAGE_SET (pango2_language_set_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (Pango2LanguageSet, pango2_language_set, PANGO2, LANGUAGE_SET, GObject) + +struct _Pango2LanguageSetClass +{ + GObjectClass parent_class; + + gboolean (* matches_language) (Pango2LanguageSet *set, + Pango2Language *language); + + Pango2Language ** (* get_languages) (Pango2LanguageSet *set); +}; + +gboolean pango2_language_set_matches_language (Pango2LanguageSet *set, + Pango2Language *language); + +Pango2Language ** pango2_language_set_get_languages (Pango2LanguageSet *set); + +char * pango2_language_set_to_string (Pango2LanguageSet *set); diff --git a/pango2/pango-language-set-simple-private.h b/pango2/pango-language-set-simple-private.h new file mode 100644 index 00000000..c473d01e --- /dev/null +++ b/pango2/pango-language-set-simple-private.h @@ -0,0 +1,32 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-language-set-private.h" + + +#define PANGO2_TYPE_LANGUAGE_SET_SIMPLE (pango2_language_set_simple_get_type ()) + +G_DECLARE_FINAL_TYPE (Pango2LanguageSetSimple, pango2_language_set_simple, PANGO2, LANGUAGE_SET_SIMPLE, Pango2LanguageSet) + +Pango2LanguageSetSimple *pango2_language_set_simple_new (void); + +void pango2_language_set_simple_add_language (Pango2LanguageSetSimple *self, + Pango2Language *language); diff --git a/pango2/pango-language-set-simple.c b/pango2/pango-language-set-simple.c new file mode 100644 index 00000000..d117a0c1 --- /dev/null +++ b/pango2/pango-language-set-simple.c @@ -0,0 +1,125 @@ +/* Pango2 + * + * Copyright (C) 2021 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include <gio/gio.h> + +#include "pango-language-set-simple-private.h" + +struct _Pango2LanguageSetSimple +{ + Pango2LanguageSet parent_instance; + + GHashTable *languages; + Pango2Language **list; +}; + +G_DEFINE_TYPE (Pango2LanguageSetSimple, pango2_language_set_simple, PANGO2_TYPE_LANGUAGE_SET) + +static void +pango2_language_set_simple_init (Pango2LanguageSetSimple *self) +{ + self->languages = g_hash_table_new (g_direct_hash, g_direct_equal); +} + +static void +pango2_language_set_simple_finalize (GObject *object) +{ + Pango2LanguageSetSimple *self = PANGO2_LANGUAGE_SET_SIMPLE (object); + + g_hash_table_unref (self->languages); + g_free (self->list); + + G_OBJECT_CLASS (pango2_language_set_simple_parent_class)->finalize (object); +} + +static gboolean +pango2_language_set_simple_matches_language (Pango2LanguageSet *set, + Pango2Language *language) +{ + Pango2LanguageSetSimple *self = PANGO2_LANGUAGE_SET_SIMPLE (set); + const char *s; + + if (g_hash_table_contains (self->languages, language)) + return TRUE; + + if (language == pango2_language_from_string ("c")) + return TRUE; + + s = pango2_language_to_string (language); + if (strchr (s, '-')) + { + char buf[10]; + + for (int i = 0; i < 10; i++) + { + buf[i] = s[i]; + if (buf[i] == '-') + { + buf[i] = '\0'; + break; + } + } + buf[9] = '\0'; + + if (g_hash_table_contains (self->languages, pango2_language_from_string (buf))) + return TRUE; + } + + return FALSE; +} + +static Pango2Language ** +pango2_language_set_simple_get_languages (Pango2LanguageSet *set) +{ + Pango2LanguageSetSimple *self = PANGO2_LANGUAGE_SET_SIMPLE (set); + + if (!self->list) + self->list = (Pango2Language **) g_hash_table_get_keys_as_array (self->languages, NULL); + + return self->list; +} + +static void +pango2_language_set_simple_class_init (Pango2LanguageSetSimpleClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + Pango2LanguageSetClass *language_set_class = PANGO2_LANGUAGE_SET_CLASS (class); + + object_class->finalize = pango2_language_set_simple_finalize; + language_set_class->matches_language = pango2_language_set_simple_matches_language; + language_set_class->get_languages = pango2_language_set_simple_get_languages; +} + +Pango2LanguageSetSimple * +pango2_language_set_simple_new (void) +{ + return g_object_new (PANGO2_TYPE_LANGUAGE_SET_SIMPLE, NULL); +} + +void +pango2_language_set_simple_add_language (Pango2LanguageSetSimple *self, + Pango2Language *language) +{ + g_return_if_fail (self->list == NULL); + + g_hash_table_add (self->languages, language); +} diff --git a/pango2/pango-language-set.c b/pango2/pango-language-set.c new file mode 100644 index 00000000..a96ba929 --- /dev/null +++ b/pango2/pango-language-set.c @@ -0,0 +1,87 @@ +/* Pango2 + * + * Copyright (C) 2021 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include <gio/gio.h> + +#include "pango-language-set-private.h" + + +G_DEFINE_ABSTRACT_TYPE (Pango2LanguageSet, pango2_language_set, G_TYPE_OBJECT) + +static void +pango2_language_set_init (Pango2LanguageSet *set) +{ +} + +static gboolean +pango2_language_set_default_matches_language (Pango2LanguageSet *set, + Pango2Language *language) +{ + return FALSE; +} + +static Pango2Language ** +pango2_language_set_default_get_languages (Pango2LanguageSet *set) +{ + return NULL; +} + +static void +pango2_language_set_class_init (Pango2LanguageSetClass *class) +{ + Pango2LanguageSetClass *language_set_class = PANGO2_LANGUAGE_SET_CLASS (class); + + language_set_class->matches_language = pango2_language_set_default_matches_language; + language_set_class->get_languages = pango2_language_set_default_get_languages; +} + +gboolean +pango2_language_set_matches_language (Pango2LanguageSet *set, + Pango2Language *language) +{ + return PANGO2_LANGUAGE_SET_GET_CLASS (set)->matches_language (set, language); +} + +Pango2Language ** +pango2_language_set_get_languages (Pango2LanguageSet *set) +{ + return PANGO2_LANGUAGE_SET_GET_CLASS (set)->get_languages (set); +} + +char * +pango2_language_set_to_string (Pango2LanguageSet *set) +{ + Pango2Language **languages; + GString *s; + + s = g_string_new (""); + + languages = pango2_language_set_get_languages (set); + for (int i = 0; languages[i]; i++) + { + if (s->len > 0) + g_string_append (s, "|"); + g_string_append (s, pango2_language_to_string (languages[i])); + } + + return g_string_free (s, FALSE); +} diff --git a/pango2/pango-language.c b/pango2/pango-language.c new file mode 100644 index 00000000..71af5a83 --- /dev/null +++ b/pango2/pango-language.c @@ -0,0 +1,1029 @@ +/* Pango2 + * pango-language.c: Language handling routines + * + * Copyright (C) 2000 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include <errno.h> +#include <string.h> +#include <stdlib.h> +#include <math.h> +#include <locale.h> + +#include "pango-language.h" +#include "pango-impl-utils.h" + +#ifdef HAVE_CORE_TEXT +#include <CoreFoundation/CoreFoundation.h> +#endif /* HAVE_CORE_TEXT */ + + +/* We embed a private struct right *before* a where a Pango2Language * + * points to. + */ + +typedef struct { + gconstpointer lang_info; + gconstpointer script_for_lang; + + int magic; /* Used for verification */ +} Pango2LanguagePrivate; + +#define PANGO2_LANGUAGE_PRIVATE_MAGIC 0x0BE4DAD0 + +static void +pango2_language_private_init (Pango2LanguagePrivate *priv) +{ + priv->magic = PANGO2_LANGUAGE_PRIVATE_MAGIC; + + priv->lang_info = (gconstpointer) -1; + priv->script_for_lang = (gconstpointer) -1; +} + +static Pango2LanguagePrivate * pango2_language_get_private (Pango2Language *language) G_GNUC_CONST; + +static Pango2LanguagePrivate * +pango2_language_get_private (Pango2Language *language) +{ + Pango2LanguagePrivate *priv; + + if (!language) + return NULL; + + priv = (Pango2LanguagePrivate *)(void *)((char *)language - sizeof (Pango2LanguagePrivate)); + + if (G_UNLIKELY (priv->magic != PANGO2_LANGUAGE_PRIVATE_MAGIC)) + { + g_critical ("Invalid Pango2Language. Did you pass in a straight string instead of calling pango2_language_from_string()?"); + return NULL; + } + + return priv; +} + + + +#define LANGUAGE_SEPARATORS ";:, \t" + +static const char canon_map[256] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '-', 0, 0, + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 0, 0, 0, 0, 0, 0, + '-', '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', 0, 0, 0, 0, '-', + 0, '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', 0, 0, 0, 0, 0 +}; + +static gboolean +lang_equal (gconstpointer v1, + gconstpointer v2) +{ + const guchar *p1 = v1; + const guchar *p2 = v2; + + while (canon_map[*p1] && canon_map[*p1] == canon_map[*p2]) + { + p1++, p2++; + } + + return (canon_map[*p1] == canon_map[*p2]); +} + +static guint +lang_hash (gconstpointer key) +{ + const guchar *p = key; + guint h = 0; + while (canon_map[*p]) + { + h = (h << 5) - h + canon_map[*p]; + p++; + } + + return h; +} + +static Pango2Language * +pango2_language_copy (Pango2Language *language) +{ + return language; /* language tags are const */ +} + +static void +pango2_language_free (Pango2Language *language G_GNUC_UNUSED) +{ + return; /* nothing */ +} + +/** + * Pango2Language: + * + * The `Pango2Language` structure is used to + * represent a language. + * + * `Pango2Language` pointers can be efficiently + * copied and compared with each other. + */ +G_DEFINE_BOXED_TYPE (Pango2Language, pango2_language, + pango2_language_copy, + pango2_language_free); + +/** + * _pango2_get_lc_ctype: + * + * Return the Unix-style locale string for the language currently in + * effect. On Unix systems, this is the return value from + * `setlocale (LC_CTYPE, NULL)`, and the user can affect this through + * the environment variables LC_ALL, LC_CTYPE or LANG (checked + * in that order). The locale strings typically is in the form lang_COUNTRY, + * where lang is an ISO-639 language code, and COUNTRY is an ISO-3166 country + * code. For instance, sv_FI for Swedish as written in Finland or pt_BR for + * Portuguese as written in Brazil. + * + * On Windows, the C library doesn't use any such environment + * variables, and setting them won't affect the behavior of functions + * like ctime(). The user sets the locale through the Regional Options + * in the Control Panel. The C library (in the setlocale() function) + * does not use country and language codes, but country and language + * names spelled out in English. + * However, this function does check the above environment + * variables, and does return a Unix-style locale string based on + * either said environment variables or the thread's current locale. + * + * Return value: a dynamically allocated string, free with [GLib.free] + */ +static char * +_pango2_get_lc_ctype (void) +{ +#ifdef G_OS_WIN32 + /* Somebody might try to set the locale for this process using the + * LANG or LC_ environment variables. The Microsoft C library + * doesn't know anything about them. You set the locale in the + * Control Panel. Setting these env vars won't have any affect on + * locale-dependent C library functions like ctime(). But just for + * kicks, do obey LC_ALL, LC_CTYPE and LANG in Pango2. (This also makes + * it easier to test GTK and Pango2 in various default languages, you + * don't have to clickety-click in the Control Panel, you can simply + * start the program with LC_ALL=something on the command line.) + */ + + char *p; + + p = getenv ("LC_ALL"); + if (p != NULL) + return g_strdup (p); + + p = getenv ("LC_CTYPE"); + if (p != NULL) + return g_strdup (p); + + p = getenv ("LANG"); + if (p != NULL) + return g_strdup (p); + + return g_win32_getlocale (); +#elif defined(HAVE_CORE_TEXT) + CFArrayRef languages; + CFStringRef language; + char ret[16]; + char *p; + + /* Take the same approach as done for Windows above. First we check + * if somebody tried to set the locale through environment variables. + */ + p = getenv ("LC_ALL"); + if (p != NULL) + return g_strdup (p); + + p = getenv ("LC_CTYPE"); + if (p != NULL) + return g_strdup (p); + + p = getenv ("LANG"); + if (p != NULL) + return g_strdup (p); + + /* If the environment variables are not set, determine the locale + * through the platform-native API. + */ + languages = CFLocaleCopyPreferredLanguages (); + language = CFArrayGetValueAtIndex (languages, 0); + + if (!CFStringGetCString (language, ret, 16, kCFStringEncodingUTF8)) + { + CFRelease (languages); + return g_strdup (setlocale (LC_CTYPE, NULL)); + } + + CFRelease (languages); + + return g_strdup (ret); +#else + { + char *lc_ctype = setlocale (LC_CTYPE, NULL); + + if (lc_ctype) + return g_strdup (lc_ctype); + else + return g_strdup ("C"); + } +#endif +} + +/** + * pango2_language_get_default: + * + * Returns the `Pango2Language` for the current locale of the process. + * + * On Unix systems, this is the return value is derived from + * `setlocale (LC_CTYPE, NULL)`, and the user can + * affect this through the environment variables LC_ALL, LC_CTYPE or + * LANG (checked in that order). The locale string typically is in + * the form lang_COUNTRY, where lang is an ISO-639 language code, and + * COUNTRY is an ISO-3166 country code. For instance, sv_FI for + * Swedish as written in Finland or pt_BR for Portuguese as written in + * Brazil. + * + * On Windows, the C library does not use any such environment + * variables, and setting them won't affect the behavior of functions + * like ctime(). The user sets the locale through the Regional Options + * in the Control Panel. The C library (in the setlocale() function) + * does not use country and language codes, but country and language + * names spelled out in English. + * However, this function does check the above environment + * variables, and does return a Unix-style locale string based on + * either said environment variables or the thread's current locale. + * + * Your application should call `setlocale(LC_ALL, "")` for the user + * settings to take effect. GTK does this in its initialization + * functions automatically (by calling gtk_set_locale()). + * See the setlocale() manpage for more details. + * + * Note that the default language can change over the life of an application. + * + * Also note that this function will not do the right thing if you + * use per-thread locales with uselocale(). In that case, you should + * just call [func@Pango2.Language.from_string] yourself. + * + * Return value: (transfer none): the default language as a `Pango2Language` + **/ +Pango2Language * +pango2_language_get_default (void) +{ + static Pango2Language *result = NULL; /* MT-safe */ + + if (g_once_init_enter (&result)) + { + char *lc_ctype; + Pango2Language *lang; + + lc_ctype = _pango2_get_lc_ctype (); + lang = pango2_language_from_string (lc_ctype); + g_free (lc_ctype); + + g_once_init_leave (&result, lang); + } + + return result; +} + +/** + * pango2_language_from_string: + * @language: (nullable): a string representing a language tag + * + * Convert a language tag to a `Pango2Language`. + * + * The language tag must be in a RFC-3066 format. `Pango2Language` pointers + * can be efficiently copied (copy the pointer) and compared with other + * language tags (compare the pointer.) + * + * This function first canonicalizes the string by converting it to + * lowercase, mapping '_' to '-', and stripping all characters other + * than letters and '-'. + * + * Use [func@Pango2.Language.get_default] if you want to get the + * `Pango2Language` for the current locale of the process. + * + * Return value: (transfer none) (nullable): a `Pango2Language` + */ +Pango2Language * +pango2_language_from_string (const char *language) +{ + G_LOCK_DEFINE_STATIC (lang_from_string); + static GHashTable *hash = NULL; /* MT-safe */ + Pango2LanguagePrivate *priv; + char *result; + int len; + char *p; + + if (language == NULL) + return NULL; + + G_LOCK (lang_from_string); + + if (G_UNLIKELY (!hash)) + hash = g_hash_table_new (lang_hash, lang_equal); + else + { + result = g_hash_table_lookup (hash, language); + if (result) + goto out; + } + + len = strlen (language); + priv = g_malloc0 (sizeof (Pango2LanguagePrivate) + len + 1); + g_assert (priv); + + result = (char *)priv; + result += sizeof (Pango2LanguagePrivate); + + pango2_language_private_init (priv); + + p = result; + while ((*(p++) = canon_map[*(guchar *)language++])) + ; + + g_hash_table_insert (hash, result, result); + +out: + G_UNLOCK (lang_from_string); + + return (Pango2Language *)result; +} + +/** + * pango2_language_to_string: + * @language: a language tag. + * + * Gets the RFC-3066 format string representing the given language tag. + * + * Returns (transfer none): a string representing the language tag + */ +const char * +(pango2_language_to_string) (Pango2Language *language) +{ + return pango2_language_to_string (language); +} + +/** + * pango2_language_matches: + * @language: (nullable): a language tag (see [func@Pango2.Language.from_string]), + * %NULL is allowed and matches nothing but '*' + * @range_list: a list of language ranges, separated by ';', ':', + * ',', or space characters. + * Each element must either be '*', or a RFC 3066 language range + * canonicalized as by [func@Pango2.Language.from_string] + * + * Checks if a language tag matches one of the elements in a list of + * language ranges. + * + * A language tag is considered to match a range in the list if the + * range is '*', the range is exactly the tag, or the range is a prefix + * of the tag, and the character after it in the tag is '-'. + * + * Return value: %TRUE if a match was found + */ +gboolean +pango2_language_matches (Pango2Language *language, + const char *range_list) +{ + const char *lang_str = pango2_language_to_string (language); + const char *p = range_list; + gboolean done = FALSE; + + while (!done) + { + const char *end = strpbrk (p, LANGUAGE_SEPARATORS); + if (!end) + { + end = p + strlen (p); + done = TRUE; + } + + if (strncmp (p, "*", 1) == 0 || + (lang_str && strncmp (lang_str, p, end - p) == 0 && + (lang_str[end - p] == '\0' || lang_str[end - p] == '-'))) + return TRUE; + + if (!done) + p = end + 1; + } + + return FALSE; +} + +static int +lang_compare_first_component (gconstpointer pa, + gconstpointer pb) +{ + const char *a = pa, *b = pb; + unsigned int da, db; + const char *p; + + p = strstr (a, "-"); + da = p ? (unsigned int) (p - a) : strlen (a); + + p = strstr (b, "-"); + db = p ? (unsigned int) (p - b) : strlen (b); + + return strncmp (a, b, MAX (da, db)); +} + +/* Finds the best record for @language in an array of records. + * Each record should start with the string representation of the language + * code for the record (embedded, not a pointer), and the records must be + * sorted on language code. + */ +static gconstpointer +find_best_lang_match (Pango2Language *language, + gconstpointer records, + guint num_records, + guint record_size) +{ + const char *lang_str; + const char *record, *start, *end; + + if (language == NULL) + return NULL; + + lang_str = pango2_language_to_string (language); + + record = bsearch (lang_str, + records, num_records, record_size, + lang_compare_first_component); + if (!record) + return NULL; + + start = (const char *) records; + end = start + num_records * record_size; + + /* find the best match among all those that have the same first-component */ + + /* go to the final one matching in the first component */ + while (record < end - record_size && + lang_compare_first_component (lang_str, record + record_size) == 0) + record += record_size; + + /* go back, find which one matches completely */ + while (start <= record && + lang_compare_first_component (lang_str, record) == 0) + { + if (pango2_language_matches (language, record)) + return record; + + record -= record_size; + } + + return NULL; +} + +static gconstpointer +find_best_lang_match_cached (Pango2Language *language, + gconstpointer *cache, + gconstpointer records, + guint num_records, + guint record_size) +{ + gconstpointer result; + + if (G_LIKELY (cache && *cache != (gconstpointer) -1)) + return *cache; + + result = find_best_lang_match (language, + records, + num_records, + record_size); + + if (cache) + *cache = result; + + return result; +} + +#define FIND_BEST_LANG_MATCH_CACHED(language, cache_key, records) \ + find_best_lang_match_cached ((language), \ + pango2_language_get_private (language) ? \ + &(pango2_language_get_private (language)->cache_key) : NULL, \ + records, \ + G_N_ELEMENTS (records), \ + sizeof (*records)); + +typedef struct { + char lang[6]; + guint16 offset; +} LangInfo; + +/* Pure black magic, based on appendix of dsohowto.pdf */ +#define POOLSTRFIELD(line) POOLSTRFIELD1(line) +#define POOLSTRFIELD1(line) str##line +struct _LangPoolStruct { + char str0[1]; +#define LANGUAGE(id, source, sample) char POOLSTRFIELD(__LINE__)[sizeof(sample)]; +#include "pango-language-sample-table.h" +#undef LANGUAGE +}; + +static const union _LangPool { + struct _LangPoolStruct lang_pool_struct; + const char str[1]; +} lang_pool = { { + "", +#define LANGUAGE(id, source, sample) sample, +#include "pango-language-sample-table.h" +#undef LANGUAGE +} }; +static const LangInfo lang_texts[] = { +#define LANGUAGE(id, source, sample) {G_STRINGIFY(id), G_STRUCT_OFFSET(struct _LangPoolStruct, POOLSTRFIELD(__LINE__))}, +#include "pango-language-sample-table.h" +#undef LANGUAGE + /* One extra entry with no final comma, to make it C89-happy */ + {"~~", 0} +}; + +/** + * pango2_language_get_sample_string: + * @language: (nullable): a `Pango2Language` + * + * Get a string that is representative of the characters needed to + * render a particular language. + * + * The sample text may be a pangram, but is not necessarily. It is chosen + * to be demonstrative of normal text in the language, as well as exposing + * font feature requirements unique to the language. It is suitable for use + * as sample text in a font selection dialog. + * + * If @language is %NULL, the default language as found by + * [func@Pango2.Language.get_default] is used. + * + * If Pango2 does not have a sample string for @language, the classic + * "The quick brown fox..." is returned. This can be detected by + * comparing the returned pointer value to that returned for (non-existent) + * language code "xx". That is, compare to: + * + * ``` + * pango2_language_get_sample_string (pango2_language_from_string ("xx")) + * ``` + * + * Return value: (transfer none): the sample string + */ +const char * +pango2_language_get_sample_string (Pango2Language *language) +{ + const LangInfo *lang_info; + + if (!language) + language = pango2_language_get_default (); + + lang_info = FIND_BEST_LANG_MATCH_CACHED (language, lang_info, lang_texts); + + if (lang_info) + return lang_pool.str + lang_info->offset; + + return "The quick brown fox jumps over the lazy dog."; +} + + + + +/* + * From language to script + */ + + +#include "pango-script-lang-table.h" + +/** + * pango2_language_get_scripts: + * @language: (nullable): a `Pango2Language` + * @num_scripts: (out caller-allocates) (optional): location to + * return number of scripts + * + * Determines the scripts used to to write @language. + * + * If nothing is known about the language tag @language, + * or if @language is %NULL, then %NULL is returned. + * The list of scripts returned starts with the script that the + * language uses most and continues to the one it uses least. + * + * The value @num_script points at will be set to the number + * of scripts in the returned array (or zero if %NULL is returned). + * + * Most languages use only one script for writing, but there are + * some that use two (Latin and Cyrillic for example), and a few + * use three (Japanese for example). Applications should not make + * any assumptions on the maximum number of scripts returned + * though, except that it is positive if the return value is not + * %NULL, and it is a small number. + * + * The [method@Pango2.Language.includes_script] function uses this + * function internally. + * + * Return value: (transfer none) (array length=num_scripts) (nullable): + * An array of `GUnicodeScript` values, with the number of entries in + * the array stored in @num_scripts, or %NULL if Pango2 does not have + * any information about this particular language tag (also the case + * if @language is %NULL). + */ +const GUnicodeScript * +pango2_language_get_scripts (Pango2Language *language, + int *num_scripts) +{ + const Pango2ScriptForLang *script_for_lang; + unsigned int j; + + script_for_lang = FIND_BEST_LANG_MATCH_CACHED (language, + script_for_lang, + pango2_script_for_lang); + + if (!script_for_lang || script_for_lang->scripts[0] == 0) + { + if (num_scripts) + *num_scripts = 0; + + return NULL; + } + + if (num_scripts) + { + for (j = 0; j < G_N_ELEMENTS (script_for_lang->scripts); j++) + if (script_for_lang->scripts[j] == 0) + break; + + g_assert (j > 0); + + *num_scripts = j; + } + + return (const GUnicodeScript *) script_for_lang->scripts; +} + +/** + * pango2_language_includes_script: + * @language: (nullable): a `Pango2Language` + * @script: a `GUnicodeScript` + * + * Determines if @script is one of the scripts used to + * write @language. + * + * The returned value is conservative; if nothing is known about + * the language tag @language, %TRUE will be returned, since, as + * far as Pango2 knows, @script might be used to write @language. + * + * This routine is used in Pango2's itemization process when + * determining if a supplied language tag is relevant to + * a particular section of text. It probably is not useful + * for applications in most circumstances. + * + * This function uses [method@Pango2.Language.get_scripts] internally. + * + * Return value: %TRUE if @script is one of the scripts used + * to write @language or if nothing is known about @language + * (including the case that @language is %NULL), %FALSE otherwise. + */ +gboolean +pango2_language_includes_script (Pango2Language *language, + GUnicodeScript script) +{ + const GUnicodeScript *scripts; + int num_scripts, j; + +/* copied from the one in pango-script.c */ +#define REAL_SCRIPT(script) \ + ((script) > G_UNICODE_SCRIPT_INHERITED && (script) != G_UNICODE_SCRIPT_UNKNOWN) + + if (!REAL_SCRIPT (script)) + return TRUE; + +#undef REAL_SCRIPT + + scripts = pango2_language_get_scripts (language, &num_scripts); + if (!scripts) + return TRUE; + + for (j = 0; j < num_scripts; j++) + if (scripts[j] == script) + return TRUE; + + return FALSE; +} + + + + +/* + * From script to language + */ + + +static Pango2Language ** +parse_default_languages (void) +{ + char *p, *p_copy; + gboolean done = FALSE; + GPtrArray *langs; + + p = getenv ("PANGO2_LANGUAGE"); + + if (p == NULL) + p = getenv ("LANGUAGE"); + + if (p == NULL) + return NULL; + + p_copy = p = g_strdup (p); + + langs = g_ptr_array_new (); + + while (!done) + { + char *end = strpbrk (p, LANGUAGE_SEPARATORS); + if (!end) + { + end = p + strlen (p); + done = TRUE; + } + else + *end = '\0'; + + /* skip empty languages, and skip the language 'C' */ + if (p != end && !(p + 1 == end && *p == 'C')) + { + Pango2Language *l = pango2_language_from_string (p); + + g_ptr_array_add (langs, l); + } + + if (!done) + p = end + 1; + } + + g_ptr_array_add (langs, NULL); + + g_free (p_copy); + + return (Pango2Language **) g_ptr_array_free (langs, FALSE); +} + +G_LOCK_DEFINE_STATIC (languages); +static gboolean initialized = FALSE; /* MT-safe */ +static Pango2Language * const * languages = NULL; /* MT-safe */ +static GHashTable *hash = NULL; /* MT-safe */ + +static Pango2Language * +_pango2_script_get_default_language (GUnicodeScript script) +{ + Pango2Language *result, * const * p; + + G_LOCK (languages); + + if (G_UNLIKELY (!initialized)) + { + languages = parse_default_languages (); + + if (languages) + hash = g_hash_table_new (NULL, NULL); + + initialized = TRUE; + } + + if (!languages) + { + result = NULL; + goto out; + } + + if (g_hash_table_lookup_extended (hash, GINT_TO_POINTER (script), NULL, (gpointer *) (gpointer) &result)) + goto out; + + for (p = languages; *p; p++) + if (pango2_language_includes_script (*p, script)) + break; + result = *p; + + g_hash_table_insert (hash, GINT_TO_POINTER (script), result); + +out: + G_UNLOCK (languages); + + return result; +} + +/** + * pango2_language_get_preferred: + * + * Returns the list of languages that the user prefers. + * + * The list is specified by the `PANGO2_LANGUAGE` or `LANGUAGE` + * environment variables, in order of preference. Note that this + * list does not necessarily include the language returned by + * [func@Pango2.Language.get_default]. + * + * When choosing language-specific resources, such as the sample + * text returned by [method@Pango2.Language.get_sample_string], + * you should first try the default language, followed by the + * languages returned by this function. + * + * Returns: (transfer none) (nullable): a %NULL-terminated array + * of `Pango2Language`* + */ +Pango2Language ** +pango2_language_get_preferred (void) +{ + /* We call this just for its side-effect of initializing languages */ + _pango2_script_get_default_language (G_UNICODE_SCRIPT_COMMON); + + return (Pango2Language **) languages; +} + +/** + * pango2_script_get_sample_language: + * @script: a `GUnicodeScript` + * + * Finds a language tag that is reasonably representative of @script. + * + * The language will usually be the most widely spoken or used language + * written in that script: for instance, the sample language for + * %G_UNICODE_SCRIPT_CYRILLIC is ru (Russian), the sample language for + * %G_UNICODE_SCRIPT_ARABIC is ar. + * + * For some scripts, no sample language will be returned because + * there is no language that is sufficiently representative. The + * best example of this is %G_UNICODE_SCRIPT_HAN, where various different + * variants of written Chinese, Japanese, and Korean all use + * significantly different sets of Han characters and forms + * of shared characters. No sample language can be provided + * for many historical scripts as well. + * + * As of 1.18, this function checks the environment variables + * `PANGO2_LANGUAGE` and `LANGUAGE` (checked in that order) first. + * If one of them is set, it is parsed as a list of language tags + * separated by colons or other separators. This function + * will return the first language in the parsed list that Pango2 + * believes may use @script for writing. This last predicate + * is tested using [method@Pango2.Language.includes_script]. This can + * be used to control Pango2's font selection for non-primary + * languages. For example, a `PANGO2_LANGUAGE` enviroment variable + * set to "en:fa" makes Pango2 choose fonts suitable for Persian (fa) + * instead of Arabic (ar) when a segment of Arabic text is found + * in an otherwise non-Arabic text. The same trick can be used to + * choose a default language for %G_UNICODE_SCRIPT_HAN when setting + * context language is not feasible. + * + * Return value: (nullable): a `Pango2Language` that is representative + * of the script + */ +Pango2Language * +pango2_script_get_sample_language (GUnicodeScript script) +{ + /* Note that in the following, we want + * pango2_language_includes_script() for the sample language + * to include the script, so alternate orthographies + * (Shavian for English, Osmanya for Somali, etc), typically + * have no sample language + */ + static const char sample_languages[][4] = { + "", /* G_UNICODE_SCRIPT_COMMON */ + "", /* G_UNICODE_SCRIPT_INHERITED */ + "ar", /* G_UNICODE_SCRIPT_ARABIC */ + "hy", /* G_UNICODE_SCRIPT_ARMENIAN */ + "bn", /* G_UNICODE_SCRIPT_BENGALI */ + /* Used primarily in Taiwan, but not part of the standard + * zh-tw orthography */ + "", /* G_UNICODE_SCRIPT_BOPOMOFO */ + "chr", /* G_UNICODE_SCRIPT_CHEROKEE */ + "cop", /* G_UNICODE_SCRIPT_COPTIC */ + "ru", /* G_UNICODE_SCRIPT_CYRILLIC */ + /* Deseret was used to write English */ + "", /* G_UNICODE_SCRIPT_DESERET */ + "hi", /* G_UNICODE_SCRIPT_DEVANAGARI */ + "am", /* G_UNICODE_SCRIPT_ETHIOPIC */ + "ka", /* G_UNICODE_SCRIPT_GEORGIAN */ + "", /* G_UNICODE_SCRIPT_GOTHIC */ + "el", /* G_UNICODE_SCRIPT_GREEK */ + "gu", /* G_UNICODE_SCRIPT_GUJARATI */ + "pa", /* G_UNICODE_SCRIPT_GURMUKHI */ + "", /* G_UNICODE_SCRIPT_HAN */ + "ko", /* G_UNICODE_SCRIPT_HANGUL */ + "he", /* G_UNICODE_SCRIPT_HEBREW */ + "ja", /* G_UNICODE_SCRIPT_HIRAGANA */ + "kn", /* G_UNICODE_SCRIPT_KANNADA */ + "ja", /* G_UNICODE_SCRIPT_KATAKANA */ + "km", /* G_UNICODE_SCRIPT_KHMER */ + "lo", /* G_UNICODE_SCRIPT_LAO */ + "en", /* G_UNICODE_SCRIPT_LATIN */ + "ml", /* G_UNICODE_SCRIPT_MALAYALAM */ + "mn", /* G_UNICODE_SCRIPT_MONGOLIAN */ + "my", /* G_UNICODE_SCRIPT_MYANMAR */ + /* Ogham was used to write old Irish */ + "", /* G_UNICODE_SCRIPT_OGHAM */ + "", /* G_UNICODE_SCRIPT_OLD_ITALIC */ + "or", /* G_UNICODE_SCRIPT_ORIYA */ + "", /* G_UNICODE_SCRIPT_RUNIC */ + "si", /* G_UNICODE_SCRIPT_SINHALA */ + "syr", /* G_UNICODE_SCRIPT_SYRIAC */ + "ta", /* G_UNICODE_SCRIPT_TAMIL */ + "te", /* G_UNICODE_SCRIPT_TELUGU */ + "dv", /* G_UNICODE_SCRIPT_THAANA */ + "th", /* G_UNICODE_SCRIPT_THAI */ + "bo", /* G_UNICODE_SCRIPT_TIBETAN */ + "iu", /* G_UNICODE_SCRIPT_CANADIAN_ABORIGINAL */ + "", /* G_UNICODE_SCRIPT_YI */ + "tl", /* G_UNICODE_SCRIPT_TAGALOG */ + /* Phillipino languages/scripts */ + "hnn", /* G_UNICODE_SCRIPT_HANUNOO */ + "bku", /* G_UNICODE_SCRIPT_BUHID */ + "tbw", /* G_UNICODE_SCRIPT_TAGBANWA */ + + "", /* G_UNICODE_SCRIPT_BRAILLE */ + "", /* G_UNICODE_SCRIPT_CYPRIOT */ + "", /* G_UNICODE_SCRIPT_LIMBU */ + /* Used for Somali (so) in the past */ + "", /* G_UNICODE_SCRIPT_OSMANYA */ + /* The Shavian alphabet was designed for English */ + "", /* G_UNICODE_SCRIPT_SHAVIAN */ + "", /* G_UNICODE_SCRIPT_LINEAR_B */ + "", /* G_UNICODE_SCRIPT_TAI_LE */ + "uga", /* G_UNICODE_SCRIPT_UGARITIC */ + + "", /* G_UNICODE_SCRIPT_NEW_TAI_LUE */ + "bug", /* G_UNICODE_SCRIPT_BUGINESE */ + /* The original script for Old Church Slavonic (chu), later + * written with Cyrillic */ + "", /* G_UNICODE_SCRIPT_GLAGOLITIC */ + /* Used for for Berber (ber), but Arabic script is more common */ + "", /* G_UNICODE_SCRIPT_TIFINAGH */ + "syl", /* G_UNICODE_SCRIPT_SYLOTI_NAGRI */ + "peo", /* G_UNICODE_SCRIPT_OLD_PERSIAN */ + "", /* G_UNICODE_SCRIPT_KHAROSHTHI */ + + "", /* G_UNICODE_SCRIPT_UNKNOWN */ + "", /* G_UNICODE_SCRIPT_BALINESE */ + "", /* G_UNICODE_SCRIPT_CUNEIFORM */ + "", /* G_UNICODE_SCRIPT_PHOENICIAN */ + "", /* G_UNICODE_SCRIPT_PHAGS_PA */ + "nqo", /* G_UNICODE_SCRIPT_NKO */ + + /* Unicode-5.1 additions */ + "", /* G_UNICODE_SCRIPT_KAYAH_LI */ + "", /* G_UNICODE_SCRIPT_LEPCHA */ + "", /* G_UNICODE_SCRIPT_REJANG */ + "", /* G_UNICODE_SCRIPT_SUNDANESE */ + "", /* G_UNICODE_SCRIPT_SAURASHTRA */ + "", /* G_UNICODE_SCRIPT_CHAM */ + "", /* G_UNICODE_SCRIPT_OL_CHIKI */ + "", /* G_UNICODE_SCRIPT_VAI */ + "", /* G_UNICODE_SCRIPT_CARIAN */ + "", /* G_UNICODE_SCRIPT_LYCIAN */ + "", /* G_UNICODE_SCRIPT_LYDIAN */ + + /* Unicode-6.0 additions */ + "", /* G_UNICODE_SCRIPT_BATAK */ + "", /* G_UNICODE_SCRIPT_BRAHMI */ + "", /* G_UNICODE_SCRIPT_MANDAIC */ + + /* Unicode-6.1 additions */ + "", /* G_UNICODE_SCRIPT_CHAKMA */ + "", /* G_UNICODE_SCRIPT_MEROITIC_CURSIVE */ + "", /* G_UNICODE_SCRIPT_MEROITIC_HIEROGLYPHS */ + "", /* G_UNICODE_SCRIPT_MIAO */ + "", /* G_UNICODE_SCRIPT_SHARADA */ + "", /* G_UNICODE_SCRIPT_SORA_SOMPENG */ + "", /* G_UNICODE_SCRIPT_TAKRI */ + }; + const char *sample_language; + Pango2Language *result; + + g_return_val_if_fail (script >= 0, NULL); + + if ((guint)script >= G_N_ELEMENTS (sample_languages)) + return NULL; + + result = _pango2_script_get_default_language (script); + if (result) + return result; + + sample_language = sample_languages[script]; + + if (!sample_language[0]) + return NULL; + else + return pango2_language_from_string (sample_language); +} diff --git a/pango2/pango-language.h b/pango2/pango-language.h new file mode 100644 index 00000000..f4484fd9 --- /dev/null +++ b/pango2/pango-language.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 1999 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <glib.h> +#include <glib-object.h> + +#include <pango2/pango-types.h> +#include <pango2/pango-version-macros.h> +#include <pango2/pango-script.h> + +G_BEGIN_DECLS + +#define PANGO2_TYPE_LANGUAGE (pango2_language_get_type ()) + +PANGO2_AVAILABLE_IN_ALL +GType pango2_language_get_type (void) G_GNUC_CONST; + +PANGO2_AVAILABLE_IN_ALL +Pango2Language * pango2_language_get_default (void) G_GNUC_CONST; + +PANGO2_AVAILABLE_IN_ALL +Pango2Language ** pango2_language_get_preferred (void) G_GNUC_CONST; + +PANGO2_AVAILABLE_IN_ALL +Pango2Language * pango2_language_from_string (const char *language); + +PANGO2_AVAILABLE_IN_ALL +const char * pango2_language_to_string (Pango2Language *language) G_GNUC_CONST; + +/* For back compat. Will have to keep indefinitely. */ +#define pango2_language_to_string(language) ((const char *)language) + +PANGO2_AVAILABLE_IN_ALL +const char * pango2_language_get_sample_string (Pango2Language *language) G_GNUC_CONST; + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_language_matches (Pango2Language *language, + const char *range_list) G_GNUC_PURE; + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_language_includes_script (Pango2Language *language, + GUnicodeScript script) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +const GUnicodeScript * pango2_language_get_scripts (Pango2Language *language, + int *num_scripts); + +G_END_DECLS diff --git a/pango2/pango-layout.c b/pango2/pango-layout.c new file mode 100644 index 00000000..9a01b490 --- /dev/null +++ b/pango2/pango-layout.c @@ -0,0 +1,1734 @@ +#include "config.h" + +#include "pango-layout.h" +#include "pango-line-breaker.h" +#include "pango-line-private.h" +#include "pango-enum-types.h" +#include "pango-markup.h" +#include "pango-context.h" + +/** + * Pango2Layout: + * + * A `Pango2Layout` structure represents an entire paragraph of text. + * + * While complete access to the layout capabilities of Pango2 is provided + * using the detailed interfaces for itemization, segmentation and shaping, + * using that functionality directly involves writing a fairly large amount + * of code. `Pango2Layout` provides a high-level driver for formatting entire + * paragraphs of text at once. This includes paragraph-level functionality + * such as line breaking, justification, alignment and ellipsization. + * + * A `Pango2Layout` is initialized with a `Pango2Context`, a UTF-8 string + * and set of attributes for that string. Once that is done, the set of + * formatted lines can be extracted in the form of a [class@Pango2.Lines] + * object, the layout can be rendered, and conversion between logical + * character positions within the layout's text, and the physical position + * of the resulting glyphs can be made. + * + * The most convenient way to access the visual extents and components + * of a formatted layout is via a [struct@Pango2.LineIter] iterator. + * + * There are a number of parameters to adjust the formatting of a + * `Pango2Layout`. The following image shows adjustable parameters + * (on the left) and font metrics (on the right): + * + * <picture> + * <source srcset="layout-dark.png" media="(prefers-color-scheme: dark)"> + * <img alt="Pango2 Layout Parameters" src="layout-light.png"> + * </picture> + * + * The following images demonstrate the effect of alignment and justification + * on the layout of text: + * + * | | | | + * | --- | --- | --- | + * | ![align=left](align-left.png) | ![align=center](align-center.png) | ![align=right](align-right.png) | + * | ![align=justify](align-left-justify.png) | | | + * + * It is possible, as well, to ignore the 2-D setup, and simply treat the + * resulting `Pango2Lines` object as a list of lines. + * + * If you have more complex line breaking needs, such as shaping text + * to flow around images, or multi-column layout, the [class@Pango2.LineBreaker] + * makes the underlying line-breaking functionality available outside of + * `Pango2Layout`. + */ + + +/* {{{ Pango2Layout implementation */ + +struct _Pango2Layout +{ + GObject parent_instance; + + Pango2Context *context; + char *text; + int length; + Pango2AttrList *attrs; + Pango2FontDescription *font_desc; + float line_height; + int spacing; + int width; + int height; + Pango2TabArray *tabs; + gboolean single_paragraph; + Pango2WrapMode wrap; + int indent; + guint serial; + guint context_serial; + Pango2Alignment alignment; + Pango2EllipsizeMode ellipsize; + gboolean auto_dir; + + Pango2Lines *lines; +}; + +struct _Pango2LayoutClass +{ + GObjectClass parent_class; +}; + +enum +{ + PROP_CONTEXT = 1, + PROP_TEXT, + PROP_ATTRIBUTES, + PROP_FONT_DESCRIPTION, + PROP_LINE_HEIGHT, + PROP_SPACING, + PROP_WIDTH, + PROP_HEIGHT, + PROP_TABS, + PROP_SINGLE_PARAGRAPH, + PROP_WRAP, + PROP_INDENT, + PROP_ALIGNMENT, + PROP_ELLIPSIZE, + PROP_AUTO_DIR, + PROP_LINES, + NUM_PROPERTIES +}; + +static GParamSpec *props[NUM_PROPERTIES] = { NULL, }; + +G_DEFINE_FINAL_TYPE (Pango2Layout, pango2_layout, G_TYPE_OBJECT) + +static void +pango2_layout_init (Pango2Layout *layout) +{ + layout->serial = 1; + layout->width = -1; + layout->height = -1; + layout->indent = 0; + layout->wrap = PANGO2_WRAP_WORD; + layout->alignment = PANGO2_ALIGN_NATURAL; + layout->ellipsize = PANGO2_ELLIPSIZE_NONE; + layout->line_height = 0.0; + layout->spacing = 0; + layout->auto_dir = TRUE; + layout->text = g_strdup (""); + layout->length = 0; +} + +static void +pango2_layout_finalize (GObject *object) +{ + Pango2Layout *layout = PANGO2_LAYOUT (object); + + g_clear_pointer (&layout->font_desc, pango2_font_description_free); + g_object_unref (layout->context); + g_free (layout->text); + g_clear_pointer (&layout->attrs, pango2_attr_list_unref); + g_clear_pointer (&layout->tabs, pango2_tab_array_free); + g_clear_object (&layout->lines); + + G_OBJECT_CLASS (pango2_layout_parent_class)->finalize (object); +} + +static void +pango2_layout_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + Pango2Layout *layout = PANGO2_LAYOUT (object); + + switch (prop_id) + { + case PROP_CONTEXT: + layout->context = g_value_dup_object (value); + layout->context_serial = pango2_context_get_serial (layout->context); + break; + + case PROP_TEXT: + pango2_layout_set_text (layout, g_value_get_string (value), -1); + break; + + case PROP_ATTRIBUTES: + pango2_layout_set_attributes (layout, g_value_get_boxed (value)); + break; + + case PROP_FONT_DESCRIPTION: + pango2_layout_set_font_description (layout, g_value_get_boxed (value)); + break; + + case PROP_LINE_HEIGHT: + pango2_layout_set_line_height (layout, g_value_get_float (value)); + break; + + case PROP_SPACING: + pango2_layout_set_spacing (layout, g_value_get_int (value)); + break; + + case PROP_WIDTH: + pango2_layout_set_width (layout, g_value_get_int (value)); + break; + + case PROP_HEIGHT: + pango2_layout_set_height (layout, g_value_get_int (value)); + break; + + case PROP_TABS: + pango2_layout_set_tabs (layout, g_value_get_boxed (value)); + break; + + case PROP_SINGLE_PARAGRAPH: + pango2_layout_set_single_paragraph (layout, g_value_get_boolean (value)); + break; + + case PROP_WRAP: + pango2_layout_set_wrap (layout, g_value_get_enum (value)); + break; + + case PROP_INDENT: + pango2_layout_set_indent (layout, g_value_get_int (value)); + break; + + case PROP_ALIGNMENT: + pango2_layout_set_alignment (layout, g_value_get_enum (value)); + break; + + case PROP_ELLIPSIZE: + pango2_layout_set_ellipsize (layout, g_value_get_enum (value)); + break; + + case PROP_AUTO_DIR: + pango2_layout_set_auto_dir (layout, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +pango2_layout_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + Pango2Layout *layout = PANGO2_LAYOUT (object); + + switch (prop_id) + { + case PROP_CONTEXT: + g_value_set_object (value, layout->context); + break; + + case PROP_TEXT: + g_value_set_string (value, layout->text); + break; + + case PROP_ATTRIBUTES: + g_value_set_boxed (value, layout->attrs); + break; + + case PROP_FONT_DESCRIPTION: + g_value_set_boxed (value, layout->font_desc); + break; + + case PROP_LINE_HEIGHT: + g_value_set_float (value, layout->line_height); + break; + + case PROP_SPACING: + g_value_set_int (value, layout->spacing); + break; + + case PROP_WIDTH: + g_value_set_int (value, layout->width); + break; + + case PROP_HEIGHT: + g_value_set_int (value, layout->height); + break; + + case PROP_TABS: + g_value_set_boxed (value, layout->tabs); + break; + + case PROP_SINGLE_PARAGRAPH: + g_value_set_boolean (value, layout->single_paragraph); + break; + + case PROP_WRAP: + g_value_set_enum (value, layout->wrap); + break; + + case PROP_INDENT: + g_value_set_int (value, layout->indent); + break; + + case PROP_ALIGNMENT: + g_value_set_enum (value, layout->alignment); + break; + + case PROP_ELLIPSIZE: + g_value_set_enum (value, layout->ellipsize); + break; + + case PROP_AUTO_DIR: + g_value_set_boolean (value, layout->auto_dir); + break; + + case PROP_LINES: + g_value_set_object (value, pango2_layout_get_lines (layout)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +pango2_layout_class_init (Pango2LayoutClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->finalize = pango2_layout_finalize; + object_class->set_property = pango2_layout_set_property; + object_class->get_property = pango2_layout_get_property; + + /** + * Pango2Layout:context: (attributes org.gtk.Property.get=pango2_layout_get_context) + * + * The context for the `Pango2Layout`. + */ + props[PROP_CONTEXT] = g_param_spec_object ("context", "context", "context", + PANGO2_TYPE_CONTEXT, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + /** + * Pango2Layout:text: (attributes org.gtk.Property.get=pango2_layout_get_text org.gtk.Property.set=pango2_layout_set_text) + * + * The text of the `Pango2Layout`. + */ + props[PROP_TEXT] = g_param_spec_string ("text", "text", "text", + "", + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * Pango2Layout:attributes: (attributes org.gtk.Property.get=pango2_layout_get_attributes org.gtk.Property.set=pango2_layout_set_attributes) + * + * The attributes of the `Pango2Layout`. + * + * Attributes can affect how the text is formatted. + */ + props[PROP_ATTRIBUTES] = g_param_spec_boxed ("attributes", "attributes", "attributes", + PANGO2_TYPE_ATTR_LIST, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * Pango2Layout:font-description: (attributes org.gtk.Property.get=pango2_layout_get_font_description org.gtk.Property.set=pango2_layout_set_font_description) + * + * The font description of the `Pango2Layout`. + */ + props[PROP_FONT_DESCRIPTION] = g_param_spec_boxed ("font-description", "font-description", "font-description", + PANGO2_TYPE_FONT_DESCRIPTION, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * Pango2Layout:line-height: (attributes org.gtk.Property.get=pango2_layout_get_line_height org.gtk.Property.set=pango2_layout_set_line_height) + * + * The line height factor of the `Pango2Layout`. + * + * If non-zero, the line height is multiplied by this factor to determine + * the logical extents of the text. + * + * The default value is 0. + */ + props[PROP_LINE_HEIGHT] = g_param_spec_float ("line-height", "line-height", "line-height", + 0., G_MAXFLOAT, 0., + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * Pango2Layout:spacing: (attributes org.gtk.Property.get=pango2_layout_get_spacing org.gtk.Property.set=pango2_layout_set_spacing) + * + * Spacing to add between the lines of the `Pango2Layout`. + * + * The spacing is specified in Pango2 units. + * + * The default value is 0. + */ + props[PROP_SPACING] = g_param_spec_int ("spacing", "spacing", "spacing", + 0, G_MAXINT, 0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * Pango2Layout:width: (attributes org.gtk.Property.get=pango2_layout_get_width org.gtk.Property.set=pango2_layout_set_width) + * + * The width to which the text of `Pango2Layout` will be broken. + * + * The width is specified in Pango2 units, with -1 meaning unlimited. + * + * The default value is -1. + */ + props[PROP_WIDTH] = g_param_spec_int ("width", "width", "width", + -1, G_MAXINT, -1, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * Pango2Layout:height: (attributes org.gtk.Property.get=pango2_layout_get_height org.gtk.Property.set=pango2_layout_set_height) + * + * The height to which the `Pango2Layout` will be ellipsized. + * + * If @height is positive, it will be the maximum height of the + * layout. Only lines would be shown that would fit, and if there + * is any text omitted, an ellipsis added. At least one line is + * included in each paragraph regardless of how small the height + * value is. A value of zero will render exactly one line for the + * entire layout. + * + * If @height is negative, it will be the (negative of) maximum + * number of lines per paragraph. That is, the total number of lines + * shown may well be more than this value if the layout contains + * multiple paragraphs of text. + * + * The default value of -1 means that the first line of each + * paragraph is ellipsized. + * + * Height setting only has effect if a positive width is set on the + * layout and its ellipsization mode is not `PANGO2_ELLIPSIZE_NONE`. + * The behavior is undefined if a height other than -1 is set and + * ellipsization mode is set to `PANGO2_ELLIPSIZE_NONE`. + * + * The default value is -1. + */ + props[PROP_HEIGHT] = g_param_spec_int ("height", "height", "height", + -G_MAXINT, G_MAXINT, -1, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * Pango2Layout:tabs: (attributes org.gtk.Property.get=pango2_layout_get_tabs org.gtk.Property.set=pango2_layout_set_tabs) + * + * The tabs to use when formatting the text of `Pango2Layout`. + * + * `Pango2Layout` will place content at the next tab position + * whenever it meets a Tab character (U+0009). + */ + props[PROP_TABS] = g_param_spec_boxed ("tabs", "tabs", "tabs", + PANGO2_TYPE_TAB_ARRAY, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * Pango2Layout:single-paragraph: (attributes org.gtk.Property.get=pango2_layout_get_single_paragraph org.gtk.Property.set=pango2_layout_set_single_paragraph) + * + * Whether to treat newlines and similar characters as paragraph + * separators or not. + * + * If this property is `TRUE`, all text is kept in a single paragraph, + * and paragraph separator characters are displayed with a glyph. + * + * This is useful to allow editing of newlines on a single text line. + * + * The default value is `FALSE`. + */ + props[PROP_SINGLE_PARAGRAPH] = g_param_spec_boolean ("single-paragraph", "single-paragraph", "single-paragraph", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * Pango2Layout:wrap: (attributes org.gtk.Property.get=pango2_layout_get_wrap org.gtk.Property.set=pango2_layout_set_wrap) + * + * The wrap mode of this `Pango2Layout`. + * + * The wrap mode influences how Pango2 chooses line breaks + * when text needs to be wrapped. + * + * The default value is `PANGO2_WRAP_WORD`. + */ + props[PROP_WRAP] = g_param_spec_enum ("wrap", "wrap", "wrap", + PANGO2_TYPE_WRAP_MODE, + PANGO2_WRAP_WORD, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * Pango2Layout:indent: (attributes org.gtk.Property.get=pango2_layout_get_indent org.gtk.Property.set=pango2_layout_set_indent) + * + * The indent of this `Pango2Layout`. + * + * The indent is specified in Pango2 units. + * + * A negative value of @indent will produce a hanging indentation. + * That is, the first line will have the full width, and subsequent + * lines will be indented by the absolute value of @indent. + * + * The default value is 0. + */ + props[PROP_INDENT] = g_param_spec_int ("indent", "indent", "indent", + G_MININT, G_MAXINT, 0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * Pango2Layout:alignment: (attributes org.gtk.Property.get=pango2_layout_get_alignment org.gtk.Property.set=pango2_layout_set_alignment) + * + * The alignment mode of this `Pango2Layout`. + * + * The default value is `PANGO2_ALIGN_NATURAL`. + */ + props[PROP_ALIGNMENT] = g_param_spec_enum ("alignment", "alignment", "alignment", + PANGO2_TYPE_ALIGNMENT, + PANGO2_ALIGN_NATURAL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * Pango2Layout:ellipsize: (attributes org.gtk.Property.get=pango2_layout_get_ellipsize org.gtk.Property.set=pango2_layout_set_ellipsize) + * + * The ellipsization mode of this `Pango2Layout`. + * + * The default value is `PANGO2_ELLIPSIZE_NONE`. + */ + props[PROP_ELLIPSIZE] = g_param_spec_enum ("ellipsize", "ellipsize", "ellipsize", + PANGO2_TYPE_ELLIPSIZE_MODE, + PANGO2_ELLIPSIZE_NONE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * Pango2Layout:auto-dir: (attributes org.gtk.Property.get=pango2_layout_get_auto_dir org.gtk.Property.set=pango2_layout_set_auto_dir) + * + * Whether this `Pango2Layout` determines the + * base direction from the content. + * + * The default value is `TRUE`. + */ + props[PROP_AUTO_DIR] = g_param_spec_boolean ("auto-dir", "auto-dir", "auto-dir", + TRUE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * Pango2Layout:lines: (attributes org.gtk.Property.get=pango2_layout_get_lines) + * + * The `Pango2Lines` object holding the formatted lines. + */ + props[PROP_LINES] = g_param_spec_object ("lines", "lines", "lines", + PANGO2_TYPE_LINES, + G_PARAM_READABLE); + + g_object_class_install_properties (object_class, NUM_PROPERTIES, props); +} + +/* }}} */ +/* {{{ Utilities */ + +static void +layout_changed (Pango2Layout *layout) +{ + layout->serial++; + if (layout->serial == 0) + layout->serial++; + + g_clear_object (&layout->lines); + g_object_notify_by_pspec (G_OBJECT (layout), props[PROP_LINES]); +} + +static void +check_context_changed (Pango2Layout *layout) +{ + guint old_serial = layout->context_serial; + + layout->context_serial = pango2_context_get_serial (layout->context); + + if (old_serial != layout->context_serial) + pango2_layout_context_changed (layout); +} + +static Pango2AttrList * +ensure_attrs (Pango2Layout *layout, + Pango2AttrList *attrs) +{ + if (attrs) + return attrs; + else if (layout->attrs) + return pango2_attr_list_copy (layout->attrs); + else + return pango2_attr_list_new (); +} + +static Pango2AttrList * +get_effective_attributes (Pango2Layout *layout) +{ + Pango2AttrList *attrs = NULL; + + if (layout->font_desc) + { + attrs = ensure_attrs (layout, attrs); + pango2_attr_list_insert_before (attrs, + pango2_attr_font_desc_new (layout->font_desc)); + } + + if (layout->line_height != 0.0) + { + attrs = ensure_attrs (layout, attrs); + pango2_attr_list_insert_before (attrs, + pango2_attr_line_height_new (layout->line_height)); + } + + if (layout->spacing != 0) + { + attrs = ensure_attrs (layout, attrs); + pango2_attr_list_insert_before (attrs, + pango2_attr_line_spacing_new (layout->spacing)); + } + + if (layout->single_paragraph) + { + attrs = ensure_attrs (layout, attrs); + pango2_attr_list_insert_before (attrs, + pango2_attr_paragraph_new ()); + } + + if (attrs) + return attrs; + + return pango2_attr_list_ref (layout->attrs); +} + +static gboolean +ends_with_paragraph_separator (Pango2Layout *layout) +{ + if (layout->single_paragraph) + return FALSE; + + return g_str_has_suffix (layout->text, "\n") || + g_str_has_suffix (layout->text, "\r") || + g_str_has_suffix (layout->text, "\r\n") || + g_str_has_suffix (layout->text, "
"); +} + +static void +ensure_lines (Pango2Layout *layout) +{ + Pango2LineBreaker *breaker; + Pango2AttrList *attrs; + int x, y, width; + int line_no; + + check_context_changed (layout); + + if (layout->lines) + return; + + breaker = pango2_line_breaker_new (layout->context); + + pango2_line_breaker_set_tabs (breaker, layout->tabs); + pango2_line_breaker_set_base_dir (breaker, + layout->auto_dir + ? PANGO2_DIRECTION_NEUTRAL + : pango2_context_get_base_dir (layout->context)); + + attrs = get_effective_attributes (layout); + pango2_line_breaker_add_text (breaker, layout->text ? layout->text : "", -1, attrs); + if (attrs) + pango2_attr_list_unref (attrs); + + layout->lines = pango2_lines_new (); + + x = y = 0; + line_no = 0; + while (pango2_line_breaker_has_line (breaker)) + { + Pango2Line *line; + Pango2Rectangle ext; + int offset; + Pango2EllipsizeMode ellipsize = PANGO2_ELLIPSIZE_NONE; + Pango2LeadingTrim trim = PANGO2_LEADING_TRIM_NONE; + + if ((line_no == 0) == (layout->indent > 0)) + { + x = abs (layout->indent); + width = layout->width - x; + } + else + { + x = 0; + width = layout->width; + } + + if (layout->height < 0 && line_no + 1 == - layout->height) + ellipsize = layout->ellipsize; + +retry: + line = pango2_line_breaker_next_line (breaker, x, width, layout->wrap, ellipsize); + + if (line->starts_paragraph) + trim |= PANGO2_LEADING_TRIM_START; + if (line->ends_paragraph) + trim |= PANGO2_LEADING_TRIM_END; + + pango2_line_get_trimmed_extents (line, trim, &ext); + + if (layout->height >= 0 && y + 2 * ext.height >= layout->height && + ellipsize != layout->ellipsize) + { + if (pango2_line_breaker_undo_line (breaker, line)) + { + g_clear_pointer (&line, pango2_line_free); + ellipsize = layout->ellipsize; + goto retry; + } + } + + /* Handle alignment and justification */ + offset = 0; + switch (layout->alignment) + { + + case PANGO2_ALIGN_LEFT: + break; + + case PANGO2_ALIGN_CENTER: + if (ext.width < width) + offset = (width - ext.width) / 2; + break; + + case PANGO2_ALIGN_JUSTIFY: + if (!pango2_line_is_paragraph_end (line)) + { + line = pango2_line_justify (line, width); + break; + } + G_GNUC_FALLTHROUGH; + + case PANGO2_ALIGN_NATURAL: + { + Pango2Line *first_line; + if (pango2_lines_get_line_count (layout->lines) > 0) + first_line = pango2_lines_get_lines (layout->lines)[0]; + else + first_line = line; + if (pango2_line_get_resolved_direction (first_line) == PANGO2_DIRECTION_LTR) + break; + } + G_GNUC_FALLTHROUGH; + + case PANGO2_ALIGN_RIGHT: + if (ext.width < width) + offset = width - ext.width; + break; + + default: + g_assert_not_reached (); + } + + pango2_lines_add_line (layout->lines, line, x + offset, y - ext.y); + + y += ext.height; + line_no++; + } + + /* Append an empty line if we end with a newline. + * And always provide at least one line + */ + if (pango2_lines_get_line_count (layout->lines) == 0 || + ends_with_paragraph_separator (layout)) + { + LineData *data; + int start_index; + int start_offset; + int offset; + Pango2Line *line; + Pango2Rectangle ext; + + if (pango2_lines_get_line_count (layout->lines) > 0) + { + Pango2Line *last; + + last = pango2_lines_get_lines (layout->lines)[pango2_lines_get_line_count (layout->lines) - 1]; + data = line_data_ref (last->data); + start_index = data->length; + start_offset = last->data->n_chars; + offset = MAX (layout->indent, 0); + } + else + { + data = line_data_new (); + data->text = g_strdup (""); + data->length = 0; + data->attrs = get_effective_attributes (layout); + data->log_attrs = g_new0 (Pango2LogAttr, 1); + data->log_attrs[0].is_cursor_position = TRUE; + start_index = 0; + start_offset = 0; + offset = 0; + } + + line = pango2_line_new (layout->context, data); + line->starts_paragraph = TRUE; + line->ends_paragraph = TRUE; + line->start_index = start_index; + line->length = 0; + line->start_offset = start_offset; + line->n_chars = 0; + + pango2_line_get_extents (line, NULL, &ext); + + pango2_lines_add_line (layout->lines, line, x + offset, y - ext.y); + + line_data_unref (data); + } + + g_object_unref (breaker); +} + +/* }}} */ +/* {{{ Public API */ + +/** + * pango2_layout_new: + * @context: a `Pango2Context` + * + * Creates a new `Pango2Layout` with attribute initialized to + * default values for a particular `Pango2Context` + * + * Return value: a newly allocated `Pango2Layout` + */ +Pango2Layout * +pango2_layout_new (Pango2Context *context) +{ + g_return_val_if_fail (PANGO2_IS_CONTEXT (context), NULL); + + return g_object_new (PANGO2_TYPE_LAYOUT, "context", context, NULL); +} + +/** + * pango2_layout_copy: + * @layout: a `Pango2Layout` + * + * Creates a deep copy-by-value of the layout. + * + * The attribute list, tab array, and text from the original layout + * are all copied by value. + * + * Return value: (transfer full): the newly allocated `Pango2Layout` + */ +Pango2Layout * +pango2_layout_copy (Pango2Layout *layout) +{ + Pango2Layout *copy; + + g_return_val_if_fail (PANGO2_IS_LAYOUT (layout), NULL); + + copy = pango2_layout_new (layout->context); + + copy->text = g_strdup (layout->text); + copy->length = layout->length; + if (layout->attrs) + copy->attrs = pango2_attr_list_copy (layout->attrs); + if (layout->font_desc) + copy->font_desc = pango2_font_description_copy (layout->font_desc); + copy->line_height = layout->line_height; + copy->spacing = layout->spacing; + copy->width = layout->width; + copy->height = layout->height; + if (layout->tabs) + copy->tabs = pango2_tab_array_copy (layout->tabs); + copy->single_paragraph = layout->single_paragraph; + copy->wrap = layout->wrap; + copy->indent = layout->indent; + copy->serial = layout->serial; + copy->context_serial = layout->context_serial; + copy->alignment = layout->alignment; + copy->ellipsize = layout->ellipsize; + copy->auto_dir = layout->auto_dir; + + return copy; +} + +/** + * pango2_layout_get_serial: + * @layout: a `Pango2Layout` + * + * Returns the current serial number of the layout. + * + * The serial number is initialized to an small number larger than zero + * when a new layout is created and is increased whenever the layout is + * changed using any of the setter functions, or the `Pango2Context` it + * uses has changed. + * + * The serial may wrap, but will never have the value 0. Since it can + * wrap, never compare it with "less than", always use "not equals". + * + * This can be used to automatically detect changes to a `Pango2Layout`, + * and is useful for example to decide whether a layout needs redrawing. + * + * Return value: The current serial number of @layout + */ +guint +pango2_layout_get_serial (Pango2Layout *layout) +{ + check_context_changed (layout); + + return layout->serial; +} + +/** + * pango2_layout_context_changed: + * @layout: a `Pango2Layout` + * + * Forces recomputation of any state in the layout that + * might depend on the layout's context. + * + * This function should be called if you make changes to the + * context subsequent to creating the layout. + */ +void +pango2_layout_context_changed (Pango2Layout *layout) +{ + g_return_if_fail (PANGO2_IS_LAYOUT (layout)); + + layout_changed (layout); +} + +/* {{{ Property getters and setters */ + +/** + * pango2_layout_get_context: + * @layout: a `Pango2Layout` + * + * Retrieves the `Pango2Context` used for this layout. + * + * Return value: (transfer none): the `Pango2Context` for the layout + */ +Pango2Context * +pango2_layout_get_context (Pango2Layout *layout) +{ + g_return_val_if_fail (PANGO2_IS_LAYOUT (layout), NULL); + + return layout->context; +} + +/** + * pango2_layout_set_text: + * @layout: a `Pango2Layout` + * @text: the text + * @length: maximum length of @text, in bytes. -1 indicates that + * the string is nul-terminated + * + * Sets the text of the layout. + */ +void +pango2_layout_set_text (Pango2Layout *layout, + const char *text, + int length) +{ + g_return_if_fail (PANGO2_IS_LAYOUT (layout)); + + if (length < 0) + length = strlen (text); + + g_free (layout->text); + layout->text = g_strndup (text, length); + layout->length = length; + + g_object_notify_by_pspec (G_OBJECT (layout), props[PROP_TEXT]); + layout_changed (layout); +} + +/** + * pango2_layout_get_text: + * @layout: a `Pango2Layout` + * + * Gets the text in the layout. + * + * The returned text should not be freed or modified. + * + * Return value: (transfer none): the text in the @layout + */ +const char * +pango2_layout_get_text (Pango2Layout *layout) +{ + g_return_val_if_fail (PANGO2_IS_LAYOUT (layout), NULL); + + return layout->text; +} + +/** + * pango2_layout_set_attributes: + * @layout: a `Pango2Layout` + * @attrs: (nullable) (transfer none): a `Pango2AttrList` + * + * Sets the attributes for a layout object. + * + * References @attrs, so the caller can unref its reference. + */ +void +pango2_layout_set_attributes (Pango2Layout *layout, + Pango2AttrList *attrs) +{ + g_return_if_fail (PANGO2_IS_LAYOUT (layout)); + + g_clear_pointer (&layout->attrs, pango2_attr_list_unref); + layout->attrs = attrs; + if (layout->attrs) + pango2_attr_list_ref (layout->attrs); + + g_object_notify_by_pspec (G_OBJECT (layout), props[PROP_ATTRIBUTES]); + layout_changed (layout); +} + +/** + * pango2_layout_get_attributes: + * @layout: a `Pango2Layout` + * + * Gets the attribute list for the layout, if any. + * + * Return value: (transfer none) (nullable): a `Pango2AttrList` + */ +Pango2AttrList * +pango2_layout_get_attributes (Pango2Layout *layout) +{ + g_return_val_if_fail (PANGO2_IS_LAYOUT (layout), NULL); + + return layout->attrs; +} + +/** + * pango2_layout_set_font_description: + * @layout: a `Pango2Layout` + * @desc: (nullable): the new `Pango2FontDescription` + * + * Sets the default font description for the layout. + * + * If no font description is set on the layout, the + * font description from the layout's context is used. + * + * This method is a shorthand for adding a font-desc attribute. + */ +void +pango2_layout_set_font_description (Pango2Layout *layout, + const Pango2FontDescription *desc) +{ + g_return_if_fail (PANGO2_IS_LAYOUT (layout)); + + if (desc != layout->font_desc && + (!desc || !layout->font_desc || !pango2_font_description_equal (desc, layout->font_desc))) + { + if (layout->font_desc) + pango2_font_description_free (layout->font_desc); + + layout->font_desc = desc ? pango2_font_description_copy (desc) : NULL; + + g_object_notify_by_pspec (G_OBJECT (layout), props[PROP_FONT_DESCRIPTION]); + layout_changed (layout); + } +} + +/** + * pango2_layout_get_font_description: + * @layout: a `Pango2Layout` + * + * Gets the font description for the layout, if any. + * + * Return value: (transfer none) (nullable): a pointer to the + * layout's font description, or %NULL if the font description + * from the layout's context is inherited. + */ +const Pango2FontDescription * +pango2_layout_get_font_description (Pango2Layout *layout) +{ + g_return_val_if_fail (PANGO2_IS_LAYOUT (layout), NULL); + + return layout->font_desc; +} + +/** + * pango2_layout_set_line_height: + * @layout: a `Pango2Layout` + * @line_height: the new line height factor + * + * Sets a factor for line height. + * + * Typical values are: 0, 1, 1.5, 2. The default values is 0. + * + * If @line_height is non-zero, lines are placed so that + * + * baseline2 = baseline1 + factor * height2 + * + * where height2 is the line height of the second line (as determined + * by the font). + * + * This method is a shorthand for adding a line-height attribute. + */ +void +pango2_layout_set_line_height (Pango2Layout *layout, + float line_height) +{ + g_return_if_fail (PANGO2_IS_LAYOUT (layout)); + + if (layout->line_height == line_height) + return; + + layout->line_height = line_height; + + g_object_notify_by_pspec (G_OBJECT (layout), props[PROP_LINE_HEIGHT]); + layout_changed (layout); +} + +/** + * pango2_layout_get_line_height: + * @layout: a `Pango2Layout` + * + * Gets the line height factor of the layout. + * + * See [method@Pango2.Layout.set_line_height]. + */ +float +pango2_layout_get_line_height (Pango2Layout *layout) +{ + g_return_val_if_fail (PANGO2_IS_LAYOUT (layout), 0.0); + + return layout->line_height; +} + +/** + * pango2_layout_set_spacing: + * @layout: a `Pango2Layout` + * @spacing: the amount of spacing, in Pango2 units + * + * Sets the amount of spacing between the lines of the layout. + * + * When placing lines with spacing, Pango2 arranges things so that + * + * line2.top = line1.bottom + spacing + * + * The default value is 0. + * + * Spacing only takes effect if the line height is not + * overridden via [method@Pango2.Layout.set_line_height]. + */ +void +pango2_layout_set_spacing (Pango2Layout *layout, + int spacing) +{ + g_return_if_fail (PANGO2_IS_LAYOUT (layout)); + + if (layout->spacing == spacing) + return; + + layout->spacing = spacing; + + g_object_notify_by_pspec (G_OBJECT (layout), props[PROP_SPACING]); + layout_changed (layout); +} + +/** + * pango2_layout_get_spacing: + * @layout: a `Pango2Layout` + * + * Gets the amount of spacing between the lines of the layout. + * + * Return value: the spacing in Pango2 units + */ +int +pango2_layout_get_spacing (Pango2Layout *layout) +{ + g_return_val_if_fail (PANGO2_IS_LAYOUT (layout), 0); + + return layout->spacing; +} + +/** + * pango2_layout_set_width: + * @layout: a `Pango2Layout`. + * @width: the desired width in Pango2 units, or -1 to indicate that no + * wrapping or ellipsization should be performed + * + * Sets the width to which the lines of the layout should + * be wrapped or ellipsized. + * + * The default value is -1: no width set. + */ +void +pango2_layout_set_width (Pango2Layout *layout, + int width) +{ + g_return_if_fail (PANGO2_IS_LAYOUT (layout)); + + if (width < -1) + width = -1; + + if (layout->width == width) + return; + + layout->width = width; + + g_object_notify_by_pspec (G_OBJECT (layout), props[PROP_WIDTH]); + layout_changed (layout); +} + +/** + * pango2_layout_get_width: + * @layout: a `Pango2Layout` + * + * Gets the width to which the lines of the layout should wrap. + * + * Return value: the width in Pango2 units, or -1 if no width set. + */ +int +pango2_layout_get_width (Pango2Layout *layout) +{ + g_return_val_if_fail (PANGO2_IS_LAYOUT (layout), -1); + + return layout->width; +} + +/** + * pango2_layout_set_height: + * @layout: a `Pango2Layout`. + * @height: the desired height + * + * Sets the height to which the `Pango2Layout` should be ellipsized at. + * + * There are two different behaviors, based on whether @height is positive + * or negative. + * + * See [property@Pango2.Layout:height] for details. + */ +void +pango2_layout_set_height (Pango2Layout *layout, + int height) +{ + g_return_if_fail (PANGO2_IS_LAYOUT (layout)); + + if (layout->height == height) + return; + + layout->height = height; + + g_object_notify_by_pspec (G_OBJECT (layout), props[PROP_HEIGHT]); + layout_changed (layout); +} + +/** + * pango2_layout_get_height: + * @layout: a `Pango2Layout` + * + * Gets the height to which the lines of the layout should ellipsize. + * + * See [property@Pango2.Layout:height] for details. + * + * Return value: the height + */ +int +pango2_layout_get_height (Pango2Layout *layout) +{ + g_return_val_if_fail (PANGO2_IS_LAYOUT (layout), -1); + + return layout->height; +} + +/** + * pango2_layout_set_tabs: + * @layout: a `Pango2Layout` + * @tabs: (nullable): a `Pango2TabArray` + * + * Sets the tabs to use for the layout, overriding the + * default tabs. + * + * Setting the tabs to `NULL` reinstates the default + * tabs. + * + * See [method@Pango2.LineBreaker.set_tabs] for details. + */ +void +pango2_layout_set_tabs (Pango2Layout *layout, + Pango2TabArray *tabs) +{ + g_return_if_fail (PANGO2_IS_LAYOUT (layout)); + + if (layout->tabs == tabs) + return; + + g_clear_pointer (&layout->tabs, pango2_tab_array_free); + if (tabs) + layout->tabs = pango2_tab_array_copy (tabs); + + g_object_notify_by_pspec (G_OBJECT (layout), props[PROP_TABS]); + layout_changed (layout); +} + +/** + * pango2_layout_get_tabs: + * @layout: a `Pango2Layout` + * + * Gets the current `Pango2TabArray` used by this layout. + * + * If no `Pango2TabArray` has been set, then the default tabs are + * in use and %NULL is returned. Default tabs are every 8 spaces. + * + * Return value: (transfer none) (nullable): the tabs for this layout + */ +Pango2TabArray * +pango2_layout_get_tabs (Pango2Layout *layout) +{ + g_return_val_if_fail (PANGO2_IS_LAYOUT (layout), NULL); + + return layout->tabs; +} + +/** + * pango2_layout_set_single_paragraph: + * @layout: a `Pango2Layout` + * @single_paragraph: the new setting + * + * Sets the single paragraph mode of the layout. + * + * If @single_paragraph is `TRUE`, do not treat newlines and similar + * characters as paragraph separators; instead, keep all text in a + * single paragraph, and display a glyph for paragraph separator + * characters. + * + * Used when you want to allow editing of newlines on a single text line. + * + * The default value is %FALSE. + */ +void +pango2_layout_set_single_paragraph (Pango2Layout *layout, + gboolean single_paragraph) +{ + g_return_if_fail (PANGO2_IS_LAYOUT (layout)); + + if (layout->single_paragraph == single_paragraph) + return; + + layout->single_paragraph = single_paragraph; + + g_object_notify_by_pspec (G_OBJECT (layout), props[PROP_SINGLE_PARAGRAPH]); + layout_changed (layout); +} + +/** + * pango2_layout_get_single_paragraph: + * @layout: a `Pango2Layout` + * + * Obtains whether this layout is in single paragraph mode. + * + * See [method@Pango2.Layout.set_single_paragraph]. + * + * Return value: `TRUE` if the layout does not break paragraphs + * at paragraph separator characters, %FALSE otherwise + */ +gboolean +pango2_layout_get_single_paragraph (Pango2Layout *layout) +{ + g_return_val_if_fail (PANGO2_IS_LAYOUT (layout), FALSE); + + return layout->single_paragraph; +} + +/** + * pango2_layout_set_wrap: + * @layout: a `Pango2Layout` + * @wrap: the wrap mode + * + * Sets the wrap mode. + * + * The wrap mode only has effect if a width is set on the layout + * with [method@Pango2.Layout.set_width]. To turn off wrapping, + * set the width to -1. + * + * The default value is %PANGO2_WRAP_WORD. + */ +void +pango2_layout_set_wrap (Pango2Layout *layout, + Pango2WrapMode wrap) +{ + g_return_if_fail (PANGO2_IS_LAYOUT (layout)); + + if (layout->wrap == wrap) + return; + + layout->wrap = wrap; + + g_object_notify_by_pspec (G_OBJECT (layout), props[PROP_WRAP]); + layout_changed (layout); +} + +/** + * pango2_layout_get_wrap: + * @layout: a `Pango2Layout` + * + * Gets the wrap mode for the layout. + * + * Return value: active wrap mode. + */ +Pango2WrapMode +pango2_layout_get_wrap (Pango2Layout *layout) +{ + g_return_val_if_fail (PANGO2_IS_LAYOUT (layout), PANGO2_WRAP_WORD); + + return layout->wrap; +} + +/** + * pango2_layout_set_indent: + * @layout: a `Pango2Layout` + * @indent: the amount by which to indent + * + * Sets the width in Pango2 units to indent each paragraph. + * + * A negative value of @indent will produce a hanging indentation. + * That is, the first line will have the full width, and subsequent + * lines will be indented by the absolute value of @indent. + * + * The default value is 0. + */ +void +pango2_layout_set_indent (Pango2Layout *layout, + int indent) +{ + g_return_if_fail (PANGO2_IS_LAYOUT (layout)); + + if (layout->indent == indent) + return; + + layout->indent = indent; + + g_object_notify_by_pspec (G_OBJECT (layout), props[PROP_INDENT]); + layout_changed (layout); +} + +/** + * pango2_layout_get_indent: + * @layout: a `Pango2Layout` + * + * Gets the paragraph indent width in Pango2 units. + * + * A negative value indicates a hanging indentation. + * + * Return value: the indent in Pango2 units + */ +int +pango2_layout_get_indent (Pango2Layout *layout) +{ + g_return_val_if_fail (PANGO2_IS_LAYOUT (layout), 0); + + return layout->indent; +} + +/** + * pango2_layout_set_alignment: + * @layout: a `Pango2Layout` + * @alignment: the alignment + * + * Sets the alignment for the layout. + * + * The alignment determines how short lines are + * positioned within the available horizontal space. + * + * The default alignment is `PANGO2_ALIGN_NATURAL`. + */ +void +pango2_layout_set_alignment (Pango2Layout *layout, + Pango2Alignment alignment) +{ + g_return_if_fail (PANGO2_IS_LAYOUT (layout)); + + if (layout->alignment == alignment) + return; + + layout->alignment = alignment; + + g_object_notify_by_pspec (G_OBJECT (layout), props[PROP_ALIGNMENT]); + layout_changed (layout); +} + +/** + * pango2_layout_get_alignment: + * @layout: a `Pango2Layout` + * + * Gets the alignment for the layout. + * + * The alignment determines how short lines are + * positioned within the available horizontal space. + * + * Return value: the alignment + */ +Pango2Alignment +pango2_layout_get_alignment (Pango2Layout *layout) +{ + g_return_val_if_fail (PANGO2_IS_LAYOUT (layout), PANGO2_ALIGN_NATURAL); + + return layout->alignment; +} + +/** + * pango2_layout_set_ellipsize: + * @layout: a `Pango2Layout` + * @ellipsize: the new ellipsization mode for @layout + * + * Sets the type of ellipsization to use for this layout. + * + * Depending on the ellipsization mode @ellipsize text is removed + * from the start, middle, or end of text so they fit within the + * width of layout set with [method@Pango2.Layout.set_width]. + * + * The default value is `PANGO2_ELLIPSIZE_NONE`. + */ +void +pango2_layout_set_ellipsize (Pango2Layout *layout, + Pango2EllipsizeMode ellipsize) +{ + g_return_if_fail (PANGO2_IS_LAYOUT (layout)); + + if (layout->ellipsize == ellipsize) + return; + + layout->ellipsize = ellipsize; + + g_object_notify_by_pspec (G_OBJECT (layout), props[PROP_ELLIPSIZE]); + layout_changed (layout); +} + +/** + * pango2_layout_get_ellipsize: + * @layout: a `Pango2Layout` + * + * Gets the type of ellipsization being performed for the layout. + * + * See [method@Pango2.Layout.set_ellipsize]. + * + * Return value: the current ellipsization mode for @layout + */ +Pango2EllipsizeMode +pango2_layout_get_ellipsize (Pango2Layout *layout) +{ + g_return_val_if_fail (PANGO2_IS_LAYOUT (layout), PANGO2_ELLIPSIZE_NONE); + + return layout->ellipsize; +} + +/** + * pango2_layout_set_auto_dir: + * @layout: a `Pango2Layout` + * @auto_dir: if %TRUE, compute the bidirectional base direction + * from the layout's contents + * + * Sets whether to calculate the base direction + * for the layout according to its contents. + * + * When this flag is on (the default), then paragraphs in + * @layout that begin with strong right-to-left characters + * (Arabic and Hebrew principally), will have right-to-left + * layout, paragraphs with letters from other scripts will + * have left-to-right layout. Paragraphs with only neutral + * characters get their direction from the surrounding + * paragraphs. + * + * When `FALSE`, the choice between left-to-right and right-to-left + * layout is done according to the base direction of the layout's + * `Pango2Context`. (See [method@Pango2.Context.set_base_dir]). + * + * When the auto-computed direction of a paragraph differs + * from the base direction of the context, the interpretation + * of `PANGO2_ALIGN_LEFT` and `PANGO2_ALIGN_RIGHT` are swapped. + */ +void +pango2_layout_set_auto_dir (Pango2Layout *layout, + gboolean auto_dir) +{ + g_return_if_fail (PANGO2_IS_LAYOUT (layout)); + + if (auto_dir == layout->auto_dir) + return; + + layout->auto_dir = auto_dir; + + g_object_notify_by_pspec (G_OBJECT (layout), props[PROP_AUTO_DIR]); + layout_changed (layout); +} + +/** + * pango2_layout_get_auto_dir: + * @layout: a `Pango2Layout` + * + * Gets whether to calculate the base direction for the layout + * according to its contents. + * + * See [method@Pango2.Layout.set_auto_dir]. + * + * Return value: `TRUE` if the bidirectional base direction + * is computed from the layout's contents, `FALSE` otherwise + */ +gboolean +pango2_layout_get_auto_dir (Pango2Layout *layout) +{ + g_return_val_if_fail (PANGO2_IS_LAYOUT (layout), TRUE); + + return layout->auto_dir; +} + +/* }}} */ +/* {{{ Miscellaneous */ + +/** + * pango2_layout_set_markup: + * @layout: a `Pango2Layout` + * @markup: marked-up text + * @length: length of @markup in bytes, or -1 if it is `NUL`-terminated + * + * Sets the layout text and attribute list from marked-up text. + * + * See [Pango2 Markup](pango2_markup.html)). + * + * Replaces the current text and attributes. + */ +void +pango2_layout_set_markup (Pango2Layout *layout, + const char *markup, + int length) +{ + Pango2AttrList *attrs; + char *text; + GError *error = NULL; + + g_return_if_fail (PANGO2_IS_LAYOUT (layout)); + g_return_if_fail (markup != NULL); + + if (!pango2_parse_markup (markup, length, 0, &attrs, &text, NULL, &error)) + { + g_warning ("pango2_layout_set_markup_with_accel: %s", error->message); + g_error_free (error); + return; + } + + g_free (layout->text); + layout->text = text; + layout->length = strlen (text); + + g_clear_pointer (&layout->attrs, pango2_attr_list_unref); + layout->attrs = attrs; + + g_object_notify_by_pspec (G_OBJECT (layout), props[PROP_TEXT]); + g_object_notify_by_pspec (G_OBJECT (layout), props[PROP_ATTRIBUTES]); + layout_changed (layout); +} + +/** + * pango2_layout_get_character_count: + * @layout: a `Pango2Layout` + * + * Returns the number of Unicode characters in the + * the text of the layout. + * + * Return value: the number of Unicode characters in @layout + */ +int +pango2_layout_get_character_count (Pango2Layout *layout) +{ + Pango2Line *line; + + g_return_val_if_fail (PANGO2_IS_LAYOUT (layout), 0); + + ensure_lines (layout); + + line = pango2_lines_get_lines (layout->lines)[0]; + + return line->data->n_chars; +} + +/* }}} */ +/* {{{ Output getters */ + +/** + * pango2_layout_get_lines: + * @layout: a `Pango2Layout` + * + * Gets the lines of the layout. + * + * The returned object will become invalid when any + * property of @layout is changed. Take a reference + * to keep it. + * + * Return value: (transfer none): a `Pango2Lines` object + * with the lines of @layout + */ +Pango2Lines * +pango2_layout_get_lines (Pango2Layout *layout) +{ + g_return_val_if_fail (PANGO2_IS_LAYOUT (layout), NULL); + + ensure_lines (layout); + + return layout->lines; +} + +/** + * pango2_layout_get_log_attrs: + * @layout: a `Pango2Layout` + * @n_attrs: (out): return location for the length of the array + * + * Gets the `Pango2LogAttr` array for the content of layout. + * + * The returned array becomes invalid when + * any properties of @layout change. Make a + * copy if you want to keep it. + * + * Returns: (transfer none): the `Pango2LogAttr` array + */ +const Pango2LogAttr * +pango2_layout_get_log_attrs (Pango2Layout *layout, + int *n_attrs) +{ + Pango2Line *line; + + g_return_val_if_fail (PANGO2_IS_LAYOUT (layout), NULL); + + ensure_lines (layout); + + line = pango2_lines_get_lines (layout->lines)[0]; + + if (n_attrs) + *n_attrs = line->data->n_chars + 1; + + return line->data->log_attrs; +} + +/** + * pango2_layout_get_iter: + * @layout: a `Pango2Layout` + * + * Returns an iterator to iterate over the visual extents + * of the layout. + * + * This is a convenience wrapper for [method@Pango2.Lines.get_iter]. + * + * Returns: the new `Pango2LineIter` + */ +Pango2LineIter * +pango2_layout_get_iter (Pango2Layout *layout) +{ + g_return_val_if_fail (PANGO2_IS_LAYOUT (layout), NULL); + + ensure_lines (layout); + + return pango2_lines_get_iter (layout->lines); +} + +/* }}} */ +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pango-layout.h b/pango2/pango-layout.h new file mode 100644 index 00000000..f3ad61dd --- /dev/null +++ b/pango2/pango-layout.h @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2000 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <glib-object.h> +#include <pango2/pango-types.h> +#include <pango2/pango-attributes.h> +#include <pango2/pango-lines.h> +#include <pango2/pango-tabs.h> + +G_BEGIN_DECLS + +#define PANGO2_TYPE_LAYOUT pango2_layout_get_type () + +PANGO2_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (Pango2Layout, pango2_layout, PANGO2, LAYOUT, GObject); + +PANGO2_AVAILABLE_IN_ALL +Pango2Layout * pango2_layout_new (Pango2Context *context); + +PANGO2_AVAILABLE_IN_ALL +Pango2Layout * pango2_layout_copy (Pango2Layout *layout); + +PANGO2_AVAILABLE_IN_ALL +guint pango2_layout_get_serial (Pango2Layout *layout); + +PANGO2_AVAILABLE_IN_ALL +Pango2Context * pango2_layout_get_context (Pango2Layout *layout); + +PANGO2_AVAILABLE_IN_ALL +void pango2_layout_context_changed (Pango2Layout *layout); + +PANGO2_AVAILABLE_IN_ALL +void pango2_layout_set_text (Pango2Layout *layout, + const char *text, + int length); +PANGO2_AVAILABLE_IN_ALL +const char * pango2_layout_get_text (Pango2Layout *layout); + +PANGO2_AVAILABLE_IN_ALL +void pango2_layout_set_markup (Pango2Layout *layout, + const char *markup, + int length); + +PANGO2_AVAILABLE_IN_ALL +int pango2_layout_get_character_count + (Pango2Layout *layout); + +PANGO2_AVAILABLE_IN_ALL +void pango2_layout_set_attributes (Pango2Layout *layout, + Pango2AttrList *attrs); + +PANGO2_AVAILABLE_IN_ALL +Pango2AttrList * pango2_layout_get_attributes (Pango2Layout *layout); + +PANGO2_AVAILABLE_IN_ALL +void pango2_layout_set_font_description + (Pango2Layout *layout, + const Pango2FontDescription *desc); + +PANGO2_AVAILABLE_IN_ALL +const Pango2FontDescription * + pango2_layout_get_font_description + (Pango2Layout *layout); + +PANGO2_AVAILABLE_IN_ALL +void pango2_layout_set_line_height + (Pango2Layout *layout, + float line_height); + +PANGO2_AVAILABLE_IN_ALL +float pango2_layout_get_line_height + (Pango2Layout *layout); + +PANGO2_AVAILABLE_IN_ALL +void pango2_layout_set_spacing (Pango2Layout *layout, + int spacing); + +PANGO2_AVAILABLE_IN_ALL +int pango2_layout_get_spacing (Pango2Layout *layout); + +PANGO2_AVAILABLE_IN_ALL +void pango2_layout_set_width (Pango2Layout *layout, + int width); + +PANGO2_AVAILABLE_IN_ALL +int pango2_layout_get_width (Pango2Layout *layout); + +PANGO2_AVAILABLE_IN_ALL +void pango2_layout_set_height (Pango2Layout *layout, + int height); + +PANGO2_AVAILABLE_IN_ALL +int pango2_layout_get_height (Pango2Layout *layout); + +PANGO2_AVAILABLE_IN_ALL +void pango2_layout_set_tabs (Pango2Layout *layout, + Pango2TabArray *tabs); + +PANGO2_AVAILABLE_IN_ALL +Pango2TabArray * pango2_layout_get_tabs (Pango2Layout *layout); + +PANGO2_AVAILABLE_IN_ALL +void pango2_layout_set_single_paragraph + (Pango2Layout *layout, + gboolean single_paragraph); +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_layout_get_single_paragraph + (Pango2Layout *layout); + +PANGO2_AVAILABLE_IN_ALL +void pango2_layout_set_wrap (Pango2Layout *layout, + Pango2WrapMode wrap); + +PANGO2_AVAILABLE_IN_ALL +Pango2WrapMode pango2_layout_get_wrap (Pango2Layout *layout); + +PANGO2_AVAILABLE_IN_ALL +void pango2_layout_set_indent (Pango2Layout *layout, + int indent); + +PANGO2_AVAILABLE_IN_ALL +int pango2_layout_get_indent (Pango2Layout *layout); + +PANGO2_AVAILABLE_IN_ALL +void pango2_layout_set_alignment (Pango2Layout *layout, + Pango2Alignment alignment); + +PANGO2_AVAILABLE_IN_ALL +Pango2Alignment pango2_layout_get_alignment (Pango2Layout *layout); + +PANGO2_AVAILABLE_IN_ALL +void pango2_layout_set_ellipsize (Pango2Layout *layout, + Pango2EllipsizeMode ellipsize); + +PANGO2_AVAILABLE_IN_ALL +Pango2EllipsizeMode pango2_layout_get_ellipsize (Pango2Layout *layout); + +PANGO2_AVAILABLE_IN_ALL +void pango2_layout_set_auto_dir (Pango2Layout *layout, + gboolean auto_dir); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_layout_get_auto_dir (Pango2Layout *layout); + +PANGO2_AVAILABLE_IN_ALL +Pango2Lines * pango2_layout_get_lines (Pango2Layout *layout); + +PANGO2_AVAILABLE_IN_ALL +Pango2LineIter * pango2_layout_get_iter (Pango2Layout *layout); + +PANGO2_AVAILABLE_IN_ALL +const Pango2LogAttr * pango2_layout_get_log_attrs (Pango2Layout *layout, + int *n_attrs); + +typedef enum { + PANGO2_LAYOUT_SERIALIZE_DEFAULT = 0, + PANGO2_LAYOUT_SERIALIZE_CONTEXT = 1 << 0, + PANGO2_LAYOUT_SERIALIZE_OUTPUT = 1 << 1, +} Pango2LayoutSerializeFlags; + +PANGO2_AVAILABLE_IN_ALL +GBytes * pango2_layout_serialize (Pango2Layout *layout, + Pango2LayoutSerializeFlags flags); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_layout_write_to_file (Pango2Layout *layout, + const char *filename); + +#define PANGO2_LAYOUT_DESERIALIZE_ERROR (pango2_layout_deserialize_error_quark ()) + +/** + * Pango2LayoutDeserializeError: + * @PANGO2_LAYOUT_DESERIALIZE_INVALID: Unspecified error + * @PANGO2_LAYOUT_DESERIALIZE_INVALID_VALUE: A JSon value could not be + * interpreted + * @PANGO2_LAYOUT_DESERIALIZE_MISSING_VALUE: A required JSon member was + * not found + * + * Errors that can be returned by [func@Pango2.Layout.deserialize]. + */ +typedef enum { + PANGO2_LAYOUT_DESERIALIZE_INVALID, + PANGO2_LAYOUT_DESERIALIZE_INVALID_VALUE, + PANGO2_LAYOUT_DESERIALIZE_MISSING_VALUE, +} Pango2LayoutDeserializeError; + +typedef enum { + PANGO2_LAYOUT_DESERIALIZE_DEFAULT = 0, + PANGO2_LAYOUT_DESERIALIZE_CONTEXT = 1 << 0, +} Pango2LayoutDeserializeFlags; + +PANGO2_AVAILABLE_IN_ALL +GQuark pango2_layout_deserialize_error_quark (void); + +PANGO2_AVAILABLE_IN_ALL +Pango2Layout * pango2_layout_deserialize (Pango2Context *context, + GBytes *bytes, + Pango2LayoutDeserializeFlags flags, + GError **error); + +G_END_DECLS diff --git a/pango2/pango-line-breaker.c b/pango2/pango-line-breaker.c new file mode 100644 index 00000000..1049fe5e --- /dev/null +++ b/pango2/pango-line-breaker.c @@ -0,0 +1,2663 @@ +#include "config.h" + +#include "pango-line-breaker.h" +#include "pango-line-private.h" + +#include "pango-tabs.h" +#include "pango-impl-utils.h" +#include "pango-attributes-private.h" +#include "pango-attr-private.h" +#include "pango-attr-list-private.h" +#include "pango-attr-iterator-private.h" +#include "pango-item-private.h" +#include "pango-bidi-private.h" + +#include <locale.h> + +#include <hb-ot.h> + +#if 0 +# define DEBUG1(...) g_debug (__VA_ARGS__) +#else +# define DEBUG1(...) do { } while (0) +#endif + +/** + * Pango2LineBreaker: + * + * A `Pango2LineBreaker` breaks text into lines. + * + * To use a `Pango2LineBreaker`, you must call [method@Pango2.LineBreaker.add_text] + * to provide text that you want to break into lines, plus possibly attributes + * to influence the formatting. + * + * Then you can call [method@Pango2.LineBreaker.next_line] repeatedly to obtain + * `Pango2Line` objects for the text, one by one. + * + * `Pango2LineBreaker` is meant to enable use cases like flowing text around images, + * or shaped paragraphs. For simple formatting needs, [class@Pango2.Layout] + * is probably more convenient to use. + */ + +typedef struct _LastTabState LastTabState; +struct _LastTabState +{ + Pango2GlyphString *glyphs; + int index; + int width; + int pos; + Pango2TabAlign align; + gunichar decimal; +}; + +struct _Pango2LineBreaker +{ + GObject parent_instance; + + /* Properties */ + Pango2Context *context; + Pango2Direction base_dir; + Pango2TabArray *tabs; + + /* Data that we're building lines from, shared among all the lines */ + GSList *datas; /* Queued up LineData */ + LineData *data; /* The LineData we're currently processing */ + GList *data_items; /* Original items for data (only used for undoing) */ + GList *items; /* The remaining unprocessed items for data */ + Pango2AttrList *render_attrs; /* Attributes to be re-added after line breaking */ + + /* Arguments to next_line, for use while processing the next line */ + Pango2WrapMode line_wrap; + Pango2EllipsizeMode line_ellipsize; + + int tab_width; /* Cached width of a tab. -1 == not yet calculated */ + int hyphen_width; /* Cached width of a hyphen. -1 == not yet calculated */ + gunichar decimal; /* Cached decimal point. 0 == not yet calculated */ + + /* State for line breaking */ + int n_lines; /* Line count, starting from 0 */ + Pango2GlyphString *glyphs; /* Glyphs for the first item in self->items */ + int start_offset; /* Character offset of first item in self->items in self->data->text */ + ItemProperties properties; /* Properties of the first item in self->items */ + int *log_widths; /* Logical widths for th efirst item in self->items */ + int num_log_widths; /* Length o fo log_widths */ + int log_widths_offset; /* Offset into log_widths to the point corresponding to + * the remaining portion of the fist item + */ + int line_start_index; /* Byte offset of line in self->data->text */ + int line_start_offset; /* Character offset of line in self->data->text */ + + int line_x; /* X offset for current line */ + int line_width; /* Goal width of current line; < 0 for unlimited */ + int remaining_width; /* Amount of spac remaining on line; < 0 for unlimited */ + + gboolean at_paragraph_start; /* TRUE if the next line starts a new paragraph */ + + GList *baseline_shifts; + + LastTabState last_tab; +}; + +struct _Pango2LineBreakerClass +{ + GObjectClass parent_class; +}; + +/* {{{ Utilities */ + +static LineData * +make_line_data (Pango2LineBreaker *self, + const char *text, + int length, + Pango2AttrList *attrs) +{ + LineData *data; + + if (length < 0) + length = strlen (text); + + data = line_data_new (); + + if (self->base_dir == PANGO2_DIRECTION_NEUTRAL) + { + data->direction = pango2_find_base_dir (text, length); + + if (data->direction == PANGO2_DIRECTION_NEUTRAL) + data->direction = pango2_context_get_base_dir (self->context); + } + else + data->direction = self->base_dir; + + data->text = g_strndup (text, length); + data->length = length; + data->n_chars = g_utf8_strlen (text, length); + if (attrs) + data->attrs = pango2_attr_list_copy (attrs); + + return data; +} + +static gboolean +item_is_paragraph_separator (Pango2LineBreaker *self, + Pango2Item *item) +{ + gunichar ch; + + if (self->properties.no_paragraph_break) + return FALSE; + + ch = g_utf8_get_char (self->data->text + item->offset); + + return ch == '\r' || ch == '\n' || ch == 0x2029; +} + +static void +apply_attributes_to_items (GList *items, + Pango2AttrList *attrs) +{ + GList *l; + Pango2AttrIterator iter; + + if (!attrs) + return; + + pango2_attr_list_init_iterator (attrs, &iter); + + for (l = items; l; l = l->next) + { + Pango2Item *item = l->data; + pango2_item_apply_attrs (item, &iter); + } + + pango2_attr_iterator_clear (&iter); +} + +static Pango2LogAttr * +get_log_attrs (LineData *data, + GList *items) +{ + Pango2LogAttr *log_attrs; + int offset; + + log_attrs = g_new0 (Pango2LogAttr, (data->n_chars + 1)); + + pango2_default_break (data->text, + data->length, + log_attrs, + data->n_chars + 1); + + offset = 0; + for (GList *l = items; l; l = l->next) + { + Pango2Item *item = l->data; + + pango2_tailor_break (data->text + item->offset, + item->length, + &item->analysis, + item->offset, + log_attrs + offset, + item->num_chars + 1); + + offset += item->num_chars; + } + + if (data->attrs) + pango2_attr_break (data->text, + data->length, + data->attrs, + 0, + log_attrs, + data->n_chars + 1); + + return log_attrs; +} + +static void +ensure_items (Pango2LineBreaker *self) +{ + Pango2AttrList *itemize_attrs = NULL; + Pango2AttrList *shape_attrs = NULL; + + if (self->items) + return; + + if (!self->data && self->datas) + { + self->data = self->datas->data; + self->datas = g_slist_remove (self->datas, self->data); + } + + if (!self->data) + return; + + self->render_attrs = pango2_attr_list_copy (self->data->attrs); + if (self->render_attrs) + { + itemize_attrs = pango2_attr_list_filter (self->render_attrs, pango2_attribute_affects_itemization, NULL); + shape_attrs = pango2_attr_list_filter (self->render_attrs, pango2_attribute_affects_break_or_shape, NULL); + } + + self->items = pango2_itemize_with_font (self->context, + self->data->direction, + self->data->text, + 0, + self->data->length, + itemize_attrs, + NULL, + NULL); + + apply_attributes_to_items (self->items, shape_attrs); + + pango2_attr_list_unref (itemize_attrs); + pango2_attr_list_unref (shape_attrs); + + self->data->log_attrs = get_log_attrs (self->data, self->items); + + self->items = pango2_itemize_post_process_items (self->context, + self->data->text, + self->data->log_attrs, + self->items); + + g_assert (self->data_items == NULL); + self->data_items = g_list_copy_deep (self->items, (GCopyFunc) pango2_item_copy, NULL); + + self->hyphen_width = -1; + self->tab_width = -1; + + self->start_offset = 0; + self->line_start_offset = 0; + self->line_start_index = 0; + + g_list_free_full (self->baseline_shifts, g_free); + self->baseline_shifts = NULL; + g_clear_pointer (&self->glyphs, pango2_glyph_string_free); + g_clear_pointer (&self->log_widths, g_free); + self->num_log_widths = 0; + self->log_widths_offset = 0; + + self->remaining_width = -1; + self->at_paragraph_start = TRUE; +} + +/* The resolved direction for the line is always one + * of LTR/RTL; not a week or neutral directions + */ +static Pango2Direction +get_resolved_dir (Pango2LineBreaker *self) +{ + Pango2Direction dir; + + ensure_items (self); + + if (!self->data) + return PANGO2_DIRECTION_NEUTRAL; + + switch (self->data->direction) + { + default: + case PANGO2_DIRECTION_LTR: + case PANGO2_DIRECTION_WEAK_LTR: + case PANGO2_DIRECTION_NEUTRAL: + dir = PANGO2_DIRECTION_LTR; + break; + case PANGO2_DIRECTION_RTL: + case PANGO2_DIRECTION_WEAK_RTL: + dir = PANGO2_DIRECTION_RTL; + break; + } + + /* The direction vs. gravity dance: + * - If gravity is SOUTH, leave direction untouched. + * - If gravity is NORTH, switch direction. + * - If gravity is EAST, set to LTR, as + * it's a clockwise-rotated layout, so the rotated + * top is unrotated left. + * - If gravity is WEST, set to RTL, as + * it's a counter-clockwise-rotated layout, so the rotated + * top is unrotated right. + * + * A similar dance is performed in pango-context.c: + * itemize_state_add_character(). Keep in synch. + */ + + switch (pango2_context_get_gravity (self->context)) + { + default: + case PANGO2_GRAVITY_AUTO: + case PANGO2_GRAVITY_SOUTH: + break; + case PANGO2_GRAVITY_NORTH: + dir = PANGO2_DIRECTION_LTR + PANGO2_DIRECTION_RTL - dir; + break; + case PANGO2_GRAVITY_EAST: + dir = PANGO2_DIRECTION_LTR; + break; + case PANGO2_GRAVITY_WEST: + dir = PANGO2_DIRECTION_RTL; + break; + } + + return dir; +} + +static gboolean +should_ellipsize_current_line (Pango2LineBreaker *self, + Pango2Line *line) +{ + return self->line_ellipsize != PANGO2_ELLIPSIZE_NONE && self->line_width >= 0; +} + +static void +get_decimal_prefix_width (Pango2Item *item, + Pango2GlyphString *glyphs, + const char *text, + gunichar decimal, + int *width, + gboolean *found) +{ + Pango2GlyphItem glyph_item = { item, glyphs, 0, 0, 0 }; + int *log_widths; + int i; + const char *p; + + log_widths = g_new (int, item->num_chars); + + pango2_glyph_item_get_logical_widths (&glyph_item, text, log_widths); + + *width = 0; + *found = FALSE; + + for (i = 0, p = text + item->offset; i < item->num_chars; i++, p = g_utf8_next_char (p)) + { + if (g_utf8_get_char (p) == decimal) + { + *width += log_widths[i] / 2; + *found = TRUE; + break; + } + + *width += log_widths[i]; + } + + g_free (log_widths); +} + +static int +pango2_line_compute_width (Pango2Line *line) +{ + int width = 0; + + /* Compute the width of the line currently - inefficient, but easier + * than keeping the current width of the line up to date everywhere + */ + for (GSList *l = line->runs; l; l = l->next) + { + Pango2GlyphItem *run = l->data; + width += pango2_glyph_string_get_width (run->glyphs); + } + + return width; +} + +static inline int +get_line_width (Pango2LineBreaker *self, + Pango2Line *line) +{ + if (self->remaining_width > -1) + return self->line_width - self->remaining_width; + + return pango2_line_compute_width (line); +} + +static inline void +ensure_decimal (Pango2LineBreaker *self) +{ + if (self->decimal == 0) + self->decimal = g_utf8_get_char (localeconv ()->decimal_point); +} + +static void +ensure_tab_width (Pango2LineBreaker *self) +{ + if (self->tab_width == -1) + { + /* Find out how wide 8 spaces are in the context's default + * font. Utter performance killer. :-( + */ + Pango2GlyphString *glyphs = pango2_glyph_string_new (); + Pango2Item *item; + GList *items; + Pango2Attribute *attr; + Pango2AttrList *attrs; + Pango2AttrList tmp_attrs; + Pango2FontDescription *font_desc = pango2_font_description_copy_static (pango2_context_get_font_description (self->context)); + Pango2Language *language = NULL; + Pango2ShapeFlags shape_flags = PANGO2_SHAPE_NONE; + Pango2Direction dir; + + if (pango2_context_get_round_glyph_positions (self->context)) + shape_flags |= PANGO2_SHAPE_ROUND_POSITIONS; + + attrs = self->data->attrs; + if (attrs) + { + Pango2AttrIterator iter; + + pango2_attr_list_init_iterator (attrs, &iter); + pango2_attr_iterator_get_font (&iter, font_desc, &language, NULL); + pango2_attr_iterator_clear (&iter); + } + + pango2_attr_list_init (&tmp_attrs); + attr = pango2_attr_font_desc_new (font_desc); + pango2_font_description_free (font_desc); + pango2_attr_list_insert_before (&tmp_attrs, attr); + + if (language) + { + attr = pango2_attr_language_new (language); + pango2_attr_list_insert_before (&tmp_attrs, attr); + } + + dir = pango2_context_get_base_dir (self->context); + items = pango2_itemize (self->context, dir, " ", 0, 1, &tmp_attrs); + + if (attrs != self->data->attrs) + { + pango2_attr_list_unref (attrs); + attrs = NULL; + } + + pango2_attr_list_destroy (&tmp_attrs); + + item = items->data; + pango2_shape (" ", 8, " ", 8, &item->analysis, glyphs, shape_flags); + + pango2_item_free (item); + g_list_free (items); + + self->tab_width = pango2_glyph_string_get_width (glyphs); + + pango2_glyph_string_free (glyphs); + + /* We need to make sure the tab_width is > 0 so finding tab positions + * terminates. This check should be necessary only under extreme + * problems with the font. + */ + if (self->tab_width <= 0) + self->tab_width = 50 * PANGO2_SCALE; /* pretty much arbitrary */ + } +} + +static int +get_item_letter_spacing (Pango2Item *item) +{ + ItemProperties properties; + + pango2_item_get_properties (item, &properties); + + return properties.letter_spacing; +} + +static void +pad_glyphstring_right (Pango2LineBreaker *self, + Pango2GlyphString *glyphs, + int adjustment) +{ + int glyph = glyphs->num_glyphs - 1; + + while (glyph >= 0 && glyphs->glyphs[glyph].geometry.width == 0) + glyph--; + + if (glyph < 0) + return; + + self->remaining_width -= adjustment; + + glyphs->glyphs[glyph].geometry.width += adjustment; + if (glyphs->glyphs[glyph].geometry.width < 0) + { + self->remaining_width += glyphs->glyphs[glyph].geometry.width; + glyphs->glyphs[glyph].geometry.width = 0; + } +} + +static void +pad_glyphstring_left (Pango2LineBreaker *self, + Pango2GlyphString *glyphs, + int adjustment) +{ + int glyph = 0; + + while (glyph < glyphs->num_glyphs && glyphs->glyphs[glyph].geometry.width == 0) + glyph++; + + if (glyph == glyphs->num_glyphs) + return; + + self->remaining_width -= adjustment; + + glyphs->glyphs[glyph].geometry.width += adjustment; + glyphs->glyphs[glyph].geometry.x_offset += adjustment; +} + +static gboolean +is_tab_run (Pango2Line *line, + Pango2GlyphItem *run) +{ + return line->data->text[run->item->offset] == '\t'; +} + +static GSList * +reorder_runs_recurse (GSList *items, + int n_items) +{ + GSList *tmp_list, *level_start_node; + int i, level_start_i; + int min_level = G_MAXINT; + GSList *result = NULL; + + if (n_items == 0) + return NULL; + + tmp_list = items; + for (i = 0; i < n_items; i++) + { + Pango2GlyphItem *run = tmp_list->data; + + min_level = MIN (min_level, run->item->analysis.level); + + tmp_list = tmp_list->next; + } + + level_start_i = 0; + level_start_node = items; + tmp_list = items; + for (i=0; i<n_items; i++) + { + Pango2GlyphItem *run = tmp_list->data; + + if (run->item->analysis.level == min_level) + { + if (min_level % 2) + { + if (i > level_start_i) + result = g_slist_concat (reorder_runs_recurse (level_start_node, i - level_start_i), result); + result = g_slist_prepend (result, run); + } + else + { + if (i > level_start_i) + result = g_slist_concat (result, reorder_runs_recurse (level_start_node, i - level_start_i)); + result = g_slist_append (result, run); + } + + level_start_i = i + 1; + level_start_node = tmp_list->next; + } + + tmp_list = tmp_list->next; + } + + if (min_level % 2) + { + if (i > level_start_i) + result = g_slist_concat (reorder_runs_recurse (level_start_node, i - level_start_i), result); + } + else + { + if (i > level_start_i) + result = g_slist_concat (result, reorder_runs_recurse (level_start_node, i - level_start_i)); + } + + return result; +} + +static void +pango2_line_reorder (Pango2Line *line) +{ + GSList *logical_runs = line->runs; + GSList *tmp_list; + gboolean all_even, all_odd; + guint8 level_or = 0, level_and = 1; + int length = 0; + + /* Check if all items are in the same direction, in that case, the + * line does not need modification and we can avoid the expensive + * reorder runs recurse procedure. + */ + for (tmp_list = logical_runs; tmp_list != NULL; tmp_list = tmp_list->next) + { + Pango2GlyphItem *run = tmp_list->data; + + level_or |= run->item->analysis.level; + level_and &= run->item->analysis.level; + + length++; + } + + /* If none of the levels had the LSB set, all numbers were even. */ + all_even = (level_or & 0x1) == 0; + + /* If all of the levels had the LSB set, all numbers were odd. */ + all_odd = (level_and & 0x1) == 1; + + if (!all_even && !all_odd) + { + line->runs = reorder_runs_recurse (logical_runs, length); + g_slist_free (logical_runs); + } + else if (all_odd) + line->runs = g_slist_reverse (logical_runs); +} + +static int +compute_n_chars (Pango2Line *line) +{ + int n_chars = 0; + + for (GSList *l = line->runs; l; l = l->next) + { + Pango2GlyphItem *run = l->data; + n_chars += run->item->num_chars; + } + + return n_chars; +} + +/* }}} */ +/* {{{ Line Breaking */ + +static void +get_tab_pos (Pango2LineBreaker *self, + Pango2Line *line, + int index, + int *tab_pos, + Pango2TabAlign *alignment, + gunichar *decimal, + gboolean *is_default) +{ + int n_tabs; + gboolean in_pixels; + int offset = 0; + + offset = self->line_x; + + if (self->tabs) + { + n_tabs = pango2_tab_array_get_size (self->tabs); + in_pixels = pango2_tab_array_get_positions_in_pixels (self->tabs); + *is_default = FALSE; + } + else + { + n_tabs = 0; + in_pixels = FALSE; + *is_default = TRUE; + } + + if (index < n_tabs) + { + pango2_tab_array_get_tab (self->tabs, index, alignment, tab_pos); + + if (in_pixels) + *tab_pos *= PANGO2_SCALE; + + *decimal = pango2_tab_array_get_decimal_point (self->tabs, index); + } + else if (n_tabs > 0) + { + /* Extrapolate tab position, repeating the last tab gap to infinity. */ + int last_pos = 0; + int next_to_last_pos = 0; + int tab_width; + + pango2_tab_array_get_tab (self->tabs, n_tabs - 1, alignment, &last_pos); + *decimal = pango2_tab_array_get_decimal_point (self->tabs, n_tabs - 1); + + if (n_tabs > 1) + pango2_tab_array_get_tab (self->tabs, n_tabs - 2, NULL, &next_to_last_pos); + else + next_to_last_pos = 0; + + if (in_pixels) + { + next_to_last_pos *= PANGO2_SCALE; + last_pos *= PANGO2_SCALE; + } + + if (last_pos > next_to_last_pos) + tab_width = last_pos - next_to_last_pos; + else + tab_width = self->tab_width; + + *tab_pos = last_pos + tab_width * (index - n_tabs + 1); + } + else + { + /* No tab array set, so use default tab width */ + *tab_pos = self->tab_width * index; + *alignment = PANGO2_TAB_LEFT; + *decimal = 0; + } + + *tab_pos -= offset; +} + +static void +shape_tab (Pango2LineBreaker *self, + Pango2Line *line, + int current_width, + Pango2Item *item, + Pango2GlyphString *glyphs) +{ + int i, space_width; + int tab_pos; + Pango2TabAlign tab_align; + gunichar tab_decimal; + + pango2_glyph_string_set_size (glyphs, 1); + + if (self->properties.showing_space) + glyphs->glyphs[0].glyph = PANGO2_GET_UNKNOWN_GLYPH ('\t'); + else + glyphs->glyphs[0].glyph = PANGO2_GLYPH_EMPTY; + + glyphs->glyphs[0].geometry.x_offset = 0; + glyphs->glyphs[0].geometry.y_offset = 0; + glyphs->glyphs[0].attr.is_cluster_start = 1; + glyphs->glyphs[0].attr.is_color = 0; + + glyphs->log_clusters[0] = 0; + + ensure_tab_width (self); + space_width = self->tab_width / 8; + + for (i = self->last_tab.index; ; i++) + { + gboolean is_default; + + get_tab_pos (self, line, i, &tab_pos, &tab_align, &tab_decimal, &is_default); + + /* Make sure there is at least a space-width of space between + * tab-aligned text and the text before it. However, only do + * this if no tab array is set on the line breaker, ie. using default + * tab positions. If the user has set tab positions, respect it + * to the pixel. + */ + if (tab_pos >= current_width + (is_default ? space_width : 1)) + { + glyphs->glyphs[0].geometry.width = tab_pos - current_width; + break; + } + } + + if (tab_decimal == 0) + { + ensure_decimal (self); + tab_decimal = self->decimal; + } + + self->last_tab.glyphs = glyphs; + self->last_tab.index = i; + self->last_tab.width = current_width; + self->last_tab.pos = tab_pos; + self->last_tab.align = tab_align; + self->last_tab.decimal = tab_decimal; +} + +static inline gboolean +can_break_at (Pango2LineBreaker *self, + int offset, + Pango2WrapMode wrap) +{ + if (offset == self->data->n_chars) + return TRUE; + else if (wrap == PANGO2_WRAP_CHAR) + return self->data->log_attrs[offset].is_char_break; + else + return self->data->log_attrs[offset].is_line_break; +} + +static inline gboolean +can_break_in (Pango2LineBreaker *self, + int start_offset, + int num_chars, + gboolean allow_break_at_start) +{ + for (int i = allow_break_at_start ? 0 : 1; i < num_chars; i++) + { + if (can_break_at (self, start_offset + i, self->line_wrap)) + return TRUE; + } + return FALSE; +} + +static inline void +distribute_letter_spacing (int letter_spacing, + int *space_left, + int *space_right) +{ + *space_left = letter_spacing / 2; + + /* hinting */ + if ((letter_spacing & (PANGO2_SCALE - 1)) == 0) + *space_left = PANGO2_UNITS_ROUND (*space_left); + *space_right = letter_spacing - *space_left; +} + +static void +pango2_shape_shape (const char *text, + unsigned int n_chars, + ShapeData *shape, + Pango2GlyphString *glyphs) +{ + unsigned int i; + const char *p; + + pango2_glyph_string_set_size (glyphs, n_chars); + + for (i = 0, p = text; i < n_chars; i++, p = g_utf8_next_char (p)) + { + glyphs->glyphs[i].glyph = PANGO2_GLYPH_EMPTY; + glyphs->glyphs[i].geometry.x_offset = 0; + glyphs->glyphs[i].geometry.y_offset = 0; + glyphs->glyphs[i].geometry.width = shape->logical_rect.width; + glyphs->glyphs[i].attr.is_cluster_start = 1; + + glyphs->log_clusters[i] = p - text; + } +} + +static Pango2GlyphString * +shape_run (Pango2LineBreaker *self, + Pango2Line *line, + Pango2Item *item) +{ + Pango2GlyphString *glyphs = pango2_glyph_string_new (); + + if (self->data->text[item->offset] == '\t') + shape_tab (self, line, get_line_width (self, line), item, glyphs); + else + { + Pango2ShapeFlags shape_flags = PANGO2_SHAPE_NONE; + + if (pango2_context_get_round_glyph_positions (self->context)) + shape_flags |= PANGO2_SHAPE_ROUND_POSITIONS; + + if (self->properties.shape) + pango2_shape_shape (self->data->text + item->offset, item->num_chars, + (ShapeData *)self->properties.shape->pointer_value, + glyphs); + else + pango2_shape_item (item, + self->data->text, self->data->length, + self->data->log_attrs + self->start_offset, + glyphs, + shape_flags); + + if (self->properties.letter_spacing) + { + Pango2GlyphItem glyph_item; + int space_left, space_right; + + glyph_item.item = item; + glyph_item.glyphs = glyphs; + + pango2_glyph_item_letter_space (&glyph_item, + self->data->text, + self->data->log_attrs + self->start_offset, + self->properties.letter_spacing); + + distribute_letter_spacing (self->properties.letter_spacing, &space_left, &space_right); + + glyphs->glyphs[0].geometry.width += space_left; + glyphs->glyphs[0].geometry.x_offset += space_left; + glyphs->glyphs[glyphs->num_glyphs - 1].geometry.width += space_right; + } + + if (self->last_tab.glyphs != NULL) + { + int w; + + g_assert (self->last_tab.glyphs->num_glyphs == 1); + + /* Update the width of the current tab to position this run properly */ + + w = self->last_tab.pos - self->last_tab.width; + + if (self->last_tab.align == PANGO2_TAB_RIGHT) + w -= pango2_glyph_string_get_width (glyphs); + else if (self->last_tab.align == PANGO2_TAB_CENTER) + w -= pango2_glyph_string_get_width (glyphs) / 2; + else if (self->last_tab.align == PANGO2_TAB_DECIMAL) + { + int width; + gboolean found; + + get_decimal_prefix_width (item, glyphs, self->data->text, self->last_tab.decimal, &width, &found); + + w -= width; + } + + self->last_tab.glyphs->glyphs[0].geometry.width = MAX (w, 0); + } + } + + return glyphs; +} + +static void +free_run (Pango2GlyphItem *run, + gpointer data) +{ + gboolean free_item = data != NULL; + if (free_item) + pango2_item_free (run->item); + + pango2_glyph_string_free (run->glyphs); + g_slice_free (Pango2GlyphItem, run); +} + +static Pango2Item * +uninsert_run (Pango2Line *line) +{ + Pango2GlyphItem *run; + Pango2Item *item; + + GSList *tmp_node = line->runs; + + run = tmp_node->data; + item = run->item; + + line->runs = tmp_node->next; + line->length -= item->length; + + g_slist_free_1 (tmp_node); + free_run (run, NULL); + + return item; +} + +static void +insert_run (Pango2LineBreaker *self, + Pango2Line *line, + Pango2Item *run_item, + Pango2GlyphString *glyphs, + gboolean last_run) +{ + Pango2GlyphItem *run = g_slice_new (Pango2GlyphItem); + + run->item = run_item; + + if (glyphs) + run->glyphs = glyphs; + else if (last_run && + self->log_widths_offset == 0 && + !(run_item->analysis.flags & PANGO2_ANALYSIS_FLAG_NEED_HYPHEN)) + { + run->glyphs = self->glyphs; + self->glyphs = NULL; + } + else + run->glyphs = shape_run (self, line, run_item); + + if (last_run && self->glyphs) + { + pango2_glyph_string_free (self->glyphs); + self->glyphs = NULL; + } + + line->runs = g_slist_prepend (line->runs, run); + line->length += run_item->length; + + if (self->last_tab.glyphs && run->glyphs != self->last_tab.glyphs) + { + gboolean found_decimal = FALSE; + int width; + + /* Adjust the tab position so placing further runs will continue to + * maintain the tab placement. In the case of decimal tabs, we are + * done once we've placed the run with the decimal point. + */ + + if (self->last_tab.align == PANGO2_TAB_RIGHT) + self->last_tab.width += pango2_glyph_string_get_width (run->glyphs); + else if (self->last_tab.align == PANGO2_TAB_CENTER) + self->last_tab.width += pango2_glyph_string_get_width (run->glyphs) / 2; + else if (self->last_tab.align == PANGO2_TAB_DECIMAL) + { + int width; + + get_decimal_prefix_width (run->item, run->glyphs, line->data->text, self->last_tab.decimal, &width, &found_decimal); + + self->last_tab.width += width; + } + + width = MAX (self->last_tab.pos - self->last_tab.width, 0); + self->last_tab.glyphs->glyphs[0].geometry.width = width; + + if (found_decimal || width == 0) + self->last_tab.glyphs = NULL; + } +} + +static gboolean +break_needs_hyphen (Pango2LineBreaker *self, + int pos) +{ + return self->data->log_attrs[self->start_offset + pos].break_inserts_hyphen || + self->data->log_attrs[self->start_offset + pos].break_removes_preceding; +} + + +static int +find_hyphen_width (Pango2Item *item) +{ + hb_font_t *hb_font; + hb_codepoint_t glyph; + + if (!item->analysis.font) + return 0; + + /* This is not technically correct, since + * a) we may end up inserting a different hyphen + * b) we should reshape the entire run + * But it is close enough in practice + */ + hb_font = pango2_font_get_hb_font (item->analysis.font); + if (hb_font_get_nominal_glyph (hb_font, 0x2010, &glyph) || + hb_font_get_nominal_glyph (hb_font, '-', &glyph)) + return hb_font_get_glyph_h_advance (hb_font, glyph); + + return 0; +} + +static inline void +ensure_hyphen_width (Pango2LineBreaker *self) +{ + if (self ->hyphen_width < 0) + { + Pango2Item *item = self->items->data; + self->hyphen_width = find_hyphen_width (item); + } +} + +static int +find_break_extra_width (Pango2LineBreaker *self, + int pos) +{ + /* Check whether to insert a hyphen, + * or whether we are breaking after one of those + * characters that turn into a hyphen, + * or after a space. + */ + if (self->data->log_attrs[self->start_offset + pos].break_inserts_hyphen) + { + ensure_hyphen_width (self); + + if (self->data->log_attrs[self->start_offset + pos].break_removes_preceding && pos > 0) + return self->hyphen_width - self->log_widths[self->log_widths_offset + pos - 1]; + else + return self->hyphen_width; + } + else if (pos > 0 && + self->data->log_attrs[self->start_offset + pos - 1].is_white) + { + return - self->log_widths[self->log_widths_offset + pos - 1]; + } + + return 0; +} + +static inline void +compute_log_widths (Pango2LineBreaker *self) +{ + Pango2Item *item = self->items->data; + Pango2GlyphItem glyph_item = { item, self->glyphs }; + + if (item->num_chars > self->num_log_widths) + { + self->log_widths = g_renew (int, self->log_widths, item->num_chars); + self->num_log_widths = item->num_chars; + } + + g_assert (self->log_widths_offset == 0); + pango2_glyph_item_get_logical_widths (&glyph_item, self->data->text, self->log_widths); +} + +/* If last_tab is set, we've added a tab and remaining_width has been updated to + * account for its origin width, which is last_tab_pos - last_tab_width. shape_run + * updates the tab width, so we need to consider the delta when comparing + * against remaining_width. + */ +static int +tab_width_change (Pango2LineBreaker *self) +{ + if (self->last_tab.glyphs) + return self->last_tab.glyphs->glyphs[0].geometry.width - (self->last_tab.pos - self->last_tab.width); + + return 0; +} + +typedef enum +{ + BREAK_NONE_FIT, + BREAK_SOME_FIT, + BREAK_ALL_FIT, + BREAK_EMPTY_FIT, + BREAK_LINE_SEPARATOR, + BREAK_PARAGRAPH_SEPARATOR +} BreakResult; + +/* Tries to insert as much as possible of the item at the head of + * self->items onto @line. Five results are possible: + * + * %BREAK_NONE_FIT: Couldn't fit anything. + * %BREAK_SOME_FIT: The item was broken in the middle. + * %BREAK_ALL_FIT: Everything fit. + * %BREAK_EMPTY_FIT: Nothing fit, but that was ok, as we can break at the first char. + * %BREAK_LINE_SEPARATOR: Item begins with a line separator. + * %BREAK_PARAGRAPH_SEPARATOR: Item begins with a paragraph separator + * + * If @force_fit is %TRUE, then %BREAK_NONE_FIT will never + * be returned, a run will be added even if inserting the minimum amount + * will cause the line to overflow. This is used at the start of a line + * and until we've found at least some place to break. + * + * If @no_break_at_end is %TRUE, then %BREAK_ALL_FIT will never be + * returned even everything fits; the run will be broken earlier, + * or %BREAK_NONE_FIT returned. This is used when the end of the + * run is not a break position. + * + * This function is the core of our line-breaking, and it is long and involved. + * Here is an outline of the algorithm, without all the bookkeeping: + * + * if item appears to fit entirely + * measure it + * if it actually fits + * return BREAK_ALL_FIT + * + * retry_break: + * for each position p in the item + * if adding more is 'obviously' not going to help and we have a breakpoint + * exit the loop + * if p is a possible break position + * if p is 'obviously' going to fit + * bc = p + * else + * measure breaking at p (taking extra break width into account + * if we don't have a break candidate yet + * bc = p + * else + * if p is better than bc + * bc = p + * + * if bc does not fit and we can loosen break conditions + * loosen break conditions and retry break + * + * return bc + */ + +static BreakResult +process_item (Pango2LineBreaker *self, + Pango2Line *line, + gboolean force_fit, + gboolean no_break_at_end, + gboolean is_last_item) +{ + Pango2Item *item = self->items->data; + int width; + int extra_width; + int orig_extra_width; + int length; + int i; + int processing_new_item; + int num_chars; + int orig_width; + Pango2WrapMode wrap; + int break_num_chars; + int break_width; + int break_extra_width; + Pango2GlyphString *break_glyphs; + Pango2FontMetrics *metrics; + int safe_distance; + + DEBUG1 ("process item '%.*s'. Remaining width %d", + item->length, self->data->text + item->offset, + self->remaining_width); + + /* We don't want to shape more than necessary, so we keep the results + * of shaping a new item in self->glyphs, self->log_widths. Once + * we break off initial parts of the item, we update self->log_widths_offset + * to take that into account. Note that the widths we calculate from the + * log_widths are an approximation, because a) log_widths are just + * evenly divided for clusters, and b) clusters may change as we + * break in the middle (think ff- i). + * + * We use self->log_widths_offset != 0 to detect if we are dealing + * with the original item, or one that has been chopped off. + */ + if (!self->glyphs) + { + pango2_item_get_properties (item, &self->properties); + self->glyphs = shape_run (self, line, item); + self->log_widths_offset = 0; + processing_new_item = TRUE; + } + else + processing_new_item = FALSE; + + if (item_is_paragraph_separator (self, item)) + { + g_clear_pointer (&self->glyphs, pango2_glyph_string_free); + return BREAK_PARAGRAPH_SEPARATOR; + } + + /* Only one character has type G_UNICODE_LINE_SEPARATOR in Unicode 5.0; + * update this if that changes. + */ +#define LINE_SEPARATOR 0x2028 + + if (g_utf8_get_char (self->data->text + item->offset) == LINE_SEPARATOR && + !should_ellipsize_current_line (self, line)) + { + insert_run (self, line, item, NULL, TRUE); + self->log_widths_offset += item->num_chars; + + return BREAK_LINE_SEPARATOR; + } + + if (self->remaining_width < 0 && !no_break_at_end) /* Wrapping off */ + { + insert_run (self, line, item, NULL, TRUE); + DEBUG1 ("no wrapping, all-fit"); + return BREAK_ALL_FIT; + } + + if (processing_new_item) + { + compute_log_widths (self); + processing_new_item = FALSE; + } + + width = 0; + g_assert (self->log_widths_offset + item->num_chars <= self->num_log_widths); + for (i = 0; i < item->num_chars; i++) + width += self->log_widths[self->log_widths_offset + i]; + + if (self->data->text[item->offset] == '\t') + { + insert_run (self, line, item, NULL, TRUE); + self->remaining_width -= width; + self->remaining_width = MAX (self->remaining_width, 0); + + DEBUG1 ("tab run, all-fit"); + return BREAK_ALL_FIT; + } + + wrap = self->line_wrap; + if (!no_break_at_end && + can_break_at (self, self->start_offset + item->num_chars, wrap)) + { + extra_width = find_break_extra_width (self, item->num_chars); + } + else + extra_width = 0; + + if ((width + extra_width <= self->remaining_width || (item->num_chars == 1 && !line->runs) || + (self->last_tab.glyphs && self->last_tab.align != PANGO2_TAB_LEFT)) && + !no_break_at_end) + { + Pango2GlyphString *glyphs; + + DEBUG1 ("%d + %d <= %d", width, extra_width, self->remaining_width); + glyphs = shape_run (self, line, item); + + width = pango2_glyph_string_get_width (glyphs) + tab_width_change (self); + + if (width + extra_width <= self->remaining_width || (item->num_chars == 1 && !line->runs)) + { + insert_run (self, line, item, glyphs, TRUE); + + self->remaining_width -= width; + self->remaining_width = MAX (self->remaining_width, 0); + + DEBUG1 ("early accept '%.*s', all-fit, remaining %d", + item->length, self->data->text + item->offset, + self->remaining_width); + return BREAK_ALL_FIT; + } + + /* if it doesn't fit after shaping, discard and proceed to break the item */ + pango2_glyph_string_free (glyphs); + } + + /*** From here on, we look for a way to break item ***/ + + orig_width = width; + orig_extra_width = extra_width; + break_width = width; + break_extra_width = extra_width; + break_num_chars = item->num_chars; + wrap = self->line_wrap;; + break_glyphs = NULL; + + /* Add some safety margin here. If we are farther away from the end of the + * line than this, we don't look carefully at a break possibility. + */ + metrics = pango2_font_get_metrics (item->analysis.font, item->analysis.language); + safe_distance = pango2_font_metrics_get_approximate_char_width (metrics) * 3; + pango2_font_metrics_free (metrics); + + if (processing_new_item) + { + compute_log_widths (self); + processing_new_item = FALSE; + } + +retry_break: + + for (num_chars = 0, width = 0; num_chars < (no_break_at_end ? item->num_chars : (item->num_chars + 1)); num_chars++) + { + extra_width = find_break_extra_width (self, num_chars); + + /* We don't want to walk the entire item if we can help it, but + * we need to keep going at least until we've found a breakpoint + * that 'works' (as in, it doesn't overflow the budget we have, + * or there is no hope of finding a better one). + * + * We rely on the fact that MIN(width + extra_width, width) is + * monotonically increasing. + */ + + if (MIN (width + extra_width, width) > self->remaining_width + safe_distance && + break_num_chars < item->num_chars) + { + DEBUG1 ("at %d, MIN(%d, %d + %d) > %d + MARGIN, breaking at %d", + num_chars, width, extra_width, width, self->remaining_width, break_num_chars); + break; + } + + /* If there are no previous runs we have to take care to grab at least one char. */ + if (can_break_at (self, self->start_offset + num_chars, wrap) && + (num_chars > 0 || line->runs)) + { + DEBUG1 ("possible breakpoint: %d, extra_width %d", num_chars, extra_width); + if (num_chars == 0 || + width + extra_width < self->remaining_width - safe_distance) + { + DEBUG1 ("trivial accept"); + break_num_chars = num_chars; + break_width = width; + break_extra_width = extra_width; + } + else + { + int length; + int new_break_width; + Pango2Item *new_item; + Pango2GlyphString *glyphs; + + length = g_utf8_offset_to_pointer (self->data->text + item->offset, num_chars) - (self->data->text + item->offset); + + if (num_chars < item->num_chars) + { + new_item = pango2_item_split (item, length, num_chars); + + if (break_needs_hyphen (self, num_chars)) + new_item->analysis.flags |= PANGO2_ANALYSIS_FLAG_NEED_HYPHEN; + else + new_item->analysis.flags &= ~PANGO2_ANALYSIS_FLAG_NEED_HYPHEN; + } + else + new_item = item; + + glyphs = shape_run (self, line, new_item); + + new_break_width = pango2_glyph_string_get_width (glyphs) + tab_width_change (self); + + if (num_chars > 0 && + (item != new_item || !is_last_item) && /* We don't collapse space at the very end */ + self->data->log_attrs[self->start_offset + num_chars - 1].is_white) + extra_width = - self->log_widths[self->log_widths_offset + num_chars - 1]; + else if (item == new_item && !is_last_item && + break_needs_hyphen (self, num_chars)) + extra_width = self->hyphen_width; + else + extra_width = 0; + + DEBUG1 ("measured breakpoint %d: %d, extra %d", num_chars, new_break_width, extra_width); + + if (new_item != item) + { + pango2_item_free (new_item); + pango2_item_unsplit (item, length, num_chars); + } + + if (break_num_chars == item->num_chars || + new_break_width + extra_width <= self->remaining_width || + new_break_width + extra_width < break_width + break_extra_width) + { + DEBUG1 ("accept breakpoint %d: %d + %d <= %d + %d", + num_chars, new_break_width, extra_width, break_width, break_extra_width); + DEBUG1 ("replace bp %d by %d", break_num_chars, num_chars); + break_num_chars = num_chars; + break_width = new_break_width; + break_extra_width = extra_width; + + if (break_glyphs) + pango2_glyph_string_free (break_glyphs); + break_glyphs = glyphs; + } + else + { + DEBUG1 ("ignore breakpoint %d", num_chars); + pango2_glyph_string_free (glyphs); + } + } + } + + DEBUG1 ("bp now %d", break_num_chars); + if (num_chars < item->num_chars) + width += self->log_widths[self->log_widths_offset + num_chars]; + } + + if (wrap == PANGO2_WRAP_WORD_CHAR && + force_fit && + break_width + break_extra_width > self->remaining_width) + { + /* Try again, with looser conditions */ + DEBUG1 ("does not fit, try again with wrap-char"); + wrap = PANGO2_WRAP_CHAR; + break_num_chars = item->num_chars; + break_width = orig_width; + break_extra_width = orig_extra_width; + if (break_glyphs) + pango2_glyph_string_free (break_glyphs); + break_glyphs = NULL; + goto retry_break; + } + + if (force_fit || break_width + break_extra_width <= self->remaining_width) /* Successfully broke the item */ + { + if (self->remaining_width >= 0) + { + self->remaining_width -= break_width + break_extra_width; + self->remaining_width = MAX (self->remaining_width, 0); + } + + if (break_num_chars == item->num_chars) + { + if (can_break_at (self, self->start_offset + break_num_chars, wrap) && + break_needs_hyphen (self, break_num_chars)) + item->analysis.flags |= PANGO2_ANALYSIS_FLAG_NEED_HYPHEN; + + insert_run (self, line, item, NULL, TRUE); + + if (break_glyphs) + pango2_glyph_string_free (break_glyphs); + + DEBUG1 ("all-fit '%.*s', remaining %d", + item->length, self->data->text + item->offset, + self->remaining_width); + return BREAK_ALL_FIT; + } + else if (break_num_chars == 0) + { + if (break_glyphs) + pango2_glyph_string_free (break_glyphs); + + DEBUG1 ("empty-fit, remaining %d", self->remaining_width); + return BREAK_EMPTY_FIT; + } + else + { + Pango2Item *new_item; + + length = g_utf8_offset_to_pointer (self->data->text + item->offset, break_num_chars) - (self->data->text + item->offset); + + new_item = pango2_item_split (item, length, break_num_chars); + + insert_run (self, line, new_item, break_glyphs, FALSE); + + self->log_widths_offset += break_num_chars; + + DEBUG1 ("some-fit '%.*s', remaining %d", + new_item->length, self->data->text + new_item->offset, + self->remaining_width); + return BREAK_SOME_FIT; + } + } + else + { + pango2_glyph_string_free (self->glyphs); + self->glyphs = NULL; + + if (break_glyphs) + pango2_glyph_string_free (break_glyphs); + + DEBUG1 ("none-fit, remaining %d", self->remaining_width); + return BREAK_NONE_FIT; + } +} + +static void +process_line (Pango2LineBreaker *self, + Pango2Line *line) +{ + gboolean have_break = FALSE; /* If we've seen a possible break yet */ + int break_remaining_width = 0; /* Remaining width before adding run with break */ + int break_start_offset = 0; /* Start offset before adding run with break */ + GSList *break_link = NULL; /* Link holding run before break */ + gboolean wrapped = FALSE; + + while (self->items) + { + Pango2Item *item = self->items->data; + BreakResult result; + int old_num_chars; + int old_remaining_width; + gboolean first_item_in_line; + gboolean last_item_in_line; + + old_num_chars = item->num_chars; + old_remaining_width = self->remaining_width; + first_item_in_line = line->runs == NULL; + last_item_in_line = self->items->next == NULL; + + result = process_item (self, line, !have_break, FALSE, last_item_in_line); + + switch (result) + { + case BREAK_ALL_FIT: + if (self->data->text[item->offset] != '\t' && + can_break_in (self, self->start_offset, old_num_chars, !first_item_in_line)) + { + have_break = TRUE; + break_remaining_width = old_remaining_width; + break_start_offset = self->start_offset; + break_link = line->runs->next; + } + + self->items = g_list_delete_link (self->items, self->items); + self->start_offset += old_num_chars; + break; + + case BREAK_EMPTY_FIT: + wrapped = TRUE; + goto done; + + case BREAK_SOME_FIT: + self->start_offset += old_num_chars - item->num_chars; + wrapped = TRUE; + goto done; + + case BREAK_NONE_FIT: + /* Back up over unused runs to run where there is a break */ + while (line->runs && line->runs != break_link) + { + Pango2GlyphItem *run = line->runs->data; + + /* Reset tab stat if we uninsert the current tab run */ + if (run->glyphs == self->last_tab.glyphs) + { + self->last_tab.glyphs = NULL; + self->last_tab.index = 0; + self->last_tab.align = PANGO2_TAB_LEFT; + } + + self->items = g_list_prepend (self->items, uninsert_run (line)); + } + + self->start_offset = break_start_offset; + self->remaining_width = break_remaining_width; + last_item_in_line = self->items->next == NULL; + + /* Reshape run to break */ + item = self->items->data; + + old_num_chars = item->num_chars; + result = process_item (self, line, TRUE, TRUE, last_item_in_line); + g_assert (result == BREAK_SOME_FIT || result == BREAK_EMPTY_FIT); + + self->start_offset += old_num_chars - item->num_chars; + + wrapped = TRUE; + goto done; + + case BREAK_LINE_SEPARATOR: + self->items = g_list_delete_link (self->items, self->items); + self->start_offset += old_num_chars; + /* A line-separate is just a forced break. Set wrapped, so we justify */ + wrapped = TRUE; + goto done; + + case BREAK_PARAGRAPH_SEPARATOR: + /* We don't add the item as a run, so don't add to + * line->length or line->n_chars here. + * But we still need the next line to start after + * the terminators, so add to self->line_start_index + */ + line->ends_paragraph = TRUE; + self->line_start_index += item->length; + self->start_offset += item->num_chars; + self->items = g_list_delete_link (self->items, self->items); + pango2_item_free (item); + goto done; + + default: + g_assert_not_reached (); + } + } + +done: + line->wrapped = wrapped; +} + +/* {{{ Post-processing */ + +static void +add_missing_hyphen (Pango2LineBreaker *self, + Pango2Line *line) +{ + Pango2GlyphItem *run; + Pango2Item *item; + + if (!line->runs) + return; + + run = line->runs->data; + item = run->item; + + if (self->data->log_attrs[self->line_start_offset + line->n_chars].break_inserts_hyphen && + !(item->analysis.flags & PANGO2_ANALYSIS_FLAG_NEED_HYPHEN)) + { + int width; + int start_offset; + + DEBUG1 ("add a missing hyphen"); + + /* The last run fit onto the line without breaking it, but it still needs a hyphen */ + width = pango2_glyph_string_get_width (run->glyphs); + + /* Ugly, shape_run uses self->start_offset, so temporarily rewind things + * to the state before the run was inserted. Otherwise, we end up passing + * the wrong log attrs to the shaping machinery. + */ + start_offset = self->start_offset; + self->start_offset = self->line_start_offset + line->n_chars - item->num_chars; + + pango2_glyph_string_free (run->glyphs); + item->analysis.flags |= PANGO2_ANALYSIS_FLAG_NEED_HYPHEN; + run->glyphs = shape_run (self, line, item); + + self->start_offset = start_offset; + + self->remaining_width += pango2_glyph_string_get_width (run->glyphs) - width; + } + + line->hyphenated = (item->analysis.flags & PANGO2_ANALYSIS_FLAG_NEED_HYPHEN) != 0; +} + +static void +zero_line_final_space (Pango2LineBreaker *self, + Pango2Line *line) +{ + Pango2GlyphItem *run; + Pango2Item *item; + Pango2GlyphString *glyphs; + int glyph; + + if (!line->runs) + return; + + run = line->runs->data; + item = run->item; + + glyphs = run->glyphs; + glyph = item->analysis.level % 2 ? 0 : glyphs->num_glyphs - 1; + + if (glyphs->glyphs[glyph].glyph == PANGO2_GET_UNKNOWN_GLYPH (0x2028)) + { + DEBUG1 ("zero final space: visible space"); + return; /* this LS is visible */ + } + + /* if the final char of line forms a cluster, and it's + * a whitespace char, zero its glyph's width as it's been wrapped + */ + if (glyphs->num_glyphs < 1 || self->start_offset == 0 || + !self->data->log_attrs[self->start_offset - 1].is_white) + { + DEBUG1 ("zero final space: not whitespace"); + return; + } + + if (glyphs->num_glyphs >= 2 && + glyphs->log_clusters[glyph] == glyphs->log_clusters[glyph + (item->analysis.level % 2 ? 1 : -1)]) + { + + DEBUG1 ("zero final space: its a cluster"); + return; + } + + DEBUG1 ("zero line final space: collapsing the space"); + glyphs->glyphs[glyph].geometry.width = 0; + glyphs->glyphs[glyph].glyph = PANGO2_GLYPH_EMPTY; +} + +/* When doing shaping, we add the letter spacing value for a + * run after every grapheme in the run. This produces ugly + * asymmetrical results, so what this routine is redistributes + * that space to the beginning and the end of the run. + * + * We also trim the letter spacing from runs adjacent to + * tabs and from the outside runs of the lines so that things + * line up properly. The line breaking and tab positioning + * were computed without this trimming so they are no longer + * exactly correct, but this won't be very noticeable in most + * cases. + */ +static void +adjust_line_letter_spacing (Pango2LineBreaker *self, + Pango2Line *line) +{ + gboolean reversed; + Pango2GlyphItem *last_run; + int tab_adjustment; + GSList *l; + + /* If we have tab stops and the resolved direction of the + * line is RTL, then we need to walk through the line + * in reverse direction to figure out the corrections for + * tab stops. + */ + reversed = FALSE; + if (line->direction == PANGO2_DIRECTION_RTL) + { + for (l = line->runs; l; l = l->next) + if (is_tab_run (line, l->data)) + { + line->runs = g_slist_reverse (line->runs); + reversed = TRUE; + break; + } + } + /* Walk over the runs in the line, redistributing letter + * spacing from the end of the run to the start of the + * run and trimming letter spacing from the ends of the + * runs adjacent to the ends of the line or tab stops. + * + * We accumulate a correction factor from this trimming + * which we add onto the next tab stop space to keep the + * things properly aligned. + */ + last_run = NULL; + tab_adjustment = 0; + for (l = line->runs; l; l = l->next) + { + Pango2GlyphItem *run = l->data; + Pango2GlyphItem *next_run = l->next ? l->next->data : NULL; + + if (is_tab_run (line, run)) + { + pad_glyphstring_right (self, run->glyphs, tab_adjustment); + tab_adjustment = 0; + } + else + { + Pango2GlyphItem *visual_next_run = reversed ? last_run : next_run; + Pango2GlyphItem *visual_last_run = reversed ? next_run : last_run; + int run_spacing = get_item_letter_spacing (run->item); + int space_left, space_right; + + distribute_letter_spacing (run_spacing, &space_left, &space_right); + + if (run->glyphs->glyphs[0].geometry.width == 0) + { + /* we've zeroed this space glyph at the end of line, now remove + * the letter spacing added to its adjacent glyph + */ + pad_glyphstring_left (self, run->glyphs, - space_left); + } + else if (!visual_last_run || is_tab_run (line, visual_last_run)) + { + pad_glyphstring_left (self, run->glyphs, - space_left); + tab_adjustment += space_left; + } + + if (run->glyphs->glyphs[run->glyphs->num_glyphs - 1].geometry.width == 0) + { + /* we've zeroed this space glyph at the end of line, now remove + * the letter spacing added to its adjacent glyph + */ + pad_glyphstring_right (self, run->glyphs, - space_right); + } + + else if (!visual_next_run || is_tab_run (line, visual_next_run)) + { + pad_glyphstring_right (self, run->glyphs, - space_right); + tab_adjustment += space_right; + } + } + + last_run = run; + } + + if (reversed) + line->runs = g_slist_reverse (line->runs); +} + +typedef struct { + Pango2Attribute *attr; + int x_offset; + int y_offset; +} BaselineItem; + +static void +collect_baseline_shift (Pango2LineBreaker *self, + Pango2Item *item, + Pango2Item *prev, + int *start_x_offset, + int *start_y_offset, + int *end_x_offset, + int *end_y_offset) +{ + *start_x_offset = 0; + *start_y_offset = 0; + *end_x_offset = 0; + *end_y_offset = 0; + + for (GSList *l = item->analysis.extra_attrs; l; l = l->next) + { + Pango2Attribute *attr = l->data; + + if (attr->type == PANGO2_ATTR_RISE) + { + int value = attr->int_value; + + *start_y_offset += value; + *end_y_offset -= value; + } + else if (attr->type == PANGO2_ATTR_BASELINE_SHIFT) + { + if (attr->start_index == item->offset) + { + BaselineItem *entry; + int value; + + entry = g_new0 (BaselineItem, 1); + entry->attr = attr; + self->baseline_shifts = g_list_prepend (self->baseline_shifts, entry); + + value = attr->int_value; + + if (value > 1024 || value < -1024) + { + entry->y_offset = value; + /* FIXME: compute an x_offset from value to italic angle */ + } + else + { + int superscript_x_offset = 0; + int superscript_y_offset = 0; + int subscript_x_offset = 0; + int subscript_y_offset = 0; + + + if (prev) + { + hb_font_t *hb_font = pango2_font_get_hb_font (prev->analysis.font); + hb_ot_metrics_get_position (hb_font, HB_OT_METRICS_TAG_SUPERSCRIPT_EM_Y_OFFSET, &superscript_y_offset); + hb_ot_metrics_get_position (hb_font, HB_OT_METRICS_TAG_SUPERSCRIPT_EM_X_OFFSET, &superscript_x_offset); + hb_ot_metrics_get_position (hb_font, HB_OT_METRICS_TAG_SUBSCRIPT_EM_Y_OFFSET, &subscript_y_offset); + hb_ot_metrics_get_position (hb_font, HB_OT_METRICS_TAG_SUBSCRIPT_EM_X_OFFSET, &subscript_x_offset); + } + + if (superscript_y_offset == 0) + superscript_y_offset = 5000; + if (subscript_y_offset == 0) + subscript_y_offset = 5000; + + switch (value) + { + case PANGO2_BASELINE_SHIFT_NONE: + entry->x_offset = 0; + entry->y_offset = 0; + break; + case PANGO2_BASELINE_SHIFT_SUPERSCRIPT: + entry->x_offset = superscript_x_offset; + entry->y_offset = superscript_y_offset; + break; + case PANGO2_BASELINE_SHIFT_SUBSCRIPT: + entry->x_offset = subscript_x_offset; + entry->y_offset = -subscript_y_offset; + break; + default: + g_assert_not_reached (); + } + } + + *start_x_offset += entry->x_offset; + *start_y_offset += entry->y_offset; + } + + if (attr->end_index == item->offset + item->length) + { + GList *t; + + for (t = self->baseline_shifts; t; t = t->next) + { + BaselineItem *entry = t->data; + + if (attr->start_index == entry->attr->start_index && + attr->end_index == entry->attr->end_index && + attr->int_value == entry->attr->int_value) + { + *end_x_offset -= entry->x_offset; + *end_y_offset -= entry->y_offset; + } + + self->baseline_shifts = g_list_remove (self->baseline_shifts, entry); + g_free (entry); + break; + } + if (t == NULL) + g_warning ("Baseline attributes mismatch\n"); + } + } + } +} + +static void +apply_baseline_shift (Pango2LineBreaker *self, + Pango2Line *line) +{ + int y_offset = 0; + Pango2Item *prev = NULL; + hb_position_t baseline_adjustment = 0; +#if HB_VERSION_ATLEAST(4,0,0) + hb_ot_layout_baseline_tag_t baseline_tag = 0; + hb_position_t baseline; + hb_position_t run_baseline; +#endif + + for (GSList *l = line->runs; l; l = l->next) + { + Pango2GlyphItem *run = l->data; + Pango2Item *item = run->item; + int start_x_offset, end_x_offset; + int start_y_offset, end_y_offset; +#if HB_VERSION_ATLEAST(4,0,0) + hb_font_t *hb_font; + hb_script_t script; + hb_language_t language; + hb_direction_t direction; + hb_tag_t script_tags[HB_OT_MAX_TAGS_PER_SCRIPT]; + hb_tag_t lang_tags[HB_OT_MAX_TAGS_PER_LANGUAGE]; + unsigned int script_count = HB_OT_MAX_TAGS_PER_SCRIPT; + unsigned int lang_count = HB_OT_MAX_TAGS_PER_LANGUAGE; +#endif + + if (item->analysis.font == NULL) + continue; + +#if HB_VERSION_ATLEAST(4,0,0) + hb_font = pango2_font_get_hb_font (item->analysis.font); + + script = (hb_script_t) g_unicode_script_to_iso15924 (item->analysis.script); + language = hb_language_from_string (pango2_language_to_string (item->analysis.language), -1); + hb_ot_tags_from_script_and_language (script, language, + &script_count, script_tags, + &lang_count, lang_tags); + + if (item->analysis.flags & PANGO2_ANALYSIS_FLAG_CENTERED_BASELINE) + direction = HB_DIRECTION_TTB; + else + direction = HB_DIRECTION_LTR; + + if (baseline_tag == 0) + { + if (item->analysis.flags & PANGO2_ANALYSIS_FLAG_CENTERED_BASELINE) + baseline_tag = HB_OT_LAYOUT_BASELINE_TAG_IDEO_EMBOX_CENTRAL; + else + baseline_tag = hb_ot_layout_get_horizontal_baseline_tag_for_script (script); + hb_ot_layout_get_baseline_with_fallback (hb_font, + baseline_tag, + direction, + script_tags[script_count - 1], + lang_count ? lang_tags[lang_count - 1] : HB_TAG_NONE, + &run_baseline); + baseline = run_baseline; + } + else + { + hb_ot_layout_get_baseline_with_fallback (hb_font, + baseline_tag, + direction, + script_tags[script_count - 1], + lang_count ? lang_tags[lang_count - 1] : HB_TAG_NONE, + &run_baseline); + } + + /* Don't do baseline adjustment in vertical, since the renderer + * is still doing its own baseline shifting there + */ + if (item->analysis.flags & PANGO2_ANALYSIS_FLAG_CENTERED_BASELINE) + baseline_adjustment = 0; + else + baseline_adjustment = baseline - run_baseline; +#endif + + collect_baseline_shift (self, item, prev, &start_x_offset, &start_y_offset, &end_x_offset, &end_y_offset); + + y_offset += start_y_offset + baseline_adjustment; + + run->y_offset = y_offset; + run->start_x_offset = start_x_offset; + run->end_x_offset = end_x_offset; + + y_offset += end_y_offset - baseline_adjustment; + + prev = item; + } +} + +static void +apply_render_attributes (Pango2LineBreaker *self, + Pango2Line *line) +{ + GSList *runs; + + if (!self->render_attrs) + return; + + runs = g_slist_reverse (line->runs); + line->runs = NULL; + + for (GSList *l = runs; l; l = l->next) + { + Pango2GlyphItem *glyph_item = l->data; + GSList *new_runs; + + new_runs = pango2_glyph_item_apply_attrs (glyph_item, + line->data->text, + self->render_attrs); + + line->runs = g_slist_concat (new_runs, line->runs); + } + + g_slist_free (runs); +} + +static void +postprocess_line (Pango2LineBreaker *self, + Pango2Line *line) +{ + add_missing_hyphen (self, line); + + /* Truncate the logical-final whitespace in the line if we broke the line at it */ + if (line->wrapped) + zero_line_final_space (self, line); + + line->runs = g_slist_reverse (line->runs); + + apply_baseline_shift (self, line); + + if (should_ellipsize_current_line (self, line)) + pango2_line_ellipsize (line, self->context, self->line_ellipsize, self->line_width); + + /* Now convert logical to visual order */ + pango2_line_reorder (line); + + /* Fixup letter spacing between runs */ + adjust_line_letter_spacing (self, line); + + apply_render_attributes (self, line); +} + +/* }}} */ +/* }}} */ +/* {{{ Pango2LineBreaker implementation */ + +G_DEFINE_FINAL_TYPE (Pango2LineBreaker, pango2_line_breaker, G_TYPE_OBJECT) + +enum { + PROP_CONTEXT = 1, + PROP_BASE_DIR, + PROP_TABS, + N_PROPERTIES +}; + +static GParamSpec *properties[N_PROPERTIES]; + +static void +pango2_line_breaker_init (Pango2LineBreaker *self) +{ + self->tabs = NULL; + self->tab_width = -1; + self->decimal = 0; + self->n_lines = 0; +} + +static void +pango2_line_breaker_finalize (GObject *object) +{ + Pango2LineBreaker *self = PANGO2_LINE_BREAKER (object); + + g_list_free_full (self->baseline_shifts, g_free); + g_clear_pointer (&self->glyphs, pango2_glyph_string_free); + g_clear_pointer (&self->log_widths, g_free); + g_list_free_full (self->items, (GDestroyNotify) pango2_item_free); + g_clear_pointer (&self->data, line_data_unref); + g_list_free_full (self->data_items, (GDestroyNotify) pango2_item_free); + g_clear_pointer (&self->render_attrs, pango2_attr_list_unref); + g_slist_free_full (self->datas, (GDestroyNotify) line_data_unref); + g_clear_pointer (&self->tabs, pango2_tab_array_free); + g_object_unref (self->context); + + G_OBJECT_CLASS (pango2_line_breaker_parent_class)->finalize (object); +} + +static void +pango2_line_breaker_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + Pango2LineBreaker *self = PANGO2_LINE_BREAKER (object); + + switch (prop_id) + { + case PROP_CONTEXT: + g_assert (self->context == NULL); + self->context = g_value_dup_object (value); + break; + + case PROP_BASE_DIR: + pango2_line_breaker_set_base_dir (self, g_value_get_enum (value)); + break; + + case PROP_TABS: + pango2_line_breaker_set_tabs (self, g_value_get_boxed (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +pango2_line_breaker_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + Pango2LineBreaker *self = PANGO2_LINE_BREAKER (object); + + switch (prop_id) + { + case PROP_CONTEXT: + g_value_set_object (value, self->context); + break; + + case PROP_BASE_DIR: + g_value_set_enum (value, pango2_line_breaker_get_base_dir (self)); + break; + + case PROP_TABS: + g_value_set_boxed (value, pango2_line_breaker_get_tabs (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +pango2_line_breaker_class_init (Pango2LineBreakerClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->finalize = pango2_line_breaker_finalize; + object_class->set_property = pango2_line_breaker_set_property; + object_class->get_property = pango2_line_breaker_get_property; + + /** + * Pango2LineBreaker:context: (attributes org.gtk.Property.get=pango2_line_breaker_get_context) + * + * The context for the `Pango2LineBreaker`. + */ + properties[PROP_CONTEXT] = + g_param_spec_object ("context", "context", "context", + PANGO2_TYPE_CONTEXT, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY|G_PARAM_EXPLICIT_NOTIFY); + + /** + * Pango2LineBreaker:base-dir: (attributes org.gtk.Property.get=pango2_line_breaker_get_base_dir org.gtk.Property.set=pango2_line_breaker_set_base_dir) + * + * The base direction for the `Pango2LineBreaker`. + * + * The default value is `PANGO2_DIRECTION_NEUTRAL`. + */ + properties[PROP_BASE_DIR] = + g_param_spec_enum ("base-dir", "base-dir", "base-dir", + PANGO2_TYPE_DIRECTION, + PANGO2_DIRECTION_NEUTRAL, + G_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); + + /** + * Pango2LineBreaker:tabs: (attributes org.gtk.Property.get=pango2_line_breaker_get_tabs org.gtk.Property.set=pango2_line_breaker_set_tabs) + * + * The tabs to use when formatting the next line of the `Pango2LineBreaker`. + * + * `Pango2LineBreaker` will place content at the next tab position + * whenever it meets a Tab character (U+0009). + */ + properties[PROP_TABS] = + g_param_spec_boxed ("tabs", "tabs", "tabs", + PANGO2_TYPE_TAB_ARRAY, + G_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, N_PROPERTIES, properties); +} + +/* }}} */ +/* {{{ Public API */ + +/** + * pango2_line_breaker_new: + * @context: a `Pango2Context` + * + * Creates a new `Pango2LineBreaker`. + * + * Returns: a newly created `Pango2LineBreaker` + */ +Pango2LineBreaker * +pango2_line_breaker_new (Pango2Context *context) +{ + g_return_val_if_fail (PANGO2_IS_CONTEXT (context), NULL); + + return g_object_new (PANGO2_TYPE_LINE_BREAKER, "context", context, NULL); +} + +/** + * pango2_line_breaker_get_context: + * @self: a `Pango2LineBreaker` + * + * Retrieves the context used for the `Pango2LineBreaker`. + * + * Returns: (transfer none): the `Pango2Context` for @self + */ +Pango2Context * +pango2_line_breaker_get_context (Pango2LineBreaker *self) +{ + g_return_val_if_fail (PANGO2_IS_LINE_BREAKER (self), NULL); + + return self->context; +} + +/** + * pango2_line_breaker_set_tabs: + * @self: a `Pango2LineBreaker` + * @tabs: (nullable): a `Pango2TabArray` + * + * Sets the tab positions to use for lines. + * + * `Pango2LineBreaker` will place content at the next tab position + * whenever it meets a Tab character (U+0009). + * + * By default, tabs are every 8 spaces. If @tabs is %NULL, the + * default tabs are reinstated. @tabs is copied by @self, you + * must free your copy of @tabs yourself. + * + * Note that tabs and justification conflict with each other: + * Justification will move content away from its tab-aligned + * positions. The same is true for alignments other than + * %PANGO2_ALIGNMENT_LEFT. + */ +void +pango2_line_breaker_set_tabs (Pango2LineBreaker *self, + Pango2TabArray *tabs) +{ + g_return_if_fail (PANGO2_IS_LINE_BREAKER (self)); + + if (self->tabs) + { + pango2_tab_array_free (self->tabs); + self->tabs = NULL; + } + + if (tabs) + { + self->tabs = pango2_tab_array_copy (tabs); + pango2_tab_array_sort (self->tabs); + } + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TABS]); +} + +/** + * pango2_line_breaker_get_tabs: + * @self: a `Pango2LineBreaker` + * + * Gets the current `Pango2TabArray` used by the `Pango2LineBreaker`. + * + * If no `Pango2TabArray` has been set, then the default tabs are + * in use and %NULL is returned. Default tabs are every 8 spaces. + * + * Return value: (transfer none) (nullable): the tabs for @self + */ +Pango2TabArray * +pango2_line_breaker_get_tabs (Pango2LineBreaker *self) +{ + g_return_val_if_fail (PANGO2_IS_LINE_BREAKER (self), NULL); + + if (self->tabs) + return self->tabs; + else + return NULL; +} + +/** + * pango2_line_breaker_set_base_dir: + * @self: a `Pango2LineBreaker` + * @direction: the direction + * + * Sets the base direction for lines produced by the `Pango2LineBreaker`. + * + * If @direction is `PANGO2_DIRECTION_NEUTRAL`, the direction is determined + * from the content. This is the default behavior. + */ +void +pango2_line_breaker_set_base_dir (Pango2LineBreaker *self, + Pango2Direction direction) +{ + g_return_if_fail (PANGO2_IS_LINE_BREAKER (self)); + + if (self->base_dir == direction) + return; + + self->base_dir = direction; + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_BASE_DIR]); +} + +/** + * pango2_line_breaker_get_base_dir: + * @self: a `Pango2LineBreaker` + * + * Gets the base direction for lines produced by the `Pango2LineBreaker`. + * + * See [method@Pango2.LineBreaker.set_base_dir]. + */ +Pango2Direction +pango2_line_breaker_get_base_dir (Pango2LineBreaker *self) +{ + g_return_val_if_fail (PANGO2_IS_LINE_BREAKER (self), PANGO2_DIRECTION_NEUTRAL); + + return self->base_dir; +} + +/** + * pango2_line_breaker_add_text: + * @text: the text to break into lines + * @length: length of @text in bytes, or -1 if @text is nul-terminated + * @attrs: (nullable): a `Pango2AttrList` with attributes for @text, or `NULL` + * + * Provides input that the `Pango2LineBreaker` should break into lines. + * + * It is possible to call this function repeatedly to add more + * input to an existing `Pango2LineBreaker`. + * + * The end of @text is treated as a paragraph break. + */ +void +pango2_line_breaker_add_text (Pango2LineBreaker *self, + const char *text, + int length, + Pango2AttrList *attrs) +{ + g_return_if_fail (PANGO2_IS_LINE_BREAKER (self)); + g_return_if_fail (text != NULL); + + self->datas = g_slist_append (self->datas, make_line_data (self, text, length, attrs)); +} + +/** + * pango2_line_breaker_get_direction: + * @self: a `Pango2LineBreaker` + * + * Obtains the resolved direction for the next line. + * + * If the `Pango2LineBreaker` has no more input, then + * `PANGO2_DIRECTION_NEUTRAL` is returned. + * + * Returns: the resolved direction of the next line. + */ +Pango2Direction +pango2_line_breaker_get_direction (Pango2LineBreaker *self) +{ + g_return_val_if_fail (PANGO2_IS_LINE_BREAKER (self), PANGO2_DIRECTION_NEUTRAL); + + return get_resolved_dir (self); +} + +/** + * pango2_line_breaker_has_line: + * @self: a `Pango2LineBreaker` + * + * Returns whether the `Pango2LineBreaker` has any text left to process. + * + * Returns: TRUE if there are more lines. + */ +gboolean +pango2_line_breaker_has_line (Pango2LineBreaker *self) +{ + g_return_val_if_fail (PANGO2_IS_LINE_BREAKER (self), FALSE); + + ensure_items (self); + + return self->items != NULL; +} + +/** + * pango2_line_breaker_next_line: + * @self: a `Pango2LineBreaker` + * @x: the X position for the line, in Pango2 units + * @width: the width for the line, or -1 for no limit, in Pango2 units + * @wrap: how to wrap the text + * @ellipsize: whether to ellipsize the text + * + * Gets the next line. + * + * The `Pango2LineBreaker` will use as much of its unprocessed text + * as will fit into @width. The @x position is used to determine + * where tabs are located are. + * + * If @ellipsize is not `PANGO2_ELLIPSIZE_NONE`, then all unprocessed + * text will be made to fit by ellipsizing. + * + * Note that the line is not positioned - the leftmost point of its baseline + * is at 0, 0. See [class@Pango2.Lines] for a way to hold a list of positioned + * `Pango2Line` objects. + * + * line = pango2_line_breaker_next_line (breaker, + * x, width, + * PANGO2_WRAP_MODE, + * PANGO2_ELLIPSIZE_NONE); + * pango2_line_get_extents (line, &ext); + * line = pango2_line_justify (line, width); + * pango2_lines_add_line (lines, line, x, y - ext.y); + * + * Returns: (transfer full) (nullable): the next line, or `NULL` + * if @self has no more input + */ +Pango2Line * +pango2_line_breaker_next_line (Pango2LineBreaker *self, + int x, + int width, + Pango2WrapMode wrap, + Pango2EllipsizeMode ellipsize) +{ + Pango2Line *line; + + g_return_val_if_fail (PANGO2_IS_LINE_BREAKER (self), NULL); + + ensure_items (self); + + if (!self->items) + return NULL; + + line = pango2_line_new (self->context, self->data); + + line->start_index = self->line_start_index; + line->start_offset = self->line_start_offset; + line->starts_paragraph = self->at_paragraph_start; + line->direction = get_resolved_dir (self); + + self->line_x = x; + self->line_width = width; + self->line_wrap = wrap; + self->line_ellipsize = ellipsize; + + self->last_tab.glyphs = NULL; + self->last_tab.index = 0; + self->last_tab.align = PANGO2_TAB_LEFT; + + if (should_ellipsize_current_line (self, line)) + self->remaining_width = -1; + else + self->remaining_width = width; + + process_line (self, line); + + line->n_chars = compute_n_chars (line); + + postprocess_line (self, line); + + if (!self->items) + line->ends_paragraph = TRUE; + + self->at_paragraph_start = line->ends_paragraph; + self->n_lines++; + self->line_start_index += line->length; + self->line_start_offset = self->start_offset; + + if (self->items == NULL) + { + g_clear_pointer (&self->data, line_data_unref); + g_list_free_full (self->data_items, (GDestroyNotify) pango2_item_free); + self->data_items = NULL; + g_clear_pointer (&self->render_attrs, pango2_attr_list_unref); + } + + pango2_line_check_invariants (line); + + return line; +} + +/** + * pango2_line_breaker_undo_line: + * @self: a `Pango2LineBreaker` + * @line: (transfer none): the most recent line produced by @self + * + * Re-adds the content of a line to the unprocessed + * input of the `Pango2LineBreaker`. + * + * This can be used to try this line again with + * different parameters passed to + * [method@Pango2.LineBreaker.next_line]. + * + * When undoing multiple lines, they have to be + * undone in the reverse order in which they + * were produced. + * + * Returns: `TRUE` on success, `FALSE` if Pango2 + * determines that the line can't be undone + */ +gboolean +pango2_line_breaker_undo_line (Pango2LineBreaker *self, + Pango2Line *line) +{ + if (self->data == NULL && + line->start_index == 0 && line->length == line->data->length) + { + g_assert (self->items == NULL); + self->datas = g_slist_prepend (self->datas, line_data_ref (line->data)); + + self->n_lines--; + + /* ensure_items will set up everything else */ + + g_clear_pointer (&self->glyphs, pango2_glyph_string_free); + + return TRUE; + } + + if (self->data == line->data && + self->line_start_index == line->start_index + line->length) + { + GList *items = NULL; + + /* recover the original items */ + for (GList *l = self->data_items; l; l = l->next) + { + Pango2Item *item = l->data; + + if (item->offset + item->length < line->start_index) + continue; + + if (item->offset > self->line_start_index) + break; + + item = pango2_item_copy (item); + + if (item->offset < line->start_index) + { + Pango2Item *new_item; + int n_chars; + + n_chars = g_utf8_strlen (self->data->text + item->offset, line->start_index - item->offset); + new_item = pango2_item_split (item, line->start_index - item->offset, n_chars); + pango2_item_free (new_item); + } + + if (item->offset + item->length > self->line_start_index) + { + Pango2Item *new_item; + int n_chars; + + n_chars = g_utf8_strlen (self->data->text + item->offset, self->line_start_index - item->offset); + new_item = pango2_item_split (item, self->line_start_index - item->offset, n_chars); + pango2_item_free (item); + item = new_item; + } + + items = g_list_prepend (items, item); + } + + self->items = g_list_concat (g_list_reverse (items), self->items); + + self->n_lines--; + + self->at_paragraph_start = line->starts_paragraph; + self->line_start_index = line->start_index; + self->line_start_offset = line->start_offset; + + g_clear_pointer (&self->glyphs, pango2_glyph_string_free); + self->start_offset = line->start_offset; + self->log_widths_offset = 0; + + return TRUE; + } + + return FALSE; +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pango-line-breaker.h b/pango2/pango-line-breaker.h new file mode 100644 index 00000000..666f67f5 --- /dev/null +++ b/pango2/pango-line-breaker.h @@ -0,0 +1,76 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <glib-object.h> +#include <pango2/pango-types.h> +#include <pango2/pango-break.h> +#include <pango2/pango-layout.h> +#include <pango2/pango-line.h> + +G_BEGIN_DECLS + +#define PANGO2_TYPE_LINE_BREAKER pango2_line_breaker_get_type () + +PANGO2_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (Pango2LineBreaker, pango2_line_breaker, PANGO2, LINE_BREAKER, GObject); + +PANGO2_AVAILABLE_IN_ALL +Pango2LineBreaker * pango2_line_breaker_new (Pango2Context *context); + +PANGO2_AVAILABLE_IN_ALL +Pango2Context * pango2_line_breaker_get_context (Pango2LineBreaker *self); + +PANGO2_AVAILABLE_IN_ALL +void pango2_line_breaker_set_tabs (Pango2LineBreaker *self, + Pango2TabArray *tabs); +PANGO2_AVAILABLE_IN_ALL +Pango2TabArray * pango2_line_breaker_get_tabs (Pango2LineBreaker *self); + +PANGO2_AVAILABLE_IN_ALL +void pango2_line_breaker_set_base_dir (Pango2LineBreaker *self, + Pango2Direction direction); +PANGO2_AVAILABLE_IN_ALL +Pango2Direction pango2_line_breaker_get_base_dir (Pango2LineBreaker *self); + +PANGO2_AVAILABLE_IN_ALL +void pango2_line_breaker_add_text (Pango2LineBreaker *self, + const char *text, + int length, + Pango2AttrList *attrs); + +PANGO2_AVAILABLE_IN_ALL +Pango2Direction pango2_line_breaker_get_direction (Pango2LineBreaker *self); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_line_breaker_has_line (Pango2LineBreaker *self); + +PANGO2_AVAILABLE_IN_ALL +Pango2Line * pango2_line_breaker_next_line (Pango2LineBreaker *self, + int x, + int width, + Pango2WrapMode wrap, + Pango2EllipsizeMode ellipsize); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_line_breaker_undo_line (Pango2LineBreaker *self, + Pango2Line *line); + +G_END_DECLS diff --git a/pango2/pango-line-iter-private.h b/pango2/pango-line-iter-private.h new file mode 100644 index 00000000..1aba2956 --- /dev/null +++ b/pango2/pango-line-iter-private.h @@ -0,0 +1,25 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-line-iter.h" + + +Pango2LineIter * pango2_line_iter_new (Pango2Lines *lines); diff --git a/pango2/pango-line-iter.c b/pango2/pango-line-iter.c new file mode 100644 index 00000000..8139e4ef --- /dev/null +++ b/pango2/pango-line-iter.c @@ -0,0 +1,881 @@ +#include "config.h" + +#include "pango-line-iter-private.h" +#include "pango-lines-private.h" +#include "pango-line-private.h" +#include "pango-run-private.h" + +/** + * Pango2LineIter: + * + * A `Pango2LineIter` can be used to iterate over the visual + * extents of a `Pango2Layout` or `Pango2Lines`. + * + * To obtain a `Pango2LineIter`, use [method@Pango2.Layout.get_iter] + * or [method@Pango2.Lines.get_iter]. + * + * The `Pango2LineIter` structure is opaque, and has no user-visible + * fields. + */ + + +/* {{{ Pango2LineIter implementation */ + +struct _Pango2LineIter +{ + Pango2Lines *lines; + guint serial; + + int line_no; + Position line_pos; + Pango2Line *line; + GSList *run_link; + Pango2Run *run; + int index; + + /* run handling */ + int run_x; + int run_width; + int end_x_offset; + gboolean ltr; + + /* cluster handling */ + int cluster_x; + int cluster_width; + int cluster_start; + int next_cluster_glyph; + int cluster_num_chars; + + int character_position; +}; + +G_DEFINE_BOXED_TYPE (Pango2LineIter, pango2_line_iter, + pango2_line_iter_copy, pango2_line_iter_free); + + +/* }}} */ +/* {{{ Utilities */ + +#define ITER_IS_VALID(iter) ((iter)->serial == (iter)->lines->serial) + +static gboolean +line_is_terminated (Pango2LineIter *iter) +{ + if (iter->line_no + 1 < pango2_lines_get_line_count (iter->lines)) + return pango2_line_is_paragraph_end (iter->line); + + return FALSE; + +} + +static int +next_cluster_start (Pango2GlyphString *glyphs, + int cluster_start) +{ + int i; + + i = cluster_start + 1; + while (i < glyphs->num_glyphs) + { + if (glyphs->glyphs[i].attr.is_cluster_start) + return i; + + i++; + } + + return glyphs->num_glyphs; +} + +static int +cluster_width (Pango2GlyphString *glyphs, + int cluster_start) +{ + int i; + int width; + + width = glyphs->glyphs[cluster_start].geometry.width; + i = cluster_start + 1; + while (i < glyphs->num_glyphs) + { + if (glyphs->glyphs[i].attr.is_cluster_start) + break; + + width += glyphs->glyphs[i].geometry.width; + i++; + } + + return width; +} + +/* Sets up the iter for the start of a new cluster. cluster_start_index + * is the byte index of the cluster start relative to the run. + */ +static void +update_cluster (Pango2LineIter *iter, + int cluster_start_index) +{ + Pango2GlyphItem *glyph_item; + char *cluster_text; + int cluster_length; + Pango2Run *run = iter->run_link->data; + + glyph_item = pango2_run_get_glyph_item (run); + + iter->character_position = 0; + + iter->cluster_width = cluster_width (glyph_item->glyphs, iter->cluster_start); + iter->next_cluster_glyph = next_cluster_start (glyph_item->glyphs, iter->cluster_start); + + if (iter->ltr) + { + /* For LTR text, finding the length of the cluster is easy + * since logical and visual runs are in the same direction. + */ + if (iter->next_cluster_glyph < glyph_item->glyphs->num_glyphs) + cluster_length = glyph_item->glyphs->log_clusters[iter->next_cluster_glyph] - cluster_start_index; + else + cluster_length = glyph_item->item->length - cluster_start_index; + } + else + { + /* For RTL text, we have to scan backwards to find the previous + * visual cluster which is the next logical cluster. + */ + int i = iter->cluster_start; + while (i > 0 && glyph_item->glyphs->log_clusters[i - 1] == cluster_start_index) + i--; + + if (i == 0) + cluster_length = glyph_item->item->length - cluster_start_index; + else + cluster_length = glyph_item->glyphs->log_clusters[i - 1] - cluster_start_index; + } + + cluster_text = iter->line->data->text + glyph_item->item->offset + cluster_start_index; + iter->cluster_num_chars = g_utf8_strlen (cluster_text, cluster_length); + + if (iter->ltr) + iter->index = cluster_text - iter->line->data->text; + else + iter->index = g_utf8_prev_char (cluster_text + cluster_length) - iter->line->data->text; +} + +/* Moves to the next non-empty line. If @include_terminators + * is set, a line with just an explicit paragraph separator + * is considered non-empty. + */ +static gboolean +next_nonempty_line (Pango2LineIter *iter, + gboolean include_terminators) +{ + gboolean result; + + while (TRUE) + { + result = pango2_line_iter_next_line (iter); + if (!result) + break; + + if (iter->line->runs) + break; + + if (include_terminators && line_is_terminated (iter)) + break; + } + + return result; +} + +/* Moves to the next non-empty run. If @include_terminators + * is set, the trailing run at the end of a line with an explicit + * paragraph separator is considered non-empty. + */ +static gboolean +next_nonempty_run (Pango2LineIter *iter, + gboolean include_terminators) +{ + gboolean result; + + while (TRUE) + { + result = pango2_line_iter_next_run (iter); + if (!result) + break; + + if (iter->run) + break; + + if (include_terminators && line_is_terminated (iter)) + break; + } + + return result; +} + +/* Like pango2_layout_next_cluster(), but if @include_terminators + * is set, includes the fake runs/clusters for empty lines. + * (But not positions introduced by line wrapping). + */ +static gboolean +next_cluster_internal (Pango2LineIter *iter, + gboolean include_terminators) +{ + Pango2GlyphItem *glyph_item; + Pango2Run *run; + + if (iter->run_link == NULL) + return next_nonempty_line (iter, include_terminators); + + run = iter->run_link->data; + glyph_item = pango2_run_get_glyph_item (run); + + if (iter->next_cluster_glyph == glyph_item->glyphs->num_glyphs) + { + return next_nonempty_run (iter, include_terminators); + } + else + { + iter->cluster_start = iter->next_cluster_glyph; + iter->cluster_x += iter->cluster_width; + update_cluster (iter, glyph_item->glyphs->log_clusters[iter->cluster_start]); + + return TRUE; + } +} + +static void +update_run (Pango2LineIter *iter, + int start_index) +{ + if (iter->run) + { + Pango2GlyphItem *glyph_item = pango2_run_get_glyph_item (iter->run); + + if (iter->run_link == iter->line->runs) + iter->run_x = 0; + else + iter->run_x += iter->run_width + iter->end_x_offset + glyph_item->start_x_offset; + + iter->run_width = pango2_glyph_string_get_width (glyph_item->glyphs); + iter->end_x_offset = glyph_item->end_x_offset; + iter->ltr = (glyph_item->item->analysis.level % 2) == 0; + iter->cluster_start = 0; + iter->cluster_x = iter->run_x; + update_cluster (iter, glyph_item->glyphs->log_clusters[0]); + } + else + { + /* The empty run at the end of a line */ + iter->run_x = 0; + + iter->run_width = 0; + iter->end_x_offset = 0; + iter->ltr = TRUE; + iter->cluster_start = 0; + iter->cluster_x = iter->run_x; + iter->cluster_width = 0; + iter->character_position = 0; + iter->cluster_num_chars = 0; + iter->index = start_index; + } +} + +static inline void +offset_line (Pango2LineIter *iter, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect) +{ + if (ink_rect) + { + ink_rect->x += iter->line_pos.x; + ink_rect->y += iter->line_pos.y; + } + if (logical_rect) + { + logical_rect->x += iter->line_pos.x; + logical_rect->y += iter->line_pos.y; + } +} + +static inline void +offset_run (Pango2LineIter *iter, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect) +{ + if (ink_rect) + ink_rect->x += iter->run_x; + if (logical_rect) + logical_rect->x += iter->run_x; +} + +/* }}} */ +/* {{{ Private API */ + +Pango2LineIter * +pango2_line_iter_new (Pango2Lines *lines) +{ + Pango2LineIter *iter; + int run_start_index; + + g_return_val_if_fail (PANGO2_IS_LINES (lines), NULL); + + iter = g_new0 (Pango2LineIter, 1); + + iter->lines = g_object_ref (lines); + iter->serial = pango2_lines_get_serial (lines); + + iter->line_no = 0; + iter->line = g_ptr_array_index (lines->lines, 0); + iter->line_pos = g_array_index (lines->positions, Position, 0); + iter->run_link = iter->line->runs; + if (iter->run_link) + { + iter->run = iter->run_link->data; + run_start_index = pango2_run_get_glyph_item (iter->run)->item->offset; + } + else + { + iter->run = NULL; + run_start_index = 0; + } + + update_run (iter, run_start_index); + + return iter; +} + +/* }}} */ +/* {{{ Public API */ + +/** + * pango2_line_iter_copy: + * @iter: (nullable): a `Pango2LineIter` + * + * Copies a `Pango2LineIter`. + * + * Return value: (nullable): the newly allocated `Pango2LineIter` + */ +Pango2LineIter * +pango2_line_iter_copy (Pango2LineIter *iter) +{ + Pango2LineIter *copy; + + if (iter == NULL) + return NULL; + + copy = g_new0 (Pango2LineIter, 1); + memcpy (iter, copy, sizeof (Pango2LineIter)); + g_object_ref (copy->lines); + + return copy; +} + +/** + * pango2_line_iter_free: + * @iter: (nullable): a `Pango2LineIter` + * + * Frees an iterator that's no longer in use. + */ +void +pango2_line_iter_free (Pango2LineIter *iter) +{ + if (iter == NULL) + return; + + g_object_unref (iter->lines); + g_free (iter); +} + +/** + * pango2_line_iter_get_lines: + * @iter: a `Pango2LineIter` + * + * Gets the `Pango2Lines` object associated with a `Pango2LineIter`. + * + * Return value: (transfer none): the lines associated with @iter + */ +Pango2Lines * +pango2_line_iter_get_lines (Pango2LineIter *iter) +{ + return iter->lines; +} + +/** + * pango2_line_iter_get_line: + * @iter: a `Pango2LineIter` + * + * Gets the current line. + * + * Return value: (transfer none): the current line + */ +Pango2Line * +pango2_line_iter_get_line (Pango2LineIter *iter) +{ + g_return_val_if_fail (ITER_IS_VALID (iter), NULL); + + return iter->line; +} + +/** + * pango2_line_iter_at_last_line: + * @iter: a `Pango2LineIter` + * + * Determines whether @iter is on the last line. + * + * Return value: %TRUE if @iter is on the last line + */ +gboolean +pango2_line_iter_at_last_line (Pango2LineIter *iter) +{ + g_return_val_if_fail (ITER_IS_VALID (iter), FALSE); + + return iter->line_no + 1 == pango2_lines_get_line_count (iter->lines); +} + +/** + * pango2_line_iter_get_run: + * @iter: a `Pango2LineIter` + * + * Gets the current run. + * + * When iterating by run, at the end of each line, there's a position + * with a %NULL run, so this function can return %NULL. The %NULL run + * at the end of each line ensures that all lines have at least one run, + * even lines consisting of only a newline. + * + * Return value: (transfer none) (nullable): the current run + */ +Pango2Run * +pango2_line_iter_get_run (Pango2LineIter *iter) +{ + g_return_val_if_fail (ITER_IS_VALID (iter), NULL); + + return iter->run; +} + +/** + * pango2_line_iter_get_index: + * @iter: a `Pango2LineIter` + * + * Gets the current byte index. + * + * The byte index is relative to the text backing the current + * line. + * + * Note that iterating forward by char moves in visual order, + * not logical order, so indexes may not be sequential. Also, + * the index may be equal to the length of the text in the + * layout, if on the %NULL run (see [method@Pango2.LineIter.get_run]). + * + * Return value: current byte index + */ +int +pango2_line_iter_get_index (Pango2LineIter *iter) +{ + g_return_val_if_fail (ITER_IS_VALID (iter), 0); + + return iter->index; +} + +/** + * pango2_line_iter_next_line: + * @iter: a `Pango2LineIter` + * + * Moves @iter forward to the start of the next line. + * + * If @iter is already on the last line, returns %FALSE. + * + * Return value: whether motion was possible + */ +gboolean +pango2_line_iter_next_line (Pango2LineIter *iter) +{ + g_return_val_if_fail (ITER_IS_VALID (iter), FALSE); + + iter->line_no++; + if (iter->line_no == iter->lines->lines->len) + return FALSE; + + iter->line = g_ptr_array_index (iter->lines->lines, iter->line_no); + iter->line_pos = g_array_index (iter->lines->positions, Position, iter->line_no); + + iter->run_link = iter->line->runs; + if (iter->run_link) + iter->run = iter->run_link->data; + else + iter->run = NULL; + + update_run (iter, iter->line->start_index); + + return TRUE; +} + +/** + * pango2_line_iter_next_run: + * @iter: a `Pango2LineIter` + * + * Moves @iter forward to the next run in visual order. + * + * If @iter was already at the end, returns %FALSE. + * + * Return value: whether motion was possible + */ +gboolean +pango2_line_iter_next_run (Pango2LineIter *iter) +{ + int run_start_index; + + g_return_val_if_fail (ITER_IS_VALID (iter), FALSE); + + if (iter->run == NULL) + return pango2_line_iter_next_line (iter); + + iter->run_link = iter->run_link->next; + if (iter->run_link == NULL) + { + Pango2Item *item = pango2_run_get_glyph_item (iter->run)->item; + run_start_index = item->offset + item->length; + iter->run = NULL; + } + else + { + iter->run = iter->run_link->data; + run_start_index = pango2_run_get_glyph_item (iter->run)->item->offset; + } + + update_run (iter, run_start_index); + + return TRUE; +} + +/** + * pango2_line_iter_next_cluster: + * @iter: a `Pango2LineIter` + * + * Moves @iter forward to the next cluster in visual order. + * + * If @iter was already at the end, returns %FALSE. + * + * Return value: whether motion was possible + */ +gboolean +pango2_line_iter_next_cluster (Pango2LineIter *iter) +{ + g_return_val_if_fail (ITER_IS_VALID (iter), FALSE); + + return next_cluster_internal (iter, FALSE); +} + +/** + * pango2_line_iter_next_char: + * @iter: a `Pango2LineIter` + * + * Moves @iter forward to the next character in visual order. + * + * If @iter was already at the end, returns %FALSE. + * + * Return value: whether motion was possible + */ +gboolean +pango2_line_iter_next_char (Pango2LineIter *iter) +{ + const char *text; + + g_return_val_if_fail (ITER_IS_VALID (iter), FALSE); + + if (iter->run == NULL) + { + /* We need to fake an iterator position in the middle of a \r\n line terminator */ + if (line_is_terminated (iter) && + strncmp (iter->line->data->text + iter->line->start_index + iter->line->length, "\r\n", 2) == 0 && + iter->character_position == 0) + { + iter->character_position++; + + return TRUE; + } + + return next_nonempty_line (iter, TRUE); + } + + iter->character_position++; + + if (iter->character_position >= iter->cluster_num_chars) + return next_cluster_internal (iter, TRUE); + + text = iter->line->data->text; + if (iter->ltr) + iter->index = g_utf8_next_char (text + iter->index) - text; + else + iter->index = g_utf8_prev_char (text + iter->index) - text; + + return TRUE; +} + +/** + * pango2_line_iter_get_layout_extents: + * @iter: a `Pango2LineIter` + * @ink_rect: (out) (optional): rectangle to fill with ink extents + * @logical_rect: (out) (optional): rectangle to fill with logical extents + * + * Obtains the extents of the `Pango2Lines` being iterated over. + */ +void +pango2_line_iter_get_layout_extents (Pango2LineIter *iter, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect) +{ + g_return_if_fail (ITER_IS_VALID (iter)); + + pango2_lines_get_extents (iter->lines, ink_rect, logical_rect); +} + +/** + * pango2_line_iter_get_line_extents: + * @iter: a `Pango2LineIter` + * @ink_rect: (out) (optional): rectangle to fill with ink extents + * @logical_rect: (out) (optional): rectangle to fill with logical extents + * + * Obtains the extents of the current line. + * + * Extents are in layout coordinates (origin is the top-left corner of the + * entire `Pango2Lines`). Thus the extents returned by this function will be + * the same width/height but not at the same x/y as the extents returned + * from [method@Pango2.Line.get_extents]. + * + * The logical extents returned by this function always have their leading + * trimmed according to paragraph boundaries: if the line starts a paragraph, + * it has its start leading trimmed; if it ends a paragraph, it has its end + * leading trimmed. If you need other trimming, use + * [method@Pango2.Line.get_trimmed_extents]. + */ +void +pango2_line_iter_get_line_extents (Pango2LineIter *iter, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect) +{ + g_return_if_fail (ITER_IS_VALID (iter)); + + pango2_line_get_extents (iter->line, ink_rect, logical_rect); + offset_line (iter, ink_rect, logical_rect); +} + +void +pango2_line_iter_get_trimmed_line_extents (Pango2LineIter *iter, + Pango2LeadingTrim trim, + Pango2Rectangle *logical_rect) +{ + g_return_if_fail (ITER_IS_VALID (iter)); + + pango2_line_get_trimmed_extents (iter->line, trim, logical_rect); + offset_line (iter, NULL, logical_rect); +} + +/** + * pango2_line_iter_get_run_extents: + * @iter: a `Pango2LineIter` + * @ink_rect: (out) (optional): rectangle to fill with ink extents + * @logical_rect: (out) (optional): rectangle to fill with logical extents + * + * Gets the extents of the current run in layout coordinates. + * + * Layout coordinates have the origin at the top left of the entire `Pango2Lines`. + * + * The logical extents returned by this function always have their leading + * trimmed off. If you need extents that include leading, use + * [method@Pango2.Run.get_extents]. + */ +void +pango2_line_iter_get_run_extents (Pango2LineIter *iter, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect) +{ + g_return_if_fail (ITER_IS_VALID (iter)); + + if (iter->run) + { + pango2_run_get_extents (iter->run, PANGO2_LEADING_TRIM_BOTH, ink_rect, logical_rect); + } + else + { + GSList *runs = iter->line->runs; + if (runs) + { + /* Virtual run at the end of a nonempty line */ + Pango2Run *run = g_slist_last (runs)->data; + + pango2_run_get_extents (run, PANGO2_LEADING_TRIM_BOTH, ink_rect, logical_rect); + if (ink_rect) + { + ink_rect->x += ink_rect->width; + ink_rect->width = 0; + } + if (logical_rect) + { + logical_rect->x += logical_rect->width; + logical_rect->width = 0; + } + } + else + { + /* Empty line */ + Pango2Rectangle r; + + pango2_line_get_empty_extents (iter->line, PANGO2_LEADING_TRIM_BOTH, &r); + + if (ink_rect) + *ink_rect = r; + + if (logical_rect) + *logical_rect = r; + } + } + + offset_line (iter, ink_rect, logical_rect); + offset_run (iter, ink_rect, logical_rect); +} + +/** + * pango2_line_iter_get_cluster_extents: + * @iter: a `Pango2LineIter` + * @ink_rect: (out) (optional): rectangle to fill with ink extents + * @logical_rect: (out) (optional): rectangle to fill with logical extents + * + * Gets the extents of the current cluster, in layout coordinates. + * + * Layout coordinates have the origin at the top left of the entire `Pango2Lines`. + */ +void +pango2_line_iter_get_cluster_extents (Pango2LineIter *iter, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect) +{ + Pango2GlyphItem *glyph_item; + + g_return_if_fail (ITER_IS_VALID (iter)); + + if (iter->run == NULL) + { + /* When on the NULL run, all extents are the same */ + pango2_line_iter_get_run_extents (iter, ink_rect, logical_rect); + return; + } + + glyph_item = pango2_run_get_glyph_item (iter->run); + + pango2_glyph_string_extents_range (glyph_item->glyphs, + iter->cluster_start, + iter->next_cluster_glyph, + glyph_item->item->analysis.font, + ink_rect, + logical_rect); + + offset_line (iter, ink_rect, logical_rect); + if (ink_rect) + { + ink_rect->x += iter->cluster_x + glyph_item->start_x_offset; + ink_rect->y -= glyph_item->y_offset; + } + + if (logical_rect) + { + g_assert (logical_rect->width == iter->cluster_width); + logical_rect->x += iter->cluster_x + glyph_item->start_x_offset; + logical_rect->y -= glyph_item->y_offset; + } +} + +/** + * pango2_line_iter_get_char_extents: + * @iter: a `Pango2LineIter` + * @logical_rect: (out caller-allocates): rectangle to fill with logical extents + * + * Gets the extents of the current character, in layout coordinates. + * + * Layout coordinates have the origin at the top left of the entire `Pango2Lines`. + * + * Only logical extents can sensibly be obtained for characters; + * ink extents make sense only down to the level of clusters. + */ +void +pango2_line_iter_get_char_extents (Pango2LineIter *iter, + Pango2Rectangle *logical_rect) +{ + Pango2Rectangle cluster_rect; + int x0, x1; + + g_return_if_fail (ITER_IS_VALID (iter)); + + if (logical_rect == NULL) + return; + + pango2_line_iter_get_cluster_extents (iter, NULL, &cluster_rect); + + if (iter->run == NULL) + { + /* When on the NULL run, all extents are the same */ + *logical_rect = cluster_rect; + return; + } + + if (iter->cluster_num_chars) + { + x0 = (iter->character_position * cluster_rect.width) / iter->cluster_num_chars; + x1 = ((iter->character_position + 1) * cluster_rect.width) / iter->cluster_num_chars; + } + else + { + x0 = x1 = 0; + } + + logical_rect->width = x1 - x0; + logical_rect->height = cluster_rect.height; + logical_rect->y = cluster_rect.y; + logical_rect->x = cluster_rect.x + x0; +} + +/** + * pango2_line_iter_get_line_baseline: + * @iter: a `Pango2LineIter` + * + * Gets the Y position of the current line's baseline, in layout + * coordinates. + * + * Layout coordinates have the origin at the top left of the entire `Pango2Lines`. + * + * Return value: baseline of current line + */ +int +pango2_line_iter_get_line_baseline (Pango2LineIter *iter) +{ + g_return_val_if_fail (ITER_IS_VALID (iter), 0); + + return iter->line_pos.y; +} + +/** + * pango2_line_iter_get_run_baseline: + * @iter: a `Pango2LineIter` + * + * Gets the Y position of the current run's baseline, in layout + * coordinates. + * + * Layout coordinates have the origin at the top left of the entire `Pango2Lines`. + * + * The run baseline can be different from the line baseline, for + * example due to superscript or subscript positioning. + */ +int +pango2_line_iter_get_run_baseline (Pango2LineIter *iter) +{ + g_return_val_if_fail (ITER_IS_VALID (iter), 0); + + if (iter->run) + return pango2_line_iter_get_line_baseline (iter) - pango2_run_get_glyph_item (iter->run)->y_offset; + else + return pango2_line_iter_get_line_baseline (iter); +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pango-line-iter.h b/pango2/pango-line-iter.h new file mode 100644 index 00000000..59a4f5c2 --- /dev/null +++ b/pango2/pango-line-iter.h @@ -0,0 +1,102 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <glib-object.h> + +#include <pango2/pango-types.h> +#include <pango2/pango-lines.h> + +G_BEGIN_DECLS + +PANGO2_AVAILABLE_IN_ALL +GType pango2_line_iter_get_type (void) G_GNUC_CONST; + +PANGO2_AVAILABLE_IN_ALL +Pango2LineIter * pango2_line_iter_copy (Pango2LineIter *iter); + +PANGO2_AVAILABLE_IN_ALL +void pango2_line_iter_free (Pango2LineIter *iter); + +PANGO2_AVAILABLE_IN_ALL +Pango2Lines * pango2_line_iter_get_lines (Pango2LineIter *iter); + +PANGO2_AVAILABLE_IN_ALL +Pango2Line * pango2_line_iter_get_line (Pango2LineIter *iter); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_line_iter_at_last_line (Pango2LineIter *iter); + +PANGO2_AVAILABLE_IN_ALL +Pango2Run * pango2_line_iter_get_run (Pango2LineIter *iter); + +PANGO2_AVAILABLE_IN_ALL +int pango2_line_iter_get_index (Pango2LineIter *iter); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_line_iter_next_line (Pango2LineIter *iter); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_line_iter_next_run (Pango2LineIter *iter); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_line_iter_next_cluster (Pango2LineIter *iter); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_line_iter_next_char (Pango2LineIter *iter); + +PANGO2_AVAILABLE_IN_ALL +void pango2_line_iter_get_layout_extents (Pango2LineIter *iter, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect); + +PANGO2_AVAILABLE_IN_ALL +void pango2_line_iter_get_line_extents (Pango2LineIter *iter, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect); + +PANGO2_AVAILABLE_IN_ALL +void pango2_line_iter_get_trimmed_line_extents (Pango2LineIter *iter, + Pango2LeadingTrim trim, + Pango2Rectangle *logical_rect); + +PANGO2_AVAILABLE_IN_ALL +void pango2_line_iter_get_run_extents (Pango2LineIter *iter, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect); + +PANGO2_AVAILABLE_IN_ALL +void pango2_line_iter_get_cluster_extents (Pango2LineIter *iter, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect); + +PANGO2_AVAILABLE_IN_ALL +void pango2_line_iter_get_char_extents (Pango2LineIter *iter, + Pango2Rectangle *logical_rect); + +PANGO2_AVAILABLE_IN_ALL +int pango2_line_iter_get_line_baseline (Pango2LineIter *iter); + +PANGO2_AVAILABLE_IN_ALL +int pango2_line_iter_get_run_baseline (Pango2LineIter *iter); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(Pango2LineIter, pango2_line_iter_free) + +G_END_DECLS diff --git a/pango2/pango-line-private.h b/pango2/pango-line-private.h new file mode 100644 index 00000000..963674c2 --- /dev/null +++ b/pango2/pango-line-private.h @@ -0,0 +1,87 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-line.h" +#include "pango-break.h" +#include "pango-attributes.h" +#include "pango-glyph-item-private.h" + + +typedef struct _LineData LineData; +struct _LineData { + char *text; + int length; + int n_chars; + Pango2Direction direction; + + Pango2AttrList *attrs; + Pango2LogAttr *log_attrs; +}; + +LineData * line_data_new (void); +LineData * line_data_ref (LineData *data); +void line_data_unref (LineData *data); +void line_data_clear (LineData *data); + +struct _Pango2Line +{ + Pango2Context *context; + LineData *data; + + int start_index; + int length; + int start_offset; + int n_chars; + GSList *runs; + Pango2Run **run_array; + int n_runs; + + guint wrapped : 1; + guint ellipsized : 1; + guint hyphenated : 1; + guint justified : 1; + guint starts_paragraph : 1; + guint ends_paragraph : 1; + guint has_extents : 1; + + Pango2Direction direction; + + Pango2Rectangle ink_rect; + Pango2Rectangle logical_rect; +}; + +Pango2Line * pango2_line_new (Pango2Context *context, + LineData *data); + +void pango2_line_ellipsize (Pango2Line *line, + Pango2Context *context, + Pango2EllipsizeMode ellipsize, + int goal_width); + +void pango2_line_index_to_run (Pango2Line *line, + int idx, + Pango2Run **run); + +void pango2_line_get_empty_extents (Pango2Line *line, + Pango2LeadingTrim trim, + Pango2Rectangle *logical_rect); + +void pango2_line_check_invariants (Pango2Line *line); diff --git a/pango2/pango-line.c b/pango2/pango-line.c new file mode 100644 index 00000000..992a29fe --- /dev/null +++ b/pango2/pango-line.c @@ -0,0 +1,1603 @@ +#include "config.h" + +#include "pango-line-private.h" + +#include "pango-tabs.h" +#include "pango-impl-utils.h" +#include "pango-attributes-private.h" +#include "pango-attr-private.h" +#include "pango-attr-list-private.h" +#include "pango-attr-iterator-private.h" +#include "pango-item-private.h" +#include "pango-glyph-iter-private.h" +#include "pango-run-private.h" +#include "pango-font-metrics-private.h" + +#include <math.h> +#include <hb-ot.h> + +/** + * Pango2Line: + * + * A `Pango2Line` is an immutable object which represents a line resulting + * from laying out text via `Pango2Layout` or `Pango2LineBreaker`. + * + * A line consists of a number of runs (i.e. ranges of text with uniform + * script, font and attributes that are shaped as a unit). Runs are + * represented as [struct@Pango2.Run] objects. + * + * A `Pango2Line` always has its origin at the leftmost point of its + * baseline. To position lines in an entire paragraph of text (i.e. in layout + * coordinates), the `Pango2Lines` object stores X and Y coordinates to + * offset each line to. + * + * The most convenient way to access the visual extents and components + * of a `Pango2Line` is via a [struct@Pango2.LineIter] iterator. + */ + +/* {{{ LineData */ + +void +line_data_clear (LineData *data) +{ + g_free (data->text); + g_clear_pointer (&data->attrs, pango2_attr_list_unref); + g_free (data->log_attrs); +} + +LineData * +line_data_new (void) +{ + return g_rc_box_new0 (LineData); +} + +LineData * +line_data_ref (LineData *data) +{ + return g_rc_box_acquire (data); +} + +void +line_data_unref (LineData *data) +{ + g_rc_box_release_full (data, (GDestroyNotify) line_data_clear); +} + +/* }}} */ +/* {{{ Pango2Line implementation */ + +G_DEFINE_BOXED_TYPE (Pango2Line, pango2_line, + pango2_line_copy, pango2_line_free); + +/* }}} */ +/* {{{ Justification */ + +static inline void +distribute_letter_spacing (int letter_spacing, + int *space_left, + int *space_right) +{ + *space_left = letter_spacing / 2; + + /* hinting */ + if ((letter_spacing & (PANGO2_SCALE - 1)) == 0) + *space_left = PANGO2_UNITS_ROUND (*space_left); + *space_right = letter_spacing - *space_left; +} + +static int +pango2_line_compute_width (Pango2Line *line) +{ + int width = 0; + + /* Compute the width of the line currently - inefficient, but easier + * than keeping the current width of the line up to date everywhere + */ + for (GSList *l = line->runs; l; l = l->next) + { + Pango2GlyphItem *run = l->data; + width += pango2_glyph_string_get_width (run->glyphs); + } + + return width; +} + +static void +justify_clusters (Pango2Line *line, + int *remaining_width) +{ + int total_remaining_width, total_gaps = 0; + int added_so_far, gaps_so_far; + gboolean is_hinted; + GSList *run_iter; + enum { + MEASURE, + ADJUST + } mode; + + total_remaining_width = *remaining_width; + if (total_remaining_width <= 0) + return; + + /* hint to full pixel if total remaining width was so */ + is_hinted = (total_remaining_width & (PANGO2_SCALE - 1)) == 0; + + for (mode = MEASURE; mode <= ADJUST; mode++) + { + gboolean leftedge = TRUE; + Pango2GlyphString *rightmost_glyphs = NULL; + int rightmost_space = 0; + int residual = 0; + + added_so_far = 0; + gaps_so_far = 0; + + for (run_iter = line->runs; run_iter; run_iter = run_iter->next) + { + Pango2GlyphItem *run = run_iter->data; + Pango2GlyphString *glyphs = run->glyphs; + Pango2GlyphItemIter cluster_iter; + gboolean have_cluster; + int dir; + int offset; + + dir = run->item->analysis.level % 2 == 0 ? +1 : -1; + offset = run->item->char_offset; + for (have_cluster = dir > 0 ? + pango2_glyph_item_iter_init_start (&cluster_iter, run, line->data->text) : + pango2_glyph_item_iter_init_end (&cluster_iter, run, line->data->text); + have_cluster; + have_cluster = dir > 0 ? + pango2_glyph_item_iter_next_cluster (&cluster_iter) : + pango2_glyph_item_iter_prev_cluster (&cluster_iter)) + { + int i; + int width = 0; + + /* don't expand in the middle of graphemes */ + if (!line->data->log_attrs[offset + cluster_iter.start_char].is_cursor_position) + continue; + + for (i = cluster_iter.start_glyph; i != cluster_iter.end_glyph; i += dir) + width += glyphs->glyphs[i].geometry.width; + + /* also don't expand zero-width clusters. */ + if (width == 0) + continue; + + gaps_so_far++; + + if (mode == ADJUST) + { + int leftmost, rightmost; + int adjustment, space_left, space_right; + + adjustment = total_remaining_width / total_gaps + residual; + if (is_hinted) + { + int old_adjustment = adjustment; + adjustment = PANGO2_UNITS_ROUND (adjustment); + residual = old_adjustment - adjustment; + } + /* distribute to before/after */ + distribute_letter_spacing (adjustment, &space_left, &space_right); + + if (cluster_iter.start_glyph < cluster_iter.end_glyph) + { + /* LTR */ + leftmost = cluster_iter.start_glyph; + rightmost = cluster_iter.end_glyph - 1; + } + else + { + /* RTL */ + leftmost = cluster_iter.end_glyph + 1; + rightmost = cluster_iter.start_glyph; + } + /* Don't add to left-side of left-most glyph of left-most non-zero run. */ + if (leftedge) + leftedge = FALSE; + else + { + glyphs->glyphs[leftmost].geometry.width += space_left ; + glyphs->glyphs[leftmost].geometry.x_offset += space_left ; + added_so_far += space_left; + } + /* Don't add to right-side of right-most glyph of right-most non-zero run. */ + { + /* Save so we can undo later. */ + rightmost_glyphs = glyphs; + rightmost_space = space_right; + + glyphs->glyphs[rightmost].geometry.width += space_right; + added_so_far += space_right; + } + } + } + } + + if (mode == MEASURE) + { + total_gaps = gaps_so_far - 1; + + if (total_gaps == 0) + { + /* a single cluster, can't really justify it */ + return; + } + } + else /* mode == ADJUST */ + { + if (rightmost_glyphs) + { + rightmost_glyphs->glyphs[rightmost_glyphs->num_glyphs - 1].geometry.width -= rightmost_space; + added_so_far -= rightmost_space; + } + } + } + + *remaining_width -= added_so_far; +} + +static void +justify_words (Pango2Line *line, + int *remaining_width) +{ + int total_remaining_width, total_space_width = 0; + int added_so_far, spaces_so_far; + gboolean is_hinted; + GSList *run_iter; + enum { + MEASURE, + ADJUST + } mode; + + total_remaining_width = *remaining_width; + if (total_remaining_width <= 0) + return; + + /* hint to full pixel if total remaining width was so */ + is_hinted = (total_remaining_width & (PANGO2_SCALE - 1)) == 0; + + for (mode = MEASURE; mode <= ADJUST; mode++) + { + added_so_far = 0; + spaces_so_far = 0; + + for (run_iter = line->runs; run_iter; run_iter = run_iter->next) + { + Pango2GlyphItem *run = run_iter->data; + Pango2GlyphString *glyphs = run->glyphs; + Pango2GlyphItemIter cluster_iter; + gboolean have_cluster; + int offset; + + offset = run->item->char_offset; + for (have_cluster = pango2_glyph_item_iter_init_start (&cluster_iter, run, line->data->text); + have_cluster; + have_cluster = pango2_glyph_item_iter_next_cluster (&cluster_iter)) + { + int i; + int dir; + + if (!line->data->log_attrs[offset + cluster_iter.start_char].is_expandable_space) + continue; + dir = (cluster_iter.start_glyph < cluster_iter.end_glyph) ? 1 : -1; + for (i = cluster_iter.start_glyph; i != cluster_iter.end_glyph; i += dir) + { + int glyph_width = glyphs->glyphs[i].geometry.width; + + if (glyph_width == 0) + continue; + + spaces_so_far += glyph_width; + + if (mode == ADJUST) + { + int adjustment; + + adjustment = ((guint64) spaces_so_far * total_remaining_width) / total_space_width - added_so_far; + if (is_hinted) + adjustment = PANGO2_UNITS_ROUND (adjustment); + + glyphs->glyphs[i].geometry.width += adjustment; + added_so_far += adjustment; + } + } + } + } + + if (mode == MEASURE) + { + total_space_width = spaces_so_far; + + if (total_space_width == 0) + { + justify_clusters (line, remaining_width); + return; + } + } + } + + *remaining_width -= added_so_far; +} + +/* }}} */ +/* {{{ Extents */ + +static void +compute_extents (Pango2Line *line, + Pango2LeadingTrim trim, + Pango2Rectangle *ink, + Pango2Rectangle *logical) +{ + int x_pos = 0; + + if (!line->runs) + { + memset (ink, 0, sizeof (Pango2Rectangle)); + pango2_line_get_empty_extents (line, trim, logical); + return; + } + + for (GSList *l = line->runs; l; l = l->next) + { + Pango2Run *run = l->data; + Pango2Rectangle run_ink; + Pango2Rectangle run_logical; + int new_pos; + + pango2_run_get_extents (run, trim, &run_ink, &run_logical); + + if (ink->width == 0 || ink->height == 0) + { + *ink = run_ink; + ink->x += x_pos; + } + else if (run_ink.width != 0 && run_ink.height != 0) + { + new_pos = MIN (ink->x, x_pos + run_ink.x); + ink->width = MAX (ink->x + ink->width, + x_pos + run_ink.x + run_ink.width) - new_pos; + ink->x = new_pos; + + new_pos = MIN (ink->y, run_ink.y); + ink->height = MAX (ink->y + ink->height, + run_ink.y + run_ink.height) - new_pos; + ink->y = new_pos; + } + + if (l == line->runs) + { + *logical = run_logical; + logical->x += x_pos; + } + else + { + new_pos = MIN (logical->x, x_pos + run_logical.x); + logical->width = MAX (logical->x + logical->width, + x_pos + run_logical.x + run_logical.width) - new_pos; + logical->x = new_pos; + + new_pos = MIN (logical->y, run_logical.y); + logical->height = MAX (logical->y + logical->height, + run_logical.y + run_logical.height) - new_pos; + logical->y = new_pos; + } + + x_pos += run_logical.width; + } +} + +/* }}} */ +/* {{{ Private API */ + +void +pango2_line_check_invariants (Pango2Line *line) +{ + /* Check that byte and char positions agree */ + g_assert (g_utf8_strlen (line->data->text + line->start_index, line->length) == line->n_chars); + g_assert (g_utf8_offset_to_pointer (line->data->text + line->start_index, line->n_chars) == line->data->text + line->start_index + line->length); + + /* Check that runs are sane */ + if (line->runs) + { + int run_min, run_max; + int n_chars; + + run_min = G_MAXINT; + run_max = 0; + n_chars = 0; + for (GSList *l = line->runs; l; l = l->next) + { + Pango2GlyphItem *run = l->data; + + run_min = MIN (run_min, run->item->offset); + run_max = MAX (run_max, run->item->offset + run->item->length); + n_chars += run->item->num_chars; + } + + g_assert (run_min == line->start_index); + g_assert (run_max == line->start_index + line->length); + g_assert (n_chars == line->n_chars); + } +} + +void +pango2_line_get_empty_extents (Pango2Line *line, + Pango2LeadingTrim trim, + Pango2Rectangle *logical_rect) +{ + Pango2FontDescription *font_desc = NULL; + gboolean free_font_desc = FALSE; + double line_height_factor = 0.0; + int absolute_line_height = 0; + Pango2Font *font; + + font_desc = pango2_context_get_font_description (line->context); + + if (line->data->attrs) + { + Pango2AttrIterator iter; + int start, end; + + pango2_attr_list_init_iterator (line->data->attrs, &iter); + + do + { + pango2_attr_iterator_range (&iter, &start, &end); + + if (start <= line->start_index && line->start_index < end) + { + Pango2Attribute *attr; + + if (!free_font_desc) + { + font_desc = pango2_font_description_copy_static (font_desc); + free_font_desc = TRUE; + } + + pango2_attr_iterator_get_font (&iter, font_desc, NULL, NULL); + + attr = pango2_attr_iterator_get (&iter, PANGO2_ATTR_LINE_HEIGHT); + if (attr) + line_height_factor = attr->double_value; + + attr = pango2_attr_iterator_get (&iter, PANGO2_ATTR_ABSOLUTE_LINE_HEIGHT); + if (attr) + absolute_line_height = attr->int_value; + + break; + } + } + while (pango2_attr_iterator_next (&iter)); + + pango2_attr_iterator_clear (&iter); + } + + memset (logical_rect, 0, sizeof (Pango2Rectangle)); + + font = pango2_context_load_font (line->context, font_desc); + if (font) + { + Pango2FontMetrics *metrics; + + metrics = pango2_font_get_metrics (font, pango2_context_get_language (line->context)); + if (metrics) + { + logical_rect->y = - pango2_font_metrics_get_ascent (metrics); + logical_rect->height = - logical_rect->y + pango2_font_metrics_get_descent (metrics); + + if (trim != PANGO2_LEADING_TRIM_BOTH) + { + int leading; + + if (absolute_line_height != 0 || line_height_factor != 0.0) + { + int line_height; + + line_height = MAX (absolute_line_height, ceilf (line_height_factor * logical_rect->height)); + + leading = line_height - logical_rect->height; + } + else + { + leading = MAX (metrics->height - (metrics->ascent + metrics->descent), 0); + } + + if ((trim & PANGO2_LEADING_TRIM_START) == 0) + logical_rect->y -= leading / 2; + if (trim == PANGO2_LEADING_TRIM_NONE) + logical_rect->height += leading; + else + logical_rect->height += (leading - leading / 2); + } + + pango2_font_metrics_free (metrics); + } + + g_object_unref (font); + } + + if (free_font_desc) + pango2_font_description_free (font_desc); +} + +/*< private > + * pango2_line_new: + * @context: the `Pango2Context` + * @data: the `LineData` + * + * Creates a new `Pango2Line`. + * + * The line shares the immutable `LineData` with other lines. + * + * The @context is needed for shape rendering. + * + * Returns: new `Pango2Line` + */ +Pango2Line * +pango2_line_new (Pango2Context *context, + LineData *data) +{ + Pango2Line *line; + + line = g_new0 (Pango2Line, 1); + line->context = g_object_ref (context); + line->data = line_data_ref (data); + + return line; +} + +/*< private > + * pango2_line_index_to_run: + * @line: a `Pango2Line` + * @idx: a byte offset in the line + * @run: (out): return location for the run + * + * Finds the run in @line which contains @idx. + */ +void +pango2_line_index_to_run (Pango2Line *line, + int idx, + Pango2Run **run) +{ + *run = NULL; + + for (GSList *l = line->runs; l; l = l->next) + { + Pango2Run *r = l->data; + Pango2Item *item; + + item = pango2_run_get_glyph_item (r)->item; + if (item->offset <= idx && idx < item->offset + item->length) + { + *run = r; + break; + } + } +} + +/* }}} */ +/* {{{ Public API */ + +Pango2Line * +pango2_line_copy (Pango2Line *line) +{ + Pango2Line *copy; + + if (line == NULL) + return NULL; + + copy = g_new0 (Pango2Line, 1); + copy->context = g_object_ref (line->context); + copy->data = line_data_ref (line->data); + copy->start_index = line->start_index; + copy->length = line->length; + copy->start_offset = line->start_offset; + copy->n_chars = line->n_chars; + copy->wrapped = line->wrapped; + copy->ellipsized = line->ellipsized; + copy->hyphenated = line->hyphenated; + copy->justified = TRUE; + copy->starts_paragraph = line->starts_paragraph; + copy->ends_paragraph = line->ends_paragraph; + copy->has_extents = FALSE; + copy->direction = line->direction; + copy->runs = g_slist_copy_deep (line->runs, (GCopyFunc) pango2_glyph_item_copy, NULL); + copy->n_runs = line->n_runs; + + return copy; +} + +void +pango2_line_free (Pango2Line *line) +{ + g_object_unref (line->context); + line_data_unref (line->data); + g_slist_free_full (line->runs, (GDestroyNotify)pango2_glyph_item_free); + g_free (line->run_array); + g_free (line); +} + +/* {{{ Simple getters */ + +/** + * pango2_line_get_run_count: + * @line: a `Pango2Line` + * + * Gets the number of runs in the line. + * + * Returns: the number of runs + */ +int +pango2_line_get_run_count (Pango2Line *line) +{ + g_return_val_if_fail (line != NULL, 0); + + pango2_line_get_runs (line); + + return line->n_runs; +} + +/** + * pango2_line_get_runs: + * @line: a `Pango2Line` + * + * Gets the runs of the line. + * + * Note that the returned list and its contents + * are owned by Pango2 and must not be modified. + * + * The length of the returned array can be obtained + * with [method@Pango2.Line.get_run_count]. + * + * Returns: (transfer none): an array of `Pango2Run` + */ +Pango2Run ** +pango2_line_get_runs (Pango2Line *line) +{ + g_return_val_if_fail (line != NULL, NULL); + + if (!line->run_array) + { + GSList *l; + int i; + + line->n_runs = g_slist_length (line->runs); + + line->run_array = g_new (Pango2Run *, line->n_runs); + for (l = line->runs, i = 0; l; l = l->next, i++) + line->run_array[i] = l->data; + } + + return line->run_array; +} + +/** + * pango2_line_get_text: + * @line: a `Pango2Line` + * @start_index: the byte index of the first byte of @line + * @length: the number of bytes in @line + * + * Gets the text that @line presents. + * + * The `Pango2Line` represents the slice from @start_index + * to @start_index + @length of the returned string. + * + * The returned string is owned by @line and must not + * be modified. + * + * Returns: the text + */ +const char * +pango2_line_get_text (Pango2Line *line, + int *start_index, + int *length) +{ + g_return_val_if_fail (line != NULL, NULL); + g_return_val_if_fail (start_index != NULL, NULL); + g_return_val_if_fail (length != NULL, NULL); + + *start_index = line->start_index; + *length = line->length; + + return line->data->text; +} + +/** + * pango2_line_get_start_index: + * @line: a `Pango2Line` + * + * Returns the start index of the line, as byte index + * into the text of the layout. + * + * Returns: the start index of the line + */ +int +pango2_line_get_start_index (Pango2Line *line) +{ + g_return_val_if_fail (line != NULL, 0); + + return line->start_index; +} + +/** + * pango2_line_get_length: + * @line: a `Pango2Line` + * + * Returns the length of the line, in bytes. + * + * Returns: the length of the line + */ +int +pango2_line_get_length (Pango2Line *line) +{ + g_return_val_if_fail (line != NULL, 0); + + return line->length; +} + +/** + * pango2_line_get_log_attrs: + * @line: a `Pango2Line` + * @start_offset: the character offset of the first character of @line + * @n_attrs: the number of attributes that apply to @line + * + * Gets the `Pango2LogAttr` array for the line. + * + * The `Pango2LogAttrs` for @line are the slice from @start_offset + * to @start_offset+@n_attrs of the returned array. @n_attrs is + * be the number of characters plus one. + * + * The returned array is owned by @line and must not be modified. + * + * Returns: the `Pango2LogAttr` array + */ +const Pango2LogAttr * +pango2_line_get_log_attrs (Pango2Line *line, + int *start_offset, + int *n_attrs) +{ + g_return_val_if_fail (line != NULL, NULL); + g_return_val_if_fail (start_offset != NULL, NULL); + g_return_val_if_fail (n_attrs != NULL, NULL); + + *start_offset = line->start_offset; + *n_attrs = line->n_chars + 1; + + return line->data->log_attrs; +} + +/** + * pango2_line_is_wrapped: + * @line: a `Pango2Line` + * + * Gets whether the line is wrapped. + * + * Returns: `TRUE` if @line has been wrapped + */ +gboolean +pango2_line_is_wrapped (Pango2Line *line) +{ + g_return_val_if_fail (line != NULL, FALSE); + + return line->wrapped; +} + +/** + * pango2_line_is_ellipsized: + * @line: a `Pango2Line` + * + * Gets whether the line is ellipsized. + * + * Returns: `TRUE` if @line has been ellipsized + */ +gboolean +pango2_line_is_ellipsized (Pango2Line *line) +{ + g_return_val_if_fail (line != NULL, FALSE); + + return line->ellipsized; +} + +/** + * pango2_line_is_hyphenated: + * @line: a `Pango2Line` + * + * Gets whether the line is hyphenated. + * + * Returns: `TRUE` if @line has been hyphenated + */ +gboolean +pango2_line_is_hyphenated (Pango2Line *line) +{ + g_return_val_if_fail (line != NULL, FALSE); + + return line->hyphenated; +} + +/** + * pango2_line_is_justified: + * @line: a `Pango2Line` + * + * Gets whether the line is justified. + * + * See [method@Pango2.Line.justify]. + * + * Returns: `TRUE` if @line has been justified + */ +gboolean +pango2_line_is_justified (Pango2Line *line) +{ + g_return_val_if_fail (line != NULL, FALSE); + + return line->justified; +} + +/** + * pango2_line_is_paragraph_start: + * @line: a `Pango2Line` + * + * Gets whether the line is the first of a paragraph. + * + * Returns: `TRUE` if @line starts a paragraph + */ +gboolean +pango2_line_is_paragraph_start (Pango2Line *line) +{ + g_return_val_if_fail (line != NULL, FALSE); + + return line->starts_paragraph; +} + +/** + * pango2_line_is_paragraph_end: + * @line: a `Pango2Line` + * + * Gets whether the line is the last of a paragraph. + * + * Returns: `TRUE` if @line ends a paragraph + */ +gboolean +pango2_line_is_paragraph_end (Pango2Line *line) +{ + g_return_val_if_fail (line != NULL, FALSE); + + return line->ends_paragraph; +} + +/** + * pango2_line_get_resolved_direction: + * @line: a `Pango2Line` + * + * Gets the resolved direction of the line. + * + * Returns: the resolved direction of @line + */ +Pango2Direction +pango2_line_get_resolved_direction (Pango2Line *line) +{ + g_return_val_if_fail (line != NULL, PANGO2_DIRECTION_LTR); + + return line->direction; +} + +/* }}} */ +/* {{{ Justification */ + +/** + * pango2_line_justify: + * @line: (transfer full): a `Pango2Line` + * @width: the width to justify @line to + * + * Creates a new `Pango2Line` that is justified + * copy of @line. + * + * The content of the returned line is justified + * to fill the given width, by modifying inter-word + * spaces (and possibly intra-word spaces too). + * + * Note that this function consumes @line. + * + * Returns: (transfer full): a new `Pango2Line` + */ +Pango2Line * +pango2_line_justify (Pango2Line *line, + int width) +{ + int remaining_width; + Pango2Line *copy; + + g_return_val_if_fail (line != NULL, NULL); + + remaining_width = width - pango2_line_compute_width (line); + if (remaining_width <= 0) + return line; + + copy = pango2_line_new (line->context, line->data); + copy->start_index = line->start_index; + copy->length = line->length; + copy->start_offset = line->start_offset; + copy->n_chars = line->n_chars; + copy->wrapped = line->wrapped; + copy->ellipsized = line->ellipsized; + copy->hyphenated = line->hyphenated; + copy->justified = TRUE; + copy->starts_paragraph = line->starts_paragraph; + copy->ends_paragraph = line->ends_paragraph; + copy->has_extents = FALSE; + copy->direction = line->direction; + copy->runs = line->runs; + line->runs = NULL; + + justify_words (copy, &remaining_width); + + pango2_line_free (line); + + return copy; +} + +/* }}} */ +/* {{{ Extents */ + +/** + * pango2_line_get_extents: + * @line: a `Pango2Line` + * @ink_rect: (out) (optional): rectangle that will be filled with ink extents + * @logical_rect: (out) (optional): rectangle that will be filled with the logical extents + * + * Gets the extents of the line. + * + * The logical extents returned by this function always include leading. + * If you need extents with trimmed leading, use [method@Pango2.Line.get_trimmed_extents]. + * + * Note that the origin is at the left end of the baseline. + * + * Pango2 is following CSS in splitting the external leading, and giving one half of it + * to the line above, and the other half the the line below. Unless the line height is set + * via attributes, the external leading is determined as the difference between the + * height and ascent + descent in font metrics: + * + * <picture> + * <source srcset="line-height1-dark.png" media="(prefers-color-scheme: dark)"> + * <img alt="Pango2 Font Metrics" src="line-height1-light.png"> + * </picture> + * + * If spacing is set, it also gets split, for the purpose of determining the + * logical extents. + * + * <picture> + * <source srcset="line-height2-dark.png" media="(prefers-color-scheme: dark)"> + * <img alt="Pango2 Extents and Spacing" src="line-height2-light.png"> + * </picture> + * + * If line height is set, it determines the logical extents. + * + * <picture> + * <source srcset="line-height3-dark.png" media="(prefers-color-scheme: dark)"> + * <img alt="Pango2 Extents and Line Height" src="line-height3-light.png"> + * </picture> + */ +void +pango2_line_get_extents (Pango2Line *line, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect) +{ + Pango2Rectangle ink = { 0, }; + Pango2Rectangle logical = { 0, }; + + if (line->has_extents) + goto cached; + + compute_extents (line, PANGO2_LEADING_TRIM_NONE, &ink, &logical); + + line->ink_rect = ink; + line->logical_rect = logical; + line->has_extents = TRUE; + +cached: + if (ink_rect) + *ink_rect = line->ink_rect; + if (logical_rect) + *logical_rect = line->logical_rect; +} + +/** + * pango2_line_get_trimmed_extents: + * @line: a `Pango2Line` + * @trim: `Pango2LeadingTrim` flags + * @logical_rect: (out): rectangle that will be filled with the logical extents + * + * Gets trimmed logical extents of the line. + * + * The @trim flags specify if line-height attributes are taken + * into consideration for determining the logical height. See the + * [CSS inline layout](https://www.w3.org/TR/css-inline-3/#inline-height) + * specification for details. + * + * Note that the origin is at the left end of the baseline. + */ +void +pango2_line_get_trimmed_extents (Pango2Line *line, + Pango2LeadingTrim trim, + Pango2Rectangle *logical_rect) +{ + Pango2Rectangle ink = { 0, }; + + if (line->has_extents && trim == PANGO2_LEADING_TRIM_NONE) + { + *logical_rect = line->logical_rect; + return; + } + + compute_extents (line, trim, &ink, logical_rect); +} + +/* }}} */ +/* {{{ Editing API */ + +/** + * pango2_line_layout_index_to_pos: + * @line: a `Pango2Line` + * @idx: byte index within @line + * @pos: (out): rectangle in which to store the position of the grapheme + * + * Converts from an index within a `Pango2Line` to the + * position corresponding to the grapheme at that index. + * + * The return value is represented as rectangle. Note that `pos->x` is + * always the leading edge of the grapheme and `pos->x + pos->width` the + * trailing edge of the grapheme. If the directionality of the grapheme + * is right-to-left, then `pos->width` will be negative. + * + * Note that @idx is allowed to be @line->start_index + @line->length. + */ +void +pango2_line_index_to_pos (Pango2Line *line, + int idx, + Pango2Rectangle *pos) +{ + Pango2Rectangle run_logical; + Pango2Rectangle line_logical; + Pango2Run *run = NULL; + int x_pos; + + pango2_line_get_extents (line, NULL, &line_logical); + + if (!line->runs) + { + *pos = line_logical; + return; + } + + if (idx == line->start_index + line->length) + run = g_slist_last (line->runs)->data; + else + pango2_line_index_to_run (line, idx, &run); + + pango2_run_get_extents (run, PANGO2_LEADING_TRIM_BOTH, NULL, &run_logical); + + pos->y = run_logical.y; + pos->height = run_logical.height; + + /* FIXME: avoid iterating through the runs multiple times */ + + pango2_line_index_to_x (line, idx, 0, &x_pos); + pos->x = line_logical.x + x_pos; + + if (idx < line->start_index + line->length) + { + pango2_line_index_to_x (line, idx, 1, &x_pos); + pos->width = (line_logical.x + x_pos) - pos->x; + } + else + pos->width = 0; +} + +/** + * pango2_line_index_to_x: + * @line: a `Pango2Line` + * @idx: byte index within @line + * @trailing: an integer indicating the edge of the grapheme to retrieve + * the position of. If > 0, the trailing edge of the grapheme, + * if 0, the leading of the grapheme + * @x_pos: (out): location to store the x_offset (in Pango2 units) + * + * Converts an index within a `Pango2Line` to a X position. + * + * Note that @idx is allowed to be @line->start_index + @line->length. + */ +void +pango2_line_index_to_x (Pango2Line *line, + int index, + int trailing, + int *x_pos) +{ + GSList *run_list = line->runs; + int width = 0; + + while (run_list) + { + Pango2GlyphItem *run = run_list->data; + + if (run->item->offset <= index && run->item->offset + run->item->length > index) + { + int offset = g_utf8_pointer_to_offset (line->data->text, line->data->text + index); + int attr_offset; + + if (trailing) + { + while (index < line->start_index + line->length && + offset + 1 < line->data->n_chars && + !line->data->log_attrs[offset + 1].is_cursor_position) + { + offset++; + index = g_utf8_next_char (line->data->text + index) - line->data->text; + } + } + else + { + while (index > line->start_index && + !line->data->log_attrs[offset].is_cursor_position) + { + offset--; + index = g_utf8_prev_char (line->data->text + index) - line->data->text; + } + } + + attr_offset = run->item->char_offset; + pango2_glyph_string_index_to_x_full (run->glyphs, + line->data->text + run->item->offset, + run->item->length, + &run->item->analysis, + line->data->log_attrs + attr_offset, + index - run->item->offset, trailing, x_pos); + if (x_pos) + *x_pos += width; + + return; + } + + width += pango2_glyph_string_get_width (run->glyphs); + + run_list = run_list->next; + } + + if (x_pos) + *x_pos = width; +} + +/** + * pango2_line_x_to_index: + * @line: a `Pango2Line` + * @x: the X offset (in Pango2 units) from the left edge of the line + * @idx: (out): location to store calculated byte index for the grapheme + * in which the user clicked + * @trailing: (out): location to store an integer indicating where in the + * grapheme the user clicked. It will either be zero, or the number of + * characters in the grapheme. 0 represents the leading edge of the grapheme. + * + * Converts from x offset to the byte index of the corresponding character + * within the text of the line. + * + * If @x is outside the line, @idx and @trailing will point to the very + * first or very last position in the line. This determination is based on the + * resolved direction of the paragraph; for example, if the resolved direction + * is right-to-left, then an X position to the right of the line (after it) + * results in 0 being stored in @idx and @trailing. An X position to the + * left of the line results in @idx pointing to the (logical) last grapheme + * in the line and @trailing being set to the number of characters in that + * grapheme. The reverse is true for a left-to-right line. + * + * Return value: %FALSE if @x_pos was outside the line, %TRUE if inside + */ +gboolean +pango2_line_x_to_index (Pango2Line *line, + int x_pos, + int *index, + int *trailing) +{ + GSList *tmp_list; + int start_pos = 0; + int first_index = 0; /* line->start_index */ + int first_offset; + int last_index; /* start of last grapheme in line */ + int last_offset; + int end_index; /* end iterator for line */ + int end_offset; /* end iterator for line */ + int last_trailing; + gboolean suppress_last_trailing; + + /* Find the last index in the line */ + first_index = line->start_index; + + if (line->length == 0) + { + if (index) + *index = first_index; + if (trailing) + *trailing = 0; + + return FALSE; + } + + g_assert (line->length > 0); + + first_offset = g_utf8_pointer_to_offset (line->data->text, line->data->text + line->start_index); + + end_index = first_index + line->length; + end_offset = first_offset + g_utf8_pointer_to_offset (line->data->text + first_index, line->data->text + end_index); + + last_index = end_index; + last_offset = end_offset; + last_trailing = 0; + do + { + last_index = g_utf8_prev_char (line->data->text + last_index) - line->data->text; + last_offset--; + last_trailing++; + } + while (last_offset > first_offset && !line->data->log_attrs[last_offset].is_cursor_position); + + /* This is a HACK. If a program only keeps track of cursor (etc) + * indices and not the trailing flag, then the trailing index of the + * last character on a wrapped line is identical to the leading + * index of the next line. So, we fake it and set the trailing flag + * to zero. + * + * That is, if the text is "now is the time", and is broken between + * 'now' and 'is' + * + * Then when the cursor is actually at: + * + * n|o|w| |i|s| + * ^ + * we lie and say it is at: + * + * n|o|w| |i|s| + * ^ + * + * So the cursor won't appear on the next line before 'the'. + * + * Actually, any program keeping cursor + * positions with wrapped lines should distinguish leading and + * trailing cursors. + */ + if (line->wrapped) + suppress_last_trailing = TRUE; + else + suppress_last_trailing = FALSE; + + if (x_pos < 0) + { + /* pick the leftmost char */ + if (index) + *index = (line->direction == PANGO2_DIRECTION_LTR) ? first_index : last_index; + /* and its leftmost edge */ + if (trailing) + *trailing = (line->direction == PANGO2_DIRECTION_LTR || suppress_last_trailing) ? 0 : last_trailing; + + return FALSE; + } + + tmp_list = line->runs; + while (tmp_list) + { + Pango2GlyphItem *run = tmp_list->data; + int logical_width; + + logical_width = pango2_glyph_string_get_width (run->glyphs); + + if (x_pos >= start_pos && x_pos < start_pos + logical_width) + { + int offset; + gboolean char_trailing; + int grapheme_start_index; + int grapheme_start_offset; + int grapheme_end_offset; + int pos; + int char_index; + + pango2_glyph_string_x_to_index (run->glyphs, + line->data->text + run->item->offset, run->item->length, + &run->item->analysis, + x_pos - start_pos, + &pos, &char_trailing); + + char_index = run->item->offset + pos; + + /* Convert from characters to graphemes */ + + offset = g_utf8_pointer_to_offset (line->data->text, line->data->text + char_index); + + grapheme_start_offset = offset; + grapheme_start_index = char_index; + while (grapheme_start_offset > first_offset && + !line->data->log_attrs[grapheme_start_offset].is_cursor_position) + { + grapheme_start_index = g_utf8_prev_char (line->data->text + grapheme_start_index) - line->data->text; + grapheme_start_offset--; + } + + grapheme_end_offset = offset; + do + { + grapheme_end_offset++; + } + + while (grapheme_end_offset < end_offset && + !line->data->log_attrs[grapheme_end_offset].is_cursor_position); + + if (index) + *index = grapheme_start_index; + if (trailing) + { + if ((grapheme_end_offset == end_offset && suppress_last_trailing) || + offset + char_trailing <= (grapheme_start_offset + grapheme_end_offset) / 2) + *trailing = 0; + else + *trailing = grapheme_end_offset - grapheme_start_offset; + } + + return TRUE; + } + + start_pos += logical_width; + tmp_list = tmp_list->next; + } + + /* pick the rightmost char */ + if (index) + *index = (line->direction == PANGO2_DIRECTION_LTR) ? last_index : first_index; + + /* and its rightmost edge */ + if (trailing) + *trailing = (line->direction == PANGO2_DIRECTION_LTR && !suppress_last_trailing) ? last_trailing : 0; + + return FALSE; +} + +/* }}} */ +/* {{{ Cursor positioning */ + +/** + * pango2_line_get_cursor_pos: + * @line: a `Pango2Line` + * @idx: the byte index of the cursor + * @strong_pos: (out) (optional): location to store the strong cursor position + * @weak_pos: (out) (optional): location to store the weak cursor position + * + * Given an index within @line, determines the positions that of the + * strong and weak cursors if the insertion point is at that index. + * + * Note that @idx is allowed to be @line->start_index + @line->length. + * + * The position of each cursor is stored as a zero-width rectangle + * with the height of the run extents. + * + * <picture> + * <source srcset="cursor-positions-dark.png" media="(prefers-color-scheme: dark)"> + * <img alt="Cursor positions" src="cursor-positions-light.png"> + * </picture> + * + * The strong cursor location is the location where characters of the + * directionality equal to the base direction of the layout are inserted. + * The weak cursor location is the location where characters of the + * directionality opposite to the base direction of the layout are inserted. + * + * The following example shows text with both a strong and a weak cursor. + * + * <picture> + * <source srcset="split-cursor-dark.png" media="(prefers-color-scheme: dark)"> + * <img alt="Strong and weak cursors" src="split-cursor-light.png"> + * </picture> + * + * The strong cursor has a little arrow pointing to the right, the weak + * cursor to the left. Typing a 'c' in this situation will insert the + * character after the 'b', and typing another Hebrew character, like 'ג', + * will insert it at the end. + */ +void +pango2_line_get_cursor_pos (Pango2Line *line, + int idx, + Pango2Rectangle *strong_pos, + Pango2Rectangle *weak_pos) +{ + Pango2Rectangle line_rect = { 666, }; + Pango2Rectangle run_rect = { 666, }; + Pango2Direction dir1, dir2; + int level1, level2; + Pango2Run *run = NULL; + int x1_trailing; + int x2; + + if (idx >= line->start_index + line->length) + { + if (line->runs) + run = g_slist_last (line->runs)->data; + } + else + pango2_line_index_to_run (line, idx, &run); + + pango2_line_get_extents (line, NULL, &line_rect); + if (run) + pango2_run_get_extents (run, PANGO2_LEADING_TRIM_BOTH, NULL, &run_rect); + else + { + run_rect = line_rect; + x1_trailing = x2 = line_rect.width; + goto done; + } + + /* Examine the trailing edge of the character before the cursor */ + if (idx == line->start_index) + { + dir1 = line->direction; + level1 = dir1 == PANGO2_DIRECTION_LTR ? 0 : 1; + if (line->direction == PANGO2_DIRECTION_LTR) + x1_trailing = 0; + else + x1_trailing = line_rect.width; + } + else + { + int prev_index = g_utf8_prev_char (line->data->text + idx) - line->data->text; + + if (prev_index >= line->start_index + line->length) + { + dir1 = line->direction; + level1 = dir1 == PANGO2_DIRECTION_LTR ? 0 : 1; + x1_trailing = line_rect.width; + } + else + { + Pango2Run *prev_run; + + pango2_line_index_to_run (line, prev_index, &prev_run); + level1 = pango2_run_get_glyph_item (prev_run)->item->analysis.level; + dir1 = level1 % 2 ? PANGO2_DIRECTION_RTL : PANGO2_DIRECTION_LTR; + pango2_line_index_to_x (line, prev_index, TRUE, &x1_trailing); + } + } + + /* Examine the leading edge of the character after the cursor */ + if (idx >= line->start_index + line->length) + { + dir2 = line->direction; + level2 = dir2 == PANGO2_DIRECTION_LTR ? 0 : 1; + if (line->direction == PANGO2_DIRECTION_LTR) + x2 = line_rect.width; + else + x2 = 0; + } + else + { + pango2_line_index_to_x (line, idx, FALSE, &x2); + level2 = pango2_run_get_glyph_item (run)->item->analysis.level; + dir2 = level2 % 2 ? PANGO2_DIRECTION_RTL : PANGO2_DIRECTION_LTR; + } + +done: + if (strong_pos) + { + strong_pos->x = line_rect.x; + + if (dir1 == line->direction && + (dir2 != dir1 || level1 < level2)) + strong_pos->x += x1_trailing; + else + strong_pos->x += x2; + + strong_pos->y = run_rect.y; + strong_pos->width = 0; + strong_pos->height = run_rect.height; + } + + if (weak_pos) + { + weak_pos->x = line_rect.x; + + if (dir1 == line->direction && + (dir2 != dir1 || level1 < level2)) + weak_pos->x += x2; + else + weak_pos->x += x1_trailing; + + weak_pos->y = run_rect.y; + weak_pos->width = 0; + weak_pos->height = run_rect.height; + } +} + +/** + * pango2_line_get_caret_pos: + * @line: a `Pango2Line` + * @idx: the byte index of the cursor + * @strong_pos: (out) (optional): location to store the strong cursor position + * @weak_pos: (out) (optional): location to store the weak cursor position + * + * Given an index within @line, determines the positions of the + * strong and weak cursors if the insertion point is at that index. + * + * Note that @idx is allowed to be @line->start_index + @line->length. + * + * This is a variant of [method@Pango2.Line.get_cursor_pos] that applies + * font metric information about caret slope and offset to the positions + * it returns. + * + * <picture> + * <source srcset="caret-metrics-dark.png" media="(prefers-color-scheme: dark)"> + * <img alt="Caret metrics" src="caret-metrics-light.png"> + * </picture> + */ +void +pango2_line_get_caret_pos (Pango2Line *line, + int idx, + Pango2Rectangle *strong_pos, + Pango2Rectangle *weak_pos) +{ + Pango2Run *run = NULL; + Pango2GlyphItem *glyph_item; + hb_font_t *hb_font; + hb_position_t caret_offset, caret_run, caret_rise, descender; + + pango2_line_get_cursor_pos (line, idx, strong_pos, weak_pos); + + if (idx >= line->start_index + line->length) + { + if (line->runs) + run = g_slist_last (line->runs)->data; + } + else + pango2_line_index_to_run (line, idx, &run); + + if (!run) + return; + + glyph_item = pango2_run_get_glyph_item (run); + hb_font = pango2_font_get_hb_font (glyph_item->item->analysis.font); + + if (hb_ot_metrics_get_position (hb_font, HB_OT_METRICS_TAG_HORIZONTAL_CARET_RISE, &caret_rise) && + hb_ot_metrics_get_position (hb_font, HB_OT_METRICS_TAG_HORIZONTAL_CARET_RUN, &caret_run) && + hb_ot_metrics_get_position (hb_font, HB_OT_METRICS_TAG_HORIZONTAL_CARET_OFFSET, &caret_offset) && + hb_ot_metrics_get_position (hb_font, HB_OT_METRICS_TAG_HORIZONTAL_DESCENDER, &descender)) + { + double slope_inv; + int x_scale, y_scale; + + if (strong_pos) + strong_pos->x += caret_offset; + + if (weak_pos) + weak_pos->x += caret_offset; + + if (caret_rise == 0) + return; + + hb_font_get_scale (hb_font, &x_scale, &y_scale); + + slope_inv = (caret_run / (double) caret_rise) * (y_scale / (double) x_scale); + + if (strong_pos) + { + strong_pos->x += descender * slope_inv; + strong_pos->width = strong_pos->height * slope_inv; + if (slope_inv < 0) + strong_pos->x -= strong_pos->width; + } + + if (weak_pos) + { + weak_pos->x += descender * slope_inv; + weak_pos->width = weak_pos->height * slope_inv; + if (slope_inv < 0) + weak_pos->x -= weak_pos->width; + } + } +} + +/* }}} */ +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pango-line.h b/pango2/pango-line.h new file mode 100644 index 00000000..31458d41 --- /dev/null +++ b/pango2/pango-line.h @@ -0,0 +1,126 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <glib-object.h> + +#include <pango2/pango-types.h> +#include <pango2/pango-run.h> + +G_BEGIN_DECLS + +PANGO2_AVAILABLE_IN_ALL +GType pango2_line_get_type (void) G_GNUC_CONST; + +PANGO2_AVAILABLE_IN_ALL +Pango2Line * pango2_line_copy (Pango2Line *line); + +PANGO2_AVAILABLE_IN_ALL +void pango2_line_free (Pango2Line *line); + +PANGO2_AVAILABLE_IN_ALL +Pango2Line * pango2_line_justify (Pango2Line *line, + int width); + +PANGO2_AVAILABLE_IN_ALL +int pango2_line_get_run_count (Pango2Line *line); + +PANGO2_AVAILABLE_IN_ALL +Pango2Run ** pango2_line_get_runs (Pango2Line *line); + +PANGO2_AVAILABLE_IN_ALL +const char * pango2_line_get_text (Pango2Line *line, + int *start_index, + int *length); + +PANGO2_AVAILABLE_IN_ALL +int pango2_line_get_start_index (Pango2Line *line); + +PANGO2_AVAILABLE_IN_ALL +int pango2_line_get_length (Pango2Line *line); + +PANGO2_AVAILABLE_IN_ALL +const Pango2LogAttr * pango2_line_get_log_attrs (Pango2Line *line, + int *start_offset, + int *n_attrs); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_line_is_wrapped (Pango2Line *line); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_line_is_ellipsized (Pango2Line *line); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_line_is_hyphenated (Pango2Line *line); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_line_is_justified (Pango2Line *line); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_line_is_paragraph_start (Pango2Line *line); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_line_is_paragraph_end (Pango2Line *line); + +PANGO2_AVAILABLE_IN_ALL +Pango2Direction pango2_line_get_resolved_direction (Pango2Line *line); + +PANGO2_AVAILABLE_IN_ALL +void pango2_line_get_extents (Pango2Line *line, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect); + +PANGO2_AVAILABLE_IN_ALL +void pango2_line_get_trimmed_extents (Pango2Line *line, + Pango2LeadingTrim trim, + Pango2Rectangle *logical_rect); + +PANGO2_AVAILABLE_IN_ALL +void pango2_line_index_to_pos (Pango2Line *line, + int idx, + Pango2Rectangle *pos); + +PANGO2_AVAILABLE_IN_ALL +void pango2_line_index_to_x (Pango2Line *line, + int idx, + int trailing, + int *x_pos); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_line_x_to_index (Pango2Line *line, + int x, + int *idx, + int *trailing); + +PANGO2_AVAILABLE_IN_ALL +void pango2_line_get_cursor_pos (Pango2Line *line, + int idx, + Pango2Rectangle *strong_pos, + Pango2Rectangle *weak_pos); + +PANGO2_AVAILABLE_IN_ALL +void pango2_line_get_caret_pos (Pango2Line *line, + int idx, + Pango2Rectangle *strong_pos, + Pango2Rectangle *weak_pos); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(Pango2Line, pango2_line_free) + +G_END_DECLS diff --git a/pango2/pango-lines-private.h b/pango2/pango-lines-private.h new file mode 100644 index 00000000..8832fba0 --- /dev/null +++ b/pango2/pango-lines-private.h @@ -0,0 +1,38 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-lines.h" + + +typedef struct _Position Position; +struct _Position +{ + int x, y; +}; + +struct _Pango2Lines +{ + GObject parent_instance; + + GPtrArray *lines; + GArray *positions; + guint serial; +}; diff --git a/pango2/pango-lines.c b/pango2/pango-lines.c new file mode 100644 index 00000000..e528ec05 --- /dev/null +++ b/pango2/pango-lines.c @@ -0,0 +1,1332 @@ +#include "config.h" + +#include "pango-lines-private.h" +#include "pango-line-private.h" +#include "pango-item-private.h" +#include "pango-run-private.h" +#include "pango-line-iter-private.h" + +/** + * Pango2Lines: + * + * A `Pango2Lines` object represents the result of formatting an + * entire paragraph (or more) of text. + * + * A `Pango2Lines` object contains a list of `Pango2Line` objects, + * together with information about where to position each line + * in layout coordinates. + * + * `Pango2Lines` has APIs to query collective information about + * its lines (such as ellipsization or unknown glyphs), to translate + * between logical character positions within the text and the physical + * position of the resulting glyphs, and to determine cursor positions + * for editing the text. + * + * One way to obtain a `Pango2Lines` object is to use a [class@Pango2.Layout]. + * But it is also possible to populate a `Pango2Lines` manually with lines + * produced by a [class@Pango2.LineBreaker] object. + * + * Note that the lines that make up a `Pango2Lines` object don't have to + * share the same underlying text. Therefore, using byte indexes to refer + * to positions within the `Pango2Lines` is, in general, ambiguous. All the + * `Pango2Lines` APIs that take a byte index as argument or return one have + * a `Pango2Line*` companion argument to handle this situation. When all + * the lines in the `Pango2Lines` share the same text (such as when they + * originate from the same `Pango2Layout`), it is safe to always pass `NULL` + * for the `Pango2Lines*`. + * + * The most convenient way to access the visual extents and components + * of a `Pango2Lines` is via a [struct@Pango2.LineIter] iterator. + */ + +/* {{{ Pango2Lines implementation */ + +struct _Pango2LinesClass +{ + GObjectClass parent_class; +}; + +G_DEFINE_TYPE (Pango2Lines, pango2_lines, G_TYPE_OBJECT) + +enum { + PROP_UNKNOWN_GLYPHS_COUNT = 1, + PROP_WRAPPED, + PROP_ELLIPSIZED, + PROP_HYPHENATED, + N_PROPERTIES +}; + +static GParamSpec *properties[N_PROPERTIES] = { NULL, }; + +static void +pango2_lines_init (Pango2Lines *lines) +{ + lines->serial = 1; + lines->lines = g_ptr_array_new_with_free_func ((GDestroyNotify) pango2_line_free); + lines->positions = g_array_new (FALSE, FALSE, sizeof (Position)); +} + +static void +pango2_lines_finalize (GObject *object) +{ + Pango2Lines *lines = PANGO2_LINES (object); + + g_ptr_array_unref (lines->lines); + g_array_unref (lines->positions); + + G_OBJECT_CLASS (pango2_lines_parent_class)->finalize (object); +} + +static void +pango2_lines_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + Pango2Lines *self = PANGO2_LINES (object); + + switch (prop_id) + { + case PROP_UNKNOWN_GLYPHS_COUNT: + g_value_set_int (value, pango2_lines_get_unknown_glyphs_count (self)); + break; + + case PROP_WRAPPED: + g_value_set_boolean (value, pango2_lines_is_wrapped (self)); + break; + + case PROP_ELLIPSIZED: + g_value_set_boolean (value, pango2_lines_is_ellipsized (self)); + break; + + case PROP_HYPHENATED: + g_value_set_boolean (value, pango2_lines_is_hyphenated (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +pango2_lines_class_init (Pango2LinesClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->finalize = pango2_lines_finalize; + object_class->get_property = pango2_lines_get_property; + + /** + * Pango2Lines:unknown-glyphs-count: (attributes org.gtk.Property.get=pango2_lines_get_unknown_glyphs_count) + * + * The number of unknown glyphs in the `Pango2Lines`. + */ + properties[PROP_UNKNOWN_GLYPHS_COUNT] = + g_param_spec_uint ("unknown-glyphs-count", NULL, NULL, 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2Lines:wrapped: (attributes org.gtk.Property.get=pango2_lines_is_wrapped) + * + * `TRUE` if the `Pango2Lines` contains any wrapped lines. + */ + properties[PROP_WRAPPED] = + g_param_spec_boolean ("wrapped", NULL, NULL, FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2Lines:ellipsized: (attributes org.gtk.Property.get=pango2_lines_is_ellipsized) + * + * `TRUE` if the `Pango2Lines` contains any ellipsized lines. + */ + properties[PROP_ELLIPSIZED] = + g_param_spec_boolean ("ellipsized", NULL, NULL, FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * Pango2Lines:hyphenated: (attributes org.gtk.Property.get=pango2_lines_is_hyphenated) + * + * `TRUE` if the `Pango2Lines` contains any hyphenated lines. + */ + properties[PROP_HYPHENATED] = + g_param_spec_boolean ("hyphenated", NULL, NULL, FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPERTIES, properties); +} + +/* }}} */ +/* {{{ Utilities*/ + +typedef struct { + int x; + int pos; +} CursorPos; + +static int +compare_cursor (gconstpointer v1, + gconstpointer v2) +{ + const CursorPos *c1 = v1; + const CursorPos *c2 = v2; + + return c1->x - c2->x; +} + +static void +pango2_line_get_cursors (Pango2Lines *lines, + Pango2Line *line, + gboolean strong, + GArray *cursors) +{ + const char *start, *end; + int start_offset; + int j; + const char *p; + Pango2Rectangle pos; + Position offset; + + g_assert (g_array_get_element_size (cursors) == sizeof (CursorPos)); + g_assert (cursors->len == 0); + + for (int i = 0; i < lines->lines->len; i++) + { + if (line == g_ptr_array_index (lines->lines, i)) + { + offset = g_array_index (lines->positions, Position, i); + break; + } + } + + start = line->data->text + line->start_index; + end = start + line->length; + start_offset = line->start_offset; + + if (line->ends_paragraph) + end++; + + for (j = start_offset, p = start; p < end; j++, p = g_utf8_next_char (p)) + { + int idx = p - line->data->text; + + if (line->data->log_attrs[j].is_cursor_position) + { + CursorPos cursor; + + pango2_line_get_cursor_pos (line, idx, + strong ? &pos : NULL, + strong ? NULL : &pos); + + cursor.x = pos.x + offset.x; + cursor.pos = idx; + g_array_append_val (cursors, cursor); + } + } + + g_array_sort (cursors, compare_cursor); +} + +/* }}} */ +/* {{{ Public API */ + +/** + * pango2_lines_new: + * + * Creates an empty `Pango2Lines` object. + * + * Returns: a newly allocated `Pango2Lines` + */ +Pango2Lines * +pango2_lines_new (void) +{ + return g_object_new (PANGO2_TYPE_LINES, NULL); +} + +/** + * pango2_lines_get_serial: + * @lines: a `Pango2Lines` + * + * Returns the current serial number of @lines. + * + * The serial number is initialized to an small number larger than zero + * when a new layout is created and is increased whenever the @lines + * object is changed (i.e. more lines are added). + * + * The serial may wrap, but will never have the value 0. Since it can + * wrap, never compare it with "less than", always use "not equals". + * + * This can be used to automatically detect changes to a `Pango2Lines`, + * and is useful for example to decide whether a layout needs redrawing. + * + * Return value: The current serial number of @lines + */ +guint +pango2_lines_get_serial (Pango2Lines *lines) +{ + return lines->serial; +} + +/** + * pango2_lines_add_line: + * @lines: a `Pango2Lines` + * @line: (transfer full): the `Pango2Line` to add + * @line_x: X coordinate of the position + * @line_y: Y coordinate of the position + * + * Adds a line to the `Pango2Lines`. + * + * The coordinates are the position + * at which @line is placed. + * + * Note that this function takes ownership of the line. + */ +void +pango2_lines_add_line (Pango2Lines *lines, + Pango2Line *line, + int x_line, + int y_line) +{ + Position pos = { x_line, y_line }; + + g_ptr_array_add (lines->lines, line); + g_array_append_val (lines->positions, pos); + + lines->serial++; + if (lines->serial == 0) + lines->serial++; + + g_object_notify_by_pspec (G_OBJECT (lines), properties[PROP_UNKNOWN_GLYPHS_COUNT]); + g_object_notify_by_pspec (G_OBJECT (lines), properties[PROP_WRAPPED]); + g_object_notify_by_pspec (G_OBJECT (lines), properties[PROP_ELLIPSIZED]); + g_object_notify_by_pspec (G_OBJECT (lines), properties[PROP_HYPHENATED]); +} + +/** + * pango2_lines_get_iter: + * @lines: a `Pango2Lines` + * + * Returns an iterator to iterate over the visual extents of the lines. + * + * The returned iterator will be invaliated when more + * lines are added to @lines, and can't be used anymore + * after that point. + * + * Note that the iter holds a reference to @lines. + * + * Return value: the new `Pango2LineIter` + */ +Pango2LineIter * +pango2_lines_get_iter (Pango2Lines *lines) +{ + return pango2_line_iter_new (lines); +} + +/** + * pango2_lines_get_line_count: + * @lines: a `Pango2Lines` + * + * Gets the number of lines in @lines. + * + * Returns: the number of lines' + */ +int +pango2_lines_get_line_count (Pango2Lines *lines) +{ + return lines->lines->len; +} + +/** + * pango2_lines_get_lines: + * @lines: a `Pango2Lines` + * + * Gets the lines. + * + * The length of the returned array can be obtained with + * [method@Pango2.Lines.get_line_count]. + * + * Returns: (transfer none): the array of lines + */ +Pango2Line ** +pango2_lines_get_lines (Pango2Lines *lines) +{ + g_return_val_if_fail (PANGO2_IS_LINES (lines), NULL); + + return (Pango2Line **)lines->lines->pdata; +} + +/** + * pango2_lines_get_line_position: + * @lines: a `Pango2Lines` + * @num: the index of the line to get the position for + * @line_x: (out) (optional): return location for the X coordinate + * @line_y: (out) (optional): return location for the Y coordinate + * + * Gets the position of a line. + */ +void +pango2_lines_get_line_position (Pango2Lines *lines, + int num, + int *line_x, + int *line_y) +{ + Position *pos; + + g_return_if_fail (PANGO2_IS_LINES (lines)); + + if (num >= lines->lines->len) + return; + + pos = &g_array_index (lines->positions, Position, num); + + if (line_x) + *line_x = pos->x; + if (line_y) + *line_y = pos->y; +} + +/* {{{ Miscellaneous */ + +/** + * pango2_lines_get_unknown_glyphs_count: + * @lines: a `Pango2Lines` object + * + * Counts the number of unknown glyphs in the `Pango2Lines` + * + * This function can be used to determine if there are any fonts + * available to render all characters in a certain string, or when + * used in combination with `PANGO2_ATTR_FALLBACK`, to check if a + * certain font supports all the characters in the string. + * + * Return value: The number of unknown glyphs in @lines + */ +int +pango2_lines_get_unknown_glyphs_count (Pango2Lines *lines) +{ + int count = 0; + + for (int i = 0; i < lines->lines->len; i++) + { + Pango2Line *line = g_ptr_array_index (lines->lines, i); + + for (GSList *l = line->runs; l; l = l->next) + { + Pango2GlyphItem *run = l->data; + + for (int j = 0; j < run->glyphs->num_glyphs; j++) + { + if (run->glyphs->glyphs[j].glyph & PANGO2_GLYPH_UNKNOWN_FLAG) + count++; + } + } + } + + return count; +} + +/** + * pango2_lines_is_wrapped: + * @lines: a `Pango2Lines` object + * + * Returns whether any of the lines is wrapped. + * + * Returns: `TRUE` if @lines is wrapped + */ +gboolean +pango2_lines_is_wrapped (Pango2Lines *lines) +{ + for (int i = 0; i < lines->lines->len; i++) + { + Pango2Line *line = g_ptr_array_index (lines->lines, i); + if (pango2_line_is_wrapped (line)) + return TRUE; + } + + return FALSE; +} + +/** + * pango2_lines_is_ellipsized: + * @lines: a `Pango2Lines` object + * + * Returns whether any of the lines is ellipsized. + * + * Returns: `TRUE` if @lines is ellipsized + */ +gboolean +pango2_lines_is_ellipsized (Pango2Lines *lines) +{ + for (int i = 0; i < lines->lines->len; i++) + { + Pango2Line *line = g_ptr_array_index (lines->lines, i); + if (pango2_line_is_ellipsized (line)) + return TRUE; + } + + return FALSE; +} + +/** + * pango2_lines_is_hyphenated: + * @lines: a `Pango2Lines` object + * + * Returns whether any of the lines is hyphenated. + * + * Returns: `TRUE` if @lines is hyphenated + */ +gboolean +pango2_lines_is_hyphenated (Pango2Lines *lines) +{ + for (int i = 0; i < lines->lines->len; i++) + { + Pango2Line *line = g_ptr_array_index (lines->lines, i); + if (pango2_line_is_hyphenated (line)) + return TRUE; + } + + return FALSE; +} + +/* }}} */ +/* {{{ Extents */ + +/** + * pango2_lines_get_extents: + * @lines: a `Pango2Lines` object + * @ink_rect: (out) (optional): return location for the ink extents + * @logical_rect: (out) (optional): return location for the logical extents + * + * Computes the extents of the lines. + * + * Logical extents are usually what you want for positioning things. Note + * that the extents may have non-zero x and y. You may want to use those + * to offset where you render the layout. Not doing that is a very typical + * bug that shows up as right-to-left layouts not being correctly positioned + * in a layout with a set width. + * + * The extents are given in layout coordinates and in Pango2 units; layout + * coordinates begin at the top left corner. + */ +void +pango2_lines_get_extents (Pango2Lines *lines, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect) +{ + for (int i = 0; i < lines->lines->len; i++) + { + Pango2Line *line = g_ptr_array_index (lines->lines, i); + Position *pos = &g_array_index (lines->positions, Position, i); + Pango2Rectangle line_ink; + Pango2Rectangle line_logical; + Pango2LeadingTrim trim = PANGO2_LEADING_TRIM_NONE; + + if (line->starts_paragraph) + trim |= PANGO2_LEADING_TRIM_START; + if (line->ends_paragraph) + trim |= PANGO2_LEADING_TRIM_END; + + pango2_line_get_extents (line, &line_ink, NULL); + pango2_line_get_trimmed_extents (line, trim, &line_logical); + + line_ink.x += pos->x; + line_ink.y += pos->y; + line_logical.x += pos->x; + line_logical.y += pos->y; + + if (i == 0) + { + if (ink_rect) + *ink_rect = line_ink; + if (logical_rect) + *logical_rect = line_logical; + } + else + { + int new_pos; + + if (ink_rect) + { + new_pos = MIN (ink_rect->x, line_ink.x); + ink_rect->width = MAX (ink_rect->x + ink_rect->width, line_ink.x + line_ink.width) - new_pos; + ink_rect->x = new_pos; + + new_pos = MIN (ink_rect->y, line_ink.y); + ink_rect->height = MAX (ink_rect->y + ink_rect->height, line_ink.y + line_ink.height) - new_pos; + ink_rect->y = new_pos; + } + if (logical_rect) + { + new_pos = MIN (logical_rect->x, line_logical.x); + logical_rect->width = MAX (logical_rect->x + logical_rect->width, line_logical.x + line_logical.width) - new_pos; + logical_rect->x = new_pos; + + new_pos = MIN (logical_rect->y, line_logical.y); + logical_rect->height = MAX (logical_rect->y + logical_rect->height, line_logical.y + line_logical.height) - new_pos; + logical_rect->y = new_pos; + } + } + } +} + +/** + * pango2_lines_get_size: + * @lines: a `Pango2Lines` + * @width: (out) (optional): location to store the logical width + * @height: (out) (optional): location to store the logical height + * + * Determines the logical width and height of a `Pango2Lines` + * in Pango2 units. + * + * This is simply a convenience function around + * [method@Pango2.Lines.get_extents]. + */ +void +pango2_lines_get_size (Pango2Lines *lines, + int *width, + int *height) +{ + Pango2Rectangle ext; + + pango2_lines_get_extents (lines, NULL, &ext); + + if (width) + *width = ext.width; + if (height) + *height = ext.height; +} + +/** + * pango2_lines_get_baseline: + * @lines: a `Pango2Lines` object + * + * Gets the Y position of baseline of the first line. + * + * Return value: baseline of first line + */ +int +pango2_lines_get_baseline (Pango2Lines *lines) +{ + Position *pos; + + g_return_val_if_fail (PANGO2_IS_LINES (lines), 0); + + if (lines->lines->len == 0) + return 0; + + pos = &g_array_index (lines->positions, Position, 0); + + return pos->y; +} + +/** + * pango2_lines_get_x_ranges: + * @lines: a `Pango2Lines` object + * @line: the `Pango2Line` in @lines whose x ranges will be reported + * @start_line: (nullable): `Pango2Line` wrt to which @start_index is + * interpreted or `NULL` for the first matching line + * @start_index: Start byte index of the logical range. If this value + * is less than the start index for the line, then the first range + * will extend all the way to the leading edge of the layout. Otherwise, + * it will start at the leading edge of the first character. + * @end_line: (nullable): `Pango2Line` wrt to which @end_index is + * interpreted or `NULL` for the first matching line + * @end_index: Ending byte index of the logical range. If this value is + * greater than the end index for the line, then the last range will + * extend all the way to the trailing edge of the layout. Otherwise, + * it will end at the trailing edge of the last character. + * @ranges: (out) (array length=n_ranges) (transfer full): location to + * store a pointer to an array of ranges. The array will be of length + * `2*n_ranges`, with each range starting at `(*ranges)[2*n]` and of + * width `(*ranges)[2*n + 1] - (*ranges)[2*n]`. This array must be freed + * with [GLib.free]. The coordinates are relative to the layout and are in + * Pango2 units. + * @n_ranges: The number of ranges stored in @ranges + * + * Gets a list of visual ranges corresponding to a given logical range. + * + * This list is not necessarily minimal - there may be consecutive + * ranges which are adjacent. The ranges will be sorted from left to + * right. The ranges are with respect to the left edge of the entire + * layout, not with respect to the line. + */ +void +pango2_lines_get_x_ranges (Pango2Lines *lines, + Pango2Line *line, + Pango2Line *start_line, + int start_index, + Pango2Line *end_line, + int end_index, + int **ranges, + int *n_ranges) +{ + int x_offset; + int line_no, start_line_no, end_line_no; + Pango2Direction dir; + int range_count; + int width; + Pango2Rectangle ext; + int accumulated_width; + + g_return_if_fail (PANGO2_IS_LINES (lines)); + g_return_if_fail (ranges != NULL); + g_return_if_fail (n_ranges != NULL); + + pango2_lines_index_to_line (lines, line->start_index, &line, &line_no, &x_offset, NULL); + g_return_if_fail (line != NULL); + + pango2_lines_index_to_line (lines, start_index, &start_line, &start_line_no, NULL, NULL); + g_return_if_fail (start_line != NULL); + g_return_if_fail (start_line_no <= line_no); + + pango2_lines_index_to_line (lines, end_index, &end_line, &end_line_no, NULL, NULL); + g_return_if_fail (end_line != NULL); + g_return_if_fail (end_line_no >= line_no); + + /* clamp start_index and end_index to be inside line, we'll use + * the line numbers to determine overshoot. + */ + if (start_line_no < line_no || line_no < end_line_no) + { + if (start_line_no < line_no) + start_index = line->start_index; + if (end_line_no > line_no) + end_index = line->start_index + line->length; + } + + *ranges = g_new (int, 2 * (2 + g_slist_length (line->runs))); + range_count = 0; + + dir = pango2_line_get_resolved_direction (line); + + if (x_offset > 0 && + ((dir == PANGO2_DIRECTION_LTR && start_line_no < line_no) || + (dir == PANGO2_DIRECTION_RTL && end_line_no > line_no))) + { + /* add range from left edge of layout to line start */ + (*ranges)[2*range_count] = 0; + (*ranges)[2*range_count + 1] = x_offset; + range_count++; + } + + accumulated_width = 0; + for (int i = 0; i < pango2_line_get_run_count (line); i++) + { + Pango2GlyphItem *run = pango2_run_get_glyph_item (pango2_line_get_runs (line)[i]); + + if ((start_index < run->item->offset + run->item->length && + end_index > run->item->offset)) + { + int run_start_index = MAX (start_index, run->item->offset); + int run_end_index = MIN (end_index, run->item->offset + run->item->length); + int run_start_x, run_end_x; + int attr_offset; + + g_assert (run_end_index > 0); + + /* Back the end_index off one since we want to find the trailing edge of the + * preceding character + */ + + run_end_index = g_utf8_prev_char (line->data->text + run_end_index) - line->data->text; + + attr_offset = run->item->char_offset; + + pango2_glyph_string_index_to_x_full (run->glyphs, + line->data->text + run->item->offset, + run->item->length, + &run->item->analysis, + line->data->log_attrs + attr_offset, + run_start_index - run->item->offset, FALSE, + &run_start_x); + pango2_glyph_string_index_to_x_full (run->glyphs, + line->data->text + run->item->offset, + run->item->length, + &run->item->analysis, + line->data->log_attrs + attr_offset, + run_end_index - run->item->offset, TRUE, + &run_end_x); + + (*ranges)[2*range_count] = x_offset + accumulated_width + MIN (run_start_x, run_end_x); + (*ranges)[2*range_count + 1] = x_offset + accumulated_width + MAX (run_start_x, run_end_x); + } + + range_count++; + + if (i + 1 < pango2_line_get_run_count (line)) + accumulated_width += pango2_glyph_string_get_width (run->glyphs); + } + + pango2_line_get_extents (line, NULL, &ext); + pango2_lines_get_size (lines, &width, NULL); + + if (x_offset + ext.width < width && + ((dir == PANGO2_DIRECTION_LTR && start_line_no < line_no) || + (dir == PANGO2_DIRECTION_RTL && end_line_no > line_no))) + { + /* add range from line end to rigth edge of layout */ + (*ranges)[2*range_count] = x_offset + ext.width; + (*ranges)[2*range_count + 1] = width; + range_count ++; + } + + *n_ranges = range_count; +} + +/* }}} */ + /* { {{ Editing API */ + +/** + * pango2_lines_index_to_line: + * @lines: a `Pango2Lines` + * @idx: index in @lines + * @line: (inout): if *@line is `NULL`, the found line will be returned here + * @line_no: (out) (optional): return location for line number + * @x_offset: (out) (optional): return location for X offset of line + * @y_offset: (out) (optional): return location for Y offset of line + * + * Given an index (and possibly line), determine the line number, + * and offset for the line. + * + * @idx may refer to any byte position inside @lines, as well as + * the position before the first or after the last character (i.e. + * line->start_index + line->length, for the last line). + * + * If @lines contains lines with different backing data (i.e. + * more than one line with line->start_index of zero), then + * *@line must be specified to disambiguate. If all lines + * are backed by the same data, it is save to pass `NULL` + * as *@line and use this function to find the line at @idx. + */ +void +pango2_lines_index_to_line (Pango2Lines *lines, + int idx, + Pango2Line **line, + int *line_no, + int *x_offset, + int *y_offset) +{ + Pango2Line *found = NULL; + int num; + int i; + Position pos; + + g_return_if_fail (PANGO2_IS_LINES (lines)); + + for (i = 0; i < lines->lines->len; i++) + { + Pango2Line *l = g_ptr_array_index (lines->lines, i); + + if (l->start_index > idx && found) + break; + + found = l; + pos = g_array_index (lines->positions, Position, i); + num = i; + + if (*line && *line == found) + break; + + if (l->start_index + l->length > idx) + break; + } + + if (found) + { + *line = found; + if (line_no) + *line_no = num; + if (x_offset) + *x_offset = pos.x; + if (y_offset) + *y_offset = pos.y; + + return; + } + + *line = NULL; +} + +/** + * pango2_lines_pos_to_line: + * @lines: a `Pango2Lines` object + * @x: the X position (in Pango2 units) + * @y: the Y position (in Pango2 units) + * @line_x: (out) (optional): return location for the X offset of the line + * @line_y: (out) (optional): return location for the Y offset of the line + * + * Finds the line at the given position. + * + * If either the X or Y positions were not inside the layout, then the + * function returns `NULL`; on an exact hit, it returns the found line. + + * Returns: (transfer none) (nullable): the line that was found + */ +Pango2Line * +pango2_lines_pos_to_line (Pango2Lines *lines, + int x, + int y, + int *line_x, + int *line_y) +{ + g_return_val_if_fail (PANGO2_IS_LINES (lines), FALSE); + + for (int i = 0; i < lines->lines->len; i++) + { + Pango2Line *line = g_ptr_array_index (lines->lines, i); + Position pos = g_array_index (lines->positions, Position, i); + Pango2Rectangle ext; + + pango2_line_get_extents (line, NULL, &ext); + + ext.x += pos.x; + ext.y += pos.y; + + if (ext.x <= x && x <= ext.x + ext.width && + ext.y <= y && y <= ext.y + ext.height) + { + if (line_x) + *line_x = pos.x; + if (line_y) + *line_y = pos.y; + + return line; + } + } + + if (line_x) + *line_x = 0; + if (line_y) + *line_y = 0; + + return NULL; +} + +/** + * pango2_lines_index_to_pos: + * @lines: a `Pango2Lines` object + * @line: (nullable): `Pango2Line` wrt to which @idx is interpreted + * or `NULL` for the first matching line + * @idx: byte index within @line + * @pos: (out): rectangle in which to store the position of the grapheme + * + * Converts from an index within a `Pango2Line` to the + * position corresponding to the grapheme at that index. + * + * The return value is represented as rectangle. Note that `pos->x` + * is always the leading edge of the grapheme and `pos->x + pos->width` + * the trailing edge of the grapheme. If the directionality of the + * grapheme is right-to-left, then `pos->width` will be negative. + * + * Note that @idx is allowed to be @line->start_index + @line->length + * for the position off the end of the last line. + */ +void +pango2_lines_index_to_pos (Pango2Lines *lines, + Pango2Line *line, + int idx, + Pango2Rectangle *pos) +{ + int x_offset, y_offset; + + g_return_if_fail (PANGO2_IS_LINES (lines)); + g_return_if_fail (idx >= 0); + g_return_if_fail (pos != NULL); + + pango2_lines_index_to_line (lines, idx, &line, NULL, &x_offset, &y_offset); + + g_return_if_fail (line != NULL); + + pango2_line_index_to_pos (line, idx, pos); + pos->x += x_offset; + pos->y += y_offset; +} + +/** + * pango2_lines_pos_to_index: + * @lines: a `Pango2Lines` object + * @x: the X offset (in Pango2 units) from the left edge of the layout + * @y: the Y offset (in Pango2 units) from the top edge of the layout + * @idx: (out): location to store calculated byte index + * @trailing: (out): location to store a integer indicating where + * in the grapheme the user clicked. It will either be zero, or the + * number of characters in the grapheme. 0 represents the leading edge + * of the grapheme. + * + * Converts from X and Y position within lines to the byte index of the + * character at that position. + * + * Returns: (transfer none) (nullable): the line that was found + */ +Pango2Line * +pango2_lines_pos_to_index (Pango2Lines *lines, + int x, + int y, + int *idx, + int *trailing) +{ + Pango2Line *line; + int x_offset; + + g_return_val_if_fail (PANGO2_IS_LINES (lines), FALSE); + g_return_val_if_fail (idx != NULL, FALSE); + g_return_val_if_fail (trailing != NULL, FALSE); + + line = pango2_lines_pos_to_line (lines, x, y, &x_offset, NULL); + if (line) + pango2_line_x_to_index (line, x - x_offset, idx, trailing); + + return line; +} + +/* }}} */ +/* {{{ Cursor positioning */ + +/** + * pango2_lines_get_cursor_pos: + * @lines: a `Pango2Lines` object + * @line: (nullable): `Pango2Line` wrt to which @idx is interpreted + * or `NULL` for the first matching line + * @idx: the byte index of the cursor + * @strong_pos: (out) (optional): location to store the strong cursor position + * @weak_pos: (out) (optional): location to store the weak cursor position + * + * Given an index within lines, determines the positions that of the + * strong and weak cursors if the insertion point is at that index. + * + * Note that @idx is allowed to be @line->start_index + @line->length + * for the position off the end of the last line. + * + * The position of each cursor is stored as a zero-width rectangle + * with the height of the run extents. + * + * <picture> + * <source srcset="cursor-positions-dark.png" media="(prefers-color-scheme: dark)"> + * <img alt="Cursor positions" src="cursor-positions-light.png"> + * </picture> + * + * The strong cursor location is the location where characters of the + * directionality equal to the base direction of the layout are inserted. + * The weak cursor location is the location where characters of the + * directionality opposite to the base direction of the layout are inserted. + * + * The following example shows text with both a strong and a weak cursor. + * + * <picture> + * <source srcset="split-cursor-dark.png" media="(prefers-color-scheme: dark)"> + * <img alt="Strong and weak cursors" src="split-cursor-light.png"> + * </picture> + * + * The strong cursor has a little arrow pointing to the right, the weak + * cursor to the left. Typing a 'c' in this situation will insert the + * character after the 'b', and typing another Hebrew character, like 'ג', + * will insert it at the end. + */ +void +pango2_lines_get_cursor_pos (Pango2Lines *lines, + Pango2Line *line, + int idx, + Pango2Rectangle *strong_pos, + Pango2Rectangle *weak_pos) +{ + int x_offset, y_offset; + Pango2Line *l; + + g_return_if_fail (PANGO2_IS_LINES (lines)); + + l = line; + pango2_lines_index_to_line (lines, idx, &l, NULL, &x_offset, &y_offset); + + g_return_if_fail (l != NULL); + g_return_if_fail (line == NULL || l == line); + + line = l; + + pango2_line_get_cursor_pos (line, idx, strong_pos, weak_pos); + + if (strong_pos) + { + strong_pos->x += x_offset; + strong_pos->y += y_offset; + } + if (weak_pos) + { + weak_pos->x += x_offset; + weak_pos->y += y_offset; + } +} + +/** + * pango2_lines_get_caret_pos: + * @lines: a `Pango2Lines` object + * @line: (nullable): `Pango2Line` wrt to which @idx is interpreted + * or `NULL` for the first matching line + * @idx: the byte index of the cursor + * @strong_pos: (out) (optional): location to store the strong cursor position + * @weak_pos: (out) (optional): location to store the weak cursor position + * + * Given an index within a layout, determines the positions of the + * strong and weak cursors if the insertion point is at that index. + * + * Note that @idx is allowed to be @line->start_index + @line->length + * for the position off the end of the last line. + * + * This is a variant of [method@Pango2.Lines.get_cursor_pos] that applies + * font metric information about caret slope and offset to the positions + * it returns. + * + * <picture> + * <source srcset="caret-metrics-dark.png" media="(prefers-color-scheme: dark)"> + * <img alt="Caret metrics" src="caret-metrics-light.png"> + * </picture> + */ +void +pango2_lines_get_caret_pos (Pango2Lines *lines, + Pango2Line *line, + int idx, + Pango2Rectangle *strong_pos, + Pango2Rectangle *weak_pos) +{ + int x_offset, y_offset; + + g_return_if_fail (PANGO2_IS_LINES (lines)); + + pango2_lines_index_to_line (lines, idx, &line, NULL, &x_offset, &y_offset); + + g_return_if_fail (line != NULL); + + pango2_line_get_caret_pos (line, idx, strong_pos, weak_pos); + + if (strong_pos) + { + strong_pos->x += x_offset; + strong_pos->y += y_offset; + } + if (weak_pos) + { + weak_pos->x += x_offset; + weak_pos->y += y_offset; + } +} + +/** + * pango2_lines_move_cursor: + * @lines: a `Pango2Lines` object + * @strong: whether the moving cursor is the strong cursor or the + * weak cursor. The strong cursor is the cursor corresponding + * to text insertion in the base direction for the layout. + * @line: (nullable): `Pango2Line` wrt to which @idx is interpreted + * or `NULL` for the first matching line + * @idx: the byte index of the current cursor position + * @trailing: if 0, the cursor was at the leading edge of the + * grapheme indicated by @old_index, if > 0, the cursor + * was at the trailing edge. + * @direction: direction to move cursor. A negative + * value indicates motion to the left + * @new_line: (nullable): `Pango2Line` wrt to which @new_idx is interpreted + * @new_idx: (out): location to store the new cursor byte index + * A value of -1 indicates that the cursor has been moved off the + * beginning of the layout. A value of %G_MAXINT indicates that + * the cursor has been moved off the end of the layout. + * @new_trailing: (out): number of characters to move forward from + * the location returned for @new_idx to get the position where + * the cursor should be displayed. This allows distinguishing the + * position at the beginning of one line from the position at the + * end of the preceding line. @new_idx is always on the line where + * the cursor should be displayed. + * + * Computes a new cursor position from an old position and a direction. + * + * If @direction is positive, then the new position will cause the strong + * or weak cursor to be displayed one position to right of where it was + * with the old cursor position. If @direction is negative, it will be + * moved to the left. + * + * In the presence of bidirectional text, the correspondence between + * logical and visual order will depend on the direction of the current + * run, and there may be jumps when the cursor is moved off of the end + * of a run. + * + * Motion here is in cursor positions, not in characters, so a single + * call to this function may move the cursor over multiple characters + * when multiple characters combine to form a single grapheme. + */ +void +pango2_lines_move_cursor (Pango2Lines *lines, + gboolean strong, + Pango2Line *line, + int idx, + int trailing, + int direction, + Pango2Line **new_line, + int *new_idx, + int *new_trailing) +{ + int line_no; + GArray *cursors; + int n_vis; + int vis_pos; + int start_offset; + gboolean off_start = FALSE; + gboolean off_end = FALSE; + Pango2Rectangle pos; + int j; + + g_return_if_fail (PANGO2_IS_LINES (lines)); + g_return_if_fail (idx >= 0); + g_return_if_fail (trailing >= 0); + g_return_if_fail (new_idx != NULL); + g_return_if_fail (new_trailing != NULL); + + direction = (direction >= 0 ? 1 : -1); + + pango2_lines_index_to_line (lines, idx, &line, &line_no, NULL, NULL); + + g_return_if_fail (line != NULL); + + while (trailing--) + idx = g_utf8_next_char (line->data->text + idx) - line->data->text; + + /* Clamp old_index to fit on the line */ + if (idx > (line->start_index + line->length)) + idx = line->start_index + line->length; + + cursors = g_array_new (FALSE, FALSE, sizeof (CursorPos)); + pango2_line_get_cursors (lines, line, strong, cursors); + + pango2_lines_get_cursor_pos (lines, line, idx, strong ? &pos : NULL, strong ? NULL : &pos); + + vis_pos = -1; + for (j = 0; j < cursors->len; j++) + { + CursorPos *cursor = &g_array_index (cursors, CursorPos, j); + if (cursor->x == pos.x) + { + vis_pos = j; + + /* If moving left, we pick the leftmost match, otherwise + * the rightmost one. Without this, we can get stuck + */ + if (direction < 0) + break; + } + } + if (vis_pos == -1 && + idx == line->start_index + line->length) + { + if (line->direction == PANGO2_DIRECTION_LTR) + vis_pos = cursors->len; + else + vis_pos = 0; + } + + /* Handling movement between lines */ + if (line->direction == PANGO2_DIRECTION_LTR) + { + if (idx == line->start_index && direction < 0) + off_start = TRUE; + if (idx == line->start_index + line->length && direction > 0) + off_end = TRUE; + } + else + { + if (idx == line->start_index + line->length && direction < 0) + off_start = TRUE; + if (idx == line->start_index && direction > 0) + off_end = TRUE; + } + if (off_start || off_end) + { + /* If we move over a paragraph boundary, count that as + * an extra position in the motion + */ + gboolean paragraph_boundary; + + if (off_start) + { + if (line_no == 0) + { + if (new_line) + *new_line = NULL; + *new_idx = -1; + *new_trailing = 0; + g_array_unref (cursors); + return; + } + + line_no--; + line = g_ptr_array_index (lines->lines, line_no); + paragraph_boundary = (line->start_index + line->length != idx); + } + else + { + if (line_no == lines->lines->len - 1) + { + if (new_line) + *new_line = NULL; + *new_idx = G_MAXINT; + *new_trailing = 0; + g_array_unref (cursors); + return; + } + + line_no++; + line = g_ptr_array_index (lines->lines, line_no); + paragraph_boundary = (line->start_index != idx); + } + + g_array_set_size (cursors, 0); + pango2_line_get_cursors (lines, line, strong, cursors); + + n_vis = cursors->len; + + if (off_start && direction < 0) + { + vis_pos = n_vis; + if (paragraph_boundary) + vis_pos++; + } + else if (off_end && direction > 0) + { + vis_pos = 0; + if (paragraph_boundary) + vis_pos--; + } + } + + if (direction < 0) + vis_pos--; + else + vis_pos++; + + if (0 <= vis_pos && vis_pos < cursors->len) + *new_idx = g_array_index (cursors, CursorPos, vis_pos).pos; + else if (vis_pos >= cursors->len - 1) + *new_idx = line->start_index + line->length; + + *new_trailing = 0; + + if (*new_idx == line->start_index + line->length && line->length > 0) + { + int log_pos; + + start_offset = g_utf8_pointer_to_offset (line->data->text, line->data->text + line->start_index); + log_pos = start_offset + g_utf8_strlen (line->data->text + line->start_index, line->length); + do + { + log_pos--; + *new_idx = g_utf8_prev_char (line->data->text + *new_idx) - line->data->text; + (*new_trailing)++; + } + while (log_pos > start_offset && !line->data->log_attrs[log_pos].is_cursor_position); + } + + if (new_line) + *new_line = line; + + g_array_unref (cursors); +} + +/* }}} */ +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pango-lines.h b/pango2/pango-lines.h new file mode 100644 index 00000000..f2a89f43 --- /dev/null +++ b/pango2/pango-lines.h @@ -0,0 +1,154 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <glib-object.h> + +#include <pango2/pango-types.h> +#include <pango2/pango-line.h> + +G_BEGIN_DECLS + +#define PANGO2_TYPE_LINES pango2_lines_get_type () + +PANGO2_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (Pango2Lines, pango2_lines, PANGO2, LINES, GObject); + +PANGO2_AVAILABLE_IN_ALL +Pango2Lines * pango2_lines_new (void); + +PANGO2_AVAILABLE_IN_ALL +guint pango2_lines_get_serial (Pango2Lines *lines); + +PANGO2_AVAILABLE_IN_ALL +void pango2_lines_add_line (Pango2Lines *lines, + Pango2Line *line, + int line_x, + int line_y); + +PANGO2_AVAILABLE_IN_ALL +int pango2_lines_get_line_count (Pango2Lines *lines); + +PANGO2_AVAILABLE_IN_ALL +Pango2Line ** pango2_lines_get_lines (Pango2Lines *lines); + +PANGO2_AVAILABLE_IN_ALL +void pango2_lines_get_line_position + (Pango2Lines *lines, + int num, + int *line_x, + int *line_y); + +PANGO2_AVAILABLE_IN_ALL +Pango2LineIter * pango2_lines_get_iter (Pango2Lines *lines); + +PANGO2_AVAILABLE_IN_ALL +void pango2_lines_get_extents (Pango2Lines *lines, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect); + +PANGO2_AVAILABLE_IN_ALL +void pango2_lines_get_size (Pango2Lines *lines, + int *width, + int *height); + +PANGO2_AVAILABLE_IN_ALL +int pango2_lines_get_baseline (Pango2Lines *lines); + +PANGO2_AVAILABLE_IN_ALL +void pango2_lines_get_x_ranges (Pango2Lines *lines, + Pango2Line *line, + Pango2Line *start_line, + int start_index, + Pango2Line *end_line, + int end_index, + int **ranges, + int *n_ranges); + +PANGO2_AVAILABLE_IN_ALL +int pango2_lines_get_unknown_glyphs_count + (Pango2Lines *lines); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_lines_is_wrapped (Pango2Lines *lines); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_lines_is_ellipsized (Pango2Lines *lines); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_lines_is_hyphenated (Pango2Lines *lines); + +PANGO2_AVAILABLE_IN_ALL +void pango2_lines_index_to_line (Pango2Lines *lines, + int idx, + Pango2Line **line, + int *line_no, + int *x_offset, + int *y_offset); + +PANGO2_AVAILABLE_IN_ALL +Pango2Line * pango2_lines_pos_to_line (Pango2Lines *lines, + int x, + int y, + int *line_x, + int *line_y); + +PANGO2_AVAILABLE_IN_ALL +void pango2_lines_index_to_pos (Pango2Lines *lines, + Pango2Line *line, + int idx, + Pango2Rectangle *pos); + +PANGO2_AVAILABLE_IN_ALL +Pango2Line * pango2_lines_pos_to_index (Pango2Lines *lines, + int x, + int y, + int *idx, + int *trailing); + +PANGO2_AVAILABLE_IN_ALL +void pango2_lines_get_cursor_pos (Pango2Lines *lines, + Pango2Line *line, + int idx, + Pango2Rectangle *strong_pos, + Pango2Rectangle *weak_pos); + +PANGO2_AVAILABLE_IN_ALL +void pango2_lines_get_caret_pos (Pango2Lines *lines, + Pango2Line *line, + int idx, + Pango2Rectangle *strong_pos, + Pango2Rectangle *weak_pos); + +PANGO2_AVAILABLE_IN_ALL +void pango2_lines_move_cursor (Pango2Lines *lines, + gboolean strong, + Pango2Line *line, + int idx, + int trailing, + int direction, + Pango2Line **new_line, + int *new_idx, + int *new_trailing); + +PANGO2_AVAILABLE_IN_ALL +GBytes * pango2_lines_serialize (Pango2Lines *lines); + +G_END_DECLS diff --git a/pango2/pango-markup.c b/pango2/pango-markup.c new file mode 100644 index 00000000..c03196e8 --- /dev/null +++ b/pango2/pango-markup.c @@ -0,0 +1,2016 @@ +/* Pango2 + * pango-markup.c: Parse markup into attributed text + * + * Copyright (C) 2000 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include <string.h> +#include <stdlib.h> +#include <errno.h> + +#include "pango-markup.h" + +#include "pango-attributes.h" +#include "pango-attr-private.h" +#include "pango-font.h" +#include "pango-enum-types.h" +#include "pango-impl-utils.h" +#include "pango-font-description-private.h" + +/* CSS size levels */ +typedef enum +{ + XXSmall = -3, + XSmall = -2, + Small = -1, + Medium = 0, + Large = 1, + XLarge = 2, + XXLarge = 3 +} SizeLevel; + +typedef struct _MarkupData MarkupData; + +struct _MarkupData +{ + Pango2AttrList *attr_list; + GString *text; + GSList *tag_stack; + gsize index; + GSList *to_apply; + gunichar accel_marker; + gunichar accel_char; +}; + +typedef struct _OpenTag OpenTag; + +struct _OpenTag +{ + GSList *attrs; + gsize start_index; + /* Current total scale level; reset whenever + * an absolute size is set. + * Each "larger" ups it 1, each "smaller" decrements it 1 + */ + int scale_level; + /* Our impact on scale_level, so we know whether we + * need to create an attribute ourselves on close + */ + int scale_level_delta; + /* Base scale factor currently in effect + * or size that this tag + * forces, or parent's scale factor or size. + */ + double base_scale_factor; + int base_font_size; + guint has_base_font_size : 1; +}; + +typedef gboolean (*TagParseFunc) (MarkupData *md, + OpenTag *tag, + const char **names, + const char **values, + GMarkupParseContext *context, + GError **error); + +static gboolean b_parse_func (MarkupData *md, + OpenTag *tag, + const char **names, + const char **values, + GMarkupParseContext *context, + GError **error); +static gboolean big_parse_func (MarkupData *md, + OpenTag *tag, + const char **names, + const char **values, + GMarkupParseContext *context, + GError **error); +static gboolean span_parse_func (MarkupData *md, + OpenTag *tag, + const char **names, + const char **values, + GMarkupParseContext *context, + GError **error); +static gboolean i_parse_func (MarkupData *md, + OpenTag *tag, + const char **names, + const char **values, + GMarkupParseContext *context, + GError **error); +static gboolean markup_parse_func (MarkupData *md, + OpenTag *tag, + const char **names, + const char **values, + GMarkupParseContext *context, + GError **error); +static gboolean s_parse_func (MarkupData *md, + OpenTag *tag, + const char **names, + const char **values, + GMarkupParseContext *context, + GError **error); +static gboolean sub_parse_func (MarkupData *md, + OpenTag *tag, + const char **names, + const char **values, + GMarkupParseContext *context, + GError **error); +static gboolean sup_parse_func (MarkupData *md, + OpenTag *tag, + const char **names, + const char **values, + GMarkupParseContext *context, + GError **error); +static gboolean small_parse_func (MarkupData *md, + OpenTag *tag, + const char **names, + const char **values, + GMarkupParseContext *context, + GError **error); +static gboolean tt_parse_func (MarkupData *md, + OpenTag *tag, + const char **names, + const char **values, + GMarkupParseContext *context, + GError **error); +static gboolean u_parse_func (MarkupData *md, + OpenTag *tag, + const char **names, + const char **values, + GMarkupParseContext *context, + GError **error); + +static gboolean +_pango2_scan_int (const char **pos, int *out) +{ + char *end; + long temp; + + errno = 0; + temp = strtol (*pos, &end, 10); + if (errno == ERANGE) + { + errno = 0; + return FALSE; + } + + *out = (int)temp; + if ((long)(*out) != temp) + { + return FALSE; + } + + *pos = end; + + return TRUE; +} + +static gboolean +parse_int (const char *word, + int *out) +{ + char *end; + long val; + int i; + + if (word == NULL) + return FALSE; + + val = strtol (word, &end, 10); + i = val; + + if (end != word && *end == '\0' && val >= 0 && val == i) + { + if (out) + *out = i; + + return TRUE; + } + + return FALSE; +} + +static gboolean +_pango2_parse_enum (GType type, + const char *str, + int *value, + gboolean warn, + char **possible_values) +{ + GEnumClass *class = NULL; + gboolean ret = TRUE; + GEnumValue *v = NULL; + + class = g_type_class_ref (type); + + if (G_LIKELY (str)) + v = g_enum_get_value_by_nick (class, str); + + if (v) + { + if (G_LIKELY (value)) + *value = v->value; + } + else if (!parse_int (str, value)) + { + ret = FALSE; + if (G_LIKELY (warn || possible_values)) + { + int i; + GString *s = g_string_new (NULL); + + for (i = 0, v = g_enum_get_value (class, i); v; + i++ , v = g_enum_get_value (class, i)) + { + if (i) + g_string_append_c (s, '/'); + g_string_append (s, v->value_nick); + } + + if (warn) + g_warning ("%s must be one of %s", + G_ENUM_CLASS_TYPE_NAME(class), + s->str); + + if (possible_values) + *possible_values = s->str; + + g_string_free (s, possible_values ? FALSE : TRUE); + } + } + + g_type_class_unref (class); + + return ret; +} + +static gboolean +pango2_parse_flags (GType type, + const char *str, + int *value, + char **possible_values) +{ + GFlagsClass *class = NULL; + gboolean ret = TRUE; + GFlagsValue *v = NULL; + + class = g_type_class_ref (type); + + v = g_flags_get_value_by_nick (class, str); + + if (v) + { + *value = v->value; + } + else if (!parse_int (str, value)) + { + char **strv = g_strsplit (str, "|", 0); + int i; + + *value = 0; + + for (i = 0; strv[i]; i++) + { + strv[i] = g_strstrip (strv[i]); + v = g_flags_get_value_by_nick (class, strv[i]); + if (!v) + { + ret = FALSE; + break; + } + *value |= v->value; + } + g_strfreev (strv); + + if (!ret && possible_values) + { + int i; + GString *s = g_string_new (NULL); + + for (i = 0; i < class->n_values; i++) + { + v = &class->values[i]; + if (i) + g_string_append_c (s, '/'); + g_string_append (s, v->value_nick); + } + + *possible_values = s->str; + + g_string_free (s, FALSE); + } + } + + g_type_class_unref (class); + + return ret; +} + +static double +scale_factor (int scale_level, double base) +{ + double factor = base; + int i; + + /* 1.2 is the CSS scale factor between sizes */ + + if (scale_level > 0) + { + i = 0; + while (i < scale_level) + { + factor *= 1.2; + + ++i; + } + } + else if (scale_level < 0) + { + i = scale_level; + while (i < 0) + { + factor /= 1.2; + + ++i; + } + } + + return factor; +} + +static void +open_tag_free (OpenTag *ot) +{ + g_slist_foreach (ot->attrs, (GFunc) pango2_attribute_destroy, NULL); + g_slist_free (ot->attrs); + g_slice_free (OpenTag, ot); +} + +static void +open_tag_set_absolute_font_size (OpenTag *ot, + int font_size) +{ + ot->base_font_size = font_size; + ot->has_base_font_size = TRUE; + ot->scale_level = 0; + ot->scale_level_delta = 0; +} + +static void +open_tag_set_absolute_font_scale (OpenTag *ot, + double scale) +{ + ot->base_scale_factor = scale; + ot->has_base_font_size = FALSE; + ot->scale_level = 0; + ot->scale_level_delta = 0; +} + +static OpenTag* +markup_data_open_tag (MarkupData *md) +{ + OpenTag *ot; + OpenTag *parent = NULL; + + if (md->attr_list == NULL) + return NULL; + + if (md->tag_stack) + parent = md->tag_stack->data; + + ot = g_slice_new (OpenTag); + ot->attrs = NULL; + ot->start_index = md->index; + ot->scale_level_delta = 0; + + if (parent == NULL) + { + ot->base_scale_factor = 1.0; + ot->base_font_size = 0; + ot->has_base_font_size = FALSE; + ot->scale_level = 0; + } + else + { + ot->base_scale_factor = parent->base_scale_factor; + ot->base_font_size = parent->base_font_size; + ot->has_base_font_size = parent->has_base_font_size; + ot->scale_level = parent->scale_level; + } + + md->tag_stack = g_slist_prepend (md->tag_stack, ot); + + return ot; +} + +static void +markup_data_close_tag (MarkupData *md) +{ + OpenTag *ot; + GSList *tmp_list; + + if (md->attr_list == NULL) + return; + + /* pop the stack */ + ot = md->tag_stack->data; + md->tag_stack = g_slist_delete_link (md->tag_stack, + md->tag_stack); + + /* Adjust end indexes, and push each attr onto the front of the + * to_apply list. This means that outermost tags are on the front of + * that list; if we apply the list in order, then the innermost + * tags will "win" which is correct. + */ + tmp_list = ot->attrs; + while (tmp_list != NULL) + { + Pango2Attribute *a = tmp_list->data; + + a->start_index = ot->start_index; + a->end_index = md->index; + + md->to_apply = g_slist_prepend (md->to_apply, a); + + tmp_list = g_slist_next (tmp_list); + } + + if (ot->scale_level_delta != 0) + { + /* We affected relative font size; create an appropriate + * attribute and reverse our effects on the current level + */ + Pango2Attribute *a; + + if (ot->has_base_font_size) + { + /* Create a font using the absolute point size as the base size + * to be scaled from. + * We need to use a local variable to ensure that the compiler won't + * implicitly cast it to integer while the result is kept in registers, + * leading to a wrong approximation in i386 (with 387 FPU) + */ + volatile double size; + + size = scale_factor (ot->scale_level, 1.0) * ot->base_font_size; + a = pango2_attr_size_new (size); + } + else + { + /* Create a font using the current scale factor + * as the base size to be scaled from + */ + a = pango2_attr_scale_new (scale_factor (ot->scale_level, + ot->base_scale_factor)); + } + + a->start_index = ot->start_index; + a->end_index = md->index; + + md->to_apply = g_slist_prepend (md->to_apply, a); + } + + g_slist_free (ot->attrs); + g_slice_free (OpenTag, ot); +} + +static void +start_element_handler (GMarkupParseContext *context, + const char *element_name, + const char **attribute_names, + const char **attribute_values, + gpointer user_data, + GError **error) +{ + TagParseFunc parse_func = NULL; + OpenTag *ot; + + switch (*element_name) + { + case 'b': + if (strcmp ("b", element_name) == 0) + parse_func = b_parse_func; + else if (strcmp ("big", element_name) == 0) + parse_func = big_parse_func; + break; + + case 'i': + if (strcmp ("i", element_name) == 0) + parse_func = i_parse_func; + break; + + case 'm': + if (strcmp ("markup", element_name) == 0) + parse_func = markup_parse_func; + break; + + case 's': + if (strcmp ("span", element_name) == 0) + parse_func = span_parse_func; + else if (strcmp ("s", element_name) == 0) + parse_func = s_parse_func; + else if (strcmp ("sub", element_name) == 0) + parse_func = sub_parse_func; + else if (strcmp ("sup", element_name) == 0) + parse_func = sup_parse_func; + else if (strcmp ("small", element_name) == 0) + parse_func = small_parse_func; + break; + + case 't': + if (strcmp ("tt", element_name) == 0) + parse_func = tt_parse_func; + break; + + case 'u': + if (strcmp ("u", element_name) == 0) + parse_func = u_parse_func; + break; + + default: + break; + } + + if (parse_func == NULL) + { + int line_number, char_number; + + g_markup_parse_context_get_position (context, + &line_number, &char_number); + + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_UNKNOWN_ELEMENT, + "Unknown tag '%s' on line %d char %d", + element_name, + line_number, char_number); + + return; + } + + ot = markup_data_open_tag (user_data); + + /* note ot may be NULL if the user didn't want the attribute list */ + + if (!(*parse_func) (user_data, ot, + attribute_names, attribute_values, + context, error)) + { + /* there's nothing to do; we return an error, and end up + * freeing ot off the tag stack later. + */ + } +} + +static void +end_element_handler (GMarkupParseContext *context G_GNUC_UNUSED, + const char *element_name G_GNUC_UNUSED, + gpointer user_data, + GError **error G_GNUC_UNUSED) +{ + markup_data_close_tag (user_data); +} + +static void +text_handler (GMarkupParseContext *context G_GNUC_UNUSED, + const char *text, + gsize text_len, + gpointer user_data, + GError **error G_GNUC_UNUSED) +{ + MarkupData *md = user_data; + + if (md->accel_marker == 0) + { + /* Just append all the text */ + + md->index += text_len; + + g_string_append_len (md->text, text, text_len); + } + else + { + /* Parse the accelerator */ + const char *p; + const char *end; + const char *range_start; + const char *range_end; + gssize uline_index = -1; + gsize uline_len = 0; /* Quiet GCC */ + + range_end = NULL; + range_start = text; + p = text; + end = text + text_len; + + while (p != end) + { + gunichar c; + + c = g_utf8_get_char (p); + + if (range_end) + { + if (c == md->accel_marker) + { + /* escaped accel marker; move range_end + * past the accel marker that came before, + * append the whole thing + */ + range_end = g_utf8_next_char (range_end); + g_string_append_len (md->text, + range_start, + range_end - range_start); + md->index += range_end - range_start; + + /* set next range_start, skipping accel marker */ + range_start = g_utf8_next_char (p); + } + else + { + /* Don't append the accel marker (leave range_end + * alone); set the accel char to c; record location for + * underline attribute + */ + if (md->accel_char == 0) + md->accel_char = c; + + g_string_append_len (md->text, + range_start, + range_end - range_start); + md->index += range_end - range_start; + + /* The underline should go underneath the char + * we're setting as the next range_start + */ + if (md->attr_list != NULL) + { + /* Add the underline indicating the accelerator */ + Pango2Attribute *attr; + + attr = pango2_attr_underline_new (PANGO2_LINE_STYLE_SOLID); + + uline_index = md->index; + uline_len = g_utf8_next_char (p) - p; + + attr->start_index = uline_index; + attr->end_index = uline_index + uline_len; + + pango2_attr_list_change (md->attr_list, attr); + + attr = pango2_attr_underline_position_new (PANGO2_UNDERLINE_POSITION_UNDER); + + attr->start_index = uline_index; + attr->end_index = uline_index + uline_len; + + pango2_attr_list_change (md->attr_list, attr); + } + + /* set next range_start to include this char */ + range_start = p; + } + + /* reset range_end */ + range_end = NULL; + } + else if (c == md->accel_marker) + { + range_end = p; + } + + p = g_utf8_next_char (p); + } + + g_string_append_len (md->text, + range_start, + end - range_start); + md->index += end - range_start; + } +} + +static gboolean +xml_isspace (char c) +{ + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; +} + +static const GMarkupParser pango2_markup_parser = { + start_element_handler, + end_element_handler, + text_handler, + NULL, + NULL +}; + +static void +destroy_markup_data (MarkupData *md) +{ + g_slist_free_full (md->tag_stack, (GDestroyNotify) open_tag_free); + g_slist_free_full (md->to_apply, (GDestroyNotify) pango2_attribute_destroy); + if (md->text) + g_string_free (md->text, TRUE); + + if (md->attr_list) + pango2_attr_list_unref (md->attr_list); + + g_slice_free (MarkupData, md); +} + +static GMarkupParseContext * +pango2_markup_parser_new_internal (char accel_marker, + GError **error, + gboolean want_attr_list) +{ + MarkupData *md; + GMarkupParseContext *context; + + md = g_slice_new (MarkupData); + + /* Don't bother creating these if they weren't requested; + * might be useful e.g. if you just want to validate + * some markup. + */ + if (want_attr_list) + md->attr_list = pango2_attr_list_new (); + else + md->attr_list = NULL; + + md->text = g_string_new (NULL); + + md->accel_marker = accel_marker; + md->accel_char = 0; + + md->index = 0; + md->tag_stack = NULL; + md->to_apply = NULL; + + context = g_markup_parse_context_new (&pango2_markup_parser, + 0, md, + (GDestroyNotify)destroy_markup_data); + + if (!g_markup_parse_context_parse (context, "<markup>", -1, error)) + g_clear_pointer (&context, g_markup_parse_context_free); + + return context; +} + +/** + * pango2_parse_markup: + * @markup_text: markup to parse (see the [Pango2 Markup](pango2_markup.html) docs) + * @length: length of @markup_text, or -1 if nul-terminated + * @accel_marker: character that precedes an accelerator, or 0 for none + * @attr_list: (out) (optional): address of return location for a `Pango2AttrList` + * @text: (out) (optional): address of return location for text with tags stripped + * @accel_char: (out) (optional): address of return location for accelerator char + * @error: address of return location for errors + * + * Parses marked-up text to create a plain-text string and an attribute list. + * + * See the [Pango2 Markup](pango2_markup.html) docs for details about the + * supported markup. + * + * If @accel_marker is nonzero, the given character will mark the + * character following it as an accelerator. For example, @accel_marker + * might be an ampersand or underscore. All characters marked + * as an accelerator will receive a %PANGO2_UNDERLINE_LOW attribute, + * and the first character so marked will be returned in @accel_char. + * Two @accel_marker characters following each other produce a single + * literal @accel_marker character. + * + * To parse a stream of pango markup incrementally, use [func@markup_parser_new]. + * + * If any error happens, none of the output arguments are touched except + * for @error. + * + * Return value: %FALSE if @error is set, otherwise %TRUE + **/ +gboolean +pango2_parse_markup (const char *markup_text, + int length, + gunichar accel_marker, + Pango2AttrList **attr_list, + char **text, + gunichar *accel_char, + GError **error) +{ + GMarkupParseContext *context = NULL; + gboolean ret = FALSE; + const char *p; + const char *end; + + g_return_val_if_fail (markup_text != NULL, FALSE); + + if (length < 0) + length = strlen (markup_text); + + p = markup_text; + end = markup_text + length; + while (p != end && xml_isspace (*p)) + ++p; + + context = pango2_markup_parser_new_internal (accel_marker, + error, + (attr_list != NULL)); + + if (!g_markup_parse_context_parse (context, + markup_text, + length, + error)) + goto out; + + if (!pango2_markup_parser_finish (context, + attr_list, + text, + accel_char, + error)) + goto out; + + ret = TRUE; + + out: + if (context != NULL) + g_markup_parse_context_free (context); + return ret; +} + +/** + * pango2_markup_parser_new: + * @accel_marker: character that precedes an accelerator, or 0 for none + * + * Incrementally parses marked-up text to create a plain-text string + * and an attribute list. + * + * See the [Pango2 Markup](pango2_markup.html) docs for details about the + * supported markup. + * + * If @accel_marker is nonzero, the given character will mark the + * character following it as an accelerator. For example, @accel_marker + * might be an ampersand or underscore. All characters marked + * as an accelerator will receive a %PANGO2_UNDERLINE_LOW attribute, + * and the first character so marked will be returned in @accel_char, + * when calling [func@markup_parser_finish]. Two @accel_marker characters + * following each other produce a single literal @accel_marker character. + * + * To feed markup to the parser, use [method@GLib.MarkupParseContext.parse] + * on the returned [struct@GLib.MarkupParseContext]. When done with feeding markup + * to the parser, use [func@markup_parser_finish] to get the data out + * of it, and then use [method@GLib.MarkupParseContext.free] to free it. + * + * This function is designed for applications that read Pango2 markup + * from streams. To simply parse a string containing Pango2 markup, + * the [func@Pango2.parse_markup] API is recommended instead. + * + * Return value: (transfer none): a `GMarkupParseContext` that should be + * destroyed with [method@GLib.MarkupParseContext.free]. + **/ +GMarkupParseContext * +pango2_markup_parser_new (gunichar accel_marker) +{ + return pango2_markup_parser_new_internal (accel_marker, NULL, TRUE); +} + +/** + * pango2_markup_parser_finish: + * @context: A valid parse context that was returned from [func@markup_parser_new] + * @attr_list: (out) (optional): address of return location for a `Pango2AttrList` + * @text: (out) (optional): address of return location for text with tags stripped + * @accel_char: (out) (optional): address of return location for accelerator char + * @error: address of return location for errors + * + * Finishes parsing markup. + * + * After feeding a Pango2 markup parser some data with [method@GLib.MarkupParseContext.parse], + * use this function to get the list of attributes and text out of the + * markup. This function will not free @context, use [method@GLib.MarkupParseContext.free] + * to do so. + * + * Return value: %FALSE if @error is set, otherwise %TRUE + */ +gboolean +pango2_markup_parser_finish (GMarkupParseContext *context, + Pango2AttrList **attr_list, + char **text, + gunichar *accel_char, + GError **error) +{ + gboolean ret = FALSE; + MarkupData *md = g_markup_parse_context_get_user_data (context); + GSList *tmp_list; + + if (!g_markup_parse_context_parse (context, + "</markup>", + -1, + error)) + goto out; + + if (!g_markup_parse_context_end_parse (context, error)) + goto out; + + if (md->attr_list) + { + /* The apply list has the most-recently-closed tags first; + * we want to apply the least-recently-closed tag last. + */ + tmp_list = md->to_apply; + while (tmp_list != NULL) + { + Pango2Attribute *attr = tmp_list->data; + + /* Innermost tags before outermost */ + pango2_attr_list_insert (md->attr_list, attr); + + tmp_list = g_slist_next (tmp_list); + } + g_slist_free (md->to_apply); + md->to_apply = NULL; + } + + if (attr_list) + { + *attr_list = md->attr_list; + md->attr_list = NULL; + } + + if (text) + { + *text = g_string_free (md->text, FALSE); + md->text = NULL; + } + + if (accel_char) + *accel_char = md->accel_char; + + g_assert (md->tag_stack == NULL); + ret = TRUE; + + out: + return ret; +} + +static void +set_bad_attribute (GError **error, + GMarkupParseContext *context, + const char *element_name, + const char *attribute_name) +{ + int line_number, char_number; + + g_markup_parse_context_get_position (context, + &line_number, &char_number); + + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE, + "Tag '%s' does not support attribute '%s' on line %d char %d", + element_name, + attribute_name, + line_number, char_number); +} + +static void +add_attribute (OpenTag *ot, + Pango2Attribute *attr) +{ + if (ot == NULL) + pango2_attribute_destroy (attr); + else + ot->attrs = g_slist_prepend (ot->attrs, attr); +} + +#define CHECK_NO_ATTRS(elem) G_STMT_START { \ + if (*names != NULL) { \ + set_bad_attribute (error, context, (elem), *names); \ + return FALSE; \ + } }G_STMT_END + +static gboolean +b_parse_func (MarkupData *md G_GNUC_UNUSED, + OpenTag *tag, + const char **names, + const char **values G_GNUC_UNUSED, + GMarkupParseContext *context, + GError **error) +{ + CHECK_NO_ATTRS("b"); + add_attribute (tag, pango2_attr_weight_new (PANGO2_WEIGHT_BOLD)); + return TRUE; +} + +static gboolean +big_parse_func (MarkupData *md G_GNUC_UNUSED, + OpenTag *tag, + const char **names, + const char **values G_GNUC_UNUSED, + GMarkupParseContext *context, + GError **error) +{ + CHECK_NO_ATTRS("big"); + + /* Grow text one level */ + if (tag) + { + tag->scale_level_delta += 1; + tag->scale_level += 1; + } + + return TRUE; +} + +static gboolean +parse_percentage (const char *input, + double *val) +{ + double v; + char *end; + + v = g_ascii_strtod (input, &end); + if (errno == 0 && strcmp (end, "%") == 0 && v > 0) + { + *val = v; + return TRUE; + } + + return FALSE; +} + +static gboolean +parse_absolute_size (OpenTag *tag, + const char *size) +{ + SizeLevel level = Medium; + double val; + double factor; + + if (strcmp (size, "xx-small") == 0) + level = XXSmall; + else if (strcmp (size, "x-small") == 0) + level = XSmall; + else if (strcmp (size, "small") == 0) + level = Small; + else if (strcmp (size, "medium") == 0) + level = Medium; + else if (strcmp (size, "large") == 0) + level = Large; + else if (strcmp (size, "x-large") == 0) + level = XLarge; + else if (strcmp (size, "xx-large") == 0) + level = XXLarge; + else if (parse_percentage (size, &val)) + { + factor = val / 100.0; + goto done; + } + else + return FALSE; + + /* This is "absolute" in that it's relative to the base font, + * but not to sizes created by any other tags + */ + factor = scale_factor (level, 1.0); + +done: + add_attribute (tag, pango2_attr_scale_new (factor)); + if (tag) + open_tag_set_absolute_font_scale (tag, factor); + + return TRUE; +} + +/* a string compare func that ignores '-' vs '_' differences */ +static int +attr_strcmp (gconstpointer pa, + gconstpointer pb) +{ + const char *a = pa; + const char *b = pb; + + int ca; + int cb; + + while (*a && *b) + { + ca = *a++; + cb = *b++; + + if (ca == cb) + continue; + + ca = ca == '_' ? '-' : ca; + cb = cb == '_' ? '-' : cb; + + if (ca != cb) + return cb - ca; + } + + ca = *a; + cb = *b; + + return cb - ca; +} + +static gboolean +span_parse_int (const char *attr_name, + const char *attr_val, + int *val, + int line_number, + GError **error) +{ + const char *end = attr_val; + + if (!_pango2_scan_int (&end, val) || *end != '\0') + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "Value of '%s' attribute on <span> tag " + "on line %d could not be parsed; " + "should be an integer, not '%s'", + attr_name, line_number, attr_val); + return FALSE; + } + + return TRUE; +} + +static gboolean +span_parse_float (const char *attr_name, + const char *attr_val, + double *val, + int line_number, + GError **error) +{ + *val = g_ascii_strtod (attr_val, NULL); + if (errno != 0) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "Value of '%s' attribute on <span> tag " + "on line %d could not be parsed; " + "should be a number, not '%s'", + attr_name, line_number, attr_val); + return FALSE; + } + + return TRUE; +} + +static gboolean +span_parse_boolean (const char *attr_name, + const char *attr_val, + gboolean *val, + int line_number, + GError **error) +{ + if (strcmp (attr_val, "true") == 0 || + strcmp (attr_val, "yes") == 0 || + strcmp (attr_val, "t") == 0 || + strcmp (attr_val, "y") == 0) + *val = TRUE; + else if (strcmp (attr_val, "false") == 0 || + strcmp (attr_val, "no") == 0 || + strcmp (attr_val, "f") == 0 || + strcmp (attr_val, "n") == 0) + *val = FALSE; + else + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "Value of '%s' attribute on <span> tag " + "line %d should have one of " + "'true/yes/t/y' or 'false/no/f/n': '%s' is not valid", + attr_name, line_number, attr_val); + return FALSE; + } + + return TRUE; +} + +static gboolean +span_parse_color (const char *attr_name, + const char *attr_val, + Pango2Color *color, + int line_number, + GError **error) +{ + if (!pango2_color_parse (color, attr_val)) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "Value of '%s' attribute on <span> tag " + "on line %d could not be parsed; " + "should be a color specification, not '%s'", + attr_name, line_number, attr_val); + return FALSE; + } + + return TRUE; +} + +static gboolean +span_parse_enum (const char *attr_name, + const char *attr_val, + GType type, + int *val, + int line_number, + GError **error) +{ + char *possible_values = NULL; + + if (!_pango2_parse_enum (type, attr_val, val, FALSE, &possible_values)) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "'%s' is not a valid value for the '%s' " + "attribute on <span> tag, line %d; valid " + "values are %s", + attr_val, attr_name, line_number, possible_values); + g_free (possible_values); + return FALSE; + } + + return TRUE; +} + +static gboolean +span_parse_flags (const char *attr_name, + const char *attr_val, + GType type, + int *val, + int line_number, + GError **error) +{ + char *possible_values = NULL; + + if (!pango2_parse_flags (type, attr_val, val, &possible_values)) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "'%s' is not a valid value for the '%s' " + "attribute on <span> tag, line %d; valid " + "values are %s or combinations with |", + attr_val, attr_name, line_number, possible_values); + g_free (possible_values); + return FALSE; + } + + return TRUE; +} + +static gboolean +parse_length (const char *attr_val, + int *result) +{ + const char *attr; + int n; + + attr = attr_val; + if (_pango2_scan_int (&attr, &n) && *attr == '\0') + { + *result = n; + return TRUE; + } + else + { + double val; + char *end; + + val = g_ascii_strtod (attr_val, &end); + if (errno == 0 && strcmp (end, "pt") == 0) + { + *result = val * PANGO2_SCALE; + return TRUE; + } + } + + return FALSE; +} + +static gboolean +span_parse_func (MarkupData *md G_GNUC_UNUSED, + OpenTag *tag, + const char **names, + const char **values, + GMarkupParseContext *context, + GError **error) +{ + int line_number, char_number; + int i; + + const char *family = NULL; + const char *size = NULL; + const char *style = NULL; + const char *weight = NULL; + const char *variant = NULL; + const char *stretch = NULL; + const char *desc = NULL; + const char *foreground = NULL; + const char *background = NULL; + const char *underline = NULL; + const char *underline_position = NULL; + const char *underline_color = NULL; + const char *overline = NULL; + const char *overline_color = NULL; + const char *strikethrough = NULL; + const char *strikethrough_color = NULL; + const char *rise = NULL; + const char *baseline_shift = NULL; + const char *letter_spacing = NULL; + const char *lang = NULL; + const char *fallback = NULL; + const char *gravity = NULL; + const char *gravity_hint = NULL; + const char *font_features = NULL; + const char *allow_breaks = NULL; + const char *insert_hyphens = NULL; + const char *show = NULL; + const char *line_height = NULL; + const char *text_transform = NULL; + const char *segment = NULL; + const char *font_scale = NULL; + + g_markup_parse_context_get_position (context, + &line_number, &char_number); + +#define CHECK_DUPLICATE(var) G_STMT_START{ \ + if ((var) != NULL) { \ + g_set_error (error, G_MARKUP_ERROR, \ + G_MARKUP_ERROR_INVALID_CONTENT, \ + "Attribute '%s' occurs twice on <span> tag " \ + "on line %d char %d, may only occur once", \ + names[i], line_number, char_number); \ + return FALSE; \ + }}G_STMT_END +#define CHECK_ATTRIBUTE2(var, name) \ + if (attr_strcmp (names[i], (name)) == 0) { \ + CHECK_DUPLICATE (var); \ + (var) = values[i]; \ + found = TRUE; \ + break; \ + } +#define CHECK_ATTRIBUTE(var) CHECK_ATTRIBUTE2 (var, G_STRINGIFY (var)) + + i = 0; + while (names[i]) + { + gboolean found = FALSE; + + switch (names[i][0]) { + case 'a': + CHECK_ATTRIBUTE (allow_breaks); + break; + case 'b': + CHECK_ATTRIBUTE (background); + CHECK_ATTRIBUTE2 (background, "bgcolor"); + CHECK_ATTRIBUTE (baseline_shift); + break; + case 'c': + CHECK_ATTRIBUTE2 (foreground, "color"); + break; + case 'f': + CHECK_ATTRIBUTE (fallback); + CHECK_ATTRIBUTE2 (desc, "font"); + CHECK_ATTRIBUTE2 (desc, "font_desc"); + CHECK_ATTRIBUTE2 (family, "face"); + + CHECK_ATTRIBUTE2 (family, "font_family"); + CHECK_ATTRIBUTE2 (size, "font_size"); + CHECK_ATTRIBUTE2 (stretch, "font_stretch"); + CHECK_ATTRIBUTE2 (style, "font_style"); + CHECK_ATTRIBUTE2 (variant, "font_variant"); + CHECK_ATTRIBUTE2 (weight, "font_weight"); + CHECK_ATTRIBUTE (font_scale); + + CHECK_ATTRIBUTE (foreground); + CHECK_ATTRIBUTE2 (foreground, "fgcolor"); + + CHECK_ATTRIBUTE (font_features); + break; + case 's': + CHECK_ATTRIBUTE (show); + CHECK_ATTRIBUTE (size); + CHECK_ATTRIBUTE (stretch); + CHECK_ATTRIBUTE (strikethrough); + CHECK_ATTRIBUTE (strikethrough_color); + CHECK_ATTRIBUTE (style); + CHECK_ATTRIBUTE (segment); + break; + case 't': + CHECK_ATTRIBUTE (text_transform); + break; + case 'g': + CHECK_ATTRIBUTE (gravity); + CHECK_ATTRIBUTE (gravity_hint); + break; + case 'i': + CHECK_ATTRIBUTE (insert_hyphens); + break; + case 'l': + CHECK_ATTRIBUTE (lang); + CHECK_ATTRIBUTE (letter_spacing); + CHECK_ATTRIBUTE (line_height); + break; + case 'o': + CHECK_ATTRIBUTE (overline); + CHECK_ATTRIBUTE (overline_color); + break; + case 'u': + CHECK_ATTRIBUTE (underline); + CHECK_ATTRIBUTE (underline_position); + CHECK_ATTRIBUTE (underline_color); + break; + case 'r': + CHECK_ATTRIBUTE (rise); + break; + case 'v': + CHECK_ATTRIBUTE (variant); + break; + case 'w': + CHECK_ATTRIBUTE (weight); + break; + default:; + } + + if (!found) + { + g_set_error (error, G_MARKUP_ERROR, + G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE, + "Attribute '%s' is not allowed on the <span> tag " + "on line %d char %d", + names[i], line_number, char_number); + return FALSE; + } + + ++i; + } + + /* Parse desc first, then modify it with other font-related attributes. */ + if (G_UNLIKELY (desc)) + { + Pango2FontDescription *parsed; + + parsed = pango2_font_description_from_string (desc); + if (parsed) + { + add_attribute (tag, pango2_attr_font_desc_new (parsed)); + if (tag) + open_tag_set_absolute_font_size (tag, pango2_font_description_get_size (parsed)); + pango2_font_description_free (parsed); + } + } + + if (G_UNLIKELY (family)) + { + add_attribute (tag, pango2_attr_family_new (family)); + } + + if (G_UNLIKELY (size)) + { + int n; + + if (parse_length (size, &n) && n > 0) + { + add_attribute (tag, pango2_attr_size_new (n)); + if (tag) + open_tag_set_absolute_font_size (tag, n); + } + else if (strcmp (size, "smaller") == 0) + { + if (tag) + { + tag->scale_level_delta -= 1; + tag->scale_level -= 1; + } + } + else if (strcmp (size, "larger") == 0) + { + if (tag) + { + tag->scale_level_delta += 1; + tag->scale_level += 1; + } + } + else if (parse_absolute_size (tag, size)) + ; /* nothing */ + else + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "Value of 'size' attribute on <span> tag on line %d " + "could not be parsed; should be an integer, or a " + "string such as 'small', not '%s'", + line_number, size); + goto error; + } + } + + if (G_UNLIKELY (style)) + { + Pango2Style pango2_style; + + if (pango2_parse_style (style, &pango2_style, FALSE)) + add_attribute (tag, pango2_attr_style_new (pango2_style)); + else + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "'%s' is not a valid value for the 'style' attribute " + "on <span> tag, line %d; valid values are " + "'normal', 'oblique', 'italic'", + style, line_number); + goto error; + } + } + + if (G_UNLIKELY (weight)) + { + Pango2Weight pango2_weight; + + if (pango2_parse_weight (weight, &pango2_weight, FALSE)) + add_attribute (tag, + pango2_attr_weight_new (pango2_weight)); + else + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "'%s' is not a valid value for the 'weight' " + "attribute on <span> tag, line %d; valid " + "values are for example 'light', 'ultrabold' or a number", + weight, line_number); + goto error; + } + } + + if (G_UNLIKELY (variant)) + { + Pango2Variant pango2_variant; + + if (pango2_parse_variant (variant, &pango2_variant, FALSE)) + add_attribute (tag, pango2_attr_variant_new (pango2_variant)); + else + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "'%s' is not a valid value for the 'variant' " + "attribute on <span> tag, line %d; valid values are " + "'normal', 'smallcaps'", + variant, line_number); + goto error; + } + } + + if (G_UNLIKELY (stretch)) + { + Pango2Stretch pango2_stretch; + + if (pango2_parse_stretch (stretch, &pango2_stretch, FALSE)) + add_attribute (tag, pango2_attr_stretch_new (pango2_stretch)); + else + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "'%s' is not a valid value for the 'stretch' " + "attribute on <span> tag, line %d; valid " + "values are for example 'condensed', " + "'ultraexpanded', 'normal'", + stretch, line_number); + goto error; + } + } + + if (G_UNLIKELY (foreground)) + { + Pango2Color color; + + if (!span_parse_color ("foreground", foreground, &color, line_number, error)) + goto error; + + add_attribute (tag, pango2_attr_foreground_new (&color)); + } + + if (G_UNLIKELY (background)) + { + Pango2Color color; + + if (!span_parse_color ("background", background, &color, line_number, error)) + goto error; + + add_attribute (tag, pango2_attr_background_new (&color)); + } + + if (G_UNLIKELY (underline)) + { + Pango2LineStyle style = PANGO2_LINE_STYLE_NONE; + + if (!span_parse_enum ("underline", underline, PANGO2_TYPE_LINE_STYLE, (int*)(void*)&style, line_number, error)) + goto error; + + add_attribute (tag, pango2_attr_underline_new (style)); + } + + if (G_UNLIKELY (underline_position)) + { + Pango2UnderlinePosition pos = PANGO2_UNDERLINE_POSITION_NORMAL; + + if (!span_parse_enum ("underline_position", underline_position, PANGO2_TYPE_UNDERLINE_POSITION, (int*)(void*)&pos, line_number, error)) + goto error; + + add_attribute (tag, pango2_attr_underline_position_new (pos)); + } + + if (G_UNLIKELY (underline_color)) + { + Pango2Color color; + + if (!span_parse_color ("underline_color", underline_color, &color, line_number, error)) + goto error; + + add_attribute (tag, pango2_attr_underline_color_new (&color)); + } + + if (G_UNLIKELY (overline)) + { + Pango2LineStyle style = PANGO2_LINE_STYLE_NONE; + + if (!span_parse_enum ("overline", overline, PANGO2_TYPE_LINE_STYLE, (int*)(void*)&style, line_number, error)) + goto error; + + add_attribute (tag, pango2_attr_overline_new (style)); + } + + if (G_UNLIKELY (overline_color)) + { + Pango2Color color; + + if (!span_parse_color ("overline_color", overline_color, &color, line_number, error)) + goto error; + + add_attribute (tag, pango2_attr_overline_color_new (&color)); + } + + if (G_UNLIKELY (gravity)) + { + Pango2Gravity gr = PANGO2_GRAVITY_SOUTH; + + if (!span_parse_enum ("gravity", gravity, PANGO2_TYPE_GRAVITY, (int*)(void*)&gr, line_number, error)) + goto error; + + if (gr == PANGO2_GRAVITY_AUTO) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "'%s' is not a valid value for the 'gravity' " + "attribute on <span> tag, line %d; valid " + "values are for example 'south', 'east', " + "'north', 'west'", + gravity, line_number); + goto error; + } + + add_attribute (tag, pango2_attr_gravity_new (gr)); + } + + if (G_UNLIKELY (gravity_hint)) + { + Pango2GravityHint hint = PANGO2_GRAVITY_HINT_NATURAL; + + if (!span_parse_enum ("gravity_hint", gravity_hint, PANGO2_TYPE_GRAVITY_HINT, (int*)(void*)&hint, line_number, error)) + goto error; + + add_attribute (tag, pango2_attr_gravity_hint_new (hint)); + } + + if (G_UNLIKELY (strikethrough)) + { + Pango2LineStyle style = PANGO2_LINE_STYLE_NONE; + + if (!span_parse_enum ("strikethrough", strikethrough, PANGO2_TYPE_LINE_STYLE, (int*)(void*)&style, line_number, error)) + goto error; + + add_attribute (tag, pango2_attr_strikethrough_new (style)); + } + + if (G_UNLIKELY (strikethrough_color)) + { + Pango2Color color; + + if (!span_parse_color ("strikethrough_color", strikethrough_color, &color, line_number, error)) + goto error; + + add_attribute (tag, pango2_attr_strikethrough_color_new (&color)); + } + + if (G_UNLIKELY (fallback)) + { + gboolean b = FALSE; + + if (!span_parse_boolean ("fallback", fallback, &b, line_number, error)) + goto error; + + add_attribute (tag, pango2_attr_fallback_new (b)); + } + + if (G_UNLIKELY (show)) + { + Pango2ShowFlags flags; + + if (!span_parse_flags ("show", show, PANGO2_TYPE_SHOW_FLAGS, (int*)(void*)&flags, line_number, error)) + goto error; + + add_attribute (tag, pango2_attr_show_new (flags)); + } + + if (G_UNLIKELY (text_transform)) + { + Pango2TextTransform tf; + + if (!span_parse_enum ("text_transform", text_transform, PANGO2_TYPE_TEXT_TRANSFORM, (int*)(void*)&tf, line_number, error)) + goto error; + + add_attribute (tag, pango2_attr_text_transform_new (tf)); + } + + if (G_UNLIKELY (rise)) + { + int n = 0; + + if (!parse_length (rise, &n)) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "Value of 'rise' attribute on <span> tag on line %d " + "could not be parsed; should be an integer, or a " + "string such as '5.5pt', not '%s'", + line_number, rise); + goto error; + } + + add_attribute (tag, pango2_attr_rise_new (n)); + } + + if (G_UNLIKELY (baseline_shift)) + { + int shift = 0; + + if (span_parse_enum ("baseline_shift", baseline_shift, PANGO2_TYPE_BASELINE_SHIFT, (int*)(void*)&shift, line_number, NULL)) + add_attribute (tag, pango2_attr_baseline_shift_new (shift)); + else if (parse_length (baseline_shift, &shift) && (shift > 1024 || shift < -1024)) + add_attribute (tag, pango2_attr_baseline_shift_new (shift)); + else + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "Value of 'baseline_shift' attribute on <span> tag on line %d " + "could not be parsed; should be 'superscript' or 'subscript' or " + "an integer, or a string such as '5.5pt', not '%s'", + line_number, baseline_shift); + goto error; + } + + } + + if (G_UNLIKELY (font_scale)) + { + Pango2FontScale scale; + + if (!span_parse_enum ("font_scale", font_scale, PANGO2_TYPE_FONT_SCALE, (int*)(void*)&scale, line_number, error)) + goto error; + + add_attribute (tag, pango2_attr_font_scale_new (scale)); + } + + if (G_UNLIKELY (letter_spacing)) + { + int n = 0; + + if (!span_parse_int ("letter_spacing", letter_spacing, &n, line_number, error)) + goto error; + + add_attribute (tag, pango2_attr_letter_spacing_new (n)); + } + + if (G_UNLIKELY (line_height)) + { + double f = 0; + + if (!span_parse_float ("line_height", line_height, &f, line_number, error)) + goto error; + + if (f > 1024.0 && strchr (line_height, '.') == 0) + add_attribute (tag, pango2_attr_line_height_new_absolute ((int)f)); + else + add_attribute (tag, pango2_attr_line_height_new (f)); + } + + if (G_UNLIKELY (lang)) + { + add_attribute (tag, + pango2_attr_language_new (pango2_language_from_string (lang))); + } + + if (G_UNLIKELY (font_features)) + { + add_attribute (tag, pango2_attr_font_features_new (font_features)); + } + + if (G_UNLIKELY (allow_breaks)) + { + gboolean b = FALSE; + + if (!span_parse_boolean ("allow_breaks", allow_breaks, &b, line_number, error)) + goto error; + + add_attribute (tag, pango2_attr_allow_breaks_new (b)); + } + + if (G_UNLIKELY (insert_hyphens)) + { + gboolean b = FALSE; + + if (!span_parse_boolean ("insert_hyphens", insert_hyphens, &b, line_number, error)) + goto error; + + add_attribute (tag, pango2_attr_insert_hyphens_new (b)); + } + + if (G_UNLIKELY (segment)) + { + if (strcmp (segment, "word") == 0) + add_attribute (tag, pango2_attr_word_new ()); + else if (strcmp (segment, "sentence") == 0) + add_attribute (tag, pango2_attr_sentence_new ()); + else if (strcmp (segment, "paragraph") == 0) + add_attribute (tag, pango2_attr_paragraph_new ()); + else + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "Value of 'segment' attribute on <span> tag on line %d " + "could not be parsed; should be one of 'word', 'sentence' " + "or 'paragraph', not '%s'", + line_number, segment); + goto error; + } + } + + return TRUE; + + error: + + return FALSE; +} + +static gboolean +i_parse_func (MarkupData *md G_GNUC_UNUSED, + OpenTag *tag, + const char **names, + const char **values G_GNUC_UNUSED, + GMarkupParseContext *context, + GError **error) +{ + CHECK_NO_ATTRS ("i"); + + add_attribute (tag, pango2_attr_style_new (PANGO2_STYLE_ITALIC)); + + return TRUE; +} + +static gboolean +markup_parse_func (MarkupData *md G_GNUC_UNUSED, + OpenTag *tag G_GNUC_UNUSED, + const char **names G_GNUC_UNUSED, + const char **values G_GNUC_UNUSED, + GMarkupParseContext *context G_GNUC_UNUSED, + GError **error G_GNUC_UNUSED) +{ + /* We don't do anything with this tag at the moment. */ + CHECK_NO_ATTRS ("markup"); + + return TRUE; +} + +static gboolean +s_parse_func (MarkupData *md G_GNUC_UNUSED, + OpenTag *tag, + const char **names, + const char **values G_GNUC_UNUSED, + GMarkupParseContext *context, + GError **error) +{ + CHECK_NO_ATTRS ("s"); + + add_attribute (tag, pango2_attr_strikethrough_new (TRUE)); + + return TRUE; +} + +static gboolean +sub_parse_func (MarkupData *md G_GNUC_UNUSED, + OpenTag *tag, + const char **names, + const char **values G_GNUC_UNUSED, + GMarkupParseContext *context, + GError **error) +{ + CHECK_NO_ATTRS ("sub"); + + add_attribute (tag, pango2_attr_font_scale_new (PANGO2_FONT_SCALE_SUBSCRIPT)); + add_attribute (tag, pango2_attr_baseline_shift_new (PANGO2_BASELINE_SHIFT_SUBSCRIPT)); + + return TRUE; +} + +static gboolean +sup_parse_func (MarkupData *md G_GNUC_UNUSED, + OpenTag *tag, + const char **names, + const char **values G_GNUC_UNUSED, + GMarkupParseContext *context, + GError **error) +{ + CHECK_NO_ATTRS ("sup"); + + add_attribute (tag, pango2_attr_font_scale_new (PANGO2_FONT_SCALE_SUPERSCRIPT)); + add_attribute (tag, pango2_attr_baseline_shift_new (PANGO2_BASELINE_SHIFT_SUPERSCRIPT)); + + return TRUE; +} + +static gboolean +small_parse_func (MarkupData *md G_GNUC_UNUSED, + OpenTag *tag, + const char **names, + const char **values G_GNUC_UNUSED, + GMarkupParseContext *context, + GError **error) +{ + CHECK_NO_ATTRS ("small"); + + /* Shrink text one level */ + if (tag) + { + tag->scale_level_delta -= 1; + tag->scale_level -= 1; + } + + return TRUE; +} + +static gboolean +tt_parse_func (MarkupData *md G_GNUC_UNUSED, + OpenTag *tag, + const char **names, + const char **values G_GNUC_UNUSED, + GMarkupParseContext *context, + GError **error) +{ + CHECK_NO_ATTRS ("tt"); + + add_attribute (tag, pango2_attr_family_new ("Monospace")); + + return TRUE; +} + +static gboolean +u_parse_func (MarkupData *md G_GNUC_UNUSED, + OpenTag *tag, + const char **names, + const char **values G_GNUC_UNUSED, + GMarkupParseContext *context, + GError **error) +{ + CHECK_NO_ATTRS ("u"); + + add_attribute (tag, pango2_attr_underline_new (PANGO2_LINE_STYLE_SOLID)); + + return TRUE; +} diff --git a/pango2/pango-markup.h b/pango2/pango-markup.h new file mode 100644 index 00000000..8bc37af7 --- /dev/null +++ b/pango2/pango-markup.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2000 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-attr-list.h> + +G_BEGIN_DECLS + + +PANGO2_AVAILABLE_IN_ALL +GMarkupParseContext * pango2_markup_parser_new (gunichar accel_marker); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_markup_parser_finish (GMarkupParseContext *context, + Pango2AttrList **attr_list, + char **text, + gunichar *accel_char, + GError **error); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_parse_markup (const char *markup_text, + int length, + gunichar accel_marker, + Pango2AttrList **attr_list, + char **text, + gunichar *accel_char, + GError **error); + + +G_END_DECLS diff --git a/pango2/pango-matrix.c b/pango2/pango-matrix.c new file mode 100644 index 00000000..9381ffcd --- /dev/null +++ b/pango2/pango-matrix.c @@ -0,0 +1,528 @@ +/* Pango2 + * pango-matrix.c: Matrix manipulation routines + * + * Copyright (C) 2000, 2006 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include <stdlib.h> +#include <math.h> + +#include "pango-matrix.h" +#include "pango-impl-utils.h" + +G_DEFINE_BOXED_TYPE (Pango2Matrix, pango2_matrix, + pango2_matrix_copy, + pango2_matrix_free); + +/** + * pango2_matrix_copy: + * @matrix: (nullable): a `Pango2Matrix` + * + * Copies a `Pango2Matrix`. + * + * Return value: (nullable): the newly allocated `Pango2Matrix` + */ +Pango2Matrix * +pango2_matrix_copy (const Pango2Matrix *matrix) +{ + Pango2Matrix *new_matrix; + + if (matrix == NULL) + return NULL; + + new_matrix = g_slice_new (Pango2Matrix); + + *new_matrix = *matrix; + + return new_matrix; +} + +/** + * pango2_matrix_free: + * @matrix: (nullable): a `Pango2Matrix`, may be %NULL + * + * Free a `Pango2Matrix`. + */ +void +pango2_matrix_free (Pango2Matrix *matrix) +{ + if (matrix == NULL) + return; + + g_slice_free (Pango2Matrix, matrix); +} + +/** + * pango2_matrix_translate: + * @matrix: a `Pango2Matrix` + * @tx: amount to translate in the X direction + * @ty: amount to translate in the Y direction + * + * Changes the transformation represented by @matrix to be the + * transformation given by first translating by (@tx, @ty) + * then applying the original transformation. + */ +void +pango2_matrix_translate (Pango2Matrix *matrix, + double tx, + double ty) +{ + g_return_if_fail (matrix != NULL); + + matrix->x0 = matrix->xx * tx + matrix->xy * ty + matrix->x0; + matrix->y0 = matrix->yx * tx + matrix->yy * ty + matrix->y0; +} + +/** + * pango2_matrix_scale: + * @matrix: a `Pango2Matrix` + * @scale_x: amount to scale by in X direction + * @scale_y: amount to scale by in Y direction + * + * Changes the transformation represented by @matrix to be the + * transformation given by first scaling by @sx in the X direction + * and @sy in the Y direction then applying the original + * transformation. + */ +void +pango2_matrix_scale (Pango2Matrix *matrix, + double scale_x, + double scale_y) +{ + g_return_if_fail (matrix != NULL); + + matrix->xx *= scale_x; + matrix->xy *= scale_y; + matrix->yx *= scale_x; + matrix->yy *= scale_y; +} + +/** + * pango2_matrix_rotate: + * @matrix: a `Pango2Matrix` + * @degrees: degrees to rotate counter-clockwise + * + * Changes the transformation represented by @matrix to be the + * transformation given by first rotating by @degrees degrees + * counter-clockwise then applying the original transformation. + */ +void +pango2_matrix_rotate (Pango2Matrix *matrix, + double degrees) +{ + Pango2Matrix tmp; + double r, s, c; + + g_return_if_fail (matrix != NULL); + + r = degrees * (G_PI / 180.); + s = sin (r); + c = cos (r); + + tmp.xx = c; + tmp.xy = s; + tmp.yx = -s; + tmp.yy = c; + tmp.x0 = 0; + tmp.y0 = 0; + + pango2_matrix_concat (matrix, &tmp); +} + +/** + * pango2_matrix_concat: + * @matrix: a `Pango2Matrix` + * @new_matrix: a `Pango2Matrix` + * + * Changes the transformation represented by @matrix to be the + * transformation given by first applying transformation + * given by @new_matrix then applying the original transformation. + */ +void +pango2_matrix_concat (Pango2Matrix *matrix, + const Pango2Matrix *new_matrix) +{ + Pango2Matrix tmp; + + g_return_if_fail (matrix != NULL); + + tmp = *matrix; + + matrix->xx = tmp.xx * new_matrix->xx + tmp.xy * new_matrix->yx; + matrix->xy = tmp.xx * new_matrix->xy + tmp.xy * new_matrix->yy; + matrix->yx = tmp.yx * new_matrix->xx + tmp.yy * new_matrix->yx; + matrix->yy = tmp.yx * new_matrix->xy + tmp.yy * new_matrix->yy; + matrix->x0 = tmp.xx * new_matrix->x0 + tmp.xy * new_matrix->y0 + tmp.x0; + matrix->y0 = tmp.yx * new_matrix->x0 + tmp.yy * new_matrix->y0 + tmp.y0; +} + +/** + * pango2_matrix_get_font_scale_factor: + * @matrix: (nullable): a `Pango2Matrix`, may be %NULL + * + * Returns the scale factor of a matrix on the height of the font. + * + * That is, the scale factor in the direction perpendicular to the + * vector that the X coordinate is mapped to. If the scale in the X + * coordinate is needed as well, use [method@Pango2.Matrix.get_font_scale_factors]. + * + * Return value: the scale factor of @matrix on the height of the font, + * or 1.0 if @matrix is %NULL. + */ +double +pango2_matrix_get_font_scale_factor (const Pango2Matrix *matrix) +{ + double yscale; + pango2_matrix_get_font_scale_factors (matrix, NULL, &yscale); + return yscale; +} + +/** + * pango2_matrix_get_font_scale_factors: + * @matrix: (nullable): a `Pango2Matrix` + * @xscale: (out) (optional): output scale factor in the x direction + * @yscale: (out) (optional): output scale factor perpendicular to the x direction + * + * Calculates the scale factor of a matrix on the width and height of the font. + * + * That is, @xscale is the scale factor in the direction of the X coordinate, + * and @yscale is the scale factor in the direction perpendicular to the + * vector that the X coordinate is mapped to. + * + * Note that output numbers will always be non-negative. + **/ +void +pango2_matrix_get_font_scale_factors (const Pango2Matrix *matrix, + double *xscale, + double *yscale) +{ +/* + * Based on cairo-matrix.c:_cairo_matrix_compute_scale_factors() + * + * Copyright 2005, Keith Packard + */ + double major = 1., minor = 1.; + + if (matrix) + { + double x = matrix->xx; + double y = matrix->yx; + major = sqrt (x*x + y*y); + + if (major) + { + double det = matrix->xx * matrix->yy - matrix->yx * matrix->xy; + + /* + * ignore mirroring + */ + if (det < 0) + det = - det; + + minor = det / major; + } + else + minor = 0.; + } + + if (xscale) + *xscale = major; + if (yscale) + *yscale = minor; +} + +#define RAD_TO_DEG(x) ((x)/G_PI * 180) + +/** + * pango2_matrix_get_rotation: + * @matrix: a `Pango2Matrix` + * + * Returns the angle (in degrees) that this + * matrix rotates the X axis by. + * + * For font matrices, this is typically zero. + * + * Returns: the rotation of @matrix + */ +double +pango2_matrix_get_rotation (const Pango2Matrix *matrix) +{ + double x, y; + + x = 1; + y = 0; + + pango2_matrix_transform_distance (matrix, &x, &y); + + return RAD_TO_DEG (acos (CLAMP (x / sqrtf (x*x + y*y), -1., 1.))); +} + +/** + * pango2_matrix_get_slant_ratio: + * @matrix: a `Pango2Matrix` + * + * Gets the slant ratio of a matrix. + * + * For a simple shear matrix in the form: + * + * 1 λ + * 0 1 + * + * this is simply λ. + * + * Returns: the slant ratio of @matrix + */ +double +pango2_matrix_get_slant_ratio (const Pango2Matrix *matrix) +{ + if (matrix) + { + double a = matrix->xx; + double b = matrix->xy; + double c = matrix->yx; + double d = matrix->yy; + + if (c != 0 || d != 0) + { + double s = sqrtf (c * c + d * d); + return (a*c + b*d) / (s*s); + } + } + + return 0; +} + +/** + * pango2_matrix_transform_distance: + * @matrix: (nullable): a `Pango2Matrix` + * @dx: (inout): in/out X component of a distance vector + * @dy: (inout): in/out Y component of a distance vector + * + * Transforms the distance vector (@dx,@dy) by @matrix. + * + * This is similar to [method@Pango2.Matrix.transform_point], + * except that the translation components of the transformation + * are ignored. The calculation of the returned vector is as follows: + * + * ``` + * dx2 = dx1 * xx + dy1 * xy; + * dy2 = dx1 * yx + dy1 * yy; + * ``` + * + * Affine transformations are position invariant, so the same vector + * always transforms to the same vector. If (@x1,@y1) transforms + * to (@x2,@y2) then (@x1+@dx1,@y1+@dy1) will transform to + * (@x1+@dx2,@y1+@dy2) for all values of @x1 and @x2. + */ +void +pango2_matrix_transform_distance (const Pango2Matrix *matrix, + double *dx, + double *dy) +{ + if (matrix) + { + double new_x, new_y; + + new_x = (matrix->xx * *dx + matrix->xy * *dy); + new_y = (matrix->yx * *dx + matrix->yy * *dy); + + *dx = new_x; + *dy = new_y; + } +} + +/** + * pango2_matrix_transform_point: + * @matrix: (nullable): a `Pango2Matrix` + * @x: (inout): in/out X position + * @y: (inout): in/out Y position + * + * Transforms the point (@x, @y) by @matrix. + */ +void +pango2_matrix_transform_point (const Pango2Matrix *matrix, + double *x, + double *y) +{ + if (matrix) + { + pango2_matrix_transform_distance (matrix, x, y); + + *x += matrix->x0; + *y += matrix->y0; + } +} + +/** + * pango2_matrix_transform_rectangle: + * @matrix: (nullable): a `Pango2Matrix` + * @rect: (inout) (optional): in/out bounding box in Pango2 units + * + * First transforms @rect using @matrix, then calculates the bounding box + * of the transformed rectangle. + * + * This function is useful for example when you want to draw a rotated + * @Pango2Layout to an image buffer, and want to know how large the image + * should be and how much you should shift the layout when rendering. + * + * If you have a rectangle in device units (pixels), use + * [method@Pango2.Matrix.transform_pixel_rectangle]. + * + * If you have the rectangle in Pango2 units and want to convert to + * transformed pixel bounding box, it is more accurate to transform it first + * (using this function) and pass the result to pango2_extents_to_pixels(), + * first argument, for an inclusive rounded rectangle. + * However, there are valid reasons that you may want to convert + * to pixels first and then transform, for example when the transformed + * coordinates may overflow in Pango2 units (large matrix translation for + * example). + */ +void +pango2_matrix_transform_rectangle (const Pango2Matrix *matrix, + Pango2Rectangle *rect) +{ + int i; + double quad_x[4], quad_y[4]; + double dx1, dy1; + double dx2, dy2; + double min_x, max_x; + double min_y, max_y; + + if (!rect || !matrix) + return; + + quad_x[0] = pango2_units_to_double (rect->x); + quad_y[0] = pango2_units_to_double (rect->y); + pango2_matrix_transform_point (matrix, &quad_x[0], &quad_y[0]); + + dx1 = pango2_units_to_double (rect->width); + dy1 = 0; + pango2_matrix_transform_distance (matrix, &dx1, &dy1); + quad_x[1] = quad_x[0] + dx1; + quad_y[1] = quad_y[0] + dy1; + + dx2 = 0; + dy2 = pango2_units_to_double (rect->height); + pango2_matrix_transform_distance (matrix, &dx2, &dy2); + quad_x[2] = quad_x[0] + dx2; + quad_y[2] = quad_y[0] + dy2; + + quad_x[3] = quad_x[0] + dx1 + dx2; + quad_y[3] = quad_y[0] + dy1 + dy2; + + min_x = max_x = quad_x[0]; + min_y = max_y = quad_y[0]; + + for (i=1; i < 4; i++) { + if (quad_x[i] < min_x) + min_x = quad_x[i]; + else if (quad_x[i] > max_x) + max_x = quad_x[i]; + + if (quad_y[i] < min_y) + min_y = quad_y[i]; + else if (quad_y[i] > max_y) + max_y = quad_y[i]; + } + + rect->x = pango2_units_from_double (min_x); + rect->y = pango2_units_from_double (min_y); + rect->width = pango2_units_from_double (max_x) - rect->x; + rect->height = pango2_units_from_double (max_y) - rect->y; +} + +/** + * pango2_matrix_transform_pixel_rectangle: + * @matrix: (nullable): a `Pango2Matrix` + * @rect: (inout) (optional): in/out bounding box in device units + * + * First transforms the @rect using @matrix, then calculates the bounding box + * of the transformed rectangle. + * + * This function is useful for example when you want to draw a rotated + * @Pango2Layout to an image buffer, and want to know how large the image + * should be and how much you should shift the layout when rendering. + * + * For better accuracy, you should use [method@Pango2.Matrix.transform_rectangle] + * on original rectangle in Pango2 units and convert to pixels afterward + * using [func@extents_to_pixels]'s first argument. + */ +void +pango2_matrix_transform_pixel_rectangle (const Pango2Matrix *matrix, + Pango2Rectangle *rect) +{ + int i; + double quad_x[4], quad_y[4]; + double dx1, dy1; + double dx2, dy2; + double min_x, max_x; + double min_y, max_y; + + if (!rect || !matrix) + return; + + quad_x[0] = rect->x; + quad_y[0] = rect->y; + pango2_matrix_transform_point (matrix, &quad_x[0], &quad_y[0]); + + dx1 = rect->width; + dy1 = 0; + pango2_matrix_transform_distance (matrix, &dx1, &dy1); + quad_x[1] = quad_x[0] + dx1; + quad_y[1] = quad_y[0] + dy1; + + dx2 = 0; + dy2 = rect->height; + pango2_matrix_transform_distance (matrix, &dx2, &dy2); + quad_x[2] = quad_x[0] + dx2; + quad_y[2] = quad_y[0] + dy2; + + quad_x[3] = quad_x[0] + dx1 + dx2; + quad_y[3] = quad_y[0] + dy1 + dy2; + + min_x = max_x = quad_x[0]; + min_y = max_y = quad_y[0]; + + for (i=1; i < 4; i++) + { + if (quad_x[i] < min_x) + min_x = quad_x[i]; + else if (quad_x[i] > max_x) + max_x = quad_x[i]; + + if (quad_y[i] < min_y) + min_y = quad_y[i]; + else if (quad_y[i] > max_y) + max_y = quad_y[i]; + } + + rect->x = floor (min_x); + rect->y = floor (min_y); + rect->width = ceil (max_x - rect->x); + rect->height = ceil (max_y - rect->y); +} + +gboolean +pango2_matrix_equal (const Pango2Matrix *m1, + const Pango2Matrix *m2) +{ + return m1->xx == m2->xx && m1->xy == m2->xy && + m1->yx == m2->yx && m1->yy == m2->yy && + m1->x0 == m2->x0 && m1->y0 == m2->y0; +} diff --git a/pango2/pango-matrix.h b/pango2/pango-matrix.h new file mode 100644 index 00000000..0456a4fc --- /dev/null +++ b/pango2/pango-matrix.h @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2002, 2006 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <glib.h> +#include <glib-object.h> + +G_BEGIN_DECLS + +typedef struct _Pango2Matrix Pango2Matrix; + +/** + * Pango2Matrix: + * @xx: 1st component of the transformation matrix + * @xy: 2nd component of the transformation matrix + * @yx: 3rd component of the transformation matrix + * @yy: 4th component of the transformation matrix + * @x0: x translation + * @y0: y translation + * + * A `Pango2Matrix` specifies a transformation between user-space + * and device coordinates. + * + * The transformation is given by + * + * ``` + * x_device = x_user * matrix->xx + y_user * matrix->xy + matrix->x0; + * y_device = x_user * matrix->yx + y_user * matrix->yy + matrix->y0; + * ``` + */ +struct _Pango2Matrix +{ + double xx; + double xy; + double yx; + double yy; + double x0; + double y0; +}; + +#define PANGO2_TYPE_MATRIX (pango2_matrix_get_type ()) + +/** + * PANGO2_MATRIX_INIT: + * + * Constant that can be used to initialize a `Pango2Matrix` to + * the identity transform. + * + * ``` + * Pango2Matrix matrix = PANGO2_MATRIX_INIT; + * pango2_matrix_rotate (&matrix, 45.); + * ``` + */ +#define PANGO2_MATRIX_INIT { 1., 0., 0., 1., 0., 0. } + +#include <pango2/pango-types.h> + +PANGO2_AVAILABLE_IN_ALL +GType pango2_matrix_get_type (void) G_GNUC_CONST; + +PANGO2_AVAILABLE_IN_ALL +Pango2Matrix * pango2_matrix_copy (const Pango2Matrix *matrix); +PANGO2_AVAILABLE_IN_ALL +void pango2_matrix_free (Pango2Matrix *matrix); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_matrix_equal (const Pango2Matrix *m1, + const Pango2Matrix *m2); + +PANGO2_AVAILABLE_IN_ALL +void pango2_matrix_translate (Pango2Matrix *matrix, + double tx, + double ty); +PANGO2_AVAILABLE_IN_ALL +void pango2_matrix_scale (Pango2Matrix *matrix, + double scale_x, + double scale_y); +PANGO2_AVAILABLE_IN_ALL +void pango2_matrix_rotate (Pango2Matrix *matrix, + double degrees); +PANGO2_AVAILABLE_IN_ALL +void pango2_matrix_concat (Pango2Matrix *matrix, + const Pango2Matrix *new_matrix); +PANGO2_AVAILABLE_IN_ALL +void pango2_matrix_transform_point (const Pango2Matrix *matrix, + double *x, + double *y); +PANGO2_AVAILABLE_IN_ALL +void pango2_matrix_transform_distance (const Pango2Matrix *matrix, + double *dx, + double *dy); +PANGO2_AVAILABLE_IN_ALL +void pango2_matrix_transform_rectangle (const Pango2Matrix *matrix, + Pango2Rectangle *rect); +PANGO2_AVAILABLE_IN_ALL +void pango2_matrix_transform_pixel_rectangle (const Pango2Matrix *matrix, + Pango2Rectangle *rect); +PANGO2_AVAILABLE_IN_ALL +double pango2_matrix_get_font_scale_factor (const Pango2Matrix *matrix) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +void pango2_matrix_get_font_scale_factors (const Pango2Matrix *matrix, + double *xscale, + double *yscale); +PANGO2_AVAILABLE_IN_ALL +double pango2_matrix_get_rotation (const Pango2Matrix *matrix) G_GNUC_PURE; +PANGO2_AVAILABLE_IN_ALL +double pango2_matrix_get_slant_ratio (const Pango2Matrix *matrix) G_GNUC_PURE; + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(Pango2Matrix, pango2_matrix_free) + + +G_END_DECLS diff --git a/pango2/pango-renderer.c b/pango2/pango-renderer.c new file mode 100644 index 00000000..5f1d3de6 --- /dev/null +++ b/pango2/pango-renderer.c @@ -0,0 +1,1731 @@ +/* Pango2 + * pango-renderer.h: Base class for rendering + * + * Copyright (C) 2004 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include <stdlib.h> + +#include "pango-renderer.h" +#include "pango-impl-utils.h" +#include "pango-layout.h" +#include "pango-attr-private.h" +#include "pango-run-private.h" +#include "pango-line-private.h" +#include "pango-attributes-private.h" +#include "pango-glyph-item-private.h" + +#define N_RENDER_PARTS 5 + +#define IS_VALID_PART(part) ((guint)part < N_RENDER_PARTS) + +typedef struct _LineState LineState; +typedef struct _Point Point; +typedef struct _Pango2RendererPrivate Pango2RendererPrivate; + +struct _Point +{ + double x, y; +}; + +struct _LineState +{ + Pango2LineStyle underline; + Pango2UnderlinePosition underline_position; + Pango2Rectangle underline_rect; + + Pango2LineStyle strikethrough; + Pango2Rectangle strikethrough_rect; + int strikethrough_glyphs; + + Pango2LineStyle overline; + Pango2Rectangle overline_rect; + + int logical_rect_end; +}; + +struct _Pango2RendererPrivate +{ + Pango2LineStyle underline; + Pango2UnderlinePosition underline_position; + Pango2LineStyle strikethrough; + Pango2LineStyle overline; + int active_count; + + Pango2Matrix *matrix; + Pango2Context *context; + + Pango2Color color[N_RENDER_PARTS]; + gboolean color_set[N_RENDER_PARTS]; + + Pango2Lines *lines; + Pango2Line *line; + LineState *line_state; +}; + +static void pango2_renderer_finalize (GObject *gobject); +static void pango2_renderer_default_draw_glyphs (Pango2Renderer *renderer, + Pango2Font *font, + Pango2GlyphString *glyphs, + int x, + int y); +static void pango2_renderer_default_draw_run (Pango2Renderer *renderer, + const char *text, + Pango2Run *run, + int x, + int y); +static void pango2_renderer_default_draw_rectangle (Pango2Renderer *renderer, + Pango2RenderPart part, + int x, + int y, + int width, + int height); +static void pango2_renderer_default_draw_styled_line (Pango2Renderer *renderer, + Pango2RenderPart part, + Pango2LineStyle style, + int x, + int y, + int width, + int height); +static void pango2_renderer_default_prepare_run (Pango2Renderer *renderer, + Pango2Run *run); +static void pango2_renderer_prepare_run (Pango2Renderer *renderer, + Pango2Run *run); + +static void +to_device (Pango2Matrix *matrix, + double x, + double y, + Point *result) +{ + if (matrix) + { + result->x = (x * matrix->xx + y * matrix->xy) / PANGO2_SCALE + matrix->x0; + result->y = (x * matrix->yx + y * matrix->yy) / PANGO2_SCALE + matrix->y0; + } + else + { + result->x = x / PANGO2_SCALE; + result->y = y / PANGO2_SCALE; + } +} + +G_DEFINE_ABSTRACT_TYPE_WITH_CODE (Pango2Renderer, pango2_renderer, G_TYPE_OBJECT, + G_ADD_PRIVATE (Pango2Renderer)) + +static void +pango2_renderer_class_init (Pango2RendererClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + klass->draw_glyphs = pango2_renderer_default_draw_glyphs; + klass->draw_run = pango2_renderer_default_draw_run; + klass->draw_rectangle = pango2_renderer_default_draw_rectangle; + klass->draw_styled_line = pango2_renderer_default_draw_styled_line; + klass->prepare_run = pango2_renderer_default_prepare_run; + + gobject_class->finalize = pango2_renderer_finalize; +} + +static void +pango2_renderer_init (Pango2Renderer *renderer) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + priv->matrix = NULL; +} + +static void +pango2_renderer_finalize (GObject *gobject) +{ + Pango2Renderer *renderer = PANGO2_RENDERER (gobject); + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + + if (priv->matrix) + pango2_matrix_free (priv->matrix); + + G_OBJECT_CLASS (pango2_renderer_parent_class)->finalize (gobject); +} + +static void +pango2_renderer_activate_with_context (Pango2Renderer *renderer, + Pango2Context *context) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + + /* We only change the matrix if the renderer isn't already active. */ + if (!priv->active_count) + { + pango2_renderer_set_matrix (renderer, context ? pango2_context_get_matrix (context) : NULL); + priv->context = context; + } + + pango2_renderer_activate (renderer); +} + +static void +draw_underline (Pango2Renderer *renderer, + LineState *state) +{ + Pango2Rectangle *rect = &state->underline_rect; + Pango2LineStyle underline = state->underline; + + state->underline = PANGO2_LINE_STYLE_NONE; + state->underline_position = PANGO2_UNDERLINE_POSITION_NORMAL; + + switch (underline) + { + case PANGO2_LINE_STYLE_NONE: + break; + case PANGO2_LINE_STYLE_DOUBLE: + pango2_renderer_draw_rectangle (renderer, + PANGO2_RENDER_PART_UNDERLINE, + rect->x, + rect->y + 2 * rect->height, + rect->width, + rect->height); + G_GNUC_FALLTHROUGH; + case PANGO2_LINE_STYLE_SOLID: + pango2_renderer_draw_rectangle (renderer, + PANGO2_RENDER_PART_UNDERLINE, + rect->x, + rect->y, + rect->width, + rect->height); + break; + case PANGO2_LINE_STYLE_DOTTED: + case PANGO2_LINE_STYLE_DASHED: + pango2_renderer_draw_styled_line (renderer, + PANGO2_RENDER_PART_UNDERLINE, + underline, + rect->x, + rect->y, + rect->width, + rect->height); + break; + case PANGO2_LINE_STYLE_WAVY: + pango2_renderer_draw_styled_line (renderer, + PANGO2_RENDER_PART_UNDERLINE, + underline, + rect->x, + rect->y, + rect->width, + 3 * rect->height); + break; + default: + break; + } +} + +static void +draw_overline (Pango2Renderer *renderer, + LineState *state) +{ + Pango2Rectangle *rect = &state->overline_rect; + Pango2LineStyle overline = state->overline; + + state->overline = PANGO2_LINE_STYLE_NONE; + + switch (overline) + { + case PANGO2_LINE_STYLE_NONE: + break; + case PANGO2_LINE_STYLE_DOUBLE: + pango2_renderer_draw_rectangle (renderer, + PANGO2_RENDER_PART_OVERLINE, + rect->x, + rect->y - 2 * rect->height, + rect->width, + rect->height); + G_GNUC_FALLTHROUGH; + case PANGO2_LINE_STYLE_SOLID: + pango2_renderer_draw_rectangle (renderer, + PANGO2_RENDER_PART_OVERLINE, + rect->x, + rect->y, + rect->width, + rect->height); + break; + case PANGO2_LINE_STYLE_DOTTED: + case PANGO2_LINE_STYLE_DASHED: + pango2_renderer_draw_styled_line (renderer, + PANGO2_RENDER_PART_OVERLINE, + overline, + rect->x, + rect->y, + rect->width, + rect->height); + break; + case PANGO2_LINE_STYLE_WAVY: + pango2_renderer_draw_styled_line (renderer, + PANGO2_RENDER_PART_OVERLINE, + overline, + rect->x, + rect->y, + rect->width, + 3 * rect->height); + break; + default: + break; + } +} + +static void +draw_strikethrough (Pango2Renderer *renderer, + LineState *state) +{ + Pango2Rectangle *rect = &state->strikethrough_rect; + + if (state->strikethrough_glyphs > 0) + { + rect->y /= state->strikethrough_glyphs; + rect->height /= state->strikethrough_glyphs; + + switch (state->strikethrough) + { + case PANGO2_LINE_STYLE_NONE: + break; + case PANGO2_LINE_STYLE_DOUBLE: + pango2_renderer_draw_rectangle (renderer, + PANGO2_RENDER_PART_STRIKETHROUGH, + rect->x, + rect->y - rect->height, + rect->width, + rect->height); + rect->y += rect->height; + G_GNUC_FALLTHROUGH; + case PANGO2_LINE_STYLE_SOLID: + pango2_renderer_draw_rectangle (renderer, + PANGO2_RENDER_PART_STRIKETHROUGH, + rect->x, + rect->y, + rect->width, + rect->height); + break; + case PANGO2_LINE_STYLE_DOTTED: + case PANGO2_LINE_STYLE_DASHED: + pango2_renderer_draw_styled_line (renderer, + PANGO2_RENDER_PART_STRIKETHROUGH, + state->strikethrough, + rect->x, + rect->y, + rect->width, + rect->height); + break; + case PANGO2_LINE_STYLE_WAVY: + pango2_renderer_draw_styled_line (renderer, + PANGO2_RENDER_PART_STRIKETHROUGH, + state->strikethrough, + rect->x, + rect->y, + rect->width, + 3 * rect->height); + break; + default: + break; + } + } + + state->strikethrough = PANGO2_LINE_STYLE_NONE; + state->strikethrough_glyphs = 0; + rect->x += rect->width; + rect->width = 0; + rect->y = 0; + rect->height = 0; +} + +static void +handle_line_state_change (Pango2Renderer *renderer, + Pango2RenderPart part) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + LineState *state = priv->line_state; + if (!state) + return; + + if (part == PANGO2_RENDER_PART_UNDERLINE && + state->underline != PANGO2_LINE_STYLE_NONE) + { + Pango2Rectangle *rect = &state->underline_rect; + + rect->width = state->logical_rect_end - rect->x; + draw_underline (renderer, state); + state->underline = priv->underline; + state->underline_position = priv->underline_position; + rect->x = state->logical_rect_end; + rect->width = 0; + } + + if (part == PANGO2_RENDER_PART_OVERLINE && + state->overline != PANGO2_LINE_STYLE_NONE) + { + Pango2Rectangle *rect = &state->overline_rect; + + rect->width = state->logical_rect_end - rect->x; + draw_overline (renderer, state); + state->overline = priv->overline; + rect->x = state->logical_rect_end; + rect->width = 0; + } + + if (part == PANGO2_RENDER_PART_STRIKETHROUGH && + state->strikethrough != PANGO2_LINE_STYLE_NONE) + { + Pango2Rectangle *rect = &state->strikethrough_rect; + + rect->width = state->logical_rect_end - rect->x; + draw_strikethrough (renderer, state); + state->strikethrough = priv->strikethrough; + } +} + +static void +add_underline (Pango2Renderer *renderer, + LineState *state, + Pango2FontMetrics *metrics, + int base_x, + int base_y, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + Pango2Rectangle *current_rect = &state->underline_rect; + Pango2Rectangle new_rect; + + int underline_thickness = pango2_font_metrics_get_underline_thickness (metrics); + int underline_position = pango2_font_metrics_get_underline_position (metrics); + + new_rect.x = base_x + MIN (ink_rect->x, logical_rect->x); + new_rect.width = MAX (ink_rect->width, logical_rect->width); + new_rect.height = underline_thickness; + new_rect.y = base_y; + + switch (priv->underline) + { + case PANGO2_LINE_STYLE_NONE: + g_assert_not_reached (); + break; + case PANGO2_LINE_STYLE_SOLID: + case PANGO2_LINE_STYLE_DASHED: + case PANGO2_LINE_STYLE_DOTTED: + if (priv->underline_position == PANGO2_UNDERLINE_POSITION_UNDER) + { + new_rect.y += ink_rect->y + ink_rect->height + underline_thickness; + break; + } + G_GNUC_FALLTHROUGH; + case PANGO2_LINE_STYLE_DOUBLE: + case PANGO2_LINE_STYLE_WAVY: + new_rect.y -= underline_position; + if (state->underline == priv->underline) + { + new_rect.y = MAX (current_rect->y, new_rect.y); + new_rect.height = MAX (current_rect->height, new_rect.height); + current_rect->y = new_rect.y; + current_rect->height = new_rect.height; + } + break; + default: + break; + } + + if (priv->underline == state->underline && + priv->underline_position == state->underline_position && + new_rect.y == current_rect->y && + new_rect.height == current_rect->height) + { + current_rect->width = new_rect.x + new_rect.width - current_rect->x; + } + else + { + draw_underline (renderer, state); + + *current_rect = new_rect; + state->underline = priv->underline; + state->underline_position = priv->underline_position; + } +} + +static void +add_overline (Pango2Renderer *renderer, + LineState *state, + Pango2FontMetrics *metrics, + int base_x, + int base_y, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + Pango2Rectangle *current_rect = &state->overline_rect; + Pango2Rectangle new_rect; + int underline_thickness = pango2_font_metrics_get_underline_thickness (metrics); + int ascent = pango2_font_metrics_get_ascent (metrics); + + new_rect.x = base_x + ink_rect->x; + new_rect.width = ink_rect->width; + new_rect.height = underline_thickness; + new_rect.y = base_y; + + switch (priv->overline) + { + case PANGO2_LINE_STYLE_NONE: + g_assert_not_reached (); + break; + case PANGO2_LINE_STYLE_SOLID: + case PANGO2_LINE_STYLE_DOUBLE: + case PANGO2_LINE_STYLE_DASHED: + case PANGO2_LINE_STYLE_DOTTED: + case PANGO2_LINE_STYLE_WAVY: + new_rect.y -= ascent; + if (state->overline == priv->overline) + { + new_rect.y = MIN (current_rect->y, new_rect.y); + new_rect.height = MAX (current_rect->height, new_rect.height); + current_rect->y = new_rect.y; + current_rect->height = new_rect.height; + } + break; + default: + break; + } + + if (priv->overline == state->overline && + new_rect.y == current_rect->y && + new_rect.height == current_rect->height) + { + current_rect->width = new_rect.x + new_rect.width - current_rect->x; + } + else + { + draw_overline (renderer, state); + + *current_rect = new_rect; + state->overline = priv->overline; + } +} + +static void +add_strikethrough (Pango2Renderer *renderer, + LineState *state, + Pango2FontMetrics *metrics, + int base_x, + int base_y, + Pango2Rectangle *ink_rect G_GNUC_UNUSED, + Pango2Rectangle *logical_rect, + int num_glyphs) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + Pango2Rectangle *current_rect = &state->strikethrough_rect; + Pango2Rectangle new_rect; + + int strikethrough_thickness = pango2_font_metrics_get_strikethrough_thickness (metrics); + int strikethrough_position = pango2_font_metrics_get_strikethrough_position (metrics); + + new_rect.x = base_x + ink_rect->x; + new_rect.width = ink_rect->width; + new_rect.y = (base_y - strikethrough_position) * num_glyphs; + new_rect.height = strikethrough_thickness * num_glyphs; + + if (state->strikethrough == priv->strikethrough) + { + current_rect->width = new_rect.x + new_rect.width - current_rect->x; + current_rect->y += new_rect.y; + current_rect->height += new_rect.height; + state->strikethrough_glyphs += num_glyphs; + } + else + { + draw_strikethrough (renderer, state); + + *current_rect = new_rect; + state->strikethrough = priv->strikethrough; + state->strikethrough_glyphs = num_glyphs; + } +} + +static void pango2_renderer_draw_runs (Pango2Renderer *renderer, + Pango2Line *line, + GSList *runs, + const char *text, + int x, + int y); + +static void +draw_shaped_glyphs (Pango2Renderer *renderer, + Pango2GlyphString *glyphs, + Pango2Attribute *attr, + int x, + int y) +{ + Pango2RendererClass *class = PANGO2_RENDERER_GET_CLASS (renderer); + int i; + + if (!class->draw_shape) + return; + + for (i = 0; i < glyphs->num_glyphs; i++) + { + Pango2GlyphInfo *gi = &glyphs->glyphs[i]; + ShapeData *data = (ShapeData *)attr->pointer_value; + + class->draw_shape (renderer, + &data->ink_rect, + &data->logical_rect, + data->data, + x, y); + + x += gi->geometry.width; + } +} + +/** + * pango2_renderer_draw_line: + * @renderer: a `Pango2Renderer` + * @line: a `Pango2Line` + * @x: X position of left edge of baseline, in user space coordinates + * in Pango2 units. + * @y: Y position of left edge of baseline, in user space coordinates + * in Pango2 units. + * + * Draws @line with the specified `Pango2Renderer`. + * + * This draws the glyph items that make up the line, as well as + * shapes, backgrounds and lines that are specified by the attributes + * of those items. + */ +void +pango2_renderer_draw_line (Pango2Renderer *renderer, + Pango2Line *line, + int x, + int y) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + LineState state = { 0, }; + + g_return_if_fail (PANGO2_IS_RENDERER (renderer)); + + pango2_renderer_activate_with_context (renderer, line->context); + + priv->line = line; + priv->line_state = &state; + + state.underline = PANGO2_LINE_STYLE_NONE; + state.underline_position = PANGO2_UNDERLINE_POSITION_NORMAL; + state.overline = PANGO2_LINE_STYLE_NONE; + state.strikethrough = PANGO2_LINE_STYLE_NONE; + + pango2_renderer_draw_runs (renderer, line, line->runs, line->data->text, x, y); + + /* Finish off any remaining underlines */ + draw_underline (renderer, &state); + draw_overline (renderer, &state); + draw_strikethrough (renderer, &state); + + priv->line = NULL; + priv->line_state = NULL; + priv->line = NULL; + + pango2_renderer_deactivate (renderer); +} + +/** + * pango2_renderer_draw_lines: + * @renderer: a `Pango2Renderer` + * @lines: a `Pango2Lines` object + * @x: X position of left edge of baseline, in user space coordinates + * in Pango2 units. + * @y: Y position of left edge of baseline, in user space coordinates + * in Pango2 units. + * + * Draws @lines with the specified `Pango2Renderer`. + */ +void +pango2_renderer_draw_lines (Pango2Renderer *renderer, + Pango2Lines *lines, + int x, + int y) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + int n; + Pango2Line *line; + int line_x, line_y; + Pango2Line **l; + + g_return_if_fail (PANGO2_IS_RENDERER (renderer)); + + priv->lines = lines; + + l = pango2_lines_get_lines (lines); + for (n = 0; n < pango2_lines_get_line_count (lines); n++) + { + line = l[n]; + pango2_lines_get_line_position (lines, n, &line_x, &line_y); + + if (n == 0) + pango2_renderer_activate_with_context (renderer, line->context); + + pango2_renderer_draw_line (renderer, line, x + line_x, y + line_y); + } + + if (n > 0) + pango2_renderer_deactivate (renderer); + + priv->lines = NULL; +} + +static void +pango2_shape_get_extents (int n_chars, + Pango2Rectangle *shape_ink, + Pango2Rectangle *shape_logical, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect) +{ + if (n_chars > 0) + { + ink_rect->x = MIN (shape_ink->x, shape_ink->x + shape_logical->width * (n_chars - 1)); + ink_rect->width = MAX (shape_ink->width, shape_ink->width + shape_logical->width * (n_chars - 1)); + ink_rect->y = shape_ink->y; + ink_rect->height = shape_ink->height; + + logical_rect->x = MIN (shape_logical->x, shape_logical->x + shape_logical->width * (n_chars - 1)); + logical_rect->width = MAX (shape_logical->width, shape_logical->width + shape_logical->width * (n_chars - 1)); + logical_rect->y = shape_logical->y; + logical_rect->height = shape_logical->height; + } + else + { + ink_rect->x = 0; + ink_rect->y = 0; + ink_rect->width = 0; + ink_rect->height = 0; + + logical_rect->x = 0; + logical_rect->y = 0; + logical_rect->width = 0; + logical_rect->height = 0; + } +} + + + +static void +pango2_renderer_draw_runs (Pango2Renderer *renderer, + Pango2Line *line, + GSList *runs, + const char *text, + int x, + int y) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + GSList *l; + int x_off = 0; + int glyph_string_width; + gboolean got_overall = FALSE; + Pango2Rectangle overall_rect; + + for (l = runs; l; l = l->next) + { + Pango2FontMetrics *metrics; + Pango2Run *run = l->data; + Pango2GlyphItem *glyph_item = pango2_run_get_glyph_item (run); + Pango2Item *item = pango2_run_get_item (run); + Pango2GlyphString *glyphs = pango2_run_get_glyphs (run); + Pango2Rectangle ink_rect, *ink = NULL; + Pango2Rectangle logical_rect, *logical = NULL; + ItemProperties properties; + int y_off; + + if (item->analysis.flags & PANGO2_ANALYSIS_FLAG_CENTERED_BASELINE) + logical = &logical_rect; + + pango2_renderer_prepare_run (renderer, run); + + pango2_item_get_properties (item, &properties); + + if (properties.shape) + { + ShapeData *data = (ShapeData *)properties.shape->pointer_value; + + ink = &ink_rect; + logical = &logical_rect; + pango2_shape_get_extents (glyphs->num_glyphs, + &data->ink_rect, + &data->logical_rect, + ink, + logical); + + glyph_string_width = logical->width; + } + else + { + if (priv->underline != PANGO2_LINE_STYLE_NONE || + priv->overline != PANGO2_LINE_STYLE_NONE || + priv->strikethrough != PANGO2_LINE_STYLE_NONE) + { + ink = &ink_rect; + logical = &logical_rect; + } + if (G_UNLIKELY (ink || logical)) + pango2_glyph_string_extents (glyphs, item->analysis.font, ink, logical); + if (logical) + glyph_string_width = logical_rect.width; + else + glyph_string_width = pango2_glyph_string_get_width (glyphs); + } + + priv->line_state->logical_rect_end = x + x_off + glyph_string_width; + + x_off += glyph_item->start_x_offset; + y_off = glyph_item->y_offset; + + if (item->analysis.flags & PANGO2_ANALYSIS_FLAG_CENTERED_BASELINE) + { + gboolean is_hinted = ((logical_rect.y | logical_rect.height) & (PANGO2_SCALE - 1)) == 0; + int adjustment = logical_rect.y + logical_rect.height / 2; + + if (is_hinted) + adjustment = PANGO2_UNITS_ROUND (adjustment); + + y_off += adjustment; + } + + if (priv->color_set[PANGO2_RENDER_PART_BACKGROUND]) + { + if (!got_overall) + { + pango2_line_get_extents (line, NULL, &overall_rect); + got_overall = TRUE; + } + + pango2_renderer_draw_rectangle (renderer, + PANGO2_RENDER_PART_BACKGROUND, + x + x_off, + y + overall_rect.y, + glyph_string_width, + overall_rect.height); + } + + if (properties.shape) + draw_shaped_glyphs (renderer, glyphs, properties.shape, x + x_off, y - y_off); + else + pango2_renderer_draw_run (renderer, text, run, x + x_off, y - y_off); + + if (priv->underline != PANGO2_LINE_STYLE_NONE || + priv->overline != PANGO2_LINE_STYLE_NONE || + priv->strikethrough != PANGO2_LINE_STYLE_NONE) + { + metrics = pango2_font_get_metrics (item->analysis.font, + item->analysis.language); + + if (priv->underline != PANGO2_LINE_STYLE_NONE) + add_underline (renderer, priv->line_state, metrics, + x + x_off, y - y_off, + ink, logical); + + if (priv->overline != PANGO2_LINE_STYLE_NONE) + add_overline (renderer, priv->line_state, metrics, + x + x_off, y - y_off, + ink, logical); + + if (priv->strikethrough != PANGO2_LINE_STYLE_NONE) + add_strikethrough (renderer, priv->line_state, metrics, + x + x_off, y - y_off, + ink, logical, glyphs->num_glyphs); + + pango2_font_metrics_free (metrics); + } + + if (priv->underline == PANGO2_LINE_STYLE_NONE && + priv->line_state->underline != PANGO2_LINE_STYLE_NONE) + draw_underline (renderer, priv->line_state); + + if (priv->overline == PANGO2_LINE_STYLE_NONE && + priv->line_state->overline != PANGO2_LINE_STYLE_NONE) + draw_overline (renderer, priv->line_state); + + if (priv->strikethrough == PANGO2_LINE_STYLE_NONE && + priv->line_state->strikethrough != PANGO2_LINE_STYLE_NONE) + draw_strikethrough (renderer, priv->line_state); + + x_off += glyph_string_width; + x_off += glyph_item->end_x_offset; + } +} + +/** + * pango2_renderer_draw_glyphs: + * @renderer: a `Pango2Renderer` + * @font: a `Pango2Font` + * @glyphs: a `Pango2GlyphString` + * @x: X position of left edge of baseline, in user space coordinates + * in Pango2 units. + * @y: Y position of left edge of baseline, in user space coordinates + * in Pango2 units. + * + * Draws the glyphs in @glyphs with the specified `Pango2Renderer`. + */ +void +pango2_renderer_draw_glyphs (Pango2Renderer *renderer, + Pango2Font *font, + Pango2GlyphString *glyphs, + int x, + int y) +{ + g_return_if_fail (PANGO2_IS_RENDERER (renderer)); + + pango2_renderer_activate (renderer); + + PANGO2_RENDERER_GET_CLASS (renderer)->draw_glyphs (renderer, font, glyphs, x, y); + + pango2_renderer_deactivate (renderer); +} + +static void +pango2_renderer_default_draw_glyphs (Pango2Renderer *renderer, + Pango2Font *font, + Pango2GlyphString *glyphs, + int x, + int y) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + int i; + int x_position = 0; + + for (i = 0; i < glyphs->num_glyphs; i++) + { + Pango2GlyphInfo *gi = &glyphs->glyphs[i]; + Point p; + + to_device (priv->matrix, + x + x_position + gi->geometry.x_offset, + y + gi->geometry.y_offset, + &p); + + pango2_renderer_draw_glyph (renderer, font, gi->glyph, p.x, p.y); + + x_position += gi->geometry.width; + } +} + +/** + * pango2_renderer_draw_run: + * @renderer: a `Pango2Renderer` + * @text: (nullable): the UTF-8 text that @glyph_item refers to + * @run: a `Pango2Run` + * @x: X position of left edge of baseline, in user space coordinates + * in Pango2 units + * @y: Y position of left edge of baseline, in user space coordinates + * in Pango2 units + * + * Draws the glyphs in @run with the specified `Pango2Renderer`, + * embedding the text associated with the glyphs in the output if the + * output format supports it. + * + * This is useful for rendering text in PDF. + * + * Note that this method does not handle attributes in @run. + * If you want colors, shapes and lines handled automatically according + * to those attributes, you need to use [method@Pango2.Renderer.draw_line] + * or [method@Pango2.Renderer.draw_lines]. + * + * Note that @text is the start of the text for layout, which is then + * indexed by `run->item->offset`. + * + * If @text is %NULL, this simply calls [method@Pango2.Renderer.draw_glyphs]. + * + * The default implementation of this method simply falls back to + * [method@Pango2.Renderer.draw_glyphs]. + */ +void +pango2_renderer_draw_run (Pango2Renderer *renderer, + const char *text, + Pango2Run *run, + int x, + int y) +{ + if (!text) + { + Pango2Item *item = pango2_run_get_item (run); + Pango2GlyphString *glyphs = pango2_run_get_glyphs (run); + + pango2_renderer_draw_glyphs (renderer, item->analysis.font, glyphs, x, y); + return; + } + + g_return_if_fail (PANGO2_IS_RENDERER (renderer)); + + pango2_renderer_activate (renderer); + + PANGO2_RENDERER_GET_CLASS (renderer)->draw_run (renderer, text, run, x, y); + + pango2_renderer_deactivate (renderer); +} + +static void +pango2_renderer_default_draw_run (Pango2Renderer *renderer, + const char *text G_GNUC_UNUSED, + Pango2Run *run, + int x, + int y) +{ + Pango2Item *item = pango2_run_get_item (run); + Pango2GlyphString *glyphs = pango2_run_get_glyphs (run); + + pango2_renderer_draw_glyphs (renderer, item->analysis.font, glyphs, x, y); +} + +/** + * pango2_renderer_draw_rectangle: + * @renderer: a `Pango2Renderer` + * @part: type of object this rectangle is part of + * @x: X position at which to draw rectangle, in user space coordinates + * in Pango2 units + * @y: Y position at which to draw rectangle, in user space coordinates + * in Pango2 units + * @width: width of rectangle in Pango2 units + * @height: height of rectangle in Pango2 units + * + * Draws an axis-aligned rectangle in user space coordinates with the + * specified `Pango2Renderer`. + * + * This should be called while @renderer is already active. + * Use [method@Pango2.Renderer.activate] to activate a renderer. + */ +void +pango2_renderer_draw_rectangle (Pango2Renderer *renderer, + Pango2RenderPart part, + int x, + int y, + int width, + int height) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + + g_return_if_fail (PANGO2_IS_RENDERER (renderer)); + g_return_if_fail (IS_VALID_PART (part)); + g_return_if_fail (priv->active_count > 0); + + PANGO2_RENDERER_GET_CLASS (renderer)->draw_rectangle (renderer, part, x, y, width, height); +} + +static int +compare_points (const void *a, + const void *b) +{ + const Point *pa = a; + const Point *pb = b; + + if (pa->y < pb->y) + return -1; + else if (pa->y > pb->y) + return 1; + else if (pa->x < pb->x) + return -1; + else if (pa->x > pb->x) + return 1; + else + return 0; +} + +static void +draw_rectangle (Pango2Renderer *renderer, + Pango2Matrix *matrix, + Pango2RenderPart part, + int x, + int y, + int width, + int height) +{ + Point points[4]; + + /* Convert the points to device coordinates, and sort + * in ascending Y order. (Ordering by X for ties) + */ + to_device (matrix, x, y, &points[0]); + to_device (matrix, x + width, y, &points[1]); + to_device (matrix, x, y + height, &points[2]); + to_device (matrix, x + width, y + height, &points[3]); + + qsort (points, 4, sizeof (Point), compare_points); + + /* There are essentially three cases. (There is a fourth + * case where trapezoid B is degenerate and we just have + * two triangles, but we don't need to handle it separately.) + * + * 1 2 3 + * + * ______ /\ /\ + * / / /A \ /A \ + * / B / /____\ /____\ + * /_____/ / B / \ B \ + * /_____/ \_____\ + * \ C / \ C / + * \ / \ / + * \/ \/ + */ + if (points[0].y == points[1].y) + { + /* Case 1 (pure shear) */ + pango2_renderer_draw_trapezoid (renderer, part, /* B */ + points[0].y, points[0].x, points[1].x, + points[2].y, points[2].x, points[3].x); + } + else if (points[1].x < points[2].x) + { + /* Case 2 */ + double tmp_width = ((points[2].x - points[0].x) * (points[1].y - points[0].y)) / (points[2].y - points[0].y); + double base_width = tmp_width + points[0].x - points[1].x; + + pango2_renderer_draw_trapezoid (renderer, part, /* A */ + points[0].y, points[0].x, points[0].x, + points[1].y, points[1].x, points[1].x + base_width); + pango2_renderer_draw_trapezoid (renderer, part, /* B */ + points[1].y, points[1].x, points[1].x + base_width, + points[2].y, points[2].x - base_width, points[2].x); + pango2_renderer_draw_trapezoid (renderer, part, /* C */ + points[2].y, points[2].x - base_width, points[2].x, + points[3].y, points[3].x, points[3].x); + } + else + { + /* case 3 */ + double tmp_width = ((points[0].x - points[2].x) * (points[1].y - points[0].y)) / (points[2].y - points[0].y); + double base_width = tmp_width + points[1].x - points[0].x; + + pango2_renderer_draw_trapezoid (renderer, part, /* A */ + points[0].y, points[0].x, points[0].x, + points[1].y, points[1].x - base_width, points[1].x); + pango2_renderer_draw_trapezoid (renderer, part, /* B */ + points[1].y, points[1].x - base_width, points[1].x, + points[2].y, points[2].x, points[2].x + base_width); + pango2_renderer_draw_trapezoid (renderer, part, /* C */ + points[2].y, points[2].x, points[2].x + base_width, + points[3].y, points[3].x, points[3].x); + } +} + +static void +pango2_renderer_default_draw_rectangle (Pango2Renderer *renderer, + Pango2RenderPart part, + int x, + int y, + int width, + int height) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + + draw_rectangle (renderer, priv->matrix, part, x, y, width, height); +} + +/** + * pango2_renderer_draw_styled_line: + * @renderer: a `Pango2Renderer` + * @part: type of object this rectangle is part of + * @style: the line style + * @x: X coordinate of line, in Pango2 units in user coordinate system + * @y: Y coordinate of line, in Pango2 units in user coordinate system + * @width: width of line, in Pango2 units in user coordinate system + * @height: height of line, in Pango2 units in user coordinate system + * + * Draw a line in the given style. + * + * For `PANGO2_LINE_STYLE_WAVY`, this should draw a squiggly line that + * approximately covers the given rectangle in the style of an underline + * used to indicate a spelling error. + * + * The width of the underline is rounded to an integer number + * of up/down segments and the resulting rectangle is centered + * in the original rectangle. + * + * This should be called while @renderer is already active. + * Use [method@Pango2.Renderer.activate] to activate a renderer. + */ +void +pango2_renderer_draw_styled_line (Pango2Renderer *renderer, + Pango2RenderPart part, + Pango2LineStyle style, + int x, + int y, + int width, + int height) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + + g_return_if_fail (PANGO2_IS_RENDERER (renderer)); + g_return_if_fail (IS_VALID_PART (part)); + g_return_if_fail (priv->active_count > 0); + + PANGO2_RENDERER_GET_CLASS (renderer)->draw_styled_line (renderer, part, style, x, y, width, height); +} + +/* We are drawing an error underline that looks like one of: + * + * /\ /\ /\ /\ /\ - + * / \ / \ / \ / \ / \ | + * \ \ /\ \ / / \ \ /\ \ | + * \ \/B \ \/ C / \ \/B \ \ | height = HEIGHT_SQUARES * square + * \ A \ /\ A \ / \ A \ /\ A \ | + * \ \/ \ \/ \ \/ \ \ | + * \ / \ / \ / \ / | + * \/ \/ \/ \/ - + * |---| + * unit_width = (HEIGHT_SQUARES - 1) * square + * + * To do this conveniently, we work in a coordinate system where A,B,C + * are axis aligned rectangles. (If fonts were square, the diagrams + * would be clearer) + * + * (0,0) + * /\ /\ + * / \ / \ + * /\ /\ /\ / + * / \/ \/ \/ + * / \ /\ / + * Y axis \/ \/ + * \ /\ + * \/ \ + * \ X axis + * + * Note that the long side in this coordinate system is HEIGHT_SQUARES + 1 + * units long + * + * The diagrams above are shown with HEIGHT_SQUARES an integer, but + * that is actually incidental; the value 2.5 below seems better than + * either HEIGHT_SQUARES=3 (a little long and skinny) or + * HEIGHT_SQUARES=2 (a bit short and stubby) + */ + +#define HEIGHT_SQUARES 2.5 + +static void +get_total_matrix (Pango2Matrix *total, + const Pango2Matrix *global, + int x, + int y, + int square) +{ + Pango2Matrix local; + double scale = 0.5 * square; + + /* The local matrix translates from the axis aligned coordinate system + * to the original user space coordinate system. + */ + local.xx = scale; + local.xy = - scale; + local.yx = scale; + local.yy = scale; + local.x0 = 0; + local.y0 = 0; + + *total = *global; + pango2_matrix_concat (total, &local); + + total->x0 = (global->xx * x + global->xy * y) / PANGO2_SCALE + global->x0; + total->y0 = (global->yx * x + global->yy * y) / PANGO2_SCALE + global->y0; +} + +static void +pango2_renderer_default_draw_styled_line (Pango2Renderer *renderer, + Pango2RenderPart part, + Pango2LineStyle style, + int x, + int y, + int width, + int height) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + int square; + int unit_width; + int width_units; + const Pango2Matrix identity = PANGO2_MATRIX_INIT; + const Pango2Matrix *matrix; + double dx, dx0, dy0; + Pango2Matrix total; + int i; + + if (width <= 0 || height <= 0) + return; + + if (style != PANGO2_LINE_STYLE_WAVY) + { + /* subclasses can do better */ + pango2_renderer_draw_rectangle (renderer, part, x, y, width, height); + return; + } + + square = height / HEIGHT_SQUARES; + unit_width = (HEIGHT_SQUARES - 1) * square; + width_units = (width + unit_width / 2) / unit_width; + + x += (width - width_units * unit_width) / 2; + + if (priv->matrix) + matrix = priv->matrix; + else + matrix = &identity; + + get_total_matrix (&total, matrix, x, y, square); + dx = unit_width * 2; + dx0 = (matrix->xx * dx) / PANGO2_SCALE; + dy0 = (matrix->yx * dx) / PANGO2_SCALE; + + i = (width_units - 1) / 2; + while (TRUE) + { + draw_rectangle (renderer, &total, PANGO2_RENDER_PART_UNDERLINE, /* A */ + 0, 0, + HEIGHT_SQUARES * 2 - 1, 1); + + if (i <= 0) + break; + i--; + + draw_rectangle (renderer, &total, PANGO2_RENDER_PART_UNDERLINE, /* B */ + HEIGHT_SQUARES * 2 - 2, - (HEIGHT_SQUARES * 2 - 3), + 1, HEIGHT_SQUARES * 2 - 3); + + total.x0 += dx0; + total.y0 += dy0; + } + if (width_units % 2 == 0) + { + draw_rectangle (renderer, &total, PANGO2_RENDER_PART_UNDERLINE, /* C */ + HEIGHT_SQUARES * 2 - 2, - (HEIGHT_SQUARES * 2 - 2), + 1, HEIGHT_SQUARES * 2 - 2); + } +} + +/** + * pango2_renderer_draw_trapezoid: + * @renderer: a `Pango2Renderer` + * @part: type of object this trapezoid is part of + * @y1_: Y coordinate of top of trapezoid + * @x11: X coordinate of left end of top of trapezoid + * @x21: X coordinate of right end of top of trapezoid + * @y2: Y coordinate of bottom of trapezoid + * @x12: X coordinate of left end of bottom of trapezoid + * @x22: X coordinate of right end of bottom of trapezoid + * + * Draws a trapezoid with the parallel sides aligned with the X axis + * using the given `Pango2Renderer`; coordinates are in device space. + */ +void +pango2_renderer_draw_trapezoid (Pango2Renderer *renderer, + Pango2RenderPart part, + double y1_, + double x11, + double x21, + double y2, + double x12, + double x22) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + + g_return_if_fail (PANGO2_IS_RENDERER (renderer)); + g_return_if_fail (priv->active_count > 0); + + if (PANGO2_RENDERER_GET_CLASS (renderer)->draw_trapezoid) + PANGO2_RENDERER_GET_CLASS (renderer)->draw_trapezoid (renderer, part, + y1_, x11, x21, + y2, x12, x22); +} + +/** + * pango2_renderer_draw_glyph: + * @renderer: a `Pango2Renderer` + * @font: a `Pango2Font` + * @glyph: the glyph index of a single glyph + * @x: X coordinate of left edge of baseline of glyph + * @y: Y coordinate of left edge of baseline of glyph + * + * Draws a single glyph with coordinates in device space. + */ +void +pango2_renderer_draw_glyph (Pango2Renderer *renderer, + Pango2Font *font, + Pango2Glyph glyph, + double x, + double y) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + + g_return_if_fail (PANGO2_IS_RENDERER (renderer)); + g_return_if_fail (priv->active_count > 0); + + if (glyph == PANGO2_GLYPH_EMPTY) /* glyph PANGO2_GLYPH_EMPTY never renders */ + return; + + if (PANGO2_RENDERER_GET_CLASS (renderer)->draw_glyph) + PANGO2_RENDERER_GET_CLASS (renderer)->draw_glyph (renderer, font, glyph, x, y); +} + +/** + * pango2_renderer_activate: + * @renderer: a `Pango2Renderer` + * + * Does initial setup before rendering operations on @renderer. + * + * [method@Pango2.Renderer.deactivate] should be called when done drawing. + * Calls such as [method@Pango2.Renderer.draw_lines] automatically + * activate the layout before drawing on it. + * + * Calls to [method@Pango2.Renderer.activate] and + * [method@Pango2.Renderer.deactivate] can be nested and the + * renderer will only be initialized and deinitialized once. + */ +void +pango2_renderer_activate (Pango2Renderer *renderer) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + + g_return_if_fail (PANGO2_IS_RENDERER (renderer)); + + priv->active_count++; + if (priv->active_count == 1) + { + if (PANGO2_RENDERER_GET_CLASS (renderer)->begin) + PANGO2_RENDERER_GET_CLASS (renderer)->begin (renderer); + } +} + +/** + * pango2_renderer_deactivate: + * @renderer: a `Pango2Renderer` + * + * Cleans up after rendering operations on @renderer. + * + * See docs for [method@Pango2.Renderer.activate]. + */ +void +pango2_renderer_deactivate (Pango2Renderer *renderer) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + + g_return_if_fail (PANGO2_IS_RENDERER (renderer)); + g_return_if_fail (priv->active_count > 0); + + if (priv->active_count == 1) + { + if (PANGO2_RENDERER_GET_CLASS (renderer)->end) + PANGO2_RENDERER_GET_CLASS (renderer)->end (renderer); + } + priv->active_count--; +} + +/** + * pango2_renderer_set_color: + * @renderer: a `Pango2Renderer` + * @part: the part to change the color of + * @color: (nullable): the new color or %NULL to unset the current color + * + * Sets the color for part of the rendering. + */ +void +pango2_renderer_set_color (Pango2Renderer *renderer, + Pango2RenderPart part, + const Pango2Color *color) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + + g_return_if_fail (PANGO2_IS_RENDERER (renderer)); + g_return_if_fail (IS_VALID_PART (part)); + + if ((!color && !priv->color_set[part]) || + (color && priv->color_set[part] && + pango2_color_equal (color, &priv->color[part]))) + return; + + pango2_renderer_part_changed (renderer, part); + + if (color) + { + priv->color_set[part] = TRUE; + priv->color[part] = *color; + } + else + { + priv->color_set[part] = FALSE; + } +} + +/** + * pango2_renderer_get_color: + * @renderer: a `Pango2Renderer` + * @part: the part to get the color for + * + * Gets the current rendering color for the specified part. + * + * Return value: (transfer none) (nullable): the color for the + * specified part, or %NULL if it hasn't been set and should be + * inherited from the environment. + */ +Pango2Color * +pango2_renderer_get_color (Pango2Renderer *renderer, + Pango2RenderPart part) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + + g_return_val_if_fail (PANGO2_IS_RENDERER (renderer), NULL); + g_return_val_if_fail (IS_VALID_PART (part), NULL); + + if (priv->color_set[part]) + return &priv->color[part]; + else + return NULL; +} + +/** + * pango2_renderer_part_changed: + * @renderer: a `Pango2Renderer` + * @part: the part for which rendering has changed. + * + * Informs Pango2 that the way that the rendering is done + * for @part has changed. + * + * This should be called if the rendering changes in a way that would + * prevent multiple pieces being joined together into one drawing call. + * For instance, if a subclass of `Pango2Renderer` was to add a stipple + * option for drawing underlines, it needs to call + * + * ``` + * pango2_renderer_part_changed (render, PANGO2_RENDER_PART_UNDERLINE); + * ``` + * + * When the stipple changes or underlines with different stipples + * might be joined together. Pango2 automatically calls this for + * changes to colors. (See [method@Pango2.Renderer.set_color]) + */ +void +pango2_renderer_part_changed (Pango2Renderer *renderer, + Pango2RenderPart part) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + + g_return_if_fail (PANGO2_IS_RENDERER (renderer)); + g_return_if_fail (IS_VALID_PART (part)); + g_return_if_fail (priv->active_count > 0); + + handle_line_state_change (renderer, part); + + if (PANGO2_RENDERER_GET_CLASS (renderer)->part_changed) + PANGO2_RENDERER_GET_CLASS (renderer)->part_changed (renderer, part); +} + +/** + * pango2_renderer_prepare_run: + * @renderer: a `Pango2Renderer` + * @run: a `Pango2Run` + * + * Set up the state of the `Pango2Renderer` for rendering @run. + */ +static void +pango2_renderer_prepare_run (Pango2Renderer *renderer, + Pango2Run *run) +{ + g_return_if_fail (PANGO2_IS_RENDERER (renderer)); + + PANGO2_RENDERER_GET_CLASS (renderer)->prepare_run (renderer, run); +} + +static void +pango2_renderer_default_prepare_run (Pango2Renderer *renderer, + Pango2Run *run) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + Pango2Color *fg_color = NULL; + Pango2Color *bg_color = NULL; + Pango2Color *underline_color = NULL; + Pango2Color *overline_color = NULL; + Pango2Color *strikethrough_color = NULL; + GSList *l; + Pango2GlyphItem *glyph_item; + + glyph_item = pango2_run_get_glyph_item (run); + + priv->underline = PANGO2_LINE_STYLE_NONE; + priv->underline_position = PANGO2_UNDERLINE_POSITION_NORMAL; + priv->overline = PANGO2_LINE_STYLE_NONE; + priv->strikethrough = PANGO2_LINE_STYLE_NONE; + + for (l = glyph_item->item->analysis.extra_attrs; l; l = l->next) + { + Pango2Attribute *attr = l->data; + + switch ((int) attr->type) + { + case PANGO2_ATTR_UNDERLINE: + priv->underline = attr->int_value; + break; + + case PANGO2_ATTR_UNDERLINE_POSITION: + priv->underline_position = attr->int_value; + break; + + case PANGO2_ATTR_OVERLINE: + priv->overline = attr->int_value; + break; + + case PANGO2_ATTR_STRIKETHROUGH: + priv->strikethrough = attr->int_value; + break; + + case PANGO2_ATTR_FOREGROUND: + fg_color = &attr->color_value; + break; + + case PANGO2_ATTR_BACKGROUND: + bg_color = &attr->color_value; + break; + + case PANGO2_ATTR_UNDERLINE_COLOR: + underline_color = &attr->color_value; + break; + + case PANGO2_ATTR_OVERLINE_COLOR: + overline_color = &attr->color_value; + break; + + case PANGO2_ATTR_STRIKETHROUGH_COLOR: + strikethrough_color = &attr->color_value; + break; + + default: + break; + } + } + + if (!underline_color) + underline_color = fg_color; + + if (!overline_color) + overline_color = fg_color; + + if (!strikethrough_color) + strikethrough_color = fg_color; + + pango2_renderer_set_color (renderer, PANGO2_RENDER_PART_FOREGROUND, fg_color); + pango2_renderer_set_color (renderer, PANGO2_RENDER_PART_BACKGROUND, bg_color); + pango2_renderer_set_color (renderer, PANGO2_RENDER_PART_UNDERLINE, underline_color); + pango2_renderer_set_color (renderer, PANGO2_RENDER_PART_STRIKETHROUGH, strikethrough_color); + pango2_renderer_set_color (renderer, PANGO2_RENDER_PART_OVERLINE, overline_color); +} + +/** + * pango2_renderer_set_matrix: + * @renderer: a `Pango2Renderer` + * @matrix: (nullable): a `Pango2Matrix`, or %NULL to unset any existing matrix + * (No matrix set is the same as setting the identity matrix.) + * + * Sets the transformation matrix that will be applied when rendering. + */ +void +pango2_renderer_set_matrix (Pango2Renderer *renderer, + const Pango2Matrix *matrix) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + + g_return_if_fail (PANGO2_IS_RENDERER (renderer)); + + pango2_matrix_free (priv->matrix); + priv->matrix = pango2_matrix_copy (matrix); +} + +/** + * pango2_renderer_get_matrix: + * @renderer: a `Pango2Renderer` + * + * Gets the transformation matrix that will be applied when + * rendering. + * + * See [method@Pango2.Renderer.set_matrix]. + * + * Return value: (nullable): the matrix, or %NULL if no matrix has + * been set (which is the same as the identity matrix). The returned + * matrix is owned by Pango2 and must not be modified or freed. + */ +const Pango2Matrix * +pango2_renderer_get_matrix (Pango2Renderer *renderer) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + + g_return_val_if_fail (PANGO2_IS_RENDERER (renderer), NULL); + + return priv->matrix; +} + +/** + * pango2_renderer_get_lines: + * @renderer: a `Pango2Renderer` + * + * Gets the `Pango2Lines` currently being rendered using @renderer. + * + * Calling this function only makes sense from inside a subclass's + * methods, like in its draw_shape vfunc, for example. + * + * The returned layout should not be modified while still being + * rendered. + * + * Return value: (transfer none) (nullable): the `Pango2Lines`, or + * %NULL if no layout is being rendered using @renderer at this time. + */ +Pango2Lines * +pango2_renderer_get_lines (Pango2Renderer *renderer) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + + return priv->lines; +} + +/** + * pango2_renderer_get_layout_line: + * @renderer: a `Pango2Renderer` + * + * Gets the line currently being rendered using @renderer. + * + * Calling this function only makes sense from inside a subclass's + * methods, like in its draw_shape vfunc, for example. + * + * The returned line should not be modified while still being + * rendered. + * + * Return value: (transfer none) (nullable): the line, or %NULL + * if no layout line is being rendered using @renderer at this time. + */ +Pango2Line * +pango2_renderer_get_layout_line (Pango2Renderer *renderer) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + + return priv->line; +} + +/** + * pango2_renderer_get_context: + * @renderer: a `Pango2Renderer` + * + * Gets the current context in which @renderer operates. + * + * Returns: (nullable) (transfer none): the `Pango2Context` + */ +Pango2Context * +pango2_renderer_get_context (Pango2Renderer *renderer) +{ + Pango2RendererPrivate *priv = pango2_renderer_get_instance_private (renderer); + + return priv->context; +} diff --git a/pango2/pango-renderer.h b/pango2/pango-renderer.h new file mode 100644 index 00000000..e641bb3e --- /dev/null +++ b/pango2/pango-renderer.h @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2004, Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-layout.h> +#include <pango2/pango-lines.h> +#include <pango2/pango-glyph.h> + +G_BEGIN_DECLS + +#define PANGO2_TYPE_RENDERER (pango2_renderer_get_type()) +#define PANGO2_RENDERER(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), PANGO2_TYPE_RENDERER, Pango2Renderer)) +#define PANGO2_IS_RENDERER(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), PANGO2_TYPE_RENDERER)) +#define PANGO2_RENDERER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), PANGO2_TYPE_RENDERER, Pango2RendererClass)) +#define PANGO2_IS_RENDERER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), PANGO2_TYPE_RENDERER)) +#define PANGO2_RENDERER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), PANGO2_TYPE_RENDERER, Pango2RendererClass)) + +typedef struct _Pango2Renderer Pango2Renderer; +typedef struct _Pango2RendererClass Pango2RendererClass; + +/** + * Pango2RenderPart: + * @PANGO2_RENDER_PART_FOREGROUND: the text itself + * @PANGO2_RENDER_PART_BACKGROUND: the area behind the text + * @PANGO2_RENDER_PART_UNDERLINE: underlines + * @PANGO2_RENDER_PART_STRIKETHROUGH: strikethrough lines + * @PANGO2_RENDER_PART_OVERLINE: overlines + * + * `Pango2RenderPart` defines different items to render for such + * purposes as setting colors. + */ +/* When extending, note N_RENDER_PARTS #define in pango-renderer.c */ +typedef enum +{ + PANGO2_RENDER_PART_FOREGROUND, + PANGO2_RENDER_PART_BACKGROUND, + PANGO2_RENDER_PART_UNDERLINE, + PANGO2_RENDER_PART_STRIKETHROUGH, + PANGO2_RENDER_PART_OVERLINE +} Pango2RenderPart; + +/** + * Pango2Renderer: + * + * `Pango2Renderer` is a base class for objects that can render text + * provided as `Pango2GlyphString` or `Pango2Layout`. + * + * By subclassing `Pango2Renderer` and overriding operations such as + * @draw_glyphs and @draw_rectangle, renderers for particular font + * backends and destinations can be created. + */ +struct _Pango2Renderer +{ + GObject parent_instance; +}; + +/** + * Pango2RendererClass: + * @draw_glyphs: draws a `Pango2GlyphString` + * @draw_rectangle: draws a rectangle + * @draw_line: draws a line in the given style that approximately + * covers the given rectangle + * @draw_trapezoid: draws a trapezoidal filled area + * @draw_glyph: draws a single glyph + * @part_changed: do renderer specific processing when rendering + * attributes change + * @begin: Do renderer-specific initialization before drawing + * @end: Do renderer-specific cleanup after drawing + * @prepare_run: updates the renderer for a new run + * @draw_glyph_item: draws a `Pango2GlyphItem` + * + * Class structure for `Pango2Renderer`. + * + * The following vfuncs take user space coordinates in Pango2 units + * and have default implementations: + * - draw_glyphs + * - draw_rectangle + * - draw_styled_line + * - draw_shape + * - draw_glyph_item + * + * The default draw_shape implementation draws nothing. + * + * The following vfuncs take device space coordinates as doubles + * and must be implemented: + * - draw_trapezoid + * - draw_glyph + */ +struct _Pango2RendererClass +{ + /*< private >*/ + GObjectClass parent_class; + + /*< public >*/ + + void (* draw_glyphs) (Pango2Renderer *renderer, + Pango2Font *font, + Pango2GlyphString *glyphs, + int x, + int y); + void (* draw_rectangle) (Pango2Renderer *renderer, + Pango2RenderPart part, + int x, + int y, + int width, + int height); + void (* draw_styled_line) (Pango2Renderer *renderer, + Pango2RenderPart part, + Pango2LineStyle style, + int x, + int y, + int width, + int height); + void (* draw_line) (Pango2Renderer *renderer, + Pango2LineStyle style, + int x, + int y, + int width, + int height); + void (* draw_shape) (Pango2Renderer *renderer, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect, + gpointer data, + int x, + int y); + void (* draw_trapezoid) (Pango2Renderer *renderer, + Pango2RenderPart part, + double y1_, + double x11, + double x21, + double y2, + double x12, + double x22); + void (* draw_glyph) (Pango2Renderer *renderer, + Pango2Font *font, + Pango2Glyph glyph, + double x, + double y); + + void (* part_changed) (Pango2Renderer *renderer, + Pango2RenderPart part); + + void (* begin) (Pango2Renderer *renderer); + void (* end) (Pango2Renderer *renderer); + + void (* prepare_run) (Pango2Renderer *renderer, + Pango2Run *run); + + void (* draw_run) (Pango2Renderer *renderer, + const char *text, + Pango2Run *run, + int x, + int y); + + /*< private >*/ + + /* Padding for future expansion */ + gpointer _pango2_reserved[8]; +}; + +PANGO2_AVAILABLE_IN_ALL +GType pango2_renderer_get_type (void) G_GNUC_CONST; + +PANGO2_AVAILABLE_IN_ALL +void pango2_renderer_draw_lines (Pango2Renderer *renderer, + Pango2Lines *lines, + int x, + int y); + +PANGO2_AVAILABLE_IN_ALL +void pango2_renderer_draw_line (Pango2Renderer *renderer, + Pango2Line *line, + int x, + int y); +PANGO2_AVAILABLE_IN_ALL +void pango2_renderer_draw_glyphs (Pango2Renderer *renderer, + Pango2Font *font, + Pango2GlyphString *glyphs, + int x, + int y); +PANGO2_AVAILABLE_IN_ALL +void pango2_renderer_draw_run (Pango2Renderer *renderer, + const char *text, + Pango2Run *run, + int x, + int y); +PANGO2_AVAILABLE_IN_ALL +void pango2_renderer_draw_rectangle (Pango2Renderer *renderer, + Pango2RenderPart part, + int x, + int y, + int width, + int height); +PANGO2_AVAILABLE_IN_ALL +void pango2_renderer_draw_styled_line (Pango2Renderer *renderer, + Pango2RenderPart part, + Pango2LineStyle style, + int x, + int y, + int width, + int height); +PANGO2_AVAILABLE_IN_ALL +void pango2_renderer_draw_error_underline (Pango2Renderer *renderer, + int x, + int y, + int width, + int height); +PANGO2_AVAILABLE_IN_ALL +void pango2_renderer_draw_trapezoid (Pango2Renderer *renderer, + Pango2RenderPart part, + double y1_, + double x11, + double x21, + double y2, + double x12, + double x22); +PANGO2_AVAILABLE_IN_ALL +void pango2_renderer_draw_glyph (Pango2Renderer *renderer, + Pango2Font *font, + Pango2Glyph glyph, + double x, + double y); + +PANGO2_AVAILABLE_IN_ALL +void pango2_renderer_activate (Pango2Renderer *renderer); +PANGO2_AVAILABLE_IN_ALL +void pango2_renderer_deactivate (Pango2Renderer *renderer); + +PANGO2_AVAILABLE_IN_ALL +void pango2_renderer_part_changed (Pango2Renderer *renderer, + Pango2RenderPart part); + +PANGO2_AVAILABLE_IN_ALL +void pango2_renderer_set_color (Pango2Renderer *renderer, + Pango2RenderPart part, + const Pango2Color *color); +PANGO2_AVAILABLE_IN_ALL +Pango2Color * pango2_renderer_get_color (Pango2Renderer *renderer, + Pango2RenderPart part); + +PANGO2_AVAILABLE_IN_ALL +void pango2_renderer_set_matrix (Pango2Renderer *renderer, + const Pango2Matrix *matrix); +PANGO2_AVAILABLE_IN_ALL +const Pango2Matrix * pango2_renderer_get_matrix (Pango2Renderer *renderer); + +PANGO2_AVAILABLE_IN_ALL +Pango2Lines * pango2_renderer_get_lines (Pango2Renderer *renderer); + +PANGO2_AVAILABLE_IN_ALL +Pango2Line * pango2_renderer_get_layout_line (Pango2Renderer *renderer); + +PANGO2_AVAILABLE_IN_ALL +Pango2Context * pango2_renderer_get_context (Pango2Renderer *renderer); + +G_END_DECLS diff --git a/pango2/pango-run-private.h b/pango2/pango-run-private.h new file mode 100644 index 00000000..2e84bdfe --- /dev/null +++ b/pango2/pango-run-private.h @@ -0,0 +1,38 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "config.h" + +#include "pango-run.h" +#include "pango-glyph-item-private.h" +#include "pango-item-private.h" + + +struct _Pango2Run +{ + Pango2GlyphItem glyph_item; +}; + +static inline Pango2GlyphItem * +pango2_run_get_glyph_item (Pango2Run *run) +{ + return &run->glyph_item; +} diff --git a/pango2/pango-run.c b/pango2/pango-run.c new file mode 100644 index 00000000..8c36f138 --- /dev/null +++ b/pango2/pango-run.c @@ -0,0 +1,271 @@ +#include "config.h" + +#include "pango-run-private.h" +#include "pango-attr-private.h" +#include "pango-item-private.h" +#include "pango-impl-utils.h" +#include "pango-font-metrics-private.h" +#include "pango-attributes-private.h" + +#include <math.h> + +/** + * Pango2Run: + * + * A `Pango2Run` represents a single run within a `Pango2Line`. + * + * A run is a range of text with uniform script, font and attributes that + * is shaped as a unit. + * + * Script, font and attributes of a run can be accessed via + * [method@Pango2.Run.get_item]. The glyphs that result from shaping + * the text of the run can be obtained via [method@Pango2.Run.get_glyphs]. + */ + +/** + * pango2_run_get_item: + * @run: a `Pango2Run` + * + * Gets the `Pango2Item` for the run. + * + * Returns: (transfer none): the `Pango2Item` of @run + */ +Pango2Item * +pango2_run_get_item (Pango2Run *run) +{ + return run->glyph_item.item; +} + +/** + * pango2_run_get_glyphs: + * @run: a `Pango2Run` + * + * Gets the `Pango2GlyphString` for the run. + * + * Returns: (transfer none): the `Pango2GlyphString` of @run + */ +Pango2GlyphString * +pango2_run_get_glyphs (Pango2Run *run) +{ + return run->glyph_item.glyphs; +} + +static void +pango2_shape_get_extents (int n_chars, + Pango2Attribute *attr, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect) +{ + if (n_chars > 0) + { + ShapeData *data = (ShapeData *)attr->pointer_value; + Pango2Rectangle *shape_ink = &data->ink_rect; + Pango2Rectangle *shape_logical = &data->logical_rect; + + if (ink_rect) + { + ink_rect->x = MIN (shape_ink->x, shape_ink->x + shape_logical->width * (n_chars - 1)); + ink_rect->width = MAX (shape_ink->width, shape_ink->width + shape_logical->width * (n_chars - 1)); + ink_rect->y = shape_ink->y; + ink_rect->height = shape_ink->height; + } + + if (logical_rect) + { + logical_rect->x = MIN (shape_logical->x, shape_logical->x + shape_logical->width * (n_chars - 1)); + logical_rect->width = MAX (shape_logical->width, shape_logical->width + shape_logical->width * (n_chars - 1)); + logical_rect->y = shape_logical->y; + logical_rect->height = shape_logical->height; + } + } + else + { + if (ink_rect) + { + ink_rect->x = 0; + ink_rect->y = 0; + ink_rect->width = 0; + ink_rect->height = 0; + } + + if (logical_rect) + { + logical_rect->x = 0; + logical_rect->y = 0; + logical_rect->width = 0; + logical_rect->height = 0; + } + } +} + +/** + * pango2_run_get_extents: + * @run: a `Pango2Run` + * @trim: `Pango2LeadingTrim` flags + * @ink_rect: (out caller-allocates) (optional): return location + * for the ink extents + * @logical_rect: (out caller-allocates) (optional): return location + * for the logical extents + * + * Gets the extents of a `Pango2Run`. + * + * The @trim flags specify if line-height attributes are taken + * into consideration for determining the logical height. See the + * [CSS inline layout](https://www.w3.org/TR/css-inline-3/#inline-height) + * specification for details. + */ +void +pango2_run_get_extents (Pango2Run *run, + Pango2LeadingTrim trim, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect) +{ + Pango2GlyphItem *glyph_item = &run->glyph_item; + ItemProperties properties; + gboolean has_underline; + gboolean has_overline; + gboolean has_strikethrough; + Pango2Rectangle logical; + Pango2FontMetrics *metrics = NULL; + int y_offset; + + pango2_item_get_properties (glyph_item->item, &properties); + + has_underline = properties.uline_style != PANGO2_LINE_STYLE_NONE; + has_overline = properties.oline_style != PANGO2_LINE_STYLE_NONE; + has_strikethrough = properties.strikethrough_style != PANGO2_LINE_STYLE_NONE; + + if (!logical_rect && (glyph_item->item->analysis.flags & PANGO2_ANALYSIS_FLAG_CENTERED_BASELINE)) + logical_rect = &logical; + + if (!logical_rect && (has_underline || has_overline || has_strikethrough)) + logical_rect = &logical; + + if (properties.shape) + pango2_shape_get_extents (glyph_item->item->num_chars, properties.shape, + ink_rect, logical_rect); + else + pango2_glyph_string_extents (glyph_item->glyphs, glyph_item->item->analysis.font, + ink_rect, logical_rect); + + if (ink_rect && (has_underline || has_overline || has_strikethrough)) + { + int underline_thickness; + int underline_position; + int strikethrough_thickness; + int strikethrough_position; + int new_pos; + + if (!metrics) + metrics = pango2_font_get_metrics (glyph_item->item->analysis.font, + glyph_item->item->analysis.language); + + underline_thickness = pango2_font_metrics_get_underline_thickness (metrics); + underline_position = pango2_font_metrics_get_underline_position (metrics); + strikethrough_thickness = pango2_font_metrics_get_strikethrough_thickness (metrics); + strikethrough_position = pango2_font_metrics_get_strikethrough_position (metrics); + + /* the underline/strikethrough takes x, width of logical_rect. + * Reflect that into ink_rect. + */ + new_pos = MIN (ink_rect->x, logical_rect->x); + ink_rect->width = MAX (ink_rect->x + ink_rect->width, logical_rect->x + logical_rect->width) - new_pos; + ink_rect->x = new_pos; + + /* We should better handle the case of height==0 in the following cases. + * If ink_rect->height == 0, we should adjust ink_rect->y appropriately. + */ + + if (has_strikethrough) + { + if (ink_rect->height == 0) + { + ink_rect->height = strikethrough_thickness; + ink_rect->y = -strikethrough_position; + } + } + + if (properties.oline_style == PANGO2_LINE_STYLE_SOLID || + properties.oline_style == PANGO2_LINE_STYLE_DASHED || + properties.oline_style == PANGO2_LINE_STYLE_DOTTED) + { + ink_rect->y -= underline_thickness; + ink_rect->height += underline_thickness; + } + else if (properties.oline_style == PANGO2_LINE_STYLE_DOUBLE || + properties.oline_style == PANGO2_LINE_STYLE_WAVY) + { + ink_rect->y -= 3 * underline_thickness; + ink_rect->height += 3 * underline_thickness; + } + + if (properties.uline_style == PANGO2_LINE_STYLE_SOLID || + properties.uline_style == PANGO2_LINE_STYLE_DASHED || + properties.uline_style == PANGO2_LINE_STYLE_DOTTED) + { + if (properties.uline_position == PANGO2_UNDERLINE_POSITION_UNDER) + ink_rect->height += 2 * underline_thickness; + else + ink_rect->height = MAX (ink_rect->height, + underline_thickness - underline_position - ink_rect->y); + } + else if (properties.uline_style == PANGO2_LINE_STYLE_DOUBLE || + properties.uline_style == PANGO2_LINE_STYLE_WAVY) + { + if (properties.uline_position == PANGO2_UNDERLINE_POSITION_UNDER) + ink_rect->height += 4 * underline_thickness; + else + ink_rect->height = MAX (ink_rect->height, + 3 * underline_thickness - underline_position - ink_rect->y); + } + } + + y_offset = glyph_item->y_offset; + + if (glyph_item->item->analysis.flags & PANGO2_ANALYSIS_FLAG_CENTERED_BASELINE) + { + gboolean is_hinted = (logical_rect->y & logical_rect->height & (PANGO2_SCALE - 1)) == 0; + int adjustment = logical_rect->y + logical_rect->height / 2; + + if (is_hinted) + adjustment = PANGO2_UNITS_ROUND (adjustment); + + y_offset += adjustment; + } + + if (ink_rect) + ink_rect->y -= y_offset; + + if (logical_rect) + logical_rect->y -= y_offset; + + if (logical_rect && trim != PANGO2_LEADING_TRIM_BOTH) + { + int leading; + + if (properties.absolute_line_height != 0 || properties.line_height != 0.0) + { + int line_height; + + line_height = MAX (properties.absolute_line_height, ceilf (properties.line_height * logical_rect->height)); + leading = (line_height - logical_rect->height); + } + else + { + /* line-height 'normal' in the CSS inline layout spec */ + if (!metrics) + metrics = pango2_font_get_metrics (glyph_item->item->analysis.font, + glyph_item->item->analysis.language); + leading = MAX (metrics->height - (metrics->ascent + metrics->descent) + properties.line_spacing, 0); + } + if ((trim & PANGO2_LEADING_TRIM_START) == 0) + logical_rect->y -= leading / 2; + if (trim == PANGO2_LEADING_TRIM_NONE) + logical_rect->height += leading; + else + logical_rect->height += (leading - leading / 2); + } + + if (metrics) + pango2_font_metrics_free (metrics); +} diff --git a/pango2/pango-run.h b/pango2/pango-run.h new file mode 100644 index 00000000..26760cb3 --- /dev/null +++ b/pango2/pango-run.h @@ -0,0 +1,37 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <glib-object.h> +#include <pango2/pango-types.h> +#include <pango2/pango-item.h> +#include <pango2/pango-glyph.h> + +PANGO2_AVAILABLE_IN_ALL +Pango2Item * pango2_run_get_item (Pango2Run *run); + +PANGO2_AVAILABLE_IN_ALL +Pango2GlyphString * pango2_run_get_glyphs (Pango2Run *run); + +PANGO2_AVAILABLE_IN_ALL +void pango2_run_get_extents (Pango2Run *run, + Pango2LeadingTrim trim, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect); diff --git a/pango2/pango-script-lang-table.h b/pango2/pango-script-lang-table.h new file mode 100644 index 00000000..1332b2d6 --- /dev/null +++ b/pango2/pango-script-lang-table.h @@ -0,0 +1,261 @@ +/* pango-script-lang-table.h: + * + * Generated by ../tools/gen-script-for-lang.c + * Date: 2019-06-06 + * Source: fontconfig-2.13.1 + * + * Do not edit. + */ +typedef struct _Pango2ScriptForLang { + const char lang[9]; + GUnicodeScript scripts[3]; +} Pango2ScriptForLang; + +static const Pango2ScriptForLang pango2_script_for_lang[] = { + { "aa", { G_UNICODE_SCRIPT_LATIN/*62*/ } }, + { "ab", { G_UNICODE_SCRIPT_CYRILLIC/*90*/ } }, + { "af", { G_UNICODE_SCRIPT_LATIN/*69*/ } }, + { "ak", { G_UNICODE_SCRIPT_LATIN/*70*/ } }, + { "am", { G_UNICODE_SCRIPT_ETHIOPIC/*264*/ } }, + { "an", { G_UNICODE_SCRIPT_LATIN/*66*/ } }, + { "ar", { G_UNICODE_SCRIPT_ARABIC/*36*/ } }, + { "as", { G_UNICODE_SCRIPT_BENGALI/*64*/ } }, + { "ast", { G_UNICODE_SCRIPT_LATIN/*70*/ } }, + { "av", { G_UNICODE_SCRIPT_CYRILLIC/*67*/ } }, + { "ay", { G_UNICODE_SCRIPT_LATIN/*60*/ } }, + { "az-az", { G_UNICODE_SCRIPT_LATIN/*66*/ } }, + { "az-ir", { G_UNICODE_SCRIPT_ARABIC/*38*/ } }, + { "ba", { G_UNICODE_SCRIPT_CYRILLIC/*82*/ } }, + { "be", { G_UNICODE_SCRIPT_CYRILLIC/*68*/ } }, + { "ber-dz", { G_UNICODE_SCRIPT_LATIN/*70*/ } }, + { "ber-ma", { G_UNICODE_SCRIPT_TIFINAGH/*32*/ } }, + { "bg", { G_UNICODE_SCRIPT_CYRILLIC/*60*/ } }, + { "bh", { G_UNICODE_SCRIPT_DEVANAGARI/*68*/ } }, + { "bho", { G_UNICODE_SCRIPT_DEVANAGARI/*68*/ } }, + { "bi", { G_UNICODE_SCRIPT_LATIN/*58*/ } }, + { "bin", { G_UNICODE_SCRIPT_LATIN/*76*/ } }, + { "bm", { G_UNICODE_SCRIPT_LATIN/*60*/ } }, + { "bn", { G_UNICODE_SCRIPT_BENGALI/*63*/ } }, + { "bo", { G_UNICODE_SCRIPT_TIBETAN/*95*/ } }, + { "br", { G_UNICODE_SCRIPT_LATIN/*64*/ } }, + { "brx", { G_UNICODE_SCRIPT_DEVANAGARI/*78*/ } }, + { "bs", { G_UNICODE_SCRIPT_LATIN/*62*/ } }, + { "bua", { G_UNICODE_SCRIPT_CYRILLIC/*70*/ } }, + { "byn", { G_UNICODE_SCRIPT_ETHIOPIC/*255*/ } }, + { "ca", { G_UNICODE_SCRIPT_LATIN/*74*/ } }, + { "ce", { G_UNICODE_SCRIPT_CYRILLIC/*67*/ } }, + { "ch", { G_UNICODE_SCRIPT_LATIN/*58*/ } }, + { "chm", { G_UNICODE_SCRIPT_CYRILLIC/*76*/ } }, + { "chr", { G_UNICODE_SCRIPT_CHEROKEE/*85*/ } }, + { "co", { G_UNICODE_SCRIPT_LATIN/*84*/ } }, + { "crh", { G_UNICODE_SCRIPT_LATIN/*68*/ } }, + { "cs", { G_UNICODE_SCRIPT_LATIN/*82*/ } }, + { "csb", { G_UNICODE_SCRIPT_LATIN/*74*/ } }, + { "cu", { G_UNICODE_SCRIPT_CYRILLIC/*103*/ } }, + { "cv", { G_UNICODE_SCRIPT_CYRILLIC/*72*/, G_UNICODE_SCRIPT_LATIN/*2*/ } }, + { "cy", { G_UNICODE_SCRIPT_LATIN/*78*/ } }, + { "da", { G_UNICODE_SCRIPT_LATIN/*70*/ } }, + { "de", { G_UNICODE_SCRIPT_LATIN/*59*/ } }, + { "doi", { G_UNICODE_SCRIPT_DEVANAGARI/*81*/ } }, + { "dv", { G_UNICODE_SCRIPT_THAANA/*49*/ } }, + { "dz", { G_UNICODE_SCRIPT_TIBETAN/*95*/ } }, + { "ee", { G_UNICODE_SCRIPT_LATIN/*96*/ } }, + { "el", { G_UNICODE_SCRIPT_GREEK/*69*/ } }, + { "en", { G_UNICODE_SCRIPT_LATIN/*72*/ } }, + { "eo", { G_UNICODE_SCRIPT_LATIN/*64*/ } }, + { "es", { G_UNICODE_SCRIPT_LATIN/*66*/ } }, + { "et", { G_UNICODE_SCRIPT_LATIN/*64*/ } }, + { "eu", { G_UNICODE_SCRIPT_LATIN/*56*/ } }, + { "fa", { G_UNICODE_SCRIPT_ARABIC/*38*/ } }, + { "fat", { G_UNICODE_SCRIPT_LATIN/*70*/ } }, + { "ff", { G_UNICODE_SCRIPT_LATIN/*62*/ } }, + { "fi", { G_UNICODE_SCRIPT_LATIN/*62*/ } }, + { "fil", { G_UNICODE_SCRIPT_LATIN/*84*/ } }, + { "fj", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "fo", { G_UNICODE_SCRIPT_LATIN/*68*/ } }, + { "fr", { G_UNICODE_SCRIPT_LATIN/*84*/ } }, + { "fur", { G_UNICODE_SCRIPT_LATIN/*66*/ } }, + { "fy", { G_UNICODE_SCRIPT_LATIN/*75*/ } }, + { "ga", { G_UNICODE_SCRIPT_LATIN/*80*/ } }, + { "gd", { G_UNICODE_SCRIPT_LATIN/*70*/ } }, + { "gez", { G_UNICODE_SCRIPT_ETHIOPIC/*218*/ } }, + { "gl", { G_UNICODE_SCRIPT_LATIN/*66*/ } }, + { "gn", { G_UNICODE_SCRIPT_LATIN/*70*/ } }, + { "gu", { G_UNICODE_SCRIPT_GUJARATI/*68*/ } }, + { "gv", { G_UNICODE_SCRIPT_LATIN/*54*/ } }, + { "ha", { G_UNICODE_SCRIPT_LATIN/*60*/ } }, + { "haw", { G_UNICODE_SCRIPT_LATIN/*62*/ } }, + { "he", { G_UNICODE_SCRIPT_HEBREW/*27*/ } }, + { "hi", { G_UNICODE_SCRIPT_DEVANAGARI/*68*/ } }, + { "hne", { G_UNICODE_SCRIPT_DEVANAGARI/*68*/ } }, + { "ho", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "hr", { G_UNICODE_SCRIPT_LATIN/*62*/ } }, + { "hsb", { G_UNICODE_SCRIPT_LATIN/*72*/ } }, + { "ht", { G_UNICODE_SCRIPT_LATIN/*56*/ } }, + { "hu", { G_UNICODE_SCRIPT_LATIN/*70*/ } }, + { "hy", { G_UNICODE_SCRIPT_ARMENIAN/*77*/ } }, + { "hz", { G_UNICODE_SCRIPT_LATIN/*56*/ } }, + { "ia", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "id", { G_UNICODE_SCRIPT_LATIN/*54*/ } }, + { "ie", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "ig", { G_UNICODE_SCRIPT_LATIN/*58*/ } }, + { "ii", { G_UNICODE_SCRIPT_YI/*1165*/ } }, + { "ik", { G_UNICODE_SCRIPT_CYRILLIC/*68*/ } }, + { "io", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "is", { G_UNICODE_SCRIPT_LATIN/*70*/ } }, + { "it", { G_UNICODE_SCRIPT_LATIN/*72*/ } }, + { "iu", { G_UNICODE_SCRIPT_CANADIAN_ABORIGINAL/*161*/ } }, + { "ja", { G_UNICODE_SCRIPT_HAN/*2134*/, G_UNICODE_SCRIPT_KATAKANA/*88*/, G_UNICODE_SCRIPT_HIRAGANA/*85*/ } }, + { "jv", { G_UNICODE_SCRIPT_LATIN/*56*/ } }, + { "ka", { G_UNICODE_SCRIPT_GEORGIAN/*33*/ } }, + { "kaa", { G_UNICODE_SCRIPT_CYRILLIC/*78*/ } }, + { "kab", { G_UNICODE_SCRIPT_LATIN/*70*/ } }, + { "ki", { G_UNICODE_SCRIPT_LATIN/*56*/ } }, + { "kj", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "kk", { G_UNICODE_SCRIPT_CYRILLIC/*77*/ } }, + { "kl", { G_UNICODE_SCRIPT_LATIN/*81*/ } }, + { "km", { G_UNICODE_SCRIPT_KHMER/*63*/ } }, + { "kn", { G_UNICODE_SCRIPT_KANNADA/*70*/ } }, + { "ko", { G_UNICODE_SCRIPT_HANGUL/*2442*/ } }, + { "kok", { G_UNICODE_SCRIPT_DEVANAGARI/*68*/ } }, + { "kr", { G_UNICODE_SCRIPT_LATIN/*56*/ } }, + { "ks", { G_UNICODE_SCRIPT_ARABIC/*33*/ } }, + { "ku-am", { G_UNICODE_SCRIPT_CYRILLIC/*64*/ } }, + { "ku-iq", { G_UNICODE_SCRIPT_ARABIC/*32*/ } }, + { "ku-ir", { G_UNICODE_SCRIPT_ARABIC/*32*/ } }, + { "ku-tr", { G_UNICODE_SCRIPT_LATIN/*62*/ } }, + { "kum", { G_UNICODE_SCRIPT_CYRILLIC/*66*/ } }, + { "kv", { G_UNICODE_SCRIPT_CYRILLIC/*70*/ } }, + { "kw", { G_UNICODE_SCRIPT_LATIN/*64*/ } }, + { "kwm", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "ky", { G_UNICODE_SCRIPT_CYRILLIC/*70*/ } }, + { "la", { G_UNICODE_SCRIPT_LATIN/*68*/ } }, + { "lah", { G_UNICODE_SCRIPT_ARABIC/*27*/ } }, + { "lb", { G_UNICODE_SCRIPT_LATIN/*75*/ } }, + { "lez", { G_UNICODE_SCRIPT_CYRILLIC/*67*/ } }, + { "lg", { G_UNICODE_SCRIPT_LATIN/*54*/ } }, + { "li", { G_UNICODE_SCRIPT_LATIN/*62*/ } }, + { "ln", { G_UNICODE_SCRIPT_LATIN/*78*/ } }, + { "lo", { G_UNICODE_SCRIPT_LAO/*55*/ } }, + { "lt", { G_UNICODE_SCRIPT_LATIN/*70*/ } }, + { "lv", { G_UNICODE_SCRIPT_LATIN/*78*/ } }, + { "mai", { G_UNICODE_SCRIPT_DEVANAGARI/*68*/ } }, + { "mg", { G_UNICODE_SCRIPT_LATIN/*56*/ } }, + { "mh", { G_UNICODE_SCRIPT_LATIN/*62*/ } }, + { "mi", { G_UNICODE_SCRIPT_LATIN/*64*/ } }, + { "mk", { G_UNICODE_SCRIPT_CYRILLIC/*42*/ } }, + { "ml", { G_UNICODE_SCRIPT_MALAYALAM/*68*/ } }, + { "mn-cn", { G_UNICODE_SCRIPT_MONGOLIAN/*130*/ } }, + { "mn-mn", { G_UNICODE_SCRIPT_CYRILLIC/*70*/ } }, + { "mni", { G_UNICODE_SCRIPT_BENGALI/*75*/ } }, + { "mo", { G_UNICODE_SCRIPT_CYRILLIC/*66*/, G_UNICODE_SCRIPT_LATIN/*62*/ } }, + { "mr", { G_UNICODE_SCRIPT_DEVANAGARI/*68*/ } }, + { "ms", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "mt", { G_UNICODE_SCRIPT_LATIN/*72*/ } }, + { "my", { G_UNICODE_SCRIPT_MYANMAR/*48*/ } }, + { "na", { G_UNICODE_SCRIPT_LATIN/*60*/ } }, + { "nb", { G_UNICODE_SCRIPT_LATIN/*70*/ } }, + { "nds", { G_UNICODE_SCRIPT_LATIN/*59*/ } }, + { "ne", { G_UNICODE_SCRIPT_DEVANAGARI/*70*/ } }, + { "ng", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "nl", { G_UNICODE_SCRIPT_LATIN/*82*/ } }, + { "nn", { G_UNICODE_SCRIPT_LATIN/*76*/ } }, + { "no", { G_UNICODE_SCRIPT_LATIN/*70*/ } }, + { "nqo", { G_UNICODE_SCRIPT_NKO/*59*/ } }, + { "nr", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "nso", { G_UNICODE_SCRIPT_LATIN/*58*/ } }, + { "nv", { G_UNICODE_SCRIPT_LATIN/*70*/ } }, + { "ny", { G_UNICODE_SCRIPT_LATIN/*54*/ } }, + { "oc", { G_UNICODE_SCRIPT_LATIN/*70*/ } }, + { "om", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "or", { G_UNICODE_SCRIPT_ORIYA/*68*/ } }, + { "os", { G_UNICODE_SCRIPT_CYRILLIC/*66*/ } }, + { "ota", { G_UNICODE_SCRIPT_ARABIC/*37*/ } }, + { "pa", { G_UNICODE_SCRIPT_GURMUKHI/*63*/ } }, + { "pa-pk", { G_UNICODE_SCRIPT_ARABIC/*27*/ } }, + { "pap-an", { G_UNICODE_SCRIPT_LATIN/*72*/ } }, + { "pap-aw", { G_UNICODE_SCRIPT_LATIN/*54*/ } }, + { "pl", { G_UNICODE_SCRIPT_LATIN/*70*/ } }, + { "ps-af", { G_UNICODE_SCRIPT_ARABIC/*49*/ } }, + { "ps-pk", { G_UNICODE_SCRIPT_ARABIC/*49*/ } }, + { "pt", { G_UNICODE_SCRIPT_LATIN/*82*/ } }, + { "qu", { G_UNICODE_SCRIPT_LATIN/*54*/ } }, + { "quz", { G_UNICODE_SCRIPT_LATIN/*54*/ } }, + { "rm", { G_UNICODE_SCRIPT_LATIN/*66*/ } }, + { "rn", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "ro", { G_UNICODE_SCRIPT_LATIN/*62*/ } }, + { "ru", { G_UNICODE_SCRIPT_CYRILLIC/*66*/ } }, + { "rw", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "sa", { G_UNICODE_SCRIPT_DEVANAGARI/*68*/ } }, + { "sah", { G_UNICODE_SCRIPT_CYRILLIC/*76*/ } }, + { "sat", { G_UNICODE_SCRIPT_DEVANAGARI/*68*/ } }, + { "sc", { G_UNICODE_SCRIPT_LATIN/*62*/ } }, + { "sco", { G_UNICODE_SCRIPT_LATIN/*56*/ } }, + { "sd", { G_UNICODE_SCRIPT_ARABIC/*54*/ } }, + { "se", { G_UNICODE_SCRIPT_LATIN/*66*/ } }, + { "sel", { G_UNICODE_SCRIPT_CYRILLIC/*66*/ } }, + { "sg", { G_UNICODE_SCRIPT_LATIN/*72*/ } }, + { "sh", { G_UNICODE_SCRIPT_CYRILLIC/*94*/, G_UNICODE_SCRIPT_LATIN/*62*/ } }, + { "shs", { G_UNICODE_SCRIPT_LATIN/*46*/ } }, + { "si", { G_UNICODE_SCRIPT_SINHALA/*73*/ } }, + { "sid", { G_UNICODE_SCRIPT_ETHIOPIC/*281*/ } }, + { "sk", { G_UNICODE_SCRIPT_LATIN/*86*/ } }, + { "sl", { G_UNICODE_SCRIPT_LATIN/*62*/ } }, + { "sm", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "sma", { G_UNICODE_SCRIPT_LATIN/*60*/ } }, + { "smj", { G_UNICODE_SCRIPT_LATIN/*60*/ } }, + { "smn", { G_UNICODE_SCRIPT_LATIN/*68*/ } }, + { "sms", { G_UNICODE_SCRIPT_LATIN/*80*/ } }, + { "sn", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "so", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "sq", { G_UNICODE_SCRIPT_LATIN/*56*/ } }, + { "sr", { G_UNICODE_SCRIPT_CYRILLIC/*60*/ } }, + { "ss", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "st", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "su", { G_UNICODE_SCRIPT_LATIN/*54*/ } }, + { "sv", { G_UNICODE_SCRIPT_LATIN/*68*/ } }, + { "sw", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "syr", { G_UNICODE_SCRIPT_SYRIAC/*45*/ } }, + { "ta", { G_UNICODE_SCRIPT_TAMIL/*48*/ } }, + { "te", { G_UNICODE_SCRIPT_TELUGU/*70*/ } }, + { "tg", { G_UNICODE_SCRIPT_CYRILLIC/*78*/ } }, + { "th", { G_UNICODE_SCRIPT_THAI/*73*/ } }, + { "ti-er", { G_UNICODE_SCRIPT_ETHIOPIC/*255*/ } }, + { "ti-et", { G_UNICODE_SCRIPT_ETHIOPIC/*281*/ } }, + { "tig", { G_UNICODE_SCRIPT_ETHIOPIC/*221*/ } }, + { "tk", { G_UNICODE_SCRIPT_LATIN/*68*/ } }, + { "tl", { G_UNICODE_SCRIPT_LATIN/*84*/ } }, + { "tn", { G_UNICODE_SCRIPT_LATIN/*58*/ } }, + { "to", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "tr", { G_UNICODE_SCRIPT_LATIN/*70*/ } }, + { "ts", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "tt", { G_UNICODE_SCRIPT_CYRILLIC/*76*/ } }, + { "tw", { G_UNICODE_SCRIPT_LATIN/*70*/ } }, + { "ty", { G_UNICODE_SCRIPT_LATIN/*64*/ } }, + { "tyv", { G_UNICODE_SCRIPT_CYRILLIC/*70*/ } }, + { "ug", { G_UNICODE_SCRIPT_ARABIC/*33*/ } }, + { "uk", { G_UNICODE_SCRIPT_CYRILLIC/*72*/ } }, + { "und-zmth", { G_UNICODE_SCRIPT_LATIN/*53*/, G_UNICODE_SCRIPT_GREEK/*51*/ } }, + { "und-zsye", { 0 } }, + { "ur", { G_UNICODE_SCRIPT_ARABIC/*27*/ } }, + { "uz", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "ve", { G_UNICODE_SCRIPT_LATIN/*62*/ } }, + { "vi", { G_UNICODE_SCRIPT_LATIN/*186*/ } }, + { "vo", { G_UNICODE_SCRIPT_LATIN/*54*/ } }, + { "vot", { G_UNICODE_SCRIPT_LATIN/*62*/ } }, + { "wa", { G_UNICODE_SCRIPT_LATIN/*70*/ } }, + { "wal", { G_UNICODE_SCRIPT_ETHIOPIC/*281*/ } }, + { "wen", { G_UNICODE_SCRIPT_LATIN/*76*/ } }, + { "wo", { G_UNICODE_SCRIPT_LATIN/*66*/ } }, + { "xh", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "yap", { G_UNICODE_SCRIPT_LATIN/*58*/ } }, + { "yi", { G_UNICODE_SCRIPT_HEBREW/*27*/ } }, + { "yo", { G_UNICODE_SCRIPT_LATIN/*114*/ } }, + { "za", { G_UNICODE_SCRIPT_LATIN/*52*/ } }, + { "zh-cn", { G_UNICODE_SCRIPT_HAN/*6763*/ } }, + { "zh-hk", { G_UNICODE_SCRIPT_HAN/*1083*/ } }, + { "zh-mo", { G_UNICODE_SCRIPT_HAN/*1083*/ } }, + { "zh-sg", { G_UNICODE_SCRIPT_HAN/*6763*/ } }, + { "zh-tw", { G_UNICODE_SCRIPT_HAN/*13063*/ } }, + { "zu", { G_UNICODE_SCRIPT_LATIN/*52*/ } } +}; diff --git a/pango2/pango-script-private.h b/pango2/pango-script-private.h new file mode 100644 index 00000000..06c84b65 --- /dev/null +++ b/pango2/pango-script-private.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2002 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + + +#define PAREN_STACK_DEPTH 128 + +typedef struct _ParenStackEntry ParenStackEntry; + +struct _ParenStackEntry +{ + int pair_index; + GUnicodeScript script_code; +}; + +struct _Pango2ScriptIter +{ + const char *text_start; + const char *text_end; + + const char *script_start; + const char *script_end; + GUnicodeScript script_code; + + ParenStackEntry paren_stack[PAREN_STACK_DEPTH]; + int paren_sp; +}; + +Pango2ScriptIter * +_pango2_script_iter_init (Pango2ScriptIter *iter, + const char *text, + int length); + +void +_pango2_script_iter_fini (Pango2ScriptIter *iter); diff --git a/pango2/pango-script.c b/pango2/pango-script.c new file mode 100644 index 00000000..e65c6265 --- /dev/null +++ b/pango2/pango-script.c @@ -0,0 +1,373 @@ +/* Pango2 + * pango-script.c: Script tag handling + * + * Copyright (C) 2002 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * Implementation of pango2_script_iter is derived from ICU: + * + * icu/sources/common/usc_impl.c + * + ********************************************************************** + * Copyright (C) 1999-2002, International Business Machines + * Corporation and others. All Rights Reserved. + ********************************************************************** + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, and/or sell copies of the Software, and to permit persons + * to whom the Software is furnished to do so, provided that the above + * copyright notice(s) and this permission notice appear in all copies of + * the Software and that both the above copyright notice(s) and this + * permission notice appear in supporting documentation. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT + * OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL + * INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING + * FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, + * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION + * WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Except as contained in this notice, the name of a copyright holder + * shall not be used in advertising or otherwise to promote the sale, use + * or other dealings in this Software without prior written authorization + * of the copyright holder. + */ + +#include "config.h" +#include <stdlib.h> +#include <string.h> + +#include "pango-script.h" +#include "pango-script-private.h" + + +/**********************************************************************/ + +static Pango2ScriptIter *pango2_script_iter_copy (Pango2ScriptIter *iter); + +G_DEFINE_BOXED_TYPE (Pango2ScriptIter, + pango2_script_iter, + pango2_script_iter_copy, + pango2_script_iter_free) + +Pango2ScriptIter * +_pango2_script_iter_init (Pango2ScriptIter *iter, + const char *text, + int length) +{ + iter->text_start = text; + if (length >= 0) + iter->text_end = text + length; + else + iter->text_end = text + strlen (text); + + iter->script_start = text; + iter->script_end = text; + iter->script_code = G_UNICODE_SCRIPT_COMMON; + + iter->paren_sp = -1; + + pango2_script_iter_next (iter); + + return iter; +} + +/** + * pango2_script_iter_new: + * @text: a UTF-8 string + * @length: length of @text, or -1 if @text is nul-terminated + * + * Create a new `Pango2ScriptIter`, used to break a string of + * Unicode text into runs by Unicode script. + * + * No copy is made of @text, so the caller needs to make + * sure it remains valid until the iterator is freed with + * [method@Pango2.ScriptIter.free]. + * + * Return value: the new script iterator, initialized + * to point at the first range in the text, which should be + * freed with [method@Pango2.ScriptIter.free]. If the string is + * empty, it will point at an empty range. + */ +Pango2ScriptIter * +pango2_script_iter_new (const char *text, + int length) +{ + return _pango2_script_iter_init (g_slice_new (Pango2ScriptIter), text, length); +} + +static Pango2ScriptIter * +pango2_script_iter_copy (Pango2ScriptIter *iter) +{ + return g_slice_dup (Pango2ScriptIter, iter); +} + +void +_pango2_script_iter_fini (Pango2ScriptIter *iter) +{ +} + +/** + * pango2_script_iter_free: + * @iter: a `Pango2ScriptIter` + * + * Frees a `Pango2ScriptIter`. + */ +void +pango2_script_iter_free (Pango2ScriptIter *iter) +{ + _pango2_script_iter_fini (iter); + g_slice_free (Pango2ScriptIter, iter); +} + +/** + * pango2_script_iter_get_range: + * @iter: a `Pango2ScriptIter` + * @start: (out) (optional): location to store start position of the range + * @end: (out) (optional): location to store end position of the range + * @script: (out) (optional): location to store script for range + * + * Gets information about the range to which @iter currently points. + * + * The range is the set of locations p where *start <= p < *end. + * (That is, it doesn't include the character stored at *end) + */ +void +pango2_script_iter_get_range (Pango2ScriptIter *iter, + const char **start, + const char **end, + GUnicodeScript *script) +{ + if (start) + *start = iter->script_start; + if (end) + *end = iter->script_end; + if (script) + *script = iter->script_code; +} + +static const gunichar paired_chars[] = { + 0x0028, 0x0029, /* ascii paired punctuation */ + 0x003c, 0x003e, + 0x005b, 0x005d, + 0x007b, 0x007d, + 0x00ab, 0x00bb, /* guillemets */ + 0x0f3a, 0x0f3b, /* tibetan */ + 0x0f3c, 0x0f3d, + 0x169b, 0x169c, /* ogham */ + 0x2018, 0x2019, /* general punctuation */ + 0x201c, 0x201d, + 0x2039, 0x203a, + 0x2045, 0x2046, + 0x207d, 0x207e, + 0x208d, 0x208e, + 0x27e6, 0x27e7, /* math */ + 0x27e8, 0x27e9, + 0x27ea, 0x27eb, + 0x27ec, 0x27ed, + 0x27ee, 0x27ef, + 0x2983, 0x2984, + 0x2985, 0x2986, + 0x2987, 0x2988, + 0x2989, 0x298a, + 0x298b, 0x298c, + 0x298d, 0x298e, + 0x298f, 0x2990, + 0x2991, 0x2992, + 0x2993, 0x2994, + 0x2995, 0x2996, + 0x2997, 0x2998, + 0x29fc, 0x29fd, + 0x2e02, 0x2e03, + 0x2e04, 0x2e05, + 0x2e09, 0x2e0a, + 0x2e0c, 0x2e0d, + 0x2e1c, 0x2e1d, + 0x2e20, 0x2e21, + 0x2e22, 0x2e23, + 0x2e24, 0x2e25, + 0x2e26, 0x2e27, + 0x2e28, 0x2e29, + 0x3008, 0x3009, /* chinese paired punctuation */ + 0x300a, 0x300b, + 0x300c, 0x300d, + 0x300e, 0x300f, + 0x3010, 0x3011, + 0x3014, 0x3015, + 0x3016, 0x3017, + 0x3018, 0x3019, + 0x301a, 0x301b, + 0xfe59, 0xfe5a, + 0xfe5b, 0xfe5c, + 0xfe5d, 0xfe5e, + 0xff08, 0xff09, + 0xff3b, 0xff3d, + 0xff5b, 0xff5d, + 0xff5f, 0xff60, + 0xff62, 0xff63 +}; + +static int +get_pair_index (gunichar ch) +{ + int lower = 0; + int upper = G_N_ELEMENTS (paired_chars) - 1; + + while (lower <= upper) + { + int mid = (lower + upper) / 2; + + if (ch < paired_chars[mid]) + upper = mid - 1; + else if (ch > paired_chars[mid]) + lower = mid + 1; + else + return mid; + } + + return -1; +} + +/* duplicated in pango-language.c */ +#define REAL_SCRIPT(script) \ + ((script) > G_UNICODE_SCRIPT_INHERITED && (script) != G_UNICODE_SCRIPT_UNKNOWN) + +#define SAME_SCRIPT(script1, script2) \ + (!REAL_SCRIPT (script1) || !REAL_SCRIPT (script2) || (script1) == (script2)) + +#define IS_OPEN(pair_index) (((pair_index) & 1) == 0) + +/** + * pango2_script_iter_next: + * @iter: a `Pango2ScriptIter` + * + * Advances a `Pango2ScriptIter` to the next range. + * + * If @iter is already at the end, it is left unchanged + * and %FALSE is returned. + * + * Return value: %TRUE if @iter was successfully advanced + */ +gboolean +pango2_script_iter_next (Pango2ScriptIter *iter) +{ + int start_sp; + + if (iter->script_end == iter->text_end) + return FALSE; + + start_sp = iter->paren_sp; + iter->script_code = G_UNICODE_SCRIPT_COMMON; + iter->script_start = iter->script_end; + + for (; iter->script_end < iter->text_end; iter->script_end = g_utf8_next_char (iter->script_end)) + { + gunichar ch = g_utf8_get_char (iter->script_end); + GUnicodeScript sc; + int pair_index; + + sc = g_unichar_get_script (ch); + if (sc != G_UNICODE_SCRIPT_COMMON) + pair_index = -1; + else + pair_index = get_pair_index (ch); + + /* + * Paired character handling: + * + * if it's an open character, push it onto the stack. + * if it's a close character, find the matching open on the + * stack, and use that script code. Any non-matching open + * characters above it on the stack will be poped. + */ + if (pair_index >= 0) + { + if (IS_OPEN (pair_index)) + { + /* + * If the paren stack is full, empty it. This + * means that deeply nested paired punctuation + * characters will be ignored, but that's an unusual + * case, and it's better to ignore them than to + * write off the end of the stack... + */ + if (++iter->paren_sp >= PAREN_STACK_DEPTH) + iter->paren_sp = 0; + + iter->paren_stack[iter->paren_sp].pair_index = pair_index; + iter->paren_stack[iter->paren_sp].script_code = iter->script_code; + } + else if (iter->paren_sp >= 0) + { + int pi = pair_index & ~1; + + while (iter->paren_sp >= 0 && iter->paren_stack[iter->paren_sp].pair_index != pi) + iter->paren_sp--; + + if (iter->paren_sp < start_sp) + start_sp = iter->paren_sp; + + if (iter->paren_sp >= 0) + sc = iter->paren_stack[iter->paren_sp].script_code; + } + } + + if (SAME_SCRIPT (iter->script_code, sc)) + { + if (!REAL_SCRIPT (iter->script_code) && REAL_SCRIPT (sc)) + { + iter->script_code = sc; + + /* + * now that we have a final script code, fix any open + * characters we pushed before we knew the script code. + */ + while (start_sp < iter->paren_sp) + iter->paren_stack[++start_sp].script_code = iter->script_code; + } + + /* + * if this character is a close paired character, + * pop it from the stack + */ + if (pair_index >= 0 && !IS_OPEN (pair_index) && iter->paren_sp >= 0) + { + iter->paren_sp--; + + if (iter->paren_sp < start_sp) + start_sp = iter->paren_sp; + } + } + else + { + /* Different script, we're done */ + break; + } + } + + return TRUE; +} + +/********************************************************** + * End of code from ICU + **********************************************************/ diff --git a/pango2/pango-script.h b/pango2/pango-script.h new file mode 100644 index 00000000..abe822c8 --- /dev/null +++ b/pango2/pango-script.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2002 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <glib-object.h> + +#include <pango2/pango-language.h> +#include <pango2/pango-version-macros.h> + +G_BEGIN_DECLS + +/** + * Pango2ScriptIter: + * + * A `Pango2ScriptIter` is used to iterate through a string + * and identify ranges in different scripts. + **/ +typedef struct _Pango2ScriptIter Pango2ScriptIter; + +PANGO2_AVAILABLE_IN_ALL +GType pango2_script_iter_get_type (void) G_GNUC_CONST; + +PANGO2_AVAILABLE_IN_ALL +Pango2ScriptIter * pango2_script_iter_new (const char *text, + int length); +PANGO2_AVAILABLE_IN_ALL +void pango2_script_iter_get_range (Pango2ScriptIter *iter, + const char **start, + const char **end, + GUnicodeScript *script); +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_script_iter_next (Pango2ScriptIter *iter); +PANGO2_AVAILABLE_IN_ALL +void pango2_script_iter_free (Pango2ScriptIter *iter); + +PANGO2_AVAILABLE_IN_ALL +Pango2Language * pango2_script_get_sample_language (GUnicodeScript script) G_GNUC_PURE; + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(Pango2ScriptIter, pango2_script_iter_free) + +G_END_DECLS diff --git a/pango2/pango-tabs.c b/pango2/pango-tabs.c new file mode 100644 index 00000000..71fdfe2f --- /dev/null +++ b/pango2/pango-tabs.c @@ -0,0 +1,617 @@ +/* Pango2 + * pango-tabs.c: Tab-related stuff + * + * Copyright (C) 2000 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include "pango-tabs.h" +#include "pango-impl-utils.h" +#include <stdlib.h> +#include <string.h> + +typedef struct _Pango2Tab Pango2Tab; + +struct _Pango2Tab +{ + int location; + Pango2TabAlign alignment; + gunichar decimal_point; +}; + +/** + * Pango2TabArray: + * + * A `Pango2TabArray` contains an array of tab stops. + * + * `Pango2TabArray` can be used to set tab stops in a `Pango2Layout`. + * Each tab stop has an alignment, a position, and optionally + * a character to use as decimal point. + */ +struct _Pango2TabArray +{ + int size; + int allocated; + gboolean positions_in_pixels; + Pango2Tab *tabs; +}; + +static void +init_tabs (Pango2TabArray *array, int start, int end) +{ + while (start < end) + { + array->tabs[start].location = 0; + array->tabs[start].alignment = PANGO2_TAB_LEFT; + array->tabs[start].decimal_point = 0; + ++start; + } +} + +/** + * pango2_tab_array_new: + * @initial_size: Initial number of tab stops to allocate, can be 0 + * @positions_in_pixels: whether positions are in pixel units + * + * Creates an array of @initial_size tab stops. + * + * Tab stops are specified in pixel units if @positions_in_pixels is %TRUE, + * otherwise in Pango2 units. All stops are initially at position 0. + * + * Return value: the newly allocated `Pango2TabArray`, which should + * be freed with [method@Pango2.TabArray.free]. + */ +Pango2TabArray* +pango2_tab_array_new (int initial_size, + gboolean positions_in_pixels) +{ + Pango2TabArray *array; + + g_return_val_if_fail (initial_size >= 0, NULL); + + /* alloc enough to treat array->tabs as an array of length + * size, though it's declared as an array of length 1. + * If we allowed tab array resizing we'd need to drop this + * optimization. + */ + array = g_slice_new (Pango2TabArray); + array->size = initial_size; array->allocated = initial_size; + + if (array->allocated > 0) + { + array->tabs = g_new (Pango2Tab, array->allocated); + init_tabs (array, 0, array->allocated); + } + else + array->tabs = NULL; + + array->positions_in_pixels = positions_in_pixels; + + return array; +} + +/** + * pango2_tab_array_new_with_positions: + * @size: number of tab stops in the array + * @positions_in_pixels: whether positions are in pixel units + * @first_alignment: alignment of first tab stop + * @first_position: position of first tab stop + * @...: additional alignment/position pairs + * + * Creates a `Pango2TabArray` and allows you to specify the alignment + * and position of each tab stop. + * + * You **must** provide an alignment and position for @size tab stops. + * + * Return value: the newly allocated `Pango2TabArray`, which should + * be freed with [method@Pango2.TabArray.free]. + */ +Pango2TabArray * +pango2_tab_array_new_with_positions (int size, + gboolean positions_in_pixels, + Pango2TabAlign first_alignment, + int first_position, + ...) +{ + Pango2TabArray *array; + va_list args; + int i; + + g_return_val_if_fail (size >= 0, NULL); + + array = pango2_tab_array_new (size, positions_in_pixels); + + if (size == 0) + return array; + + array->tabs[0].alignment = first_alignment; + array->tabs[0].location = first_position; + array->tabs[0].decimal_point = 0; + + if (size == 1) + return array; + + va_start (args, first_position); + + i = 1; + while (i < size) + { + Pango2TabAlign align = va_arg (args, Pango2TabAlign); + int pos = va_arg (args, int); + + array->tabs[i].alignment = align; + array->tabs[i].location = pos; + array->tabs[i].decimal_point = 0; + + ++i; + } + + va_end (args); + + return array; +} + +G_DEFINE_BOXED_TYPE (Pango2TabArray, pango2_tab_array, + pango2_tab_array_copy, + pango2_tab_array_free); + +/** + * pango2_tab_array_copy: + * @src: `Pango2TabArray` to copy + * + * Copies a `Pango2TabArray`. + * + * Return value: the newly allocated `Pango2TabArray`, which should + * be freed with [method@Pango2.TabArray.free]. + */ +Pango2TabArray* +pango2_tab_array_copy (Pango2TabArray *src) +{ + Pango2TabArray *copy; + + g_return_val_if_fail (src != NULL, NULL); + + copy = pango2_tab_array_new (src->size, src->positions_in_pixels); + + if (copy->tabs) + memcpy (copy->tabs, src->tabs, sizeof(Pango2Tab) * src->size); + + return copy; +} + +/** + * pango2_tab_array_free: + * @tab_array: a `Pango2TabArray` + * + * Frees a tab array and associated resources. + */ +void +pango2_tab_array_free (Pango2TabArray *tab_array) +{ + g_return_if_fail (tab_array != NULL); + + g_free (tab_array->tabs); + + g_slice_free (Pango2TabArray, tab_array); +} + +/** + * pango2_tab_array_get_size: + * @tab_array: a `Pango2TabArray` + * + * Gets the number of tab stops in @tab_array. + * + * Return value: the number of tab stops in the array. + */ +int +pango2_tab_array_get_size (Pango2TabArray *tab_array) +{ + g_return_val_if_fail (tab_array != NULL, 0); + + return tab_array->size; +} + +/** + * pango2_tab_array_resize: + * @tab_array: a `Pango2TabArray` + * @new_size: new size of the array + * + * Resizes a tab array. + * + * You must subsequently initialize any tabs + * that were added as a result of growing the array. + */ +void +pango2_tab_array_resize (Pango2TabArray *tab_array, + int new_size) +{ + if (new_size > tab_array->allocated) + { + int current_end = tab_array->allocated; + + /* Ratchet allocated size up above the index. */ + if (tab_array->allocated == 0) + tab_array->allocated = 2; + + while (new_size > tab_array->allocated) + tab_array->allocated = tab_array->allocated * 2; + + tab_array->tabs = g_renew (Pango2Tab, tab_array->tabs, + tab_array->allocated); + + init_tabs (tab_array, current_end, tab_array->allocated); + } + + tab_array->size = new_size; +} + +/** + * pango2_tab_array_set_tab: + * @tab_array: a `Pango2TabArray` + * @tab_index: the index of a tab stop + * @alignment: tab alignment + * @location: tab location in Pango2 units + * + * Sets the alignment and location of a tab stop. + */ +void +pango2_tab_array_set_tab (Pango2TabArray *tab_array, + int tab_index, + Pango2TabAlign alignment, + int location) +{ + g_return_if_fail (tab_array != NULL); + g_return_if_fail (tab_index >= 0); + g_return_if_fail (location >= 0); + + if (tab_index >= tab_array->size) + pango2_tab_array_resize (tab_array, tab_index + 1); + + tab_array->tabs[tab_index].alignment = alignment; + tab_array->tabs[tab_index].location = location; +} + +/** + * pango2_tab_array_get_tab: + * @tab_array: a `Pango2TabArray` + * @tab_index: tab stop index + * @alignment: (out) (optional): location to store alignment + * @location: (out) (optional): location to store tab position + * + * Gets the alignment and position of a tab stop. + */ +void +pango2_tab_array_get_tab (Pango2TabArray *tab_array, + int tab_index, + Pango2TabAlign *alignment, + int *location) +{ + g_return_if_fail (tab_array != NULL); + g_return_if_fail (tab_index < tab_array->size); + g_return_if_fail (tab_index >= 0); + + if (alignment) + *alignment = tab_array->tabs[tab_index].alignment; + + if (location) + *location = tab_array->tabs[tab_index].location; +} + +/** + * pango2_tab_array_get_tabs: + * @tab_array: a `Pango2TabArray` + * @alignments: (out) (optional): location to store an array of tab + * stop alignments + * @locations: (out) (optional) (array): location to store an array + * of tab positions + * + * If non-%NULL, @alignments and @locations are filled with allocated + * arrays. + * + * The arrays are of length [method@Pango2.TabArray.get_size]. + * You must free the returned array. + */ +void +pango2_tab_array_get_tabs (Pango2TabArray *tab_array, + Pango2TabAlign **alignments, + int **locations) +{ + int i; + + g_return_if_fail (tab_array != NULL); + + if (alignments) + *alignments = g_new (Pango2TabAlign, tab_array->size); + + if (locations) + *locations = g_new (int, tab_array->size); + + i = 0; + while (i < tab_array->size) + { + if (alignments) + (*alignments)[i] = tab_array->tabs[i].alignment; + if (locations) + (*locations)[i] = tab_array->tabs[i].location; + + ++i; + } +} + +/** + * pango2_tab_array_get_positions_in_pixels: + * @tab_array: a `Pango2TabArray` + * + * Returns %TRUE if the tab positions are in pixels, + * %FALSE if they are in Pango2 units. + * + * Return value: whether positions are in pixels. + */ +gboolean +pango2_tab_array_get_positions_in_pixels (Pango2TabArray *tab_array) +{ + g_return_val_if_fail (tab_array != NULL, FALSE); + + return tab_array->positions_in_pixels; +} + +/** + * pango2_tab_array_set_positions_in_pixels: + * @tab_array: a `Pango2TabArray` + * @positions_in_pixels: whether positions are in pixels + * + * Sets whether positions in this array are specified in + * pixels. + */ +void +pango2_tab_array_set_positions_in_pixels (Pango2TabArray *tab_array, + gboolean positions_in_pixels) +{ + g_return_if_fail (tab_array != NULL); + + tab_array->positions_in_pixels = positions_in_pixels; +} + +/** + * pango2_tab_array_to_string: + * @tab_array: a `Pango2TabArray` + * + * Serializes a `Pango2TabArray` to a string. + * + * No guarantees are made about the format of the string, + * it may change between Pango2 versions. + * + * The intended use of this function is testing and + * debugging. The format is not meant as a permanent + * storage format. + * + * Returns: (transfer full): a newly allocated string + */ +char * +pango2_tab_array_to_string (Pango2TabArray *tab_array) +{ + GString *s; + + s = g_string_new (""); + + for (int i = 0; i < tab_array->size; i++) + { + if (i > 0) + g_string_append_c (s, '\n'); + + if (tab_array->tabs[i].alignment == PANGO2_TAB_RIGHT) + g_string_append (s, "right:"); + else if (tab_array->tabs[i].alignment == PANGO2_TAB_CENTER) + g_string_append (s, "center:"); + else if (tab_array->tabs[i].alignment == PANGO2_TAB_DECIMAL) + g_string_append (s, "decimal:"); + + g_string_append_printf (s, "%d", tab_array->tabs[i].location); + if (tab_array->positions_in_pixels) + g_string_append (s, "px"); + + if (tab_array->tabs[i].decimal_point != 0) + g_string_append_printf (s, ":%d", tab_array->tabs[i].decimal_point); + } + + return g_string_free (s, FALSE); +} + +static const char * +skip_whitespace (const char *p) +{ + while (g_ascii_isspace (*p)) + p++; + return p; +} + +/** + * pango2_tab_array_from_string: + * @text: a string + * + * Deserializes a `Pango2TabArray` from a string. + * + * This is the counterpart to [method@Pango2.TabArray.to_string]. + * See that functions for details about the format. + * + * Returns: (transfer full) (nullable): a new `Pango2TabArray` + */ +Pango2TabArray * +pango2_tab_array_from_string (const char *text) +{ + Pango2TabArray *array; + gboolean pixels; + const char *p; + int i; + + pixels = strstr (text, "px") != NULL; + + array = pango2_tab_array_new (0, pixels); + + p = skip_whitespace (text); + + i = 0; + while (*p) + { + char *endp; + gint64 pos; + Pango2TabAlign align; + + if (g_str_has_prefix (p, "left:")) + { + align = PANGO2_TAB_LEFT; + p += strlen ("left:"); + } + else if (g_str_has_prefix (p, "right:")) + { + align = PANGO2_TAB_RIGHT; + p += strlen ("right:"); + } + else if (g_str_has_prefix (p, "center:")) + { + align = PANGO2_TAB_CENTER; + p += strlen ("center:"); + } + else if (g_str_has_prefix (p, "decimal:")) + { + align = PANGO2_TAB_DECIMAL; + p += strlen ("decimal:"); + } + else + { + align = PANGO2_TAB_LEFT; + } + + pos = g_ascii_strtoll (p, &endp, 10); + if (pos < 0 || + (pixels && *endp != 'p') || + (!pixels && !g_ascii_isspace (*endp) && *endp != ':' && *endp != '\0')) goto fail; + + pango2_tab_array_set_tab (array, i, align, pos); + + p = (const char *)endp; + if (pixels) + { + if (p[0] != 'p' || p[1] != 'x') goto fail; + p += 2; + } + + if (p[0] == ':') + { + gunichar ch; + + p++; + ch = g_ascii_strtoll (p, &endp, 10); + if (!g_ascii_isspace (*endp) && *endp != '\0') goto fail; + + pango2_tab_array_set_decimal_point (array, i, ch); + + p = (const char *)endp; + } + + p = skip_whitespace (p); + + i++; + } + + goto success; + +fail: + pango2_tab_array_free (array); + array = NULL; + +success: + return array; +} + +/** + * pango2_tab_array_set_decimal_point: + * @tab_array: a `Pango2TabArray` + * @tab_index: the index of a tab stop + * @decimal_point: the decimal point to use + * + * Sets the Unicode character to use as decimal point. + * + * This is only relevant for tabs with %PANGO2_TAB_DECIMAL alignment, + * which align content at the first occurrence of the decimal point + * character. + * + * By default, Pango2 uses the decimal point according + * to the current locale. + */ +void +pango2_tab_array_set_decimal_point (Pango2TabArray *tab_array, + int tab_index, + gunichar decimal_point) +{ + g_return_if_fail (tab_array != NULL); + g_return_if_fail (tab_index >= 0); + + if (tab_index >= tab_array->size) + pango2_tab_array_resize (tab_array, tab_index + 1); + + tab_array->tabs[tab_index].decimal_point = decimal_point; +} + +/** + * pango2_tab_array_get_decimal_point: + * @tab_array: a `Pango2TabArray` + * @tab_index: the index of a tab stop + * + * Gets the Unicode character to use as decimal point. + * + * This is only relevant for tabs with %PANGO2_TAB_DECIMAL alignment, + * which align content at the first occurrence of the decimal point + * character. + * + * The default value of 0 means that Pango2 will use the + * decimal point according to the current locale. + */ +gunichar +pango2_tab_array_get_decimal_point (Pango2TabArray *tab_array, + int tab_index) +{ + g_return_val_if_fail (tab_array != NULL, 0); + g_return_val_if_fail (tab_index < tab_array->size, 0); + g_return_val_if_fail (tab_index >= 0, 0); + + return tab_array->tabs[tab_index].decimal_point; +} + +static int +compare_tabs (const void *p1, const void *p2) +{ + const Pango2Tab *t1 = p1; + const Pango2Tab *t2 = p2; + + return t1->location - t2->location; +} + +/** + * pango2_tab_array_sort: + * @tab_array: a `Pango2TabArray` + * + * Utility function to ensure that the tab stops are in increasing order. + */ +void +pango2_tab_array_sort (Pango2TabArray *tab_array) +{ + g_return_if_fail (tab_array != NULL); + + qsort (tab_array->tabs, tab_array->size, sizeof (Pango2Tab), compare_tabs); +} diff --git a/pango2/pango-tabs.h b/pango2/pango-tabs.h new file mode 100644 index 00000000..b5303789 --- /dev/null +++ b/pango2/pango-tabs.h @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2000 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-types.h> + +G_BEGIN_DECLS + +typedef struct _Pango2TabArray Pango2TabArray; + +/** + * Pango2TabAlign: + * @PANGO2_TAB_LEFT: the text appears to the right of the tab stop position + * @PANGO2_TAB_RIGHT: the text appears to the left of the tab stop position + * until the available space is filled. + * @PANGO2_TAB_CENTER: the text is centered at the tab stop position + * until the available space is filled. + * @PANGO2_TAB_DECIMAL: text before the first occurrence of the decimal point + * character appears to the left of the tab stop position (until the available + * space is filled), the rest to the right. + * + * `Pango2TabAlign` specifies where the text appears relative to the tab stop + * position. + */ +typedef enum +{ + PANGO2_TAB_LEFT, + PANGO2_TAB_RIGHT, + PANGO2_TAB_CENTER, + PANGO2_TAB_DECIMAL +} Pango2TabAlign; + +#define PANGO2_TYPE_TAB_ARRAY (pango2_tab_array_get_type ()) + +PANGO2_AVAILABLE_IN_ALL +GType pango2_tab_array_get_type (void) G_GNUC_CONST; + +PANGO2_AVAILABLE_IN_ALL +Pango2TabArray * pango2_tab_array_new (int initial_size, + gboolean positions_in_pixels); +PANGO2_AVAILABLE_IN_ALL +Pango2TabArray * pango2_tab_array_new_with_positions (int size, + gboolean positions_in_pixels, + Pango2TabAlign first_alignment, + int first_position, + ...); +PANGO2_AVAILABLE_IN_ALL +Pango2TabArray * pango2_tab_array_copy (Pango2TabArray *src); +PANGO2_AVAILABLE_IN_ALL +void pango2_tab_array_free (Pango2TabArray *tab_array); +PANGO2_AVAILABLE_IN_ALL +int pango2_tab_array_get_size (Pango2TabArray *tab_array); +PANGO2_AVAILABLE_IN_ALL +void pango2_tab_array_resize (Pango2TabArray *tab_array, + int new_size); +PANGO2_AVAILABLE_IN_ALL +void pango2_tab_array_set_tab (Pango2TabArray *tab_array, + int tab_index, + Pango2TabAlign alignment, + int location); +PANGO2_AVAILABLE_IN_ALL +void pango2_tab_array_get_tab (Pango2TabArray *tab_array, + int tab_index, + Pango2TabAlign *alignment, + int *location); +PANGO2_AVAILABLE_IN_ALL +void pango2_tab_array_get_tabs (Pango2TabArray *tab_array, + Pango2TabAlign **alignments, + int **locations); + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_tab_array_get_positions_in_pixels (Pango2TabArray *tab_array); + +PANGO2_AVAILABLE_IN_ALL +void pango2_tab_array_set_positions_in_pixels (Pango2TabArray *tab_array, + gboolean positions_in_pixels); + +PANGO2_AVAILABLE_IN_ALL +char * pango2_tab_array_to_string (Pango2TabArray *tab_array); +PANGO2_AVAILABLE_IN_ALL +Pango2TabArray * pango2_tab_array_from_string (const char *text); + +PANGO2_AVAILABLE_IN_ALL +void pango2_tab_array_set_decimal_point (Pango2TabArray *tab_array, + int tab_index, + gunichar decimal_point); +PANGO2_AVAILABLE_IN_ALL +gunichar pango2_tab_array_get_decimal_point (Pango2TabArray *tab_array, + int tab_index); + +PANGO2_AVAILABLE_IN_ALL +void pango2_tab_array_sort (Pango2TabArray *tab_array); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(Pango2TabArray, pango2_tab_array_free) + +G_END_DECLS diff --git a/pango2/pango-trace-private.h b/pango2/pango-trace-private.h new file mode 100644 index 00000000..5ef2a5ff --- /dev/null +++ b/pango2/pango-trace-private.h @@ -0,0 +1,49 @@ +/* + * Copyright 2020 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#ifdef HAVE_SYSPROF +#include <sysprof-capture.h> +#endif + +#include <glib.h> + + +#ifdef HAVE_SYSPROF +#define PANGO2_TRACE_CURRENT_TIME SYSPROF_CAPTURE_CURRENT_TIME +#else +#define PANGO2_TRACE_CURRENT_TIME 0 +#endif + +void pango2_trace_mark (gint64 begin_time, + const char *name, + const char *message_format, + ...) G_GNUC_PRINTF (3, 4); + +#ifndef HAVE_SYSPROF +/* Optimise the whole call out */ +#if defined(G_HAVE_ISO_VARARGS) +#define pango2_trace_mark(b, n, m, ...) G_STMT_START { } G_STMT_END +#elif defined(G_HAVE_GNUC_VARARGS) +#define pango2_trace_mark(b, n, m...) G_STMT_START { } G_STMT_END +#else +/* no varargs macro support; the call will have to be optimised out by the compiler */ +#endif +#endif diff --git a/pango2/pango-trace.c b/pango2/pango-trace.c new file mode 100644 index 00000000..99417c45 --- /dev/null +++ b/pango2/pango-trace.c @@ -0,0 +1,40 @@ +/* Pango2 + * pango-trace.c: + * + * Copyright (C) 2020 Red Hat, Inc + * + * 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 2 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 + * Library 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 <http://www.gnu.org/licenses/>. + */ + +#include "config.h" + +#include "pango-trace-private.h" + +#include <stdarg.h> + +void +(pango2_trace_mark) (gint64 begin_time, + const char *name, + const char *message_format, + ...) +{ +#ifdef HAVE_SYSPROF + gint64 end_time = PANGO2_TRACE_CURRENT_TIME; + va_list args; + + va_start (args, message_format); + sysprof_collector_mark_vprintf (begin_time, end_time - begin_time, "Pango2", name, message_format, args); + va_end (args); +#endif /* HAVE_SYSPROF */ +} diff --git a/pango2/pango-types.h b/pango2/pango-types.h new file mode 100644 index 00000000..003dec8e --- /dev/null +++ b/pango2/pango-types.h @@ -0,0 +1,361 @@ +/* + * Copyright (C) 1999 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <glib.h> +#include <glib-object.h> + +#include <pango2/pango-version-macros.h> + +G_BEGIN_DECLS + +typedef struct _Pango2LogAttr Pango2LogAttr; + +typedef struct _Pango2FontDescription Pango2FontDescription; + +typedef struct _Pango2Font Pango2Font; +typedef struct _Pango2FontFace Pango2FontFace; +typedef struct _Pango2FontFamily Pango2FontFamily; + +typedef struct _Pango2FontMap Pango2FontMap; + +typedef struct _Pango2Rectangle Pango2Rectangle; + +typedef struct _Pango2Context Pango2Context; + +typedef struct _Pango2Language Pango2Language; + +/* A index of a glyph into a font. Rendering system dependent */ +/** + * Pango2Glyph: + * + * A `Pango2Glyph` represents a single glyph in the output form of a string. + */ +typedef guint32 Pango2Glyph; + +typedef struct _Pango2Lines Pango2Lines; +typedef struct _Pango2Run Pango2Run; +typedef struct _Pango2Line Pango2Line; +typedef struct _Pango2LineIter Pango2LineIter; + +/** + * PANGO2_SCALE: + * + * The scale between dimensions used for Pango2 distances and device units. + * + * The definition of device units is dependent on the output device; it will + * typically be pixels for a screen, and points for a printer. %PANGO2_SCALE is + * currently 1024, but this may be changed in the future. + * + * When setting font sizes, device units are always considered to be + * points (as in "12 point font"), rather than pixels. + */ +/** + * PANGO2_PIXELS: + * @d: a dimension in Pango2 units. + * + * Converts a dimension to device units by rounding. + * + * Return value: rounded dimension in device units. + */ +/** + * PANGO2_PIXELS_FLOOR: + * @d: a dimension in Pango2 units. + * + * Converts a dimension to device units by flooring. + * + * Return value: floored dimension in device units. + */ +/** + * PANGO2_PIXELS_CEIL: + * @d: a dimension in Pango2 units. + * + * Converts a dimension to device units by ceiling. + * + * Return value: ceiled dimension in device units. + */ +#define PANGO2_SCALE 1024 +#define PANGO2_PIXELS(d) (((int)(d) + 512) >> 10) +#define PANGO2_PIXELS_FLOOR(d) (((int)(d)) >> 10) +#define PANGO2_PIXELS_CEIL(d) (((int)(d) + 1023) >> 10) +/* The above expressions are just slightly wrong for floating point d; + * For example we'd expect PANGO2_PIXELS(-512.5) => -1 but instead we get 0. + * That's unlikely to matter for practical use and the expression is much + * more compact and faster than alternatives that work exactly for both + * integers and floating point. + * + * PANGO2_PIXELS also behaves differently for +512 and -512. + */ + +/** + * PANGO2_UNITS_FLOOR: + * @d: a dimension in Pango2 units. + * + * Rounds a dimension down to whole device units, but does not + * convert it to device units. + * + * Return value: rounded down dimension in Pango2 units. + */ +#define PANGO2_UNITS_FLOOR(d) \ + ((d) & ~(PANGO2_SCALE - 1)) + +/** + * PANGO2_UNITS_CEIL: + * @d: a dimension in Pango2 units. + * + * Rounds a dimension up to whole device units, but does not + * convert it to device units. + * + * Return value: rounded up dimension in Pango2 units. + */ +#define PANGO2_UNITS_CEIL(d) \ + (((d) + (PANGO2_SCALE - 1)) & ~(PANGO2_SCALE - 1)) + +/** + * PANGO2_UNITS_ROUND: + * @d: a dimension in Pango2 units. + * + * Rounds a dimension to whole device units, but does not + * convert it to device units. + * + * Return value: rounded dimension in Pango2 units. + */ +#define PANGO2_UNITS_ROUND(d) \ + (((d) + (PANGO2_SCALE >> 1)) & ~(PANGO2_SCALE - 1)) + + +PANGO2_AVAILABLE_IN_ALL +int pango2_units_from_double (double d) G_GNUC_CONST; +PANGO2_AVAILABLE_IN_ALL +double pango2_units_to_double (int i) G_GNUC_CONST; + + + +/** + * Pango2Rectangle: + * @x: X coordinate of the left side of the rectangle. + * @y: Y coordinate of the the top side of the rectangle. + * @width: width of the rectangle. + * @height: height of the rectangle. + * + * The `Pango2Rectangle` structure represents a rectangle. + * + * `Pango2Rectangle` is frequently used to represent the logical or ink + * extents of a single glyph or section of text. (See, for instance, + * [method@Pango2.Font.get_glyph_extents].) + */ +struct _Pango2Rectangle +{ + int x; + int y; + int width; + int height; +}; + +/* Macros to translate from extents rectangles to ascent/descent/lbearing/rbearing + */ +/** + * PANGO2_ASCENT: + * @rect: a `Pango2Rectangle` + * + * Extracts the *ascent* from a `Pango2Rectangle` + * representing glyph extents. + * + * The ascent is the distance from the baseline to the + * highest point of the character. This is positive if the + * glyph ascends above the baseline. + */ +/** + * PANGO2_DESCENT: + * @rect: a `Pango2Rectangle` + * + * Extracts the *descent* from a `Pango2Rectangle` + * representing glyph extents. + * + * The descent is the distance from the baseline to the + * lowest point of the character. This is positive if the + * glyph descends below the baseline. + */ +/** + * PANGO2_LBEARING: + * @rect: a `Pango2Rectangle` + * + * Extracts the *left bearing* from a `Pango2Rectangle` + * representing glyph extents. + * + * The left bearing is the distance from the horizontal + * origin to the farthest left point of the character. + * This is positive for characters drawn completely to + * the right of the glyph origin. + */ +/** + * PANGO2_RBEARING: + * @rect: a `Pango2Rectangle` + * + * Extracts the *right bearing* from a `Pango2Rectangle` + * representing glyph extents. + * + * The right bearing is the distance from the horizontal + * origin to the farthest right point of the character. + * This is positive except for characters drawn completely + * to the left of the horizontal origin. + */ +#define PANGO2_ASCENT(rect) (-(rect).y) +#define PANGO2_DESCENT(rect) ((rect).y + (rect).height) +#define PANGO2_LBEARING(rect) ((rect).x) +#define PANGO2_RBEARING(rect) ((rect).x + (rect).width) + +PANGO2_AVAILABLE_IN_ALL +void pango2_extents_to_pixels (Pango2Rectangle *inclusive, + Pango2Rectangle *nearest); + + +#include <pango2/pango-direction.h> +#include <pango2/pango-gravity.h> +#include <pango2/pango-language.h> +#include <pango2/pango-matrix.h> +#include <pango2/pango-script.h> + +/** + * Pango2Alignment: + * @PANGO2_ALIGN_LEFT: Put all available space on the right + * @PANGO2_ALIGN_CENTER: Center the line within the available space + * @PANGO2_ALIGN_RIGHT: Put all available space on the left + * @PANGO2_ALIGN_NATURAL: Use left or right alignment, depending + * on the text direction of the paragraph + * @PANGO2_ALIGN_JUSTIFY: Justify the content to fill the available space + * + * `Pango2Alignment` describes how to align the lines of a `Pango2Layout` + * within the available space. + */ +typedef enum +{ + PANGO2_ALIGN_LEFT, + PANGO2_ALIGN_CENTER, + PANGO2_ALIGN_RIGHT, + PANGO2_ALIGN_NATURAL, + PANGO2_ALIGN_JUSTIFY +} Pango2Alignment; + +/** + * Pango2WrapMode: + * @PANGO2_WRAP_WORD: wrap lines at word boundaries. + * @PANGO2_WRAP_CHAR: wrap lines at character boundaries. + * @PANGO2_WRAP_WORD_CHAR: wrap lines at word boundaries, but fall back to + * character boundaries if there is not enough space for a full word. + * + * `Pango2WrapMode` describes how to wrap the lines of a `Pango2Layout` + * to the desired width. + * + * For @PANGO2_WRAP_WORD, Pango2 uses break opportunities that are determined + * by the Unicode line breaking algorithm. For @PANGO2_WRAP_CHAR, Pango2 allows + * breaking at grapheme boundaries that are determined by the Unicode text + * segmentation algorithm. + */ +typedef enum { + PANGO2_WRAP_WORD, + PANGO2_WRAP_CHAR, + PANGO2_WRAP_WORD_CHAR +} Pango2WrapMode; + +/** + * Pango2EllipsizeMode: + * @PANGO2_ELLIPSIZE_NONE: No ellipsization + * @PANGO2_ELLIPSIZE_START: Omit characters at the start of the text + * @PANGO2_ELLIPSIZE_MIDDLE: Omit characters in the middle of the text + * @PANGO2_ELLIPSIZE_END: Omit characters at the end of the text + * + * `Pango2EllipsizeMode` describes what sort of ellipsization + * should be applied to text. + * + * In the ellipsization process characters are removed from the + * text in order to make it fit to a given width and replaced + * with an ellipsis. + */ +typedef enum { + PANGO2_ELLIPSIZE_NONE, + PANGO2_ELLIPSIZE_START, + PANGO2_ELLIPSIZE_MIDDLE, + PANGO2_ELLIPSIZE_END +} Pango2EllipsizeMode; + +/** +* Pango2LeadingTrim: + * @PANGO2_LEADING_TRIM_NONE: No trimming + * @PANGO2_LEADING_TRIM_START: Trim leading at the top + * @PANGO2_LEADING_TRIM_END: Trim leading at the bottom + * + * The `Pango2LeadingTrim` flags control how the line height affects + * the extents of runs and lines. + */ +typedef enum +{ + PANGO2_LEADING_TRIM_NONE = 0, + PANGO2_LEADING_TRIM_START = 1 << 0, + PANGO2_LEADING_TRIM_END = 1 << 1, +} Pango2LeadingTrim; + +/** + * PANGO2_LEADING_TRIM_BOTH: + * + * Shorthand for `PANGO2_LEADING_TRIM_START|PANGO2_LEADING_TRIM_END`. + */ +#define PANGO2_LEADING_TRIM_BOTH (PANGO2_LEADING_TRIM_START|PANGO2_LEADING_TRIM_END) + +/* + * PANGO2_DECLARE_INTERNAL_TYPE: + * @ModuleObjName: The name of the new type, in camel case (like GtkWidget) + * @module_obj_name: The name of the new type in lowercase, with words + * separated by '_' (like 'gtk_widget') + * @MODULE: The name of the module, in all caps (like 'GTK') + * @OBJ_NAME: The bare name of the type, in all caps (like 'WIDGET') + * @ParentName: the name of the parent type, in camel case (like GtkWidget) + * + * A convenience macro for emitting the usual declarations in the + * header file for a type which is intended to be subclassed only + * by internal consumers. + * + * This macro differs from %G_DECLARE_DERIVABLE_TYPE and %G_DECLARE_FINAL_TYPE + * by declaring a type that is only derivable internally. Internal users can + * derive this type, assuming they have access to the instance and class + * structures; external users will not be able to subclass this type. + */ +#define PANGO2_DECLARE_INTERNAL_TYPE(ModuleObjName, module_obj_name, MODULE, OBJ_NAME, ParentName) \ + GType module_obj_name##_get_type (void); \ + G_GNUC_BEGIN_IGNORE_DEPRECATIONS \ + typedef struct _##ModuleObjName ModuleObjName; \ + typedef struct _##ModuleObjName##Class ModuleObjName##Class; \ + \ + _GLIB_DEFINE_AUTOPTR_CHAINUP (ModuleObjName, ParentName) \ + G_DEFINE_AUTOPTR_CLEANUP_FUNC (ModuleObjName##Class, g_type_class_unref) \ + \ + G_GNUC_UNUSED static inline ModuleObjName * MODULE##_##OBJ_NAME (gpointer ptr) { \ + return G_TYPE_CHECK_INSTANCE_CAST (ptr, module_obj_name##_get_type (), ModuleObjName); } \ + G_GNUC_UNUSED static inline ModuleObjName##Class * MODULE##_##OBJ_NAME##_CLASS (gpointer ptr) { \ + return G_TYPE_CHECK_CLASS_CAST (ptr, module_obj_name##_get_type (), ModuleObjName##Class); } \ + G_GNUC_UNUSED static inline gboolean MODULE##_IS_##OBJ_NAME (gpointer ptr) { \ + return G_TYPE_CHECK_INSTANCE_TYPE (ptr, module_obj_name##_get_type ()); } \ + G_GNUC_UNUSED static inline gboolean MODULE##_IS_##OBJ_NAME##_CLASS (gpointer ptr) { \ + return G_TYPE_CHECK_CLASS_TYPE (ptr, module_obj_name##_get_type ()); } \ + G_GNUC_UNUSED static inline ModuleObjName##Class * MODULE##_##OBJ_NAME##_GET_CLASS (gpointer ptr) { \ + return G_TYPE_INSTANCE_GET_CLASS (ptr, module_obj_name##_get_type (), ModuleObjName##Class); } \ + G_GNUC_END_IGNORE_DEPRECATIONS + +G_END_DECLS diff --git a/pango2/pango-userface-private.h b/pango2/pango-userface-private.h new file mode 100644 index 00000000..a8a564fc --- /dev/null +++ b/pango2/pango-userface-private.h @@ -0,0 +1,40 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-userface.h" +#include "pango-fontmap.h" +#include "pango-font-face-private.h" + + +struct _Pango2UserFace +{ + Pango2FontFace parent_instance; + + char *faceid; + + Pango2UserFaceGetFontInfoFunc font_info_func; + Pango2UserFaceUnicodeToGlyphFunc glyph_func; + Pango2UserFaceGetGlyphInfoFunc glyph_info_func; + Pango2UserFaceTextToGlyphFunc shape_func; + Pango2UserFaceRenderGlyphFunc render_func; + gpointer user_data; + GDestroyNotify destroy; +}; diff --git a/pango2/pango-userface.c b/pango2/pango-userface.c new file mode 100644 index 00000000..d4cb60f5 --- /dev/null +++ b/pango2/pango-userface.c @@ -0,0 +1,481 @@ +/* Pango2 + * + * Copyright (C) 2022 Matthias Clasen + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include "pango-font-face-private.h" +#include "pango-userface-private.h" +#include "pango-userfont-private.h" +#include "pango-utils.h" +#include "pango-item-private.h" +#include "pango-impl-utils.h" + +#include <string.h> +#include <hb-ot.h> + +/** + * Pango2UserFace: + * + * `Pango2UserFace` is a `Pango2FontFace` implementation that uses callbacks. + * + * It allows to draw the glyphs in a font using custom code. This can + * be used to implement fonts in non-standard formats, but can also be + * used by games and other application to draw "funky" fonts. + * + * To get a font instance at a specific size from a `Pango2UserFace`, + * use [ctor@Pango2.UserFont.new]. + */ + +/* {{{ Utilities */ + +static void +ensure_faceid (Pango2UserFace *self) +{ + Pango2FontFace *face = PANGO2_FONT_FACE (self); + char *psname; + char *p; + + if (self->faceid) + return; + + psname = g_strconcat (pango2_font_description_get_family (face->description), "_", face->name, NULL); + + /* PostScript name should not contain problematic chars, but just in case, + * make sure we don't have any ' ', '=' or ',' that would give us parsing + * problems. + */ + p = psname; + while ((p = strpbrk (p, " =,")) != NULL) + *p = '?'; + + self->faceid = g_strconcat ("user:", psname, NULL); + + g_free (psname); +} + +static const char * +style_from_font_description (const Pango2FontDescription *desc) +{ + Pango2Style style = pango2_font_description_get_style (desc); + Pango2Weight weight = pango2_font_description_get_weight (desc); + + switch (style) + { + case PANGO2_STYLE_ITALIC: + if (weight == PANGO2_WEIGHT_BOLD) + return "Bold Italic"; + else + return "Italic"; + break; + case PANGO2_STYLE_OBLIQUE: + if (weight == PANGO2_WEIGHT_BOLD) + return "Bold Oblique"; + else + return "Oblique"; + break; + case PANGO2_STYLE_NORMAL: + if (weight == PANGO2_WEIGHT_BOLD) + return "Bold"; + else + return "Regular"; + break; + default: ; + } + + return NULL; +} + +static gboolean +default_shape_func (Pango2UserFace *face, + int size, + const char *text, + int length, + const Pango2Analysis *analysis, + Pango2GlyphString *glyphs, + Pango2ShapeFlags flags, + gpointer user_data) +{ + int n_chars; + const char *p; + int cluster = 0; + int i; + int last_cluster; + gboolean is_color; + hb_glyph_extents_t ext; + hb_position_t dummy; + hb_font_t *hb_font; + hb_font_extents_t font_extents; + + n_chars = g_utf8_strlen (text, length); + + pango2_glyph_string_set_size (glyphs, n_chars); + + last_cluster = -1; + + hb_font = pango2_font_get_hb_font (analysis->font); + hb_font_get_h_extents (hb_font, &font_extents); + + p = text; + for (i = 0; i < n_chars; i++) + { + gunichar wc; + Pango2Glyph glyph; + Pango2Rectangle ink_rect; + Pango2Rectangle logical_rect; + + wc = g_utf8_get_char (p); + + if (g_unichar_type (wc) != G_UNICODE_NON_SPACING_MARK) + cluster = p - text; + + if (pango2_is_zero_width (wc)) + glyph = PANGO2_GLYPH_EMPTY; + else if (!face->glyph_func (face, wc, &glyph, face->user_data)) + glyph = PANGO2_GET_UNKNOWN_GLYPH (wc); + + face->glyph_info_func (face, size, glyph, &ext, &dummy, &dummy, &is_color, face->user_data); + pango2_font_get_glyph_extents (analysis->font, glyph, &ink_rect, &logical_rect); + + glyphs->glyphs[i].glyph = glyph; + + glyphs->glyphs[i].attr.is_cluster_start = cluster != last_cluster; + glyphs->glyphs[i].attr.is_color = is_color; + + if (analysis->gravity == PANGO2_GRAVITY_EAST) + { + glyphs->glyphs[i].geometry.x_offset = font_extents.ascender; + glyphs->glyphs[i].geometry.y_offset = - logical_rect.y - (logical_rect.height - ink_rect.height) / 2; + glyphs->glyphs[i].geometry.width = logical_rect.width; + } + else if (analysis->gravity == PANGO2_GRAVITY_WEST) + { + glyphs->glyphs[i].geometry.x_offset = font_extents.descender; + glyphs->glyphs[i].geometry.y_offset = logical_rect.y + (logical_rect.height - ink_rect.height) / 2; + glyphs->glyphs[i].geometry.width = logical_rect.width; + } + else if (analysis->gravity == PANGO2_GRAVITY_SOUTH) + { + glyphs->glyphs[i].geometry.x_offset = 0; + glyphs->glyphs[i].geometry.y_offset = 0; + glyphs->glyphs[i].geometry.width = logical_rect.width; + } + else if (analysis->gravity == PANGO2_GRAVITY_NORTH) + { + glyphs->glyphs[i].geometry.x_offset = 0; + glyphs->glyphs[i].geometry.y_offset = 0; + glyphs->glyphs[i].geometry.width = - logical_rect.width; + } + + glyphs->log_clusters[i] = cluster; + last_cluster = cluster; + + p = g_utf8_next_char (p); + } + + if (analysis->level & 1) + pango2_glyph_string_reverse_range (glyphs, 0, glyphs->num_glyphs); + + return TRUE; +} + +static gboolean +default_render_func (Pango2UserFace *face, + int size, + hb_codepoint_t glyph, + gpointer user_data, + const char *backend_id, + gpointer backend_data) +{ + /* Draw nothing... not very exciting */ + return TRUE; +} + +/* }}} */ +/* {{{ Pango2FontFace implementation */ + +struct _Pango2UserFaceClass +{ + Pango2FontFaceClass parent_class; +}; + +G_DEFINE_FINAL_TYPE (Pango2UserFace, pango2_user_face, PANGO2_TYPE_FONT_FACE) + +static void +pango2_user_face_init (Pango2UserFace *self) +{ +} + +static void +pango2_user_face_finalize (GObject *object) +{ + Pango2UserFace *self = PANGO2_USER_FACE (object); + + g_free (self->faceid); + if (self->destroy) + self->destroy (self->user_data); + + G_OBJECT_CLASS (pango2_user_face_parent_class)->finalize (object); +} + +static gboolean +pango2_user_face_is_synthesized (Pango2FontFace *face) +{ + return TRUE; +} + +static gboolean +pango2_user_face_is_monospace (Pango2FontFace *face) +{ + return FALSE; +} + +static gboolean +pango2_user_face_is_variable (Pango2FontFace *face) +{ + return FALSE; +} + +static gboolean +pango2_user_face_has_char (Pango2FontFace *face, + gunichar wc) +{ + Pango2UserFace *self = PANGO2_USER_FACE (face); + hb_codepoint_t glyph; + + return self->glyph_func (self, wc, &glyph, self->user_data); +} + +static const char * +pango2_user_face_get_faceid (Pango2FontFace *face) +{ + Pango2UserFace *self = PANGO2_USER_FACE (face); + + ensure_faceid (self); + + return self->faceid; +} + +static Pango2Font * +pango2_user_face_create_font (Pango2FontFace *face, + const Pango2FontDescription *desc, + float dpi, + const Pango2Matrix *ctm) +{ + Pango2UserFace *self = PANGO2_USER_FACE (face); + + return PANGO2_FONT (pango2_user_font_new_for_description (self, desc, dpi, ctm)); +} + +static void +pango2_user_face_class_init (Pango2UserFaceClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + Pango2FontFaceClass *face_class = PANGO2_FONT_FACE_CLASS (class); + + object_class->finalize = pango2_user_face_finalize; + + face_class->is_synthesized = pango2_user_face_is_synthesized; + face_class->is_monospace = pango2_user_face_is_monospace; + face_class->is_variable = pango2_user_face_is_variable; + face_class->has_char = pango2_user_face_has_char; + face_class->get_faceid = pango2_user_face_get_faceid; + face_class->create_font = pango2_user_face_create_font; +} + +/* }}} */ + /* {{{ Public API */ + +/** + * Pango2UserFaceGetFontInfoFunc: + * @face: the `Pango2UserFace` + * @size: the size of the font that is being created + * @extents: (out caller-allocates): return location for font extents + * user_data: user data that was passed to [ctor@Pango2.UserFace.new] + * + * Callback to obtain font extents for user fonts. + * + * Typically, the `ascender` and `descender` fields in the @extents + * struct should both the filled with non-negative values that add + * up to @size. + * + * Returns: `TRUE` on success + */ + +/** + * Pango2UserFaceUnicodeToGlyphFunc: + * @face: the `Pango2UserFace` + * @unicode: the Unicode character + * @glyph: (out caller-allocates): return location for the glyph that + * @user_data: user data that was passed to [ctor@Pango2.UserFace.new] + * + * Callback to determine if a user font can render a character, + * and what glyph it will use. + * + * Returns: `TRUE` on success + */ + +/** + * Pango2UserFaceGetGlyphInfoFunc: + * @face: the `Pango2UserFace` + * @size: the size of the font that is queried + * @glyph: the glyph that is being queried + * @extents: (out caller-allocates): return location for the glyphs ink rectangle + * @h_advance: (out caller-allocates): return location for the h advance + * @v_advance: (out caller-allocates): return location for the v advance + * @is_color_glyph: (out caller-allocates): return location for information about + * whether @glyph has color + * @user_data: user data that was passed to [ctor@Pango2.UserFace.new] + * + * Callback to obtain information about a glyph in a user font. + * + * The @extents, @h_advance and @v_advance arguments should be filled with + * values that are scaled according to @size. Note that @y_bearing will typically + * be positive, and @height negative. + * + * Returns: `TRUE` on success + */ + +/** + * Pango2UserFaceTextToGlyphFunc: + * @face: the `Pango2UserFace` + * @size: the size of the font that is used + * @text: the text to shape + * @length: the length of @text + * @analysis: `Pango2Analysis` for @text + * @glyphs: (out caller-allocates): the `Pango2GlyphString` to populate + * @flags: `Pango2ShapeFlags` to use + * @user_data: user data that was pased to [ctor@Pango2.UserFace.new] + * + * Callback to shape a segment of text with a user font. + * + * This callback is optional when creating a user font. If it isn't + * provided, Pango2 will rely on the `Pango2UserFaceUnicodeToGlyphFunc` + * and the `Pango2UserFaceGetGlyphInfo` callback to translate Unicode + * characters to glyphs 1-1, and position the glyphs according to their + * advance widths. + * + * If this callback is provided, it replaces all of Pango2's own shaping. + * The function can implement ligatures, reordering, and other features + * that turn the text-to-glyph mapping into an m-n relationship. The + * function is responsible for filling not just the glyphs and their + * positions, but also cluster information and glyph attributes in + * [struct@Pango2.GlyphVisAttr]. + * + * Returns: `TRUE` on success + */ + +/** + * Pango2UserFaceRenderGlyphFunc: + * @face: the `Pango2UserFace` + * @size: the size of the font that is used + * @glyph: the glyph that is being queried + * @user_data: user data that was pased to [ctor@Pango2.UserFace.new] + * @backend_id: a string identifying the [class@Pango2.Renderer] in use + * @backend_data: backend-specific data + * + * Callback to render a glyph with a user font. + * + * This callback is optional when creating a user font. If it isn't + * provided, the font will not produce any visible output. + * + * The @backend_id identifies the [class@Pango2.Renderer] in use. + * Implementations should return `FALSE` for unsupported backends. + * + * The cairo backend uses the string "cairo" as @backend_id, and + * provides a `cairo_t` as @backend_data. The context is set up + * to render in `font space`, i.e. The transformation is set up + * to map the unit square to @size x @size. If supported, Pango2 + * uses `cairo_user_font_face_set_render_color_glyph_func` to + * allow glyphs to be rendered with colors. For more information, + * see the cairo documentation about user fonts. + * + * Returns: `TRUE` on success + */ + +/** + * pango2_user_face_new: + * @font_info_func: (scope notified): a `Pango2UserFaceGetFontInfoFunc` + * @glyph_func: (scope notified): a `Pango2UserFaceUnicodeToGlyphFunc` + * @glyph_info_func: (scope notified): a `Pango2UserFaceGetGlyphInfoFunc` + * @shape_func: (scope notified) (nullable): a `Pango2UserFaceTextToGlyphFunc` + * @render_func: (scope notified) (nullable): a `Pango2UserFaceRenderGlyphFunc` + * @user_data: user data that will be assed to the callbacks + * @destroy: destroy notify for @user_data + * @name: name for the face + * @description: `Pango2FontDescription` for the font + * + * Creates a new user font face. + * + * A user font face does not rely on font data from a font file, + * but instead uses callbacks to determine glyph extents, positions + * and rendering. + * + * If @shape_func is `NULL`, Pango2 will rely on @glyph_func and + * @glyph_info_func to find and position a glyph for each character. + * + * If @render_func is `NULL`, the font will not produce any visible + * glyphs. + * + * Returns: (transfer full): a newly created `Pango2UserFace` + */ +Pango2UserFace * +pango2_user_face_new (Pango2UserFaceGetFontInfoFunc font_info_func, + Pango2UserFaceUnicodeToGlyphFunc glyph_func, + Pango2UserFaceGetGlyphInfoFunc glyph_info_func, + Pango2UserFaceTextToGlyphFunc shape_func, + Pango2UserFaceRenderGlyphFunc render_func, + gpointer user_data, + GDestroyNotify destroy, + const char *name, + const Pango2FontDescription *description) +{ + Pango2UserFace *self; + Pango2FontFace *face; + + g_return_val_if_fail (font_info_func != NULL, NULL); + g_return_val_if_fail (glyph_func != NULL, NULL); + g_return_val_if_fail (glyph_info_func != NULL, NULL); + g_return_val_if_fail (name != NULL, NULL); + g_return_val_if_fail (description != NULL, NULL); + + self = g_object_new (PANGO2_TYPE_USER_FACE, NULL); + + self->font_info_func = font_info_func; + self->glyph_func = glyph_func; + self->glyph_info_func = glyph_info_func; + self->shape_func = shape_func ? shape_func : default_shape_func; + self->render_func = render_func ? render_func : default_render_func; + self->user_data = user_data; + self->destroy = destroy; + + if (!name) + name = style_from_font_description (description); + + face = PANGO2_FONT_FACE (self); + + face->name = g_strdup (name); + face->description = pango2_font_description_copy (description); + + return self; +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pango-userface.h b/pango2/pango-userface.h new file mode 100644 index 00000000..359ded72 --- /dev/null +++ b/pango2/pango-userface.h @@ -0,0 +1,81 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-types.h> +#include <pango2/pango-glyph.h> +#include <pango2/pango-font-face.h> + +G_BEGIN_DECLS + +typedef struct _Pango2UserFont Pango2UserFont; + +#define PANGO2_TYPE_USER_FACE (pango2_user_face_get_type ()) + +PANGO2_AVAILABLE_IN_ALL +PANGO2_DECLARE_INTERNAL_TYPE (Pango2UserFace, pango2_user_face, PANGO2, USER_FACE, Pango2FontFace) + +typedef gboolean (* Pango2UserFaceGetFontInfoFunc) (Pango2UserFace *face, + int size, + hb_font_extents_t *extents, + gpointer user_data); + +typedef gboolean (* Pango2UserFaceUnicodeToGlyphFunc) (Pango2UserFace *face, + hb_codepoint_t unicode, + hb_codepoint_t *glyph, + gpointer user_data); + +typedef gboolean (* Pango2UserFaceGetGlyphInfoFunc) (Pango2UserFace *face, + int size, + hb_codepoint_t glyph, + hb_glyph_extents_t *extents, + hb_position_t *h_advance, + hb_position_t *v_advance, + gboolean *is_color_glyph, + gpointer user_data); + +typedef gboolean (* Pango2UserFaceTextToGlyphFunc) (Pango2UserFace *face, + int size, + const char *text, + int length, + const Pango2Analysis *analysis, + Pango2GlyphString *glyphs, + Pango2ShapeFlags flags, + gpointer user_data); + +typedef gboolean (* Pango2UserFaceRenderGlyphFunc) (Pango2UserFace *face, + int size, + hb_codepoint_t glyph, + gpointer user_data, + const char *backend_id, + gpointer backend_data); + +PANGO2_AVAILABLE_IN_ALL +Pango2UserFace * pango2_user_face_new (Pango2UserFaceGetFontInfoFunc font_info_func, + Pango2UserFaceUnicodeToGlyphFunc glyph_func, + Pango2UserFaceGetGlyphInfoFunc glyph_info_func, + Pango2UserFaceTextToGlyphFunc shape_func, + Pango2UserFaceRenderGlyphFunc render_func, + gpointer user_data, + GDestroyNotify destroy, + const char *name, + const Pango2FontDescription *description); + +G_END_DECLS diff --git a/pango2/pango-userfont-private.h b/pango2/pango-userfont-private.h new file mode 100644 index 00000000..94be5a18 --- /dev/null +++ b/pango2/pango-userfont-private.h @@ -0,0 +1,30 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-userfont.h" +#include "pango-userface.h" +#include "pango-font-private.h" + + +struct _Pango2UserFont +{ + Pango2Font parent_instance; +}; diff --git a/pango2/pango-userfont.c b/pango2/pango-userfont.c new file mode 100644 index 00000000..6bd4b20c --- /dev/null +++ b/pango2/pango-userfont.c @@ -0,0 +1,442 @@ +/* Pango2 + * + * Copyright (C) 2021 Matthias Clasen + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include "pango-userfont-private.h" + +#include "pango-font-private.h" +#include "pango-font-metrics-private.h" +#include "pango-userface-private.h" +#include "pango-hbfamily-private.h" +#include "pango-impl-utils.h" +#include "pango-language-set-private.h" + +#include <hb-ot.h> + +/** + * Pango2UserFont: + * + * `Pango2UserFont` is a `Pango2Font` implementation that uses callbacks. + */ + +/* {{{ Pango2Font implementation */ + +struct _Pango2UserFontClass +{ + Pango2FontClass parent_class; +}; + +G_DEFINE_FINAL_TYPE (Pango2UserFont, pango2_user_font, PANGO2_TYPE_FONT) + +static void +pango2_user_font_init (Pango2UserFont *self G_GNUC_UNUSED) +{ +} + +static void +pango2_user_font_finalize (GObject *object) +{ + G_OBJECT_CLASS (pango2_user_font_parent_class)->finalize (object); +} + +static Pango2FontDescription * +pango2_user_font_describe (Pango2Font *font) +{ + Pango2FontDescription *desc; + + desc = pango2_font_face_describe (PANGO2_FONT_FACE (font->face)); + pango2_font_description_set_gravity (desc, font->gravity); + pango2_font_description_set_size (desc, font->size); + + return desc; +} + +static void +pango2_user_font_get_glyph_extents (Pango2Font *font, + Pango2Glyph glyph, + Pango2Rectangle *ink_rect, + Pango2Rectangle *logical_rect) +{ + hb_font_t *hb_font = pango2_font_get_hb_font (font); + hb_glyph_extents_t extents; + hb_direction_t direction; + hb_font_extents_t font_extents; + + direction = PANGO2_GRAVITY_IS_VERTICAL (font->gravity) + ? HB_DIRECTION_TTB + : HB_DIRECTION_LTR; + + hb_font_get_extents_for_direction (hb_font, direction, &font_extents); + + if (glyph == PANGO2_GLYPH_EMPTY || (glyph & PANGO2_GLYPH_UNKNOWN_FLAG)) + { + if (ink_rect) + ink_rect->x = ink_rect->y = ink_rect->width = ink_rect->height = 0; + + if (logical_rect) + { + logical_rect->x = logical_rect->width = 0; + logical_rect->y = - font_extents.ascender; + logical_rect->height = font_extents.ascender - font_extents.descender; + } + + return; + } + + hb_font_get_glyph_extents (hb_font, glyph, &extents); + + if (ink_rect) + { + Pango2Rectangle r; + + r.x = extents.x_bearing; + r.y = - extents.y_bearing; + r.width = extents.width; + r.height = - extents.height; + + switch (font->gravity) + { + case PANGO2_GRAVITY_AUTO: + case PANGO2_GRAVITY_SOUTH: + ink_rect->x = r.x; + ink_rect->y = r.y; + ink_rect->width = r.width; + ink_rect->height = r.height; + break; + case PANGO2_GRAVITY_NORTH: + ink_rect->x = - r.x; + ink_rect->y = - r.y; + ink_rect->width = - r.width; + ink_rect->height = - r.height; + break; + case PANGO2_GRAVITY_EAST: + ink_rect->x = r.y; + ink_rect->y = - r.x - r.width; + ink_rect->width = r.height; + ink_rect->height = r.width; + break; + case PANGO2_GRAVITY_WEST: + ink_rect->x = - r.y - r.height; + ink_rect->y = r.x; + ink_rect->width = r.height; + ink_rect->height = r.width; + break; + default: + g_assert_not_reached (); + } + + if (PANGO2_GRAVITY_IS_IMPROPER (font->gravity)) + { + Pango2Matrix matrix = (Pango2Matrix) PANGO2_MATRIX_INIT; + pango2_matrix_scale (&matrix, -1, -1); + pango2_matrix_transform_rectangle (&matrix, ink_rect); + } + } + + if (logical_rect) + { + hb_position_t h_advance; + hb_font_extents_t extents; + + h_advance = hb_font_get_glyph_h_advance (hb_font, glyph); + hb_font_get_h_extents (hb_font, &extents); + + logical_rect->x = 0; + logical_rect->height = extents.ascender - extents.descender; + + switch (font->gravity) + { + case PANGO2_GRAVITY_AUTO: + case PANGO2_GRAVITY_SOUTH: + logical_rect->y = - extents.ascender; + logical_rect->width = h_advance; + break; + case PANGO2_GRAVITY_NORTH: + logical_rect->y = extents.descender; + logical_rect->width = h_advance; + break; + case PANGO2_GRAVITY_EAST: + logical_rect->y = - logical_rect->height / 2; + logical_rect->width = logical_rect->height; + break; + case PANGO2_GRAVITY_WEST: + logical_rect->y = - logical_rect->height / 2; + logical_rect->width = - logical_rect->height; + break; + default: + g_assert_not_reached (); + } + + if (PANGO2_GRAVITY_IS_IMPROPER (font->gravity)) + { + logical_rect->height = - logical_rect->height; + logical_rect->y = - logical_rect->y; + } + } +} + +static Pango2FontMetrics * +pango2_user_font_get_metrics (Pango2Font *font, + Pango2Language *language) +{ + hb_font_t *hb_font = pango2_font_get_hb_font (font); + Pango2FontMetrics *metrics; + hb_font_extents_t extents; + + metrics = pango2_font_metrics_new (); + + hb_font_get_extents_for_direction (hb_font, HB_DIRECTION_LTR, &extents); + + metrics->descent = - extents.descender; + metrics->ascent = extents.ascender; + metrics->height = extents.ascender - extents.descender + extents.line_gap; + + metrics->underline_thickness = PANGO2_SCALE; + metrics->underline_position = - PANGO2_SCALE; + metrics->strikethrough_thickness = PANGO2_SCALE; + metrics->strikethrough_position = metrics->ascent / 2; + metrics->approximate_char_width = 0; /* FIXME */ + metrics->approximate_digit_width = 0; + + return metrics; +} + +static hb_bool_t +nominal_glyph_func (hb_font_t *hb_font, + void *font_data, + hb_codepoint_t unicode, + hb_codepoint_t *glyph, + void *user_data) +{ + Pango2Font *font = font_data; + Pango2UserFace *face = PANGO2_USER_FACE (font->face); + + return face->glyph_func (face, unicode, glyph, face->user_data); +} + +static hb_position_t +glyph_h_advance_func (hb_font_t *hb_font, + void *font_data, + hb_codepoint_t glyph, + void *user_data) +{ + Pango2Font *font = font_data; + Pango2UserFace *face = PANGO2_USER_FACE (font->face); + int size = font->size * font->dpi / 72.; + hb_position_t h_advance, v_advance; + hb_glyph_extents_t glyph_extents; + gboolean is_color; + + face->glyph_info_func (face, size, glyph, + &glyph_extents, + &h_advance, &v_advance, + &is_color, + face->user_data); + + return h_advance; +} + +static hb_bool_t +glyph_extents_func (hb_font_t *hb_font, + void *font_data, + hb_codepoint_t glyph, + hb_glyph_extents_t *extents, + void *user_data) +{ + Pango2Font *font = font_data; + Pango2UserFace *face = PANGO2_USER_FACE (font->face); + int size = font->size * font->dpi / 72.; + hb_position_t h_advance, v_advance; + gboolean is_color; + + face->glyph_info_func (face, size, glyph, + extents, + &h_advance, &v_advance, + &is_color, + face->user_data); + + if (PANGO2_GRAVITY_IS_IMPROPER (font->gravity)) + { + extents->x_bearing = - extents->x_bearing; + extents->y_bearing = - extents->y_bearing; + extents->width = - extents->width; + extents->height = - extents->height; + } + + return TRUE; +} + +static hb_bool_t +font_extents_func (hb_font_t *hb_font, + void *font_data, + hb_font_extents_t *extents, + void *user_data) +{ + Pango2Font *font = font_data; + Pango2UserFace *face = PANGO2_USER_FACE (font->face); + int size = font->size * font->dpi / 72.; + gboolean ret; + + ret = face->font_info_func (face, size, extents, face->user_data); + + if (PANGO2_GRAVITY_IS_IMPROPER (font->gravity)) + { + extents->ascender = - extents->ascender; + extents->descender = - extents->descender; + } + + return ret; +} + +static hb_font_t * +pango2_user_font_create_hb_font (Pango2Font *font) +{ + double x_scale, y_scale; + int size; + hb_blob_t *blob; + hb_face_t *hb_face; + hb_font_t *hb_font; + hb_font_funcs_t *funcs; + + blob = hb_blob_create ("", 0, HB_MEMORY_MODE_READONLY, NULL, NULL); + hb_face = hb_face_create (blob, 0); + hb_font = hb_font_create (hb_face); + + funcs = hb_font_funcs_create (); + + hb_font_funcs_set_nominal_glyph_func (funcs, nominal_glyph_func, NULL, NULL); + hb_font_funcs_set_glyph_h_advance_func (funcs, glyph_h_advance_func, NULL, NULL); + hb_font_funcs_set_glyph_extents_func (funcs, glyph_extents_func, NULL, NULL); + hb_font_funcs_set_font_h_extents_func (funcs, font_extents_func, NULL, NULL); + hb_font_funcs_set_font_v_extents_func (funcs, font_extents_func, NULL, NULL); + + hb_font_set_funcs (hb_font, funcs, font, NULL); + + hb_font_funcs_destroy (funcs); + hb_face_destroy (hb_face); + hb_blob_destroy (blob); + + size = font->size * font->dpi / 72.f; + x_scale = y_scale = 1; + + if (PANGO2_GRAVITY_IS_IMPROPER (font->gravity)) + { + x_scale = - x_scale; + y_scale = - y_scale; + } + + hb_font_set_scale (hb_font, size * x_scale, size * y_scale); + hb_font_set_ptem (hb_font, font->size / PANGO2_SCALE); + + return hb_font; +} + +static void +pango2_user_font_class_init (Pango2UserFontClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + Pango2FontClass *font_class = PANGO2_FONT_CLASS (class); + + object_class->finalize = pango2_user_font_finalize; + + font_class->describe = pango2_user_font_describe; + font_class->get_glyph_extents = pango2_user_font_get_glyph_extents; + font_class->get_metrics = pango2_user_font_get_metrics; + font_class->create_hb_font = pango2_user_font_create_hb_font; +} + +/* }}} */ + /* {{{ Public API */ + +/** + * pango2_user_font_new: + * @face: the `Pango2UserFace` to use + * @size: the desired size in points, scaled by `PANGO2_SCALE` + * @gravity: the gravity to use when rendering + * @dpi: the dpi used when rendering + * @ctm: (nullable): transformation matrix to use when rendering + * + * Creates a new `Pango2UserFont`. + * + * Returns: a newly created `Pango2UserFont` + */ +Pango2UserFont * +pango2_user_font_new (Pango2UserFace *face, + int size, + Pango2Gravity gravity, + float dpi, + const Pango2Matrix *ctm) +{ + Pango2UserFont *self; + Pango2Font *font; + + self = g_object_new (PANGO2_TYPE_USER_FONT, NULL); + + font = PANGO2_FONT (self); + + pango2_font_set_face (font, PANGO2_FONT_FACE (face)); + pango2_font_set_size (font, size); + pango2_font_set_dpi (font, dpi); + pango2_font_set_gravity (font, gravity); + pango2_font_set_ctm (font, ctm); + + return self; +} + +/** + * pango2_user_font_new_for_description: + * @face: the `Pango2UserFace` to use + * @description: a `Pango2FontDescription` + * @dpi: the dpi used when rendering + * @ctm: (nullable): transformation matrix to use when rendering + * + * Creates a new `Pango2UserFont` with size and gravity taken + * from a font description. + * + * Returns: a newly created `Pango2HbFont` + */ + +Pango2UserFont * +pango2_user_font_new_for_description (Pango2UserFace *face, + const Pango2FontDescription *description, + float dpi, + const Pango2Matrix *ctm) +{ + int size; + Pango2Gravity gravity; + + if (pango2_font_description_get_size_is_absolute (description)) + size = pango2_font_description_get_size (description) * 72. / dpi; + else + size = pango2_font_description_get_size (description); + + if ((pango2_font_description_get_set_fields (description) & PANGO2_FONT_MASK_GRAVITY) != 0 && + pango2_font_description_get_gravity (description) != PANGO2_GRAVITY_SOUTH) + gravity = pango2_font_description_get_gravity (description); + else + gravity = PANGO2_GRAVITY_AUTO; + + return pango2_user_font_new (face, size, gravity, dpi, ctm); +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pango-userfont.h b/pango2/pango-userfont.h new file mode 100644 index 00000000..47b23b01 --- /dev/null +++ b/pango2/pango-userfont.h @@ -0,0 +1,46 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-types.h> +#include <pango2/pango-userface.h> +#include <hb.h> + +G_BEGIN_DECLS + +#define PANGO2_TYPE_USER_FONT (pango2_user_font_get_type ()) + +PANGO2_AVAILABLE_IN_ALL +PANGO2_DECLARE_INTERNAL_TYPE (Pango2UserFont, pango2_user_font, PANGO2, USER_FONT, Pango2Font) + +PANGO2_AVAILABLE_IN_ALL +Pango2UserFont * pango2_user_font_new (Pango2UserFace *face, + int size, + Pango2Gravity gravity, + float dpi, + const Pango2Matrix *ctm); + +PANGO2_AVAILABLE_IN_ALL +Pango2UserFont * pango2_user_font_new_for_description (Pango2UserFace *face, + const Pango2FontDescription *description, + float dpi, + const Pango2Matrix *ctm); + +G_END_DECLS diff --git a/pango2/pango-utils.c b/pango2/pango-utils.c new file mode 100644 index 00000000..89e20dac --- /dev/null +++ b/pango2/pango-utils.c @@ -0,0 +1,377 @@ +/* Pango2 + * pango-utils.c: Utilities for internal functions and modules + * + * Copyright (C) 2000 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include <errno.h> +#include <string.h> +#include <stdlib.h> +#include <math.h> +#include <locale.h> + +#include "pango-font.h" +#include "pango-features.h" +#include "pango-impl-utils.h" + +#include <glib/gstdio.h> + +#ifndef HAVE_FLOCKFILE +# define flockfile(f) (void)1 +# define funlockfile(f) (void)1 +# define getc_unlocked(f) getc(f) +#endif /* !HAVE_FLOCKFILE */ + +#ifdef G_OS_WIN32 + +#include <sys/types.h> + +#define STRICT +#include <windows.h> + +#endif + +/** + * pango2_version: + * + * Returns the encoded version of Pango2 available at run-time. + * + * This is similar to the macro %PANGO2_VERSION except that the macro + * returns the encoded version available at compile-time. A version + * number can be encoded into an integer using PANGO2_VERSION_ENCODE(). + * + * Returns: The encoded version of Pango2 library available at run time. + */ +int +pango2_version (void) +{ + return PANGO2_VERSION; +} + +/** + * pango2_version_string: + * + * Returns the version of Pango2 available at run-time. + * + * This is similar to the macro %PANGO2_VERSION_STRING except that the + * macro returns the version available at compile-time. + * + * Returns: A string containing the version of Pango2 library available + * at run time. The returned string is owned by Pango2 and should not + * be modified or freed. + */ +const char * +pango2_version_string (void) +{ + return PANGO2_VERSION_STRING; +} + +/** + * pango2_version_check: + * @required_major: the required major version + * @required_minor: the required minor version + * @required_micro: the required major version + * + * Checks that the Pango2 library in use is compatible with the + * given version. + * + * Generally you would pass in the constants %PANGO2_VERSION_MAJOR, + * %PANGO2_VERSION_MINOR, %PANGO2_VERSION_MICRO as the three arguments + * to this function; that produces a check that the library in use at + * run-time is compatible with the version of Pango2 the application or + * module was compiled against. + * + * Compatibility is defined by two things: first the version + * of the running library is newer than the version + * @required_major.required_minor.@required_micro. Second + * the running library must be binary compatible with the + * version @required_major.required_minor.@required_micro + * (same major version.) + * + * For compile-time version checking use PANGO2_VERSION_CHECK(). + * + * Return value: (nullable): %NULL if the Pango2 library is compatible + * with the given version, or a string describing the version + * mismatch. The returned string is owned by Pango2 and should not + * be modified or freed. + */ +const char * +pango2_version_check (int required_major, + int required_minor, + int required_micro) +{ + int pango2_effective_micro = 100 * PANGO2_VERSION_MINOR + PANGO2_VERSION_MICRO; + int required_effective_micro = 100 * required_minor + required_micro; + + if (required_major > PANGO2_VERSION_MAJOR) + return "Pango2 version too old (major mismatch)"; + if (required_major < PANGO2_VERSION_MAJOR) + return "Pango2 version too new (major mismatch)"; + if (required_effective_micro < pango2_effective_micro - PANGO2_BINARY_AGE) + return "Pango2 version too new (micro mismatch)"; + if (required_effective_micro > pango2_effective_micro) + return "Pango2 version too old (micro mismatch)"; + return NULL; +} + +/** + * pango2_is_zero_width: + * @ch: a Unicode character + * + * Checks if a character that should not be normally rendered. + * + * This includes all Unicode characters with "ZERO WIDTH" in their name, + * as well as *bidi* formatting characters, and a few other ones. + * + * This is totally different from [func@GLib.unichar_iszerowidth] and is at best misnamed. + * + * Return value: %TRUE if @ch is a zero-width character, %FALSE otherwise + */ +gboolean +pango2_is_zero_width (gunichar ch) +{ +/* Zero Width characters: + * + * 00AD SOFT HYPHEN + * 034F COMBINING GRAPHEME JOINER + * + * 200B ZERO WIDTH SPACE + * 200C ZERO WIDTH NON-JOINER + * 200D ZERO WIDTH JOINER + * 200E LEFT-TO-RIGHT MARK + * 200F RIGHT-TO-LEFT MARK + * + * 2028 LINE SEPARATOR + * + * 2060 WORD JOINER + * 2061 FUNCTION APPLICATION + * 2062 INVISIBLE TIMES + * 2063 INVISIBLE SEPARATOR + * + * 2066 LEFT-TO-RIGHT ISOLATE + * 2067 RIGHT-TO-LEFT ISOLATE + * 2068 FIRST STRONG ISOLATE + * 2069 POP DIRECTIONAL ISOLATE + * + * 202A LEFT-TO-RIGHT EMBEDDING + * 202B RIGHT-TO-LEFT EMBEDDING + * 202C POP DIRECTIONAL FORMATTING + * 202D LEFT-TO-RIGHT OVERRIDE + * 202E RIGHT-TO-LEFT OVERRIDE + * + * FEFF ZERO WIDTH NO-BREAK SPACE + */ + return ((ch & ~(gunichar)0x007F) == 0x2000 && ( + (ch >= 0x200B && ch <= 0x200F) || + (ch >= 0x202A && ch <= 0x202E) || + (ch >= 0x2060 && ch <= 0x2063) || + (ch >= 0x2066 && ch <= 0x2069) || + (ch == 0x2028) + )) || G_UNLIKELY (ch == 0x00AD + || ch == 0x034F + || ch == 0xFEFF); +} + +/** + * pango2_units_from_double: + * @d: double floating-point value + * + * Converts a floating-point number to Pango2 units. + * + * The conversion is done by multiplying @d by %PANGO2_SCALE and + * rounding the result to nearest integer. + * + * Return value: the value in Pango2 units. + */ +int +pango2_units_from_double (double d) +{ + return (int)floor (d * PANGO2_SCALE + 0.5); +} + +/** + * pango2_units_to_double: + * @i: value in Pango2 units + * + * Converts a number in Pango2 units to floating-point. + * + * The conversion is done by dividing @i by %PANGO2_SCALE. + * + * Return value: the double value. + */ +double +pango2_units_to_double (int i) +{ + return (double)i / PANGO2_SCALE; +} + +/** + * pango2_extents_to_pixels: + * @inclusive: (nullable): rectangle to round to pixels inclusively + * @nearest: (nullable): rectangle to round to nearest pixels + * + * Converts extents from Pango2 units to device units. + * + * The conversion is done by dividing by the %PANGO2_SCALE factor and + * performing rounding. + * + * The @inclusive rectangle is converted by flooring the x/y coordinates + * and extending width/height, such that the final rectangle completely + * includes the original rectangle. + * + * The @nearest rectangle is converted by rounding the coordinates + * of the rectangle to the nearest device unit (pixel). + * + * The rule to which argument to use is: if you want the resulting device-space + * rectangle to completely contain the original rectangle, pass it in as + * @inclusive. If you want two touching-but-not-overlapping rectangles stay + * touching-but-not-overlapping after rounding to device units, pass them in + * as @nearest. + */ +void +pango2_extents_to_pixels (Pango2Rectangle *inclusive, + Pango2Rectangle *nearest) +{ + if (inclusive) + { + int orig_x = inclusive->x; + int orig_y = inclusive->y; + + inclusive->x = PANGO2_PIXELS_FLOOR (inclusive->x); + inclusive->y = PANGO2_PIXELS_FLOOR (inclusive->y); + + inclusive->width = PANGO2_PIXELS_CEIL (orig_x + inclusive->width ) - inclusive->x; + inclusive->height = PANGO2_PIXELS_CEIL (orig_y + inclusive->height) - inclusive->y; + } + + if (nearest) + { + int orig_x = nearest->x; + int orig_y = nearest->y; + + nearest->x = PANGO2_PIXELS (nearest->x); + nearest->y = PANGO2_PIXELS (nearest->y); + + nearest->width = PANGO2_PIXELS (orig_x + nearest->width ) - nearest->x; + nearest->height = PANGO2_PIXELS (orig_y + nearest->height) - nearest->y; + } +} + +/** + * pango2_find_paragraph_boundary: + * @text: UTF-8 text + * @length: length of @text in bytes, or -1 if nul-terminated + * @paragraph_delimiter_index: (out): return location for index of + * delimiter + * @next_paragraph_start: (out): return location for start of next + * paragraph + * + * Locates a paragraph boundary in @text. + * + * A boundary is caused by delimiter characters, such as + * a newline, carriage return, carriage return-newline pair, + * or Unicode paragraph separator character. + * + * The index of the run of delimiters is returned in + * @paragraph_delimiter_index. The index of the start of the + * next paragraph (index after all delimiters) is stored n + * @next_paragraph_start. + * + * If no delimiters are found, both @paragraph_delimiter_index + * and @next_paragraph_start are filled with the length of @text + * (an index one off the end). + */ +void +pango2_find_paragraph_boundary (const char *text, + int length, + int *paragraph_delimiter_index, + int *next_paragraph_start) +{ + const char *p = text; + const char *end; + const char *start = NULL; + const char *delimiter = NULL; + + /* Only one character has type G_UNICODE_PARAGRAPH_SEPARATOR in + * Unicode 5.0; update the following code if that changes. + */ + + /* prev_sep is the first byte of the previous separator. Since + * the valid separators are \r, \n, and PARAGRAPH_SEPARATOR, the + * first byte is enough to identify it. + */ + char prev_sep; + +#define PARAGRAPH_SEPARATOR_STRING "\xE2\x80\xA9" + + if (length < 0) + length = strlen (text); + + end = text + length; + + if (paragraph_delimiter_index) + *paragraph_delimiter_index = length; + + if (next_paragraph_start) + *next_paragraph_start = length; + + if (length == 0) + return; + + prev_sep = 0; + while (p < end) + { + if (prev_sep == '\n' || + prev_sep == PARAGRAPH_SEPARATOR_STRING[0]) + { + g_assert (delimiter); + start = p; + break; + } + else if (prev_sep == '\r') + { + /* don't break between \r and \n */ + if (*p != '\n') + { + g_assert (delimiter); + start = p; + break; + } + } + + if (*p == '\n' || + *p == '\r' || + !strncmp(p, PARAGRAPH_SEPARATOR_STRING, strlen (PARAGRAPH_SEPARATOR_STRING))) + { + if (delimiter == NULL) + delimiter = p; + prev_sep = *p; + } + else + prev_sep = 0; + + p = g_utf8_next_char (p); + } + + if (delimiter && paragraph_delimiter_index) + *paragraph_delimiter_index = delimiter - text; + + if (start && next_paragraph_start) + *next_paragraph_start = start - text; +} diff --git a/pango2/pango-utils.h b/pango2/pango-utils.h new file mode 100644 index 00000000..f5187dc9 --- /dev/null +++ b/pango2/pango-utils.h @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2000 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdio.h> +#include <glib.h> +#include <pango2/pango-font.h> + +G_BEGIN_DECLS + +PANGO2_AVAILABLE_IN_ALL +gboolean pango2_is_zero_width (gunichar ch) G_GNUC_CONST; + +PANGO2_AVAILABLE_IN_ALL +void pango2_find_paragraph_boundary (const char *text, + int length, + int *paragraph_delimiter_index, + int *next_paragraph_start); + +/** + * PANGO2_RENDERING_CAIRO: + * + * Defined to 1 at compile time if Pango2 was built with cairo support. + */ + +/* Encode a Pango2 version as an integer */ +/* Pango2 version checking */ + +/** + * PANGO2_VERSION_ENCODE: + * @major: the major component of the version number + * @minor: the minor component of the version number + * @micro: the micro component of the version number + * + * This macro encodes the given Pango2 version into an integer. The numbers + * returned by %PANGO2_VERSION and pango2_version() are encoded using this macro. + * Two encoded version numbers can be compared as integers. + */ +#define PANGO2_VERSION_ENCODE(major, minor, micro) ( \ + ((major) * 10000) \ + + ((minor) * 100) \ + + ((micro) * 1)) + +/* Encoded version of Pango2 at compile-time */ +/** + * PANGO2_VERSION: + * + * The version of Pango2 available at compile-time, encoded using PANGO2_VERSION_ENCODE(). + */ +/** + * PANGO2_VERSION_STRING: + * + * A string literal containing the version of Pango2 available at compile-time. + */ +/** + * PANGO2_VERSION_MAJOR: + * + * The major component of the version of Pango2 available at compile-time. + */ +/** + * PANGO2_VERSION_MINOR: + * + * The minor component of the version of Pango2 available at compile-time. + */ +/** + * PANGO2_VERSION_MICRO: + * + * The micro component of the version of Pango2 available at compile-time. + */ +#define PANGO2_VERSION PANGO2_VERSION_ENCODE( \ + PANGO2_VERSION_MAJOR, \ + PANGO2_VERSION_MINOR, \ + PANGO2_VERSION_MICRO) + +/* Check that compile-time Pango2 is as new as required */ +/** + * PANGO2_VERSION_CHECK: + * @major: the major component of the version number + * @minor: the minor component of the version number + * @micro: the micro component of the version number + * + * Checks that the version of Pango2 available at compile-time is not older than + * the provided version number. + */ +#define PANGO2_VERSION_CHECK(major,minor,micro) \ + (PANGO2_VERSION >= PANGO2_VERSION_ENCODE(major,minor,micro)) + +PANGO2_AVAILABLE_IN_ALL +int pango2_version (void) G_GNUC_CONST; + +PANGO2_AVAILABLE_IN_ALL +const char * pango2_version_string (void) G_GNUC_CONST; + +PANGO2_AVAILABLE_IN_ALL +const char * pango2_version_check (int required_major, + int required_minor, + int required_micro) G_GNUC_CONST; + +G_END_DECLS diff --git a/pango2/pango-version-macros.h b/pango2/pango-version-macros.h new file mode 100644 index 00000000..bce4e314 --- /dev/null +++ b/pango2/pango-version-macros.h @@ -0,0 +1,157 @@ +/* + * Copyright (C) 1999 Red Hat Software + * Copyright (C) 2012 Ryan Lortie, Matthias Clasen and Emmanuele Bassi + * Copyright (C) 2016 Chun-wei Fan + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-features.h> + +#include <glib.h> + +#ifndef _PANGO2_EXTERN +#define _PANGO2_EXTERN extern +#endif + +#define PANGO2_AVAILABLE_IN_ALL _PANGO2_EXTERN + +/* XXX: Every new stable minor release bump should add a macro here */ + +/** + * PANGO2_VERSION_0_90 + * + * A macro that evaluates to the 0.90 version of Pango2, in a format + * that can be used by the C pre-processor. + */ +#define PANGO2_VERSION_0_90 (G_ENCODE_VERSION (0, 90)) + +/** + * PANGO2_VERSION_1_0 + * + * A macro that evaluates to the 1.0 version of Pango2, in a format + * that can be used by the C pre-processor. + */ +#define PANGO2_VERSION_1_0 (G_ENCODE_VERSION (1, 0)) + +/* evaluates to the current stable version; for development cycles, + * this means the next stable target + */ +#if (PANGO2_VERSION_MINOR % 2) +#define PANGO2_VERSION_CUR_STABLE (G_ENCODE_VERSION (PANGO2_VERSION_MAJOR, PANGO2_VERSION_MINOR + 1)) +#else +#define PANGO2_VERSION_CUR_STABLE (G_ENCODE_VERSION (PANGO2_VERSION_MAJOR, PANGO2_VERSION_MINOR)) +#endif + +/* evaluates to the previous stable version */ +#if (PANGO2_VERSION_MINOR % 2) +#define PANGO2_VERSION_PREV_STABLE (G_ENCODE_VERSION (PANGO2_VERSION_MAJOR, PANGO2_VERSION_MINOR - 1)) +#else +#define PANGO2_VERSION_PREV_STABLE (G_ENCODE_VERSION (PANGO2_VERSION_MAJOR, PANGO2_VERSION_MINOR - 2)) +#endif + +/** + * PANGO2_VERSION_MIN_REQUIRED: + * + * A macro that should be defined by the user prior to including + * the pango.h header. + * The definition should be one of the predefined Pango2 version + * macros: %PANGO2_VERSION_2_0,… + * + * This macro defines the earliest version of Pango2 that the package is + * required to be able to compile against. + * + * If the compiler is configured to warn about the use of deprecated + * functions, then using functions that were deprecated in version + * %PANGO2_VERSION_MIN_REQUIRED or earlier will cause warnings (but + * using functions deprecated in later releases will not). + */ +/* If the package sets PANGO2_VERSION_MIN_REQUIRED to some future + * PANGO2_VERSION_X_Y value that we don't know about, it will compare as + * 0 in preprocessor tests. + */ +#ifndef PANGO2_VERSION_MIN_REQUIRED +# define PANGO2_VERSION_MIN_REQUIRED (PANGO2_VERSION_CUR_STABLE) +#elif PANGO2_VERSION_MIN_REQUIRED == 0 +# undef PANGO2_VERSION_MIN_REQUIRED +# define PANGO2_VERSION_MIN_REQUIRED (PANGO2_VERSION_CUR_STABLE + 2) +#endif + +/** + * PANGO2_VERSION_MAX_ALLOWED: + * + * A macro that should be defined by the user prior to including + * the glib.h header. + * The definition should be one of the predefined Pango2 version + * macros: %PANGO2_VERSION_2_0,… + * + * This macro defines the latest version of the Pango2 API that the + * package is allowed to make use of. + * + * If the compiler is configured to warn about the use of deprecated + * functions, then using functions added after version + * %PANGO2_VERSION_MAX_ALLOWED will cause warnings. + * + * Unless you are using PANGO2_VERSION_CHECK() or the like to compile + * different code depending on the Pango2 version, then this should be + * set to the same value as %PANGO2_VERSION_MIN_REQUIRED. + */ +#if !defined (PANGO2_VERSION_MAX_ALLOWED) || (PANGO2_VERSION_MAX_ALLOWED == 0) +# undef PANGO2_VERSION_MAX_ALLOWED +# define PANGO2_VERSION_MAX_ALLOWED (PANGO2_VERSION_CUR_STABLE) +#endif + +/* sanity checks */ +#if PANGO2_VERSION_MIN_REQUIRED > PANGO2_VERSION_CUR_STABLE +#error "PANGO2_VERSION_MIN_REQUIRED must be <= PANGO2_VERSION_CUR_STABLE" +#endif +#if PANGO2_VERSION_MAX_ALLOWED < PANGO2_VERSION_MIN_REQUIRED +#error "PANGO2_VERSION_MAX_ALLOWED must be >= PANGO2_VERSION_MIN_REQUIRED" +#endif +#if PANGO2_VERSION_MIN_REQUIRED < PANGO2_VERSION_0_90 +#error "PANGO2_VERSION_MIN_REQUIRED must be >= PANGO2_VERSION_0_90" +#endif + +/* These macros are used to mark deprecated functions in Pango2 headers, + * and thus have to be exposed in installed headers. + */ +#ifdef PANGO2_DISABLE_DEPRECATION_WARNINGS +# define PANGO2_DEPRECATED _PANGO2_EXTERN +# define PANGO2_DEPRECATED_FOR(f) _PANGO2_EXTERN +# define PANGO2_UNAVAILABLE(maj,min) _PANGO2_EXTERN +#else +# define PANGO2_DEPRECATED G_DEPRECATED _PANGO2_EXTERN +# define PANGO2_DEPRECATED_FOR(f) G_DEPRECATED_FOR(f) _PANGO2_EXTERN +# define PANGO2_UNAVAILABLE(maj,min) G_UNAVAILABLE(maj,min) _PANGO2_EXTERN +#endif + +/* XXX: Every new stable minor release should add a set of macros here */ + +#if PANGO2_VERSION_MIN_REQUIRED >= PANGO2_VERSION_1_0 +# define PANGO2_DEPRECATED_IN_1_0 PANGO2_DEPRECATED +# define PANGO2_DEPRECATED_IN_1_0_FOR(f) PANGO2_DEPRECATED_FOR(f) +#else +# define PANGO2_DEPRECATED_IN_1_0 _PANGO2_EXTERN +# define PANGO2_DEPRECATED_IN_1_0_FOR(f) _PANGO2_EXTERN +#endif + +#if PANGO2_VERSION_MAX_ALLOWED < PANGO2_VERSION_1_0 +# define PANGO2_AVAILABLE_IN_1_0 PANGO2_UNAVAILABLE(2, 0) +#else +# define PANGO2_AVAILABLE_IN_1_0 _PANGO2_EXTERN +#endif diff --git a/pango2/pango.h b/pango2/pango.h new file mode 100644 index 00000000..f3486c4d --- /dev/null +++ b/pango2/pango.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 1999 Red Hat Software + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango-attr.h> +#include <pango2/pango-attr-list.h> +#include <pango2/pango-attr-iterator.h> +#include <pango2/pango-attributes.h> +#include <pango2/pango-break.h> +#include <pango2/pango-color.h> +#include <pango2/pango-context.h> +#include <pango2/pango-direction.h> +#include <pango2/pango-enum-types.h> +#include <pango2/pango-features.h> +#include <pango2/pango-font.h> +#include <pango2/pango-font-description.h> +#include <pango2/pango-font-face.h> +#include <pango2/pango-font-family.h> +#include <pango2/pango-font-metrics.h> +#include <pango2/pango-fontmap.h> +#include <pango2/pango-fontset.h> +#include <pango2/pango-generic-family.h> +#include <pango2/pango-glyph.h> +#include <pango2/pango-gravity.h> +#include <pango2/pango-hbface.h> +#include <pango2/pango-hbfont.h> +#include <pango2/pango-item.h> +#include <pango2/pango-language.h> +#include <pango2/pango-layout.h> +#include <pango2/pango-line.h> +#include <pango2/pango-line-breaker.h> +#include <pango2/pango-line-iter.h> +#include <pango2/pango-lines.h> +#include <pango2/pango-markup.h> +#include <pango2/pango-matrix.h> +#include <pango2/pango-renderer.h> +#include <pango2/pango-run.h> +#include <pango2/pango-script.h> +#include <pango2/pango-tabs.h> +#include <pango2/pango-types.h> +#include <pango2/pango-userface.h> +#include <pango2/pango-userfont.h> +#include <pango2/pango-utils.h> +#include <pango2/pango-version-macros.h> diff --git a/pango2/pango.rc.in b/pango2/pango.rc.in new file mode 100644 index 00000000..3af81445 --- /dev/null +++ b/pango2/pango.rc.in @@ -0,0 +1,30 @@ +#include <winver.h> + +VS_VERSION_INFO VERSIONINFO + FILEVERSION @PANGO_VERSION_MAJOR@,@PANGO_VERSION_MINOR@,@PANGO_VERSION_MICRO@,0 + PRODUCTVERSION @PANGO_VERSION_MAJOR@,@PANGO_VERSION_MINOR@,@PANGO_VERSION_MICRO@,0 + FILEFLAGSMASK 0 + FILEFLAGS 0 + FILEOS VOS__WINDOWS32 + FILETYPE VFT_DLL + FILESUBTYPE VFT2_UNKNOWN + BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "CompanyName", "Red Hat Software" + VALUE "FileDescription", "Pango" + VALUE "FileVersion", "@PANGO_VERSION_MAJOR@.@PANGO_VERSION_MINOR@.@PANGO_VERSION_MICRO@.0" + VALUE "InternalName", "pango-@PANGO_API_VERSION@-@PANGO_CURRENT_MINUS_AGE@" + VALUE "LegalCopyright", "Copyright 1999 Red Hat Software." + VALUE "OriginalFilename", "pango-@PANGO_API_VERSION@-@PANGO_CURRENT_MINUS_AGE@.dll" + VALUE "ProductName", "Pango" + VALUE "ProductVersion", "@PANGO_VERSION_MAJOR@.@PANGO_VERSION_MINOR@.@PANGO_VERSION_MICRO@" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END + END diff --git a/pango2/pangocairo-context.c b/pango2/pangocairo-context.c new file mode 100644 index 00000000..2346b635 --- /dev/null +++ b/pango2/pangocairo-context.c @@ -0,0 +1,274 @@ +/* Pango2 + * pangocairo-context.c: Cairo context handling + * + * Copyright (C) 2000-2005 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include "pangocairo.h" +#include "pangocairo-private.h" +#include "pango-impl-utils.h" +#include "pango-context-private.h" + +#include <string.h> + +/** + * pango2_cairo_update_context: + * @cr: a Cairo context + * @context: a `Pango2Context`, from a pangocairo font map + * + * Updates a `Pango2Context` previously created for use with Cairo to + * match the current transformation and target surface of a Cairo + * context. + * + * If any layouts have been created for the context, it's necessary + * to call [method@Pango2.Layout.context_changed] on those layouts. + */ +void +pango2_cairo_update_context (cairo_t *cr, + Pango2Context *context) +{ + cairo_matrix_t cairo_matrix; + cairo_surface_t *target; + Pango2Matrix pango2_matrix; + const Pango2Matrix *current_matrix, identity_matrix = PANGO2_MATRIX_INIT; + const cairo_font_options_t *merged_options; + cairo_font_options_t *old_merged_options; + gboolean changed = FALSE; + + g_return_if_fail (cr != NULL); + g_return_if_fail (PANGO2_IS_CONTEXT (context)); + + target = cairo_get_target (cr); + + if (!context->surface_options) + context->surface_options = cairo_font_options_create (); + cairo_surface_get_font_options (target, context->surface_options); + if (!context->set_options_explicit) + { + if (!context->set_options) + context->set_options = cairo_font_options_create (); + cairo_get_font_options (cr, context->set_options); + } + + old_merged_options = context->merged_options; + context->merged_options = NULL; + + merged_options = pango2_cairo_context_get_merged_font_options (context); + + if (old_merged_options) + { + if (!cairo_font_options_equal (merged_options, old_merged_options)) + changed = TRUE; + cairo_font_options_destroy (old_merged_options); + old_merged_options = NULL; + } + else + changed = TRUE; + + cairo_get_matrix (cr, &cairo_matrix); + pango2_matrix.xx = cairo_matrix.xx; + pango2_matrix.yx = cairo_matrix.yx; + pango2_matrix.xy = cairo_matrix.xy; + pango2_matrix.yy = cairo_matrix.yy; + pango2_matrix.x0 = 0; + pango2_matrix.y0 = 0; + + current_matrix = pango2_context_get_matrix (context); + if (!current_matrix) + current_matrix = &identity_matrix; + + /* layout is matrix-independent if metrics-hinting is off. + * also ignore matrix translation offsets + */ + if ((cairo_font_options_get_hint_metrics (merged_options) != CAIRO_HINT_METRICS_OFF) && + (0 != memcmp (&pango2_matrix, current_matrix, sizeof (Pango2Matrix)))) + changed = TRUE; + + pango2_context_set_matrix (context, &pango2_matrix); + + if (changed) + pango2_context_changed (context); +} + +/** + * pango2_cairo_context_set_font_options: + * @context: a `Pango2Context`, from a pangocairo font map + * @options: (nullable): a `cairo_font_options_t`, or %NULL to unset + * any previously set options. A copy is made. + * + * Sets the font options used when rendering text with this context. + * + * These options override any options that [func@Pango2.cairo_update_context] + * derives from the target surface. + */ +void +pango2_cairo_context_set_font_options (Pango2Context *context, + const cairo_font_options_t *options) +{ + g_return_if_fail (PANGO2_IS_CONTEXT (context)); + + if (!context->set_options && !options) + return; + + if (context->set_options && options && + cairo_font_options_equal (context->set_options, options)) + return; + + if (context->set_options || options) + pango2_context_changed (context); + + if (context->set_options) + cairo_font_options_destroy (context->set_options); + + if (options) + { + context->set_options = cairo_font_options_copy (options); + context->set_options_explicit = TRUE; + } + else + { + context->set_options = NULL; + context->set_options_explicit = FALSE; + } + + if (context->merged_options) + { + cairo_font_options_destroy (context->merged_options); + context->merged_options = NULL; + } +} + +/** + * pango2_cairo_context_get_font_options: + * @context: a `Pango2Context`, from a pangocairo font map + * + * Retrieves any font rendering options previously set with + * [func@Pango2.cairo_context_set_font_options]. + * + * This function does not report options that are derived from + * the target surface by [func@Pango2.cairo_update_context]. + * + * Return value: (nullable): the font options previously set on the + * context, or %NULL if no options have been set. This value is + * owned by the context and must not be modified or freed. + */ +const cairo_font_options_t * +pango2_cairo_context_get_font_options (Pango2Context *context) +{ + g_return_val_if_fail (PANGO2_IS_CONTEXT (context), NULL); + + return context->set_options; +} + +const cairo_font_options_t * +pango2_cairo_context_get_merged_font_options (Pango2Context *context) +{ + if (!context->merged_options) + { + context->merged_options = cairo_font_options_create (); + + if (context->surface_options) + cairo_font_options_merge (context->merged_options, context->surface_options); + if (context->set_options) + cairo_font_options_merge (context->merged_options, context->set_options); + } + + return context->merged_options; +} + +/** + * pango2_cairo_create_context: + * @cr: a Cairo context + * + * Creates a context object set up to match the current transformation + * and target surface of the Cairo context. + * + * This context can then be + * used to create a layout using [ctor@Pango2.Layout.new]. + * + * This function is a convenience function that creates a context + * using the default font map, then updates it to @cr. + * + * Return value: (transfer full): the newly created `Pango2Context` + */ +Pango2Context * +pango2_cairo_create_context (cairo_t *cr) +{ + Pango2Context *context; + + g_return_val_if_fail (cr != NULL, NULL); + + context = pango2_context_new (); + pango2_cairo_update_context (cr, context); + + return context; +} + +/** + * pango2_cairo_update_layout: + * @cr: a Cairo context + * @layout: a `Pango2Layout` + * + * Updates the private `Pango2Context` of a `Pango2Layout` to match + * the current transformation and target surface of a Cairo context. + */ +void +pango2_cairo_update_layout (cairo_t *cr, + Pango2Layout *layout) +{ + g_return_if_fail (cr != NULL); + g_return_if_fail (PANGO2_IS_LAYOUT (layout)); + + pango2_cairo_update_context (cr, pango2_layout_get_context (layout)); +} + +/** + * pango2_cairo_create_layout: + * @cr: a Cairo context + * + * Creates a layout object set up to match the current transformation + * and target surface of the Cairo context. + * + * This layout can then be used for text measurement with functions + * like [method@Pango2.Lines.get_size] or drawing with functions like + * [func@Pango2.cairo_show_layout]. If you change the transformation or target + * surface for @cr, you need to call [func@Pango2.cairo_update_layout]. + * + * This function is the most convenient way to use Cairo with Pango2, + * however it is slightly inefficient since it creates a separate + * `Pango2Context` object for each layout. This might matter in an + * application that was laying out large amounts of text. + * + * Return value: (transfer full): the newly created `Pango2Layout` + */ +Pango2Layout * +pango2_cairo_create_layout (cairo_t *cr) +{ + Pango2Context *context; + Pango2Layout *layout; + + g_return_val_if_fail (cr != NULL, NULL); + + context = pango2_cairo_create_context (cr); + layout = pango2_layout_new (context); + g_object_unref (context); + + return layout; +} diff --git a/pango2/pangocairo-context.h b/pango2/pangocairo-context.h new file mode 100644 index 00000000..5532beab --- /dev/null +++ b/pango2/pangocairo-context.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 1999, 2004 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango.h> +#include <cairo.h> + +G_BEGIN_DECLS + +PANGO2_AVAILABLE_IN_ALL +Pango2Context * pango2_cairo_create_context (cairo_t *cr); +PANGO2_AVAILABLE_IN_ALL +void pango2_cairo_update_context (cairo_t *cr, + Pango2Context *context); +PANGO2_AVAILABLE_IN_ALL +Pango2Layout * pango2_cairo_create_layout (cairo_t *cr); +PANGO2_AVAILABLE_IN_ALL +void pango2_cairo_update_layout (cairo_t *cr, + Pango2Layout *layout); +PANGO2_AVAILABLE_IN_ALL +void pango2_cairo_context_set_font_options (Pango2Context *context, + const cairo_font_options_t *options); +PANGO2_AVAILABLE_IN_ALL +const cairo_font_options_t * + pango2_cairo_context_get_font_options (Pango2Context *context); + +G_END_DECLS diff --git a/pango2/pangocairo-dwrite-font.cpp b/pango2/pangocairo-dwrite-font.cpp new file mode 100644 index 00000000..ed5b4f0c --- /dev/null +++ b/pango2/pangocairo-dwrite-font.cpp @@ -0,0 +1,53 @@ +/* Pango + * pangocairo-dwrite-font.cpp: PangoCairo fonts using DirectWrite + * + * Copyright (C) 2022 the GTK team + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#ifdef HAVE_DIRECT_WRITE + +#include <windows.h> +#include <dwrite.h> +#include <hb-directwrite.h> +#include <cairo-win32.h> + +#include "pangocairo-private.h" + +/* {{{ DirectWrite PangoCairo utilities */ +cairo_font_face_t * +pango_cairo_create_font_face_for_dwrite_pango_font (PangoFont *font) +{ + hb_font_t *hb_font; + IDWriteFontFace *dwrite_font_face = NULL; + cairo_font_face_t *result; + + hb_font = pango_font_get_hb_font (font); + dwrite_font_face = hb_directwrite_face_get_font_face (hb_font_get_face (hb_font)); + + result = cairo_dwrite_font_face_create_for_dwrite_fontface (dwrite_font_face); + + return result; +} + +#endif /* HAVE_DIRECT_WRITE */ + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pangocairo-font.c b/pango2/pangocairo-font.c new file mode 100644 index 00000000..3eda3d5a --- /dev/null +++ b/pango2/pangocairo-font.c @@ -0,0 +1,796 @@ +/* Pango2 + * pangocairo-font.c: Cairo font handling + * + * Copyright (C) 2000-2005 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include <math.h> +#include <string.h> + +#include "pangocairo.h" +#include "pangocairo-private.h" +#include "pango-font-private.h" +#include "pango-font-metrics-private.h" +#include "pango-impl-utils.h" +#include "pango-hbfont-private.h" +#include "pango-hbface-private.h" +#include "pango-userfont-private.h" +#include "pango-userface-private.h" +#include "pango-font-private.h" + +#if defined (HAVE_CORE_TEXT) + +#include <Carbon/Carbon.h> +#include <cairo-quartz.h> +#include <hb-coretext.h> + +#elif defined (HAVE_FONTCONFIG) + +#include <hb-ot.h> +#include <cairo-ft.h> +#include <freetype/ftmm.h> + +#endif + +static Pango2CairoFontPrivate * _pango2_font_get_cairo_font_private (Pango2Font *font); +static cairo_scaled_font_t * _pango2_font_get_scaled_font (Pango2Font *font); +static void _pango2_cairo_font_private_initialize (Pango2CairoFontPrivate *cf_priv, + Pango2Font *font, + Pango2Gravity gravity, + const cairo_font_options_t *font_options, + const Pango2Matrix *pango2_ctm, + const cairo_matrix_t *font_matrix); +static void _pango2_cairo_font_private_finalize (Pango2CairoFontPrivate *cf_priv); + + + +static Pango2CairoFontPrivateScaledFontData * +_pango2_cairo_font_private_scaled_font_data_create (void) +{ + return g_slice_new (Pango2CairoFontPrivateScaledFontData); +} + +static void +_pango2_cairo_font_private_scaled_font_data_destroy (Pango2CairoFontPrivateScaledFontData *data) +{ + if (data) + { + cairo_font_options_destroy (data->options); + g_slice_free (Pango2CairoFontPrivateScaledFontData, data); + } +} + +static cairo_user_data_key_t cairo_user_data; + +static cairo_status_t +render_func (cairo_scaled_font_t *scaled_font, + unsigned long glyph, + cairo_t *cr, + cairo_text_extents_t *extents) +{ + cairo_font_face_t *font_face; + Pango2Font *font; + Pango2UserFace *face; + hb_glyph_extents_t glyph_extents; + hb_position_t h_advance; + hb_position_t v_advance; + gboolean is_color; + + font_face = cairo_scaled_font_get_font_face (scaled_font); + font = cairo_font_face_get_user_data (font_face, &cairo_user_data); + face = PANGO2_USER_FACE (font->face); + + extents->x_bearing = 0; + extents->y_bearing = 0; + extents->width = 0; + extents->height = 0; + extents->x_advance = 0; + extents->y_advance = 0; + + if (!face->glyph_info_func (face, 1024, + (hb_codepoint_t)glyph, + &glyph_extents, + &h_advance, &v_advance, + &is_color, + face->user_data)) + { + return CAIRO_STATUS_USER_FONT_ERROR; + } + + extents->x_bearing = glyph_extents.x_bearing / 1024.; + extents->y_bearing = - glyph_extents.y_bearing / 1024.; + extents->width = glyph_extents.width / 1024.; + extents->height = - glyph_extents.height / 1024.; + extents->x_advance = h_advance / 1024.; + extents->y_advance = v_advance / 1024.; + + if (!face->render_func (face, font->size, + (hb_codepoint_t)glyph, + face->user_data, + "cairo", + cr)) + { + return CAIRO_STATUS_USER_FONT_ERROR; + } + + return CAIRO_STATUS_SUCCESS; +} + +static cairo_status_t +init_func (cairo_scaled_font_t *scaled_font, + cairo_t *cr, + cairo_font_extents_t *extents) +{ + cairo_font_face_t *cairo_face; + Pango2Font *font; + Pango2UserFace *face; + hb_font_extents_t font_extents; + + cairo_face = cairo_scaled_font_get_font_face (scaled_font); + font = cairo_font_face_get_user_data (cairo_face, &cairo_user_data); + face = (Pango2UserFace *) pango2_font_get_face (font); + + face->font_info_func (face, + pango2_font_get_size (font), + &font_extents, + face->user_data); + + extents->ascent = font_extents.ascender / (font_extents.ascender + font_extents.descender); + extents->descent = font_extents.descender / (font_extents.ascender + font_extents.descender); + + return CAIRO_STATUS_SUCCESS; +} + +static cairo_font_face_t * +create_cairo_font_face_for_user_font (Pango2Font *font) +{ + cairo_font_face_t *cairo_face; + + cairo_face = cairo_user_font_face_create (); + cairo_font_face_set_user_data (cairo_face, &cairo_user_data, font, NULL); + cairo_user_font_face_set_init_func (cairo_face, init_func); + cairo_user_font_face_set_render_color_glyph_func (cairo_face, render_func); + + return cairo_face; +} + +#if defined (HAVE_CORE_TEXT) + +static cairo_font_face_t * +create_cairo_font_face_for_hb_font (Pango2Font *font) +{ + hb_font_t *hbfont; + CTFontRef ctfont; + CGFontRef cgfont; + cairo_font_face_t *cairo_face; + + hbfont = pango2_font_get_hb_font (font); + ctfont = hb_coretext_font_get_ct_font (hbfont); + cgfont = CTFontCopyGraphicsFont (ctfont, NULL); + + cairo_face = cairo_quartz_font_face_create_for_cgfont (cgfont); + + CFRelease (cgfont); + + return cairo_face; +} + +#elif defined (HAVE_DIRECT_WRITE) + +static cairo_font_face_t * +create_cairo_font_face_for_hb_font (Pango2Font *font) +{ + return pango2_cairo_create_font_face_for_dwrite_pango2_font (font); +} + +#else + +static cairo_font_face_t * +create_cairo_font_face_for_hb_font (Pango2Font *font) +{ + static FT_Library ft_library; + + Pango2HbFace *face = PANGO2_HB_FACE (font->face); + hb_blob_t *blob; + const char *blob_data; + unsigned int blob_length; + FT_Face ft_face; + hb_font_t *hb_font; + unsigned int num_coords; + const int *coords; + cairo_font_face_t *cairo_face; + static const cairo_user_data_key_t key; + static const cairo_user_data_key_t key2; + FT_Error error; + + if (g_once_init_enter (&ft_library)) + { + FT_Library library; + FT_Init_FreeType (&library); + g_once_init_leave (&ft_library, library); + } + + hb_font = pango2_font_get_hb_font (font); + blob = hb_face_reference_blob (hb_font_get_face (hb_font)); + blob_data = hb_blob_get_data (blob, &blob_length); + + if ((error = FT_New_Memory_Face (ft_library, + (const FT_Byte *) blob_data, + blob_length, + hb_face_get_index (face->face), + &ft_face)) != 0) + { + hb_blob_destroy (blob); + g_warning ("FT_New_Memory_Face failed: %d %s", error, FT_Error_String (error)); + return NULL; + } + + coords = hb_font_get_var_coords_normalized (hb_font, &num_coords); + if (num_coords > 0) + { + FT_Fixed *ft_coords = (FT_Fixed *) g_alloca (num_coords * sizeof (FT_Fixed)); + + for (unsigned int i = 0; i < num_coords; i++) + ft_coords[i] = coords[i] << 2; + + FT_Set_Var_Blend_Coordinates (ft_face, num_coords, ft_coords); + } + + cairo_face = cairo_ft_font_face_create_for_ft_face (ft_face, FT_LOAD_NO_HINTING | FT_LOAD_COLOR); + + if (face->embolden) + cairo_ft_font_face_set_synthesize (cairo_face, CAIRO_FT_SYNTHESIZE_BOLD); + + cairo_font_face_set_user_data (cairo_face, &key, + ft_face, (cairo_destroy_func_t) FT_Done_Face); + cairo_font_face_set_user_data (cairo_face, &key2, + blob, (cairo_destroy_func_t) hb_blob_destroy); + + return cairo_face; +} + +#endif + +static cairo_scaled_font_t * +_pango2_cairo_font_private_get_scaled_font (Pango2CairoFontPrivate *cf_priv) +{ + cairo_font_face_t *font_face; + + if (G_LIKELY (cf_priv->scaled_font)) + return cf_priv->scaled_font; + + /* need to create it */ + + if (G_UNLIKELY (cf_priv->data == NULL)) + { + /* we have tried to create and failed before */ + return NULL; + } + + if (PANGO2_IS_HB_FONT (cf_priv->cfont)) + font_face = create_cairo_font_face_for_hb_font (cf_priv->cfont); + else if (PANGO2_IS_USER_FONT (cf_priv->cfont)) + font_face = create_cairo_font_face_for_user_font (cf_priv->cfont); + + if (G_UNLIKELY (font_face == NULL)) + goto done; + + cf_priv->scaled_font = cairo_scaled_font_create (font_face, + &cf_priv->data->font_matrix, + &cf_priv->data->ctm, + cf_priv->data->options); + + cairo_font_face_destroy (font_face); + +done: + + if (G_UNLIKELY (cf_priv->scaled_font == NULL || cairo_scaled_font_status (cf_priv->scaled_font) != CAIRO_STATUS_SUCCESS)) + { + cairo_scaled_font_t *scaled_font = cf_priv->scaled_font; + Pango2Font *font = cf_priv->cfont; + static GQuark warned_quark = 0; /* MT-safe */ + if (!warned_quark) + warned_quark = g_quark_from_static_string ("pangocairo-scaledfont-warned"); + + if (!g_object_get_qdata (G_OBJECT (font), warned_quark)) + { + Pango2FontDescription *desc; + char *s; + + desc = pango2_font_describe (font); + s = pango2_font_description_to_string (desc); + pango2_font_description_free (desc); + + g_warning ("failed to create cairo %s, expect ugly output. the offending font is '%s'", + font_face ? "scaled font" : "font face", + s); + + if (!font_face) + g_warning ("font_face is NULL"); + else + g_warning ("font_face status is: %s", + cairo_status_to_string (cairo_font_face_status (font_face))); + + if (!scaled_font) + g_warning ("scaled_font is NULL"); + else + g_warning ("scaled_font status is: %s", + cairo_status_to_string (cairo_scaled_font_status (scaled_font))); + + g_free (s); + + g_object_set_qdata_full (G_OBJECT (font), warned_quark, + GINT_TO_POINTER (1), NULL); + } + } + + _pango2_cairo_font_private_scaled_font_data_destroy (cf_priv->data); + cf_priv->data = NULL; + + return cf_priv->scaled_font; +} + +cairo_scaled_font_t * +_pango2_font_get_scaled_font (Pango2Font *font) +{ + Pango2CairoFontPrivate *cf_priv; + + cf_priv = _pango2_font_get_cairo_font_private (font); + + if (G_UNLIKELY (!cf_priv)) + return NULL; + + return _pango2_cairo_font_private_get_scaled_font (cf_priv); +} + +/** + * _pango2_cairo_font_install: + * @font: a `Pango2CairoFont` + * @cr: a #cairo_t + * + * Makes @font the current font for rendering in the specified + * Cairo context. + * + * Return value: %TRUE if font was installed successfully, %FALSE otherwise. + */ +gboolean +_pango2_cairo_font_install (Pango2Font *font, + cairo_t *cr) +{ + cairo_scaled_font_t *scaled_font; + + scaled_font = _pango2_font_get_scaled_font (font); + + if (G_UNLIKELY (scaled_font == NULL || cairo_scaled_font_status (scaled_font) != CAIRO_STATUS_SUCCESS)) + return FALSE; + + cairo_set_scaled_font (cr, scaled_font); + + return TRUE; +} + +static Pango2CairoFontHexBoxInfo * +_pango2_cairo_font_private_get_hex_box_info (Pango2CairoFontPrivate *cf_priv) +{ + const char hexdigits[] = "0123456789ABCDEF"; + char c[2] = {0, 0}; + Pango2Font *mini_font; + Pango2CairoFontHexBoxInfo *hbi; + + /* for metrics hinting */ + double scale_x = 1., scale_x_inv = 1., scale_y = 1., scale_y_inv = 1.; + gboolean is_hinted; + + int i; + int rows; + double pad; + double width = 0; + double height = 0; + cairo_font_options_t *font_options; + cairo_font_extents_t font_extents; + double size, mini_size; + Pango2FontDescription *desc; + cairo_scaled_font_t *scaled_font, *scaled_mini_font; + Pango2Matrix pango2_ctm, pango2_font_matrix; + cairo_matrix_t cairo_ctm, cairo_font_matrix; + /*Pango2Gravity gravity;*/ + + if (!cf_priv) + return NULL; + + if (cf_priv->hbi) + return cf_priv->hbi; + + scaled_font = _pango2_cairo_font_private_get_scaled_font (cf_priv); + if (G_UNLIKELY (scaled_font == NULL || cairo_scaled_font_status (scaled_font) != CAIRO_STATUS_SUCCESS)) + return NULL; + + is_hinted = cf_priv->is_hinted; + + font_options = cairo_font_options_create (); + desc = pango2_font_describe_with_absolute_size (cf_priv->cfont); + /*gravity = pango2_font_description_get_gravity (desc);*/ + + cairo_scaled_font_get_ctm (scaled_font, &cairo_ctm); + cairo_scaled_font_get_font_matrix (scaled_font, &cairo_font_matrix); + cairo_scaled_font_get_font_options (scaled_font, font_options); + /* I started adding support for vertical hexboxes here, but it's too much + * work. Easier to do with cairo user fonts and vertical writing mode + * support in cairo. + */ + /*cairo_matrix_rotate (&cairo_ctm, pango2_gravity_to_rotation (gravity));*/ + pango2_ctm.xx = cairo_ctm.xx; + pango2_ctm.yx = cairo_ctm.yx; + pango2_ctm.xy = cairo_ctm.xy; + pango2_ctm.yy = cairo_ctm.yy; + pango2_ctm.x0 = cairo_ctm.x0; + pango2_ctm.y0 = cairo_ctm.y0; + pango2_font_matrix.xx = cairo_font_matrix.xx; + pango2_font_matrix.yx = cairo_font_matrix.yx; + pango2_font_matrix.xy = cairo_font_matrix.xy; + pango2_font_matrix.yy = cairo_font_matrix.yy; + pango2_font_matrix.x0 = cairo_font_matrix.x0; + pango2_font_matrix.y0 = cairo_font_matrix.y0; + + size = pango2_matrix_get_font_scale_factor (&pango2_font_matrix) / + pango2_matrix_get_font_scale_factor (&pango2_ctm); + + if (is_hinted) + { + /* prepare for some hinting */ + double x, y; + + x = 1.; y = 0.; + cairo_matrix_transform_distance (&cairo_ctm, &x, &y); + scale_x = sqrt (x*x + y*y); + scale_x_inv = 1 / scale_x; + + x = 0.; y = 1.; + cairo_matrix_transform_distance (&cairo_ctm, &x, &y); + scale_y = sqrt (x*x + y*y); + scale_y_inv = 1 / scale_y; + } + +/* we hint to the nearest device units */ +#define HINT(value, scale, scale_inv) (ceil ((value-1e-5) * scale) * scale_inv) +#define HINT_X(value) HINT ((value), scale_x, scale_x_inv) +#define HINT_Y(value) HINT ((value), scale_y, scale_y_inv) + + /* create mini_font description */ + { + Pango2FontFace *face; + Pango2FontFamily *family; + Pango2FontMap *fontmap; + Pango2Context *context; + + /* XXX this is racy. need a ref'ing getter... */ + face = pango2_font_get_face (cf_priv->cfont); + family = pango2_font_face_get_family (face); + if (!family) + return NULL; + fontmap = pango2_font_family_get_font_map (family); + if (!fontmap) + return NULL; + fontmap = g_object_ref (fontmap); + + /* we inherit most font properties for the mini font. just + * change family and size. means, you get bold hex digits + * in the hexbox for a bold font. + */ + + /* We should rotate the box, not glyphs */ + pango2_font_description_unset_fields (desc, PANGO2_FONT_MASK_GRAVITY); + + pango2_font_description_set_family_static (desc, "monospace"); + + rows = 2; + mini_size = size / 2.2; + if (is_hinted) + { + mini_size = HINT_Y (mini_size); + + if (mini_size < 6.0) + { + rows = 1; + mini_size = MIN (MAX (size - 1, 0), 6.0); + } + } + + pango2_font_description_set_absolute_size (desc, pango2_units_from_double (mini_size)); + + /* load mini_font */ + + context = pango2_context_new_with_font_map (fontmap); + + pango2_context_set_matrix (context, &pango2_ctm); + pango2_context_set_language (context, pango2_script_get_sample_language (G_UNICODE_SCRIPT_LATIN)); + pango2_cairo_context_set_font_options (context, font_options); + mini_font = pango2_font_map_load_font (fontmap, context, desc); + + g_object_unref (context); + g_object_unref (fontmap); + } + + pango2_font_description_free (desc); + cairo_font_options_destroy (font_options); + + scaled_mini_font = _pango2_font_get_scaled_font (mini_font); + if (G_UNLIKELY (scaled_mini_font == NULL || cairo_scaled_font_status (scaled_mini_font) != CAIRO_STATUS_SUCCESS)) + return NULL; + + for (i = 0 ; i < 16 ; i++) + { + cairo_text_extents_t extents; + + c[0] = hexdigits[i]; + cairo_scaled_font_text_extents (scaled_mini_font, c, &extents); + width = MAX (width, extents.width); + height = MAX (height, extents.height); + } + + cairo_scaled_font_extents (scaled_font, &font_extents); + if (font_extents.ascent + font_extents.descent <= 0) + { + font_extents.ascent = PANGO2_UNKNOWN_GLYPH_HEIGHT; + font_extents.descent = 0; + } + + pad = (font_extents.ascent + font_extents.descent) / 43; + pad = MIN (pad, mini_size); + + hbi = g_slice_new (Pango2CairoFontHexBoxInfo); + hbi->font = mini_font; + hbi->rows = rows; + + hbi->digit_width = width; + hbi->digit_height = height; + + hbi->pad_x = pad; + hbi->pad_y = pad; + + if (is_hinted) + { + hbi->digit_width = HINT_X (hbi->digit_width); + hbi->digit_height = HINT_Y (hbi->digit_height); + hbi->pad_x = HINT_X (hbi->pad_x); + hbi->pad_y = HINT_Y (hbi->pad_y); + } + + hbi->line_width = MIN (hbi->pad_x, hbi->pad_y); + + hbi->box_height = 3 * hbi->pad_y + rows * (hbi->pad_y + hbi->digit_height); + + if (rows == 1 || hbi->box_height <= font_extents.ascent) + { + hbi->box_descent = 2 * hbi->pad_y; + } + else if (hbi->box_height <= font_extents.ascent + font_extents.descent - 2 * hbi->pad_y) + { + hbi->box_descent = 2 * hbi->pad_y + hbi->box_height - font_extents.ascent; + } + else + { + hbi->box_descent = font_extents.descent * hbi->box_height / + (font_extents.ascent + font_extents.descent); + } + if (is_hinted) + { + hbi->box_descent = HINT_Y (hbi->box_descent); + } + + cf_priv->hbi = hbi; + return hbi; +} + +static void +_pango2_cairo_font_hex_box_info_destroy (Pango2CairoFontHexBoxInfo *hbi) +{ + if (hbi) + { + g_object_unref (hbi->font); + g_slice_free (Pango2CairoFontHexBoxInfo, hbi); + } +} + +static void +free_cairo_font_private (gpointer data) +{ + Pango2CairoFontPrivate *cf_priv = data; + _pango2_cairo_font_private_finalize (cf_priv); + g_free (data); +} + +static Pango2CairoFontPrivate * +_pango2_font_get_cairo_font_private (Pango2Font *font) +{ + Pango2CairoFontPrivate *cf_priv; + + cf_priv = g_object_get_data (G_OBJECT (font), "pango-font-cairo_private"); + if (!cf_priv) + { + cairo_font_options_t *font_options; + cairo_matrix_t font_matrix; + double x_scale, y_scale; + int size; + + cairo_matrix_init (&font_matrix, 1., 0., 0., 1., 0., 0.); + x_scale = y_scale = 1; + + if (PANGO2_IS_HB_FONT (font)) + { + Pango2HbFace *face = PANGO2_HB_FACE (font->face); + if (face->transform) + cairo_matrix_init (&font_matrix, + face->transform->xx, + - face->transform->yx, + - face->transform->xy, + face->transform->yy, + 0., 0.); + + x_scale = face->x_scale; + y_scale = face->y_scale; + } + + size = font->size * font->dpi / 72.; + + cairo_matrix_scale (&font_matrix, + x_scale * size / (double)PANGO2_SCALE, + y_scale * size / (double)PANGO2_SCALE); + + font_options = (cairo_font_options_t *)pango2_cairo_font_get_font_options (font); + if (font_options) + font_options = cairo_font_options_copy (font_options); + else + { + font_options = cairo_font_options_create (); + cairo_font_options_set_hint_style (font_options, CAIRO_HINT_STYLE_NONE); + cairo_font_options_set_hint_metrics (font_options, CAIRO_HINT_METRICS_OFF); + } + + cf_priv = g_new0 (Pango2CairoFontPrivate, 1); + _pango2_cairo_font_private_initialize (cf_priv, + font, + font->gravity, + font_options, + &font->ctm, + &font_matrix); + + cairo_font_options_destroy (font_options); + + g_object_set_data_full (G_OBJECT (font), "pango-font-cairo_private", + cf_priv, free_cairo_font_private); + } + + return cf_priv; +} + +Pango2CairoFontHexBoxInfo * +_pango2_cairo_font_get_hex_box_info (Pango2Font *font) +{ + Pango2CairoFontPrivate *cf_priv = _pango2_font_get_cairo_font_private (font); + + return _pango2_cairo_font_private_get_hex_box_info (cf_priv); +} + +void +_pango2_cairo_font_private_initialize (Pango2CairoFontPrivate *cf_priv, + Pango2Font *cfont, + Pango2Gravity gravity, + const cairo_font_options_t *font_options, + const Pango2Matrix *pango2_ctm, + const cairo_matrix_t *font_matrix) +{ + cairo_matrix_t gravity_matrix; + + cf_priv->cfont = cfont; + cf_priv->gravity = gravity != PANGO2_GRAVITY_AUTO ? gravity : PANGO2_GRAVITY_SOUTH; + + cf_priv->data = _pango2_cairo_font_private_scaled_font_data_create (); + + /* first apply gravity rotation, then font_matrix, such that + * vertical italic text comes out "correct". we don't do anything + * like baseline adjustment etc though. should be specially + * handled when we support italic correction. + */ + cairo_matrix_init_rotate (&gravity_matrix, + pango2_gravity_to_rotation (cf_priv->gravity)); + cairo_matrix_multiply (&cf_priv->data->font_matrix, + font_matrix, + &gravity_matrix); + + if (pango2_ctm) + cairo_matrix_init (&cf_priv->data->ctm, + pango2_ctm->xx, + pango2_ctm->yx, + pango2_ctm->xy, + pango2_ctm->yy, + 0., 0.); + else + cairo_matrix_init_identity (&cf_priv->data->ctm); + + cf_priv->data->options = cairo_font_options_copy (font_options); + cf_priv->is_hinted = cairo_font_options_get_hint_metrics (font_options) != CAIRO_HINT_METRICS_OFF; + + cf_priv->scaled_font = NULL; + cf_priv->hbi = NULL; +} + +void +_pango2_cairo_font_private_finalize (Pango2CairoFontPrivate *cf_priv) +{ + _pango2_cairo_font_private_scaled_font_data_destroy (cf_priv->data); + + if (cf_priv->scaled_font) + cairo_scaled_font_destroy (cf_priv->scaled_font); + cf_priv->scaled_font = NULL; + + _pango2_cairo_font_hex_box_info_destroy (cf_priv->hbi); + cf_priv->hbi = NULL; +} + +/** + * pango2_cairo_font_set_font_options: + * @font: a `Pango2Font` + * @options: (nullable): a `cairo_font_options_t`, or %NULL to unset + * any previously set options. A copy is made. + * + * Sets the font options used when rendering text with this font. + * + * This is rarely needed. Fonts usually get font options from the + * `Pango2Context` in which they are loaded. + */ +void +pango2_cairo_font_set_font_options (Pango2Font *font, + const cairo_font_options_t *options) +{ + g_return_if_fail (PANGO2_IS_FONT (font)); + + if (!font->options && !options) + return; + + if (font->options && options && + cairo_font_options_equal (font->options, options)) + return; + + if (font->options) + cairo_font_options_destroy (font->options); + + if (options) + font->options = cairo_font_options_copy (options); + else + font->options = NULL; +} + +/** + * pango2_cairo_font_get_font_options: + * @font: a `Pango2Font` + * + * Gets font options for the font. + * + * Returns: (transfer none): font options that are + * applied when rendering text with this font + */ +const cairo_font_options_t * +pango2_cairo_font_get_font_options (Pango2Font *font) +{ + g_return_val_if_fail (PANGO2_IS_FONT (font), NULL); + + return font->options; +} diff --git a/pango2/pangocairo-font.h b/pango2/pangocairo-font.h new file mode 100644 index 00000000..9c57318d --- /dev/null +++ b/pango2/pangocairo-font.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 1999, 2004 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango.h> +#include <cairo.h> + +G_BEGIN_DECLS + +PANGO2_AVAILABLE_IN_ALL +void pango2_cairo_font_set_font_options (Pango2Font *font, + const cairo_font_options_t *options); +PANGO2_AVAILABLE_IN_ALL +const cairo_font_options_t * + pango2_cairo_font_get_font_options (Pango2Font *font); + +G_END_DECLS diff --git a/pango2/pangocairo-private.h b/pango2/pangocairo-private.h new file mode 100644 index 00000000..d3ad6994 --- /dev/null +++ b/pango2/pangocairo-private.h @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2000, 2004 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pangocairo.h" +#include "pango-renderer.h" + +G_BEGIN_DECLS + +typedef struct _Pango2CairoFontPrivate Pango2CairoFontPrivate; +typedef struct _HexBoxInfo Pango2CairoFontHexBoxInfo; +typedef struct _Pango2CairoFontPrivateScaledFontData Pango2CairoFontPrivateScaledFontData; + +struct _Pango2CairoFontPrivateScaledFontData +{ + cairo_matrix_t font_matrix; + cairo_matrix_t ctm; + cairo_font_options_t *options; +}; + +struct _Pango2CairoFontPrivate +{ + Pango2Font *cfont; + + Pango2CairoFontPrivateScaledFontData *data; + + cairo_scaled_font_t *scaled_font; + Pango2CairoFontHexBoxInfo *hbi; + + gboolean is_hinted; + Pango2Gravity gravity; + + Pango2Rectangle font_extents; +}; + +gboolean _pango2_cairo_font_install (Pango2Font *font, + cairo_t *cr); +Pango2CairoFontHexBoxInfo *_pango2_cairo_font_get_hex_box_info (Pango2Font *font); + +#define PANGO2_TYPE_CAIRO_RENDERER (pango2_cairo_renderer_get_type()) +#define PANGO2_CAIRO_RENDERER(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), PANGO2_TYPE_CAIRO_RENDERER, Pango2CairoRenderer)) +#define PANGO2_IS_CAIRO_RENDERER(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), PANGO2_TYPE_CAIRO_RENDERER)) + +typedef struct _Pango2CairoRenderer Pango2CairoRenderer; + +_PANGO2_EXTERN +GType pango2_cairo_renderer_get_type (void) G_GNUC_CONST; + +const cairo_font_options_t * + pango2_cairo_context_get_merged_font_options (Pango2Context *context); + +#ifdef HAVE_DIRECT_WRITE +cairo_font_face_t * +pango2_cairo_create_font_face_for_dwrite_pango2_font (Pango2Font *font); +#endif + +G_END_DECLS diff --git a/pango2/pangocairo-render.c b/pango2/pangocairo-render.c new file mode 100644 index 00000000..b5189bee --- /dev/null +++ b/pango2/pangocairo-render.c @@ -0,0 +1,1255 @@ +/* Pango2 + * pangocairo-render.c: Rendering routines to Cairo surfaces + * + * Copyright (C) 2004 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include <math.h> + +#include "pango-item-private.h" +#include "pango-font-private.h" +#include "pangocairo-private.h" +#include "pango-glyph-item-private.h" +#include "pango-glyph-iter-private.h" +#include "pango-run-private.h" +#include "pango-impl-utils.h" +#include "pango-hbfont-private.h" + +typedef struct _Pango2CairoRendererClass Pango2CairoRendererClass; + +#define PANGO2_CAIRO_RENDERER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), PANGO2_TYPE_CAIRO_RENDERER, Pango2CairoRendererClass)) +#define PANGO2_IS_CAIRO_RENDERER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), PANGO2_TYPE_CAIRO_RENDERER)) +#define PANGO2_CAIRO_RENDERER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), PANGO2_TYPE_CAIRO_RENDERER, Pango2CairoRendererClass)) + +struct _Pango2CairoRenderer +{ + Pango2Renderer parent_instance; + + cairo_t *cr; + gboolean do_path; + gboolean has_show_text_glyphs; + double x_offset, y_offset; + + /* house-keeping options */ + gboolean is_cached_renderer; + gboolean cr_had_current_point; +}; + +struct _Pango2CairoRendererClass +{ + Pango2RendererClass parent_class; +}; + +G_DEFINE_TYPE (Pango2CairoRenderer, pango2_cairo_renderer, PANGO2_TYPE_RENDERER) + +static void +set_color (Pango2CairoRenderer *crenderer, + Pango2RenderPart part) +{ + Pango2Color *color = pango2_renderer_get_color ((Pango2Renderer *) (crenderer), part); + double red, green, blue, alpha; + + if (!color) + return; + + if (color) + { + red = color->red / 65535.; + green = color->green / 65535.; + blue = color->blue / 65535.; + alpha = color->alpha / 65535.; + } + else + { + cairo_pattern_t *pattern = cairo_get_source (crenderer->cr); + + if (pattern && cairo_pattern_get_type (pattern) == CAIRO_PATTERN_TYPE_SOLID) + cairo_pattern_get_rgba (pattern, &red, &green, &blue, &alpha); + else + { + red = 0.; + green = 0.; + blue = 0.; + alpha = 1.; + } + } + + cairo_set_source_rgba (crenderer->cr, red, green, blue, alpha); +} + +/* note: modifies crenderer->cr without doing cairo_save/restore() */ +static void +_pango2_cairo_renderer_draw_frame (Pango2CairoRenderer *crenderer, + double x, + double y, + double width, + double height, + double line_width, + gboolean invalid) +{ + cairo_t *cr = crenderer->cr; + + if (crenderer->do_path) + { + double d2 = line_width * .5, d = line_width; + + /* we draw an outer box in one winding direction and an inner one in the + * opposite direction. This works for both cairo windings rules. + * + * what we really want is cairo_stroke_to_path(), but that's not + * implemented in cairo yet. + */ + + /* outer */ + cairo_rectangle (cr, x-d2, y-d2, width+d, height+d); + + /* inner */ + if (invalid) + { + /* delicacies of computing the joint... this is REALLY slow */ + + double alpha, tan_alpha2, cos_alpha; + double sx, sy; + + alpha = atan2 (height, width); + + tan_alpha2 = tan (alpha * .5); + if (tan_alpha2 < 1e-5 || (sx = d2 / tan_alpha2, 2. * sx > width - d)) + sx = (width - d) * .5; + + cos_alpha = cos (alpha); + if (cos_alpha < 1e-5 || (sy = d2 / cos_alpha, 2. * sy > height - d)) + sy = (height - d) * .5; + + /* top triangle */ + cairo_new_sub_path (cr); + cairo_line_to (cr, x+width-sx, y+d2); + cairo_line_to (cr, x+sx, y+d2); + cairo_line_to (cr, x+.5*width, y+.5*height-sy); + cairo_close_path (cr); + + /* bottom triangle */ + cairo_new_sub_path (cr); + cairo_line_to (cr, x+width-sx, y+height-d2); + cairo_line_to (cr, x+.5*width, y+.5*height+sy); + cairo_line_to (cr, x+sx, y+height-d2); + cairo_close_path (cr); + + + alpha = G_PI_2 - alpha; + tan_alpha2 = tan (alpha * .5); + if (tan_alpha2 < 1e-5 || (sy = d2 / tan_alpha2, 2. * sy > height - d)) + sy = (height - d) * .5; + + cos_alpha = cos (alpha); + if (cos_alpha < 1e-5 || (sx = d2 / cos_alpha, 2. * sx > width - d)) + sx = (width - d) * .5; + + /* left triangle */ + cairo_new_sub_path (cr); + cairo_line_to (cr, x+d2, y+sy); + cairo_line_to (cr, x+d2, y+height-sy); + cairo_line_to (cr, x+.5*width-sx, y+.5*height); + cairo_close_path (cr); + + /* right triangle */ + cairo_new_sub_path (cr); + cairo_line_to (cr, x+width-d2, y+sy); + cairo_line_to (cr, x+.5*width+sx, y+.5*height); + cairo_line_to (cr, x+width-d2, y+height-sy); + cairo_close_path (cr); + } + else + cairo_rectangle (cr, x+width-d2, y+d2, - (width-d), height-d); + } + else + { + cairo_rectangle (cr, x, y, width, height); + + if (invalid) + { + /* draw an X */ + + cairo_new_sub_path (cr); + cairo_move_to (cr, x, y); + cairo_rel_line_to (cr, width, height); + + cairo_new_sub_path (cr); + cairo_move_to (cr, x + width, y); + cairo_rel_line_to (cr, -width, height); + + cairo_set_line_cap (cr, CAIRO_LINE_CAP_BUTT); + } + + cairo_set_line_width (cr, line_width); + cairo_set_line_join (cr, CAIRO_LINE_JOIN_MITER); + cairo_set_miter_limit (cr, 2.); + cairo_stroke (cr); + } +} + +static void +_pango2_cairo_renderer_draw_box_glyph (Pango2CairoRenderer *crenderer, + Pango2GlyphInfo *gi, + double cx, + double cy, + gboolean invalid) +{ + cairo_save (crenderer->cr); + + _pango2_cairo_renderer_draw_frame (crenderer, + cx + 1.5, + cy + 1.5 - PANGO2_UNKNOWN_GLYPH_HEIGHT, + (double)gi->geometry.width / PANGO2_SCALE - 3.0, + PANGO2_UNKNOWN_GLYPH_HEIGHT - 3.0, + 1.0, + invalid); + + cairo_restore (crenderer->cr); +} + +static void +_pango2_cairo_renderer_draw_unknown_glyph (Pango2CairoRenderer *crenderer, + Pango2Font *font, + Pango2GlyphInfo *gi, + double cx, + double cy) +{ + char buf[7]; + double x0, y0; + int row, col; + int rows, cols; + double width, lsb; + char hexbox_string[2] = { 0, 0 }; + Pango2CairoFontHexBoxInfo *hbi; + gunichar ch; + gboolean invalid_input; + const char *p; + const char *name; + + cairo_save (crenderer->cr); + + ch = gi->glyph & ~PANGO2_GLYPH_UNKNOWN_FLAG; + invalid_input = G_UNLIKELY (gi->glyph == PANGO2_GLYPH_INVALID_INPUT || ch > 0x10FFFF); + + if (PANGO2_IS_HB_FONT (font)) + hbi = PANGO2_HB_FONT (font)->hex_box_info; + else + hbi = _pango2_cairo_font_get_hex_box_info (font); + if (!hbi || !_pango2_cairo_font_install ((Pango2Font *)(hbi->font), crenderer->cr)) + { + _pango2_cairo_renderer_draw_box_glyph (crenderer, gi, cx, cy, invalid_input); + goto done; + } + + if (G_UNLIKELY (invalid_input)) + { + rows = hbi->rows; + cols = 1; + } + else if (ch == 0x2423 || + g_unichar_type (ch) == G_UNICODE_SPACE_SEPARATOR) + { + /* We never want to show a hex box or other drawing for + * space. If we want space to be visible, we replace 0x20 + * by 0x2423 (visible space). + * + * Since we don't want to rely on glyph availability, + * we render a centered dot ourselves. + */ + double x = cx + 0.5 *((double)gi->geometry.width / PANGO2_SCALE); + double y = cy + hbi->box_descent - 0.5 * hbi->box_height; + + cairo_new_sub_path (crenderer->cr); + cairo_arc (crenderer->cr, x, y, 1.5 * hbi->line_width, 0, 2 * G_PI); + cairo_close_path (crenderer->cr); + cairo_fill (crenderer->cr); + goto done; + } + else if (ch == '\t') + { + /* Since we don't want to rely on glyph availability, + * we render an arrow like ↦ ourselves. + */ + double y = cy + hbi->box_descent - 0.5 * hbi->box_height; + double width = (double)gi->geometry.width / PANGO2_SCALE; + double offset = 0.2 * width; + double x = cx + offset; + double al = width - 2 * offset; /* arrow length */ + double tl = MIN (hbi->digit_width, 0.75 * al); /* tip length */ + double tw2 = 2.5 * hbi->line_width; /* tip width / 2 */ + double lw2 = 0.5 * hbi->line_width; /* line width / 2 */ + + cairo_move_to (crenderer->cr, x - lw2, y - tw2); + cairo_line_to (crenderer->cr, x + lw2, y - tw2); + cairo_line_to (crenderer->cr, x + lw2, y - lw2); + cairo_line_to (crenderer->cr, x + al - tl, y - lw2); + cairo_line_to (crenderer->cr, x + al - tl, y - tw2); + cairo_line_to (crenderer->cr, x + al, y); + cairo_line_to (crenderer->cr, x + al - tl, y + tw2); + cairo_line_to (crenderer->cr, x + al - tl, y + lw2); + cairo_line_to (crenderer->cr, x + lw2, y + lw2); + cairo_line_to (crenderer->cr, x + lw2, y + tw2); + cairo_line_to (crenderer->cr, x - lw2, y + tw2); + cairo_close_path (crenderer->cr); + cairo_fill (crenderer->cr); + goto done; + } + else if (ch == '\n' || ch == 0x2028 || ch == 0x2029) + { + /* Since we don't want to rely on glyph availability, + * we render an arrow like ↵ ourselves. + */ + double width = (double)gi->geometry.width / PANGO2_SCALE; + double offset = 0.2 * width; + double al = width - 2 * offset; /* arrow length */ + double tl = MIN (hbi->digit_width, 0.75 * al); /* tip length */ + double ah = al - 0.5 * tl; /* arrow height */ + double tw2 = 2.5 * hbi->line_width; /* tip width / 2 */ + double x = cx + offset; + double y = cy - (hbi->box_height - al) / 2; + double lw2 = 0.5 * hbi->line_width; /* line width / 2 */ + + cairo_move_to (crenderer->cr, x, y); + cairo_line_to (crenderer->cr, x + tl, y - tw2); + cairo_line_to (crenderer->cr, x + tl, y - lw2); + cairo_line_to (crenderer->cr, x + al - lw2, y - lw2); + cairo_line_to (crenderer->cr, x + al - lw2, y - ah); + cairo_line_to (crenderer->cr, x + al + lw2, y - ah); + cairo_line_to (crenderer->cr, x + al + lw2, y + lw2); + cairo_line_to (crenderer->cr, x + tl, y + lw2); + cairo_line_to (crenderer->cr, x + tl, y + tw2); + cairo_close_path (crenderer->cr); + cairo_fill (crenderer->cr); + goto done; + } + else if ((name = pango2_get_ignorable_size (ch, &rows, &cols))) + { + /* Nothing else to do, we render 'default ignorable' chars + * as hex box with their nick. + */ + } + else + { + /* Everything else gets a traditional hex box. */ + rows = hbi->rows; + cols = (ch > 0xffff ? 6 : 4) / rows; + g_snprintf (buf, sizeof(buf), (ch > 0xffff) ? "%06X" : "%04X", ch); + name = buf; + } + + width = (3 * hbi->pad_x + cols * (hbi->digit_width + hbi->pad_x)); + lsb = ((double)gi->geometry.width / PANGO2_SCALE - width) * .5; + lsb = floor (lsb / hbi->pad_x) * hbi->pad_x; + + _pango2_cairo_renderer_draw_frame (crenderer, + cx + lsb + .5 * hbi->pad_x, + cy + hbi->box_descent - hbi->box_height + hbi->pad_y * 0.5, + width - hbi->pad_x, + (hbi->box_height - hbi->pad_y), + hbi->line_width, + invalid_input); + + if (invalid_input) + goto done; + + x0 = cx + lsb + hbi->pad_x * 2; + y0 = cy + hbi->box_descent - hbi->pad_y * 2 - ((hbi->rows - rows) * hbi->digit_height / 2); + + for (row = 0, p = name; row < rows; row++) + { + double y = y0 - (rows - 1 - row) * (hbi->digit_height + hbi->pad_y); + for (col = 0; col < cols; col++, p++) + { + double x = x0 + col * (hbi->digit_width + hbi->pad_x); + + if (!p) + goto done; + + cairo_move_to (crenderer->cr, x, y); + + hexbox_string[0] = p[0]; + + if (crenderer->do_path) + cairo_text_path (crenderer->cr, hexbox_string); + else + cairo_show_text (crenderer->cr, hexbox_string); + } + } + +done: + cairo_restore (crenderer->cr); +} + +#ifndef STACK_BUFFER_SIZE +#define STACK_BUFFER_SIZE (512 * sizeof (int)) +#endif + +#define STACK_ARRAY_LENGTH(T) (STACK_BUFFER_SIZE / sizeof(T)) + +static void +pango2_cairo_renderer_show_text_glyphs (Pango2Renderer *renderer, + const char *text, + int text_len, + Pango2GlyphString *glyphs, + cairo_text_cluster_t *clusters, + int num_clusters, + gboolean backward, + Pango2Font *font, + int x, + int y) +{ + Pango2CairoRenderer *crenderer = (Pango2CairoRenderer *) (renderer); + + int i, count; + int x_position = 0; + cairo_glyph_t *cairo_glyphs; + cairo_glyph_t stack_glyphs[STACK_ARRAY_LENGTH (cairo_glyph_t)]; + double base_x = crenderer->x_offset + (double)x / PANGO2_SCALE; + double base_y = crenderer->y_offset + (double)y / PANGO2_SCALE; + + cairo_save (crenderer->cr); + if (!crenderer->do_path) + set_color (crenderer, PANGO2_RENDER_PART_FOREGROUND); + + if (!_pango2_cairo_font_install (font, crenderer->cr)) + { + for (i = 0; i < glyphs->num_glyphs; i++) + { + Pango2GlyphInfo *gi = &glyphs->glyphs[i]; + + if (gi->glyph != PANGO2_GLYPH_EMPTY) + { + double cx = base_x + (double)(x_position + gi->geometry.x_offset) / PANGO2_SCALE; + double cy = gi->geometry.y_offset == 0 ? + base_y : + base_y + (double)(gi->geometry.y_offset) / PANGO2_SCALE; + + _pango2_cairo_renderer_draw_unknown_glyph (crenderer, font, gi, cx, cy); + } + x_position += gi->geometry.width; + } + + goto done; + } + + if (glyphs->num_glyphs > (int) G_N_ELEMENTS (stack_glyphs)) + cairo_glyphs = g_new (cairo_glyph_t, glyphs->num_glyphs); + else + cairo_glyphs = stack_glyphs; + + count = 0; + for (i = 0; i < glyphs->num_glyphs; i++) + { + Pango2GlyphInfo *gi = &glyphs->glyphs[i]; + + if (gi->glyph != PANGO2_GLYPH_EMPTY) + { + double cx = base_x + (double)(x_position + gi->geometry.x_offset) / PANGO2_SCALE; + double cy = gi->geometry.y_offset == 0 ? + base_y : + base_y + (double)(gi->geometry.y_offset) / PANGO2_SCALE; + + if (gi->glyph & PANGO2_GLYPH_UNKNOWN_FLAG) + { + if (gi->glyph == (0x20 | PANGO2_GLYPH_UNKNOWN_FLAG)) + ; /* no hex boxes for space, please */ + else + _pango2_cairo_renderer_draw_unknown_glyph (crenderer, font, gi, cx, cy); + } + else + { + cairo_glyphs[count].index = gi->glyph; + cairo_glyphs[count].x = cx; + cairo_glyphs[count].y = cy; + count++; + } + } + x_position += gi->geometry.width; + } + + if (G_UNLIKELY (crenderer->do_path)) + cairo_glyph_path (crenderer->cr, cairo_glyphs, count); + else + if (G_UNLIKELY (clusters)) + cairo_show_text_glyphs (crenderer->cr, + text, text_len, + cairo_glyphs, count, + clusters, num_clusters, + backward ? CAIRO_TEXT_CLUSTER_FLAG_BACKWARD : 0); + else + cairo_show_glyphs (crenderer->cr, cairo_glyphs, count); + + if (cairo_glyphs != stack_glyphs) + g_free (cairo_glyphs); + +done: + cairo_restore (crenderer->cr); +} + +static void +pango2_cairo_renderer_draw_glyphs (Pango2Renderer *renderer, + Pango2Font *font, + Pango2GlyphString *glyphs, + int x, + int y) +{ + pango2_cairo_renderer_show_text_glyphs (renderer, NULL, 0, glyphs, NULL, 0, FALSE, font, x, y); +} + +static void +pango2_cairo_renderer_draw_run (Pango2Renderer *renderer, + const char *text, + Pango2Run *run, + int x, + int y) +{ + Pango2CairoRenderer *crenderer = (Pango2CairoRenderer *) (renderer); + Pango2Item *item = pango2_run_get_item (run); + Pango2GlyphString *glyphs = pango2_run_get_glyphs (run); + Pango2Font *font = item->analysis.font; + gboolean backward = (item->analysis.level & 1) != 0; + Pango2GlyphItem *glyph_item = pango2_run_get_glyph_item (run); + Pango2GlyphItemIter iter; + cairo_text_cluster_t *cairo_clusters; + cairo_text_cluster_t stack_clusters[STACK_ARRAY_LENGTH (cairo_text_cluster_t)]; + int num_clusters; + + if (!crenderer->has_show_text_glyphs || crenderer->do_path) + { + pango2_cairo_renderer_show_text_glyphs (renderer, NULL, 0, glyphs, NULL, 0, FALSE, font, x, y); + return; + } + + if (glyphs->num_glyphs > (int) G_N_ELEMENTS (stack_clusters)) + cairo_clusters = g_new (cairo_text_cluster_t, glyphs->num_glyphs); + else + cairo_clusters = stack_clusters; + + num_clusters = 0; + if (pango2_glyph_item_iter_init_start (&iter, glyph_item, text)) + { + do { + int num_bytes, num_glyphs, i; + + num_bytes = iter.end_index - iter.start_index; + num_glyphs = backward ? iter.start_glyph - iter.end_glyph : iter.end_glyph - iter.start_glyph; + + if (num_bytes < 1) + g_warning ("pango2_cairo_renderer_draw_glyph_item: bad cluster has num_bytes %d", num_bytes); + if (num_glyphs < 1) + g_warning ("pango2_cairo_renderer_draw_glyph_item: bad cluster has num_glyphs %d", num_glyphs); + + /* Discount empty and unknown glyphs */ + for (i = MIN (iter.start_glyph, iter.end_glyph+1); + i < MAX (iter.start_glyph+1, iter.end_glyph); + i++) + { + Pango2GlyphInfo *gi = &glyphs->glyphs[i]; + + if (gi->glyph == PANGO2_GLYPH_EMPTY || + gi->glyph & PANGO2_GLYPH_UNKNOWN_FLAG) + num_glyphs--; + } + + cairo_clusters[num_clusters].num_bytes = num_bytes; + cairo_clusters[num_clusters].num_glyphs = num_glyphs; + num_clusters++; + } while (pango2_glyph_item_iter_next_cluster (&iter)); + } + + pango2_cairo_renderer_show_text_glyphs (renderer, + text + item->offset, item->length, + glyphs, + cairo_clusters, num_clusters, + backward, + font, + x, y); + + if (cairo_clusters != stack_clusters) + g_free (cairo_clusters); +} + +static void +pango2_cairo_renderer_draw_rectangle (Pango2Renderer *renderer, + Pango2RenderPart part, + int x, + int y, + int width, + int height) +{ + Pango2CairoRenderer *crenderer = (Pango2CairoRenderer *) (renderer); + + if (!crenderer->do_path) + { + cairo_save (crenderer->cr); + + set_color (crenderer, part); + } + + cairo_rectangle (crenderer->cr, + crenderer->x_offset + (double)x / PANGO2_SCALE, + crenderer->y_offset + (double)y / PANGO2_SCALE, + (double)width / PANGO2_SCALE, (double)height / PANGO2_SCALE); + + if (!crenderer->do_path) + { + cairo_fill (crenderer->cr); + + cairo_restore (crenderer->cr); + } +} + +static void +pango2_cairo_renderer_draw_trapezoid (Pango2Renderer *renderer, + Pango2RenderPart part, + double y1_, + double x11, + double x21, + double y2, + double x12, + double x22) +{ + Pango2CairoRenderer *crenderer = (Pango2CairoRenderer *) (renderer); + cairo_t *cr; + double x, y; + + cr = crenderer->cr; + + cairo_save (cr); + + if (!crenderer->do_path) + set_color (crenderer, part); + + x = crenderer->x_offset, + y = crenderer->y_offset; + cairo_user_to_device_distance (cr, &x, &y); + cairo_identity_matrix (cr); + cairo_translate (cr, x, y); + + cairo_move_to (cr, x11, y1_); + cairo_line_to (cr, x21, y1_); + cairo_line_to (cr, x22, y2); + cairo_line_to (cr, x12, y2); + cairo_close_path (cr); + + if (!crenderer->do_path) + cairo_fill (cr); + + cairo_restore (cr); +} + +/* Draws an error underline that looks like one of: + * H E H + * /\ /\ /\ /\ /\ - + * A/ \ / \ / \ A/ \ / \ | + * \ \ / \ / /D \ \ / \ | + * \ \/ C \/ / \ \/ C \ | height = HEIGHT_SQUARES * square + * \ /\ F / \ F /\ \ | + * \ / \ / \ / \ \G | + * \ / \ / \ / \ / | + * \/ \/ \/ \/ - + * B B + * |---| + * unit_width = (HEIGHT_SQUARES - 1) * square + * + * The x, y, width, height passed in give the desired bounding box; + * x/width are adjusted to make the underline a integer number of units + * wide. + */ +#define HEIGHT_SQUARES 2.5 + +static void +draw_wavy_line (cairo_t *cr, + double x, + double y, + double width, + double height) +{ + double square = height / HEIGHT_SQUARES; + double unit_width = (HEIGHT_SQUARES - 1) * square; + double double_width = 2 * unit_width; + int width_units = (width + unit_width / 2) / unit_width; + double y_top, y_bottom; + double x_left, x_middle, x_right; + int i; + + x += (width - width_units * unit_width) / 2; + + y_top = y; + y_bottom = y + height; + + /* Bottom of squiggle */ + x_middle = x + unit_width; + x_right = x + double_width; + cairo_move_to (cr, x - square / 2, y_top + square / 2); /* A */ + for (i = 0; i < width_units-2; i += 2) + { + cairo_line_to (cr, x_middle, y_bottom); /* B */ + cairo_line_to (cr, x_right, y_top + square); /* C */ + + x_middle += double_width; + x_right += double_width; + } + cairo_line_to (cr, x_middle, y_bottom); /* B */ + + if (i + 1 == width_units) + cairo_line_to (cr, x_middle + square / 2, y_bottom - square / 2); /* G */ + else if (i + 2 == width_units) { + cairo_line_to (cr, x_right + square / 2, y_top + square / 2); /* D */ + cairo_line_to (cr, x_right, y_top); /* E */ + } + + /* Top of squiggle */ + x_left = x_middle - unit_width; + for (; i >= 0; i -= 2) + { + cairo_line_to (cr, x_middle, y_bottom - square); /* F */ + cairo_line_to (cr, x_left, y_top); /* H */ + + x_left -= double_width; + x_middle -= double_width; + } +} + +static void +pango2_cairo_renderer_draw_styled_line (Pango2Renderer *renderer, + Pango2RenderPart part, + Pango2LineStyle style, + int x, + int y, + int width, + int height) +{ + Pango2CairoRenderer *crenderer = (Pango2CairoRenderer *) (renderer); + cairo_t *cr = crenderer->cr; + + if (!crenderer->do_path) + { + cairo_save (cr); + + set_color (crenderer, part); + + cairo_new_path (cr); + } + + switch (style) + { + case PANGO2_LINE_STYLE_NONE: + break; + + case PANGO2_LINE_STYLE_DOTTED: + case PANGO2_LINE_STYLE_DASHED: + cairo_save (cr); + cairo_set_line_width (cr, (double)height / PANGO2_SCALE); + if (style == PANGO2_LINE_STYLE_DOTTED) + { + cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND); + cairo_set_dash (cr, (const double []){0., (double)(2 * height) / PANGO2_SCALE}, 2, 0.); + } + else + { + cairo_set_line_cap (cr, CAIRO_LINE_CAP_SQUARE); + cairo_set_dash (cr, (const double []){(double)(3 * height) / PANGO2_SCALE, (double)(3 * height) / PANGO2_SCALE}, 2, 0.); + } + cairo_move_to (cr, + crenderer->x_offset + (double)x / PANGO2_SCALE + (double)height / (2 * PANGO2_SCALE), + crenderer->y_offset + (double)y / PANGO2_SCALE + (double)height / (2 * PANGO2_SCALE)); + cairo_line_to (cr, + crenderer->x_offset + (double)x / PANGO2_SCALE + (double)width / PANGO2_SCALE - (double)height / PANGO2_SCALE, + crenderer->y_offset + (double)y / PANGO2_SCALE + (double)height / (2 * PANGO2_SCALE)); + cairo_stroke (cr); + cairo_restore (cr); + break; + + case PANGO2_LINE_STYLE_SOLID: + cairo_rectangle (cr, + crenderer->x_offset + (double)x / PANGO2_SCALE, + crenderer->y_offset + (double)y / PANGO2_SCALE, + (double)width / PANGO2_SCALE, (double)height / PANGO2_SCALE); + break; + + case PANGO2_LINE_STYLE_DOUBLE: + cairo_rectangle (cr, + crenderer->x_offset + (double)x / PANGO2_SCALE, + crenderer->y_offset + (double)y / PANGO2_SCALE, + (double)width / PANGO2_SCALE, (double)height / (3 * PANGO2_SCALE)); + cairo_rectangle (cr, + crenderer->x_offset + (double)x / PANGO2_SCALE, + crenderer->y_offset + (double)y / PANGO2_SCALE + (double)(2 * height) / (3 * PANGO2_SCALE), + (double)width / PANGO2_SCALE, (double)height / (3 * PANGO2_SCALE)); + break; + + + case PANGO2_LINE_STYLE_WAVY: + draw_wavy_line (cr, + crenderer->x_offset + (double)x / PANGO2_SCALE, + crenderer->y_offset + (double)y / PANGO2_SCALE, + (double)width / PANGO2_SCALE, (double)height / PANGO2_SCALE); + break; + default: + g_assert_not_reached (); + } + + if (!crenderer->do_path) + { + cairo_fill (cr); + + cairo_restore (cr); + } +} + +static void +pango2_cairo_renderer_init (Pango2CairoRenderer *renderer G_GNUC_UNUSED) +{ +} + +static void +pango2_cairo_renderer_class_init (Pango2CairoRendererClass *klass) +{ + Pango2RendererClass *renderer_class = PANGO2_RENDERER_CLASS (klass); + + renderer_class->draw_glyphs = pango2_cairo_renderer_draw_glyphs; + renderer_class->draw_run = pango2_cairo_renderer_draw_run; + renderer_class->draw_rectangle = pango2_cairo_renderer_draw_rectangle; + renderer_class->draw_styled_line = pango2_cairo_renderer_draw_styled_line; + renderer_class->draw_trapezoid = pango2_cairo_renderer_draw_trapezoid; +} + +static Pango2CairoRenderer *cached_renderer = NULL; /* MT-safe */ +G_LOCK_DEFINE_STATIC (cached_renderer); + +static Pango2CairoRenderer * +acquire_renderer (void) +{ + Pango2CairoRenderer *renderer; + + if (G_LIKELY (G_TRYLOCK (cached_renderer))) + { + if (G_UNLIKELY (!cached_renderer)) + { + cached_renderer = g_object_new (PANGO2_TYPE_CAIRO_RENDERER, NULL); + cached_renderer->is_cached_renderer = TRUE; + } + + renderer = cached_renderer; + } + else + { + renderer = g_object_new (PANGO2_TYPE_CAIRO_RENDERER, NULL); + } + + return renderer; +} + +static void +release_renderer (Pango2CairoRenderer *renderer) +{ + if (G_LIKELY (renderer->is_cached_renderer)) + { + renderer->cr = NULL; + renderer->do_path = FALSE; + renderer->has_show_text_glyphs = FALSE; + renderer->x_offset = 0.; + renderer->y_offset = 0.; + + G_UNLOCK (cached_renderer); + } + else + g_object_unref (renderer); +} + +static void +save_current_point (Pango2CairoRenderer *renderer) +{ + renderer->cr_had_current_point = cairo_has_current_point (renderer->cr); + cairo_get_current_point (renderer->cr, &renderer->x_offset, &renderer->y_offset); + + /* abuse save_current_point() to cache cairo_has_show_text_glyphs() result */ + renderer->has_show_text_glyphs = cairo_surface_has_show_text_glyphs (cairo_get_target (renderer->cr)); +} + +static void +restore_current_point (Pango2CairoRenderer *renderer) +{ + if (renderer->cr_had_current_point) + /* XXX should do cairo_set_current_point() when we have that function */ + cairo_move_to (renderer->cr, renderer->x_offset, renderer->y_offset); + else + cairo_new_sub_path (renderer->cr); +} + + +/* convenience wrappers using the default renderer */ + + +static void +_pango2_cairo_do_glyph_string (cairo_t *cr, + Pango2Font *font, + Pango2GlyphString *glyphs, + gboolean do_path) +{ + Pango2CairoRenderer *crenderer = acquire_renderer (); + Pango2Renderer *renderer = (Pango2Renderer *) crenderer; + + crenderer->cr = cr; + crenderer->do_path = do_path; + save_current_point (crenderer); + + if (!do_path) + { + /* unset all part colors, since when drawing just a glyph string, + * prepare_run() isn't called. + */ + + pango2_renderer_activate (renderer); + + pango2_renderer_set_color (renderer, PANGO2_RENDER_PART_FOREGROUND, NULL); + pango2_renderer_set_color (renderer, PANGO2_RENDER_PART_BACKGROUND, NULL); + pango2_renderer_set_color (renderer, PANGO2_RENDER_PART_UNDERLINE, NULL); + pango2_renderer_set_color (renderer, PANGO2_RENDER_PART_STRIKETHROUGH, NULL); + pango2_renderer_set_color (renderer, PANGO2_RENDER_PART_OVERLINE, NULL); + } + + pango2_renderer_draw_glyphs (renderer, font, glyphs, 0, 0); + + if (!do_path) + { + pango2_renderer_deactivate (renderer); + } + + restore_current_point (crenderer); + + release_renderer (crenderer); +} + +static void +_pango2_cairo_do_run (cairo_t *cr, + const char *text, + Pango2Run *run, + gboolean do_path) +{ + Pango2CairoRenderer *crenderer = acquire_renderer (); + Pango2Renderer *renderer = (Pango2Renderer *) crenderer; + + crenderer->cr = cr; + crenderer->do_path = do_path; + save_current_point (crenderer); + + if (!do_path) + { + /* unset all part colors, since when drawing just a glyph string, + * prepare_run() isn't called. + */ + + pango2_renderer_activate (renderer); + + pango2_renderer_set_color (renderer, PANGO2_RENDER_PART_FOREGROUND, NULL); + pango2_renderer_set_color (renderer, PANGO2_RENDER_PART_BACKGROUND, NULL); + pango2_renderer_set_color (renderer, PANGO2_RENDER_PART_UNDERLINE, NULL); + pango2_renderer_set_color (renderer, PANGO2_RENDER_PART_STRIKETHROUGH, NULL); + pango2_renderer_set_color (renderer, PANGO2_RENDER_PART_OVERLINE, NULL); + } + + pango2_renderer_draw_run (renderer, text, run, 0, 0); + + if (!do_path) + pango2_renderer_deactivate (renderer); + + restore_current_point (crenderer); + + release_renderer (crenderer); +} + +static void +_pango2_cairo_do_line (cairo_t *cr, + Pango2Line *line, + gboolean do_path) +{ + Pango2CairoRenderer *crenderer = acquire_renderer (); + Pango2Renderer *renderer = (Pango2Renderer *) crenderer; + + crenderer->cr = cr; + crenderer->do_path = do_path; + save_current_point (crenderer); + + pango2_renderer_draw_line (renderer, line, 0, 0); + + restore_current_point (crenderer); + + release_renderer (crenderer); +} + +static void +_pango2_cairo_do_lines (cairo_t *cr, + Pango2Lines *lines, + gboolean do_path) +{ + Pango2CairoRenderer *crenderer = acquire_renderer (); + Pango2Renderer *renderer = (Pango2Renderer *) crenderer; + + crenderer->cr = cr; + crenderer->do_path = do_path; + save_current_point (crenderer); + + pango2_renderer_draw_lines (renderer, lines, 0, 0); + + restore_current_point (crenderer); + + release_renderer (crenderer); +} + +static void +_pango2_cairo_do_layout (cairo_t *cr, + Pango2Layout *layout, + gboolean do_path) +{ + Pango2CairoRenderer *crenderer = acquire_renderer (); + Pango2Renderer *renderer = (Pango2Renderer *) crenderer; + + crenderer->cr = cr; + crenderer->do_path = do_path; + save_current_point (crenderer); + + pango2_renderer_draw_lines (renderer, pango2_layout_get_lines (layout), 0, 0); + + restore_current_point (crenderer); + + release_renderer (crenderer); +} + +/* public wrapper of above to show or append path */ + + +/** + * pango2_cairo_show_glyph_string: + * @cr: a Cairo context + * @font: a `Pango2Font` from a `Pango2CairoFontMap` + * @glyphs: a `Pango2GlyphString` + * + * Draws the glyphs in @glyphs in the specified cairo context. + * + * The origin of the glyphs (the left edge of the baseline) will + * be drawn at the current point of the cairo context. + */ +void +pango2_cairo_show_glyph_string (cairo_t *cr, + Pango2Font *font, + Pango2GlyphString *glyphs) +{ + g_return_if_fail (cr != NULL); + g_return_if_fail (glyphs != NULL); + + _pango2_cairo_do_glyph_string (cr, font, glyphs, FALSE); +} + +/** + * pango2_cairo_show_run: + * @cr: a Cairo context + * @text: the UTF-8 text that @run refers to + * @run: a `Pango2Run` + * + * Draws the glyphs in @run in the specified cairo context, + * embedding the text associated with the glyphs in the output if the + * output format supports it (PDF for example), otherwise it acts + * similar to [func@Pango2.cairo_show_glyph_string]. + * + * The origin of the glyphs (the left edge of the baseline) will + * be drawn at the current point of the cairo context. + * + * Note that @text is the start of the text for layout, which is then + * indexed by `run->item->offset`. + */ +void +pango2_cairo_show_run (cairo_t *cr, + const char *text, + Pango2Run *run) +{ + g_return_if_fail (cr != NULL); + g_return_if_fail (text != NULL); + g_return_if_fail (run != NULL); + + _pango2_cairo_do_run (cr, text, run, FALSE); +} + +/** + * pango2_cairo_show_line: + * @cr: a Cairo context + * @line: a `Pango2Line` + * + * Draws a `Pango2Line` in the specified cairo context. + * + * The origin of the glyphs (the left edge of the line) will + * be drawn at the current point of the cairo context. + */ +void +pango2_cairo_show_line (cairo_t *cr, + Pango2Line *line) +{ + g_return_if_fail (cr != NULL); + g_return_if_fail (line != NULL); + + _pango2_cairo_do_line (cr, line, FALSE); +} + +/** + * pango2_cairo_show_lines: + * @cr: a Cairo context + * @lines: a `Pango2Lines` object + * + * Draws a `Pango2Lines` object in the specified cairo context. + * + * The top-left corner of the `Pango2Lines` will be drawn + * at the current point of the cairo context. + */ +void +pango2_cairo_show_lines (cairo_t *cr, + Pango2Lines *lines) +{ + g_return_if_fail (cr != NULL); + g_return_if_fail (lines != NULL); + + _pango2_cairo_do_lines (cr, lines, FALSE); +} + +/** + * pango2_cairo_show_layout: + * @cr: a Cairo context + * @layout: a Pango2 layout + * + * Draws a `Pango2Layout` in the specified cairo context. + * + * The top-left corner of the `Pango2Layout` will be drawn + * at the current point of the cairo context. + */ +void +pango2_cairo_show_layout (cairo_t *cr, + Pango2Layout *layout) +{ + g_return_if_fail (cr != NULL); + g_return_if_fail (PANGO2_IS_LAYOUT (layout)); + + _pango2_cairo_do_layout (cr, layout, FALSE); +} + +/** + * pango2_cairo_glyph_string_path: + * @cr: a Cairo context + * @font: a `Pango2Font` from a `Pango2CairoFontMap` + * @glyphs: a `Pango2GlyphString` + * + * Adds the glyphs in @glyphs to the current path in the specified + * cairo context. + * + * The origin of the glyphs (the left edge of the baseline) + * will be at the current point of the cairo context. + */ +void +pango2_cairo_glyph_string_path (cairo_t *cr, + Pango2Font *font, + Pango2GlyphString *glyphs) +{ + g_return_if_fail (cr != NULL); + g_return_if_fail (glyphs != NULL); + + _pango2_cairo_do_glyph_string (cr, font, glyphs, TRUE); +} + +/** + * pango2_cairo_run_path: + * @cr: a Cairo context + * @text: the UTF-8 text that @run refers to + * @run: a `Pango2Run` + * + * Adds the text in `Pango2Run` to the current path in the + * specified cairo context. + * + * The origin of the glyphs (the left edge of the line) will be + * at the current point of the cairo context. + */ +void +pango2_cairo_run_path (cairo_t *cr, + const char *text, + Pango2Run *run) +{ + _pango2_cairo_do_run (cr, text, run, TRUE); +} + +/** + * pango2_cairo_line_path: + * @cr: a Cairo context + * @line: a `Pango2Line` + * + * Adds the text in `Pango2Line` to the current path in the + * specified cairo context. + * + * The origin of the glyphs (the left edge of the line) will be + * at the current point of the cairo context. + */ +void +pango2_cairo_line_path (cairo_t *cr, + Pango2Line *line) +{ + g_return_if_fail (cr != NULL); + + _pango2_cairo_do_line (cr, line, TRUE); +} + +/** + * pango2_cairo_layout_path: + * @cr: a Cairo context + * @layout: a Pango2 layout + * + * Adds the text in a `Pango2Layout` to the current path in the + * specified cairo context. + * + * The top-left corner of the `Pango2Layout` will be at the + * current point of the cairo context. + */ +void +pango2_cairo_layout_path (cairo_t *cr, + Pango2Layout *layout) +{ + g_return_if_fail (cr != NULL); + g_return_if_fail (PANGO2_IS_LAYOUT (layout)); + + _pango2_cairo_do_layout (cr, layout, TRUE); +} + +/** + * pango2_cairo_lines_path: + * @cr: a Cairo context + * @lines: a `Pango2Lines` object + * + * Adds the text in a `Pango2Lines` to the current path in the + * specified cairo context. + * + * The top-left corner of the `Pango2Layout` will be at the + * current point of the cairo context. + */ +void +pango2_cairo_lines_path (cairo_t *cr, + Pango2Lines *lines) +{ + g_return_if_fail (cr != NULL); + g_return_if_fail (PANGO2_IS_LINES (lines)); + + _pango2_cairo_do_lines (cr, lines, TRUE); +} diff --git a/pango2/pangocairo-render.h b/pango2/pangocairo-render.h new file mode 100644 index 00000000..fb40fba9 --- /dev/null +++ b/pango2/pangocairo-render.h @@ -0,0 +1,63 @@ +/* + * Copyright (C) 1999, 2004 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango.h> +#include <cairo.h> + +G_BEGIN_DECLS + +PANGO2_AVAILABLE_IN_ALL +void pango2_cairo_show_glyph_string (cairo_t *cr, + Pango2Font *font, + Pango2GlyphString *glyphs); + +PANGO2_AVAILABLE_IN_ALL +void pango2_cairo_show_run (cairo_t *cr, + const char *text, + Pango2Run *run); +PANGO2_AVAILABLE_IN_ALL +void pango2_cairo_show_line (cairo_t *cr, + Pango2Line *line); +PANGO2_AVAILABLE_IN_ALL +void pango2_cairo_show_lines (cairo_t *cr, + Pango2Lines *lines); +PANGO2_AVAILABLE_IN_ALL +void pango2_cairo_show_layout (cairo_t *cr, + Pango2Layout *layout); +PANGO2_AVAILABLE_IN_ALL +void pango2_cairo_glyph_string_path (cairo_t *cr, + Pango2Font *font, + Pango2GlyphString *glyphs); +PANGO2_AVAILABLE_IN_ALL +void pango2_cairo_run_path (cairo_t *cr, + const char *text, + Pango2Run *run); +PANGO2_AVAILABLE_IN_ALL +void pango2_cairo_line_path (cairo_t *cr, + Pango2Line *line); +PANGO2_AVAILABLE_IN_ALL +void pango2_cairo_lines_path (cairo_t *cr, + Pango2Lines *lines); +PANGO2_AVAILABLE_IN_ALL +void pango2_cairo_layout_path (cairo_t *cr, + Pango2Layout *layout); + +G_END_DECLS diff --git a/pango2/pangocairo.h b/pango2/pangocairo.h new file mode 100644 index 00000000..2291fb8e --- /dev/null +++ b/pango2/pangocairo.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 1999, 2004 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pangocairo-context.h> +#include <pango2/pangocairo-font.h> +#include <pango2/pangocairo-render.h> diff --git a/pango2/pangocoretext-fontmap.c b/pango2/pangocoretext-fontmap.c new file mode 100644 index 00000000..3b7cdd47 --- /dev/null +++ b/pango2/pangocoretext-fontmap.c @@ -0,0 +1,460 @@ +/* Pango2 + * + * Copyright (C) 2021 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include <math.h> + +#include <gio/gio.h> + +#include "pangocoretext-fontmap.h" +#include "pango-hbfamily-private.h" +#include "pango-fontmap-private.h" +#include "pango-hbface-private.h" +#include "pango-hbfont-private.h" +#include "pango-context.h" +#include "pango-impl-utils.h" +#include "pango-trace-private.h" + +#include <Carbon/Carbon.h> +#include <hb-ot.h> +#include <hb-coretext.h> + + +/** + * Pango2CoreTextFontMap: + * + * `Pango2CoreTextFontMap` is a subclass of `Pango2FontMap` that uses + * CoreText to populate the fontmap with the available fonts. + */ + + +struct _Pango2CoreTextFontMap +{ + Pango2FontMap parent_instance; +}; + +struct _Pango2CoreTextFontMapClass +{ + Pango2FontMapClass parent_class; +}; + +/* {{{ CoreText utilities */ + +#define ct_weight_min -0.7f +#define ct_weight_max 0.8f + +/* This map is based on empirical data from analyzing a large collection of + * fonts and comparing the opentype value with the value that OSX returns. + * see: https://bugzilla.gnome.org/show_bug.cgi?id=766148 + * FIXME: This need recalibrating, values outside these bounds do occur! + */ + +static struct { + float ct_weight; + Pango2Weight pango2_weight; +} ct_weight_map[] = { + { ct_weight_min, PANGO2_WEIGHT_THIN }, + { -0.5, PANGO2_WEIGHT_ULTRALIGHT }, + { -0.23, PANGO2_WEIGHT_LIGHT }, + { -0.115, PANGO2_WEIGHT_SEMILIGHT }, + { 0.00, PANGO2_WEIGHT_NORMAL }, + { 0.2, PANGO2_WEIGHT_MEDIUM }, + { 0.3, PANGO2_WEIGHT_SEMIBOLD }, + { 0.4, PANGO2_WEIGHT_BOLD }, + { 0.6, PANGO2_WEIGHT_ULTRABOLD }, + { ct_weight_max, PANGO2_WEIGHT_HEAVY } +}; + +static int +lerp (float x, float x1, float x2, int y1, int y2) +{ + float dx = x2 - x1; + int dy = y2 - y1; + return y1 + (dy * (x - x1) + dx / 2) / dx; +} + +static Pango2Weight +ct_font_descriptor_get_weight (CTFontDescriptorRef desc) +{ + CFDictionaryRef dict; + CFNumberRef cf_number; + CGFloat value; + Pango2Weight weight = PANGO2_WEIGHT_NORMAL; + guint i; + + dict = CTFontDescriptorCopyAttribute (desc, kCTFontTraitsAttribute); + cf_number = (CFNumberRef)CFDictionaryGetValue (dict, kCTFontWeightTrait); + + if (cf_number != NULL && CFNumberGetValue (cf_number, kCFNumberCGFloatType, &value)) + { + if (!(value >= ct_weight_min && value <= ct_weight_max)) + { + i = value > ct_weight_max ? G_N_ELEMENTS (ct_weight_map) - 1 : 0; + weight = ct_weight_map[i].ct_weight; + } + else + { + for (i = 1; value > ct_weight_map[i].ct_weight; ++i) + ; + + weight = lerp (value, ct_weight_map[i-1].ct_weight, ct_weight_map[i].ct_weight, + ct_weight_map[i-1].pango2_weight, ct_weight_map[i].pango2_weight); + + } + } + else + weight = PANGO2_WEIGHT_NORMAL; + + CFRelease (dict); + + return weight; +} + +static char * +gchar_from_cf_string (CFStringRef str) +{ + CFIndex len; + char *buffer; + + /* GetLength returns the number of UTF-16 pairs, so this number + * times 2 should definitely gives us enough space for UTF8. + * We add one for the terminating zero. + */ + len = CFStringGetLength (str) * 2 + 1; + buffer = g_new0 (char, len); + CFStringGetCString (str, buffer, len, kCFStringEncodingUTF8); + + return buffer; +} + +static char * +ct_font_descriptor_get_family_name (CTFontDescriptorRef desc, + gboolean may_fail) +{ + CFStringRef cf_str; + char *buffer; + + cf_str = CTFontDescriptorCopyAttribute (desc, kCTFontFamilyNameAttribute); + if (!cf_str) + { + int i; + + /* No font family name is set, try to retrieve font name and deduce + * the family name from that instead. + */ + cf_str = CTFontDescriptorCopyAttribute (desc, kCTFontNameAttribute); + if (!cf_str) + { + if (may_fail) + return NULL; + + /* This font is likely broken, return a default family name ... */ + return g_strdup ("Sans"); + } + + buffer = gchar_from_cf_string (cf_str); + CFRelease (cf_str); + + for (i = 0; i < strlen (buffer); i++) + if (buffer[i] == '-') + break; + + if (i < strlen (buffer)) + { + char *ret; + + ret = g_strndup (buffer, i); + g_free (buffer); + + return ret; + } + else + return buffer; + } + + buffer = gchar_from_cf_string (cf_str); + CFRelease (cf_str); + + return buffer; +} + +static char * +ct_font_descriptor_get_style_name (CTFontDescriptorRef desc) +{ + CFStringRef cf_str; + char *buffer; + + cf_str = CTFontDescriptorCopyAttribute (desc, kCTFontStyleNameAttribute); + if (!cf_str) + return NULL; + + buffer = gchar_from_cf_string (cf_str); + CFRelease (cf_str); + + return buffer; +} + +static CTFontSymbolicTraits +ct_font_descriptor_get_traits (CTFontDescriptorRef desc) +{ + CFDictionaryRef dict; + CFNumberRef cf_number; + SInt64 traits; + + /* This is interesting, the value stored is a CTFontSymbolicTraits which + * is defined as uint32_t. CFNumber does not have an obvious type which + * deals with unsigned values. Upon inspection with CFNumberGetType, + * it turns out this value is stored as SInt64, so we use that to + * obtain the value from the CFNumber. + */ + dict = CTFontDescriptorCopyAttribute (desc, kCTFontTraitsAttribute); + cf_number = (CFNumberRef)CFDictionaryGetValue (dict, kCTFontSymbolicTrait); + if (!CFNumberGetValue (cf_number, kCFNumberSInt64Type, &traits)) + traits = 0; + CFRelease (dict); + + return (CTFontSymbolicTraits)traits; +} + +static gboolean +ct_font_descriptor_is_small_caps (CTFontDescriptorRef desc) +{ + CFIndex i, count; + CFArrayRef array; + CFStringRef str; + gboolean retval = FALSE; + + /* See http://stackoverflow.com/a/4811371 for why this works and an + * explanation of the magic number "3" used below. + */ + array = CTFontDescriptorCopyAttribute (desc, kCTFontFeaturesAttribute); + if (!array) + return FALSE; + + str = CFStringCreateWithCString (NULL, "CTFeatureTypeIdentifier", + kCFStringEncodingASCII); + + count = CFArrayGetCount (array); + for (i = 0; i < count; i++) + { + CFDictionaryRef dict = CFArrayGetValueAtIndex (array, i); + CFNumberRef num; + + num = (CFNumberRef)CFDictionaryGetValue (dict, str); + if (num) + { + int value = 0; + + if (CFNumberGetValue (num, kCFNumberSInt32Type, &value) && + value == 3) + { + /* This font supports small caps. */ + retval = TRUE; + break; + } + } + } + + CFRelease (str); + CFRelease (array); + + return retval; +} + +static gboolean +pango2_core_text_style_name_is_oblique (const char *style_name) +{ + return style_name && strstr (style_name, "Oblique"); +} + +static Pango2FontDescription * +font_description_from_ct_font_descriptor (CTFontDescriptorRef desc) +{ + SInt64 font_traits; + char *family_name; + char *style_name; + Pango2FontDescription *font_desc; + + font_desc = pango2_font_description_new (); + + family_name = ct_font_descriptor_get_family_name (desc, FALSE); + pango2_font_description_set_family (font_desc, family_name); + g_free (family_name); + + pango2_font_description_set_weight (font_desc, ct_font_descriptor_get_weight (desc)); + + font_traits = ct_font_descriptor_get_traits (desc); + style_name = ct_font_descriptor_get_style_name (desc); + + if ((font_traits & kCTFontItalicTrait) == kCTFontItalicTrait) + pango2_font_description_set_style (font_desc, PANGO2_STYLE_ITALIC); + else if (pango2_core_text_style_name_is_oblique (style_name)) + pango2_font_description_set_style (font_desc, PANGO2_STYLE_OBLIQUE); + else + pango2_font_description_set_style (font_desc, PANGO2_STYLE_NORMAL); + + if ((font_traits & kCTFontCondensedTrait) == kCTFontCondensedTrait) + pango2_font_description_set_stretch (font_desc, PANGO2_STRETCH_CONDENSED); + + if (ct_font_descriptor_is_small_caps (desc)) + pango2_font_description_set_variant (font_desc, PANGO2_VARIANT_SMALL_CAPS); + else + pango2_font_description_set_variant (font_desc, PANGO2_VARIANT_NORMAL); + + g_free (style_name); + + return font_desc; +} + +static Pango2HbFace * +face_from_ct_font_descriptor (CTFontDescriptorRef desc) +{ + Pango2HbFace *face; + CTFontRef ctfont; + CGFontRef cgfont; + hb_face_t *hb_face; + char *name; + Pango2FontDescription *description; + + ctfont = CTFontCreateWithFontDescriptor (desc, 0.0, NULL); + cgfont = CTFontCopyGraphicsFont (ctfont, NULL); + + hb_face = hb_coretext_face_create (cgfont); + + CFRelease (cgfont); + CFRelease (ctfont); + + name = ct_font_descriptor_get_style_name (desc); + description = font_description_from_ct_font_descriptor (desc); + pango2_font_description_unset_fields (description, PANGO2_FONT_MASK_VARIANT | + PANGO2_FONT_MASK_SIZE | + PANGO2_FONT_MASK_GRAVITY); + + hb_face_make_immutable (hb_face); + + face = pango2_hb_face_new_from_hb_face (hb_face, -1, name, description); + // FIXME: what about languages? see CTFontCopySupportedLanguages + + pango2_font_description_free (description); + g_free (name); + hb_face_destroy (hb_face); + + return face; +} + +/* }}} */ +/* {{{ Pango2FontMap implementation */ + +static void +pango2_core_text_font_map_populate (Pango2FontMap *map) +{ + CTFontCollectionRef collection; + CFArrayRef ctfaces; + CFIndex count; + + /* Add all system fonts */ + collection = CTFontCollectionCreateFromAvailableFonts (0); + ctfaces = CTFontCollectionCreateMatchingFontDescriptors (collection); + count = CFArrayGetCount (ctfaces); + + for (int i = 0; i < count; i++) + { + CTFontDescriptorRef desc = CFArrayGetValueAtIndex (ctfaces, i); + Pango2HbFace *face = face_from_ct_font_descriptor (desc); + pango2_font_map_add_face (map, PANGO2_FONT_FACE (face)); + } + + /* Add generic aliases */ + struct { + const char *alias_name; + const char *family_name; + } aliases[] = { + { "Monospace", "Courier" }, + { "Sans-serif", "Helvetica" }, + { "Sans", "Helvetica" }, + { "Serif", "Times" }, + { "Cursive", "Apple Chancery" }, + { "Fantasy", "Papyrus", }, + { "System-ui", ".AppleSystemUIFont" }, + { "Emoji", "Apple Color Emoji" } + }; + + for (int i = 0; i < G_N_ELEMENTS (aliases); i++) + { + Pango2FontFamily *family = pango2_font_map_get_family (map, aliases[i].family_name); + + if (family) + { + Pango2GenericFamily *alias_family; + + alias_family = pango2_generic_family_new (aliases[i].alias_name); + pango2_generic_family_add_family (alias_family, family); + pango2_font_map_add_family (map, PANGO2_FONT_FAMILY (alias_family)); + } + } +} + +/* }}} */ +/* {{{ Pango2CoreTextFontMap implementation */ + +G_DEFINE_FINAL_TYPE (Pango2CoreTextFontMap, pango2_core_text_font_map, PANGO2_TYPE_FONT_MAP) + +static void +pango2_core_text_font_map_init (Pango2CoreTextFontMap *self) +{ + pango2_font_map_repopulate (PANGO2_FONT_MAP (self), TRUE); +} + +static void +pango2_core_text_font_map_finalize (GObject *object) +{ + G_OBJECT_CLASS (pango2_core_text_font_map_parent_class)->finalize (object); +} + +static void +pango2_core_text_font_map_class_init (Pango2CoreTextFontMapClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + Pango2FontMapClass *font_map_class = PANGO2_FONT_MAP_CLASS (class); + + object_class->finalize = pango2_core_text_font_map_finalize; + + font_map_class->populate = pango2_core_text_font_map_populate; +} + +/* }}} */ + /* {{{ Public API */ + +/** + * pango2_core_text_font_map_new: + * + * Creates a new `Pango2CoreTextFontMap` object. + * + * Returns: a new `Pango2CoreTextFontMap` + */ +Pango2CoreTextFontMap * +pango2_core_text_font_map_new (void) +{ + return g_object_new (PANGO2_TYPE_CORE_TEXT_FONT_MAP, NULL); +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pangocoretext-fontmap.h b/pango2/pangocoretext-fontmap.h new file mode 100644 index 00000000..ecbe7c51 --- /dev/null +++ b/pango2/pangocoretext-fontmap.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 Red Hat, Inc + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango.h> + +G_BEGIN_DECLS + +#define PANGO2_TYPE_CORE_TEXT_FONT_MAP (pango2_core_text_font_map_get_type ()) + +PANGO2_AVAILABLE_IN_ALL +PANGO2_DECLARE_INTERNAL_TYPE (Pango2CoreTextFontMap, pango2_core_text_font_map, PANGO2, CORE_TEXT_FONT_MAP, Pango2FontMap) + +PANGO2_AVAILABLE_IN_ALL +Pango2CoreTextFontMap * pango2_core_text_font_map_new (void); + + +G_END_DECLS diff --git a/pango2/pangodwrite-fontmap.cpp b/pango2/pangodwrite-fontmap.cpp new file mode 100644 index 00000000..37307a3d --- /dev/null +++ b/pango2/pangodwrite-fontmap.cpp @@ -0,0 +1,392 @@ +/* Pango + * pangodwrite-fontmap.cpp: Fontmap using DirectWrite + * + * Copyright (C) 2022 the GTK team + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include <windows.h> +#include <initguid.h> +#include <dwrite.h> + +#include <hb-ot.h> +#include <hb-directwrite.h> + +#include "pangodwrite-fontmap.h" +#include "pango-hbfamily-private.h" +#include "pango-fontmap-private.h" +#include "pango-hbface-private.h" +#include "pango-hbfont-private.h" +#include "pango-context.h" +#include "pango-impl-utils.h" +#include "pango-trace-private.h" + + +/** + * PangoDirectWriteFontMap: + * + * `PangoDirectWriteFontMap` is a subclass of `PangoFontMap` that + * uses DirectWrite to populate the fontmap with the available fonts. + */ + + +struct _PangoDirectWriteFontMap +{ + PangoFontMap parent_instance; + + IDWriteFactory *dwrite_factory; +}; + +struct _PangoDirectWriteFontMapClass +{ + PangoFontMapClass parent_class; +}; + +#ifdef _MSC_VER +# define UUID_OF_IDWriteFactory __uuidof (IDWriteFactory) +#else +# define UUID_OF_IDWriteFactory IID_IDWriteFactory +#endif + +/* {{{ DirectWrite utilities */ + +static PangoStretch +util_to_pango_stretch (DWRITE_FONT_STRETCH stretch) +{ + int value = (int) stretch; + + if G_UNLIKELY (stretch <= DWRITE_FONT_STRETCH_UNDEFINED || + stretch > DWRITE_FONT_STRETCH_ULTRA_EXPANDED) + return PANGO_STRETCH_NORMAL; + + return (PangoStretch) --value; +} + +static PangoStyle +util_to_pango_style (DWRITE_FONT_STYLE style) +{ + switch (style) + { + case DWRITE_FONT_STYLE_NORMAL: + return PANGO_STYLE_NORMAL; + case DWRITE_FONT_STYLE_OBLIQUE: + return PANGO_STYLE_OBLIQUE; + case DWRITE_FONT_STYLE_ITALIC: + return PANGO_STYLE_ITALIC; + default: + g_assert_not_reached (); + return PANGO_STYLE_NORMAL; + } +} + +static int +util_map_weight (int weight) +{ + if G_UNLIKELY (weight < 100) + weight = 100; + + if G_UNLIKELY (weight > 1000) + weight = 1000; + + return weight; +} + +static PangoWeight +util_to_pango_weight (DWRITE_FONT_WEIGHT weight) +{ + /* DirectWrite weight values range from 1 to 999, Pango values + * range from 100 to 1000. */ + + return (PangoWeight) util_map_weight (weight); +} + +static PangoFontDescription* +util_get_pango_font_description (IDWriteFont *font, + const char *family_name) +{ + DWRITE_FONT_STRETCH stretch = font->GetStretch (); + DWRITE_FONT_STYLE style = font->GetStyle (); + DWRITE_FONT_WEIGHT weight = font->GetWeight (); + PangoFontDescription *description; + + description = pango_font_description_new (); + pango_font_description_set_family (description, family_name); + pango_font_description_set_stretch (description, util_to_pango_stretch (stretch)); + pango_font_description_set_style (description, util_to_pango_style (style)); + pango_font_description_set_weight (description, util_to_pango_weight (weight)); + + return description; +} + +static char* +util_free_to_string (IDWriteLocalizedStrings *strings) +{ + char *string = NULL; + HRESULT hr; + + if (strings->GetCount() > 0) + { + UINT32 index = 0; + BOOL exists = FALSE; + UINT32 length = 0; + + hr = strings->FindLocaleName (L"en-us", &index, &exists); + if (FAILED (hr) || !exists || index == UINT32_MAX) + index = 0; + + hr = strings->GetStringLength (index, &length); + if (SUCCEEDED (hr) && length > 0) + { + gunichar2 *string_utf16 = g_new (gunichar2, length + 1); + + hr = strings->GetString (index, (wchar_t*) string_utf16, length + 1); + if (SUCCEEDED (hr)) + string = g_utf16_to_utf8 (string_utf16, -1, NULL, NULL, NULL); + + g_free (string_utf16); + } + } + + strings->Release (); + + return string; +} + +static char* +util_dwrite_get_font_variant_name (IDWriteFont *font) +{ + IDWriteLocalizedStrings *strings = NULL; + HRESULT hr; + + hr = font->GetFaceNames (&strings); + if (FAILED (hr) || strings == NULL) + { + g_warning ("IDWriteFont::GetFaceNames failed with error code %x", (unsigned) hr); + return NULL; + } + + return util_free_to_string (strings); +} + +static char* +util_dwrite_get_font_family_name (IDWriteFontFamily *family) +{ + IDWriteLocalizedStrings *strings = NULL; + HRESULT hr; + + hr = family->GetFamilyNames (&strings); + if (FAILED (hr) || strings == NULL) + { + g_warning ("IDWriteFontFamily::GetFamilyNames failed with error code %x", (unsigned) hr); + return NULL; + } + + return util_free_to_string (strings); +} + +static PangoHbFace* +util_create_pango_hb_face (IDWriteFontFamily *family, + IDWriteFont *font, + IDWriteFontFace *face) +{ + char *family_name = util_dwrite_get_font_family_name (family); + char *variant_name = util_dwrite_get_font_variant_name (font); + PangoHbFace *pango_face = NULL; + + if (family_name && variant_name) + { + PangoFontDescription *description = util_get_pango_font_description (font, family_name); + hb_face_t *hb_face = hb_directwrite_face_create (face); + char *name = g_strconcat (family_name, " ", variant_name, NULL); + + hb_face_make_immutable (hb_face); + + pango_face = pango_hb_face_new_from_hb_face (hb_face, -1, name, description); + + g_free (name); + hb_face_destroy (hb_face); + pango_font_description_free (description); + } + + g_free (family_name); + g_free (variant_name); + + return pango_face; +} + +/* }}} */ +/* {{{ PangoFontMap implementation */ + +static void +pango_direct_write_font_map_populate (PangoFontMap *map) +{ + PangoDirectWriteFontMap *dwrite_map = PANGO_DIRECT_WRITE_FONT_MAP (map); + IDWriteFontCollection *collection = NULL; + UINT32 count; + HRESULT hr; + + hr = dwrite_map->dwrite_factory->GetSystemFontCollection (&collection, FALSE); + if (FAILED (hr) || collection == NULL) + g_error ("IDWriteFactory::GetSystemFontCollection failed with error code %x\n", (unsigned)hr); + + count = collection->GetFontFamilyCount (); + + for (UINT32 i = 0; i < count; i++) + { + IDWriteFontFamily *family = NULL; + UINT32 font_count; + + hr = collection->GetFontFamily (i, &family); + if G_UNLIKELY (FAILED (hr) || family == NULL) + { + g_warning ("IDWriteFontCollection::GetFontFamily failed with error code %x\n", (unsigned)hr); + continue; + } + + font_count = family->GetFontCount (); + + for (UINT32 j = 0; j < font_count; j++) + { + IDWriteFont *font = NULL; + IDWriteFontFace *face = NULL; + PangoHbFace *pango_face = NULL; + + hr = family->GetFont (j, &font); + if (FAILED (hr) || font == NULL) + { + g_warning ("IDWriteFontFamily::GetFont failed with error code %x\n", (unsigned)hr); + break; + } + + hr = font->CreateFontFace (&face); + if (FAILED (hr) || face == NULL) + { + g_warning ("IDWriteFont::CreateFontFace failed with error code %x\n", (unsigned)hr); + font->Release (); + continue; + } + + pango_face = util_create_pango_hb_face (family, font, face); + if (pango_face) + pango_font_map_add_face (map, PANGO_FONT_FACE (pango_face)); + + face->Release (); + font->Release (); + } + + family->Release (); + } + + collection->Release (); + collection = NULL; + + /* Add generic aliases */ + struct { + const char *alias_name; + const char *family_name; + } aliases[] = { + { "Monospace", "Consolas" }, + { "Sans-serif", "Arial" }, + { "Sans", "Arial" }, + { "Serif", "Times New Roman" }, + { "System-ui", "Segoe UI" }, + { "Emoji", "Segoe UI Emoji" } + }; + +#if 0 + if (IsWindows11OrLater ()) + aliases[0].family_name = "Cascadia Mono"; +#endif + + for (gsize i = 0; i < G_N_ELEMENTS (aliases); i++) + { + PangoFontFamily *family = pango_font_map_get_family (map, aliases[i].family_name); + + if (family) + { + PangoGenericFamily *alias_family; + + alias_family = pango_generic_family_new (aliases[i].alias_name); + pango_generic_family_add_family (alias_family, family); + pango_font_map_add_family (map, PANGO_FONT_FAMILY (alias_family)); + } + } +} + +/* }}} */ +/* {{{ PangoDirectWriteFontMap implementation */ + +G_DEFINE_FINAL_TYPE (PangoDirectWriteFontMap, pango_direct_write_font_map, PANGO_TYPE_FONT_MAP) + +static void +pango_direct_write_font_map_init (PangoDirectWriteFontMap *self) +{ + HRESULT hr; + + hr = DWriteCreateFactory (DWRITE_FACTORY_TYPE_SHARED, + UUID_OF_IDWriteFactory, + reinterpret_cast<IUnknown**> (&self->dwrite_factory)); + + if (FAILED (hr) || !self->dwrite_factory) + g_error ("DWriteCreateFactory failed with error code %x", (unsigned)hr); + + pango_font_map_repopulate (PANGO_FONT_MAP (self), TRUE); +} + +static void +pango_direct_write_font_map_finalize (GObject *object) +{ + PangoDirectWriteFontMap *dwrite_map = PANGO_DIRECT_WRITE_FONT_MAP (object); + + dwrite_map->dwrite_factory->Release (); + dwrite_map->dwrite_factory = NULL; + + G_OBJECT_CLASS (pango_direct_write_font_map_parent_class)->finalize (object); +} + +static void +pango_direct_write_font_map_class_init (PangoDirectWriteFontMapClass *class_) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class_); + PangoFontMapClass *font_map_class = PANGO_FONT_MAP_CLASS (class_); + + object_class->finalize = pango_direct_write_font_map_finalize; + + font_map_class->populate = pango_direct_write_font_map_populate; +} + +/* }}} */ + /* {{{ Public API */ + +/** + * pango_direct_write_font_map_new: + * + * Creates a new `PangoDirectWriteFontMap` object. + * + * Returns: a new `PangoDirectWriteFontMap` + */ +PangoDirectWriteFontMap * +pango_direct_write_font_map_new (void) +{ + return (PangoDirectWriteFontMap *) g_object_new (PANGO_TYPE_DIRECT_WRITE_FONT_MAP, NULL); +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pangodwrite-fontmap.h b/pango2/pangodwrite-fontmap.h new file mode 100644 index 00000000..c873759d --- /dev/null +++ b/pango2/pangodwrite-fontmap.h @@ -0,0 +1,40 @@ +/* Pango2 + * pangodwrite-fontmap.h: Fontmap using DirectWrite + * + * Copyright (C) 2022 the GTK team + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#pragma once + +#include <pango2/pango.h> + +G_BEGIN_DECLS + +#define PANGO2_TYPE_DIRECT_WRITE_FONT_MAP (pango2_direct_write_font_map_get_type ()) + +PANGO2_AVAILABLE_IN_ALL +PANGO2_DECLARE_INTERNAL_TYPE (Pango2DirectWriteFontMap, + pango2_direct_write_font_map, + PANGO2, DIRECT_WRITE_FONT_MAP, + Pango2FontMap) + +PANGO2_AVAILABLE_IN_ALL +Pango2DirectWriteFontMap * pango2_direct_write_font_map_new (void); + + +G_END_DECLS diff --git a/pango2/pangofc-fontmap.c b/pango2/pangofc-fontmap.c new file mode 100644 index 00000000..c3d026e2 --- /dev/null +++ b/pango2/pangofc-fontmap.c @@ -0,0 +1,624 @@ +/* Pango2 + * + * Copyright (C) 2021 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" +#include <math.h> + +#include <gio/gio.h> + +#include "pangofc-fontmap.h" +#include "pango-hbfamily-private.h" +#include "pango-generic-family-private.h" +#include "pango-fontmap-private.h" +#include "pango-hbface-private.h" +#include "pango-hbfont-private.h" +#include "pango-context.h" +#include "pango-impl-utils.h" +#include "pango-trace-private.h" +#include "pangofc-language-set-private.h" + +#include <fontconfig/fontconfig.h> +#include <hb-ot.h> + + +/** + * Pango2FcFontMap: + * + * `Pango2FcFontMap` is a subclass of `Pango2FontMap` that uses + * fontconfig to populate the fontmap with the available fonts. + */ + + +struct _Pango2FcFontMap +{ + Pango2FontMap parent_instance; + + FcConfig *config; +}; + +struct _Pango2FcFontMapClass +{ + Pango2FontMapClass parent_class; +}; + +/* {{{ Fontconfig utilities */ + +static gboolean +is_supported_font_format (FcPattern *pattern) +{ + FcResult res; + const char *fontformat; + const char *file; + + /* Harfbuzz loads woff fonts, but we don't get any glyphs */ + res = FcPatternGetString (pattern, FC_FILE, 0, (FcChar8 **)(void*)&file); + if (res == FcResultMatch && + (g_str_has_suffix (file, ".woff") || g_str_has_suffix (file, ".woff2"))) + return FALSE; + + res = FcPatternGetString (pattern, FC_FONTFORMAT, 0, (FcChar8 **)(void*)&fontformat); + if (res != FcResultMatch) + return FALSE; + + /* Harfbuzz supports only SFNT fonts */ + + /* FIXME: "CFF" is used for both CFF in OpenType and bare CFF files, but + * HarfBuzz does not support the later and FontConfig does not seem + * to have a way to tell them apart. + */ + if (g_ascii_strcasecmp (fontformat, "TrueType") == 0 || + g_ascii_strcasecmp (fontformat, "CFF") == 0) + return TRUE; + + return FALSE; +} + +static Pango2Style +convert_fc_slant_to_pango (int fc_style) +{ + switch (fc_style) + { + case FC_SLANT_ROMAN: + return PANGO2_STYLE_NORMAL; + case FC_SLANT_ITALIC: + return PANGO2_STYLE_ITALIC; + case FC_SLANT_OBLIQUE: + return PANGO2_STYLE_OBLIQUE; + default: + return PANGO2_STYLE_NORMAL; + } +} + +static Pango2Stretch +convert_fc_width_to_pango (int fc_stretch) +{ + switch (fc_stretch) + { + case FC_WIDTH_NORMAL: + return PANGO2_STRETCH_NORMAL; + case FC_WIDTH_ULTRACONDENSED: + return PANGO2_STRETCH_ULTRA_CONDENSED; + case FC_WIDTH_EXTRACONDENSED: + return PANGO2_STRETCH_EXTRA_CONDENSED; + case FC_WIDTH_CONDENSED: + return PANGO2_STRETCH_CONDENSED; + case FC_WIDTH_SEMICONDENSED: + return PANGO2_STRETCH_SEMI_CONDENSED; + case FC_WIDTH_SEMIEXPANDED: + return PANGO2_STRETCH_SEMI_EXPANDED; + case FC_WIDTH_EXPANDED: + return PANGO2_STRETCH_EXPANDED; + case FC_WIDTH_EXTRAEXPANDED: + return PANGO2_STRETCH_EXTRA_EXPANDED; + case FC_WIDTH_ULTRAEXPANDED: + return PANGO2_STRETCH_ULTRA_EXPANDED; + default: + return PANGO2_STRETCH_NORMAL; + } +} + +static Pango2Weight +convert_fc_weight_to_pango (double fc_weight) +{ + return FcWeightToOpenTypeDouble (fc_weight); +} + +#define PANGO2_FC_GRAVITY "gravity" + +/* Create font description that matches the pattern. + * We explicitly don't want to include variant, gravity, + * variations and font features here, since there are + * handled on the font level, by Pango2HbFont, not + * by Pango2HbFace. + */ +static Pango2FontDescription * +pango2_font_description_from_pattern (FcPattern *pattern) +{ + Pango2FontDescription *desc; + Pango2Style style; + Pango2Weight weight; + Pango2Stretch stretch; + FcChar8 *s; + int i; + double d; + FcResult res; + + desc = pango2_font_description_new (); + + res = FcPatternGetString (pattern, FC_FAMILY, 0, (FcChar8 **) &s); + g_assert (res == FcResultMatch); + + pango2_font_description_set_family (desc, (char *)s); + + if (FcPatternGetInteger (pattern, FC_SLANT, 0, &i) == FcResultMatch) + style = convert_fc_slant_to_pango (i); + else + style = PANGO2_STYLE_NORMAL; + + pango2_font_description_set_style (desc, style); + + if (FcPatternGetDouble (pattern, FC_WEIGHT, 0, &d) == FcResultMatch) + weight = convert_fc_weight_to_pango (d); + else + weight = PANGO2_WEIGHT_NORMAL; + + pango2_font_description_set_weight (desc, weight); + + if (FcPatternGetInteger (pattern, FC_WIDTH, 0, &i) == FcResultMatch) + stretch = convert_fc_width_to_pango (i); + else + stretch = PANGO2_STRETCH_NORMAL; + + pango2_font_description_set_stretch (desc, stretch); + + return desc; +} + +static const char * +style_name_from_pattern (FcPattern *pattern) +{ + const char *font_style; + int weight, slant; + + if (FcPatternGetString (pattern, FC_STYLE, 0, (FcChar8 **)(void*)&font_style) == FcResultMatch) + return font_style; + + if (FcPatternGetInteger(pattern, FC_WEIGHT, 0, &weight) != FcResultMatch) + weight = FC_WEIGHT_MEDIUM; + + if (FcPatternGetInteger(pattern, FC_SLANT, 0, &slant) != FcResultMatch) + slant = FC_SLANT_ROMAN; + + if (weight <= FC_WEIGHT_MEDIUM) + { + if (slant == FC_SLANT_ROMAN) + return "Regular"; + else + return "Italic"; + } + else + { + if (slant == FC_SLANT_ROMAN) + return "Bold"; + else + return "Bold Italic"; + } + + return "Normal"; +} + +static gboolean +font_matrix_from_pattern (FcPattern *pattern, + Pango2Matrix *matrix) +{ + FcMatrix fc_matrix, *fc_matrix_val; + int i; + + FcMatrixInit (&fc_matrix); + for (i = 0; FcPatternGetMatrix (pattern, FC_MATRIX, i, &fc_matrix_val) == FcResultMatch; i++) + FcMatrixMultiply (&fc_matrix, &fc_matrix, fc_matrix_val); + FcMatrixScale (&fc_matrix, 1, -1); + + if (i > 0) + { + matrix->xx = fc_matrix.xx; + matrix->yx = fc_matrix.yx; + matrix->xy = fc_matrix.xy; + matrix->yy = fc_matrix.yy; + return TRUE; + } + + return FALSE; +} + +static Pango2HbFace * +pango2_hb_face_from_pattern (Pango2FcFontMap *self, + FcPattern *pattern, + GHashTable *lang_hash) +{ + const char *family_name, *file; + int index; + int instance_id; + Pango2FontDescription *description; + const char *name; + Pango2HbFace *face; + Pango2Matrix font_matrix; + FcLangSet *langs; + gboolean variable; + + if (!is_supported_font_format (pattern)) + return NULL; + + if (FcPatternGetString (pattern, FC_FAMILY, 0, (FcChar8 **)(void*)&family_name) != FcResultMatch) + return NULL; + + if (FcPatternGetString (pattern, FC_FILE, 0, (FcChar8 **)(void*)&file) != FcResultMatch) + return NULL; + + if (FcPatternGetInteger (pattern, FC_INDEX, 0, &index) != FcResultMatch) + index = 0; + + if (FcPatternGetBool (pattern, FC_VARIABLE, 0, &variable) != FcResultMatch) + variable = FALSE; + + instance_id = (index >> 16) - 1; + index = index & 0xffff; + + if (variable) + instance_id = -2; + + description = pango2_font_description_from_pattern (pattern); + name = style_name_from_pattern (pattern); + + face = pango2_hb_face_new_from_file (file, index, instance_id, name, description); + + pango2_font_description_free (description); + + if (font_matrix_from_pattern (pattern, &font_matrix)) + pango2_hb_face_set_matrix (face, &font_matrix); + + if (FcPatternGetLangSet (pattern, FC_LANG, 0, &langs) == FcResultMatch) + { + Pango2FcLanguageSet lookup; + Pango2LanguageSet *languages; + FcLangSet *empty = FcLangSetCreate (); + + if (!FcLangSetEqual (langs, empty)) + { + lookup.langs = langs; + languages = g_hash_table_lookup (lang_hash, &lookup); + if (!languages) + { + languages = PANGO2_LANGUAGE_SET (pango2_fc_language_set_new (langs)); + g_hash_table_add (lang_hash, languages); + } + pango2_hb_face_set_language_set (face, languages); + } + + FcLangSetDestroy (empty); + } + + return face; +} + +/* }}} */ +/* {{{ Pango2FontMap implementation */ + +static void +pango2_fc_font_map_populate (Pango2FontMap *map) +{ + Pango2FcFontMap *self = PANGO2_FC_FONT_MAP (map); + FcPattern *pat; + FcFontSet *fontset; + int k; + GHashTable *lang_hash; + FcObjectSet *os; + gint64 before G_GNUC_UNUSED; + + before = PANGO2_TRACE_CURRENT_TIME; + + os = FcObjectSetBuild (FC_FAMILY, FC_SPACING, FC_STYLE, FC_WEIGHT, + FC_WIDTH, FC_SLANT, FC_VARIABLE, FC_FONTFORMAT, + FC_FILE, FC_INDEX, FC_LANG, NULL); + + pat = FcPatternCreate (); + fontset = FcFontList (self->config, pat, os); + FcPatternDestroy (pat); + + lang_hash = g_hash_table_new_full ((GHashFunc) pango2_fc_language_set_hash, + (GEqualFunc) pango2_fc_language_set_equal, + NULL, g_object_unref); + + for (k = 0; k < fontset->nfont; k++) + { + Pango2HbFace *face = pango2_hb_face_from_pattern (self, fontset->fonts[k], lang_hash); + if (!face) + continue; + + pango2_font_map_add_face (PANGO2_FONT_MAP (self), PANGO2_FONT_FACE (face)); + } + + g_hash_table_unref (lang_hash); + + FcFontSetDestroy (fontset); + + /* Add aliases */ + const char *alias_names[] = { + "System-ui", + "Emoji" + }; + + for (int i = 0; i < G_N_ELEMENTS (alias_names); i++) + { + FcPattern *pattern; + FcPattern *ret; + FcResult res; + const char *family_name; + + if (pango2_font_map_get_family (map, alias_names[i])) + continue; + + pattern = FcPatternCreate (); + FcPatternAddString (pattern, FC_FAMILY, (FcChar8 *) alias_names[i]); + + FcConfigSubstitute (self->config, pattern, FcMatchPattern); + FcDefaultSubstitute (pattern); + + ret = FcFontMatch (self->config, pattern, &res); + + if (FcPatternGetString (ret, FC_FAMILY, 0, (FcChar8 **)(void*)&family_name) == FcResultMatch) + { + Pango2FontFamily *family = pango2_font_map_get_family (map, family_name); + if (family) + { + Pango2GenericFamily *alias_family; + + alias_family = pango2_generic_family_new (alias_names[i]); + pango2_generic_family_add_family (alias_family, family); + pango2_font_map_add_family (map, PANGO2_FONT_FAMILY (alias_family)); + } + } + + FcPatternDestroy (ret); + FcPatternDestroy (pattern); + } + + /* Add generic aliases. Unfortunately, we need both sans-serif and sans, + * since the old fontconfig backend had 'Sans', and fontconfig itself + * has 'sans-serif' + */ + const char *generic_names[] = { + "Monospace", + "Sans-serif", + "Sans", + "Serif", + "Cursive", + "Fantasy", + }; + FcLangSet *no_langs = FcLangSetCreate (); + FcLangSet *emoji_langs = FcLangSetCreate (); + + FcLangSetAdd (emoji_langs, (FcChar8 *)"und-zsye"); + + for (int i = 0; i < G_N_ELEMENTS (generic_names); i++) + { + Pango2GenericFamily *generic_family; + GHashTable *families_hash; + FcPattern *pattern; + FcFontSet *ret; + FcResult res; + + if (pango2_font_map_get_family (map, generic_names[i])) + continue; + + generic_family = pango2_generic_family_new (generic_names[i]); + + families_hash = g_hash_table_new (NULL, NULL); + + pattern = FcPatternCreate (); + FcPatternAddString (pattern, FC_FAMILY, (FcChar8 *) generic_names[i]); + + FcConfigSubstitute (self->config, pattern, FcMatchPattern); + FcDefaultSubstitute (pattern); + + ret = FcFontSort (self->config, pattern, TRUE, NULL, &res); + for (int j = 0; j < ret->nfont; j++) + { + Pango2HbFamily *family; + const char *file; + int index; + FcLangSet *langs; + int spacing; + const char *family_name; + + pat = ret->fonts[j]; + + if (!is_supported_font_format (pat)) + continue; + + if (FcPatternGetLangSet (pat, FC_LANG, 0, &langs) != FcResultMatch) + continue; + + if (FcLangSetEqual (langs, no_langs)) + continue; + + if ((strcmp (generic_names[i], "emoji") == 0) != FcLangSetEqual (langs, emoji_langs)) + continue; + + if (FcPatternGetInteger (pat, FC_SPACING, 0, &spacing) != FcResultMatch) + spacing = FC_PROPORTIONAL; + + if (strcmp (generic_names[i], "monospace") == 0 && spacing != FC_MONO) + continue; + + if (FcPatternGetString (pat, FC_FAMILY, 0, (FcChar8 **)(void*)&family_name) != FcResultMatch) + continue; + + if (FcPatternGetString (pat, FC_FILE, 0, (FcChar8 **)(void*)&file) != FcResultMatch) + continue; + + if (FcPatternGetInteger (pat, FC_INDEX, 0, &index) != FcResultMatch) + index = 0; + + index = index & 0xffff; + + family = PANGO2_HB_FAMILY (pango2_font_map_get_family (map, family_name)); + if (!family) + continue; + + if (g_hash_table_contains (families_hash, family)) + continue; + + pango2_generic_family_add_family (generic_family, PANGO2_FONT_FAMILY (family)); + g_hash_table_add (families_hash, family); + } + + FcFontSetDestroy (ret); + FcPatternDestroy (pattern); + g_hash_table_unref (families_hash); + + if (g_list_model_get_n_items (G_LIST_MODEL (generic_family)) > 0) + pango2_font_map_add_family (map, PANGO2_FONT_FAMILY (generic_family)); + else + g_object_unref (generic_family); + } + + FcLangSetDestroy (no_langs); + FcLangSetDestroy (emoji_langs); + + FcObjectSetDestroy (os); + + pango2_trace_mark (before, "populate FcFontMap", NULL); +} + +/* }}} */ +/* {{{ Pango2FcFontMap implementation */ + +G_DEFINE_FINAL_TYPE (Pango2FcFontMap, pango2_fc_font_map, PANGO2_TYPE_FONT_MAP) + +static void +pango2_fc_font_map_init (Pango2FcFontMap *self) +{ + pango2_font_map_repopulate (PANGO2_FONT_MAP (self), TRUE); +} + +static void +pango2_fc_font_map_finalize (GObject *object) +{ + Pango2FcFontMap *self = PANGO2_FC_FONT_MAP (object); + + if (self->config) + FcConfigDestroy (self->config); + + G_OBJECT_CLASS (pango2_fc_font_map_parent_class)->finalize (object); +} + +static void +pango2_fc_font_map_class_init (Pango2FcFontMapClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + Pango2FontMapClass *font_map_class = PANGO2_FONT_MAP_CLASS (class); + gint64 before G_GNUC_UNUSED; + + before = PANGO2_TRACE_CURRENT_TIME; + + FcInit (); + + pango2_trace_mark (before, "FcInit", NULL); + + object_class->finalize = pango2_fc_font_map_finalize; + + font_map_class->populate = pango2_fc_font_map_populate; +} + +/* }}} */ +/* {{{ Public API */ + +/** + * pango2_fc_font_map_new: + * + * Creates a new `Pango2FcFontMap` using the default + * fontconfig configuration. + * + * Returns: a new `Pango2FcFontMap` + */ +Pango2FcFontMap * +pango2_fc_font_map_new (void) +{ + return g_object_new (PANGO2_TYPE_FC_FONT_MAP, NULL); +} + +/** + * pango2_fc_font_map_set_config: (skip) + * @self: a `Pango2FcFontMap` + * @config: (nullable): the `FcConfig` to use, or `NULL` to use + * the default configuration + * + * Sets the fontconfig configuration to use. + * + * Note that changing the fontconfig configuration + * removes all cached font families, faces and fonts. + */ +void +pango2_fc_font_map_set_config (Pango2FcFontMap *self, + FcConfig *config) +{ + g_return_if_fail (PANGO2_IS_FC_FONT_MAP (self)); + + if (self->config == config && FcConfigUptoDate (config)) + return; + + if (self->config != config) + { + if (self->config) + FcConfigDestroy (self->config); + + self->config = config; + + if (self->config) + FcConfigReference (self->config); + } + + pango2_font_map_repopulate (PANGO2_FONT_MAP (self), TRUE); +} + +/** + * pango2_fc_font_map_get_config: (skip) + * @self: a `Pango2FcFontMap` + * + * Fetches the fontconfig configuration of the fontmap. + * + * See also: [method@Pango2Fc.FontMap.set_config]. + * + * Returns: (nullable): the `FcConfig` object attached to + * @self, which might be `NULL`. The return value is + * owned by Pango2 and should not be freed. + */ +FcConfig * +pango2_fc_font_map_get_config (Pango2FcFontMap *self) +{ + g_return_val_if_fail (PANGO2_IS_FC_FONT_MAP (self), NULL); + + return self->config; +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/pangofc-fontmap.h b/pango2/pangofc-fontmap.h new file mode 100644 index 00000000..7af8f0bb --- /dev/null +++ b/pango2/pangofc-fontmap.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2021 Red Hat, Inc + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pango2/pango.h> +#include <fontconfig/fontconfig.h> + +G_BEGIN_DECLS + +#define PANGO2_TYPE_FC_FONT_MAP (pango2_fc_font_map_get_type ()) + +PANGO2_AVAILABLE_IN_ALL +PANGO2_DECLARE_INTERNAL_TYPE (Pango2FcFontMap, pango2_fc_font_map, PANGO2, FC_FONT_MAP, Pango2FontMap) + +PANGO2_AVAILABLE_IN_ALL +Pango2FcFontMap * pango2_fc_font_map_new (void); + +PANGO2_AVAILABLE_IN_ALL +void pango2_fc_font_map_set_config (Pango2FcFontMap *self, + FcConfig *config); + +PANGO2_AVAILABLE_IN_ALL +FcConfig * pango2_fc_font_map_get_config (Pango2FcFontMap *self); + +G_END_DECLS diff --git a/pango2/pangofc-language-set-private.h b/pango2/pangofc-language-set-private.h new file mode 100644 index 00000000..e1c48d4c --- /dev/null +++ b/pango2/pangofc-language-set-private.h @@ -0,0 +1,46 @@ +/* + * Copyright 2022 Red Hat, Inc. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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 2.1 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "pango-language-set-private.h" +#include <fontconfig/fontconfig.h> + + +#define PANGO2_TYPE_FC_LANGUAGE_SET (pango2_fc_language_set_get_type ()) + +G_DECLARE_FINAL_TYPE (Pango2FcLanguageSet, pango2_fc_language_set, PANGO2, FC_LANGUAGE_SET, Pango2LanguageSet) + +struct _Pango2FcLanguageSet +{ + Pango2LanguageSet parent_instance; + + FcLangSet *langs; +}; + +struct _Pango2FcLanguageSetClass +{ + Pango2LanguageSetClass parent_class; +}; + +Pango2FcLanguageSet * pango2_fc_language_set_new (FcLangSet *langs); + +guint pango2_fc_language_set_hash (const Pango2FcLanguageSet *self); +gboolean pango2_fc_language_set_equal (const Pango2FcLanguageSet *a, + const Pango2FcLanguageSet *b); diff --git a/pango2/pangofc-language-set.c b/pango2/pangofc-language-set.c new file mode 100644 index 00000000..1c4534c2 --- /dev/null +++ b/pango2/pangofc-language-set.c @@ -0,0 +1,142 @@ +/* Pango2 + * + * Copyright (C) 2021 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include "pangofc-language-set-private.h" + + +G_DEFINE_TYPE (Pango2FcLanguageSet, pango2_fc_language_set, PANGO2_TYPE_LANGUAGE_SET) + +static void +pango2_fc_language_set_init (Pango2FcLanguageSet *self) +{ +} + +static void +pango2_fc_language_set_finalize (GObject *object) +{ + Pango2FcLanguageSet *self = PANGO2_FC_LANGUAGE_SET (object); + + FcLangSetDestroy (self->langs); + + G_OBJECT_CLASS (pango2_fc_language_set_parent_class)->finalize (object); +} + +static gboolean +pango2_fc_language_set_matches_language (Pango2LanguageSet *set, + Pango2Language *language) +{ + Pango2FcLanguageSet *self = PANGO2_FC_LANGUAGE_SET (set); + const char *s; + + if (language == pango2_language_from_string ("c")) + return TRUE; + + if (FcLangSetHasLang (self->langs, (FcChar8 *) pango2_language_to_string (language)) != FcLangDifferentLang) + return TRUE; + + s = pango2_language_to_string (language); + if (strchr (s, '-')) + { + char buf[10]; + + for (int i = 0; i < 10; i++) + { + buf[i] = s[i]; + if (buf[i] == '-') + { + buf[i] = '\0'; + break; + } + } + buf[9] = '\0'; + + if (FcLangSetHasLang (self->langs, (FcChar8 *) buf) != FcLangDifferentLang) + return TRUE; + } + + return FALSE; +} + +static Pango2Language ** +pango2_fc_language_set_get_languages (Pango2LanguageSet *set) +{ + Pango2FcLanguageSet *self = PANGO2_FC_LANGUAGE_SET (set); + FcStrSet *strset; + FcStrList *list; + FcChar8 *s; + GPtrArray *langs; + + langs = g_ptr_array_new (); + + strset = FcLangSetGetLangs (self->langs); + list = FcStrListCreate (strset); + + FcStrListFirst (list); + while ((s = FcStrListNext (list))) + g_ptr_array_add (langs, pango2_language_from_string ((const char *)s)); + + FcStrListDone (list); + FcStrSetDestroy (strset); + + g_ptr_array_add (langs, NULL); + + return (Pango2Language **) g_ptr_array_free (langs, FALSE); +} + +static void +pango2_fc_language_set_class_init (Pango2FcLanguageSetClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + Pango2LanguageSetClass *language_set_class = PANGO2_LANGUAGE_SET_CLASS (class); + + object_class->finalize = pango2_fc_language_set_finalize; + + language_set_class->matches_language = pango2_fc_language_set_matches_language; + language_set_class->get_languages = pango2_fc_language_set_get_languages; +} + +Pango2FcLanguageSet * +pango2_fc_language_set_new (FcLangSet *langs) +{ + Pango2FcLanguageSet *self; + + self = g_object_new (PANGO2_TYPE_FC_LANGUAGE_SET, NULL); + + self->langs = FcLangSetCopy (langs); + + return self; +} + +guint +pango2_fc_language_set_hash (const Pango2FcLanguageSet *self) +{ + return (guint) FcLangSetHash (self->langs); +} + +gboolean +pango2_fc_language_set_equal (const Pango2FcLanguageSet *a, + const Pango2FcLanguageSet *b) +{ + return FcLangSetEqual (a->langs, b->langs); +} + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/serializer.c b/pango2/serializer.c new file mode 100644 index 00000000..4e7e8a39 --- /dev/null +++ b/pango2/serializer.c @@ -0,0 +1,1803 @@ +/* Pango2 + * serializer.c: Code to serialize various Pango2 objects + * + * Copyright (C) 2021 Red Hat, Inc + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include "pango-layout.h" +#include "pango-context-private.h" +#include "pango-enum-types.h" +#include "pango-font-private.h" +#include "pango-line-private.h" +#include "pango-hbface.h" +#include "pango-hbfont.h" +#include "pango-attributes.h" +#include "pango-attr-private.h" +#include "pango-item-private.h" + +#include <hb-ot.h> +#include "json/gtkjsonparserprivate.h" +#include "json/gtkjsonprinterprivate.h" + +/* {{{ Error handling */ + +G_DEFINE_QUARK(pango-layout-deserialize-error-quark, pango2_layout_deserialize_error) + +/* }}} */ +/* {{{ Enum names */ + +static const char *style_names[] = { + "normal", + "oblique", + "italic", + NULL +}; + +static const char *variant_names[] = { + "normal", + "small-caps", + "all-small-caps", + "petite-caps", + "all-petite-caps", + "unicase", + "titlecase", + NULL +}; + +static const char *stretch_names[] = { + "ultra-condensed", + "extra-condensed", + "condensed", + "semi-condensed", + "normal", + "semi-expanded", + "expanded", + "extra-expanded", + "ultra-expanded", + NULL +}; + +static const char *line_style_names[] = { + "none", + "single", + "double", + "dashed", + "dotted", + "wavy", + NULL +}; + +static const char *underline_position_names[] = { + "normal", + "under", + NULL +}; + +static const char *gravity_names[] = { + "south", + "east", + "north", + "west", + "auto", + NULL +}; + +static const char *gravity_hint_names[] = { + "natural", + "strong", + "line", + NULL +}; + +static const char *text_transform_names[] = { + "none", + "lowercase", + "uppercase", + "capitalize", + NULL +}; + +static const char *baseline_shift_names[] = { + "none", + "superscript", + "subscript", + NULL +}; + +static const char *font_scale_names[] = { + "none", + "superscript", + "subscript", + "small-caps", + NULL +}; + +static const char *weight_names[] = { + "thin", + "ultralight", + "light", + "semilight", + "book", + "normal", + "medium", + "semibold", + "bold", + "ultrabold", + "heavy", + "ultraheavy", + NULL +}; + +static int named_weights[] = { 100, 200, 300, 350, 380, 400, 500, 600, 700, 800, 900, 1000 }; + +static int +get_weight (int pos) +{ + return named_weights[pos]; +} + +static const char * +get_weight_name (int weight) +{ + for (int i = 0; i < G_N_ELEMENTS (named_weights); i++) + { + if (named_weights[i] == weight) + return weight_names[i]; + } + + return NULL; +} + +static Pango2AttrType +get_attr_type (const char *nick) +{ + GEnumClass *enum_class; + GEnumValue *enum_value = NULL; + + enum_class = g_type_class_ref (PANGO2_TYPE_ATTR_TYPE); + for (int i = 0; i < enum_class->n_values; i++) + { + enum_value = &enum_class->values[i]; + if (strcmp (enum_value->value_nick, nick) == 0) + break; + enum_value = NULL; + } + g_type_class_unref (enum_class); + + if (enum_value) + return enum_value->value; + + return 0; +} + +static const char * +get_attr_type_name (Pango2AttrType type) +{ + GEnumClass *enum_class; + GEnumValue *enum_value = NULL; + + enum_class = g_type_class_ref (PANGO2_TYPE_ATTR_TYPE); + for (int i = 0; i < enum_class->n_values; i++) + { + enum_value = &enum_class->values[i]; + if (enum_value->value == type) + break; + enum_value = NULL; + } + g_type_class_unref (enum_class); + + if (enum_value) + return enum_value->value_nick; + + return NULL; +} + +static void +get_script_name (GUnicodeScript script, + char *buf) +{ + guint32 tag = g_unicode_script_to_iso15924 (script); + hb_tag_to_string (tag, buf); +} + +static const char *tab_align_names[] = { + "left", + "right", + "center", + "decimal", + NULL +}; + +static const char *direction_names[] = { + "ltr", + "rtl", + "ttb-ltr", + "ttb-rtl", + "weak-ltr", + "weak-rtl", + "neutral", + NULL +}; + +static const char *wrap_names[] = { + "word", + "char", + "word-char", + NULL +}; + +static const char *alignment_names[] = { + "left", + "center", + "right", + "natural", + "justify", + NULL +}; + +static const char *ellipsize_names[] = { + "none", + "start", + "middle", + "end", + NULL +}; + +/* }}} */ +/* {{{ Serialization */ + +static char * +font_description_to_string (Pango2FontDescription *desc) +{ + Pango2FontDescription *copy; + char *s; + + /* Leave out the faceid for now, since it would make serialization + * backend-dependent. + */ + copy = pango2_font_description_copy_static (desc); + pango2_font_description_unset_fields (copy, PANGO2_FONT_MASK_FACEID); + s = pango2_font_description_to_string (copy); + pango2_font_description_free (copy); + + return s; +} + +static void +add_attribute (GtkJsonPrinter *printer, + Pango2Attribute *attr) +{ + char *str; + + gtk_json_printer_start_object (printer, NULL); + + if (attr->start_index != PANGO2_ATTR_INDEX_FROM_TEXT_BEGINNING) + gtk_json_printer_add_integer (printer, "start", (int)attr->start_index); + if (attr->end_index != PANGO2_ATTR_INDEX_TO_TEXT_END) + gtk_json_printer_add_integer (printer, "end", (int)attr->end_index); + gtk_json_printer_add_string (printer, "type", get_attr_type_name (attr->type)); + + switch (PANGO2_ATTR_VALUE_TYPE (attr)) + { + case PANGO2_ATTR_VALUE_STRING: + gtk_json_printer_add_string (printer, "value", attr->str_value); + break; + + case PANGO2_ATTR_VALUE_INT: + switch ((int)attr->type) + { + case PANGO2_ATTR_STYLE: + gtk_json_printer_add_string (printer, "value", style_names[attr->int_value]); + break; + + case PANGO2_ATTR_VARIANT: + gtk_json_printer_add_string (printer, "value", variant_names[attr->int_value]); + break; + + case PANGO2_ATTR_STRETCH: + gtk_json_printer_add_string (printer, "value", stretch_names[attr->int_value]); + break; + + case PANGO2_ATTR_UNDERLINE: + case PANGO2_ATTR_STRIKETHROUGH: + case PANGO2_ATTR_OVERLINE: + gtk_json_printer_add_string (printer, "value", line_style_names[attr->int_value]); + break; + + case PANGO2_ATTR_UNDERLINE_POSITION: + gtk_json_printer_add_string (printer, "value", underline_position_names[attr->int_value]); + break; + + case PANGO2_ATTR_GRAVITY: + gtk_json_printer_add_string (printer, "value", gravity_names[attr->int_value]); + break; + + case PANGO2_ATTR_GRAVITY_HINT: + gtk_json_printer_add_string (printer, "value", gravity_hint_names[attr->int_value]); + break; + + case PANGO2_ATTR_TEXT_TRANSFORM: + gtk_json_printer_add_string (printer, "value", text_transform_names[attr->int_value]); + break; + + case PANGO2_ATTR_FONT_SCALE: + gtk_json_printer_add_string (printer, "value", font_scale_names[attr->int_value]); + break; + + case PANGO2_ATTR_WEIGHT: + { + const char *name = get_weight_name (attr->int_value); + if (name) + gtk_json_printer_add_string (printer, "value", name); + else + gtk_json_printer_add_integer (printer, "value", attr->int_value); + } + break; + + case PANGO2_ATTR_BASELINE_SHIFT: + gtk_json_printer_add_string (printer, "value", baseline_shift_names[attr->int_value]); + break; + + default: + gtk_json_printer_add_integer (printer, "value", attr->int_value); + break; + } + + break; + + case PANGO2_ATTR_VALUE_BOOLEAN: + gtk_json_printer_add_boolean (printer, "value", attr->boolean_value); + break; + + case PANGO2_ATTR_VALUE_LANGUAGE: + gtk_json_printer_add_string (printer, "value", pango2_language_to_string (attr->lang_value)); + break; + + case PANGO2_ATTR_VALUE_FONT_DESC: + str = font_description_to_string (attr->font_value); + gtk_json_printer_add_string (printer, "value", str); + g_free (str); + break; + + case PANGO2_ATTR_VALUE_COLOR: + str = pango2_color_to_string (&attr->color_value); + gtk_json_printer_add_string (printer, "value", str); + g_free (str); + break; + + case PANGO2_ATTR_VALUE_FLOAT: + gtk_json_printer_add_number (printer, "value", attr->double_value); + break; + + case PANGO2_ATTR_VALUE_POINTER: + str = pango2_attr_value_serialize (attr); + gtk_json_printer_add_string (printer, "value", str); + g_free (str); + break; + + default: + g_assert_not_reached (); + } + + gtk_json_printer_end (printer); +} + +static void +add_attr_list (GtkJsonPrinter *printer, + Pango2AttrList *attrs) +{ + GSList *attributes, *l; + + if (!attrs) + return; + + attributes = pango2_attr_list_get_attributes (attrs); + + if (!attributes) + return; + + gtk_json_printer_start_array (printer, "attributes"); + + for (l = attributes; l; l = l->next) + { + Pango2Attribute *attr = l->data; + add_attribute (printer, attr); + } + g_slist_free_full (attributes, (GDestroyNotify) pango2_attribute_destroy); + + gtk_json_printer_end (printer); +} + +static void +add_tab_array (GtkJsonPrinter *printer, + Pango2TabArray *tabs) +{ + if (!tabs || pango2_tab_array_get_size (tabs) == 0) + return; + + gtk_json_printer_start_object (printer, "tabs"); + + gtk_json_printer_add_boolean (printer, "positions-in-pixels", pango2_tab_array_get_positions_in_pixels (tabs)); + gtk_json_printer_start_array (printer, "positions"); + for (int i = 0; i < pango2_tab_array_get_size (tabs); i++) + { + Pango2TabAlign align; + int pos; + pango2_tab_array_get_tab (tabs, i, &align, &pos); + gtk_json_printer_start_object (printer, NULL); + gtk_json_printer_add_integer (printer, "position", pos); + gtk_json_printer_add_string (printer, "alignment", tab_align_names[align]); + gtk_json_printer_add_integer (printer, "decimal-point", pango2_tab_array_get_decimal_point (tabs, i)); + gtk_json_printer_end (printer); + } + gtk_json_printer_end (printer); + + gtk_json_printer_end (printer); +} + +static void +add_context (GtkJsonPrinter *printer, + Pango2Context *context) +{ + char *str; + const Pango2Matrix *matrix; + Pango2Matrix identity = PANGO2_MATRIX_INIT; + + gtk_json_printer_start_object (printer, "context"); + + /* Note: since we don't create the context when deserializing, + * we don't strip out default values here to ensure that the + * context gets updated as expected. + */ + + str = font_description_to_string (context->font_desc); + gtk_json_printer_add_string (printer, "font", str); + g_free (str); + + if (context->set_language) + gtk_json_printer_add_string (printer, "language", pango2_language_to_string (context->set_language)); + + gtk_json_printer_add_string (printer, "base-gravity", gravity_names[context->base_gravity]); + gtk_json_printer_add_string (printer, "gravity-hint", gravity_hint_names[context->gravity_hint]); + gtk_json_printer_add_string (printer, "base-dir", direction_names[context->base_dir]); + gtk_json_printer_add_boolean (printer, "round-glyph-positions", context->round_glyph_positions); + + matrix = pango2_context_get_matrix (context); + if (!matrix) + matrix = &identity; + + gtk_json_printer_start_array (printer, "transform"); + gtk_json_printer_add_number (printer, NULL, matrix->xx); + gtk_json_printer_add_number (printer, NULL, matrix->xy); + gtk_json_printer_add_number (printer, NULL, matrix->yx); + gtk_json_printer_add_number (printer, NULL, matrix->yy); + gtk_json_printer_add_number (printer, NULL, matrix->x0); + gtk_json_printer_add_number (printer, NULL, matrix->y0); + gtk_json_printer_end (printer); + + gtk_json_printer_end (printer); +} + +static void +add_log_attrs (GtkJsonPrinter *printer, + const Pango2LogAttr *log_attrs, + int n_attrs) +{ + gtk_json_printer_start_array (printer, "log-attrs"); + + for (int i = 0; i < n_attrs; i++) + { + gtk_json_printer_start_object (printer, NULL); + if (log_attrs[i].is_line_break) + gtk_json_printer_add_boolean (printer, "line-break", TRUE); + if (log_attrs[i].is_mandatory_break) + gtk_json_printer_add_boolean (printer, "mandatory-break", TRUE); + if (log_attrs[i].is_char_break) + gtk_json_printer_add_boolean (printer, "char-break", TRUE); + if (log_attrs[i].is_white) + gtk_json_printer_add_boolean (printer, "white", TRUE); + if (log_attrs[i].is_cursor_position) + gtk_json_printer_add_boolean (printer, "cursor-position", TRUE); + if (log_attrs[i].is_word_start) + gtk_json_printer_add_boolean (printer, "word-start", TRUE); + if (log_attrs[i].is_word_end) + gtk_json_printer_add_boolean (printer, "word-end", TRUE); + if (log_attrs[i].is_sentence_boundary) + gtk_json_printer_add_boolean (printer, "sentence-boundary", TRUE); + if (log_attrs[i].is_sentence_start) + gtk_json_printer_add_boolean (printer, "sentence-start", TRUE); + if (log_attrs[i].is_sentence_end) + gtk_json_printer_add_boolean (printer, "sentence-end", TRUE); + if (log_attrs[i].backspace_deletes_character) + gtk_json_printer_add_boolean (printer, "backspace-deletes-character", TRUE); + if (log_attrs[i].is_expandable_space) + gtk_json_printer_add_boolean (printer, "expandable-space", TRUE); + if (log_attrs[i].is_word_boundary) + gtk_json_printer_add_boolean (printer, "word-boundary", TRUE); + if (log_attrs[i].break_inserts_hyphen) + gtk_json_printer_add_boolean (printer, "break-inserts-hyphen", TRUE); + if (log_attrs[i].break_removes_preceding) + gtk_json_printer_add_boolean (printer, "break-removes-preceding", TRUE); + gtk_json_printer_end (printer); + } + + gtk_json_printer_end (printer); +} + +static void +add_font (GtkJsonPrinter *printer, + const char *member, + Pango2Font *font) +{ + Pango2FontDescription *desc; + char *str; + hb_font_t *hb_font; + hb_face_t *face; + hb_blob_t *blob; + const char *data; + guint length; + const int *coords; + const hb_feature_t *features; + Pango2Matrix matrix; + + gtk_json_printer_start_object (printer, member); + + desc = pango2_font_describe (font); + str = font_description_to_string (desc); + gtk_json_printer_add_string (printer, "description", str); + g_free (str); + pango2_font_description_free (desc); + + hb_font = pango2_font_get_hb_font (font); + face = hb_font_get_face (hb_font); + blob = hb_face_reference_blob (face); + + data = hb_blob_get_data (blob, &length); + str = g_compute_checksum_for_data (G_CHECKSUM_SHA256, (const guchar *)data, length); + + gtk_json_printer_add_string (printer, "checksum", str); + + g_free (str); + hb_blob_destroy (blob); + + coords = hb_font_get_var_coords_normalized (hb_font, &length); + if (length > 0) + { + guint count; + hb_ot_var_axis_info_t *axes; + + count = hb_ot_var_get_axis_count (face); + g_assert (count == length); + + axes = g_alloca (count * sizeof (hb_ot_var_axis_info_t)); + hb_ot_var_get_axis_infos (face, 0, &count, axes); + + gtk_json_printer_start_object (printer, "variations"); + + for (int i = 0; i < length; i++) + { + char buf[5] = { 0, }; + + hb_tag_to_string (axes[i].tag, buf); + gtk_json_printer_add_integer (printer, buf, coords[i]); + } + + gtk_json_printer_end (printer); + } + + length = 0; + if (PANGO2_IS_HB_FONT (font)) + features = pango2_hb_font_get_features (PANGO2_HB_FONT (font), &length); + if (length > 0) + { + gtk_json_printer_start_object (printer, "features"); + + for (int i = 0; i < length; i++) + { + char buf[5] = { 0, }; + + hb_tag_to_string (features[i].tag, buf); + gtk_json_printer_add_integer (printer, buf, features[i].value); + } + + gtk_json_printer_end (printer); + } + + pango2_font_get_transform (font, &matrix); + if (!pango2_matrix_equal (&matrix, &(const Pango2Matrix)PANGO2_MATRIX_INIT)) + { + gtk_json_printer_start_array (printer, "matrix"); + gtk_json_printer_add_number (printer, NULL, matrix.xx); + gtk_json_printer_add_number (printer, NULL, matrix.xy); + gtk_json_printer_add_number (printer, NULL, matrix.yx); + gtk_json_printer_add_number (printer, NULL, matrix.yy); + gtk_json_printer_add_number (printer, NULL, matrix.x0); + gtk_json_printer_add_number (printer, NULL, matrix.y0); + gtk_json_printer_end (printer); + } + + gtk_json_printer_end (printer); +} + +#define ANALYSIS_FLAGS (PANGO2_ANALYSIS_FLAG_CENTERED_BASELINE | \ + PANGO2_ANALYSIS_FLAG_IS_ELLIPSIS | \ + PANGO2_ANALYSIS_FLAG_NEED_HYPHEN) + +static void +add_run (GtkJsonPrinter *printer, + const char *text, + Pango2GlyphItem *run) +{ + char *str; + char buf[5] = { 0, }; + + gtk_json_printer_start_object (printer, NULL); + + gtk_json_printer_add_integer (printer, "offset", run->item->offset); + gtk_json_printer_add_integer (printer, "length", run->item->length); + + str = g_strndup (text + run->item->offset, run->item->length); + gtk_json_printer_add_string (printer, "text", str); + g_free (str); + + gtk_json_printer_add_integer (printer, "bidi-level", run->item->analysis.level); + gtk_json_printer_add_string (printer, "gravity", gravity_names[run->item->analysis.gravity]); + gtk_json_printer_add_string (printer, "language", pango2_language_to_string (run->item->analysis.language)); + get_script_name (run->item->analysis.script, buf); + gtk_json_printer_add_string (printer, "script", buf); + + add_font (printer, "font", run->item->analysis.font); + + gtk_json_printer_add_integer (printer, "flags", run->item->analysis.flags & ANALYSIS_FLAGS); + + if (run->item->analysis.extra_attrs) + { + GSList *l; + + gtk_json_printer_start_array (printer, "extra-attributes"); + for (l = run->item->analysis.extra_attrs; l; l = l->next) + { + Pango2Attribute *attr = l->data; + add_attribute (printer, attr); + } + gtk_json_printer_end (printer); + } + + gtk_json_printer_add_integer (printer, "y-offset", run->y_offset); + gtk_json_printer_add_integer (printer, "start-x-offset", run->start_x_offset); + gtk_json_printer_add_integer (printer, "end-x-offset", run->end_x_offset); + + gtk_json_printer_start_array (printer, "glyphs"); + for (int i = 0; i < run->glyphs->num_glyphs; i++) + { + gtk_json_printer_start_object (printer, NULL); + + gtk_json_printer_add_integer (printer, "glyph", run->glyphs->glyphs[i].glyph); + gtk_json_printer_add_integer (printer, "width", run->glyphs->glyphs[i].geometry.width); + + if (run->glyphs->glyphs[i].geometry.x_offset != 0) + gtk_json_printer_add_integer (printer, "x-offset", run->glyphs->glyphs[i].geometry.x_offset); + + if (run->glyphs->glyphs[i].geometry.y_offset != 0) + gtk_json_printer_add_integer (printer, "y-offset", run->glyphs->glyphs[i].geometry.y_offset); + + if (run->glyphs->glyphs[i].attr.is_cluster_start) + gtk_json_printer_add_boolean (printer, "is-cluster-start", TRUE); + + if (run->glyphs->glyphs[i].attr.is_color) + gtk_json_printer_add_boolean (printer, "is-color", TRUE); + + gtk_json_printer_add_integer (printer, "log-cluster", run->glyphs->log_clusters[i]); + + gtk_json_printer_end (printer); + } + + gtk_json_printer_end (printer); + + gtk_json_printer_end (printer); +} + +#undef ANALYSIS_FLAGS + +static void +line_to_json (GtkJsonPrinter *printer, + Pango2Line *line, + int x, + int y) +{ + gtk_json_printer_start_object (printer, NULL); + + gtk_json_printer_start_array (printer, "position"); + gtk_json_printer_add_number (printer, NULL, x); + gtk_json_printer_add_number (printer, NULL, y); + gtk_json_printer_end (printer); + + gtk_json_printer_start_object (printer, "line"); + + gtk_json_printer_add_integer (printer, "start-index", line->start_index); + gtk_json_printer_add_integer (printer, "length", line->length); + gtk_json_printer_add_integer (printer, "start-offset", line->start_offset); + gtk_json_printer_add_integer (printer, "n-chars", line->n_chars); + + gtk_json_printer_add_boolean (printer, "wrapped", line->wrapped); + gtk_json_printer_add_boolean (printer, "ellipsized", line->ellipsized); + gtk_json_printer_add_boolean (printer, "hyphenated", line->hyphenated); + gtk_json_printer_add_boolean (printer, "justified", line->justified); + gtk_json_printer_add_boolean (printer, "paragraph-start", line->starts_paragraph); + gtk_json_printer_add_boolean (printer, "paragraph-end", line->ends_paragraph); + gtk_json_printer_add_string (printer, "direction", direction_names[line->direction]); + + gtk_json_printer_start_array (printer, "runs"); + for (GSList *l = line->runs; l; l = l->next) + { + Pango2GlyphItem *run = l->data; + add_run (printer, line->data->text, run); + } + gtk_json_printer_end (printer); + + gtk_json_printer_end (printer); + + gtk_json_printer_end (printer); +} + +static void +lines_to_json (GtkJsonPrinter *printer, + Pango2Lines *lines) +{ + int width, height; + Pango2Line **l; + + gtk_json_printer_start_object (printer, "output"); + + gtk_json_printer_add_boolean (printer, "wrapped", pango2_lines_is_wrapped (lines)); + gtk_json_printer_add_boolean (printer, "ellipsized", pango2_lines_is_ellipsized (lines)); + gtk_json_printer_add_boolean (printer, "hypenated", pango2_lines_is_hyphenated (lines)); + gtk_json_printer_add_integer (printer, "unknown-glyphs", pango2_lines_get_unknown_glyphs_count (lines)); + pango2_lines_get_size (lines, &width, &height); + gtk_json_printer_add_integer (printer, "width", width); + gtk_json_printer_add_integer (printer, "height", height); + + gtk_json_printer_start_array (printer, "lines"); + + l = pango2_lines_get_lines (lines); + for (int i = 0; i < pango2_lines_get_line_count (lines); i++) + { + int x, y; + pango2_lines_get_line_position (lines, i, &x, &y); + line_to_json (printer, l[i], x, y); + } + + gtk_json_printer_end (printer); + + gtk_json_printer_end (printer); +} + +static void +layout_to_json (GtkJsonPrinter *printer, + Pango2Layout *layout, + Pango2LayoutSerializeFlags flags) +{ + const char *str; + + gtk_json_printer_start_object (printer, NULL); + + if (flags & PANGO2_LAYOUT_SERIALIZE_CONTEXT) + add_context (printer, pango2_layout_get_context (layout)); + + str = (const char *) g_object_get_data (G_OBJECT (layout), "comment"); + if (str) + gtk_json_printer_add_string (printer, "comment", str); + + gtk_json_printer_add_string (printer, "text", pango2_layout_get_text (layout)); + + add_attr_list (printer, pango2_layout_get_attributes (layout)); + + if (pango2_layout_get_font_description (layout)) + { + char *str = pango2_font_description_to_string (pango2_layout_get_font_description (layout)); + gtk_json_printer_add_string (printer, "font", str); + g_free (str); + } + + add_tab_array (printer, pango2_layout_get_tabs (layout)); + + if (!pango2_layout_get_auto_dir (layout)) + gtk_json_printer_add_boolean (printer, "auto-dir", FALSE); + + if (pango2_layout_get_alignment (layout) != PANGO2_ALIGN_NATURAL) + gtk_json_printer_add_string (printer, "alignment", alignment_names[pango2_layout_get_alignment (layout)]); + + if (pango2_layout_get_wrap (layout) != PANGO2_WRAP_WORD) + gtk_json_printer_add_string (printer, "wrap", wrap_names[pango2_layout_get_wrap (layout)]); + + if (pango2_layout_get_ellipsize (layout) != PANGO2_ELLIPSIZE_NONE) + gtk_json_printer_add_string (printer, "ellipsize", ellipsize_names[pango2_layout_get_ellipsize (layout)]); + + if (pango2_layout_get_width (layout) != -1) + gtk_json_printer_add_integer (printer, "width", pango2_layout_get_width (layout)); + + if (pango2_layout_get_height (layout) != -1) + gtk_json_printer_add_integer (printer, "height", pango2_layout_get_height (layout)); + + if (pango2_layout_get_indent (layout) != 0) + gtk_json_printer_add_integer (printer, "indent", pango2_layout_get_indent (layout)); + + if (pango2_layout_get_line_height (layout) != 0.) + gtk_json_printer_add_number (printer, "line-height", pango2_layout_get_line_height (layout)); + + if (pango2_layout_get_spacing (layout) != 0) + gtk_json_printer_add_integer (printer, "spacing", pango2_layout_get_spacing (layout)); + + if (flags & PANGO2_LAYOUT_SERIALIZE_OUTPUT) + { + const Pango2LogAttr *log_attrs; + int n_attrs; + + log_attrs = pango2_layout_get_log_attrs (layout, &n_attrs); + add_log_attrs (printer, log_attrs, n_attrs); + + lines_to_json (printer, pango2_layout_get_lines (layout)); + } + + gtk_json_printer_end (printer); +} + +static void +gstring_write (GtkJsonPrinter *printer, + const char *s, + gpointer data) +{ + GString *str = data; + g_string_append (str, s); +} + +/* }}} */ +/* {{{ Deserialization */ + +static int +parser_select_string (GtkJsonParser *parser, + const char **options) +{ + int value; + + value = gtk_json_parser_select_string (parser, options); + if (value == -1) + { + char *str = gtk_json_parser_get_string (parser); + char *opts = g_strjoinv (", ", (char **)options); + + gtk_json_parser_value_error (parser, + "Failed to parse string: %s, valid options are: %s", + str, opts); + + g_free (opts); + g_free (str); + + value = 0; + } + + return value; +} + +static Pango2FontDescription * +parser_get_font_description (GtkJsonParser *parser) +{ + char *str = gtk_json_parser_get_string (parser); + Pango2FontDescription *desc = pango2_font_description_from_string (str); + + if (!desc) + gtk_json_parser_value_error (parser, + "Failed to parse font: %s", str); + g_free (str); + + return desc; +} + +static void +parser_get_color (GtkJsonParser *parser, + Pango2Color *color) +{ + char *str = gtk_json_parser_get_string (parser); + if (!pango2_color_parse (color, str)) + { + gtk_json_parser_value_error (parser, + "Failed to parse color: %s", str); + color->red = color->green = color->blue = 0; + } + + g_free (str); +} + +static Pango2Attribute * +attr_for_type (GtkJsonParser *parser, + Pango2AttrType type, + int start, + int end) +{ + Pango2Attribute *attr; + Pango2FontDescription *desc; + Pango2Color color; + char *str; + + switch (type) + { + case PANGO2_ATTR_SHAPE: + default: + g_assert_not_reached (); + + case PANGO2_ATTR_INVALID: + gtk_json_parser_schema_error (parser, "Missing attribute type"); + return NULL; + + case PANGO2_ATTR_LANGUAGE: + str = gtk_json_parser_get_string (parser); + attr = pango2_attr_language_new (pango2_language_from_string (str)); + g_free (str); + break; + + case PANGO2_ATTR_FAMILY: + str = gtk_json_parser_get_string (parser); + attr = pango2_attr_family_new (str); + g_free (str); + break; + + case PANGO2_ATTR_STYLE: + attr = pango2_attr_style_new ((Pango2Style) parser_select_string (parser, style_names)); + break; + + case PANGO2_ATTR_WEIGHT: + if (gtk_json_parser_get_node (parser) == GTK_JSON_STRING) + attr = pango2_attr_weight_new (get_weight (parser_select_string (parser, weight_names))); + else + attr = pango2_attr_weight_new ((int) gtk_json_parser_get_int (parser)); + break; + + case PANGO2_ATTR_VARIANT: + attr = pango2_attr_variant_new ((Pango2Variant) parser_select_string (parser, variant_names)); + break; + + case PANGO2_ATTR_STRETCH: + attr = pango2_attr_stretch_new ((Pango2Stretch) parser_select_string (parser, stretch_names)); + break; + + case PANGO2_ATTR_SIZE: + attr = pango2_attr_size_new ((int) gtk_json_parser_get_number (parser)); + break; + + case PANGO2_ATTR_FONT_DESC: + desc = parser_get_font_description (parser); + attr = pango2_attr_font_desc_new (desc); + pango2_font_description_free (desc); + break; + + case PANGO2_ATTR_FOREGROUND: + parser_get_color (parser, &color); + attr = pango2_attr_foreground_new (&color); + break; + + case PANGO2_ATTR_BACKGROUND: + parser_get_color (parser, &color); + attr = pango2_attr_background_new (&color); + break; + + case PANGO2_ATTR_UNDERLINE: + attr = pango2_attr_underline_new ((Pango2LineStyle) parser_select_string (parser, line_style_names)); + break; + + case PANGO2_ATTR_UNDERLINE_POSITION: + attr = pango2_attr_underline_position_new ((Pango2UnderlinePosition) parser_select_string (parser, underline_position_names)); + break; + + case PANGO2_ATTR_STRIKETHROUGH: + attr = pango2_attr_strikethrough_new ((Pango2LineStyle) parser_select_string (parser, line_style_names)); + break; + + case PANGO2_ATTR_RISE: + attr = pango2_attr_rise_new ((int) gtk_json_parser_get_number (parser)); + break; + + case PANGO2_ATTR_SCALE: + attr = pango2_attr_scale_new (gtk_json_parser_get_number (parser)); + break; + + case PANGO2_ATTR_FALLBACK: + attr = pango2_attr_fallback_new (gtk_json_parser_get_boolean (parser)); + break; + + case PANGO2_ATTR_LETTER_SPACING: + attr = pango2_attr_letter_spacing_new ((int) gtk_json_parser_get_number (parser)); + break; + + case PANGO2_ATTR_UNDERLINE_COLOR: + parser_get_color (parser, &color); + attr = pango2_attr_underline_color_new (&color); + break; + + case PANGO2_ATTR_STRIKETHROUGH_COLOR: + parser_get_color (parser, &color); + attr = pango2_attr_strikethrough_color_new (&color); + break; + + case PANGO2_ATTR_ABSOLUTE_SIZE: + attr = pango2_attr_size_new_absolute ((int) gtk_json_parser_get_number (parser)); + break; + + case PANGO2_ATTR_GRAVITY: + attr = pango2_attr_gravity_new ((Pango2Gravity) parser_select_string (parser, gravity_names)); + break; + + case PANGO2_ATTR_GRAVITY_HINT: + attr = pango2_attr_gravity_hint_new ((Pango2GravityHint) parser_select_string (parser, gravity_hint_names)); + break; + + case PANGO2_ATTR_FONT_FEATURES: + str = gtk_json_parser_get_string (parser); + attr = pango2_attr_font_features_new (str); + g_free (str); + break; + + case PANGO2_ATTR_ALLOW_BREAKS: + attr = pango2_attr_allow_breaks_new (gtk_json_parser_get_boolean (parser)); + break; + + case PANGO2_ATTR_SHOW: + attr = pango2_attr_show_new ((int) gtk_json_parser_get_number (parser)); + break; + + case PANGO2_ATTR_INSERT_HYPHENS: + attr = pango2_attr_insert_hyphens_new ((int) gtk_json_parser_get_number (parser)); + break; + + case PANGO2_ATTR_OVERLINE: + attr = pango2_attr_overline_new ((Pango2LineStyle) parser_select_string (parser, line_style_names)); + break; + + case PANGO2_ATTR_OVERLINE_COLOR: + parser_get_color (parser, &color); + attr = pango2_attr_overline_color_new (&color); + break; + + case PANGO2_ATTR_LINE_HEIGHT: + attr = pango2_attr_line_height_new (gtk_json_parser_get_number (parser)); + break; + + case PANGO2_ATTR_ABSOLUTE_LINE_HEIGHT: + attr = pango2_attr_line_height_new_absolute ((int) gtk_json_parser_get_number (parser)); + break; + + case PANGO2_ATTR_LINE_SPACING: + attr = pango2_attr_line_spacing_new ((int) gtk_json_parser_get_number (parser)); + break; + + case PANGO2_ATTR_TEXT_TRANSFORM: + attr = pango2_attr_text_transform_new ((Pango2TextTransform) parser_select_string (parser, text_transform_names)); + break; + + case PANGO2_ATTR_WORD: + attr = pango2_attr_word_new (); + break; + + case PANGO2_ATTR_SENTENCE: + attr = pango2_attr_sentence_new (); + break; + + case PANGO2_ATTR_BASELINE_SHIFT: + attr = pango2_attr_baseline_shift_new (parser_select_string (parser, baseline_shift_names)); + break; + + case PANGO2_ATTR_FONT_SCALE: + attr = pango2_attr_font_scale_new ((Pango2FontScale) parser_select_string (parser, font_scale_names)); + break; + + case PANGO2_ATTR_PARAGRAPH: + attr = pango2_attr_paragraph_new (); + break; + + } + + attr->start_index = start; + attr->end_index = end; + + return attr; +} + +enum { + ATTR_START, + ATTR_END, + ATTR_TYPE, + ATTR_VALUE +}; + +static const char *attr_members[] = { + "start", + "end", + "type", + "value", + NULL +}; + +static Pango2Attribute * +json_to_attribute (GtkJsonParser *parser) +{ + Pango2Attribute *attr = NULL; + Pango2AttrType type = PANGO2_ATTR_INVALID; + guint start = PANGO2_ATTR_INDEX_FROM_TEXT_BEGINNING; + guint end = PANGO2_ATTR_INDEX_TO_TEXT_END; + + gtk_json_parser_start_object (parser); + + do + { + switch (gtk_json_parser_select_member (parser, attr_members)) + { + case ATTR_START: + start = (int)gtk_json_parser_get_number (parser); + break; + + case ATTR_END: + end = (int)gtk_json_parser_get_number (parser); + break; + + case ATTR_TYPE: + type = get_attr_type (gtk_json_parser_get_string (parser)); + break; + + case ATTR_VALUE: + attr = attr_for_type (parser, type, start, end); + break; + + default: + break; + } + } + while (gtk_json_parser_next (parser)); + + if (!attr && !gtk_json_parser_get_error (parser)) + { + if (type == PANGO2_ATTR_INVALID) + gtk_json_parser_schema_error (parser, "Invalid attribute \"type\""); + else + gtk_json_parser_schema_error (parser, "Attribute missing \"value\""); + } + + gtk_json_parser_end (parser); + + return attr; +} + +static void +json_parser_fill_attr_list (GtkJsonParser *parser, + Pango2AttrList *attributes) +{ + gtk_json_parser_start_array (parser); + + do + { + Pango2Attribute *attr = json_to_attribute (parser); + if (attr) + pango2_attr_list_insert (attributes, attr); + } + while (gtk_json_parser_next (parser)); + + gtk_json_parser_end (parser); +} + +enum { + TAB_POSITION, + TAB_ALIGNMENT, + TAB_DECIMAL_POINT +}; + +static const char *tab_members[] = { + "position", + "alignment", + "decimal-point", + NULL, +}; + + +static void +json_parser_fill_tabs (GtkJsonParser *parser, + Pango2TabArray *tabs) +{ + int index; + + gtk_json_parser_start_array (parser); + + index = 0; + do + { + int pos; + Pango2TabAlign align = PANGO2_TAB_LEFT; + gunichar ch = 0; + + if (gtk_json_parser_get_node (parser) == GTK_JSON_OBJECT) + { + gtk_json_parser_start_object (parser); + do + { + switch (gtk_json_parser_select_member (parser, tab_members)) + { + case TAB_POSITION: + pos = (int) gtk_json_parser_get_number (parser); + break; + + case TAB_ALIGNMENT: + align = (Pango2TabAlign) parser_select_string (parser, tab_align_names); + break; + + case TAB_DECIMAL_POINT: + ch = (int) gtk_json_parser_get_number (parser); + break; + + default: + break; + } + } + while (gtk_json_parser_next (parser)); + + gtk_json_parser_end (parser); + } + else + pos = (int) gtk_json_parser_get_number (parser); + + pango2_tab_array_set_tab (tabs, index, align, pos); + pango2_tab_array_set_decimal_point (tabs, index, ch); + index++; + } + while (gtk_json_parser_next (parser)); + + gtk_json_parser_end (parser); +} + +enum { + TABS_POSITIONS_IN_PIXELS, + TABS_POSITIONS +}; + +static const char *tabs_members[] = { + "positions-in-pixels", + "positions", + NULL +}; + +static void +json_parser_fill_tab_array (GtkJsonParser *parser, + Pango2TabArray *tabs) +{ + gtk_json_parser_start_object (parser); + + do + { + switch (gtk_json_parser_select_member (parser, tabs_members)) + { + case TABS_POSITIONS_IN_PIXELS: + pango2_tab_array_set_positions_in_pixels (tabs, gtk_json_parser_get_boolean (parser)); + break; + + case TABS_POSITIONS: + json_parser_fill_tabs (parser, tabs); + break; + + default: + break; + } + } + while (gtk_json_parser_next (parser)); + + gtk_json_parser_end (parser); +} + +enum { + CONTEXT_LANGUAGE, + CONTEXT_FONT, + CONTEXT_BASE_GRAVITY, + CONTEXT_GRAVITY_HINT, + CONTEXT_BASE_DIR, + CONTEXT_ROUND_GLYPH_POSITIONS, + CONTEXT_TRANSFORM, +}; + +static const char *context_members[] = { + "language", + "font", + "base-gravity", + "gravity-hint", + "base-dir", + "round-glyph-positions", + "transform", + NULL, +}; + +static void +json_parser_fill_context (GtkJsonParser *parser, + Pango2Context *context) +{ + gtk_json_parser_start_object (parser); + + do + { + char *str; + + switch (gtk_json_parser_select_member (parser, context_members)) + { + case CONTEXT_LANGUAGE: + str = gtk_json_parser_get_string (parser); + Pango2Language *language = pango2_language_from_string (str); + pango2_context_set_language (context, language); + g_free (str); + break; + + case CONTEXT_FONT: + { + Pango2FontDescription *desc = parser_get_font_description (parser); + pango2_context_set_font_description (context, desc); + pango2_font_description_free (desc); + } + break; + + case CONTEXT_BASE_GRAVITY: + pango2_context_set_base_gravity (context, (Pango2Gravity) parser_select_string (parser, gravity_names)); + break; + + case CONTEXT_GRAVITY_HINT: + pango2_context_set_gravity_hint (context, (Pango2GravityHint) parser_select_string (parser, gravity_hint_names)); + break; + + case CONTEXT_BASE_DIR: + pango2_context_set_base_dir (context, (Pango2Direction) parser_select_string (parser, direction_names)); + break; + + case CONTEXT_ROUND_GLYPH_POSITIONS: + pango2_context_set_round_glyph_positions (context, gtk_json_parser_get_boolean (parser)); + break; + + case CONTEXT_TRANSFORM: + { + Pango2Matrix m = PANGO2_MATRIX_INIT; + + gtk_json_parser_start_array (parser); + m.xx = gtk_json_parser_get_number (parser); + gtk_json_parser_next (parser); + m.xy = gtk_json_parser_get_number (parser); + gtk_json_parser_next (parser); + m.yx = gtk_json_parser_get_number (parser); + gtk_json_parser_next (parser); + m.yy = gtk_json_parser_get_number (parser); + gtk_json_parser_next (parser); + m.x0 = gtk_json_parser_get_number (parser); + gtk_json_parser_next (parser); + m.y0 = gtk_json_parser_get_number (parser); + gtk_json_parser_end (parser); + + pango2_context_set_matrix (context, &m); + } + break; + + default: + break; + } + } + while (gtk_json_parser_next (parser)); + + gtk_json_parser_end (parser); +} + +enum { + LAYOUT_CONTEXT, + LAYOUT_COMMENT, + LAYOUT_TEXT, + LAYOUT_ATTRIBUTES, + LAYOUT_FONT, + LAYOUT_TABS, + LAYOUT_AUTO_DIR, + LAYOUT_ALIGNMENT, + LAYOUT_WRAP, + LAYOUT_ELLIPSIZE, + LAYOUT_WIDTH, + LAYOUT_HEIGHT, + LAYOUT_INDENT, + LAYOUT_LINE_HEIGHT, + LAYOUT_LINES +}; + +static const char *layout_members[] = { + "context", + "comment", + "text", + "attributes", + "font", + "tabs", + "auto-dir", + "alignment", + "wrap", + "ellipsize", + "width", + "height", + "indent", + "line-height", + "lines", + NULL +}; + +static void +json_parser_fill_layout (GtkJsonParser *parser, + Pango2Layout *layout, + Pango2LayoutDeserializeFlags flags) +{ + gtk_json_parser_start_object (parser); + + do + { + char *str; + + switch (gtk_json_parser_select_member (parser, layout_members)) + { + case LAYOUT_CONTEXT: + if (flags & PANGO2_LAYOUT_DESERIALIZE_CONTEXT) + json_parser_fill_context (parser, pango2_layout_get_context (layout)); + break; + + case LAYOUT_COMMENT: + str = gtk_json_parser_get_string (parser); + g_object_set_data_full (G_OBJECT (layout), "comment", str, g_free); + break; + + case LAYOUT_TEXT: + str = gtk_json_parser_get_string (parser); + pango2_layout_set_text (layout, str, -1); + g_free (str); + break; + + case LAYOUT_ATTRIBUTES: + { + Pango2AttrList *attributes = pango2_attr_list_new (); + json_parser_fill_attr_list (parser, attributes); + pango2_layout_set_attributes (layout, attributes); + pango2_attr_list_unref (attributes); + } + break; + + case LAYOUT_FONT: + { + Pango2FontDescription *desc = parser_get_font_description (parser);; + pango2_layout_set_font_description (layout, desc); + pango2_font_description_free (desc); + } + break; + + case LAYOUT_AUTO_DIR: + pango2_layout_set_auto_dir (layout, gtk_json_parser_get_boolean (parser)); + break; + + case LAYOUT_LINE_HEIGHT: + pango2_layout_set_line_height (layout, gtk_json_parser_get_number (parser)); + break; + + case LAYOUT_TABS: + { + Pango2TabArray *tabs = pango2_tab_array_new (0, FALSE); + json_parser_fill_tab_array (parser, tabs); + pango2_layout_set_tabs (layout, tabs); + pango2_tab_array_free (tabs); + } + break; + + case LAYOUT_ALIGNMENT: + pango2_layout_set_alignment (layout, (Pango2Alignment) parser_select_string (parser, alignment_names)); + break; + + case LAYOUT_WRAP: + pango2_layout_set_wrap (layout, (Pango2WrapMode) parser_select_string (parser, wrap_names)); + break; + + case LAYOUT_ELLIPSIZE: + pango2_layout_set_ellipsize (layout, (Pango2EllipsizeMode) parser_select_string (parser, ellipsize_names)); + break; + + case LAYOUT_WIDTH: + pango2_layout_set_width (layout, (int) gtk_json_parser_get_number (parser)); + break; + + case LAYOUT_HEIGHT: + pango2_layout_set_height (layout, (int) gtk_json_parser_get_number (parser)); + break; + + case LAYOUT_INDENT: + pango2_layout_set_indent (layout, (int) gtk_json_parser_get_number (parser)); + break; + + case LAYOUT_LINES: + break; + + default: + break; + } + } + while (gtk_json_parser_next (parser)); + + gtk_json_parser_end (parser); +} + +enum { + FONT_DESCRIPTION, + FONT_CHECKSUM, + FONT_VARIATIONS, + FONT_FEATURES, + FONT_MATRIX +}; + +static const char *font_members[] = { + "description", + "checksum", + "variations", + "features", + "matrix", + NULL +}; + +static Pango2Font * +json_parser_load_font (GtkJsonParser *parser, + Pango2Context *context, + GError **error) +{ + Pango2Font *font = NULL; + + gtk_json_parser_start_object (parser); + + switch (gtk_json_parser_select_member (parser, font_members)) + { + case FONT_DESCRIPTION: + { + Pango2FontDescription *desc = parser_get_font_description (parser); + font = pango2_context_load_font (context, desc); + pango2_font_description_free (desc); + } + break; + + default: + break; + } + + gtk_json_parser_end (parser); + + return font; +} + +/* }}} */ +/* {{{ Public API */ + +/** + * pango2_layout_serialize: + * @layout: a `Pango2Layout` + * @flags: `Pango2LayoutSerializeFlags` + * + * Serializes the @layout for later deserialization via [func@Pango2.Layout.deserialize]. + * + * There are no guarantees about the format of the output across different + * versions of Pango2 and [func@Pango2.Layout.deserialize] will reject data + * that it cannot parse. + * + * The intended use of this function is testing, benchmarking and debugging. + * The format is not meant as a permanent storage format. + * + * Returns: a `GBytes` containing the serialized form of @layout + */ +GBytes * +pango2_layout_serialize (Pango2Layout *layout, + Pango2LayoutSerializeFlags flags) +{ + GString *str; + GtkJsonPrinter *printer; + char *data; + gsize size; + + g_return_val_if_fail (PANGO2_IS_LAYOUT (layout), NULL); + + str = g_string_new (""); + + printer = gtk_json_printer_new (gstring_write, str, NULL); + gtk_json_printer_set_flags (printer, GTK_JSON_PRINTER_PRETTY); + layout_to_json (printer, layout, flags); + gtk_json_printer_free (printer); + + g_string_append_c (str, '\n'); + + size = str->len; + data = g_string_free (str, FALSE); + + return g_bytes_new_take (data, size); +} + +/** + * pango2_layout_write_to_file: + * @layout: a `Pango2Layout` + * + * A convenience method to serialize a layout to a file. + * + * It is equivalent to calling [method@Pango2.Layout.serialize] + * followed by [func@GLib.file_set_contents]. + * + * See those two functions for details on the arguments. + * + * It is mostly intended for use inside a debugger to quickly dump + * a layout to a file for later inspection. + * + * Returns: %TRUE if saving was successful + */ +gboolean +pango2_layout_write_to_file (Pango2Layout *layout, + const char *filename) +{ + GBytes *bytes; + gboolean result; + + g_return_val_if_fail (PANGO2_IS_LAYOUT (layout), FALSE); + g_return_val_if_fail (filename != NULL, FALSE); + + bytes = pango2_layout_serialize (layout, PANGO2_LAYOUT_SERIALIZE_CONTEXT | + PANGO2_LAYOUT_SERIALIZE_OUTPUT); + + result = g_file_set_contents (filename, + g_bytes_get_data (bytes, NULL), + g_bytes_get_size (bytes), + NULL); + g_bytes_unref (bytes); + + return result; +} + +/** + * pango2_layout_deserialize: + * @context: a `Pango2Context` + * @flags: `Pango2LayoutDeserializeFlags` + * @bytes: the bytes containing the data + * @error: return location for an error + * + * Loads data previously created via [method@Pango2.Layout.serialize]. + * + * For a discussion of the supported format, see that function. + * + * Note: to verify that the returned layout is identical to + * the one that was serialized, you can compare @bytes to the + * result of serializing the layout again. + * + * Returns: (nullable) (transfer full): a new `Pango2Layout` + */ +Pango2Layout * +pango2_layout_deserialize (Pango2Context *context, + GBytes *bytes, + Pango2LayoutDeserializeFlags flags, + GError **error) +{ + Pango2Layout *layout; + GtkJsonParser *parser; + const GError *parser_error; + + g_return_val_if_fail (PANGO2_IS_CONTEXT (context), NULL); + + layout = pango2_layout_new (context); + + parser = gtk_json_parser_new_for_bytes (bytes); + json_parser_fill_layout (parser, layout, flags); + + parser_error = gtk_json_parser_get_error (parser); + + if (parser_error) + { + gsize start, end; + int code; + + gtk_json_parser_get_error_offset (parser, &start, &end); + + if (g_error_matches (parser_error, GTK_JSON_ERROR, GTK_JSON_ERROR_VALUE)) + code = PANGO2_LAYOUT_DESERIALIZE_INVALID_VALUE; + else if (g_error_matches (parser_error, GTK_JSON_ERROR, GTK_JSON_ERROR_SCHEMA)) + code = PANGO2_LAYOUT_DESERIALIZE_MISSING_VALUE; + else + code = PANGO2_LAYOUT_DESERIALIZE_INVALID; + + g_set_error (error, PANGO2_LAYOUT_DESERIALIZE_ERROR, code, + "%" G_GSIZE_FORMAT ":%" G_GSIZE_FORMAT ": %s", + start, end, parser_error->message); + + g_clear_object (&layout); + } + + gtk_json_parser_free (parser); + + return layout; +} + +/** + * pango2_font_serialize: + * @font: a `Pango2Font` + * + * Serializes the @font in a way that can be uniquely identified. + * + * There are no guarantees about the format of the output across different + * versions of Pango2. + * + * The intended use of this function is testing, benchmarking and debugging. + * The format is not meant as a permanent storage format. + * + * To recreate a font from its serialized form, use [func@Pango2.Font.deserialize]. + * + * Returns: a `GBytes` containing the serialized form of @font + */ +GBytes * +pango2_font_serialize (Pango2Font *font) +{ + GString *str; + GtkJsonPrinter *printer; + char *data; + gsize size; + + g_return_val_if_fail (PANGO2_IS_FONT (font), NULL); + + str = g_string_new (""); + + printer = gtk_json_printer_new (gstring_write, str, NULL); + gtk_json_printer_set_flags (printer, GTK_JSON_PRINTER_PRETTY); + add_font (printer, NULL, font); + gtk_json_printer_free (printer); + + size = str->len; + data = g_string_free (str, FALSE); + + return g_bytes_new_take (data, size); +} + +/** + * pango2_font_deserialize: + * @context: a `Pango2Context` + * @bytes: the bytes containing the data + * @error: return location for an error + * + * Loads data previously created via [method@Pango2.Font.serialize]. + * + * For a discussion of the supported format, see that function. + * + * Note: to verify that the returned font is identical to + * the one that was serialized, you can compare @bytes to the + * result of serializing the font again. + * + * Returns: (nullable) (transfer full): a new `Pango2Font` + */ +Pango2Font * +pango2_font_deserialize (Pango2Context *context, + GBytes *bytes, + GError **error) +{ + Pango2Font *font; + GtkJsonParser *parser; + + g_return_val_if_fail (PANGO2_IS_CONTEXT (context), NULL); + + parser = gtk_json_parser_new_for_bytes (bytes); + font = json_parser_load_font (parser, context, error); + gtk_json_parser_free (parser); + + return font; +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/pango2/shape.c b/pango2/shape.c new file mode 100644 index 00000000..62ea953a --- /dev/null +++ b/pango2/shape.c @@ -0,0 +1,868 @@ +/* Pango2 + * shape.c: Convert characters into glyphs. + * + * Copyright (C) 1999 Red Hat Software + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#include <string.h> +#include <math.h> +#include <glib.h> + +#include "pango-impl-utils.h" +#include "pango-glyph.h" + +#include "pango-attr-private.h" +#include "pango-item-private.h" +#include "pango-font-private.h" +#include "pango-userfont-private.h" +#include "pango-userface-private.h" + +#include <hb-ot.h> + +/* {{{ Harfbuzz shaping */ +/* {{{ Buffer handling */ + +static hb_buffer_t *cached_buffer = NULL; /* MT-safe */ +G_LOCK_DEFINE_STATIC (cached_buffer); + +static hb_buffer_t * +acquire_buffer (gboolean *free_buffer) +{ + hb_buffer_t *buffer; + + if (G_LIKELY (G_TRYLOCK (cached_buffer))) + { + if (G_UNLIKELY (!cached_buffer)) + cached_buffer = hb_buffer_create (); + + buffer = cached_buffer; + *free_buffer = FALSE; + } + else + { + buffer = hb_buffer_create (); + *free_buffer = TRUE; + } + + return buffer; +} + +static void +release_buffer (hb_buffer_t *buffer, + gboolean free_buffer) +{ + if (G_LIKELY (!free_buffer)) + { + hb_buffer_reset (buffer); + G_UNLOCK (cached_buffer); + } + else + hb_buffer_destroy (buffer); +} + +/* }}} */ +/* {{{ Use Pango2Font with Harfbuzz */ + +typedef struct +{ + Pango2Font *font; + hb_font_t *parent; + Pango2ShowFlags show_flags; +} Pango2HbShapeContext; + +static hb_bool_t +pango2_hb_font_get_nominal_glyph (hb_font_t *font, + void *font_data, + hb_codepoint_t unicode, + hb_codepoint_t *glyph, + void *user_data G_GNUC_UNUSED) +{ + Pango2HbShapeContext *context = (Pango2HbShapeContext *) font_data; + + if (context->show_flags != 0) + { + if ((context->show_flags & PANGO2_SHOW_SPACES) != 0 && + g_unichar_type (unicode) == G_UNICODE_SPACE_SEPARATOR) + { + /* Replace 0x20 by visible space, since we + * don't draw a hex box for 0x20 + */ + if (unicode == 0x20) + unicode = 0x2423; + else + { + *glyph = PANGO2_GET_UNKNOWN_GLYPH (unicode); + return TRUE; + } + } + + if ((context->show_flags & PANGO2_SHOW_IGNORABLES) != 0 && + pango2_is_default_ignorable (unicode)) + { + if (pango2_get_ignorable (unicode)) + *glyph = PANGO2_GET_UNKNOWN_GLYPH (unicode); + else + *glyph = PANGO2_GLYPH_EMPTY; + return TRUE; + } + + if ((context->show_flags & PANGO2_SHOW_LINE_BREAKS) != 0 && + unicode == 0x2028) + { + /* Always mark LS as unknown. If it ends up at the line end, + * the line breaking code takes care of hiding them, and if + * they end up in the middle of a line, we are in single + * paragraph mode and want to show the LS. + */ + *glyph = PANGO2_GET_UNKNOWN_GLYPH (unicode); + return TRUE; + } + } + + if (hb_font_get_nominal_glyph (context->parent, unicode, glyph)) + return TRUE; + + /* HarfBuzz knows how to synthesize other spaces from 0x20, so never + * replace them with unknown glyphs, just tell HarfBuzz that we don't + * have a glyph. + * + * For 0x20, on the other hand, we need to pretend that we have a glyph + * and rely on our glyph extents code to provide a reasonable width for + * PANGO2_GET_UNKNOWN_WIDTH (0x20). + */ + if (g_unichar_type (unicode) == G_UNICODE_SPACE_SEPARATOR) + { + if (unicode == 0x20) + { + *glyph = PANGO2_GET_UNKNOWN_GLYPH (0x20); + return TRUE; + } + + return FALSE; + } + + *glyph = PANGO2_GET_UNKNOWN_GLYPH (unicode); + + /* We draw our own invalid-Unicode shape, so prevent HarfBuzz + * from using REPLACEMENT CHARACTER. + */ + if (unicode > 0x10FFFF) + return TRUE; + + return FALSE; +} + +static hb_position_t +pango2_hb_font_get_glyph_h_advance (hb_font_t *font, + void *font_data, + hb_codepoint_t glyph, + void *user_data G_GNUC_UNUSED) +{ + Pango2HbShapeContext *context = (Pango2HbShapeContext *) font_data; + + if (glyph & PANGO2_GLYPH_UNKNOWN_FLAG) + { + Pango2Rectangle logical; + + pango2_font_get_glyph_extents (context->font, glyph, NULL, &logical); + return logical.width; + } + + return hb_font_get_glyph_h_advance (context->parent, glyph); +} + +static hb_position_t +pango2_hb_font_get_glyph_v_advance (hb_font_t *font, + void *font_data, + hb_codepoint_t glyph, + void *user_data G_GNUC_UNUSED) +{ + Pango2HbShapeContext *context = (Pango2HbShapeContext *) font_data; + + if (glyph & PANGO2_GLYPH_UNKNOWN_FLAG) + { + Pango2Rectangle logical; + + pango2_font_get_glyph_extents (context->font, glyph, NULL, &logical); + return logical.height; + } + + return hb_font_get_glyph_v_advance (context->parent, glyph); +} + +static hb_bool_t +pango2_hb_font_get_glyph_extents (hb_font_t *font, + void *font_data, + hb_codepoint_t glyph, + hb_glyph_extents_t *extents, + void *user_data G_GNUC_UNUSED) +{ + Pango2HbShapeContext *context = (Pango2HbShapeContext *) font_data; + + if (glyph & PANGO2_GLYPH_UNKNOWN_FLAG) + { + Pango2Rectangle ink; + + pango2_font_get_glyph_extents (context->font, glyph, &ink, NULL); + + extents->x_bearing = ink.x; + extents->y_bearing = ink.y; + extents->width = ink.width; + extents->height = ink.height; + + return TRUE; + } + + return hb_font_get_glyph_extents (context->parent, glyph, extents); +} + +static hb_font_t * +pango2_font_get_hb_font_for_context (Pango2Font *font, + Pango2HbShapeContext *context) +{ + hb_font_t *hb_font; + static hb_font_funcs_t *funcs; + + hb_font = pango2_font_get_hb_font (font); + + if (G_UNLIKELY (!funcs)) + { + funcs = hb_font_funcs_create (); + + hb_font_funcs_set_nominal_glyph_func (funcs, pango2_hb_font_get_nominal_glyph, NULL, NULL); + hb_font_funcs_set_glyph_h_advance_func (funcs, pango2_hb_font_get_glyph_h_advance, NULL, NULL); + hb_font_funcs_set_glyph_v_advance_func (funcs, pango2_hb_font_get_glyph_v_advance, NULL, NULL); + hb_font_funcs_set_glyph_extents_func (funcs, pango2_hb_font_get_glyph_extents, NULL, NULL); + + hb_font_funcs_make_immutable (funcs); + } + + context->font = font; + context->parent = hb_font; + + hb_font = hb_font_create_sub_font (hb_font); + hb_font_set_funcs (hb_font, funcs, context, NULL); + + return hb_font; +} + +/* }}} */ +/* {{{ Utilities */ + +static Pango2ShowFlags +find_show_flags (const Pango2Analysis *analysis) +{ + GSList *l; + Pango2ShowFlags flags = 0; + + for (l = analysis->extra_attrs; l; l = l->next) + { + Pango2Attribute *attr = l->data; + + if (attr->type == PANGO2_ATTR_SHOW) + flags |= attr->int_value; + } + + return flags; +} + +static Pango2TextTransform +find_text_transform (const Pango2Analysis *analysis) +{ + GSList *l; + Pango2TextTransform transform = PANGO2_TEXT_TRANSFORM_NONE; + + for (l = analysis->extra_attrs; l; l = l->next) + { + Pango2Attribute *attr = l->data; + + if (attr->type == PANGO2_ATTR_TEXT_TRANSFORM) + transform = (Pango2TextTransform) attr->int_value; + } + + return transform; +} + +static gboolean +glyph_has_color (hb_font_t *font, + hb_codepoint_t glyph) +{ + hb_face_t *face; + hb_blob_t *blob; + + face = hb_font_get_face (font); + + if (hb_ot_color_glyph_get_layers (face, glyph, 0, NULL, NULL) > 0) + return TRUE; + + if (hb_ot_color_has_png (face)) + { + blob = hb_ot_color_glyph_reference_png (font, glyph); + if (blob) + { + guint length = hb_blob_get_length (blob); + hb_blob_destroy (blob); + if (length > 0) + return TRUE; + } + } + + if (hb_ot_color_has_svg (face)) + { + blob = hb_ot_color_glyph_reference_svg (face, glyph); + if (blob) + { + guint length = hb_blob_get_length (blob); + hb_blob_destroy (blob); + if (length > 0) + return TRUE; + } + } + + return FALSE; +} + +/* }}} */ + +static void +pango2_hb_shape (const char *item_text, + int item_length, + const char *paragraph_text, + int paragraph_length, + const Pango2Analysis *analysis, + Pango2LogAttr *log_attrs, + int num_chars, + Pango2GlyphString *glyphs, + Pango2ShapeFlags flags) +{ + Pango2HbShapeContext context = { 0, }; + hb_buffer_flags_t hb_buffer_flags; + hb_font_t *hb_font; + hb_buffer_t *hb_buffer; + hb_direction_t hb_direction; + gboolean free_buffer; + hb_glyph_info_t *hb_glyph; + hb_glyph_position_t *hb_position; + int last_cluster; + guint i, num_glyphs; + unsigned int item_offset = item_text - paragraph_text; + hb_feature_t features[32]; + unsigned int num_features = 0; + Pango2GlyphInfo *infos; + Pango2TextTransform transform; + int hyphen_index; + + g_return_if_fail (analysis != NULL); + g_return_if_fail (analysis->font != NULL); + + context.show_flags = find_show_flags (analysis); + hb_font = pango2_font_get_hb_font_for_context (analysis->font, &context); + hb_buffer = acquire_buffer (&free_buffer); + + transform = find_text_transform (analysis); + + hb_direction = PANGO2_GRAVITY_IS_VERTICAL (analysis->gravity) ? HB_DIRECTION_TTB : HB_DIRECTION_LTR; + if (analysis->level % 2) + hb_direction = HB_DIRECTION_REVERSE (hb_direction); + if (PANGO2_GRAVITY_IS_IMPROPER (analysis->gravity)) + hb_direction = HB_DIRECTION_REVERSE (hb_direction); + + hb_buffer_flags = HB_BUFFER_FLAG_BOT | HB_BUFFER_FLAG_EOT; + + if (context.show_flags & PANGO2_SHOW_IGNORABLES) + hb_buffer_flags |= HB_BUFFER_FLAG_PRESERVE_DEFAULT_IGNORABLES; + + /* setup buffer */ + + hb_buffer_set_direction (hb_buffer, hb_direction); + hb_buffer_set_script (hb_buffer, (hb_script_t) g_unicode_script_to_iso15924 (analysis->script)); + hb_buffer_set_language (hb_buffer, hb_language_from_string (pango2_language_to_string (analysis->language), -1)); + hb_buffer_set_cluster_level (hb_buffer, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS); + hb_buffer_set_flags (hb_buffer, hb_buffer_flags); + hb_buffer_set_invisible_glyph (hb_buffer, PANGO2_GLYPH_EMPTY); + + if (analysis->flags & PANGO2_ANALYSIS_FLAG_NEED_HYPHEN) + { + const char *p = paragraph_text + item_offset + item_length; + int last_char_len = p - g_utf8_prev_char (p); + + hyphen_index = item_offset + item_length - last_char_len; + + if (log_attrs[num_chars].break_removes_preceding) + item_length -= last_char_len; + } + + /* Add pre-context */ + hb_buffer_add_utf8 (hb_buffer, paragraph_text, item_offset, item_offset, 0); + + if (transform == PANGO2_TEXT_TRANSFORM_NONE) + { + hb_buffer_add_utf8 (hb_buffer, paragraph_text, item_offset + item_length, item_offset, item_length); + } + else + { + const char *p; + int i; + + /* Transform the item text according to text transform. + * Note: we assume text transforms won't cross font boundaries + */ + for (p = paragraph_text + item_offset, i = 0; + p < paragraph_text + item_offset + item_length; + p = g_utf8_next_char (p), i++) + { + int index = p - paragraph_text; + gunichar ch = g_utf8_get_char (p); + char *str = NULL; + + switch (transform) + { + case PANGO2_TEXT_TRANSFORM_LOWERCASE: + if (g_unichar_isalnum (ch)) + str = g_utf8_strdown (p, g_utf8_next_char (p) - p); + break; + + case PANGO2_TEXT_TRANSFORM_UPPERCASE: + if (g_unichar_isalnum (ch)) + str = g_utf8_strup (p, g_utf8_next_char (p) - p); + break; + + case PANGO2_TEXT_TRANSFORM_CAPITALIZE: + if (log_attrs[i].is_word_start) + ch = g_unichar_totitle (ch); + break; + + case PANGO2_TEXT_TRANSFORM_NONE: + default: + g_assert_not_reached (); + } + + if (str) + { + for (const char *q = str; *q; q = g_utf8_next_char (q)) + { + ch = g_utf8_get_char (q); + hb_buffer_add (hb_buffer, ch, index); + } + g_free (str); + } + else + hb_buffer_add (hb_buffer, ch, index); + } + } + + /* Add post-context */ + hb_buffer_add_utf8 (hb_buffer, paragraph_text, paragraph_length, item_offset + item_length, 0); + + if (analysis->flags & PANGO2_ANALYSIS_FLAG_NEED_HYPHEN) + { + /* Insert either a Unicode or ASCII hyphen. We may + * want to look for script-specific hyphens here. + */ + hb_codepoint_t glyph; + + /* Note: We rely on hb_buffer_add clearing existing post-context */ + if (hb_font_get_nominal_glyph (hb_font, 0x2010, &glyph)) + hb_buffer_add (hb_buffer, 0x2010, hyphen_index); + else if (hb_font_get_nominal_glyph (hb_font, '-', &glyph)) + hb_buffer_add (hb_buffer, '-', hyphen_index); + } + + pango2_analysis_collect_features (analysis, features, G_N_ELEMENTS (features), &num_features); + + hb_shape (hb_font, hb_buffer, features, num_features); + + if (PANGO2_GRAVITY_IS_IMPROPER (analysis->gravity)) + hb_buffer_reverse (hb_buffer); + + /* buffer output */ + num_glyphs = hb_buffer_get_length (hb_buffer); + hb_glyph = hb_buffer_get_glyph_infos (hb_buffer, NULL); + pango2_glyph_string_set_size (glyphs, num_glyphs); + infos = glyphs->glyphs; + last_cluster = -1; + + for (i = 0; i < num_glyphs; i++) + { + infos[i].glyph = hb_glyph->codepoint; + glyphs->log_clusters[i] = hb_glyph->cluster - item_offset; + infos[i].attr.is_cluster_start = glyphs->log_clusters[i] != last_cluster; + infos[i].attr.is_color = glyph_has_color (hb_font, hb_glyph->codepoint); + hb_glyph++; + last_cluster = glyphs->log_clusters[i]; + } + + hb_position = hb_buffer_get_glyph_positions (hb_buffer, NULL); + if (PANGO2_GRAVITY_IS_VERTICAL (analysis->gravity)) + for (i = 0; i < num_glyphs; i++) + { + /* 90 degrees rotation counter-clockwise. */ + infos[i].geometry.width = - hb_position->y_advance; + infos[i].geometry.x_offset = - hb_position->y_offset; + infos[i].geometry.y_offset = - hb_position->x_offset; + hb_position++; + } + else /* horizontal */ + for (i = 0; i < num_glyphs; i++) + { + infos[i].geometry.width = hb_position->x_advance; + infos[i].geometry.x_offset = hb_position->x_offset; + infos[i].geometry.y_offset = - hb_position->y_offset; + hb_position++; + } + + release_buffer (hb_buffer, free_buffer); + hb_font_destroy (hb_font); +} + +/* }}} */ +/* {{{ User shaping */ + +static void +pango2_user_shape (const char *text, + unsigned int length, + const Pango2Analysis *analysis, + Pango2GlyphString *glyphs, + Pango2ShapeFlags flags) +{ + Pango2Font *font = analysis->font; + Pango2UserFace *face = PANGO2_USER_FACE (font->face); + + face->shape_func (face, font->size, + text, length, + analysis, + glyphs, flags, + face->user_data); +} + +/* }}} */ +/* {{{ Fallback shaping */ + +/* This is not meant to produce reasonable results */ + +static void +fallback_shape (const char *text, + unsigned int length, + const Pango2Analysis *analysis, + Pango2GlyphString *glyphs) +{ + int n_chars; + const char *p; + int cluster = 0; + int i; + + n_chars = text ? pango2_utf8_strlen (text, length) : 0; + + pango2_glyph_string_set_size (glyphs, n_chars); + + p = text; + for (i = 0; i < n_chars; i++) + { + gunichar wc; + Pango2Glyph glyph; + Pango2Rectangle logical_rect; + + wc = g_utf8_get_char (p); + + if (g_unichar_type (wc) != G_UNICODE_NON_SPACING_MARK) + cluster = p - text; + + if (pango2_is_zero_width (wc)) + glyph = PANGO2_GLYPH_EMPTY; + else + glyph = PANGO2_GET_UNKNOWN_GLYPH (wc); + + pango2_font_get_glyph_extents (analysis->font, glyph, NULL, &logical_rect); + + glyphs->glyphs[i].glyph = glyph; + + glyphs->glyphs[i].geometry.x_offset = 0; + glyphs->glyphs[i].geometry.y_offset = 0; + glyphs->glyphs[i].geometry.width = logical_rect.width; + + glyphs->log_clusters[i] = cluster; + + p = g_utf8_next_char (p); + } + + if (analysis->level & 1) + pango2_glyph_string_reverse_range (glyphs, 0, glyphs->num_glyphs); +} + +/* }}} */ +/* {{{ Shaping implementation */ + +static void +pango2_shape_internal (const char *item_text, + int item_length, + const char *paragraph_text, + int paragraph_length, + const Pango2Analysis *analysis, + Pango2LogAttr *log_attrs, + int num_chars, + Pango2GlyphString *glyphs, + Pango2ShapeFlags flags) +{ + int i; + int last_cluster; + + glyphs->num_glyphs = 0; + + if (item_length == -1) + item_length = strlen (item_text); + + if (!paragraph_text) + { + paragraph_text = item_text; + paragraph_length = item_length; + } + if (paragraph_length == -1) + paragraph_length = strlen (paragraph_text); + + g_return_if_fail (paragraph_text <= item_text); + g_return_if_fail (paragraph_text + paragraph_length >= item_text + item_length); + + if (PANGO2_IS_USER_FONT (analysis->font)) + pango2_user_shape (item_text, item_length, analysis, glyphs, flags); + else if (analysis->font) + pango2_hb_shape (item_text, item_length, + paragraph_text, paragraph_length, + analysis, + log_attrs, num_chars, + glyphs, flags); + else + glyphs->num_glyphs = 0; + + if (analysis->font && glyphs->num_glyphs == 0) + { + /* If a font has been correctly chosen, but no glyphs are output, + * there's probably something wrong with the font. + * + * Trying to be informative, we print out the font description, + * and the text, but to not flood the terminal with + * zillions of the message, we set a flag to only err once per + * font. + */ + GQuark warned_quark = g_quark_from_static_string ("pango-shape-fail-warned"); + + if (!g_object_get_qdata (G_OBJECT (analysis->font), warned_quark)) + { + Pango2FontDescription *desc; + char *font_name; + + desc = pango2_font_describe (analysis->font); + font_name = pango2_font_description_to_string (desc); + pango2_font_description_free (desc); + + g_warning ("shaping failure, expect ugly output. font='%s', text='%.*s'", + font_name, item_length, item_text); + + g_free (font_name); + + g_object_set_qdata (G_OBJECT (analysis->font), warned_quark, + GINT_TO_POINTER (1)); + } + } + + if (G_UNLIKELY (!glyphs->num_glyphs)) + { + fallback_shape (item_text, item_length, analysis, glyphs); + if (G_UNLIKELY (!glyphs->num_glyphs)) + return; + } + + /* make sure last_cluster is invalid */ + last_cluster = glyphs->log_clusters[0] - 1; + for (i = 0; i < glyphs->num_glyphs; i++) + { + /* Set glyphs[i].attr.is_cluster_start based on log_clusters[] */ + if (glyphs->log_clusters[i] != last_cluster) + { + glyphs->glyphs[i].attr.is_cluster_start = TRUE; + last_cluster = glyphs->log_clusters[i]; + } + else + glyphs->glyphs[i].attr.is_cluster_start = FALSE; + + + /* Shift glyph if width is negative, and negate width. + * This is useful for rotated font matrices and shouldn't + * harm in normal cases. + */ + if (glyphs->glyphs[i].geometry.width < 0) + { + glyphs->glyphs[i].geometry.width = -glyphs->glyphs[i].geometry.width; + glyphs->glyphs[i].geometry.x_offset += glyphs->glyphs[i].geometry.width; + } + } + + /* Make sure glyphstring direction conforms to analysis->level */ + if (G_UNLIKELY ((analysis->level & 1) && + glyphs->log_clusters[0] < glyphs->log_clusters[glyphs->num_glyphs - 1])) + { + g_warning ("Expected RTL run but got LTR. Fixing."); + + /* *Fix* it so we don't crash later */ + pango2_glyph_string_reverse_range (glyphs, 0, glyphs->num_glyphs); + } + + if (flags & PANGO2_SHAPE_ROUND_POSITIONS) + { + if (analysis->font && pango2_font_is_hinted (analysis->font)) + { + double x_scale_inv, y_scale_inv; + double x_scale, y_scale; + + pango2_font_get_scale_factors (analysis->font, &x_scale_inv, &y_scale_inv); + + if (PANGO2_GRAVITY_IS_IMPROPER (analysis->gravity)) + { + x_scale_inv = -x_scale_inv; + y_scale_inv = -y_scale_inv; + } + + x_scale = 1.0 / x_scale_inv; + y_scale = 1.0 / y_scale_inv; + + if (x_scale == 1.0 && y_scale == 1.0) + { + for (i = 0; i < glyphs->num_glyphs; i++) + glyphs->glyphs[i].geometry.width = PANGO2_UNITS_ROUND (glyphs->glyphs[i].geometry.width); + } + else + { + #if 0 + if (PANGO2_GRAVITY_IS_VERTICAL (analysis->gravity)) + { + /* XXX */ + double tmp = x_scale; + x_scale = y_scale; + y_scale = -tmp; + } + #endif + #define HINT(value, scale_inv, scale) (PANGO2_UNITS_ROUND ((int) ((value) * scale)) * scale_inv) + #define HINT_X(value) HINT ((value), x_scale, x_scale_inv) + #define HINT_Y(value) HINT ((value), y_scale, y_scale_inv) + for (i = 0; i < glyphs->num_glyphs; i++) + { + glyphs->glyphs[i].geometry.width = HINT_X (glyphs->glyphs[i].geometry.width); + glyphs->glyphs[i].geometry.x_offset = HINT_X (glyphs->glyphs[i].geometry.x_offset); + glyphs->glyphs[i].geometry.y_offset = HINT_Y (glyphs->glyphs[i].geometry.y_offset); + } + #undef HINT_Y + #undef HINT_X + #undef HINT + } + } + else + { + for (i = 0; i < glyphs->num_glyphs; i++) + { + glyphs->glyphs[i].geometry.width = + PANGO2_UNITS_ROUND (glyphs->glyphs[i].geometry.width); + glyphs->glyphs[i].geometry.x_offset = + PANGO2_UNITS_ROUND (glyphs->glyphs[i].geometry.x_offset); + glyphs->glyphs[i].geometry.y_offset = + PANGO2_UNITS_ROUND (glyphs->glyphs[i].geometry.y_offset); + } + } + } +} + +/* }}} */ +/* {{{ Public API */ + +/** + * pango2_shape: + * @item_text: valid UTF-8 text to shape + * @item_length: the length (in bytes) of @item_text. + * -1 means nul-terminated text. + * @paragraph_text: (nullable): text of the paragraph (see details). + * @paragraph_length: the length (in bytes) of @paragraph_text. + * -1 means nul-terminated text. + * @analysis: `Pango2Analysis` structure from [func@Pango2.itemize] + * @glyphs: glyph string in which to store results + * @flags: flags influencing the shaping process + * + * Convert the characters in @text into glyphs. + * + * Given a segment of text and the corresponding `Pango2Analysis` structure + * returned from [func@Pango2.itemize], convert the characters into glyphs. + * You may also pass in only a substring of the item from [func@Pango2.itemize]. + * + * Note that the extra attributes in the @analyis that is returned from + * [func@Pango2.itemize] have indices that are relative to the entire paragraph, + * so you do not pass the full paragraph text as @paragraph_text, you need + * to subtract the item offset from their indices first. + */ +void +pango2_shape (const char *item_text, + int item_length, + const char *paragraph_text, + int paragraph_length, + const Pango2Analysis *analysis, + Pango2GlyphString *glyphs, + Pango2ShapeFlags flags) +{ + pango2_shape_internal (item_text, item_length, + paragraph_text, paragraph_length, + analysis, NULL, 0, + glyphs, flags); +} + +/** + * pango2_shape_item: + * @item: `Pango2Item` to shape + * @paragraph_text: (nullable): text of the paragraph (see details). + * @paragraph_length: the length (in bytes) of @paragraph_text. + * -1 means nul-terminated text. + * @log_attrs: (nullable): array of `Pango2LogAttr` for @item + * @glyphs: glyph string in which to store results + * @flags: flags influencing the shaping process + * + * Convert the characters in @item into glyphs. + * + * This is similar to [func@Pango2.shape], except it takes a `Pango2Item` + * instead of separate @item_text and @analysis arguments. It also takes + * @log_attrs, which may be used in implementing text transforms. + * + * Note that the extra attributes in the @analyis that is returned from + * [func@Pango2.itemize] have indices that are relative to the entire paragraph, + * so you do not pass the full paragraph text as @paragraph_text, you need to + * subtract the item offset from their indices before calling [func@Pango2.shape]. + */ +void +pango2_shape_item (Pango2Item *item, + const char *paragraph_text, + int paragraph_length, + Pango2LogAttr *log_attrs, + Pango2GlyphString *glyphs, + Pango2ShapeFlags flags) +{ + pango2_shape_internal (paragraph_text + item->offset, item->length, + paragraph_text, paragraph_length, + &item->analysis, + log_attrs, item->num_chars, + glyphs, flags); +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ |