1 | /* Pango |
2 | * itemize.c: Turning text into items |
3 | * |
4 | * Copyright (C) 2000, 2006 Red Hat Software |
5 | * |
6 | * This library is free software; you can redistribute it and/or |
7 | * modify it under the terms of the GNU Library General Public |
8 | * License as published by the Free Software Foundation; either |
9 | * version 2 of the License, or (at your option) any later version. |
10 | * |
11 | * This library is distributed in the hope that it will be useful, |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
14 | * Library General Public License for more details. |
15 | * |
16 | * You should have received a copy of the GNU Library General Public |
17 | * License along with this library; if not, write to the |
18 | * Free Software Foundation, Inc., 59 Temple Place - Suite 330, |
19 | * Boston, MA 02111-1307, USA. |
20 | */ |
21 | |
22 | #include "config.h" |
23 | #include <string.h> |
24 | #include <stdlib.h> |
25 | |
26 | #include "pango-context-private.h" |
27 | #include "pango-impl-utils.h" |
28 | |
29 | #include "pango-font-private.h" |
30 | #include "pango-fontset.h" |
31 | #include "pango-fontmap-private.h" |
32 | #include "pango-script-private.h" |
33 | #include "pango-emoji-private.h" |
34 | #include "pango-attributes-private.h" |
35 | #include "pango-item-private.h" |
36 | |
37 | #include <hb-ot.h> |
38 | |
39 | /* {{{ Font cache */ |
40 | |
41 | /* |
42 | * We cache the results of character,fontset => font in a hash table |
43 | */ |
44 | |
45 | typedef struct { |
46 | GHashTable *hash; |
47 | } FontCache; |
48 | |
49 | typedef struct { |
50 | PangoFont *font; |
51 | int position; /* position of the font in the fontset */ |
52 | } FontElement; |
53 | |
54 | static void |
55 | font_cache_destroy (FontCache *cache) |
56 | { |
57 | g_hash_table_destroy (hash_table: cache->hash); |
58 | g_slice_free (FontCache, cache); |
59 | } |
60 | |
61 | static void |
62 | font_element_destroy (FontElement *element) |
63 | { |
64 | if (element->font) |
65 | g_object_unref (object: element->font); |
66 | g_slice_free (FontElement, element); |
67 | } |
68 | |
69 | static FontCache * |
70 | get_font_cache (PangoFontset *fontset) |
71 | { |
72 | FontCache *cache; |
73 | |
74 | static GQuark cache_quark = 0; /* MT-safe */ |
75 | if (G_UNLIKELY (!cache_quark)) |
76 | cache_quark = g_quark_from_static_string (string: "pango-font-cache" ); |
77 | |
78 | retry: |
79 | cache = g_object_get_qdata (G_OBJECT (fontset), quark: cache_quark); |
80 | if (G_UNLIKELY (!cache)) |
81 | { |
82 | cache = g_slice_new (FontCache); |
83 | cache->hash = g_hash_table_new_full (hash_func: g_direct_hash, NULL, |
84 | NULL, value_destroy_func: (GDestroyNotify)font_element_destroy); |
85 | if (!g_object_replace_qdata (G_OBJECT (fontset), quark: cache_quark, NULL, |
86 | newval: cache, destroy: (GDestroyNotify)font_cache_destroy, |
87 | NULL)) |
88 | { |
89 | font_cache_destroy (cache); |
90 | goto retry; |
91 | } |
92 | } |
93 | |
94 | return cache; |
95 | } |
96 | |
97 | static gboolean |
98 | font_cache_get (FontCache *cache, |
99 | gunichar wc, |
100 | PangoFont **font, |
101 | int *position) |
102 | { |
103 | FontElement *element; |
104 | |
105 | element = g_hash_table_lookup (hash_table: cache->hash, GUINT_TO_POINTER (wc)); |
106 | if (element) |
107 | { |
108 | *font = element->font; |
109 | *position = element->position; |
110 | return TRUE; |
111 | } |
112 | else |
113 | return FALSE; |
114 | } |
115 | |
116 | static void |
117 | font_cache_insert (FontCache *cache, |
118 | gunichar wc, |
119 | PangoFont *font, |
120 | int position) |
121 | { |
122 | FontElement *element = g_slice_new (FontElement); |
123 | element->font = font ? g_object_ref (font) : NULL; |
124 | element->position = position; |
125 | |
126 | g_hash_table_insert (hash_table: cache->hash, GUINT_TO_POINTER (wc), value: element); |
127 | } |
128 | |
129 | /* }}} */ |
130 | /* {{{ Width Iter */ |
131 | |
132 | typedef struct _PangoWidthIter PangoWidthIter; |
133 | |
134 | struct _PangoWidthIter |
135 | { |
136 | const gchar *text_start; |
137 | const gchar *text_end; |
138 | const gchar *start; |
139 | const gchar *end; |
140 | gboolean upright; |
141 | }; |
142 | |
143 | static gboolean |
144 | width_iter_is_upright (gunichar ch) |
145 | { |
146 | /* https://www.unicode.org/Public/11.0.0/ucd/VerticalOrientation.txt |
147 | * VO=U or Tu table generated by tools/gen-vertical-orientation-U-table.py. |
148 | * |
149 | * FIXME: In the future, If GLib supports VerticalOrientation, please use it. |
150 | */ |
151 | static const gunichar upright[][2] = { |
152 | {0x00A7, 0x00A7}, {0x00A9, 0x00A9}, {0x00AE, 0x00AE}, {0x00B1, 0x00B1}, |
153 | {0x00BC, 0x00BE}, {0x00D7, 0x00D7}, {0x00F7, 0x00F7}, {0x02EA, 0x02EB}, |
154 | {0x1100, 0x11FF}, {0x1401, 0x167F}, {0x18B0, 0x18FF}, {0x2016, 0x2016}, |
155 | {0x2020, 0x2021}, {0x2030, 0x2031}, {0x203B, 0x203C}, {0x2042, 0x2042}, |
156 | {0x2047, 0x2049}, {0x2051, 0x2051}, {0x2065, 0x2065}, {0x20DD, 0x20E0}, |
157 | {0x20E2, 0x20E4}, {0x2100, 0x2101}, {0x2103, 0x2109}, {0x210F, 0x210F}, |
158 | {0x2113, 0x2114}, {0x2116, 0x2117}, {0x211E, 0x2123}, {0x2125, 0x2125}, |
159 | {0x2127, 0x2127}, {0x2129, 0x2129}, {0x212E, 0x212E}, {0x2135, 0x213F}, |
160 | {0x2145, 0x214A}, {0x214C, 0x214D}, {0x214F, 0x2189}, {0x218C, 0x218F}, |
161 | {0x221E, 0x221E}, {0x2234, 0x2235}, {0x2300, 0x2307}, {0x230C, 0x231F}, |
162 | {0x2324, 0x2328}, {0x232B, 0x232B}, {0x237D, 0x239A}, {0x23BE, 0x23CD}, |
163 | {0x23CF, 0x23CF}, {0x23D1, 0x23DB}, {0x23E2, 0x2422}, {0x2424, 0x24FF}, |
164 | {0x25A0, 0x2619}, {0x2620, 0x2767}, {0x2776, 0x2793}, {0x2B12, 0x2B2F}, |
165 | {0x2B50, 0x2B59}, {0x2BB8, 0x2BD1}, {0x2BD3, 0x2BEB}, {0x2BF0, 0x2BFF}, |
166 | {0x2E80, 0x3007}, {0x3012, 0x3013}, {0x3020, 0x302F}, {0x3031, 0x309F}, |
167 | {0x30A1, 0x30FB}, {0x30FD, 0xA4CF}, {0xA960, 0xA97F}, {0xAC00, 0xD7FF}, |
168 | {0xE000, 0xFAFF}, {0xFE10, 0xFE1F}, {0xFE30, 0xFE48}, {0xFE50, 0xFE57}, |
169 | {0xFE5F, 0xFE62}, {0xFE67, 0xFE6F}, {0xFF01, 0xFF07}, {0xFF0A, 0xFF0C}, |
170 | {0xFF0E, 0xFF19}, {0xFF1F, 0xFF3A}, {0xFF3C, 0xFF3C}, {0xFF3E, 0xFF3E}, |
171 | {0xFF40, 0xFF5A}, {0xFFE0, 0xFFE2}, {0xFFE4, 0xFFE7}, {0xFFF0, 0xFFF8}, |
172 | {0xFFFC, 0xFFFD}, {0x10980, 0x1099F}, {0x11580, 0x115FF}, {0x11A00, 0x11AAF}, |
173 | {0x13000, 0x1342F}, {0x14400, 0x1467F}, {0x16FE0, 0x18AFF}, {0x1B000, 0x1B12F}, |
174 | {0x1B170, 0x1B2FF}, {0x1D000, 0x1D1FF}, {0x1D2E0, 0x1D37F}, {0x1D800, 0x1DAAF}, |
175 | {0x1F000, 0x1F7FF}, {0x1F900, 0x1FA6F}, {0x20000, 0x2FFFD}, {0x30000, 0x3FFFD}, |
176 | {0xF0000, 0xFFFFD}, {0x100000, 0x10FFFD} |
177 | }; |
178 | static const int max = sizeof(upright) / sizeof(upright[0]); |
179 | int st = 0; |
180 | int ed = max; |
181 | |
182 | if (ch < upright[0][0]) |
183 | return FALSE; |
184 | |
185 | while (st <= ed) |
186 | { |
187 | int mid = (st + ed) / 2; |
188 | if (upright[mid][0] <= ch && ch <= upright[mid][1]) |
189 | return TRUE; |
190 | else |
191 | if (upright[mid][0] <= ch) |
192 | st = mid + 1; |
193 | else |
194 | ed = mid - 1; |
195 | } |
196 | |
197 | return FALSE; |
198 | } |
199 | |
200 | static void |
201 | width_iter_next (PangoWidthIter *iter) |
202 | { |
203 | gboolean met_joiner = FALSE; |
204 | iter->start = iter->end; |
205 | |
206 | if (iter->end < iter->text_end) |
207 | { |
208 | gunichar ch = g_utf8_get_char (p: iter->end); |
209 | iter->upright = width_iter_is_upright (ch); |
210 | } |
211 | |
212 | while (iter->end < iter->text_end) |
213 | { |
214 | gunichar ch = g_utf8_get_char (p: iter->end); |
215 | |
216 | /* for zero width joiner */ |
217 | if (ch == 0x200D) |
218 | { |
219 | iter->end = g_utf8_next_char (iter->end); |
220 | met_joiner = TRUE; |
221 | continue; |
222 | } |
223 | |
224 | /* ignore the upright check if met joiner */ |
225 | if (met_joiner) |
226 | { |
227 | iter->end = g_utf8_next_char (iter->end); |
228 | met_joiner = FALSE; |
229 | continue; |
230 | } |
231 | |
232 | /* for variation selector, tag and emoji modifier. */ |
233 | if (G_UNLIKELY (ch == 0xFE0EU || ch == 0xFE0FU || |
234 | (ch >= 0xE0020 && ch <= 0xE007F) || |
235 | (ch >= 0x1F3FB && ch <= 0x1F3FF))) |
236 | { |
237 | iter->end = g_utf8_next_char (iter->end); |
238 | continue; |
239 | } |
240 | |
241 | if (width_iter_is_upright (ch) != iter->upright) |
242 | break; |
243 | |
244 | iter->end = g_utf8_next_char (iter->end); |
245 | } |
246 | } |
247 | |
248 | static void |
249 | width_iter_init (PangoWidthIter *iter, |
250 | const char *text, |
251 | int length) |
252 | { |
253 | iter->text_start = text; |
254 | iter->text_end = text + length; |
255 | iter->start = iter->end = text; |
256 | |
257 | width_iter_next (iter); |
258 | } |
259 | |
260 | static void |
261 | width_iter_fini (PangoWidthIter *iter) |
262 | { |
263 | } |
264 | |
265 | /* }}} */ |
266 | /* {{{ Itemization */ |
267 | |
268 | typedef struct _ItemizeState ItemizeState; |
269 | |
270 | |
271 | typedef enum { |
272 | EMBEDDING_CHANGED = 1 << 0, |
273 | SCRIPT_CHANGED = 1 << 1, |
274 | LANG_CHANGED = 1 << 2, |
275 | FONT_CHANGED = 1 << 3, |
276 | DERIVED_LANG_CHANGED = 1 << 4, |
277 | WIDTH_CHANGED = 1 << 5, |
278 | EMOJI_CHANGED = 1 << 6, |
279 | } ChangedFlags; |
280 | |
281 | |
282 | struct _ItemizeState |
283 | { |
284 | PangoContext *context; |
285 | const char *text; |
286 | const char *end; |
287 | |
288 | const char *run_start; |
289 | const char *run_end; |
290 | |
291 | GList *result; |
292 | PangoItem *item; |
293 | |
294 | guint8 *embedding_levels; |
295 | int embedding_end_offset; |
296 | const char *embedding_end; |
297 | guint8 embedding; |
298 | |
299 | PangoGravity gravity; |
300 | PangoGravityHint gravity_hint; |
301 | PangoGravity resolved_gravity; |
302 | PangoGravity font_desc_gravity; |
303 | gboolean centered_baseline; |
304 | |
305 | PangoAttrIterator *attr_iter; |
306 | gboolean free_attr_iter; |
307 | const char *attr_end; |
308 | PangoFontDescription *font_desc; |
309 | PangoFontDescription *emoji_font_desc; |
310 | PangoLanguage *lang; |
311 | GSList *; |
312 | gboolean ; |
313 | |
314 | ChangedFlags changed; |
315 | |
316 | PangoScriptIter script_iter; |
317 | const char *script_end; |
318 | PangoScript script; |
319 | |
320 | PangoWidthIter width_iter; |
321 | PangoEmojiIter emoji_iter; |
322 | |
323 | PangoLanguage *derived_lang; |
324 | |
325 | PangoFontset *current_fonts; |
326 | FontCache *cache; |
327 | PangoFont *base_font; |
328 | gboolean enable_fallback; |
329 | |
330 | const char *first_space; /* first of a sequence of spaces we've seen */ |
331 | int font_position; /* position of the current font in the fontset */ |
332 | }; |
333 | |
334 | static void |
335 | update_embedding_end (ItemizeState *state) |
336 | { |
337 | state->embedding = state->embedding_levels[state->embedding_end_offset]; |
338 | while (state->embedding_end < state->end && |
339 | state->embedding_levels[state->embedding_end_offset] == state->embedding) |
340 | { |
341 | state->embedding_end_offset++; |
342 | state->embedding_end = g_utf8_next_char (state->embedding_end); |
343 | } |
344 | |
345 | state->changed |= EMBEDDING_CHANGED; |
346 | } |
347 | |
348 | static PangoAttribute * |
349 | find_attribute (GSList *attr_list, |
350 | PangoAttrType type) |
351 | { |
352 | GSList *node; |
353 | |
354 | for (node = attr_list; node; node = node->next) |
355 | if (((PangoAttribute *) node->data)->klass->type == type) |
356 | return (PangoAttribute *) node->data; |
357 | |
358 | return NULL; |
359 | } |
360 | |
361 | static void |
362 | update_attr_iterator (ItemizeState *state) |
363 | { |
364 | PangoLanguage *old_lang; |
365 | PangoAttribute *attr; |
366 | int end_index; |
367 | |
368 | pango_attr_iterator_range (iterator: state->attr_iter, NULL, end: &end_index); |
369 | if (end_index < state->end - state->text) |
370 | state->attr_end = state->text + end_index; |
371 | else |
372 | state->attr_end = state->end; |
373 | |
374 | if (state->emoji_font_desc) |
375 | { |
376 | pango_font_description_free (desc: state->emoji_font_desc); |
377 | state->emoji_font_desc = NULL; |
378 | } |
379 | |
380 | old_lang = state->lang; |
381 | if (state->font_desc) |
382 | pango_font_description_free (desc: state->font_desc); |
383 | state->font_desc = pango_font_description_copy_static (desc: state->context->font_desc); |
384 | pango_attr_iterator_get_font (iterator: state->attr_iter, desc: state->font_desc, |
385 | language: &state->lang, extra_attrs: &state->extra_attrs); |
386 | if (pango_font_description_get_set_fields (desc: state->font_desc) & PANGO_FONT_MASK_GRAVITY) |
387 | state->font_desc_gravity = pango_font_description_get_gravity (desc: state->font_desc); |
388 | else |
389 | state->font_desc_gravity = PANGO_GRAVITY_AUTO; |
390 | |
391 | state->copy_extra_attrs = FALSE; |
392 | |
393 | if (!state->lang) |
394 | state->lang = state->context->language; |
395 | |
396 | attr = find_attribute (attr_list: state->extra_attrs, type: PANGO_ATTR_FALLBACK); |
397 | state->enable_fallback = (attr == NULL || ((PangoAttrInt *)attr)->value); |
398 | |
399 | attr = find_attribute (attr_list: state->extra_attrs, type: PANGO_ATTR_GRAVITY); |
400 | state->gravity = attr == NULL ? PANGO_GRAVITY_AUTO : ((PangoAttrInt *)attr)->value; |
401 | |
402 | attr = find_attribute (attr_list: state->extra_attrs, type: PANGO_ATTR_GRAVITY_HINT); |
403 | state->gravity_hint = attr == NULL ? state->context->gravity_hint : (PangoGravityHint)((PangoAttrInt *)attr)->value; |
404 | |
405 | state->changed |= FONT_CHANGED; |
406 | if (state->lang != old_lang) |
407 | state->changed |= LANG_CHANGED; |
408 | } |
409 | |
410 | static void |
411 | update_end (ItemizeState *state) |
412 | { |
413 | state->run_end = state->embedding_end; |
414 | if (state->attr_end < state->run_end) |
415 | state->run_end = state->attr_end; |
416 | if (state->script_end < state->run_end) |
417 | state->run_end = state->script_end; |
418 | if (state->width_iter.end < state->run_end) |
419 | state->run_end = state->width_iter.end; |
420 | if (state->emoji_iter.end < state->run_end) |
421 | state->run_end = state->emoji_iter.end; |
422 | } |
423 | |
424 | |
425 | static void |
426 | itemize_state_init (ItemizeState *state, |
427 | PangoContext *context, |
428 | const char *text, |
429 | PangoDirection base_dir, |
430 | int start_index, |
431 | int length, |
432 | PangoAttrList *attrs, |
433 | PangoAttrIterator *cached_iter, |
434 | const PangoFontDescription *desc) |
435 | { |
436 | state->context = context; |
437 | state->text = text; |
438 | state->end = text + start_index + length; |
439 | |
440 | state->result = NULL; |
441 | state->item = NULL; |
442 | |
443 | state->run_start = text + start_index; |
444 | state->changed = EMBEDDING_CHANGED | SCRIPT_CHANGED | LANG_CHANGED | |
445 | FONT_CHANGED | WIDTH_CHANGED | EMOJI_CHANGED; |
446 | |
447 | /* First, apply the bidirectional algorithm to break |
448 | * the text into directional runs. |
449 | */ |
450 | state->embedding_levels = pango_log2vis_get_embedding_levels (text: text + start_index, length, pbase_dir: &base_dir); |
451 | |
452 | state->embedding_end_offset = 0; |
453 | state->embedding_end = text + start_index; |
454 | update_embedding_end (state); |
455 | |
456 | state->gravity = PANGO_GRAVITY_AUTO; |
457 | state->centered_baseline = PANGO_GRAVITY_IS_VERTICAL (state->context->resolved_gravity); |
458 | state->gravity_hint = state->context->gravity_hint; |
459 | state->resolved_gravity = PANGO_GRAVITY_AUTO; |
460 | |
461 | /* Initialize the attribute iterator |
462 | */ |
463 | if (cached_iter) |
464 | { |
465 | state->attr_iter = cached_iter; |
466 | state->free_attr_iter = FALSE; |
467 | } |
468 | else if (attrs) |
469 | { |
470 | state->attr_iter = pango_attr_list_get_iterator (list: attrs); |
471 | state->free_attr_iter = TRUE; |
472 | } |
473 | else |
474 | { |
475 | state->attr_iter = NULL; |
476 | state->free_attr_iter = FALSE; |
477 | } |
478 | |
479 | state->emoji_font_desc = NULL; |
480 | if (state->attr_iter) |
481 | { |
482 | state->font_desc = NULL; |
483 | state->lang = NULL; |
484 | |
485 | pango_attr_iterator_advance (iterator: state->attr_iter, index: start_index); |
486 | update_attr_iterator (state); |
487 | } |
488 | else |
489 | { |
490 | state->font_desc = pango_font_description_copy_static (desc: desc ? desc : state->context->font_desc); |
491 | state->lang = state->context->language; |
492 | state->extra_attrs = NULL; |
493 | state->copy_extra_attrs = FALSE; |
494 | |
495 | state->attr_end = state->end; |
496 | state->enable_fallback = TRUE; |
497 | } |
498 | |
499 | /* Initialize the script iterator |
500 | */ |
501 | _pango_script_iter_init (iter: &state->script_iter, text: text + start_index, length); |
502 | pango_script_iter_get_range (iter: &state->script_iter, NULL, |
503 | end: &state->script_end, script: &state->script); |
504 | |
505 | width_iter_init (iter: &state->width_iter, text: text + start_index, length); |
506 | _pango_emoji_iter_init (iter: &state->emoji_iter, text: text + start_index, length); |
507 | |
508 | if (!PANGO_GRAVITY_IS_VERTICAL (state->context->resolved_gravity)) |
509 | state->width_iter.end = state->end; |
510 | else if (state->emoji_iter.is_emoji) |
511 | state->width_iter.end = MAX (state->width_iter.end, state->emoji_iter.end); |
512 | |
513 | update_end (state); |
514 | |
515 | if (pango_font_description_get_set_fields (desc: state->font_desc) & PANGO_FONT_MASK_GRAVITY) |
516 | state->font_desc_gravity = pango_font_description_get_gravity (desc: state->font_desc); |
517 | else |
518 | state->font_desc_gravity = PANGO_GRAVITY_AUTO; |
519 | |
520 | state->derived_lang = NULL; |
521 | state->current_fonts = NULL; |
522 | state->cache = NULL; |
523 | state->base_font = NULL; |
524 | state->first_space = NULL; |
525 | state->font_position = 0xffff; |
526 | } |
527 | |
528 | static gboolean |
529 | itemize_state_next (ItemizeState *state) |
530 | { |
531 | if (state->run_end == state->end) |
532 | return FALSE; |
533 | |
534 | state->changed = 0; |
535 | |
536 | state->run_start = state->run_end; |
537 | |
538 | if (state->run_end == state->embedding_end) |
539 | { |
540 | update_embedding_end (state); |
541 | } |
542 | |
543 | if (state->run_end == state->attr_end) |
544 | { |
545 | pango_attr_iterator_next (iterator: state->attr_iter); |
546 | update_attr_iterator (state); |
547 | } |
548 | |
549 | if (state->run_end == state->script_end) |
550 | { |
551 | pango_script_iter_next (iter: &state->script_iter); |
552 | pango_script_iter_get_range (iter: &state->script_iter, NULL, |
553 | end: &state->script_end, script: &state->script); |
554 | state->changed |= SCRIPT_CHANGED; |
555 | } |
556 | if (state->run_end == state->emoji_iter.end) |
557 | { |
558 | _pango_emoji_iter_next (iter: &state->emoji_iter); |
559 | state->changed |= EMOJI_CHANGED; |
560 | |
561 | if (state->emoji_iter.is_emoji) |
562 | state->width_iter.end = MAX (state->width_iter.end, state->emoji_iter.end); |
563 | } |
564 | if (state->run_end == state->width_iter.end) |
565 | { |
566 | width_iter_next (iter: &state->width_iter); |
567 | state->changed |= WIDTH_CHANGED; |
568 | } |
569 | |
570 | update_end (state); |
571 | |
572 | return TRUE; |
573 | } |
574 | |
575 | static GSList * |
576 | copy_attr_slist (GSList *attr_slist) |
577 | { |
578 | GSList *new_list = NULL; |
579 | GSList *l; |
580 | |
581 | for (l = attr_slist; l; l = l->next) |
582 | new_list = g_slist_prepend (list: new_list, data: pango_attribute_copy (attr: l->data)); |
583 | |
584 | return g_slist_reverse (list: new_list); |
585 | } |
586 | |
587 | static void |
588 | itemize_state_fill_font (ItemizeState *state, |
589 | PangoFont *font) |
590 | { |
591 | GList *l; |
592 | |
593 | for (l = state->result; l; l = l->next) |
594 | { |
595 | PangoItem *item = l->data; |
596 | if (item->analysis.font) |
597 | break; |
598 | if (font) |
599 | item->analysis.font = g_object_ref (font); |
600 | } |
601 | } |
602 | |
603 | static void |
604 | itemize_state_add_character (ItemizeState *state, |
605 | PangoFont *font, |
606 | int font_position, |
607 | gboolean force_break, |
608 | const char *pos, |
609 | gboolean is_space) |
610 | { |
611 | const char *first_space = state->first_space; |
612 | int n_spaces = 0; |
613 | |
614 | if (is_space) |
615 | { |
616 | if (state->first_space == NULL) |
617 | state->first_space = pos; |
618 | } |
619 | else |
620 | state->first_space = NULL; |
621 | |
622 | if (state->item) |
623 | { |
624 | if (!state->item->analysis.font && font) |
625 | { |
626 | itemize_state_fill_font (state, font); |
627 | state->font_position = font_position; |
628 | } |
629 | else if (state->item->analysis.font && !font) |
630 | { |
631 | font = state->item->analysis.font; |
632 | font_position = state->font_position; |
633 | } |
634 | |
635 | if (!force_break && |
636 | state->item->analysis.font == font) |
637 | { |
638 | state->item->num_chars++; |
639 | return; |
640 | } |
641 | |
642 | /* Font is changing, we are about to end the current item. |
643 | * If it ended in a sequence of spaces (but wasn't only spaces), |
644 | * check if we should move those spaces to the new item (since |
645 | * the font is less "fallback". |
646 | * |
647 | * See https://gitlab.gnome.org/GNOME/pango/-/issues/249 |
648 | */ |
649 | if (state->text + state->item->offset < first_space && |
650 | font_position < state->font_position) |
651 | { |
652 | n_spaces = g_utf8_strlen (p: first_space, max: pos - first_space); |
653 | state->item->num_chars -= n_spaces; |
654 | pos = first_space; |
655 | } |
656 | |
657 | state->item->length = (pos - state->text) - state->item->offset; |
658 | } |
659 | |
660 | state->item = pango_item_new (); |
661 | state->item->offset = pos - state->text; |
662 | state->item->length = 0; |
663 | state->item->num_chars = n_spaces + 1; |
664 | |
665 | if (font) |
666 | g_object_ref (font); |
667 | state->item->analysis.font = font; |
668 | state->font_position = font_position; |
669 | |
670 | state->item->analysis.level = state->embedding; |
671 | state->item->analysis.gravity = state->resolved_gravity; |
672 | |
673 | /* The level vs. gravity dance: |
674 | * - If gravity is SOUTH, leave level untouched. |
675 | * - If gravity is NORTH, step level one up, to |
676 | * not get mirrored upside-down text. |
677 | * - If gravity is EAST, step up to an even level, as |
678 | * it's a clockwise-rotated layout, so the rotated |
679 | * top is unrotated left. |
680 | * - If gravity is WEST, step up to an odd level, as |
681 | * it's a counter-clockwise-rotated layout, so the rotated |
682 | * top is unrotated right. |
683 | * |
684 | * A similar dance is performed in pango-layout.c: |
685 | * line_set_resolved_dir(). Keep in synch. |
686 | */ |
687 | switch (state->item->analysis.gravity) |
688 | { |
689 | case PANGO_GRAVITY_SOUTH: |
690 | default: |
691 | break; |
692 | case PANGO_GRAVITY_NORTH: |
693 | state->item->analysis.level++; |
694 | break; |
695 | case PANGO_GRAVITY_EAST: |
696 | state->item->analysis.level += 1; |
697 | state->item->analysis.level &= ~1; |
698 | break; |
699 | case PANGO_GRAVITY_WEST: |
700 | state->item->analysis.level |= 1; |
701 | break; |
702 | } |
703 | |
704 | state->item->analysis.flags |= state->centered_baseline ? PANGO_ANALYSIS_FLAG_CENTERED_BASELINE : 0; |
705 | |
706 | state->item->analysis.script = state->script; |
707 | state->item->analysis.language = state->derived_lang; |
708 | |
709 | if (state->copy_extra_attrs) |
710 | { |
711 | state->item->analysis.extra_attrs = copy_attr_slist (attr_slist: state->extra_attrs); |
712 | } |
713 | else |
714 | { |
715 | state->item->analysis.extra_attrs = state->extra_attrs; |
716 | state->copy_extra_attrs = TRUE; |
717 | } |
718 | |
719 | state->result = g_list_prepend (list: state->result, data: state->item); |
720 | } |
721 | |
722 | typedef struct { |
723 | PangoLanguage *lang; |
724 | gunichar wc; |
725 | PangoFont *font; |
726 | int position; |
727 | } GetFontInfo; |
728 | |
729 | static gboolean |
730 | get_font_foreach (PangoFontset *fontset, |
731 | PangoFont *font, |
732 | gpointer data) |
733 | { |
734 | GetFontInfo *info = data; |
735 | |
736 | if (G_UNLIKELY (!font)) |
737 | return FALSE; |
738 | |
739 | if (pango_font_has_char (font, wc: info->wc)) |
740 | { |
741 | info->font = font; |
742 | return TRUE; |
743 | } |
744 | |
745 | if (!fontset) |
746 | { |
747 | info->font = font; |
748 | return TRUE; |
749 | } |
750 | |
751 | info->position++; |
752 | |
753 | return FALSE; |
754 | } |
755 | |
756 | static PangoFont * |
757 | get_base_font (ItemizeState *state) |
758 | { |
759 | if (!state->base_font) |
760 | state->base_font = pango_font_map_load_font (fontmap: state->context->font_map, |
761 | context: state->context, |
762 | desc: state->font_desc); |
763 | return state->base_font; |
764 | } |
765 | |
766 | static gboolean |
767 | get_font (ItemizeState *state, |
768 | gunichar wc, |
769 | PangoFont **font, |
770 | int *position) |
771 | { |
772 | GetFontInfo info; |
773 | |
774 | /* We'd need a separate cache when fallback is disabled, but since lookup |
775 | * with fallback disabled is faster anyways, we just skip caching |
776 | */ |
777 | if (state->enable_fallback && font_cache_get (cache: state->cache, wc, font, position)) |
778 | return TRUE; |
779 | |
780 | info.lang = state->derived_lang; |
781 | info.wc = wc; |
782 | info.font = NULL; |
783 | info.position = 0; |
784 | |
785 | if (state->enable_fallback) |
786 | pango_fontset_foreach (fontset: state->current_fonts, func: get_font_foreach, data: &info); |
787 | |
788 | if (!info.font) |
789 | info.font = get_base_font (state); |
790 | |
791 | *font = info.font; |
792 | *position = info.position; |
793 | |
794 | /* skip caching if fallback disabled (see above) */ |
795 | if (state->enable_fallback) |
796 | font_cache_insert (cache: state->cache, wc, font: *font, position: *position); |
797 | |
798 | return TRUE; |
799 | } |
800 | |
801 | static PangoLanguage * |
802 | compute_derived_language (PangoLanguage *lang, |
803 | PangoScript script) |
804 | { |
805 | PangoLanguage *derived_lang; |
806 | |
807 | /* Make sure the language tag is consistent with the derived |
808 | * script. There is no point in marking up a section of |
809 | * Arabic text with the "en" language tag. |
810 | */ |
811 | if (lang && pango_language_includes_script (language: lang, script)) |
812 | derived_lang = lang; |
813 | else |
814 | { |
815 | derived_lang = pango_script_get_sample_language (script); |
816 | /* If we don't find a sample language for the script, we |
817 | * use a language tag that shouldn't actually be used |
818 | * anywhere. This keeps fontconfig (for the PangoFc* |
819 | * backend) from using the language tag to affect the |
820 | * sort order. I don't have a reference for 'xx' being |
821 | * safe here, though Keith Packard claims it is. |
822 | */ |
823 | if (!derived_lang) |
824 | derived_lang = pango_language_from_string (language: "xx" ); |
825 | } |
826 | |
827 | return derived_lang; |
828 | } |
829 | |
830 | static void |
831 | itemize_state_update_for_new_run (ItemizeState *state) |
832 | { |
833 | /* This block should be moved to update_attr_iterator, but I'm too lazy to |
834 | * do it right now */ |
835 | if (state->changed & (FONT_CHANGED | SCRIPT_CHANGED | WIDTH_CHANGED)) |
836 | { |
837 | /* Font-desc gravity overrides everything */ |
838 | if (state->font_desc_gravity != PANGO_GRAVITY_AUTO) |
839 | { |
840 | state->resolved_gravity = state->font_desc_gravity; |
841 | } |
842 | else |
843 | { |
844 | PangoGravity gravity = state->gravity; |
845 | PangoGravityHint gravity_hint = state->gravity_hint; |
846 | |
847 | if (G_LIKELY (gravity == PANGO_GRAVITY_AUTO)) |
848 | gravity = state->context->resolved_gravity; |
849 | |
850 | state->resolved_gravity = pango_gravity_get_for_script_and_width (script: state->script, |
851 | wide: state->width_iter.upright, |
852 | base_gravity: gravity, |
853 | hint: gravity_hint); |
854 | } |
855 | |
856 | if (state->font_desc_gravity != state->resolved_gravity) |
857 | { |
858 | pango_font_description_set_gravity (desc: state->font_desc, gravity: state->resolved_gravity); |
859 | state->changed |= FONT_CHANGED; |
860 | } |
861 | } |
862 | |
863 | if (state->changed & (SCRIPT_CHANGED | LANG_CHANGED)) |
864 | { |
865 | PangoLanguage *old_derived_lang = state->derived_lang; |
866 | state->derived_lang = compute_derived_language (lang: state->lang, script: state->script); |
867 | if (old_derived_lang != state->derived_lang) |
868 | state->changed |= DERIVED_LANG_CHANGED; |
869 | } |
870 | |
871 | if (state->changed & (EMOJI_CHANGED)) |
872 | { |
873 | state->changed |= FONT_CHANGED; |
874 | } |
875 | |
876 | if (state->changed & (FONT_CHANGED | DERIVED_LANG_CHANGED) && |
877 | state->current_fonts) |
878 | { |
879 | g_object_unref (object: state->current_fonts); |
880 | state->current_fonts = NULL; |
881 | state->cache = NULL; |
882 | } |
883 | |
884 | if (!state->current_fonts) |
885 | { |
886 | gboolean is_emoji = state->emoji_iter.is_emoji; |
887 | if (is_emoji && !state->emoji_font_desc) |
888 | { |
889 | state->emoji_font_desc = pango_font_description_copy_static (desc: state->font_desc); |
890 | pango_font_description_set_family_static (desc: state->emoji_font_desc, family: "emoji" ); |
891 | } |
892 | state->current_fonts = pango_font_map_load_fontset (fontmap: state->context->font_map, |
893 | context: state->context, |
894 | desc: is_emoji ? state->emoji_font_desc : state->font_desc, |
895 | language: state->derived_lang); |
896 | state->cache = get_font_cache (fontset: state->current_fonts); |
897 | } |
898 | |
899 | if ((state->changed & FONT_CHANGED) && state->base_font) |
900 | { |
901 | g_object_unref (object: state->base_font); |
902 | state->base_font = NULL; |
903 | } |
904 | } |
905 | |
906 | /* We don't want space characters to affect font selection; in general, |
907 | * it's always wrong to select a font just to render a space. |
908 | * |
909 | * We assume that all fonts have the ASCII space, and for other space |
910 | * characters if they don't, HarfBuzz will compatibility-decompose them |
911 | * to ASCII space... |
912 | * See bugs #355987 and #701652. |
913 | * |
914 | * We don't want to change fonts just for variation selectors. |
915 | * See bug #781123. |
916 | * |
917 | * We don't want to change fonts for default ignorables such as Cf chars. |
918 | * Note that Cf chars in the Arabic block are visible and need to have |
919 | * a font, so we exclude. |
920 | * |
921 | * Finally, don't change fonts for line or paragraph separators. |
922 | * |
923 | * Note that we want spaces to use the 'better' font, comparing |
924 | * the font that is used before and after the space. This is handled |
925 | * in itemize_state_add_character(). |
926 | */ |
927 | static gboolean |
928 | consider_as_space (gunichar wc) |
929 | { |
930 | GUnicodeType type = g_unichar_type (c: wc); |
931 | return type == G_UNICODE_CONTROL || |
932 | (type == G_UNICODE_FORMAT && !((wc >= 0x600 && wc <= 0x06ff) || wc == 0x70f || wc == 0x8e2)) || |
933 | type == G_UNICODE_SURROGATE || |
934 | type == G_UNICODE_LINE_SEPARATOR || |
935 | type == G_UNICODE_PARAGRAPH_SEPARATOR || |
936 | (type == G_UNICODE_SPACE_SEPARATOR && wc != 0x1680u /* OGHAM SPACE MARK */) || |
937 | (wc >= 0xfe00u && wc <= 0xfe0fu) || |
938 | (wc >= 0xe0100u && wc <= 0xe01efu); |
939 | } |
940 | |
941 | static void |
942 | itemize_state_process_run (ItemizeState *state) |
943 | { |
944 | const char *p; |
945 | gboolean last_was_forced_break = FALSE; |
946 | gboolean is_space; |
947 | |
948 | /* Only one character has type G_UNICODE_LINE_SEPARATOR in Unicode 4.0; |
949 | * update this if that changes. */ |
950 | #define LINE_SEPARATOR 0x2028 |
951 | |
952 | itemize_state_update_for_new_run (state); |
953 | |
954 | /* We should never get an empty run */ |
955 | g_assert (state->run_end != state->run_start); |
956 | |
957 | for (p = state->run_start; |
958 | p < state->run_end; |
959 | p = g_utf8_next_char (p)) |
960 | { |
961 | gunichar wc = g_utf8_get_char (p); |
962 | gboolean is_forced_break = (wc == '\t' || wc == LINE_SEPARATOR); |
963 | PangoFont *font; |
964 | int font_position; |
965 | |
966 | if (consider_as_space (wc)) |
967 | { |
968 | font = NULL; |
969 | font_position = 0xffff; |
970 | is_space = TRUE; |
971 | } |
972 | else |
973 | { |
974 | get_font (state, wc, font: &font, position: &font_position); |
975 | is_space = FALSE; |
976 | } |
977 | |
978 | itemize_state_add_character (state, font, font_position, |
979 | force_break: is_forced_break || last_was_forced_break, |
980 | pos: p, |
981 | is_space); |
982 | |
983 | last_was_forced_break = is_forced_break; |
984 | } |
985 | |
986 | /* Finish the final item from the current segment */ |
987 | state->item->length = (p - state->text) - state->item->offset; |
988 | if (!state->item->analysis.font) |
989 | { |
990 | PangoFont *font; |
991 | int position; |
992 | |
993 | if (G_UNLIKELY (!get_font (state, ' ', &font, &position))) |
994 | { |
995 | /* If no font was found, warn once per fontmap/script pair */ |
996 | PangoFontMap *fontmap = state->context->font_map; |
997 | char *script_tag = g_strdup_printf (format: "g-unicode-script-%d" , state->script); |
998 | |
999 | if (!g_object_get_data (G_OBJECT (fontmap), key: script_tag)) |
1000 | { |
1001 | g_warning ("failed to choose a font, expect ugly output. script='%d'" , |
1002 | state->script); |
1003 | |
1004 | g_object_set_data_full (G_OBJECT (fontmap), key: script_tag, |
1005 | GINT_TO_POINTER (1), NULL); |
1006 | } |
1007 | |
1008 | g_free (mem: script_tag); |
1009 | |
1010 | font = NULL; |
1011 | } |
1012 | itemize_state_fill_font (state, font); |
1013 | } |
1014 | state->item = NULL; |
1015 | } |
1016 | |
1017 | static void |
1018 | itemize_state_finish (ItemizeState *state) |
1019 | { |
1020 | g_free (mem: state->embedding_levels); |
1021 | if (state->free_attr_iter) |
1022 | pango_attr_iterator_destroy (iterator: state->attr_iter); |
1023 | _pango_script_iter_fini (iter: &state->script_iter); |
1024 | pango_font_description_free (desc: state->font_desc); |
1025 | pango_font_description_free (desc: state->emoji_font_desc); |
1026 | width_iter_fini (iter: &state->width_iter); |
1027 | _pango_emoji_iter_fini (iter: &state->emoji_iter); |
1028 | |
1029 | if (state->current_fonts) |
1030 | g_object_unref (object: state->current_fonts); |
1031 | if (state->base_font) |
1032 | g_object_unref (object: state->base_font); |
1033 | } |
1034 | |
1035 | /* }}} */ |
1036 | /* {{{ Post-processing */ |
1037 | |
1038 | /* {{{ Handling font scale */ |
1039 | |
1040 | typedef struct { |
1041 | PangoAttribute *attr; |
1042 | double scale; |
1043 | } ScaleItem; |
1044 | |
1045 | static gboolean |
1046 | collect_font_scale (PangoContext *context, |
1047 | GList **stack, |
1048 | PangoItem *item, |
1049 | PangoItem *prev, |
1050 | double *scale, |
1051 | gboolean *is_small_caps) |
1052 | { |
1053 | gboolean retval = FALSE; |
1054 | GList *l; |
1055 | |
1056 | for (GSList *l = item->analysis.extra_attrs; l; l = l->next) |
1057 | { |
1058 | PangoAttribute *attr = l->data; |
1059 | |
1060 | if (attr->klass->type == PANGO_ATTR_FONT_SCALE) |
1061 | { |
1062 | if (attr->start_index == item->offset) |
1063 | { |
1064 | ScaleItem *entry; |
1065 | int y_scale; |
1066 | hb_position_t y_size; |
1067 | hb_position_t cap_height; |
1068 | hb_position_t x_height; |
1069 | |
1070 | entry = g_new (ScaleItem, 1); |
1071 | entry->attr = attr; |
1072 | *stack = g_list_prepend (list: *stack, data: entry); |
1073 | |
1074 | switch (((PangoAttrInt *)attr)->value) |
1075 | { |
1076 | case PANGO_FONT_SCALE_NONE: |
1077 | break; |
1078 | case PANGO_FONT_SCALE_SUPERSCRIPT: |
1079 | if (prev && |
1080 | hb_ot_metrics_get_position (font: pango_font_get_hb_font (font: prev->analysis.font), |
1081 | metrics_tag: HB_OT_METRICS_TAG_SUPERSCRIPT_EM_Y_SIZE, |
1082 | position: &y_size)) |
1083 | { |
1084 | hb_font_get_scale (font: pango_font_get_hb_font (font: prev->analysis.font), NULL, y_scale: &y_scale); |
1085 | entry->scale = y_size / (double) y_scale; |
1086 | } |
1087 | else |
1088 | { |
1089 | entry->scale = 1 / 1.2; |
1090 | } |
1091 | break; |
1092 | case PANGO_FONT_SCALE_SUBSCRIPT: |
1093 | if (prev && |
1094 | hb_ot_metrics_get_position (font: pango_font_get_hb_font (font: prev->analysis.font), |
1095 | metrics_tag: HB_OT_METRICS_TAG_SUBSCRIPT_EM_Y_SIZE, |
1096 | position: &y_size)) |
1097 | { |
1098 | hb_font_get_scale (font: pango_font_get_hb_font (font: prev->analysis.font), NULL, y_scale: &y_scale); |
1099 | entry->scale = y_size / (double) y_scale; |
1100 | } |
1101 | else |
1102 | { |
1103 | entry->scale = 1 / 1.2; |
1104 | } |
1105 | break; |
1106 | case PANGO_FONT_SCALE_SMALL_CAPS: |
1107 | if (hb_ot_metrics_get_position (font: pango_font_get_hb_font (font: item->analysis.font), |
1108 | metrics_tag: HB_OT_METRICS_TAG_CAP_HEIGHT, |
1109 | position: &cap_height) && |
1110 | hb_ot_metrics_get_position (font: pango_font_get_hb_font (font: item->analysis.font), |
1111 | metrics_tag: HB_OT_METRICS_TAG_X_HEIGHT, |
1112 | position: &x_height)) |
1113 | { |
1114 | entry->scale = x_height / (double) cap_height; |
1115 | } |
1116 | else |
1117 | { |
1118 | entry->scale = 0.8; |
1119 | } |
1120 | break; |
1121 | default: |
1122 | g_assert_not_reached (); |
1123 | } |
1124 | } |
1125 | } |
1126 | } |
1127 | |
1128 | *scale = 1.0; |
1129 | *is_small_caps = TRUE; |
1130 | |
1131 | for (l = *stack; l; l = l->next) |
1132 | { |
1133 | ScaleItem *entry = l->data; |
1134 | *scale *= entry->scale; |
1135 | if (((PangoAttrInt *)entry->attr)->value != PANGO_FONT_SCALE_SMALL_CAPS) |
1136 | *is_small_caps = FALSE; |
1137 | retval = TRUE; |
1138 | } |
1139 | |
1140 | l = *stack; |
1141 | while (l) |
1142 | { |
1143 | ScaleItem *entry = l->data; |
1144 | GList *next = l->next; |
1145 | |
1146 | if (entry->attr->end_index == item->offset + item->length) |
1147 | { |
1148 | *stack = g_list_delete_link (list: *stack, link_: l); |
1149 | g_free (mem: entry); |
1150 | } |
1151 | |
1152 | l = next; |
1153 | } |
1154 | |
1155 | return retval; |
1156 | } |
1157 | |
1158 | static void |
1159 | apply_scale_to_item (PangoContext *context, |
1160 | PangoItem *item, |
1161 | double scale, |
1162 | gboolean is_small_caps) |
1163 | { |
1164 | PangoFontDescription *desc; |
1165 | double size; |
1166 | |
1167 | if (!item->analysis.font) |
1168 | return; |
1169 | |
1170 | if (is_small_caps) |
1171 | pango_analysis_set_size_font (analysis: &item->analysis, font: item->analysis.font); |
1172 | |
1173 | desc = pango_font_describe (font: item->analysis.font); |
1174 | size = scale * pango_font_description_get_size (desc); |
1175 | |
1176 | if (pango_font_description_get_size_is_absolute (desc)) |
1177 | pango_font_description_set_absolute_size (desc, size); |
1178 | else |
1179 | pango_font_description_set_size (desc, size); |
1180 | |
1181 | g_object_unref (object: item->analysis.font); |
1182 | item->analysis.font = pango_font_map_load_font (fontmap: context->font_map, context, desc); |
1183 | |
1184 | pango_font_description_free (desc); |
1185 | } |
1186 | |
1187 | static void |
1188 | apply_font_scale (PangoContext *context, |
1189 | GList *items) |
1190 | { |
1191 | PangoItem *prev = NULL; |
1192 | GList *stack = NULL; |
1193 | |
1194 | for (GList *l = items; l; l = l->next) |
1195 | { |
1196 | PangoItem *item = l->data; |
1197 | double scale; |
1198 | gboolean is_small_caps; |
1199 | |
1200 | if (collect_font_scale (context, stack: &stack, item, prev, scale: &scale, is_small_caps: &is_small_caps)) |
1201 | apply_scale_to_item (context, item, scale, is_small_caps); |
1202 | |
1203 | prev = item; |
1204 | } |
1205 | |
1206 | if (stack != NULL) |
1207 | { |
1208 | g_warning ("Leftover font scales" ); |
1209 | g_list_free_full (list: stack, free_func: g_free); |
1210 | } |
1211 | } |
1212 | |
1213 | /* }}} */ |
1214 | /* {{{ Handling Casing variants */ |
1215 | |
1216 | static gboolean |
1217 | all_features_supported (PangoItem *item, |
1218 | hb_tag_t *features, |
1219 | guint n_features) |
1220 | { |
1221 | hb_font_t *font = pango_font_get_hb_font (font: item->analysis.font); |
1222 | hb_face_t *face = hb_font_get_face (font); |
1223 | hb_script_t script; |
1224 | hb_language_t language; |
1225 | guint script_count = HB_OT_MAX_TAGS_PER_SCRIPT; |
1226 | hb_tag_t script_tags[HB_OT_MAX_TAGS_PER_SCRIPT]; |
1227 | hb_tag_t chosen_script; |
1228 | guint language_count = HB_OT_MAX_TAGS_PER_LANGUAGE; |
1229 | hb_tag_t language_tags[HB_OT_MAX_TAGS_PER_LANGUAGE]; |
1230 | guint script_index, language_index; |
1231 | guint index; |
1232 | |
1233 | script = g_unicode_script_to_iso15924 (script: item->analysis.script); |
1234 | language = hb_language_from_string (pango_language_to_string (item->analysis.language), len: -1); |
1235 | |
1236 | hb_ot_tags_from_script_and_language (script, language, |
1237 | script_count: &script_count, script_tags, |
1238 | language_count: &language_count, language_tags); |
1239 | hb_ot_layout_table_select_script (face, HB_OT_TAG_GSUB, |
1240 | script_count, script_tags, |
1241 | script_index: &script_index, |
1242 | chosen_script: &chosen_script); |
1243 | hb_ot_layout_script_select_language (face, HB_OT_TAG_GSUB, |
1244 | script_index, |
1245 | language_count, language_tags, |
1246 | language_index: &language_index); |
1247 | |
1248 | for (int i = 0; i < n_features; i++) |
1249 | { |
1250 | if (!hb_ot_layout_language_find_feature (face, HB_OT_TAG_GSUB, |
1251 | script_index, language_index, |
1252 | feature_tag: features[i], |
1253 | feature_index: &index)) |
1254 | return FALSE; |
1255 | } |
1256 | |
1257 | return TRUE; |
1258 | } |
1259 | |
1260 | static gboolean |
1261 | variant_supported (PangoItem *item, |
1262 | PangoVariant variant) |
1263 | { |
1264 | hb_tag_t features[2]; |
1265 | guint num_features = 0; |
1266 | |
1267 | switch (variant) |
1268 | { |
1269 | case PANGO_VARIANT_NORMAL: |
1270 | case PANGO_VARIANT_TITLE_CAPS: |
1271 | return TRUE; |
1272 | case PANGO_VARIANT_SMALL_CAPS: |
1273 | features[num_features++] = HB_TAG ('s', 'm', 'c', 'p'); |
1274 | break; |
1275 | case PANGO_VARIANT_ALL_SMALL_CAPS: |
1276 | features[num_features++] = HB_TAG ('s', 'm', 'c', 'p'); |
1277 | features[num_features++] = HB_TAG ('c', '2', 's', 'c'); |
1278 | break; |
1279 | case PANGO_VARIANT_PETITE_CAPS: |
1280 | features[num_features++] = HB_TAG ('p', 'c', 'a', 'p'); |
1281 | break; |
1282 | case PANGO_VARIANT_ALL_PETITE_CAPS: |
1283 | features[num_features++] = HB_TAG ('p', 'c', 'a', 'p'); |
1284 | features[num_features++] = HB_TAG ('c', '2', 'p', 'c'); |
1285 | break; |
1286 | case PANGO_VARIANT_UNICASE: |
1287 | features[num_features++] = HB_TAG ('u', 'n', 'i', 'c'); |
1288 | break; |
1289 | default: |
1290 | g_assert_not_reached (); |
1291 | } |
1292 | |
1293 | return all_features_supported (item, features, n_features: num_features); |
1294 | } |
1295 | |
1296 | static PangoVariant |
1297 | get_font_variant (PangoItem *item) |
1298 | { |
1299 | if (item->analysis.font) |
1300 | return pango_font_get_variant (font: item->analysis.font); |
1301 | |
1302 | return PANGO_VARIANT_NORMAL; |
1303 | } |
1304 | |
1305 | static PangoTextTransform |
1306 | find_text_transform (const PangoAnalysis *analysis) |
1307 | { |
1308 | GSList *l; |
1309 | PangoTextTransform transform = PANGO_TEXT_TRANSFORM_NONE; |
1310 | |
1311 | for (l = analysis->extra_attrs; l; l = l->next) |
1312 | { |
1313 | PangoAttribute *attr = l->data; |
1314 | |
1315 | if (attr->klass->type == PANGO_ATTR_TEXT_TRANSFORM) |
1316 | transform = (PangoTextTransform) ((PangoAttrInt*)attr)->value; |
1317 | } |
1318 | |
1319 | return transform; |
1320 | } |
1321 | |
1322 | /* Split list_item into upper- and lowercase runs, and |
1323 | * add font scale and text transform attributes to make |
1324 | * them be appear according to variant. The log_attrs are |
1325 | * needed for taking text transforms into account when |
1326 | * determining the case of characters int he run. |
1327 | */ |
1328 | static void |
1329 | split_item_for_variant (const char *text, |
1330 | PangoLogAttr *log_attrs, |
1331 | PangoVariant variant, |
1332 | GList *list_item) |
1333 | { |
1334 | PangoItem *item = list_item->data; |
1335 | const char *start, *end; |
1336 | const char *p, *p0; |
1337 | gunichar wc; |
1338 | PangoTextTransform transform = PANGO_TEXT_TRANSFORM_NONE; |
1339 | PangoFontScale lowercase_scale = PANGO_FONT_SCALE_NONE; |
1340 | PangoFontScale uppercase_scale = PANGO_FONT_SCALE_NONE; |
1341 | PangoTextTransform item_transform; |
1342 | gboolean is_word_start; |
1343 | int offset; |
1344 | |
1345 | switch (variant) |
1346 | { |
1347 | case PANGO_VARIANT_ALL_SMALL_CAPS: |
1348 | case PANGO_VARIANT_ALL_PETITE_CAPS: |
1349 | uppercase_scale = PANGO_FONT_SCALE_SMALL_CAPS; |
1350 | G_GNUC_FALLTHROUGH; |
1351 | case PANGO_VARIANT_SMALL_CAPS: |
1352 | case PANGO_VARIANT_PETITE_CAPS: |
1353 | transform = PANGO_TEXT_TRANSFORM_UPPERCASE; |
1354 | lowercase_scale = PANGO_FONT_SCALE_SMALL_CAPS; |
1355 | break; |
1356 | case PANGO_VARIANT_UNICASE: |
1357 | uppercase_scale = PANGO_FONT_SCALE_SMALL_CAPS; |
1358 | break; |
1359 | case PANGO_VARIANT_NORMAL: |
1360 | case PANGO_VARIANT_TITLE_CAPS: |
1361 | default: |
1362 | g_assert_not_reached (); |
1363 | } |
1364 | |
1365 | item_transform = find_text_transform (analysis: &item->analysis); |
1366 | |
1367 | start = text + item->offset; |
1368 | end = start + item->length; |
1369 | offset = ((PangoItemPrivate *)item)->char_offset; |
1370 | |
1371 | p = start; |
1372 | while (p < end) |
1373 | { |
1374 | p0 = p; |
1375 | wc = g_utf8_get_char (p); |
1376 | is_word_start = log_attrs && log_attrs[offset].is_word_start; |
1377 | while (p < end && (item_transform == PANGO_TEXT_TRANSFORM_LOWERCASE || |
1378 | consider_as_space (wc) || |
1379 | (g_unichar_islower (c: wc) && |
1380 | !(item_transform == PANGO_TEXT_TRANSFORM_UPPERCASE || |
1381 | (item_transform == PANGO_TEXT_TRANSFORM_CAPITALIZE && is_word_start))))) |
1382 | { |
1383 | p = g_utf8_next_char (p); |
1384 | wc = g_utf8_get_char (p); |
1385 | offset++; |
1386 | is_word_start = log_attrs && log_attrs[offset].is_word_start; |
1387 | } |
1388 | |
1389 | if (p0 < p) |
1390 | { |
1391 | PangoItem *new_item; |
1392 | PangoAttribute *attr; |
1393 | |
1394 | /* p0 .. p is a lowercase segment */ |
1395 | if (p < end) |
1396 | { |
1397 | new_item = pango_item_split (orig: item, split_index: p - p0, split_offset: g_utf8_strlen (p: p0, max: p - p0)); |
1398 | list_item->data = new_item; |
1399 | list_item = g_list_insert_before (list: list_item, sibling: list_item->next, data: item); |
1400 | list_item = list_item->next; |
1401 | } |
1402 | else |
1403 | { |
1404 | new_item = item; |
1405 | } |
1406 | |
1407 | if (transform != PANGO_TEXT_TRANSFORM_NONE) |
1408 | { |
1409 | attr = pango_attr_text_transform_new (transform); |
1410 | attr->start_index = new_item->offset; |
1411 | attr->end_index = new_item->offset + new_item->length; |
1412 | new_item->analysis.extra_attrs = g_slist_append (list: new_item->analysis.extra_attrs, data: attr); |
1413 | } |
1414 | |
1415 | if (lowercase_scale != PANGO_FONT_SCALE_NONE) |
1416 | { |
1417 | attr = pango_attr_font_scale_new (scale: lowercase_scale); |
1418 | attr->start_index = new_item->offset; |
1419 | attr->end_index = new_item->offset + new_item->length; |
1420 | new_item->analysis.extra_attrs = g_slist_append (list: new_item->analysis.extra_attrs, data: attr); |
1421 | } |
1422 | } |
1423 | |
1424 | p0 = p; |
1425 | wc = g_utf8_get_char (p); |
1426 | is_word_start = log_attrs && log_attrs[offset].is_word_start; |
1427 | while (p < end && (item_transform == PANGO_TEXT_TRANSFORM_UPPERCASE || |
1428 | consider_as_space (wc) || |
1429 | !(item_transform == PANGO_TEXT_TRANSFORM_LOWERCASE || g_unichar_islower (c: wc)) || |
1430 | (item_transform == PANGO_TEXT_TRANSFORM_CAPITALIZE && is_word_start))) |
1431 | { |
1432 | p = g_utf8_next_char (p); |
1433 | wc = g_utf8_get_char (p); |
1434 | offset++; |
1435 | is_word_start = log_attrs && log_attrs[offset].is_word_start; |
1436 | } |
1437 | |
1438 | if (p0 < p) |
1439 | { |
1440 | PangoItem *new_item; |
1441 | PangoAttribute *attr; |
1442 | |
1443 | /* p0 .. p is a uppercase segment */ |
1444 | if (p < end) |
1445 | { |
1446 | new_item = pango_item_split (orig: item, split_index: p - p0, split_offset: g_utf8_strlen (p: p0, max: p - p0)); |
1447 | list_item->data = new_item; |
1448 | list_item = g_list_insert_before (list: list_item, sibling: list_item->next, data: item); |
1449 | list_item = list_item->next; |
1450 | } |
1451 | else |
1452 | { |
1453 | new_item = item; |
1454 | } |
1455 | |
1456 | if (uppercase_scale != PANGO_FONT_SCALE_NONE) |
1457 | { |
1458 | attr = pango_attr_font_scale_new (scale: uppercase_scale); |
1459 | attr->start_index = new_item->offset; |
1460 | attr->end_index = new_item->offset + new_item->length; |
1461 | new_item->analysis.extra_attrs = g_slist_append (list: new_item->analysis.extra_attrs, data: attr); |
1462 | } |
1463 | } |
1464 | } |
1465 | } |
1466 | |
1467 | static void |
1468 | handle_variants_for_item (const char *text, |
1469 | PangoLogAttr *log_attrs, |
1470 | GList *l) |
1471 | { |
1472 | PangoItem *item = l->data; |
1473 | PangoVariant variant; |
1474 | |
1475 | variant = get_font_variant (item); |
1476 | if (!variant_supported (item, variant)) |
1477 | split_item_for_variant (text, log_attrs, variant, list_item: l); |
1478 | } |
1479 | |
1480 | static void |
1481 | handle_variants (const char *text, |
1482 | PangoLogAttr *log_attrs, |
1483 | GList *items) |
1484 | { |
1485 | GList *next; |
1486 | |
1487 | for (GList *l = items; l; l = next) |
1488 | { |
1489 | next = l->next; |
1490 | handle_variants_for_item (text, log_attrs, l); |
1491 | } |
1492 | } |
1493 | |
1494 | /* }}} */ |
1495 | |
1496 | static GList * |
1497 | reorder_items (PangoContext *context, |
1498 | GList *items) |
1499 | { |
1500 | int char_offset = 0; |
1501 | |
1502 | items = g_list_reverse (list: items); |
1503 | |
1504 | /* Also cmpute the char offset for each item here */ |
1505 | for (GList *l = items; l; l = l->next) |
1506 | { |
1507 | PangoItemPrivate *item = l->data; |
1508 | item->char_offset = char_offset; |
1509 | char_offset += item->num_chars; |
1510 | } |
1511 | |
1512 | return items; |
1513 | } |
1514 | |
1515 | static GList * |
1516 | post_process_items (PangoContext *context, |
1517 | const char *text, |
1518 | PangoLogAttr *log_attrs, |
1519 | GList *items) |
1520 | { |
1521 | handle_variants (text, log_attrs, items); |
1522 | apply_font_scale (context, items); |
1523 | |
1524 | return items; |
1525 | } |
1526 | |
1527 | /* }}} */ |
1528 | /* {{{ Private API */ |
1529 | |
1530 | /* Like pango_itemize_with_base_dir, but takes a font description. |
1531 | * In contrast to pango_itemize_with_base_dir, this function does |
1532 | * not call pango_itemize_post_process_items, so you need to do that |
1533 | * separately, after applying attributes that affect segmentation and |
1534 | * computing the log attrs. |
1535 | */ |
1536 | GList * |
1537 | pango_itemize_with_font (PangoContext *context, |
1538 | PangoDirection base_dir, |
1539 | const char *text, |
1540 | int start_index, |
1541 | int length, |
1542 | PangoAttrList *attrs, |
1543 | PangoAttrIterator *cached_iter, |
1544 | const PangoFontDescription *desc) |
1545 | { |
1546 | ItemizeState state; |
1547 | |
1548 | g_return_val_if_fail (context->font_map != NULL, NULL); |
1549 | |
1550 | if (length == 0 || g_utf8_get_char (p: text + start_index) == '\0') |
1551 | return NULL; |
1552 | |
1553 | itemize_state_init (state: &state, context, text, base_dir, start_index, length, |
1554 | attrs, cached_iter, desc); |
1555 | |
1556 | do |
1557 | itemize_state_process_run (state: &state); |
1558 | while (itemize_state_next (state: &state)); |
1559 | |
1560 | itemize_state_finish (state: &state); |
1561 | |
1562 | return reorder_items (context, items: state.result); |
1563 | } |
1564 | |
1565 | /* Apply post-processing steps that may require log attrs. |
1566 | */ |
1567 | GList * |
1568 | pango_itemize_post_process_items (PangoContext *context, |
1569 | const char *text, |
1570 | PangoLogAttr *log_attrs, |
1571 | GList *items) |
1572 | { |
1573 | return post_process_items (context, text, log_attrs, items); |
1574 | } |
1575 | |
1576 | /* }}} */ |
1577 | /* {{{ Public API */ |
1578 | |
1579 | /** |
1580 | * pango_itemize_with_base_dir: |
1581 | * @context: a structure holding information that affects |
1582 | * the itemization process. |
1583 | * @base_dir: base direction to use for bidirectional processing |
1584 | * @text: the text to itemize. |
1585 | * @start_index: first byte in @text to process |
1586 | * @length: the number of bytes (not characters) to process |
1587 | * after @start_index. This must be >= 0. |
1588 | * @attrs: the set of attributes that apply to @text. |
1589 | * @cached_iter: (nullable): Cached attribute iterator |
1590 | * |
1591 | * Like `pango_itemize()`, but with an explicitly specified base direction. |
1592 | * |
1593 | * The base direction is used when computing bidirectional levels. |
1594 | * [func@itemize] gets the base direction from the `PangoContext` |
1595 | * (see [method@Pango.Context.set_base_dir]). |
1596 | * |
1597 | * Return value: (transfer full) (element-type Pango.Item): a `GList` of |
1598 | * [struct@Pango.Item] structures. The items should be freed using |
1599 | * [method@Pango.Item.free] probably in combination with [func@GLib.List.free_full]. |
1600 | * |
1601 | * Since: 1.4 |
1602 | */ |
1603 | GList * |
1604 | pango_itemize_with_base_dir (PangoContext *context, |
1605 | PangoDirection base_dir, |
1606 | const char *text, |
1607 | int start_index, |
1608 | int length, |
1609 | PangoAttrList *attrs, |
1610 | PangoAttrIterator *cached_iter) |
1611 | { |
1612 | GList *items; |
1613 | |
1614 | g_return_val_if_fail (context != NULL, NULL); |
1615 | g_return_val_if_fail (start_index >= 0, NULL); |
1616 | g_return_val_if_fail (length >= 0, NULL); |
1617 | g_return_val_if_fail (length == 0 || text != NULL, NULL); |
1618 | |
1619 | items = pango_itemize_with_font (context, base_dir, |
1620 | text, start_index, length, |
1621 | attrs, cached_iter, |
1622 | NULL); |
1623 | |
1624 | return pango_itemize_post_process_items (context, text, NULL, items); |
1625 | } |
1626 | |
1627 | /** |
1628 | * pango_itemize: |
1629 | * @context: a structure holding information that affects |
1630 | * the itemization process. |
1631 | * @text: the text to itemize. Must be valid UTF-8 |
1632 | * @start_index: first byte in @text to process |
1633 | * @length: the number of bytes (not characters) to process |
1634 | * after @start_index. This must be >= 0. |
1635 | * @attrs: the set of attributes that apply to @text. |
1636 | * @cached_iter: (nullable): Cached attribute iterator |
1637 | * |
1638 | * Breaks a piece of text into segments with consistent directional |
1639 | * level and font. |
1640 | * |
1641 | * Each byte of @text will be contained in exactly one of the items in the |
1642 | * returned list; the generated list of items will be in logical order (the |
1643 | * start offsets of the items are ascending). |
1644 | * |
1645 | * @cached_iter should be an iterator over @attrs currently positioned |
1646 | * at a range before or containing @start_index; @cached_iter will be |
1647 | * advanced to the range covering the position just after |
1648 | * @start_index + @length. (i.e. if itemizing in a loop, just keep passing |
1649 | * in the same @cached_iter). |
1650 | * |
1651 | * Return value: (transfer full) (element-type Pango.Item): a `GList` of |
1652 | * [struct@Pango.Item] structures. The items should be freed using |
1653 | * [method@Pango.Item.free] in combination with [func@GLib.List.free_full]. |
1654 | */ |
1655 | GList * |
1656 | pango_itemize (PangoContext *context, |
1657 | const char *text, |
1658 | int start_index, |
1659 | int length, |
1660 | PangoAttrList *attrs, |
1661 | PangoAttrIterator *cached_iter) |
1662 | { |
1663 | g_return_val_if_fail (context != NULL, NULL); |
1664 | g_return_val_if_fail (start_index >= 0, NULL); |
1665 | g_return_val_if_fail (length >= 0, NULL); |
1666 | g_return_val_if_fail (length == 0 || text != NULL, NULL); |
1667 | |
1668 | return pango_itemize_with_base_dir (context, base_dir: context->base_dir, |
1669 | text, start_index, length, |
1670 | attrs, cached_iter); |
1671 | } |
1672 | |
1673 | /* }}} */ |
1674 | |
1675 | /* vim:set foldmethod=marker expandtab: */ |
1676 | |