diff options
Diffstat (limited to 'src/os/windows/string_uniscribe.cpp')
-rw-r--r-- | src/os/windows/string_uniscribe.cpp | 508 |
1 files changed, 508 insertions, 0 deletions
diff --git a/src/os/windows/string_uniscribe.cpp b/src/os/windows/string_uniscribe.cpp new file mode 100644 index 000000000..fbf908e09 --- /dev/null +++ b/src/os/windows/string_uniscribe.cpp @@ -0,0 +1,508 @@ +/* $Id$ */ + +/* + * This file is part of OpenTTD. + * 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. + * 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. + * 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 <http://www.gnu.org/licenses/>. + */ + +/** @file string_uniscribe.cpp Functions related to laying out text on Win32. */ + +#if defined(WITH_UNISCRIBE) + +#include "../../stdafx.h" +#include "../../debug.h" +#include "string_uniscribe.h" +#include "../../language.h" +#include "../../strings_func.h" +#include "../../table/control_codes.h" +#include "win32.h" +#include <vector> + +#include <windows.h> +#include <usp10.h> + +#include "../../safeguards.h" + +#ifdef _MSC_VER +# pragma comment(lib, "usp10") +#endif + + +/** Uniscribe cache for internal font information, cleared when OTTD changes fonts. */ +static SCRIPT_CACHE _script_cache[FS_END]; + +/** + * Contains all information about a run of characters. A run are consecutive + * characters that share a single font and language. + */ +struct UniscribeRun { + int pos; + int len; + Font *font; + + std::vector<GlyphID> ft_glyphs; + + SCRIPT_ANALYSIS sa; + std::vector<WORD> char_to_glyph; + + std::vector<SCRIPT_VISATTR> vis_attribs; + std::vector<WORD> glyphs; + std::vector<int> advances; + std::vector<GOFFSET> offsets; + int total_advance; + + UniscribeRun(int pos, int len, Font *font, SCRIPT_ANALYSIS &sa) : pos(pos), len(len), font(font), sa(sa) {} +}; + +/** Break a string into language formatting ranges. */ +static std::vector<SCRIPT_ITEM> UniscribeItemizeString(UniscribeParagraphLayoutFactory::CharType *buff, int32 length); +/** Generate and place glyphs for a run of characters. */ +static bool UniscribeShapeRun(const UniscribeParagraphLayoutFactory::CharType *buff, UniscribeRun &range); + +/** + * Wrapper for doing layouts with Uniscribe. + */ +class UniscribeParagraphLayout : public ParagraphLayouter { +private: + const UniscribeParagraphLayoutFactory::CharType *text_buffer; + + std::vector<UniscribeRun> ranges; ///< All runs of the text. + std::vector<UniscribeRun>::iterator cur_range; ///< The next run to be output. + int cur_range_offset = 0; ///< Offset from the start of the current run from where to output. + +public: + /** Visual run contains data about the bit of text with the same font. */ + class UniscribeVisualRun : public ParagraphLayouter::VisualRun { + private: + std::vector<GlyphID> glyphs; + std::vector<float> positions; + std::vector<WORD> char_to_glyph; + + int start_pos; + int total_advance; + int num_glyphs; + Font *font; + + mutable int *glyph_to_char = NULL; + + public: + UniscribeVisualRun(const UniscribeRun &range, int x); + virtual ~UniscribeVisualRun() + { + free(this->glyph_to_char); + } + + virtual const GlyphID *GetGlyphs() const { return &this->glyphs[0]; } + virtual const float *GetPositions() const { return &this->positions[0]; } + virtual const int *GetGlyphToCharMap() const; + + virtual const Font *GetFont() const { return this->font; } + virtual int GetLeading() const { return this->font->fc->GetHeight(); } + virtual int GetGlyphCount() const { return this->num_glyphs; } + int GetAdvance() const { return this->total_advance; } + }; + + /** A single line worth of VisualRuns. */ + class UniscribeLine : public AutoDeleteSmallVector<UniscribeVisualRun *, 4>, public ParagraphLayouter::Line { + public: + virtual int GetLeading() const; + virtual int GetWidth() const; + virtual int CountRuns() const { return this->Length(); } + virtual const VisualRun *GetVisualRun(int run) const { return *this->Get(run); } + + int GetInternalCharLength(WChar c) const + { + /* Uniscribe uses UTF-16 internally which means we need to account for surrogate pairs. */ + return c >= 0x010000U ? 2 : 1; + } + }; + + UniscribeParagraphLayout(std::vector<UniscribeRun> &ranges, const UniscribeParagraphLayoutFactory::CharType *buffer) : text_buffer(buffer), ranges(ranges) + { + this->Reflow(); + } + + virtual ~UniscribeParagraphLayout() {} + + virtual void Reflow() + { + this->cur_range = this->ranges.begin(); + this->cur_range_offset = 0; + } + + virtual const Line *NextLine(int max_width); +}; + +void UniscribeResetScriptCache(FontSize size) +{ + if (_script_cache[size] != NULL) { + ScriptFreeCache(&_script_cache[size]); + _script_cache[size] = NULL; + } +} + +/** Load the matching native Windows font. */ +static HFONT HFontFromFont(Font *font) +{ + LOGFONT logfont; + ZeroMemory(&logfont, sizeof(LOGFONT)); + logfont.lfHeight = font->fc->GetHeight(); + logfont.lfWeight = FW_NORMAL; + logfont.lfCharSet = DEFAULT_CHARSET; + convert_to_fs(font->fc->GetFontName(), logfont.lfFaceName, lengthof(logfont.lfFaceName)); + + return CreateFontIndirect(&logfont); +} + +/** Determine the glyph positions for a run. */ +static bool UniscribeShapeRun(const UniscribeParagraphLayoutFactory::CharType *buff, UniscribeRun &range) +{ + /* Initial size guess for the number of glyphs recommended by Uniscribe. */ + range.glyphs.resize(range.len * 3 / 2 + 16); + range.vis_attribs.resize(range.glyphs.size()); + + /* The char-to-glyph array is the same size as the input. */ + range.char_to_glyph.resize(range.len); + + HDC temp_dc = NULL; + HFONT old_font = NULL; + HFONT cur_font = NULL; + + while (true) { + /* Shape the text run by determining the glyphs needed for display. */ + int glyphs_used = 0; + HRESULT hr = ScriptShape(temp_dc, &_script_cache[range.font->fc->GetSize()], buff + range.pos, range.len, (int)range.glyphs.size(), &range.sa, &range.glyphs[0], &range.char_to_glyph[0], &range.vis_attribs[0], &glyphs_used); + + if (SUCCEEDED(hr)) { + range.glyphs.resize(glyphs_used); + range.vis_attribs.resize(glyphs_used); + + /* Calculate the glyph positions. */ + ABC abc; + range.advances.resize(range.glyphs.size()); + range.offsets.resize(range.glyphs.size()); + hr = ScriptPlace(temp_dc, &_script_cache[range.font->fc->GetSize()], &range.glyphs[0], (int)range.glyphs.size(), &range.vis_attribs[0], &range.sa, &range.advances[0], &range.offsets[0], &abc); + if (SUCCEEDED(hr)) { + /* We map our special sprite chars to values that don't fit into a WORD. Copy the glyphs + * into a new vector and query the real glyph to use for these special chars. */ + range.ft_glyphs.resize(range.glyphs.size()); + for (size_t g_id = 0; g_id < range.glyphs.size(); g_id++) { + range.ft_glyphs[g_id] = range.glyphs[g_id]; + } + for (int i = 0; i < range.len; i++) { + if (buff[range.pos + i] >= SCC_SPRITE_START && buff[range.pos + i] <= SCC_SPRITE_END) { + range.ft_glyphs[range.char_to_glyph[i]] = range.font->fc->MapCharToGlyph(buff[range.pos + i]); + } + } + + /* FreeType and GDI/Uniscribe seems to occasionally disagree over the width of a glyph. */ + range.total_advance = 0; + for (size_t i = 0; i < range.advances.size(); i++) { + if (range.advances[i] > 0 && range.ft_glyphs[i] != 0xFFFF) range.advances[i] = range.font->fc->GetGlyphWidth(range.ft_glyphs[i]); + range.total_advance += range.advances[i]; + } + break; + } + } + + if (hr == E_OUTOFMEMORY) { + /* The glyph buffer needs to be larger. Just double it every time. */ + range.glyphs.resize(range.glyphs.size() * 2); + range.vis_attribs.resize(range.vis_attribs.size() * 2); + } else if (hr == E_PENDING) { + /* Glyph data is not in cache, load native font. */ + cur_font = HFontFromFont(range.font); + if (cur_font == NULL) return false; // Sorry, no dice. + + temp_dc = CreateCompatibleDC(NULL); + SetMapMode(temp_dc, MM_TEXT); + old_font = (HFONT)SelectObject(temp_dc, cur_font); + } else if (hr == USP_E_SCRIPT_NOT_IN_FONT && range.sa.eScript != SCRIPT_UNDEFINED) { + /* Try again with the generic shaping engine. */ + range.sa.eScript = SCRIPT_UNDEFINED; + } else { + /* Some unknown other error. */ + if (temp_dc != NULL) { + SelectObject(temp_dc, old_font); + DeleteObject(cur_font); + ReleaseDC(NULL, temp_dc); + } + return false; + } + } + + if (temp_dc != NULL) { + SelectObject(temp_dc, old_font); + DeleteObject(cur_font); + ReleaseDC(NULL, temp_dc); + } + + return true; +} + +static std::vector<SCRIPT_ITEM> UniscribeItemizeString(UniscribeParagraphLayoutFactory::CharType *buff, int32 length) +{ + /* Itemize text. */ + SCRIPT_CONTROL control; + ZeroMemory(&control, sizeof(SCRIPT_CONTROL)); + control.uDefaultLanguage = _current_language->winlangid; + + SCRIPT_STATE state; + ZeroMemory(&state, sizeof(SCRIPT_STATE)); + state.uBidiLevel = _current_text_dir == TD_RTL ? 1 : 0; + + std::vector<SCRIPT_ITEM> items(16); + while (true) { + /* We subtract one from max_items to work around a buffer overflow on some older versions of Windows. */ + int generated = 0; + HRESULT hr = ScriptItemize(buff, length, (int)items.size() - 1, &control, &state, &items[0], &generated); + + if (SUCCEEDED(hr)) { + /* Resize the item buffer. Note that Uniscribe will always add an additional end sentinel item. */ + items.resize(generated + 1); + break; + } + /* Some kind of error except item buffer too small. */ + if (hr != E_OUTOFMEMORY) return std::vector<SCRIPT_ITEM>(); + + items.resize(items.size() * 2); + } + + return items; +} + +/* static */ ParagraphLayouter *UniscribeParagraphLayoutFactory::GetParagraphLayout(CharType *buff, CharType *buff_end, FontMap &fontMapping) +{ + int32 length = buff_end - buff; + /* Can't layout an empty string. */ + if (length == 0) return NULL; + + /* Can't layout our in-built sprite fonts. */ + for (FontMap::const_iterator i = fontMapping.Begin(); i != fontMapping.End(); i++) { + if (i->second->fc->IsBuiltInFont()) return NULL; + } + + /* Itemize text. */ + std::vector<SCRIPT_ITEM> items = UniscribeItemizeString(buff, length); + if (items.size() == 0) return NULL; + + /* Build ranges from the items and the font map. A range is a run of text + * that is part of a single item and formatted using a single font style. */ + std::vector<UniscribeRun> ranges; + + int cur_pos = 0; + std::vector<SCRIPT_ITEM>::iterator cur_item = items.begin(); + for (FontMap::const_iterator i = fontMapping.Begin(); i != fontMapping.End(); i++) { + while (cur_pos < i->first && cur_item != items.end() - 1) { + /* Add a range that spans the intersection of the remaining item and font run. */ + int stop_pos = min(i->first, (cur_item + 1)->iCharPos); + assert(stop_pos - cur_pos > 0); + ranges.push_back(UniscribeRun(cur_pos, stop_pos - cur_pos, i->second, cur_item->a)); + + /* Shape the range. */ + if (!UniscribeShapeRun(buff, ranges.back())) { + return NULL; + } + + /* If we are at the end of the current item, advance to the next item. */ + if (stop_pos == (cur_item + 1)->iCharPos) cur_item++; + cur_pos = stop_pos; + } + } + + return new UniscribeParagraphLayout(ranges, buff); +} + +/* virtual */ const ParagraphLayouter::Line *UniscribeParagraphLayout::NextLine(int max_width) +{ + std::vector<UniscribeRun>::iterator start_run = this->cur_range; + std::vector<UniscribeRun>::iterator last_run = this->cur_range; + + if (start_run == this->ranges.end()) return NULL; + + /* Add remaining width of the first run if it is a broken run. */ + int cur_width = 0; + if (this->cur_range_offset != 0) { + std::vector<int> dx(start_run->len); + ScriptGetLogicalWidths(&start_run->sa, start_run->len, (int)start_run->glyphs.size(), &start_run->advances[0], &start_run->char_to_glyph[0], &start_run->vis_attribs[0], &dx[0]); + + for (std::vector<int>::const_iterator c = dx.begin() + this->cur_range_offset; c != dx.end(); c++) { + cur_width += *c; + } + ++last_run; + } + + /* Gather runs until the line is full. */ + while (last_run != this->ranges.end() && cur_width < max_width) { + cur_width += last_run->total_advance; + ++last_run; + } + + /* If the text does not fit into the available width, find a suitable breaking point. */ + int remaing_offset = (last_run - 1)->len; + if (cur_width > max_width) { + std::vector<SCRIPT_LOGATTR> log_attribs; + + /* Get word break information. */ + int width_avail = max_width; + int num_chars = this->cur_range_offset; + int start_offs = this->cur_range_offset; + int last_cluster = this->cur_range_offset + 1; + for (std::vector<UniscribeRun>::iterator r = start_run; r != last_run; r++) { + log_attribs.resize(r->pos - start_run->pos + r->len); + if (FAILED(ScriptBreak(this->text_buffer + r->pos + start_offs, r->len - start_offs, &r->sa, &log_attribs[r->pos - start_run->pos + start_offs]))) return NULL; + + std::vector<int> dx(r->len); + ScriptGetLogicalWidths(&r->sa, r->len, (int)r->glyphs.size(), &r->advances[0], &r->char_to_glyph[0], &r->vis_attribs[0], &dx[0]); + + /* Count absolute max character count on the line. */ + for (int c = start_offs; c < r->len && width_avail > 0; c++, num_chars++) { + if (c > start_offs && log_attribs[num_chars].fCharStop) last_cluster = num_chars; + width_avail -= dx[c]; + } + + start_offs = 0; + } + + /* Walk backwards to find the last suitable breaking point. */ + while (--num_chars > this->cur_range_offset && !log_attribs[num_chars].fSoftBreak && !log_attribs[num_chars].fWhiteSpace) {} + + if (num_chars == this->cur_range_offset) { + /* Didn't find any suitable word break point, just break on the last cluster boundary. */ + num_chars = last_cluster; + } + + /* Include whitespace characters after the breaking point. */ + while (num_chars < (int)log_attribs.size() && log_attribs[num_chars].fWhiteSpace) { + num_chars++; + } + + /* Get last run that corresponds to the number of characters to show. */ + for (std::vector<UniscribeRun>::iterator run = start_run; run != last_run; run++) { + num_chars -= run->len; + + if (num_chars <= 0) { + remaing_offset = num_chars + run->len + 1; + last_run = run + 1; + assert(remaing_offset - 1 > 0); + break; + } + } + } + + /* Build display order from the runs. */ + std::vector<BYTE> bidi_level; + for (std::vector<UniscribeRun>::iterator r = start_run; r != last_run; r++) { + bidi_level.push_back(r->sa.s.uBidiLevel); + } + std::vector<INT> vis_to_log(bidi_level.size()); + if (FAILED(ScriptLayout((int)bidi_level.size(), &bidi_level[0], &vis_to_log[0], NULL))) return NULL; + + /* Create line. */ + UniscribeLine *line = new UniscribeLine(); + + int cur_pos = 0; + for (std::vector<INT>::iterator l = vis_to_log.begin(); l != vis_to_log.end(); l++) { + std::vector<UniscribeRun>::iterator i_run = start_run + *l; + UniscribeRun run = *i_run; + + /* Partial run after line break (either start or end)? Reshape run to get the first/last glyphs right. */ + if (i_run == last_run - 1 && remaing_offset < (last_run - 1)->len) { + run.len = remaing_offset - 1; + + if (!UniscribeShapeRun(this->text_buffer, run)) return NULL; + } + if (i_run == start_run && this->cur_range_offset > 0) { + assert(run.len - this->cur_range_offset > 0); + run.pos += this->cur_range_offset; + run.len -= this->cur_range_offset; + + if (!UniscribeShapeRun(this->text_buffer, run)) return NULL; + } + + *line->Append() = new UniscribeVisualRun(run, cur_pos); + cur_pos += run.total_advance; + } + + if (remaing_offset < (last_run - 1)->len) { + /* We didn't use up all of the last run, store remainder for the next line. */ + this->cur_range_offset = remaing_offset - 1; + this->cur_range = last_run - 1; + assert(this->cur_range->len > this->cur_range_offset); + } else { + this->cur_range_offset = 0; + this->cur_range = last_run; + } + + return line; +} + +/** + * Get the height of the line. + * @return The maximum height of the line. + */ +int UniscribeParagraphLayout::UniscribeLine::GetLeading() const +{ + int leading = 0; + for (const UniscribeVisualRun * const *run = this->Begin(); run != this->End(); run++) { + leading = max(leading, (*run)->GetLeading()); + } + + return leading; +} + +/** + * Get the width of this line. + * @return The width of the line. + */ +int UniscribeParagraphLayout::UniscribeLine::GetWidth() const +{ + int length = 0; + for (const UniscribeVisualRun * const *run = this->Begin(); run != this->End(); run++) { + length += (*run)->GetAdvance(); + } + + return length; +} + +UniscribeParagraphLayout::UniscribeVisualRun::UniscribeVisualRun(const UniscribeRun &range, int x) : glyphs(range.ft_glyphs), char_to_glyph(range.char_to_glyph), start_pos(range.pos), total_advance(range.total_advance), font(range.font) +{ + this->num_glyphs = (int)glyphs.size(); + this->positions.resize(this->num_glyphs * 2 + 2); + + int advance = 0; + for (int i = 0; i < this->num_glyphs; i++) { + this->positions[i * 2 + 0] = range.offsets[i].du + advance + x; + this->positions[i * 2 + 1] = range.offsets[i].dv; + + advance += range.advances[i]; + } + this->positions[this->num_glyphs * 2] = advance + x; +} + +const int *UniscribeParagraphLayout::UniscribeVisualRun::GetGlyphToCharMap() const +{ + if (this->glyph_to_char == NULL) { + this->glyph_to_char = CallocT<int>(this->GetGlyphCount()); + + /* The char to glyph array contains the first glyph index of the cluster that is associated + * with each character. It is possible for a cluster to be formed of several chars. */ + for (int c = 0; c < (int)this->char_to_glyph.size(); c++) { + /* If multiple chars map to one glyph, only refer back to the first character. */ + if (this->glyph_to_char[this->char_to_glyph[c]] == 0) this->glyph_to_char[this->char_to_glyph[c]] = c + this->start_pos; + } + + /* We only marked the first glyph of each cluster in the loop above. Fill the gaps. */ + int last_char = this->glyph_to_char[0]; + for (int g = 0; g < this->GetGlyphCount(); g++) { + if (this->glyph_to_char[g] != 0) last_char = this->glyph_to_char[g]; + this->glyph_to_char[g] = last_char; + } + } + + return this->glyph_to_char; +} + +#endif /* defined(WITH_UNISCRIBE) */ |