OpenTTD Source 20260911-master-gee2b2ac12a
gfx_layout_icu.cpp
Go to the documentation of this file.
1/*
2 * This file is part of OpenTTD.
3 * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4 * OpenTTD 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.
5 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <https://www.gnu.org/licenses/old-licenses/gpl-2.0>.
6 */
7
9
10#include "stdafx.h"
11#include "gfx_layout_icu.h"
12
13#include "debug.h"
14#include "misc/autorelease.hpp"
15#include "strings_func.h"
16#include "language.h"
17#include "table/control_codes.h"
18#include "zoom_func.h"
19
20#include "3rdparty/icu/scriptrun.h"
21
22#include <unicode/ubidi.h>
23#include <unicode/brkiter.h>
24
25#include <hb.h>
26#include <hb-ft.h>
27
28#include "safeguards.h"
29
31constexpr float FONT_SCALE = 64.0;
32
38class ICURun {
39public:
40 int start;
41 int length;
42 UBiDiLevel level;
43 UScriptCode script;
45
46 std::vector<GlyphID> glyphs;
47 std::vector<int> advance;
48 std::vector<int> glyph_to_char;
49 std::vector<ParagraphLayouter::Position> positions;
50 int total_advance = 0;
51
52 ICURun(int start, int length, UBiDiLevel level, UScriptCode script = USCRIPT_UNKNOWN, Font *font = nullptr) : start(start), length(length), level(level), script(script), font(font) {}
53
54 void Shape(UChar *buff, size_t length);
55};
56
60class ICUParagraphLayout : public ParagraphLayouter {
61public:
64 private:
65 std::vector<GlyphID> glyphs;
66 std::vector<Position> positions;
67 std::vector<int> glyph_to_char;
68
69 int total_advance;
70 const Font *font;
71
72 public:
73 ICUVisualRun(const ICURun &run, int x);
74
75 std::span<const GlyphID> GetGlyphs() const override { return this->glyphs; }
76 std::span<const Position> GetPositions() const override { return this->positions; }
77 std::span<const int> GetGlyphToCharMap() const override { return this->glyph_to_char; }
78
79 const Font *GetFont() const override { return this->font; }
80 int GetLeading() const override { return this->font->fc->GetHeight(); }
81 size_t GetGlyphCount() const override { return this->glyphs.size(); }
82 int GetAdvance() const { return this->total_advance; }
83 };
84
86 class ICULine : public std::vector<ICUVisualRun>, public ParagraphLayouter::Line {
87 public:
88 int GetLeading() const override;
89 int GetWidth() const override;
90 size_t CountRuns() const override { return this->size(); }
91 const VisualRun &GetVisualRun(size_t run) const override { return this->at(run); }
92
93 int GetInternalCharLength(char32_t c) const override
94 {
95 /* ICU uses UTF-16 internally which means we need to account for surrogate pairs. */
96 return c >= 0x010000U ? 2 : 1;
97 }
98 };
99
100private:
101 std::vector<ICURun> runs;
102 UChar *buff;
103 size_t buff_length;
104 std::vector<ICURun>::iterator current_run;
105 int partial_offset;
106
107public:
108 ICUParagraphLayout(std::vector<ICURun> &&runs, UChar *buff, size_t buff_length) : runs(std::move(runs)), buff(buff), buff_length(buff_length)
109 {
110 this->Reflow();
111 }
112
113 ~ICUParagraphLayout() override = default;
114
115 void Reflow() override
116 {
117 this->current_run = this->runs.begin();
118 this->partial_offset = 0;
119 }
120
121 std::unique_ptr<const Line> NextLine(int max_width) override;
122};
123
133 glyphs(run.glyphs), glyph_to_char(run.glyph_to_char), total_advance(run.total_advance), font(run.font)
134{
135 /* If there are no positions, the ICURun was not Shaped; that should never happen. */
136 assert(!run.positions.empty());
137 this->positions.reserve(run.positions.size());
138
139 /* Copy positions, moving x coordinate by x offset. */
140 for (const auto &pos : run.positions) {
141 this->positions.emplace_back(pos.left + x, pos.right + x, pos.top);
142 }
143}
144
151void ICURun::Shape(UChar *buff, size_t buff_length)
152{
153 auto hbfont = hb_ft_font_create_referenced(*(static_cast<const FT_Face *>(font->fc->GetOSHandle())));
154 /* Match the flags with how we render the glyphs. */
155 hb_ft_font_set_load_flags(hbfont, GetFontAAState() ? FT_LOAD_TARGET_NORMAL : FT_LOAD_TARGET_MONO);
156
157 /* ICU buffer is in UTF-16. */
158 auto hbbuf = hb_buffer_create();
159 hb_buffer_add_utf16(hbbuf, reinterpret_cast<uint16_t *>(buff), buff_length, this->start, this->length);
160
161 /* Set all the properties of this segment. */
162 hb_buffer_set_direction(hbbuf, (this->level & 1) == 1 ? HB_DIRECTION_RTL : HB_DIRECTION_LTR);
163 hb_buffer_set_script(hbbuf, hb_script_from_string(uscript_getShortName(this->script), -1));
164 hb_buffer_set_language(hbbuf, hb_language_from_string(_current_language->isocode, -1));
165 hb_buffer_set_cluster_level(hbbuf, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_GRAPHEMES);
166
167 /* Shape the segment. */
168 hb_shape(hbfont, hbbuf, nullptr, 0);
169
170 unsigned int glyph_count;
171 auto glyph_info = hb_buffer_get_glyph_infos(hbbuf, &glyph_count);
172 auto glyph_pos = hb_buffer_get_glyph_positions(hbbuf, &glyph_count);
173
174 /* Make sure any former run is lost. */
175 this->glyphs.clear();
176 this->glyph_to_char.clear();
177 this->positions.clear();
178 this->advance.clear();
179
180 /* Reserve space, as we already know the size. */
181 this->glyphs.reserve(glyph_count);
182 this->glyph_to_char.reserve(glyph_count);
183 this->positions.reserve(glyph_count);
184 this->advance.reserve(glyph_count);
185
186 /* Prepare the glyphs/position. ICUVisualRun will give the position an offset if needed. */
187 hb_position_t advance = 0;
188 for (unsigned int i = 0; i < glyph_count; i++) {
189 int x_advance;
190
191 if (buff[glyph_info[i].cluster] >= SCC_SPRITE_START && buff[glyph_info[i].cluster] <= SCC_SPRITE_END && glyph_info[i].codepoint == 0) {
192 auto glyph = this->font->fc->MapCharToGlyph(buff[glyph_info[i].cluster]);
193 x_advance = this->font->fc->GetGlyphWidth(glyph);
194 this->glyphs.push_back(glyph);
195 this->positions.emplace_back(advance, advance + x_advance - 1, (this->font->fc->GetHeight() - ScaleSpriteTrad(FontCache::GetDefaultFontHeight(this->font->fc->GetSize()))) / 2); // Align sprite font to centre
196 } else {
197 x_advance = glyph_pos[i].x_advance / FONT_SCALE;
198 this->glyphs.push_back(glyph_info[i].codepoint);
199 this->positions.emplace_back(glyph_pos[i].x_offset / FONT_SCALE + advance, glyph_pos[i].x_offset / FONT_SCALE + advance + x_advance - 1, glyph_pos[i].y_offset / FONT_SCALE);
200 }
201
202 this->glyph_to_char.push_back(glyph_info[i].cluster);
203 this->advance.push_back(x_advance);
204 advance += x_advance;
205 }
206
207 /* Track the total advancement we made. */
208 this->total_advance = advance;
209
210 hb_buffer_destroy(hbbuf);
211 hb_font_destroy(hbfont);
212}
213
219{
220 int leading = 0;
221 for (const auto &run : *this) {
222 leading = std::max(leading, run.GetLeading());
223 }
224
225 return leading;
226}
227
233{
234 int length = 0;
235 for (const auto &run : *this) {
236 length += run.GetAdvance();
237 }
238
239 return length;
240}
241
251std::vector<ICURun> ItemizeBidi(UChar *buff, size_t length)
252{
253 auto ubidi = AutoRelease<UBiDi, ubidi_close>(ubidi_open());
254
255 auto parLevel = _current_text_dir == TD_RTL ? UBIDI_RTL : UBIDI_LTR;
256
257 UErrorCode err = U_ZERO_ERROR;
258 ubidi_setPara(ubidi.get(), buff, length, parLevel, nullptr, &err);
259 if (U_FAILURE(err)) {
260 Debug(Facility::Fontcache, Severity::Critical, "Failed to set paragraph: {}", u_errorName(err));
261 return {};
262 }
263
264 int32_t count = ubidi_countRuns(ubidi.get(), &err);
265 if (U_FAILURE(err)) {
266 Debug(Facility::Fontcache, Severity::Critical, "Failed to count runs: {}", u_errorName(err));
267 return {};
268 }
269
270 std::vector<ICURun> runs;
271 runs.reserve(count);
272
273 /* Find the breakpoints for the logical runs. So we get runs that say "from START to END". */
274 int32_t logical_pos = 0;
275 while (static_cast<size_t>(logical_pos) < length) {
276 auto start_pos = logical_pos;
277
278 /* Fetch the embedding level, so we can order bidi correctly later on. */
279 UBiDiLevel level;
280 ubidi_getLogicalRun(ubidi.get(), start_pos, &logical_pos, &level);
281
282 runs.emplace_back(start_pos, logical_pos - start_pos, level);
283 }
284
285 assert(static_cast<size_t>(count) == runs.size());
286
287 return runs;
288}
289
300std::vector<ICURun> ItemizeScript(UChar *buff, size_t length, std::vector<ICURun> &runs_current)
301{
302 std::vector<ICURun> runs;
303 icu::ScriptRun script_itemizer(buff, length);
304
305 int cur_pos = 0;
306 auto cur_run = runs_current.begin();
307 while (true) {
308 while (cur_pos < script_itemizer.getScriptEnd() && cur_run != runs_current.end()) {
309 int stop_pos = std::min(script_itemizer.getScriptEnd(), cur_run->start + cur_run->length);
310 assert(stop_pos - cur_pos > 0);
311
312 runs.emplace_back(cur_pos, stop_pos - cur_pos, cur_run->level, script_itemizer.getScriptCode());
313
314 if (stop_pos == cur_run->start + cur_run->length) cur_run++;
315 cur_pos = stop_pos;
316 }
317
318 if (!script_itemizer.next()) break;
319 }
320
321 return runs;
322}
323
333std::vector<ICURun> ItemizeStyle(std::vector<ICURun> &runs_current, FontMap &font_mapping)
334{
335 std::vector<ICURun> runs;
336
337 int cur_pos = 0;
338 auto cur_run = runs_current.begin();
339 for (auto const &[position, font] : font_mapping) {
340 while (cur_pos < position && cur_run != runs_current.end()) {
341 int stop_pos = std::min(position, cur_run->start + cur_run->length);
342 assert(stop_pos - cur_pos > 0);
343
344 runs.emplace_back(cur_pos, stop_pos - cur_pos, cur_run->level, cur_run->script, font);
345
346 if (stop_pos == cur_run->start + cur_run->length) cur_run++;
347 cur_pos = stop_pos;
348 }
349 }
350
351 return runs;
352}
353
354/* static */ std::unique_ptr<ParagraphLayouter> ICUParagraphLayoutFactory::GetParagraphLayout(UChar *buff, UChar *buff_end, FontMap &font_mapping)
355{
356 size_t length = buff_end - buff;
357 /* Can't layout an empty string. */
358 if (length == 0) return nullptr;
359
360 /* Can't layout our in-built sprite fonts. */
361 for (auto const &[position, font] : font_mapping) {
362 if (font->fc->IsBuiltInFont()) return nullptr;
363 }
364
365 auto runs = ItemizeBidi(buff, length);
366 runs = ItemizeScript(buff, length, runs);
367 runs = ItemizeStyle(runs, font_mapping);
368
369 if (runs.empty()) return nullptr;
370
371 for (auto &run : runs) {
372 run.Shape(buff, length);
373 }
374
375 return std::make_unique<ICUParagraphLayout>(std::move(runs), buff, length);
376}
377
378/* static */ std::unique_ptr<icu::BreakIterator> ICUParagraphLayoutFactory::break_iterator;
379
384{
385 auto locale = icu::Locale(_current_language->isocode);
386 UErrorCode status = U_ZERO_ERROR;
387 ICUParagraphLayoutFactory::break_iterator.reset(icu::BreakIterator::createLineInstance(locale, status));
388 assert(U_SUCCESS(status));
389}
390
395/* static */ std::unique_ptr<icu::BreakIterator> ICUParagraphLayoutFactory::GetBreakIterator()
396{
397 assert(ICUParagraphLayoutFactory::break_iterator != nullptr);
398
399 return std::unique_ptr<icu::BreakIterator>(ICUParagraphLayoutFactory::break_iterator->clone());
400}
401
402std::unique_ptr<const ICUParagraphLayout::Line> ICUParagraphLayout::NextLine(int max_width)
403{
404 std::vector<ICURun>::iterator start_run = this->current_run;
405 std::vector<ICURun>::iterator last_run = this->current_run;
406
407 if (start_run == this->runs.end()) return nullptr;
408
409 int cur_width = 0;
410
411 /* Add remaining width of the first run if it is a broken run. */
412 if (this->partial_offset > 0) {
413 if ((start_run->level & 1) == 0) {
414 for (size_t i = this->partial_offset; i < start_run->advance.size(); i++) {
415 cur_width += start_run->advance[i];
416 }
417 } else {
418 for (int i = 0; i < this->partial_offset; i++) {
419 cur_width += start_run->advance[i];
420 }
421 }
422 last_run++;
423 }
424
425 /* Gather runs until the line is full. */
426 while (last_run != this->runs.end() && cur_width < max_width) {
427 cur_width += last_run->total_advance;
428 last_run++;
429 }
430
431 /* If the text does not fit into the available width, find a suitable breaking point. */
432 int new_partial_length = 0;
433 if (cur_width > max_width) {
434 /* Create a break-iterator to find a good place to break lines. */
435 auto break_iterator = ICUParagraphLayoutFactory::GetBreakIterator();
436 icu::UnicodeString text(this->buff, this->buff_length);
437 break_iterator->setText(text);
438
439 auto overflow_run = last_run - 1;
440
441 /* Find the last glyph that fits. */
442 size_t index;
443 if ((overflow_run->level & 1) == 0) {
444 /* LTR */
445 for (index = overflow_run->glyphs.size(); index > 0; /* nothing */) {
446 --index;
447 cur_width -= overflow_run->advance[index];
448 if (cur_width <= max_width) break;
449 }
450 } else {
451 /* RTL */
452 for (index = 0; index < overflow_run->glyphs.size(); index++) {
453 cur_width -= overflow_run->advance[index];
454 if (cur_width <= max_width) break;
455 }
456 }
457
458 /* Find the character that matches; this is the start of the cluster. */
459 auto char_pos = overflow_run->glyph_to_char[index];
460
461 /* See if there is a good breakpoint inside this run. */
462 int32_t break_pos = break_iterator->preceding(char_pos + 1);
463 auto overflow_run_start = overflow_run->start;
464 if (overflow_run == start_run) overflow_run_start += this->partial_offset;
465 if (break_pos != icu::BreakIterator::DONE && break_pos > overflow_run_start) {
466 /* There is a line-break inside this run that is suitable. */
467 new_partial_length = break_pos - overflow_run_start;
468 } else if (overflow_run != start_run) {
469 /* There is no suitable line-break in this run, but it is also not
470 * the only run on this line. So we remove the run. */
471 last_run--;
472 } else {
473 /* There is no suitable line-break and this is the only run on the
474 * line. So we break at the cluster. This is not pretty, but the
475 * best we can do. */
476 new_partial_length = char_pos - overflow_run_start;
477 }
478 }
479
480 /* Reorder the runs on this line for display. */
481 std::vector<UBiDiLevel> bidi_level;
482 for (auto run = start_run; run != last_run; run++) {
483 bidi_level.push_back(run->level);
484 }
485 std::vector<int32_t> vis_to_log(bidi_level.size());
486 ubidi_reorderVisual(bidi_level.data(), bidi_level.size(), vis_to_log.data());
487
488 /* Create line. */
489 std::unique_ptr<ICULine> line = std::make_unique<ICULine>();
490
491 int cur_pos = 0;
492 for (auto &i : vis_to_log) {
493 auto i_run = start_run + i;
494 /* Copy the ICURun here, so we can modify it in case of a partial. */
495 ICURun run = *i_run;
496
497 if (i_run == last_run - 1 && new_partial_length > 0) {
498 if (i_run == start_run && this->partial_offset > 0) {
499 assert(run.length > this->partial_offset);
500 run.start += this->partial_offset;
501 run.length -= this->partial_offset;
502 }
503
504 assert(run.length > new_partial_length);
505 run.length = new_partial_length;
506
507 run.Shape(this->buff, this->buff_length);
508 } else if (i_run == start_run && this->partial_offset > 0) {
509 assert(run.length > this->partial_offset);
510
511 run.start += this->partial_offset;
512 run.length -= this->partial_offset;
513
514 run.Shape(this->buff, this->buff_length);
515 }
516
517 auto total_advance = run.total_advance;
518 line->emplace_back(std::move(run), cur_pos);
519 cur_pos += total_advance;
520 }
521
522 if (new_partial_length > 0) {
523 this->current_run = last_run - 1;
524 if (this->current_run != start_run) this->partial_offset = 0;
525 this->partial_offset += new_partial_length;
526 } else {
527 this->current_run = last_run;
528 this->partial_offset = 0;
529 }
530
531 return line;
532}
533
534/* static */ size_t ICUParagraphLayoutFactory::AppendToBuffer(UChar *buff, const UChar *buffer_last, char32_t c)
535{
536 assert(buff < buffer_last);
537 /* Transform from UTF-32 to internal ICU format of UTF-16. */
538 int32_t length = 0;
539 UErrorCode err = U_ZERO_ERROR;
540 u_strFromUTF32(buff, buffer_last - buff, &length, (UChar32*)&c, 1, &err);
541 return length;
542}
Helper for std::unique_ptr to use an arbitrary function as the deleter.
std::unique_ptr< T, DeleterFromFunc< Tfunc > > AutoRelease
Specialisation of std::unique_ptr for objects which must be deleted by calling a function.
int GetHeight() const
Get the height of the font.
Definition fontcache.h:65
Container with information about a font.
Definition gfx_layout.h:118
FontCache * fc
The font we are using.
Definition gfx_layout.h:120
static std::unique_ptr< icu::BreakIterator > GetBreakIterator()
Get a thread-safe line break iterator.
static void InitializeLayouter()
Initialize data needed for the ICU layouter.
A single line worth of VisualRuns.
int GetInternalCharLength(char32_t c) const override
Get the number of elements the given character occupies in the underlying text buffer of the Layouter...
const VisualRun & GetVisualRun(size_t run) const override
Get a reference to the given run.
int GetLeading() const override
Get the height of the line.
int GetWidth() const override
Get the width of this line.
size_t CountRuns() const override
Get the number of runs in this line.
std::span< const Position > GetPositions() const override
Get the positions for each of the glyphs.
ICUVisualRun(const ICURun &run, int x)
Constructor for a new ICUVisualRun.
int GetLeading() const override
Get the font leading, or distance between the baselines of consecutive lines.
const Font * GetFont() const override
Get the font.
std::span< const GlyphID > GetGlyphs() const override
Get the glyphs to draw.
size_t GetGlyphCount() const override
Get the number of glyphs.
std::span< const int > GetGlyphToCharMap() const override
The offset for each of the glyphs to the character run that was passed to the Layouter.
Wrapper for doing layouts with ICU.
void Reflow() override
Reset the position to the start of the paragraph.
std::unique_ptr< const Line > NextLine(int max_width) override
Construct a new line with a maximum width.
Helper class to store the information of all the runs of a paragraph in.
UScriptCode script
Script of the run.
std::vector< int > glyph_to_char
The mapping from glyphs to characters. Valid after Shape() is called.
void Shape(UChar *buff, size_t length)
Shape a single run.
std::vector< GlyphID > glyphs
The glyphs of the run. Valid after Shape() is called.
int total_advance
The total advance of the run. Valid after Shape() is called.
Font * font
Font of the run.
std::vector< int > advance
The advance (width) of the glyphs. Valid after Shape() is called.
int length
Length of the run in the buffer.
int start
Start of the run in the buffer.
std::vector< ParagraphLayouter::Position > positions
The positions of the glyphs. Valid after Shape() is called.
UBiDiLevel level
Embedding level of the run.
A single line worth of VisualRuns.
Definition gfx_layout.h:197
Visual run contains data about the bit of text with the same font.
Definition gfx_layout.h:154
Interface to glue fallback and normal layouter into one.
Definition gfx_layout.h:132
Control codes that are embedded in the translation strings.
Functions related to debugging.
#define Debug(facility, severity, format_string,...)
Output a line of debugging information.
Definition debug.h:37
@ Fontcache
Fontcache message facility.
Definition debug_type.h:37
@ Critical
Critical, user should know about this.
Definition debug_type.h:15
std::vector< std::pair< int, Font * > > FontMap
Mapping from index to font.
Definition gfx_layout.h:127
std::vector< ICURun > ItemizeStyle(std::vector< ICURun > &runs_current, FontMap &font_mapping)
Itemize the string into runs per style, based on the previous created runs.
constexpr float FONT_SCALE
HarfBuzz FreeType integration sets the font scaling, which is always in 1/64th of a pixel.
std::vector< ICURun > ItemizeBidi(UChar *buff, size_t length)
Itemize the string into runs per embedding level.
std::vector< ICURun > ItemizeScript(UChar *buff, size_t length, std::vector< ICURun > &runs_current)
Itemize the string into runs per script, based on the previous created runs.
Functions related to laying out the texts with ICU.
Information about languages and their files.
const LanguageMetadata * _current_language
The currently loaded language.
Definition strings.cpp:54
A number of safeguards to prevent using unsafe methods.
Definition of base types and functions in a cross-platform compatible way.
TextDirection _current_text_dir
Text direction of the currently selected language.
Definition strings.cpp:56
Functions related to OTTD's strings.
@ TD_RTL
Text is written right-to-left by default.
Functions related to zooming.
int ScaleSpriteTrad(int value)
Scale traditional pixel dimensions to GUI zoom level, for drawing sprites.
Definition zoom_func.h:107