summaryrefslogtreecommitdiff
path: root/src/mbgl/text/glyph_set.cpp
blob: 537d9c0579908efecc90dd42370d1e3908b6d310 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
#include <mbgl/math/minmax.hpp>
#include <mbgl/text/glyph_set.hpp>
#include <mbgl/util/i18n.hpp>
#include <mbgl/util/logging.hpp>

#include <boost/algorithm/string.hpp>

#include <algorithm>
#include <cassert>

namespace mbgl {

void GlyphSet::insert(uint32_t id, SDFGlyph&& glyph) {
    auto it = sdfs.find(id);
    if (it == sdfs.end()) {
        // Glyph doesn't exist yet.
        sdfs.emplace(id, std::move(glyph));
    } else if (it->second.metrics == glyph.metrics) {
        if (it->second.bitmap != glyph.bitmap) {
            // The actual bitmap was updated; this is unsupported.
            Log::Warning(Event::Glyph, "Modified glyph changed bitmap represenation");
        }
        // At least try to update it in case it's currently unsused.
        // If it is already used; we won't attempt to update the glyph atlas texture.
        it->second.bitmap = std::move(glyph.bitmap);
    } else {
        // The metrics were updated; this is unsupported.
        Log::Warning(Event::Glyph, "Modified glyph has different metrics");
        return;
    }
}

const std::map<uint32_t, SDFGlyph>& GlyphSet::getSDFs() const {
    return sdfs;
}

const Shaping GlyphSet::getShaping(const std::u16string& logicalInput,
                                   const float maxWidth,
                                   const float lineHeight,
                                   const float horizontalAlign,
                                   const float verticalAlign,
                                   const float justify,
                                   const float spacing,
                                   const Point<float>& translate,
                                   BiDi& bidi) const {

    // The string stored in shaping.text is used for finding duplicates, but may end up quite
    // different from the glyphs that get shown
    Shaping shaping(translate.x * 24, translate.y * 24, logicalInput);

    std::vector<std::u16string> reorderedLines =
        bidi.processText(logicalInput,
                         determineLineBreaks(logicalInput, spacing, maxWidth));

    shapeLines(shaping, reorderedLines, spacing, lineHeight, horizontalAlign, verticalAlign,
               justify, translate);

    return shaping;
}

void align(Shaping& shaping,
           const float justify,
           const float horizontalAlign,
           const float verticalAlign,
           const float maxLineLength,
           const float lineHeight,
           const uint32_t lineCount,
           const Point<float>& translate) {
    const float shiftX =
        (justify - horizontalAlign) * maxLineLength + ::round(translate.x * 24 /* one em */);
    const float shiftY =
        (-verticalAlign * lineCount + 0.5) * lineHeight + ::round(translate.y * 24 /* one em */);

    for (auto& glyph : shaping.positionedGlyphs) {
        glyph.x += shiftX;
        glyph.y += shiftY;
    }
}

// justify left = 0, right = 1, center = .5
void justifyLine(std::vector<PositionedGlyph>& positionedGlyphs,
                 const std::map<uint32_t, SDFGlyph>& sdfs,
                 uint32_t start,
                 uint32_t end,
                 float justify) {
    if (!justify) {
        return;
    }

    PositionedGlyph& glyph = positionedGlyphs[end];
    auto it = sdfs.find(glyph.glyph);
    if (it != sdfs.end()) {
        const uint32_t lastAdvance = it->second.metrics.advance;
        const float lineIndent = float(glyph.x + lastAdvance) * justify;

        for (uint32_t j = start; j <= end; j++) {
            positionedGlyphs[j].x -= lineIndent;
        }
    }
}

float GlyphSet::determineIdeographicLineWidth(const std::u16string& logicalInput,
                                              const float spacing,
                                              float maxWidth) const {
    float totalWidth = 0;

    // totalWidth doesn't include the last character for magical tuning reasons. This makes the
    // algorithm a little more agressive about trying to fit the text into fewer lines, taking
    // advantage of the tolerance for going a little over maxWidth
    for (uint32_t i = 0; i < logicalInput.size() - 1; i++) {
        auto it = sdfs.find(logicalInput[i]);
        if (it != sdfs.end()) {
            totalWidth += it->second.metrics.advance + spacing;
        }
    }

    int32_t lineCount = std::fmax(1, std::ceil(totalWidth / maxWidth));
    return totalWidth / lineCount;
}

// We determine line breaks based on shaped text in logical order. Working in visual order would be
//  more intuitive, but we can't do that because the visual order may be changed by line breaks!
std::set<int32_t> GlyphSet::determineLineBreaks(const std::u16string& logicalInput,
                                                const float spacing,
                                                float maxWidth) const {
    if (!maxWidth) {
        return {};
    }

    if (logicalInput.empty()) {
        return {};
    }

    if (util::i18n::allowsIdeographicBreaking(logicalInput)) {
        maxWidth = determineIdeographicLineWidth(logicalInput, spacing, maxWidth);
    }

    std::set<int32_t> lineBreakPoints;
    float currentX = 0;
    uint32_t lastSafeBreak = 0;
    float lastSafeBreakX = 0;

    for (uint32_t i = 0; i < logicalInput.size(); i++) {
        auto it = sdfs.find(logicalInput[i]);
        if (it == sdfs.end()) {
            continue;
        }

        const SDFGlyph& glyph = it->second;

        // Ideographic characters, spaces, and word-breaking punctuation that often appear without
        // surrounding spaces.
        if (util::i18n::allowsWordBreaking(glyph.id) ||
            util::i18n::allowsIdeographicBreaking(glyph.id)) {
            lastSafeBreak = i;
            lastSafeBreakX = currentX;
        }

        if (currentX > maxWidth && lastSafeBreak > 0) {
            lineBreakPoints.insert(lastSafeBreak);
            currentX -= lastSafeBreakX;
            lastSafeBreakX = 0;
        }

        currentX += glyph.metrics.advance + spacing;
    }

    return lineBreakPoints;
}

void GlyphSet::shapeLines(Shaping& shaping,
                          const std::vector<std::u16string>& lines,
                          const float spacing,
                          const float lineHeight,
                          const float horizontalAlign,
                          const float verticalAlign,
                          const float justify,
                          const Point<float>& translate) const {

    // the y offset *should* be part of the font metadata
    const int32_t yOffset = -17;

    float x = 0;
    float y = yOffset;

    float maxLineLength = 0;

    for (std::u16string line : lines) {
        // Collapse whitespace so it doesn't throw off justification
        boost::algorithm::trim_if(line, boost::algorithm::is_any_of(u" \t\n\v\f\r"));

        if (line.empty()) {
            y += lineHeight; // Still need a line feed after empty line
            continue;
        }

        uint32_t lineStartIndex = static_cast<uint32_t>(shaping.positionedGlyphs.size());
        for (char16_t chr : line) {
            auto it = sdfs.find(chr);
            if (it == sdfs.end()) {
                continue;
            }

            const SDFGlyph& glyph = it->second;
            shaping.positionedGlyphs.emplace_back(chr, x, y);
            x += glyph.metrics.advance + spacing;
        }

        // Only justify if we placed at least one glyph
        if (static_cast<uint32_t>(shaping.positionedGlyphs.size()) != lineStartIndex) {
            float lineLength = x - spacing; // Don't count trailing spacing
            maxLineLength = util::max(lineLength, maxLineLength);
            
            justifyLine(shaping.positionedGlyphs, sdfs, lineStartIndex,
                        static_cast<uint32_t>(shaping.positionedGlyphs.size()) - 1, justify);
        }

        x = 0;
        y += lineHeight;
    }

    align(shaping, justify, horizontalAlign, verticalAlign, maxLineLength, lineHeight,
          static_cast<uint32_t>(lines.size()), translate);
    const uint32_t height = lines.size() * lineHeight;

    // Calculate the bounding box
    shaping.top += -verticalAlign * height;
    shaping.bottom = shaping.top + height;
    shaping.left += -horizontalAlign * maxLineLength;
    shaping.right = shaping.left + maxLineLength;
}

} // end namespace mbgl